Commit 3d778a92 authored by AI-甘富林's avatar AI-甘富林

feat(ui): streamline chat composer and inline trace rendering

parent a4d243ae
This diff is collapsed.
This diff is collapsed.
import { randomUUID } from "node:crypto";
import { randomUUID } from "node:crypto";
import WebSocket from "ws";
import type {
ChatMessage,
GatewayHealth,
GatewayStatus,
LogEntry,
MessageRole,
PromptResult,
SessionSummary
} from "@qjclaw/shared-types";
......@@ -48,6 +49,7 @@ interface PendingChatRun {
sessionKey: string;
accumulatedText: string;
onDelta?: (value: GatewayPromptStreamDelta) => void;
onStatus?: (value: GatewayPromptStreamStatus) => void;
onCompleted?: (value: { sessionId: string; runId: string; reply: ChatMessage }) => void;
onError?: (value: { sessionId: string; runId?: string; error: Error }) => void;
}
......@@ -100,7 +102,7 @@ interface SessionsListResult {
interface ChatHistoryResult {
sessionKey?: string;
messages?: Array<{
role?: "system" | "user" | "assistant";
role?: string;
timestamp?: number;
content?: Array<{ type?: string; text?: string }>;
}>;
......@@ -119,9 +121,18 @@ export interface GatewayPromptStreamDelta {
fullText?: string;
}
export interface GatewayPromptStreamStatus {
sessionId: string;
runId?: string;
stage: string;
label: string;
detail?: string;
}
export interface GatewayPromptStreamHandlers {
onStarted?: (value: GatewayPromptStreamStart) => void;
onDelta?: (value: GatewayPromptStreamDelta) => void;
onStatus?: (value: GatewayPromptStreamStatus) => void;
onCompleted?: (value: { sessionId: string; runId: string; reply: ChatMessage }) => void;
onError?: (value: { sessionId: string; runId?: string; error: Error }) => void;
}
......@@ -410,7 +421,7 @@ export class GatewayClient {
const result = (await this.request("chat.history", { sessionKey, limit: 100 })) as ChatHistoryResult;
const messages = (result.messages ?? []).map((message, messageIndex) => ({
id: `${sessionKey}:${message.timestamp ?? messageIndex}:${messageIndex}`,
role: message.role ?? "assistant",
role: this.normalizeChatRole(message.role),
content: this.flattenContent(message.content),
createdAt: new Date(message.timestamp ?? Date.now()).toISOString()
}));
......@@ -492,7 +503,8 @@ export class GatewayClient {
if (runId) {
this.emitChatDelta(runId, payload);
if (state === "final") {
this.completeChatRun(runId, this.buildChatMessage(runId, payload));
const reply = await this.resolveCompletedChatReply(runId, payload);
this.completeChatRun(runId, reply);
}
}
}
......@@ -511,6 +523,11 @@ export class GatewayClient {
if (runId) {
this.emitChatDelta(runId, payload.data ?? payload);
}
} else if (runId) {
const status = this.describeAgentStatus(payload, stream);
if (status) {
this.emitChatStatus(runId, status);
}
}
}
......@@ -614,6 +631,7 @@ export class GatewayClient {
sessionKey,
accumulatedText: "",
onDelta: handlers.onDelta,
onStatus: handlers.onStatus,
onCompleted: handlers.onCompleted,
onError: handlers.onError
});
......@@ -679,6 +697,80 @@ export class GatewayClient {
});
}
private emitChatStatus(runId: string, status: Omit<GatewayPromptStreamStatus, "sessionId" | "runId">): void {
const pending = this.pendingChatRuns.get(runId);
if (!pending) {
return;
}
this.refreshChatRunTimer(runId);
pending.onStatus?.({
sessionId: pending.sessionKey,
runId,
...status
});
}
private async resolveCompletedChatReply(runId: string, payload: Record<string, unknown>): Promise<ChatMessage> {
const reply = this.buildChatMessage(runId, payload);
if (reply.content.trim()) {
return reply;
}
const pending = this.pendingChatRuns.get(runId);
if (!pending) {
return reply;
}
try {
const history = await this.listMessages(pending.sessionKey);
const assistant = [...history].reverse().find((message) => message.role === "assistant" && message.content.trim());
if (assistant) {
return assistant;
}
} catch {
}
return reply;
}
private describeAgentStatus(payload: Record<string, unknown>, stream: string): Omit<GatewayPromptStreamStatus, "sessionId" | "runId"> | null {
const normalizedStage = stream.trim().toLowerCase();
const toolName = this.findStringDeep(payload, ["toolName", "tool_name", "name"]);
const detail = this.extractTextCandidate(payload.data) ?? this.extractTextCandidate(payload);
const label = this.buildStatusLabel(normalizedStage, toolName);
const compactDetail = detail && detail !== label ? detail.slice(0, 240) : undefined;
if (!label && !compactDetail) {
return null;
}
return {
stage: stream,
label: label || "\u6b63\u5728\u6574\u7406\u4e2d\u95f4\u7ed3\u679c",
detail: compactDetail
};
}
private buildStatusLabel(stage: string, toolName?: string): string {
if (stage.includes("reason") || stage.includes("think")) {
return "\u6b63\u5728\u7406\u89e3\u4f60\u7684\u95ee\u9898";
}
if (stage.includes("tool") && stage.includes("result")) {
return toolName ? `${toolName} \u5df2\u8fd4\u56de\u4fe1\u606f` : "\u5df2\u62ff\u5230\u8865\u5145\u4fe1\u606f";
}
if (stage.includes("tool") || stage.includes("call")) {
return toolName ? `\u6b63\u5728\u8c03\u7528 ${toolName}` : "\u6b63\u5728\u8c03\u7528\u8f85\u52a9\u80fd\u529b";
}
if (stage.includes("search") || stage.includes("fetch") || stage.includes("browser") || stage.includes("web")) {
return toolName ? `\u6b63\u5728\u901a\u8fc7 ${toolName} \u67e5\u627e\u4fe1\u606f` : "\u6b63\u5728\u67e5\u627e\u8865\u5145\u4fe1\u606f";
}
if (stage.includes("plan") || stage.includes("route")) {
return "\u6b63\u5728\u5b89\u6392\u5904\u7406\u6b65\u9aa4";
}
return toolName ? `\u6b63\u5728\u6574\u7406 ${toolName} \u8fd4\u56de\u7684\u4fe1\u606f` : "\u6b63\u5728\u6574\u7406\u4e2d\u95f4\u7ed3\u679c";
}
private completeChatRun(runId: string, reply: ChatMessage): void {
const pending = this.pendingChatRuns.get(runId);
if (!pending) {
......@@ -717,6 +809,13 @@ export class GatewayClient {
};
}
private normalizeChatRole(role: unknown): MessageRole {
if (role === "system" || role === "user" || role === "assistant" || role === "tool" || role === "toolResult") {
return role;
}
return "system";
}
private extractTextCandidate(value: unknown): string | undefined {
if (typeof value === "string") {
const normalized = value.replace(/\r\n/g, "\n");
......@@ -993,7 +1092,7 @@ export class GatewayClient {
private stripStructuredLogPrefix(message: string): string {
return message
.replace(LOG_PREFIX_PATTERN, "")
.replace(/闁跨喓绁?/g, "ok ")
.replace(/闂佽法鍠撶粊?/g, "ok ")
.replace(/[?]{2,}/g, "")
.replace(/\s+/g, " ")
.trim();
......@@ -1043,3 +1142,6 @@ export class GatewayClient {
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment