feat: refine codex live chat context flows
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import type { ChatMessagePart } from './types';
|
||||
|
||||
const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\s*$/i;
|
||||
const PROMPT_LINE_PATTERN = /^\s*\[\[prompt:(.+?)\]\]\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;
|
||||
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
||||
const CHAT_DOT_CODEX_MARKER = '/.codex_chat/';
|
||||
const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/';
|
||||
type PromptPart = Extract<ChatMessagePart, { type: 'prompt' }>;
|
||||
type PromptOption = PromptPart['options'][number];
|
||||
type PromptPreview = NonNullable<PromptOption['preview']>;
|
||||
type PromptStep = NonNullable<PromptPart['steps']>[number];
|
||||
|
||||
function normalizeText(value: unknown) {
|
||||
return String(value ?? '').trim();
|
||||
@@ -21,6 +29,25 @@ function normalizeUrl(value: string) {
|
||||
return `/${malformedResourceMatch[1]}`;
|
||||
}
|
||||
|
||||
const apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
||||
if (apiMarkerIndex >= 0) {
|
||||
const apiPath = normalized.slice(apiMarkerIndex);
|
||||
const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER);
|
||||
return dotCodexIndex >= 0
|
||||
? `${CHAT_API_RESOURCE_MARKER}${apiPath.slice(dotCodexIndex + 1)}`
|
||||
: apiPath;
|
||||
}
|
||||
|
||||
const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER);
|
||||
if (publicDotCodexIndex >= 0) {
|
||||
return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(publicDotCodexIndex + 8)}`;
|
||||
}
|
||||
|
||||
const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER);
|
||||
if (dotCodexIndex >= 0) {
|
||||
return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(dotCodexIndex + 1)}`;
|
||||
}
|
||||
|
||||
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
@@ -28,6 +55,116 @@ function normalizeUrl(value: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizePromptPreview(value: unknown): PromptPreview | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const type =
|
||||
record.type === 'image' || record.type === 'markdown' || record.type === 'html' || record.type === 'resource'
|
||||
? record.type
|
||||
: null;
|
||||
const url = normalizeUrl(normalizeText(record.url));
|
||||
const content = String(record.content ?? '').trim() || null;
|
||||
const alt = normalizeText(record.alt) || null;
|
||||
const title = normalizeText(record.title) || null;
|
||||
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'image' || type === 'resource') {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
} else if (!content && !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
url: url || null,
|
||||
content,
|
||||
alt,
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
function isPromptOption(value: PromptOption | null): value is PromptOption {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
function normalizePromptOption(value: unknown): PromptOption | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const optionValue = normalizeText(record.value);
|
||||
const label = normalizeText(record.label);
|
||||
|
||||
if (!optionValue || !label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: optionValue,
|
||||
label,
|
||||
description: normalizeText(record.description) || null,
|
||||
preview: normalizePromptPreview(record.preview),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePromptSelectedValues(value: unknown) {
|
||||
return [
|
||||
...(Array.isArray(value) ? value : []),
|
||||
]
|
||||
.map((item) => normalizeText(item))
|
||||
.filter(Boolean)
|
||||
.filter((item, index, array) => array.indexOf(item) === index);
|
||||
}
|
||||
|
||||
function normalizePromptSteps(value: unknown): PromptStep[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.flatMap((item, index) => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>;
|
||||
const key = normalizeText(record.key) || `step-${index + 1}`;
|
||||
const title = normalizeText(record.title);
|
||||
const options = Array.isArray(record.options)
|
||||
? record.options.map((option) => normalizePromptOption(option)).filter(isPromptOption)
|
||||
: [];
|
||||
|
||||
if (!title || options.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key,
|
||||
title,
|
||||
description: normalizeText(record.description) || null,
|
||||
submitLabel: normalizeText(record.submitLabel) || null,
|
||||
mode: record.mode === 'direct' || record.mode === 'queue' ? record.mode : null,
|
||||
multiple: record.multiple === true,
|
||||
optional: record.optional === true,
|
||||
responseTemplate: normalizeText(record.responseTemplate) || null,
|
||||
freeTextLabel: normalizeText(record.freeTextLabel) || null,
|
||||
freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
|
||||
selectedValues: normalizePromptSelectedValues(record.selectedValues),
|
||||
options,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function decodeUrlComponentSafely(value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
@@ -135,6 +272,64 @@ function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPromptPart(rawBody: string): ChatMessagePart | null {
|
||||
let parsed: unknown;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(rawBody);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = parsed as Record<string, unknown>;
|
||||
const title = normalizeText(record.title);
|
||||
const options = Array.isArray(record.options)
|
||||
? record.options.map((item) => normalizePromptOption(item)).filter(isPromptOption)
|
||||
: [];
|
||||
const steps = normalizePromptSteps(record.steps);
|
||||
|
||||
if (!title || (options.length === 0 && steps.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mode = record.mode === 'direct' || record.mode === 'queue' ? record.mode : null;
|
||||
const selectedValues = [
|
||||
...normalizePromptSelectedValues(record.selectedValues),
|
||||
...(record.selectedValue != null ? [record.selectedValue] : []),
|
||||
]
|
||||
.map((item) => normalizeText(item))
|
||||
.filter(Boolean)
|
||||
.filter((value, index, values) => values.indexOf(value) === index);
|
||||
const resolvedBy =
|
||||
record.resolvedBy === 'user' || record.resolvedBy === 'timeout' || record.resolvedBy === 'system'
|
||||
? record.resolvedBy
|
||||
: null;
|
||||
|
||||
return {
|
||||
type: 'prompt',
|
||||
title,
|
||||
description: normalizeText(record.description) || null,
|
||||
submitLabel: normalizeText(record.submitLabel) || null,
|
||||
mode,
|
||||
multiple: record.multiple === true,
|
||||
responseTemplate: normalizeText(record.responseTemplate) || null,
|
||||
freeTextLabel: normalizeText(record.freeTextLabel) || null,
|
||||
freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
|
||||
currentStepKey: normalizeText(record.currentStepKey) || null,
|
||||
steps: steps.length > 0 ? steps : undefined,
|
||||
readOnly: record.readOnly === true || selectedValues.length > 0,
|
||||
selectedValues,
|
||||
resolvedBy,
|
||||
resolvedAt: normalizeText(record.resolvedAt) || null,
|
||||
resultText: normalizeText(record.resultText) || null,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractChatMessageParts(text: string) {
|
||||
const lines = String(text ?? '').split('\n');
|
||||
const keptLines: string[] = [];
|
||||
@@ -145,7 +340,38 @@ export function extractChatMessageParts(text: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dedupeKey = `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`;
|
||||
const dedupeKey =
|
||||
nextPart.type === 'link_card'
|
||||
? `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`
|
||||
: [
|
||||
nextPart.type,
|
||||
nextPart.title,
|
||||
nextPart.options
|
||||
.map((option) =>
|
||||
[
|
||||
option.value,
|
||||
option.label,
|
||||
option.preview?.type ?? '',
|
||||
option.preview?.url ?? '',
|
||||
option.preview?.content ?? '',
|
||||
option.preview?.title ?? '',
|
||||
].join('|'),
|
||||
)
|
||||
.join(','),
|
||||
(nextPart.steps ?? [])
|
||||
.map((step) =>
|
||||
[
|
||||
step.key,
|
||||
step.title,
|
||||
step.options.map((option) => `${option.value}:${option.label}`).join(','),
|
||||
].join('|'),
|
||||
)
|
||||
.join(','),
|
||||
nextPart.selectedValues?.join(',') ?? '',
|
||||
nextPart.resolvedBy ?? '',
|
||||
nextPart.resultText ?? '',
|
||||
nextPart.readOnly === true ? 'readonly' : '',
|
||||
].join(':');
|
||||
|
||||
if (seenLinkKeys.has(dedupeKey)) {
|
||||
return true;
|
||||
@@ -157,6 +383,15 @@ export function extractChatMessageParts(text: string) {
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const promptMatched = line.match(PROMPT_LINE_PATTERN);
|
||||
|
||||
if (promptMatched) {
|
||||
if (!pushPart(buildPromptPart(promptMatched[1] ?? ''))) {
|
||||
keptLines.push(line);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const matched = line.match(LINK_CARD_LINE_PATTERN);
|
||||
|
||||
if (!matched) {
|
||||
@@ -190,7 +425,7 @@ export function extractChatMessageParts(text: string) {
|
||||
}
|
||||
|
||||
const latestPart = parts.at(-1);
|
||||
if (latestPart && isInternalResourceUrl(latestPart.url)) {
|
||||
if (latestPart?.type === 'link_card' && isInternalResourceUrl(latestPart.url)) {
|
||||
parts.pop();
|
||||
seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
|
||||
keptLines.push(latestPart.url);
|
||||
|
||||
Reference in New Issue
Block a user