feat: expand live chat and work server tools

This commit is contained in:
2026-04-30 11:40:02 +09:00
parent 42ae640470
commit 2df0ba30cb
112 changed files with 15241 additions and 996 deletions

View File

@@ -0,0 +1,216 @@
export type ChatMessagePart =
| {
type: 'link_card';
title: string;
url: string;
actionLabel?: string | null;
};
const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\s*$/i;
const STANDALONE_MARKDOWN_LINK_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?\[([^\]]+)\]\(([^)]+)\)\s*$/;
const STANDALONE_URL_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?((?:https?:\/\/|\/)[^\s<>)\]]+)\s*$/i;
const RESOURCE_PATH_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
function normalizeText(value: unknown) {
return String(value ?? '').trim();
}
function normalizeUrl(value: string) {
const normalized = normalizeText(value);
if (!normalized) {
return '';
}
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
return normalized;
}
return '';
}
function hasKnownFileExtension(url: string) {
const pathname = url.split('?')[0] ?? '';
return /\.[a-z0-9]{1,8}$/i.test(pathname);
}
function isStructuredLinkCardCandidate(url: string) {
const normalized = normalizeUrl(url);
if (!normalized) {
return false;
}
if (RESOURCE_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
return false;
}
if (/^https?:\/\//i.test(normalized)) {
return !hasKnownFileExtension(normalized);
}
return !hasKnownFileExtension(normalized);
}
function buildFallbackLinkTitle(url: string) {
try {
const parsed = new URL(url, 'https://local.invalid');
const lastSegment = parsed.pathname.split('/').filter(Boolean).at(-1)?.trim();
return lastSegment || parsed.hostname || normalizeText(url);
} catch {
return normalizeText(url);
}
}
function normalizeStandaloneTitle(value: string) {
return value
.replace(/^\s*(?:[-*+]\s+|\d+\.\s+)?/, '')
.replace(/[`'"]+/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function resolveStandaloneLinkTitle(keptLines: string[], url: string) {
for (let index = keptLines.length - 1; index >= 0; index -= 1) {
const candidate = normalizeStandaloneTitle(keptLines[index] ?? '');
if (candidate) {
return candidate;
}
}
return buildFallbackLinkTitle(url);
}
function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
const segments = rawBody
.split('|')
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length < 2) {
return null;
}
const [rawTitle, rawUrl, rawActionLabel] = segments;
const title = normalizeText(rawTitle);
const url = normalizeUrl(rawUrl);
const actionLabel = normalizeText(rawActionLabel) || null;
if (!title || !url) {
return null;
}
return {
type: 'link_card',
title,
url,
actionLabel,
};
}
export function extractChatMessageParts(text: string) {
const lines = String(text ?? '').split('\n');
const keptLines: string[] = [];
const parts: ChatMessagePart[] = [];
const seenLinkKeys = new Set<string>();
const pushPart = (nextPart: ChatMessagePart | null) => {
if (!nextPart) {
return false;
}
const dedupeKey = `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`;
if (seenLinkKeys.has(dedupeKey)) {
return true;
}
seenLinkKeys.add(dedupeKey);
parts.push(nextPart);
return true;
};
for (const line of lines) {
const matched = line.match(LINK_CARD_LINE_PATTERN);
if (!matched) {
const markdownLinkMatch = line.match(STANDALONE_MARKDOWN_LINK_LINE_PATTERN);
if (markdownLinkMatch) {
const [, rawTitle, rawUrl] = markdownLinkMatch;
if (isStructuredLinkCardCandidate(rawUrl ?? '')) {
if (pushPart(buildLinkCardPart(`${rawTitle}|${rawUrl}`))) {
continue;
}
}
}
const standaloneUrlMatch = line.match(STANDALONE_URL_LINE_PATTERN);
if (standaloneUrlMatch) {
const rawUrl = standaloneUrlMatch[1] ?? '';
if (isStructuredLinkCardCandidate(rawUrl)) {
if (pushPart(buildLinkCardPart(`${resolveStandaloneLinkTitle(keptLines, rawUrl)}|${rawUrl}`))) {
continue;
}
}
}
keptLines.push(line);
continue;
}
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
keptLines.push(line);
}
}
return {
strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
parts,
};
}
export function stringifyChatMessageParts(parts: ChatMessagePart[] | null | undefined) {
return JSON.stringify(Array.isArray(parts) ? parts : []);
}
export function parseChatMessageParts(value: unknown): ChatMessagePart[] {
if (Array.isArray(value)) {
return value
.map((item) => {
if (!item || typeof item !== 'object') {
return null;
}
const record = item as Record<string, unknown>;
if (record.type !== 'link_card') {
return null;
}
const title = normalizeText(record.title);
const url = normalizeUrl(String(record.url ?? ''));
const actionLabel = normalizeText(record.actionLabel) || null;
if (!title || !url) {
return null;
}
return {
type: 'link_card' as const,
title,
url,
actionLabel,
};
})
.filter(Boolean) as ChatMessagePart[];
}
if (typeof value === 'string') {
try {
return parseChatMessageParts(JSON.parse(value));
} catch {
return [];
}
}
return [];
}