|
|
@@ -720,12 +720,15 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
const input = text.trim();
|
|
|
if (!input) return;
|
|
|
|
|
|
+ // 同一会话不允许重复提交;不同会话可以并行跑(各自独立的事件流与回合)
|
|
|
+ if (this.activeThreadId && this.runningThreads.includes(this.activeThreadId)) return;
|
|
|
+ // 应用模型必须赶在建线程之前:它会重启 app-server,
|
|
|
+ // 线程要是建在旧进程里,这一轮就直接 thread not found
|
|
|
+ await this.ensureModelApplied();
|
|
|
+
|
|
|
const session = await this.ensureSession();
|
|
|
const threadId = session.threadId;
|
|
|
- // 同一会话不允许重复提交;不同会话可以并行跑(各自独立的事件流与回合)
|
|
|
if (this.runningThreads.includes(threadId)) return;
|
|
|
- // 先确保模型已应用,再落提问:应用失败(端点不通等)不该留下一条问不出去的消息
|
|
|
- await this.ensureModelApplied();
|
|
|
|
|
|
const now = new Date().toISOString();
|
|
|
const parsed: CodexUserMessage = {
|
|
|
@@ -761,17 +764,7 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
sandbox: session.sandbox ?? this.runDefaults.sandbox,
|
|
|
});
|
|
|
} catch (error: any) {
|
|
|
- const message = error?.message || 'Codex 执行失败';
|
|
|
- this.lastError = message;
|
|
|
- this.acceptEvent({
|
|
|
- id: `local-error:${threadId}:${Date.now()}`,
|
|
|
- threadId,
|
|
|
- turnId: `local-error:${Date.now()}`,
|
|
|
- kind: 'error',
|
|
|
- title: '运行错误',
|
|
|
- message,
|
|
|
- timestamp: new Date().toISOString(),
|
|
|
- });
|
|
|
+ this.noteTurnFailure(threadId, error?.message || 'Codex 执行失败');
|
|
|
} finally {
|
|
|
this.runningThreads = this.runningThreads.filter((id) => id !== threadId);
|
|
|
await this.ensureEvents(threadId);
|
|
|
@@ -783,6 +776,29 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 记一条本地失败事件。
|
|
|
+ *
|
|
|
+ * 回合卡住时(子进程被换掉、线程失效)Codex 再也不会补 turn/completed,
|
|
|
+ * 所以要把错误挂到那一轮自己的分组上,否则气泡一直停在「正在处理中」。
|
|
|
+ */
|
|
|
+ noteTurnFailure(threadId: string, message: string) {
|
|
|
+ this.lastError = message;
|
|
|
+ const stuck = [...buildCodexMessages(this.events[threadId] ?? [], [])]
|
|
|
+ .reverse()
|
|
|
+ .find((item) => item.role === 'assistant' && item.status === 'running');
|
|
|
+ const stamp = Date.now();
|
|
|
+ this.acceptEvent({
|
|
|
+ id: `local-error:${threadId}:${stamp}`,
|
|
|
+ threadId,
|
|
|
+ turnId: stuck?.role === 'assistant' ? stuck.turnId : `local-error:${stamp}`,
|
|
|
+ kind: 'error',
|
|
|
+ title: '运行错误',
|
|
|
+ message,
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
/** 中断当前回合 */
|
|
|
async stop() {
|
|
|
const threadId = this.activeThreadId;
|
|
|
@@ -793,7 +809,9 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
try {
|
|
|
await codexApi.codexTurnInterrupt(threadId, running.turnId);
|
|
|
} catch (error: any) {
|
|
|
- this.lastError = error?.message || '中断失败';
|
|
|
+ // 中断失败通常意味着那一轮早就不存在了(进程已重启):
|
|
|
+ // 本地状态照样收尾,不能让用户对着转圈的「正在处理中」无计可施
|
|
|
+ this.noteTurnFailure(threadId, error?.message || '中断失败');
|
|
|
throw error;
|
|
|
}
|
|
|
},
|