- api.ts chatStream 新增可选 mode,空则不写键(后端回落chat) - useChat.ts 发送时带上 useModeStore 当前 mode,使非chat模式消息在Web端真正落库 - 修正 docblock(移除虚构的WS chat回退描述)
67 lines
3.0 KiB
TypeScript
67 lines
3.0 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
import { chatStream } from "./api";
|
|
import { connectWs, isWsOpen } from "./ws";
|
|
import { useAppStore } from "../store/useAppStore";
|
|
import { useModeStore } from "../store/useModeStore";
|
|
import { getClientId } from "./client";
|
|
import { now } from "./util";
|
|
|
|
/** Drives one user turn: push the user bubble, blanket a streaming assistant
|
|
* bubble, trigger the backend loop over REST (`stream:true`), and let the WS
|
|
* stream render into that bubble. If the REST trigger fails, the failure is
|
|
* surfaced inline on the bubble rather than left streaming forever.
|
|
*
|
|
* The behavior mode is chosen by the agent / backend hot-switch (design/01 §3);
|
|
* each message carries the mode in effect when the turn is sent, and the bubble
|
|
* renders per that message's mode. We read the current UI mode here and pass it
|
|
* on the turn so the backend persists it alongside the message (A3). */
|
|
export function useSendChat(): {
|
|
send: (text: string, image?: string) => Promise<void>;
|
|
sending: boolean;
|
|
} {
|
|
const [sending, setSending] = useState(false);
|
|
const sessionId = useAppStore((s) => s.activeSessionId);
|
|
const addUserMessage = useAppStore((s) => s.addUserMessage);
|
|
const beginAssistant = useAppStore((s) => s.beginAssistant);
|
|
const endStream = useAppStore((s) => s.endStream);
|
|
const appendChunk = useAppStore((s) => s.appendChunk);
|
|
const addTool = useAppStore((s) => s.addTool);
|
|
const appendTaskLog = useAppStore((s) => s.appendTaskLog);
|
|
const mode = useModeStore((s) => s.mode);
|
|
|
|
const send = useCallback(
|
|
async (text: string, image?: string) => {
|
|
const trimmed = text.trim();
|
|
if ((!trimmed && !image) || sending) return;
|
|
setSending(true);
|
|
if (!isWsOpen()) connectWs();
|
|
|
|
// The gateway's chat body still requires a non-empty text, so a pure image
|
|
// turn (vision) falls back to a default prompt to keep the trigger valid.
|
|
const prompt = trimmed || (image ? "分析这张图片" : "");
|
|
// Tag this browser's persistent client id onto the optimistic bubbles so the
|
|
// source label shows immediately and matches the persisted turn (design/16 §4.5).
|
|
const clientId = getClientId();
|
|
addUserMessage(trimmed, undefined, image, clientId);
|
|
beginAssistant(mode, clientId);
|
|
|
|
try {
|
|
await chatStream(sessionId, prompt, image, mode);
|
|
} catch (err: unknown) {
|
|
// Backend loop not wired or gateway unreachable: show the failure inline
|
|
// rather than leaving a permanently-streaming bubble.
|
|
endStream();
|
|
const msg = err instanceof Error ? err.message : "触发失败";
|
|
appendChunk(`[stream 触发失败] ${msg}`);
|
|
appendTaskLog("main", `[error] ${msg}\n`);
|
|
addTool({ name: "stream_trigger", summary: "失败", ok: false, detail: msg, at: now() });
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
},
|
|
[sending, sessionId, mode, addUserMessage, beginAssistant, endStream, appendChunk, addTool, appendTaskLog]
|
|
);
|
|
|
|
return { send, sending };
|
|
}
|