chat: 持久化继续消息,chat-history 交替历史
基线 main 811912c(PR #125 Muse 风格之后):服务端与单测与 9538825 相同;React 侧在 Muse 风格 ContinuePanel 上重放 chat-history 集成(新版输入框占位文案保持 Muse 风格的“消息”), console.html 由新版 src 重新构建。 - 新表 task_continue_messages:POST /continue 的用户消息与状态改回 queued 同一事务落盘,409 被拒不留 phantom 消息 - GET /tasks/{id}/chat-history:prompt + 继续 + 后续消息 + 链路按 时间交错拼好返回;部署前继续以 legacy_continue_count 如实标注 - React ContinuePanel 优先用 chat-history,旧服务端自动回退
This commit is contained in:
1 parent
811912c841
commit
f02c513729
9 files changed
+524
-40
No files matched your search
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api, getKey } from "../api/client";
|
||||
import { paths } from "../api/paths";
|
||||
import { TRACE_TERMINAL } from "../api/types";
|
||||
import type { Agent, Approval, Followup, PeerMessage, Review, Task, ThreadInfo, TraceEntry } from "../api/types";
|
||||
import type { Agent, Approval, ChatHistory, Followup, PeerMessage, Review, Task, ThreadInfo, TraceEntry } from "../api/types";
|
||||
|
||||
// Task detail state: the selected task, its trace SSE/one-shot stream,
|
||||
// and every structured sub-list. Chat sending lives here too:
|
||||
@@ -36,6 +36,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
const [threadMsg, setThreadMsg] = useState("");
|
||||
const [threadList, setThreadList] = useState<ThreadInfo[]>([]);
|
||||
const [chatMsg, setChatMsg] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<ChatHistory | null>(null);
|
||||
const traceAbortRef = useRef<AbortController | null>(null);
|
||||
const agentTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const currentTaskRef = useRef<Task | null>(null);
|
||||
@@ -72,6 +73,15 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
catch { setFollowups([]); }
|
||||
}, []);
|
||||
|
||||
// True alternating history: server-merged prompt + persisted
|
||||
// continues + followups + trace, in timestamp order. On failure
|
||||
// (old server without /chat-history) keep null and ContinuePanel
|
||||
// falls back to its previous prompt + trace + followups view.
|
||||
const loadChatHistory = useCallback(async (taskId: string) => {
|
||||
try { const d = await api<ChatHistory>(paths.taskChatHistory(taskId)); setChatHistory(d); }
|
||||
catch { setChatHistory(null); }
|
||||
}, []);
|
||||
|
||||
const loadReviews = useCallback(async (taskId?: string) => {
|
||||
const id = taskId || currentTaskRef.current?.id;
|
||||
if (!id) return;
|
||||
@@ -226,6 +236,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
void startTrace(taskId, t.status);
|
||||
void loadAgents(taskId);
|
||||
void loadCoordChildren(t);
|
||||
void loadChatHistory(taskId);
|
||||
void loadFollowups(taskId);
|
||||
void loadPeerMessages(taskId);
|
||||
void loadThread(taskId);
|
||||
@@ -236,7 +247,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
setCurrentTask(null);
|
||||
setDetailBody("加载失败:" + e.message);
|
||||
}
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, getRole]);
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadChatHistory, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, getRole]);
|
||||
|
||||
const refreshDetailLists = useCallback(async () => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -250,6 +261,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
setDetailBody(body || "(暂无 result/error 输出)");
|
||||
} catch { /* light header refresh must not clear the detail */ }
|
||||
void loadFollowups(t.id);
|
||||
void loadChatHistory(t.id);
|
||||
void loadReviews(t.id);
|
||||
void loadApprovals(t.id);
|
||||
void loadCoordChildren(t);
|
||||
@@ -257,7 +269,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
void loadPeerMessages(t.id);
|
||||
void loadThread(t.id);
|
||||
void loadThreads(t.id);
|
||||
}, [loadFollowups, loadReviews, loadApprovals, loadCoordChildren, loadAgents, loadPeerMessages, loadThread, loadThreads]);
|
||||
}, [loadFollowups, loadChatHistory, loadReviews, loadApprovals, loadCoordChildren, loadAgents, loadPeerMessages, loadThread, loadThreads]);
|
||||
|
||||
const doAction = useCallback(async (kind: string) => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -370,11 +382,12 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
await api(paths.taskFollowups(t.id), "POST", { body: v, mode: "direct" });
|
||||
setChatMsg("已直接发送");
|
||||
await loadFollowups(t.id);
|
||||
await loadChatHistory(t.id);
|
||||
} else {
|
||||
setChatMsg("merged/merge-failed 任务不可继续,也不可发后续消息");
|
||||
}
|
||||
} catch (e: any) { setChatMsg("发送失败:" + e.message); }
|
||||
}, [showDetail, onTasksReload, loadFollowups]);
|
||||
}, [showDetail, onTasksReload, loadFollowups, loadChatHistory]);
|
||||
|
||||
const downloadTrace = useCallback(async () => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -399,7 +412,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
currentTask, detailBody, actionMsg, actionReasons, costBox, forkVisible, setForkVisible, forkMsg,
|
||||
coordChildren, agents, agentsNote, reviews, reviewMsg, reviewError, traceEntries, traceNote, traceDone,
|
||||
traceKind, setTraceKind, traceMsg, followups, approvals, approvalMsg, peerMessages, peerMsg,
|
||||
threadText, threadGateNote, threadMsg, threadList, chatMsg,
|
||||
threadText, threadGateNote, threadMsg, threadList, chatMsg, chatHistory,
|
||||
showDetail, refreshDetailLists, doAction, taskCost, forkTask, postReview, submitFindings,
|
||||
decideApproval, postPeer, threadAction, sendChatMessage, downloadTrace,
|
||||
refreshReviews: () => void loadReviews(), refreshApprovals: () => void loadApprovals(),
|
||||
|
||||
Reference in new issue
Block a user