修复 Codex rollout 历史合并并重新打包
This commit is contained in:
@@ -18,6 +18,20 @@ function createCodexRolloutStore(deps) {
|
||||
turn.content = turn.content ? `${turn.content}\n\n${text}` : text;
|
||||
}
|
||||
|
||||
function stableHash(value) {
|
||||
return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 24);
|
||||
}
|
||||
|
||||
function extractTurnKey(entry, payload, fallback) {
|
||||
const context = payload?.turn_context || entry?.turn_context || {};
|
||||
const value = entry?.turn_id
|
||||
|| payload?.turn_id
|
||||
|| context?.turn_id
|
||||
|| context?.id
|
||||
|| null;
|
||||
return value ? String(value) : fallback;
|
||||
}
|
||||
|
||||
function extractCcwebSourceConversation(text) {
|
||||
const match = String(text || '').match(/^来自「([^」]+)」对话(ID:\s*([0-9a-fA-F-]{36}))的消息:/);
|
||||
if (!match) return null;
|
||||
@@ -39,6 +53,8 @@ function createCodexRolloutStore(deps) {
|
||||
};
|
||||
const totalUsage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
|
||||
let currentAssistant = null;
|
||||
let currentTurnKey = null;
|
||||
let implicitTurnSequence = 0;
|
||||
let sawRealUserMessage = false;
|
||||
const fallbackUserMessages = [];
|
||||
|
||||
@@ -50,9 +66,21 @@ function createCodexRolloutStore(deps) {
|
||||
meta.sourceConversationTitle = sourceConversation.title;
|
||||
}
|
||||
|
||||
function ensureAssistant(ts) {
|
||||
function ensureAssistant(ts, turnKey = null) {
|
||||
// 没有 turn_id 的旧 rollout 仍需在每次新助手输出时生成新的隐式轮次键。
|
||||
// 否则不同轮次会共用同一个 nativeTurnKey,前端按稳定键去重时会误删后续回复。
|
||||
const resolvedTurnKey = turnKey || currentTurnKey || `${meta.threadId || 'native'}:implicit:${implicitTurnSequence += 1}`;
|
||||
if (currentAssistant && currentTurnKey !== resolvedTurnKey) flushAssistant();
|
||||
currentTurnKey = resolvedTurnKey;
|
||||
if (!currentAssistant) {
|
||||
currentAssistant = { role: 'assistant', content: '', toolCalls: [], timestamp: ts || null };
|
||||
currentAssistant = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
toolCalls: [],
|
||||
timestamp: ts || null,
|
||||
nativeTurnKey: `native:${meta.threadId || 'unknown-thread'}:${currentTurnKey}`,
|
||||
id: `native:${stableHash(`${meta.threadId || ''}:${currentTurnKey}`)}`,
|
||||
};
|
||||
} else if (!currentAssistant.timestamp && ts) {
|
||||
currentAssistant.timestamp = ts;
|
||||
}
|
||||
@@ -65,6 +93,7 @@ function createCodexRolloutStore(deps) {
|
||||
messages.push(currentAssistant);
|
||||
}
|
||||
currentAssistant = null;
|
||||
currentTurnKey = null;
|
||||
pendingToolCalls.clear();
|
||||
}
|
||||
|
||||
@@ -84,6 +113,24 @@ function createCodexRolloutStore(deps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === 'event_msg' && ['task_started', 'turn_started'].includes(entry.payload?.type)) {
|
||||
flushAssistant();
|
||||
currentTurnKey = extractTurnKey(entry, entry.payload, `${meta.threadId || 'native'}:turn:${implicitTurnSequence += 1}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === 'turn_context') {
|
||||
const turnKey = extractTurnKey(entry, entry.payload, currentTurnKey || `${meta.threadId || 'native'}:turn:${implicitTurnSequence += 1}`);
|
||||
if (currentAssistant && currentTurnKey !== turnKey) flushAssistant();
|
||||
currentTurnKey = turnKey;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === 'event_msg' && ['task_complete', 'turn_completed', 'turn_failed', 'turn_aborted'].includes(entry.payload?.type)) {
|
||||
flushAssistant();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
|
||||
const total = entry.payload?.info?.total_token_usage || null;
|
||||
const usage = entry.payload?.info?.last_token_usage || null;
|
||||
@@ -104,9 +151,18 @@ function createCodexRolloutStore(deps) {
|
||||
if (text) {
|
||||
sawRealUserMessage = true;
|
||||
flushAssistant();
|
||||
const sourceConversation = extractCcwebSourceConversation(text);
|
||||
rememberSourceConversation(text);
|
||||
// cc-web 跨对话投递在 native rollout 中会留下内部 user 输入。
|
||||
// 目标会话已经保存了带来源的消息,这里不再伪造普通 user 气泡。
|
||||
if (sourceConversation) continue;
|
||||
if (!meta.title) meta.title = text.slice(0, 80).replace(/\n/g, ' ');
|
||||
messages.push({ role: 'user', content: text, timestamp: ts });
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: ts,
|
||||
id: `native-user:${stableHash(`${meta.threadId || ''}:${ts || ''}:${text}`)}`,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -119,22 +175,26 @@ function createCodexRolloutStore(deps) {
|
||||
if (payload.role === 'assistant') {
|
||||
const text = extractCodexMessageText(payload.content);
|
||||
if (text.trim()) {
|
||||
if (currentAssistant && ((currentAssistant.content || '').trim() || currentAssistant.toolCalls.length > 0)) {
|
||||
flushAssistant();
|
||||
}
|
||||
appendAssistantContent(ensureAssistant(ts), text);
|
||||
appendAssistantContent(ensureAssistant(ts, extractTurnKey(entry, payload, currentTurnKey)), text);
|
||||
}
|
||||
} else if (payload.role === 'user' && !sawRealUserMessage) {
|
||||
const text = extractCodexMessageText(payload.content);
|
||||
if (text.trim()) {
|
||||
rememberSourceConversation(text);
|
||||
fallbackUserMessages.push({ role: 'user', content: text, timestamp: ts });
|
||||
if (!extractCcwebSourceConversation(text)) {
|
||||
fallbackUserMessages.push({
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: ts,
|
||||
id: `native-user:${stableHash(`${meta.threadId || ''}:${ts || ''}:${text}`)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'function_call': {
|
||||
const assistant = ensureAssistant(ts);
|
||||
const assistant = ensureAssistant(ts, extractTurnKey(entry, payload, currentTurnKey));
|
||||
const toolUseId = payload.call_id || payload.id || crypto.randomUUID();
|
||||
const tc = {
|
||||
name: payload.name || 'FunctionCall',
|
||||
@@ -147,7 +207,7 @@ function createCodexRolloutStore(deps) {
|
||||
break;
|
||||
}
|
||||
case 'function_call_output': {
|
||||
const assistant = ensureAssistant(ts);
|
||||
const assistant = ensureAssistant(ts, extractTurnKey(entry, payload, currentTurnKey));
|
||||
const toolUseId = payload.call_id || crypto.randomUUID();
|
||||
let tc = pendingToolCalls.get(toolUseId);
|
||||
if (!tc) {
|
||||
@@ -161,6 +221,34 @@ function createCodexRolloutStore(deps) {
|
||||
: JSON.stringify(payload.output || '')).slice(0, 2000);
|
||||
break;
|
||||
}
|
||||
case 'custom_tool_call': {
|
||||
const assistant = ensureAssistant(ts, extractTurnKey(entry, payload, currentTurnKey));
|
||||
const toolUseId = payload.call_id || payload.id || crypto.randomUUID();
|
||||
const tc = {
|
||||
name: payload.name || 'CustomToolCall',
|
||||
id: toolUseId,
|
||||
input: sanitizeToolInput(payload.name || 'CustomToolCall', payload.input || payload.arguments || ''),
|
||||
done: payload.status === 'completed',
|
||||
};
|
||||
assistant.toolCalls.push(tc);
|
||||
pendingToolCalls.set(toolUseId, tc);
|
||||
break;
|
||||
}
|
||||
case 'custom_tool_call_output': {
|
||||
const assistant = ensureAssistant(ts, extractTurnKey(entry, payload, currentTurnKey));
|
||||
const toolUseId = payload.call_id || crypto.randomUUID();
|
||||
let tc = pendingToolCalls.get(toolUseId);
|
||||
if (!tc) {
|
||||
tc = { name: 'CustomToolCall', id: toolUseId, input: null, done: false };
|
||||
assistant.toolCalls.push(tc);
|
||||
pendingToolCalls.set(toolUseId, tc);
|
||||
}
|
||||
tc.done = true;
|
||||
tc.result = (typeof payload.output === 'string'
|
||||
? payload.output
|
||||
: JSON.stringify(payload.output || '')).slice(0, 2000);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user