feat(agent): extend Codex reasoning effort options to include 'max' and 'ultra', and update related logging and UI components

This commit is contained in:
HouYunFei
2026-07-31 11:12:03 +08:00
parent 9330bdb318
commit 576f7485bf
6 changed files with 52 additions and 57 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ export type CodexTurnError = JsonRecord & { message: string };
export type CodexItem = JsonRecord & { id: string; type: string; text?: string }; export type CodexItem = JsonRecord & { id: string; type: string; text?: string };
export type CodexPlanStep = { step: string; status: "pending" | "inProgress" | "completed" }; export type CodexPlanStep = { step: string; status: "pending" | "inProgress" | "completed" };
export type CodexPlanUpdate = { threadId: string; turnId: string; explanation?: string | null; plan: CodexPlanStep[]; turnStatus?: string }; export type CodexPlanUpdate = { threadId: string; turnId: string; explanation?: string | null; plan: CodexPlanStep[]; turnStatus?: string };
export type CodexReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; export type CodexReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
export type CodexModel = JsonRecord & { export type CodexModel = JsonRecord & {
id: string; id: string;
model: string; model: string;
+7 -5
View File
@@ -148,7 +148,9 @@ 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", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length }); const model = String(req.body?.model || "") || undefined;
const effort = reasoningEffort(req.body?.effort);
logger.info("Codex turn accepted", { threadId: req.body?.threadId, model: model || "default", reasoningEffort: effort || "default", 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 || "");
@@ -176,8 +178,8 @@ export function startHttpServer() {
threadId, threadId,
cwd: workspace.workspacePath, cwd: workspace.workspacePath,
permissionMode: permissionMode(req.body?.permissionMode), permissionMode: permissionMode(req.body?.permissionMode),
model: String(req.body?.model || "") || undefined, model,
effort: reasoningEffort(req.body?.effort), effort,
appEmit: emit, appEmit: emit,
onStart: clientId ? () => session.bindClient(clientId) : undefined, onStart: clientId ? () => session.bindClient(clientId) : undefined,
onThread: (actualThreadId) => { onThread: (actualThreadId) => {
@@ -193,7 +195,7 @@ export function startHttpServer() {
}, },
onTurn: (actualTurnId) => { onTurn: (actualTurnId) => {
turnId = actualTurnId; turnId = actualTurnId;
logger.info("Codex turn started", { threadId, turnId }); logger.info("Codex turn started", { threadId, turnId, model: model || "default", reasoningEffort: effort || "default" });
session.setCodexState({ busy: true, threadId, turnId }); session.setCodexState({ busy: true, threadId, turnId });
}, },
onFinish: () => { onFinish: () => {
@@ -254,7 +256,7 @@ function permissionMode(value: unknown): AgentPermissionMode {
} }
function reasoningEffort(value: unknown): CodexReasoningEffort | undefined { function reasoningEffort(value: unknown): CodexReasoningEffort | undefined {
return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" ? value : undefined; return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max" || value === "ultra" ? value : undefined;
} }
/** 使用当前操作系统的文件管理器定位本地文件。 */ /** 使用当前操作系统的文件管理器定位本地文件。 */
+1 -1
View File
@@ -5,7 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试 # 待测试
- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型,切换模型后强度选项随模型能力更新,刷新页面后保留选择,发送任务时实际使用所选模型和强度。 - Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型且不显示内部审查模型或重复项,切换模型后强度选项随模型能力更新且无空白选项,刷新页面后保留选择,发送任务时实际使用所选模型和强度;本地控制台与右侧「日志」应记录本轮使用的模型和推理强度
- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。 - Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。
- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。 - Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。
- Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。 - Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。
@@ -1,7 +1,8 @@
import { useRef, type ReactNode } from "react"; import { useRef, type ReactNode } from "react";
import { Button, Dropdown, Tooltip } from "antd"; import { Button, Dropdown, Tooltip } from "antd";
import { ArrowUp, Check, ChevronUp, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Sparkles, Square, X } from "lucide-react"; import { ArrowUp, Check, ChevronUp, Cpu, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Square, X } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { canvasThemes } from "@/lib/canvas-theme"; import { canvasThemes } from "@/lib/canvas-theme";
import { isPlainEnterKey } from "@/lib/keyboard-event"; import { isPlainEnterKey } from "@/lib/keyboard-event";
import type { AgentModel, AgentPermissionMode, AgentReasoningEffort } from "@/stores/use-agent-store"; import type { AgentModel, AgentPermissionMode, AgentReasoningEffort } from "@/stores/use-agent-store";
@@ -105,7 +106,7 @@ export function AgentChatComposer({
) : null} ) : null}
{onConfirmToolsChange ? <ToolConfirmationMenu confirmTools={Boolean(confirmTools)} theme={theme} onChange={onConfirmToolsChange} /> : null} {onConfirmToolsChange ? <ToolConfirmationMenu confirmTools={Boolean(confirmTools)} theme={theme} onChange={onConfirmToolsChange} /> : null}
{permissionMode && onPermissionModeChange ? <PermissionModeMenu permissionMode={permissionMode} theme={theme} onChange={onPermissionModeChange} /> : null} {permissionMode && onPermissionModeChange ? <PermissionModeMenu permissionMode={permissionMode} theme={theme} onChange={onPermissionModeChange} /> : null}
{models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? <AgentModelMenu models={models} model={model} reasoningEffort={reasoningEffort} theme={theme} onModelChange={onModelChange} onReasoningEffortChange={onReasoningEffortChange} /> : null} {models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? <AgentModelControls models={models} model={model} reasoningEffort={reasoningEffort} onModelChange={onModelChange} onReasoningEffortChange={onReasoningEffortChange} /> : null}
{left} {left}
</div> </div>
<div className="flex shrink-0 items-center gap-1.5"> <div className="flex shrink-0 items-center gap-1.5">
@@ -121,42 +122,28 @@ export function AgentChatComposer({
); );
} }
function AgentModelMenu({ models, model, reasoningEffort, theme, onModelChange, onReasoningEffortChange }: { models: AgentModel[]; model: string; reasoningEffort: AgentReasoningEffort; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onModelChange: (model: string) => void; onReasoningEffortChange: (effort: AgentReasoningEffort) => void }) { function AgentModelControls({ models, model, reasoningEffort, onModelChange, onReasoningEffortChange }: { models: AgentModel[]; model: string; reasoningEffort: AgentReasoningEffort; onModelChange: (model: string) => void; onReasoningEffortChange: (effort: AgentReasoningEffort) => void }) {
const current = models.find((item) => item.model === model) || models[0]; const current = models.find((item) => item.model === model) || models[0];
return ( return (
<Dropdown <div className="flex min-w-0 items-center gap-1">
trigger={["click"]} <Select value={model} onValueChange={onModelChange}>
placement="topLeft" <SelectTrigger className="h-9 min-w-0 max-w-36 rounded-full border-0 bg-transparent px-2.5 text-xs font-medium shadow-none hover:bg-black/5 focus:ring-0 dark:bg-transparent dark:hover:bg-white/10" title={current.displayName || current.model} aria-label="选择 Codex 模型">
menu={{ <Cpu className="size-3.5 shrink-0 opacity-70" />
items: [ <span className="min-w-0 flex-1 truncate text-left">{current.displayName || current.model}</span>
{ </SelectTrigger>
key: "model", <SelectContent data-canvas-no-zoom position="popper" side="top" align="start" sideOffset={6} className="z-[1200] w-64 rounded-xl border border-border/70 bg-popover p-1 shadow-xl">
label: "模型", {models.map((item) => <SelectItem key={item.model} value={item.model}>{item.displayName || item.model}</SelectItem>)}
children: models.map((item) => ({ </SelectContent>
key: item.model, </Select>
label: <PickerOption title={item.displayName || item.model} selected={item.model === model} />, <Select value={reasoningEffort} onValueChange={(value) => onReasoningEffortChange(value as AgentReasoningEffort)}>
onClick: () => onModelChange(item.model), <SelectTrigger className="h-9 rounded-full border-0 bg-transparent px-2.5 text-xs font-medium shadow-none hover:bg-black/5 focus:ring-0 dark:bg-transparent dark:hover:bg-white/10" aria-label="选择推理强度">
})), <span>{effortLabels[reasoningEffort]}</span>
}, </SelectTrigger>
{ <SelectContent data-canvas-no-zoom position="popper" side="top" align="start" sideOffset={6} className="z-[1200] min-w-32 rounded-xl border border-border/70 bg-popover p-1 shadow-xl">
key: "effort", {current.supportedReasoningEfforts.map((item) => <SelectItem key={item.reasoningEffort} value={item.reasoningEffort}>{effortLabels[item.reasoningEffort]}</SelectItem>)}
label: "推理强度", </SelectContent>
children: current.supportedReasoningEfforts.map((item) => ({ </Select>
key: item.reasoningEffort, </div>
label: <PickerOption title={effortLabels[item.reasoningEffort]} selected={item.reasoningEffort === reasoningEffort} />,
onClick: () => onReasoningEffortChange(item.reasoningEffort),
})),
},
],
}}
>
<button type="button" className="flex h-9 min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition hover:bg-black/5 dark:hover:bg-white/10" style={{ color: theme.node.text }} aria-label="选择 Codex 模型和推理强度">
<Sparkles className="size-3.5 shrink-0" />
<span className="max-w-28 truncate">{current.displayName || current.model}</span>
<span className="shrink-0 opacity-55">{effortLabels[reasoningEffort]}</span>
<ChevronUp className="size-3 shrink-0 opacity-50" />
</button>
</Dropdown>
); );
} }
@@ -166,17 +153,10 @@ const effortLabels: Record<AgentReasoningEffort, string> = {
medium: "中", medium: "中",
high: "高", high: "高",
xhigh: "极高", xhigh: "极高",
max: "最高",
ultra: "Ultra",
}; };
function PickerOption({ title, selected }: { title: string; selected: boolean }) {
return (
<div className="flex min-w-48 items-center gap-3 py-0.5">
<span className="min-w-0 flex-1 text-sm font-medium">{title}</span>
{selected ? <Check className="size-4 shrink-0" /> : null}
</div>
);
}
function PermissionModeMenu({ permissionMode, theme, onChange }: { permissionMode: AgentPermissionMode; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (permissionMode: AgentPermissionMode) => void }) { function PermissionModeMenu({ permissionMode, theme, onChange }: { permissionMode: AgentPermissionMode; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (permissionMode: AgentPermissionMode) => void }) {
const current = permissionOptions.find((item) => item.key === permissionMode) || permissionOptions[0]; const current = permissionOptions.find((item) => item.key === permissionMode) || permissionOptions[0];
return ( return (
+17 -4
View File
@@ -61,6 +61,8 @@ import { AgentPanelTabs } from "./agent-panel-tabs";
const MAX_ATTACHMENTS = 6; const MAX_ATTACHMENTS = 6;
const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024; const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024;
const DEFAULT_AGENT_URL = "http://127.0.0.1:17371"; const DEFAULT_AGENT_URL = "http://127.0.0.1:17371";
const AGENT_REASONING_EFFORTS = new Set<AgentReasoningEffort>(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
const AGENT_REASONING_LABELS: Record<AgentReasoningEffort, string> = { minimal: "最低", low: "轻度", medium: "中", high: "高", xhigh: "极高", max: "最高", ultra: "Ultra" };
type AgentWorkspace = { workspacePath: string; activeThreadId?: string }; type AgentWorkspace = { workspacePath: string; activeThreadId?: string };
type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] }; type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] };
@@ -285,15 +287,24 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
useEffect(() => { useEffect(() => {
if (!connected) return; if (!connected) return;
void fetchAgentJson<AgentModelsResponse>(endpoint, token, "/agent/codex/models").then(({ data = [] }) => { void fetchAgentJson<AgentModelsResponse>(endpoint, token, "/agent/codex/models").then(({ data = [] }) => {
if (!data.length) return; const names = new Set<string>();
const models = data.flatMap((item) => {
const name = item.displayName || item.model;
const efforts = item.supportedReasoningEfforts.filter(({ reasoningEffort }) => AGENT_REASONING_EFFORTS.has(reasoningEffort));
if (item.model === "codex-auto-review" || names.has(name) || !efforts.length) return [];
names.add(name);
const defaultReasoningEffort = efforts.some((effort) => effort.reasoningEffort === item.defaultReasoningEffort) ? item.defaultReasoningEffort : efforts[0].reasoningEffort;
return [{ ...item, supportedReasoningEfforts: efforts, defaultReasoningEffort }];
});
if (!models.length) return;
const savedModel = useAgentStore.getState().model; const savedModel = useAgentStore.getState().model;
const current = data.find((item) => item.model === savedModel) || data.find((item) => item.isDefault) || data[0]; const current = models.find((item) => item.model === savedModel) || models.find((item) => item.isDefault) || models[0];
const savedEffort = useAgentStore.getState().reasoningEffort; const savedEffort = useAgentStore.getState().reasoningEffort;
const efforts = current.supportedReasoningEfforts.map((item) => item.reasoningEffort); const efforts = current.supportedReasoningEfforts.map((item) => item.reasoningEffort);
const nextEffort = efforts.includes(savedEffort as AgentReasoningEffort) ? savedEffort as AgentReasoningEffort : current.defaultReasoningEffort || efforts[0]; const nextEffort = efforts.includes(savedEffort as AgentReasoningEffort) ? savedEffort as AgentReasoningEffort : current.defaultReasoningEffort || efforts[0];
localStorage.setItem("canvas-agent-model", current.model); localStorage.setItem("canvas-agent-model", current.model);
localStorage.setItem("canvas-agent-reasoning-effort", nextEffort); localStorage.setItem("canvas-agent-reasoning-effort", nextEffort);
setAgentState({ models: data, model: current.model, reasoningEffort: nextEffort }); setAgentState({ models, model: current.model, reasoningEffort: nextEffort });
}).catch((error) => addEventLog("读取模型列表失败", error)); }).catch((error) => addEventLog("读取模型列表失败", error));
}, [connected, endpoint, setAgentState, token]); }, [connected, endpoint, setAgentState, token]);
@@ -333,7 +344,9 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
setAgentState({ activeThreadId: threadId, tokenUsage: null }); setAgentState({ activeThreadId: threadId, tokenUsage: null });
} }
if (files.length) void saveAgentUserMessage(threadId, { id: messageId, role: "user", text: userText, historyText: requestPrompt, attachments: files }).catch(() => undefined); if (files.length) void saveAgentUserMessage(threadId, { id: messageId, role: "user", text: userText, historyText: requestPrompt, attachments: files }).catch(() => undefined);
addEventLog("发送任务", `${compactText(text) || "仅附件"}${files.length ? ` · 附件 ${files.length}` : ""}`); const modelName = models.find((item) => item.model === model)?.displayName || model || "默认模型";
const effortName = reasoningEffort ? AGENT_REASONING_LABELS[reasoningEffort] : "默认强度";
addEventLog("发送任务", `${modelName} · ${effortName}${files.length ? ` · 附件 ${files.length}` : ""} · ${compactText(text) || "仅附件"}`);
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",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
+1 -1
View File
@@ -8,7 +8,7 @@ export type AgentChatItem = { id: string; role: AgentChatRole; title?: string; t
export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown }; export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown };
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 AgentPermissionMode = "request" | "automatic" | "full"; export type AgentPermissionMode = "request" | "automatic" | "full";
export type AgentReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; export type AgentReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
export type AgentModel = { export type AgentModel = {
id: string; id: string;
model: string; model: string;