mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
feat(logging): update logging format to plain text and enhance HTTP diagnostics
This commit is contained in:
+4
-1
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
+ [新增] Canvas Agent 支持通过 `--debug` 输出彩色详细日志并按日期保存到本地日志文件。
|
+ [新增] Canvas Agent 支持通过 `--debug` 输出详细日志并按日期保存到本地日志文件。
|
||||||
|
+ [优化] Canvas Agent Debug 日志改为简洁的纯文本单行格式。
|
||||||
|
+ [优化] 精简网页与本地 Agent 的 HTTP 诊断日志,过滤轮询和流式重复事件并改用任务摘要展示。
|
||||||
|
+ [优化] Agent 对话采用透明无气泡的简洁用户消息样式,输入框上方居中实时展示带平滑数字动画的最新一次模型调用 Token 用量。
|
||||||
+ [修复] 修复 Agent 发送后的运行提示闪退及回复需要切换标签页才显示的问题。
|
+ [修复] 修复 Agent 发送后的运行提示闪退及回复需要切换标签页才显示的问题。
|
||||||
+ [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
|
+ [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
|
||||||
+ [优化] Canvas Agent 改为从独立指令文件初始化当前工作目录的 AGENTS.md,不再为每轮消息重复拼接前置提示词。
|
+ [优化] Canvas Agent 改为从独立指令文件初始化当前工作目录的 AGENTS.md,不再为每轮消息重复拼接前置提示词。
|
||||||
|
|||||||
+19
-15
@@ -40,7 +40,6 @@ export function interruptCodexTurn(threadId?: string) {
|
|||||||
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) {
|
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) {
|
||||||
let files: string[] = [];
|
let files: string[] = [];
|
||||||
try {
|
try {
|
||||||
logger.info("Preparing Codex turn", { threadId: options.threadId, cwd: options.cwd, prompt, attachmentCount: attachments.length });
|
|
||||||
options.onStart?.();
|
options.onStart?.();
|
||||||
files = await writeAttachmentFiles(attachments);
|
files = await writeAttachmentFiles(attachments);
|
||||||
const app = await getCodexApp(options.appEmit || emit);
|
const app = await getCodexApp(options.appEmit || emit);
|
||||||
@@ -147,7 +146,6 @@ class CodexAppClient {
|
|||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
private buffer = "";
|
private buffer = "";
|
||||||
private textByItem = new Map<string, string>();
|
private textByItem = new Map<string, string>();
|
||||||
private deltaCount = 0;
|
|
||||||
private lastUsage: unknown = null;
|
private lastUsage: unknown = null;
|
||||||
private pending = new Map<number, PendingRequest>();
|
private pending = new Map<number, PendingRequest>();
|
||||||
private activeTurns = new Map<string, PendingRequest>();
|
private activeTurns = new Map<string, PendingRequest>();
|
||||||
@@ -245,7 +243,9 @@ class CodexAppClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private write(value: unknown) {
|
private write(value: unknown) {
|
||||||
logger.debug("Codex app-server request", value);
|
const method = String(field(value, "method") || "");
|
||||||
|
const params = field(value, "params");
|
||||||
|
if (method) logger.debug(`Codex ${method}`, { id: field(value, "id"), threadId: field(params, "threadId") });
|
||||||
this.child.stdin?.write(`${JSON.stringify(value)}\n`);
|
this.child.stdin?.write(`${JSON.stringify(value)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,9 +264,12 @@ class CodexAppClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handle(message: Json) {
|
private handle(message: Json) {
|
||||||
logger.debug("Codex app-server response", message);
|
|
||||||
const id = Number(message.id);
|
const id = Number(message.id);
|
||||||
if (message.error && this.pending.has(id)) return this.reject(id, String(field(message.error, "message") || "Codex request failed"));
|
if (message.error && this.pending.has(id)) {
|
||||||
|
const error = String(field(message.error, "message") || "Codex request failed");
|
||||||
|
logger.warn("Codex request failed", { id, error });
|
||||||
|
return this.reject(id, error);
|
||||||
|
}
|
||||||
if (this.pending.has(id)) return this.resolve(id, message.result);
|
if (this.pending.has(id)) return this.resolve(id, message.result);
|
||||||
if (typeof message.method === "string" && "id" in message) return this.answerServerRequest(message);
|
if (typeof message.method === "string" && "id" in message) return this.answerServerRequest(message);
|
||||||
if (typeof message.method === "string") this.handleNotification(message.method, (message.params || {}) as Json);
|
if (typeof message.method === "string") this.handleNotification(message.method, (message.params || {}) as Json);
|
||||||
@@ -274,7 +277,11 @@ class CodexAppClient {
|
|||||||
|
|
||||||
private handleNotification(method: string, params: Json) {
|
private handleNotification(method: string, params: Json) {
|
||||||
if (method === "item/agentMessage/delta") return this.emitDelta(params);
|
if (method === "item/agentMessage/delta") return this.emitDelta(params);
|
||||||
if (method === "thread/tokenUsage/updated") this.lastUsage = normalizeUsage(params);
|
if (method === "thread/tokenUsage/updated") {
|
||||||
|
this.lastUsage = normalizeUsage(params);
|
||||||
|
this.emit("agent_event", { agent: "codex", type: "usage.updated", usage: this.lastUsage, ...codexEventScope(params) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const event = normalizeCodexNotification(method, params);
|
const event = normalizeCodexNotification(method, params);
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
if (event.type === "item.completed") {
|
if (event.type === "item.completed") {
|
||||||
@@ -296,8 +303,6 @@ class CodexAppClient {
|
|||||||
} else if (turnId) {
|
} else if (turnId) {
|
||||||
this.completedTurns.set(turnId, error ? new Error(String(field(error, "message") || "Codex turn failed")) : null);
|
this.completedTurns.set(turnId, error ? new Error(String(field(error, "message") || "Codex turn failed")) : null);
|
||||||
}
|
}
|
||||||
this.emit("agent_event", { agent: "codex", type: "stream.summary", delta_count: this.deltaCount, ...codexEventScope(params) });
|
|
||||||
this.deltaCount = 0;
|
|
||||||
this.emit("agent_done", { agent: "codex", usage: event.usage, ...codexEventScope(params) });
|
this.emit("agent_done", { agent: "codex", usage: event.usage, ...codexEventScope(params) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +310,6 @@ class CodexAppClient {
|
|||||||
private emitDelta(params: Json) {
|
private emitDelta(params: Json) {
|
||||||
const id = String(field(params, "itemId") || "");
|
const id = String(field(params, "itemId") || "");
|
||||||
const text = `${this.textByItem.get(id) || ""}${String(field(params, "delta") || "")}`;
|
const text = `${this.textByItem.get(id) || ""}${String(field(params, "delta") || "")}`;
|
||||||
this.deltaCount += 1;
|
|
||||||
this.textByItem.set(id, text);
|
this.textByItem.set(id, text);
|
||||||
this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: "agent_message", text }, ...codexEventScope(params) });
|
this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: "agent_message", text }, ...codexEventScope(params) });
|
||||||
}
|
}
|
||||||
@@ -353,7 +357,7 @@ function normalizeCodexNotification(method: string, params: Json): AgentEvent |
|
|||||||
const scope = codexEventScope(params);
|
const scope = codexEventScope(params);
|
||||||
if (method === "thread/started") return { type: "thread.started", ...scope };
|
if (method === "thread/started") return { type: "thread.started", ...scope };
|
||||||
if (method === "turn/started") return { type: "turn.started", ...scope };
|
if (method === "turn/started") return { type: "turn.started", ...scope };
|
||||||
if (method === "turn/completed") return { type: "turn.completed", usage: null, ...scope };
|
if (method === "turn/completed") return { type: "turn.completed", usage: null, duration_ms: field(field(params, "turn"), "durationMs"), ...scope };
|
||||||
if (method === "item/started") return { type: "item.started", item: normalizeItem(field(params, "item")), ...scope };
|
if (method === "item/started") return { type: "item.started", item: normalizeItem(field(params, "item")), ...scope };
|
||||||
if (method === "item/completed") return { type: "item.completed", item: normalizeItem(field(params, "item")), ...scope };
|
if (method === "item/completed") return { type: "item.completed", item: normalizeItem(field(params, "item")), ...scope };
|
||||||
if (method === "error") return { type: "error", message: field(params, "message"), ...scope };
|
if (method === "error") return { type: "error", message: field(params, "message"), ...scope };
|
||||||
@@ -405,12 +409,12 @@ function normalizeItem(item: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUsage(params: Json) {
|
function normalizeUsage(params: Json) {
|
||||||
const total = field(field(params, "tokenUsage"), "total") as Json | undefined;
|
const last = field(field(params, "tokenUsage"), "last") as Json | undefined;
|
||||||
return {
|
return {
|
||||||
input_tokens: field(total, "inputTokens"),
|
input_tokens: field(last, "inputTokens"),
|
||||||
cached_input_tokens: field(total, "cachedInputTokens"),
|
cached_input_tokens: field(last, "cachedInputTokens"),
|
||||||
output_tokens: field(total, "outputTokens"),
|
output_tokens: field(last, "outputTokens"),
|
||||||
reasoning_output_tokens: field(total, "reasoningOutputTokens"),
|
reasoning_output_tokens: field(last, "reasoningOutputTokens"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export function startHttpServer() {
|
|||||||
const emit = (type: string, payload: unknown) => {
|
const emit = (type: string, payload: unknown) => {
|
||||||
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
|
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
|
||||||
const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
|
const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
|
||||||
logger.debug("Agent event", { type, threadId, payload: data });
|
|
||||||
threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
|
threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
|
||||||
};
|
};
|
||||||
const setActiveThread = (activeThreadId: string, payload: Record<string, unknown> = {}) => {
|
const setActiveThread = (activeThreadId: string, payload: Record<string, unknown> = {}) => {
|
||||||
@@ -31,7 +30,10 @@ export function startHttpServer() {
|
|||||||
if (!logger.enabled) return next();
|
if (!logger.enabled) return next();
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const url = requestUrl(req, config);
|
const url = requestUrl(req, config);
|
||||||
res.on("finish", () => logger.debug("HTTP request", { method: req.method, path: url.pathname, status: res.statusCode, durationMs: Date.now() - startedAt, origin: req.headers.origin, clientId: url.searchParams.get("clientId") }));
|
res.on("finish", () => {
|
||||||
|
if (req.method === "OPTIONS" || (res.statusCode < 400 && ["/health", "/canvas/state", "/canvas/activate"].includes(url.pathname))) return;
|
||||||
|
logger.debug(`HTTP ${req.method} ${url.pathname}`, { status: res.statusCode, durationMs: Date.now() - startedAt });
|
||||||
|
});
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -117,7 +119,7 @@ export function startHttpServer() {
|
|||||||
const prompt = String(req.body?.prompt || "");
|
const prompt = String(req.body?.prompt || "");
|
||||||
if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" });
|
if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" });
|
||||||
const clientId = String(req.body?.clientId || "");
|
const clientId = String(req.body?.clientId || "");
|
||||||
logger.info("Codex turn accepted", { clientId, threadId: req.body?.threadId, prompt, attachments: attachments.map(({ id, name, type, size, width, height }) => ({ id, name, type, size, width, height })) });
|
logger.info("Codex turn accepted", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length });
|
||||||
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
|
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
|
||||||
try {
|
try {
|
||||||
let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
|
let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export class Logger {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
|
fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
|
||||||
const line = format.printf(({level, message, timestamp, details}) => `[${level.toUpperCase()}][${timestamp}] ${message}${formatDetails(details)}`);
|
const line = format.printf(({level, message, timestamp, details}) => `${timestamp} ${level.toUpperCase()} ${message}${formatDetails(details)}`);
|
||||||
this.logger = winston.createLogger({
|
this.logger = winston.createLogger({
|
||||||
level: "debug",
|
level: "debug",
|
||||||
transports: [
|
transports: [
|
||||||
new transports.Console({format: format.combine(format.colorize(), format.timestamp({format: "HH:mm:ss"}), line)}),
|
new transports.Console({format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
|
||||||
new transports.File({filename: this.filePath, format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
|
new transports.File({filename: this.filePath, format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -59,7 +59,8 @@ export class Logger {
|
|||||||
function formatDetails(details: unknown) {
|
function formatDetails(details: unknown) {
|
||||||
if (details === undefined) return "";
|
if (details === undefined) return "";
|
||||||
if (!details || typeof details !== "object" || Array.isArray(details)) return ` ${inspect(details, {depth: null, breakLength: Infinity})}`;
|
if (!details || typeof details !== "object" || Array.isArray(details)) return ` ${inspect(details, {depth: null, breakLength: Infinity})}`;
|
||||||
return ` ${Object.entries(details).map(([key, value]) => `${key}=${inspect(value, {depth: null, breakLength: Infinity})}`).join(" ")}`;
|
const text = Object.entries(details).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${inspect(value, {depth: null, breakLength: Infinity})}`).join(" ");
|
||||||
|
return text ? ` ${text}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理日志内容中的敏感数据和不可序列化引用。 */
|
/** 清理日志内容中的敏感数据和不可序列化引用。 */
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ description: 当前版本已实现但仍需人工验证的变更项
|
|||||||
|
|
||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
- Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应以 `[DEBUG][HH:mm:ss]` 等传统格式输出带级别颜色的详细日志并显示日志文件路径,`~/.infinite-canvas/logs/` 下应按启动日期生成不含颜色代码的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。
|
- Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应按“时间 级别 消息 详情”的纯文本单行格式输出日志并显示日志文件路径,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。
|
||||||
|
- Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。
|
||||||
|
- Agent 对话统计:用户消息应右对齐并使用透明无边框、无气泡背景的简洁样式,用户消息和 Codex 回复下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
|
||||||
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现 GPT 图标和 `working...`,任务运行期间不应闪退;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。
|
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现 GPT 图标和 `working...`,任务运行期间不应闪退;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。
|
||||||
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
|
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
|
||||||
- Agent 工作目录指令:`canvas-agent/agent-instructions.md` 应作为独立维护源;重启 Canvas Agent 后,当前工作目录应自动生成 `AGENTS.md`;新建对话发送消息时,Codex 日志中的用户消息只包含本轮请求和必要的附件上下文,不再重复整段 Infinite Canvas 前置提示词,画布及工作台工具仍可正常调用。
|
- Agent 工作目录指令:`canvas-agent/agent-instructions.md` 应作为独立维护源;重启 Canvas Agent 后,当前工作目录应自动生成 `AGENTS.md`;新建对话发送消息时,Codex 日志中的用户消息只包含本轮请求和必要的附件上下文,不再重复整段 Infinite Canvas 前置提示词,画布及工作台工具仍可正常调用。
|
||||||
|
|||||||
@@ -49,16 +49,8 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
|
|||||||
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
||||||
{!isUser ? <AgentAvatar theme={theme} /> : null}
|
{!isUser ? <AgentAvatar theme={theme} /> : null}
|
||||||
<div
|
<div
|
||||||
className={isUser ? "min-w-0 max-w-[82%] rounded-xl rounded-br-sm border px-3.5 py-2.5 text-left text-sm leading-6" : "min-w-0 flex-1 text-left text-sm leading-6"}
|
className={isUser ? "min-w-0 max-w-[82%] py-1 text-right text-sm leading-6" : "min-w-0 flex-1 text-left text-sm leading-6"}
|
||||||
style={
|
style={{ color }}
|
||||||
isUser
|
|
||||||
? {
|
|
||||||
color,
|
|
||||||
background: `color-mix(in srgb, ${theme.node.text} 7%, ${theme.toolbar.panel})`,
|
|
||||||
borderColor: `color-mix(in srgb, ${theme.node.text} 14%, transparent)`,
|
|
||||||
}
|
|
||||||
: { color }
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<div className="whitespace-pre-wrap break-words">{item.text}</div>
|
<div className="whitespace-pre-wrap break-words">{item.text}</div>
|
||||||
@@ -66,7 +58,7 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
|
|||||||
<Streamdown animated isAnimating={!!item.streamId}>{item.text}</Streamdown>
|
<Streamdown animated isAnimating={!!item.streamId}>{item.text}</Streamdown>
|
||||||
)}
|
)}
|
||||||
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} /> : null}
|
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} /> : null}
|
||||||
{item.meta ? <div className={`mt-1 text-[11px] opacity-45 ${isUser ? "text-right" : ""}`}>{item.meta}</div> : null}
|
{item.meta ? <div className={`mt-1 text-[11px] tabular-nums opacity-55 ${isUser ? "text-right" : ""}`}>{item.meta}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
{isUser ? <AgentUserAvatar user={user} theme={theme} /> : null}
|
{isUser ? <AgentUserAvatar user={user} theme={theme} /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
|||||||
import { App, Button, Checkbox, Input, Segmented, Tooltip } from "antd";
|
import { App, Button, Checkbox, Input, Segmented, Tooltip } from "antd";
|
||||||
import copyToClipboard from "copy-to-clipboard";
|
import copyToClipboard from "copy-to-clipboard";
|
||||||
import { ChevronDown, Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react";
|
import { ChevronDown, Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react";
|
||||||
|
import { motion, useSpring, useTransform } from "motion/react";
|
||||||
|
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
import { imageMetadata } from "@/lib/canvas/canvas-node-factory";
|
import { imageMetadata } from "@/lib/canvas/canvas-node-factory";
|
||||||
@@ -13,7 +14,7 @@ import { uploadImage } from "@/services/image-storage";
|
|||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
import { useThemeStore } from "@/stores/use-theme-store";
|
||||||
import { useUserStore } from "@/stores/use-user-store";
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAgentStore, type AgentAttachment, type AgentCanvasContext, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "@/stores/use-agent-store";
|
import { useAgentStore, type AgentAttachment, type AgentCanvasContext, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary, type AgentTokenUsage } from "@/stores/use-agent-store";
|
||||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||||
import { isSiteTool, runSiteTool, SITE_TOOL_LABELS } from "@/lib/agent/agent-site-tools";
|
import { isSiteTool, runSiteTool, SITE_TOOL_LABELS } from "@/lib/agent/agent-site-tools";
|
||||||
import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui";
|
import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui";
|
||||||
@@ -39,6 +40,7 @@ type AgentEventPayload = {
|
|||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
message?: string;
|
message?: string;
|
||||||
usage?: Record<string, unknown>;
|
usage?: Record<string, unknown>;
|
||||||
|
duration_ms?: number;
|
||||||
};
|
};
|
||||||
type AgentEventItem = { id?: string; type?: string; text?: unknown; message?: unknown; server?: string; tool?: string; status?: string; arguments?: unknown; result?: unknown; error?: { message?: string } };
|
type AgentEventItem = { id?: string; type?: string; text?: unknown; message?: unknown; server?: string; tool?: string; status?: string; arguments?: unknown; result?: unknown; error?: { message?: string } };
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
// 注意:canvasContext 不在此订阅内 —— 它在拖拽/resize 时会被 project 每帧写入,
|
// 注意:canvasContext 不在此订阅内 —— 它在拖拽/resize 时会被 project 每帧写入,
|
||||||
// 但面板只在 ref 同步与防抖 postState 中用到它、渲染层从不读它。若把它放进订阅,
|
// 但面板只在 ref 同步与防抖 postState 中用到它、渲染层从不读它。若把它放进订阅,
|
||||||
// 面板会随画布每帧重渲染(性能问题,也是 #185 崩溃的放大器)。改为下方 subscribe 命令式监听。
|
// 面板会随画布每帧重渲染(性能问题,也是 #185 崩溃的放大器)。改为下方 subscribe 命令式监听。
|
||||||
const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, messages, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, activity, connectError, pendingTool } = useAgentStore(
|
const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, messages, tokenUsage, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, activity, connectError, pendingTool } = useAgentStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
width: state.width,
|
width: state.width,
|
||||||
url: state.url,
|
url: state.url,
|
||||||
@@ -75,6 +77,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
sending: state.sending,
|
sending: state.sending,
|
||||||
waiting: state.waiting,
|
waiting: state.waiting,
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
|
tokenUsage: state.tokenUsage,
|
||||||
eventLogs: state.eventLogs,
|
eventLogs: state.eventLogs,
|
||||||
threads: state.threads,
|
threads: state.threads,
|
||||||
activeThreadId: state.activeThreadId,
|
activeThreadId: state.activeThreadId,
|
||||||
@@ -233,7 +236,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
enqueueEvent(async () => {
|
enqueueEvent(async () => {
|
||||||
const nextThreadId = data.activeThreadId ?? data.threadId ?? "";
|
const nextThreadId = data.activeThreadId ?? data.threadId ?? "";
|
||||||
pendingToolRef.current = null;
|
pendingToolRef.current = null;
|
||||||
setAgentState({ activeThreadId: nextThreadId, messages: [], pendingTool: null });
|
setAgentState({ activeThreadId: nextThreadId, messages: [], tokenUsage: null, pendingTool: null });
|
||||||
await loadThreads(data.emptyThread);
|
await loadThreads(data.emptyThread);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -263,7 +266,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
const silent = useAgentStore.getState().silentConnect && !wasConnected;
|
const silent = useAgentStore.getState().silentConnect && !wasConnected;
|
||||||
const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token";
|
const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token";
|
||||||
if (!errorLoggedRef.current || wasConnected) {
|
if (!errorLoggedRef.current || wasConnected) {
|
||||||
addEventLog(wasConnected ? "连接断开" : "连接失败", { endpoint, error: text });
|
addEventLog(wasConnected ? "连接断开" : "连接失败", text);
|
||||||
if (!headless && !silent) message.error(text);
|
if (!headless && !silent) message.error(text);
|
||||||
}
|
}
|
||||||
errorLoggedRef.current = true;
|
errorLoggedRef.current = true;
|
||||||
@@ -310,7 +313,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
setAgentState({ activity: "发送中", sending: true });
|
setAgentState({ activity: "发送中", sending: true });
|
||||||
const messageId = createId();
|
const messageId = createId();
|
||||||
addMessage({ id: messageId, role: "user", text: text || "发送了图片", attachments: files });
|
addMessage({ id: messageId, role: "user", text: text || "发送了图片", attachments: files });
|
||||||
addEventLog("用户发送", { text, attachments: files.map(({ name, type, size }) => ({ name, type, size })) });
|
addEventLog("发送任务", `${compactText(text) || "仅附件"}${files.length ? ` · 附件 ${files.length}` : ""}`);
|
||||||
try {
|
try {
|
||||||
const data = await fetchAgentJson<{ threadId?: string }>(endpoint, token, "/agent/codex/turn", {
|
const data = await fetchAgentJson<{ threadId?: string }>(endpoint, token, "/agent/codex/turn", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -325,7 +328,6 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (data.threadId) setAgentState({ activeThreadId: data.threadId });
|
if (data.threadId) setAgentState({ activeThreadId: data.threadId });
|
||||||
addEventLog("本地 Agent 已接收", { threadId: data.threadId });
|
|
||||||
files.forEach((item) => {
|
files.forEach((item) => {
|
||||||
URL.revokeObjectURL(item.url);
|
URL.revokeObjectURL(item.url);
|
||||||
attachmentUrlsRef.current.delete(item.url);
|
attachmentUrlsRef.current.delete(item.url);
|
||||||
@@ -349,7 +351,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
setAgentState({ activity: "停止中" });
|
setAgentState({ activity: "停止中" });
|
||||||
try {
|
try {
|
||||||
await fetch(`${endpoint}/agent/codex/interrupt?token=${encodeURIComponent(token)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ threadId: useAgentStore.getState().activeThreadId || undefined }) });
|
await fetch(`${endpoint}/agent/codex/interrupt?token=${encodeURIComponent(token)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ threadId: useAgentStore.getState().activeThreadId || undefined }) });
|
||||||
addEventLog("用户停止", {});
|
addEventLog("停止任务", "已发送停止请求");
|
||||||
} catch {
|
} catch {
|
||||||
setAgentState({ activity: "停止失败" });
|
setAgentState({ activity: "停止失败" });
|
||||||
}
|
}
|
||||||
@@ -534,6 +536,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
turnSyncSequenceRef.current += 1;
|
turnSyncSequenceRef.current += 1;
|
||||||
setAgentState({
|
setAgentState({
|
||||||
messages: [],
|
messages: [],
|
||||||
|
tokenUsage: null,
|
||||||
threads: [],
|
threads: [],
|
||||||
activeThreadId: "",
|
activeThreadId: "",
|
||||||
workspacePath: "",
|
workspacePath: "",
|
||||||
@@ -551,7 +554,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
setAgentState({ loadingThreads: true });
|
setAgentState({ loadingThreads: true });
|
||||||
try {
|
try {
|
||||||
const data = await fetchAgentJson<AgentThreadResponse>(endpoint, token, "/agent/codex/threads/new", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
|
const data = await fetchAgentJson<AgentThreadResponse>(endpoint, token, "/agent/codex/threads/new", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
|
||||||
setAgentState({ activeThreadId: data.thread?.id || data.workspace?.activeThreadId || "", messages: [], activeTab: "chat", activity: "新对话" });
|
setAgentState({ activeThreadId: data.thread?.id || data.workspace?.activeThreadId || "", messages: [], tokenUsage: null, activeTab: "chat", activity: "新对话" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addEventLog("新建对话失败", error);
|
addEventLog("新建对话失败", error);
|
||||||
message.error(error instanceof Error ? error.message : "新建对话失败");
|
message.error(error instanceof Error ? error.message : "新建对话失败");
|
||||||
@@ -565,7 +568,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
setAgentState({ loadingThreads: true });
|
setAgentState({ loadingThreads: true });
|
||||||
try {
|
try {
|
||||||
const data = await fetchAgentJson<AgentThreadResponse>(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/resume`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
|
const data = await fetchAgentJson<AgentThreadResponse>(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/resume`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
|
||||||
setAgentState({ activeThreadId: data.thread?.id || threadId, messages: normalizeHistoryMessages(data.messages || []), activeTab: "chat", activity: "已恢复会话" });
|
setAgentState({ activeThreadId: data.thread?.id || threadId, messages: normalizeHistoryMessages(data.messages || []), tokenUsage: null, activeTab: "chat", activity: "已恢复会话" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addEventLog("恢复对话失败", error);
|
addEventLog("恢复对话失败", error);
|
||||||
message.error(error instanceof Error ? error.message : "恢复对话失败");
|
message.error(error instanceof Error ? error.message : "恢复对话失败");
|
||||||
@@ -585,6 +588,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
threads: current.threads.filter((thread) => !deleted.has(thread.id)),
|
threads: current.threads.filter((thread) => !deleted.has(thread.id)),
|
||||||
activeThreadId: deleted.has(current.activeThreadId) ? "" : current.activeThreadId,
|
activeThreadId: deleted.has(current.activeThreadId) ? "" : current.activeThreadId,
|
||||||
messages: deleted.has(current.activeThreadId) ? [] : current.messages,
|
messages: deleted.has(current.activeThreadId) ? [] : current.messages,
|
||||||
|
tokenUsage: deleted.has(current.activeThreadId) ? null : current.tokenUsage,
|
||||||
});
|
});
|
||||||
message.success(`已删除 ${threadIds.length} 条记录`);
|
message.success(`已删除 ${threadIds.length} 条记录`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -630,18 +634,23 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addEventLog = (title: string, text: unknown, raw?: unknown) => {
|
const addEventLog = (title: string, text: unknown, raw?: unknown) => {
|
||||||
pushEventLog({ id: `${Date.now()}-${Math.random()}`, time: new Date().toLocaleTimeString(), title, text: normalizeText(text) || title, raw });
|
const value = normalizeText(text) || title;
|
||||||
|
const last = useAgentStore.getState().eventLogs.at(-1);
|
||||||
|
if (last?.title === title && last.text === value) return;
|
||||||
|
pushEventLog({ id: `${Date.now()}-${Math.random()}`, time: new Date().toLocaleTimeString(), title, text: value, raw });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAgentEvent = (event: AgentEventPayload) => {
|
const handleAgentEvent = (event: AgentEventPayload) => {
|
||||||
if (shouldLogAgentEvent(event)) addEventLog(eventTitle(event), event, event);
|
if (event.type === "usage.updated") setAgentState({ tokenUsage: eventUsage(event) });
|
||||||
|
const log = formatAgentEventLog(event);
|
||||||
|
if (log) addEventLog(log.title, log.text);
|
||||||
if (event.type === "thread.started" && event.thread_id) setAgentState({ activeThreadId: event.thread_id });
|
if (event.type === "thread.started" && event.thread_id) setAgentState({ activeThreadId: event.thread_id });
|
||||||
if (event.type === "item.completed" && event.item?.type === "agent_message" && event.item.id) {
|
if (event.type === "item.completed" && event.item?.type === "agent_message" && event.item.id) {
|
||||||
const currentMessages = useAgentStore.getState().messages;
|
const currentMessages = useAgentStore.getState().messages;
|
||||||
const index = currentMessages.findIndex((message) => message.streamId === event.item?.id);
|
const index = currentMessages.findIndex((message) => message.streamId === event.item?.id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const text = stringText(event.item.text);
|
const text = stringText(event.item.text);
|
||||||
setAgentState({ messages: currentMessages.map((message, i) => (i === index ? { ...message, text: text || message.text, meta: usageText(event) || message.meta, streamId: undefined } : message)) });
|
setAgentState({ messages: currentMessages.map((message, i) => (i === index ? { ...message, text: text || message.text, streamId: undefined } : message)) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -743,6 +752,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{tokenUsage ? <AgentUsageBar usage={tokenUsage} theme={theme} /> : null}
|
||||||
<AgentChatComposer
|
<AgentChatComposer
|
||||||
prompt={prompt}
|
prompt={prompt}
|
||||||
attachments={attachments.map(agentAttachmentToChatAttachment)}
|
attachments={attachments.map(agentAttachmentToChatAttachment)}
|
||||||
@@ -772,6 +782,31 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
|||||||
return embedded ? content : null;
|
return embedded ? content : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AgentUsageBar({ usage, theme }: { usage: AgentTokenUsage; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center gap-4 px-4 pt-1 text-[11px] tabular-nums" style={{ color: theme.node.muted }}>
|
||||||
|
<span className="opacity-70">最新调用</span>
|
||||||
|
<UsageNumber label="输入" value={usage.input} color={theme.node.text} />
|
||||||
|
<UsageNumber label="缓存" value={usage.cached} color={theme.node.text} />
|
||||||
|
<UsageNumber label="输出" value={usage.output} color={theme.node.text} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsageNumber({ label, value, color }: { label: string; value: number; color: string }) {
|
||||||
|
const spring = useSpring(value, { stiffness: 110, damping: 24, mass: 0.7 });
|
||||||
|
const text = useTransform(spring, (current) => Math.round(current).toLocaleString());
|
||||||
|
useEffect(() => spring.set(value), [spring, value]);
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-baseline gap-1" aria-label={`${label} ${value.toLocaleString()}`}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<motion.span aria-hidden className="font-medium" style={{ color }}>
|
||||||
|
{text}
|
||||||
|
</motion.span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AgentLogView({
|
function AgentLogView({
|
||||||
logs,
|
logs,
|
||||||
theme,
|
theme,
|
||||||
@@ -1125,7 +1160,7 @@ async function postToolResult(endpoint: string, token: string, clientId: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function agentMessageToChatMessage(item: AgentChatItem) {
|
function agentMessageToChatMessage(item: AgentChatItem) {
|
||||||
return { ...item, attachments: item.attachments?.map(agentAttachmentToChatAttachment) };
|
return { ...item, meta: item.role === "user" || item.role === "assistant" ? undefined : item.meta, attachments: item.attachments?.map(agentAttachmentToChatAttachment) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentAttachmentToChatAttachment(item: AgentAttachment): CanvasAgentChatAttachment {
|
function agentAttachmentToChatAttachment(item: AgentAttachment): CanvasAgentChatAttachment {
|
||||||
@@ -1135,10 +1170,8 @@ function agentAttachmentToChatAttachment(item: AgentAttachment): CanvasAgentChat
|
|||||||
function formatAgentEvent(event: AgentEventPayload): Omit<AgentChatItem, "id"> | null {
|
function formatAgentEvent(event: AgentEventPayload): Omit<AgentChatItem, "id"> | null {
|
||||||
const item = event.item;
|
const item = event.item;
|
||||||
if (event.type === "item.completed" && item?.type === "error") return { role: "error", title: "错误", text: normalizeText(item.message), detail: item };
|
if (event.type === "item.completed" && item?.type === "error") return { role: "error", title: "错误", text: normalizeText(item.message), detail: item };
|
||||||
if ((event.type === "item.updated" || event.type === "item.completed") && item?.type === "agent_message") return { role: "assistant", title: "Codex", text: stringText(item.text), meta: usageText(event), streamId: event.type === "item.updated" ? item.id : undefined };
|
if ((event.type === "item.updated" || event.type === "item.completed") && item?.type === "agent_message") return { role: "assistant", title: "Codex", text: stringText(item.text), streamId: event.type === "item.updated" ? item.id : undefined };
|
||||||
if (event.type === "item.completed" && isMcpToolItem(item) && isReadTool(String(item?.tool || ""))) return { role: "tool", title: `${toolName(String(item?.tool || ""))}完成`, text: item?.error?.message || toolSummary(item), detail: toolDetail(item) };
|
if (event.type === "item.completed" && isMcpToolItem(item) && isReadTool(String(item?.tool || ""))) return { role: "tool", title: `${toolName(String(item?.tool || ""))}完成`, text: item?.error?.message || toolSummary(item), detail: toolDetail(item) };
|
||||||
const text = eventText(event);
|
|
||||||
if (text) return { role: "assistant", title: "Codex", text, meta: usageText(event) };
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1157,22 +1190,12 @@ function isCurrentThreadEvent(event: { threadId?: string; thread_id?: string })
|
|||||||
|
|
||||||
function formatLogText(logs: AgentEventLog[], context: AgentLogContext) {
|
function formatLogText(logs: AgentEventLog[], context: AgentLogContext) {
|
||||||
const head = [
|
const head = [
|
||||||
"Infinite Canvas Agent 诊断日志",
|
"Infinite Canvas Agent 诊断",
|
||||||
`Canvas Agent: ${context.endpoint}`,
|
`地址:${context.endpoint}`,
|
||||||
`连接: ${context.connected ? "在线" : context.enabled ? "连接中" : "未启用"}`,
|
`连接:${context.connected ? "在线" : context.enabled ? "连接中" : "未启用"} · 状态:${context.activity}`,
|
||||||
`状态: ${context.activity}`,
|
`消息:${context.messages} · 工具:${context.pendingTool ? toolName(context.pendingTool) : "无"}`,
|
||||||
`waiting: ${context.waiting}`,
|
|
||||||
`sending: ${context.sending}`,
|
|
||||||
`messages: ${context.messages}`,
|
|
||||||
`pendingTool: ${context.pendingTool ? toolName(context.pendingTool) : "none"}`,
|
|
||||||
`logs: ${logs.length}`,
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const body = logs
|
const body = logs.map((item) => `${item.time} ${item.title}${item.text && item.text !== item.title ? ` · ${item.text}` : ""}`).join("\n");
|
||||||
.map((item, index) => {
|
|
||||||
const detail = item.raw == null ? item.text : JSON.stringify(item.raw, null, 2);
|
|
||||||
return [`#${index + 1} ${item.time} ${item.title}`, detail].filter(Boolean).join("\n");
|
|
||||||
})
|
|
||||||
.join("\n\n---\n\n");
|
|
||||||
return [head, body || "暂无事件日志"].join("\n\n");
|
return [head, body || "暂无事件日志"].join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1180,37 +1203,37 @@ function formatLogJson(logs: AgentEventLog[], context: AgentLogContext) {
|
|||||||
return JSON.stringify({ context, logs: logs.map(({ time, title, text, raw }) => ({ time, title, text, raw })) }, null, 2);
|
return JSON.stringify({ context, logs: logs.map(({ time, title, text, raw }) => ({ time, title, text, raw })) }, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventText(event: AgentEventPayload) {
|
function formatAgentEventLog(event: AgentEventPayload) {
|
||||||
return event.type === "item.completed" && event.item?.type === "agent_message" ? stringText(event.item.text) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function usageText(event: AgentEventPayload) {
|
|
||||||
const usage = event.usage;
|
|
||||||
if (!usage || typeof usage !== "object") return undefined;
|
|
||||||
const total = numberField(usage, "total_tokens");
|
|
||||||
const input = numberField(usage, "input_tokens");
|
|
||||||
const output = numberField(usage, "output_tokens");
|
|
||||||
if (total) return `${total} tok`;
|
|
||||||
if (input || output) return `${input || 0}/${output || 0} tok`;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventTitle(event: AgentEventPayload) {
|
|
||||||
const item = event.item;
|
const item = event.item;
|
||||||
if (event.type === "thread.started") return "已创建 Codex 会话";
|
if (event.type === "thread.started") return { title: "创建会话", text: shortId(event.thread_id) };
|
||||||
if (event.type === "turn.started") return "开始处理";
|
if (event.type === "turn.started") return { title: "开始处理", text: shortId(event.turn_id) };
|
||||||
if (event.type === "turn.completed") return "本轮完成";
|
if (event.type === "turn.completed") return { title: "处理完成", text: turnSummary(event) };
|
||||||
if (event.type === "stream.summary") return "流式摘要";
|
if (event.type === "turn.failed" || event.type === "error") return { title: "处理失败", text: event.message || event.error?.message || "未知错误" };
|
||||||
if (event.type === "turn.failed" || event.type === "error") return "本轮失败";
|
if (event.type === "item.started" && isMcpToolItem(item)) return { title: "调用工具", text: toolName(String(item?.tool || "")) };
|
||||||
if (event.type === "item.started" && isMcpToolItem(item)) return `调用工具:${toolName(String(item?.tool || ""))}`;
|
if (event.type === "item.completed" && isMcpToolItem(item)) return { title: item.error ? "工具失败" : "工具完成", text: `${toolName(String(item?.tool || ""))}${item.error?.message ? ` · ${item.error.message}` : ""}` };
|
||||||
if (event.type === "item.completed" && isMcpToolItem(item)) return `工具完成:${toolName(String(item?.tool || ""))}`;
|
if (event.type === "item.completed" && item?.type === "agent_message") return { title: "收到回复", text: compactText(stringText(item.text)) };
|
||||||
if (event.type === "item.completed" && item?.type === "agent_message") return "Codex 回复";
|
return null;
|
||||||
return event.type || "Codex 事件";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldLogAgentEvent(event: AgentEventPayload) {
|
function turnSummary(event: AgentEventPayload) {
|
||||||
const itemType = event.item?.type || "";
|
return event.duration_ms ? `${(event.duration_ms / 1000).toFixed(1)} 秒` : "完成";
|
||||||
return !["item.updated"].includes(event.type || "") && !["reasoning"].includes(itemType) && !(event.type === "item.started" && itemType === "agent_message");
|
}
|
||||||
|
|
||||||
|
function eventUsage(event: AgentEventPayload): AgentTokenUsage {
|
||||||
|
return {
|
||||||
|
input: numberField(event.usage, "input_tokens"),
|
||||||
|
cached: numberField(event.usage, "cached_input_tokens"),
|
||||||
|
output: numberField(event.usage, "output_tokens"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortId(value?: string) {
|
||||||
|
return value ? value.slice(0, 8) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactText(value: string, maxLength = 120) {
|
||||||
|
const text = value.replace(/\s+/g, " ").trim();
|
||||||
|
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConnectionErrorMessage(item: AgentChatItem) {
|
function isConnectionErrorMessage(item: AgentChatItem) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type AgentEventLog = { id: string; time: string; title: string; text: str
|
|||||||
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } & Record<string, unknown> };
|
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } & Record<string, unknown> };
|
||||||
export type AgentCanvasContext = { snapshot: CanvasAgentSnapshot; applyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; undoOps: () => CanvasAgentSnapshot | null; canUndo: boolean };
|
export type AgentCanvasContext = { snapshot: CanvasAgentSnapshot; applyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; undoOps: () => CanvasAgentSnapshot | null; canUndo: boolean };
|
||||||
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
|
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
|
||||||
|
export type AgentTokenUsage = { input: number; cached: number; output: number };
|
||||||
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
|
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
|
||||||
|
|
||||||
const CONNECT_TIMEOUT_MS = 6000;
|
const CONNECT_TIMEOUT_MS = 6000;
|
||||||
@@ -31,6 +32,7 @@ type AgentStore = {
|
|||||||
sending: boolean;
|
sending: boolean;
|
||||||
waiting: boolean;
|
waiting: boolean;
|
||||||
messages: AgentChatItem[];
|
messages: AgentChatItem[];
|
||||||
|
tokenUsage: AgentTokenUsage | null;
|
||||||
eventLogs: AgentEventLog[];
|
eventLogs: AgentEventLog[];
|
||||||
threads: AgentThreadSummary[];
|
threads: AgentThreadSummary[];
|
||||||
activeThreadId: string;
|
activeThreadId: string;
|
||||||
@@ -71,6 +73,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
sending: false,
|
sending: false,
|
||||||
waiting: false,
|
waiting: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
tokenUsage: null,
|
||||||
eventLogs: [],
|
eventLogs: [],
|
||||||
threads: [],
|
threads: [],
|
||||||
activeThreadId: "",
|
activeThreadId: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user