feat: expand live chat and work server tools
This commit is contained in:
216
etc/servers/work-server/src/services/chat-message-parts.ts
Normal file
216
etc/servers/work-server/src/services/chat-message-parts.ts
Normal 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 [];
|
||||
}
|
||||
Reference in New Issue
Block a user