feat(agent): upgrade Codex to version 0.146.0 and implement version checks at startup

This commit is contained in:
HouYunFei
2026-07-31 10:15:33 +08:00
parent afc7bfebaf
commit 3c6af91e8d
9 changed files with 1627 additions and 24 deletions
+3
View File
@@ -2,6 +2,9 @@
## Unreleased
+ [优化] Canvas Agent 升级至最新 Codex,启动时检查版本更新,
+ [优化] 统一通过公共日志输出 Info 以上信息,美化转发日志时间。
## v0.12.0 - 2026-07-30
+ [新增] 画布 Agent 支持三档 Codex 权限,并可在对话中审批操作。
+1532
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@basketikun/canvas-agent",
"version": "0.3.0",
"version": "0.3.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -22,8 +22,9 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"@openai/codex": "0.145.0",
"@openai/codex": "0.146.0",
"express": "^5.1.0",
"strip-ansi": "7.2.0",
"winston": "^3.19.0",
"zod": "^3.25.0"
},
+2 -1
View File
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import stripAnsi from "strip-ansi";
import { VERSION } from "../config.js";
import { logger } from "../utils/logger.js";
@@ -43,7 +44,7 @@ export class CodexAppClient {
const client = new CodexAppClient(child, emit);
child.stdout?.on("data", (chunk) => client.read(chunk.toString()));
child.stderr?.on("data", (chunk) => {
const text = chunk.toString();
const text = stripAnsi(chunk.toString()).replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/, "");
logger.warn("Codex app-server stderr", { text });
emit("agent_log", { text });
});
+2
View File
@@ -9,6 +9,7 @@ import type { AgentAttachment, AgentPermissionMode } from "../agent/types.js";
import { CanvasSession } from "../canvas/session.js";
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, type CanvasAgentConfig } from "../config.js";
import { logger } from "../utils/logger.js";
import { checkVersions } from "../version-check.js";
/** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
export function startHttpServer() {
@@ -223,6 +224,7 @@ export function startHttpServer() {
app.listen(port, "127.0.0.1", () => {
console.log("Infinite Canvas Agent");
checkVersions();
console.log(`Local URL: ${config.url}`);
console.log(`Connect token: ${config.token}`);
console.log("Codex MCP is not installed by this command.");
+15 -18
View File
@@ -11,47 +11,44 @@ import {formatDateForFilename} from "./date.js";
export class Logger {
readonly enabled = process.argv.includes("--debug");
readonly filePath = this.enabled ? path.join(os.homedir(), ".infinite-canvas", "logs", `canvas-agent-${formatDateForFilename()}.log`) : "";
private readonly logger: WinstonLogger | null;
private readonly logger: WinstonLogger;
/** 根据命令行 Debug 参数初始化日志输出。 */
/** 普通模式输出 Info 以上日志,Debug 模式额外输出 Debug 并写入文件。 */
constructor() {
if (!this.enabled) {
this.logger = null;
return;
}
fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
const line = format.printf(({level, message, timestamp, details}) => `${timestamp} ${level.toUpperCase()} ${message}${formatDetails(details)}`);
const output = format.combine(format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}), line);
if (this.enabled) fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
this.logger = winston.createLogger({
level: "debug",
level: this.enabled ? "debug" : "info",
transports: [
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.Console({format: output}),
...(this.enabled ? [new transports.File({filename: this.filePath, format: output})] : []),
],
});
}
/** 输出 Debug 级别日志。 */
debug(message: string, details?: unknown) {
if (details === undefined) this.logger?.debug(message);
else this.logger?.debug(message, {details: sanitize(details)});
if (details === undefined) this.logger.debug(message);
else this.logger.debug(message, {details: sanitize(details)});
}
/** 输出 Info 级别日志。 */
info(message: string, details?: unknown) {
if (details === undefined) this.logger?.info(message);
else this.logger?.info(message, {details: sanitize(details)});
if (details === undefined) this.logger.info(message);
else this.logger.info(message, {details: sanitize(details)});
}
/** 输出 Warn 级别日志。 */
warn(message: string, details?: unknown) {
if (details === undefined) this.logger?.warn(message);
else this.logger?.warn(message, {details: sanitize(details)});
if (details === undefined) this.logger.warn(message);
else this.logger.warn(message, {details: sanitize(details)});
}
/** 输出 Error 级别日志。 */
error(message: string, details?: unknown) {
if (details === undefined) this.logger?.error(message);
else this.logger?.error(message, {details: sanitize(details)});
if (details === undefined) this.logger.error(message);
else this.logger.error(message, {details: sanitize(details)});
}
}
+65
View File
@@ -0,0 +1,65 @@
import { execFile, execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { promisify } from "node:util";
import { VERSION } from "./config.js";
import { logger } from "./utils/logger.js";
const require = createRequire(import.meta.url);
const execFileAsync = promisify(execFile);
const CODEX_VERSION = String((require("@openai/codex/package.json") as { version: string }).version);
/** 输出当前版本,并在后台检查 npm 最新版本。 */
export function checkVersions() {
const localCodexVersion = commandVersion("codex");
logger.info("Canvas Agent version", { version: VERSION });
logger.info("Bundled Codex version", { version: CODEX_VERSION });
logger.info("Local Codex version", { version: localCodexVersion || "not found" });
if (!localCodexVersion) {
logger.warn("Local Codex was not found. Install the latest version with: npm install -g @openai/codex@latest");
} else if (localCodexVersion !== CODEX_VERSION) {
logger.warn(`Bundled Codex ${CODEX_VERSION} does not match local Codex ${localCodexVersion}. Keep both current with: npm install -g @openai/codex@latest && npx -y @basketikun/canvas-agent@latest`);
}
void checkLatestVersions(localCodexVersion);
}
/** 查询 npm,提醒升级不再维护的旧版本。 */
async function checkLatestVersions(localCodexVersion: string) {
try {
const [latestAgent, latestCodex] = await Promise.all([
npmVersion("@basketikun/canvas-agent"),
npmVersion("@openai/codex"),
]);
if (isOlder(VERSION, latestAgent)) logger.warn(`Update available: Canvas Agent ${VERSION} -> ${latestAgent}. Run: npx -y @basketikun/canvas-agent@latest`);
if (isOlder(CODEX_VERSION, latestCodex)) logger.warn(`Update available: bundled Codex ${CODEX_VERSION} -> ${latestCodex}. Upgrade Canvas Agent with: npx -y @basketikun/canvas-agent@latest`);
if (localCodexVersion && isOlder(localCodexVersion, latestCodex)) logger.warn(`Update available: local Codex ${localCodexVersion} -> ${latestCodex}. Run: npm install -g @openai/codex@latest`);
} catch {
logger.warn("Unable to check the latest npm versions; startup will continue.");
}
}
/** 读取本机命令输出中的语义版本号。 */
function commandVersion(command: string) {
try {
return execFileSync(command, ["--version"], { encoding: "utf8", timeout: 5_000 }).match(/\d+\.\d+\.\d+/)?.[0] || "";
} catch {
return "";
}
}
/** 读取 npm 包的最新版本。 */
async function npmVersion(name: string) {
const command = process.platform === "win32" ? "npm.cmd" : "npm";
const { stdout } = await execFileAsync(command, ["view", name, "version"], { encoding: "utf8", timeout: 10_000 });
return stdout.trim();
}
/** 比较仅包含数字段的稳定版语义版本。 */
function isOlder(current: string, latest: string) {
const left = current.split(".").map(Number);
const right = latest.split(".").map(Number);
for (let index = 0; index < Math.max(left.length, right.length); index++) {
if ((left[index] || 0) !== (right[index] || 0)) return (left[index] || 0) < (right[index] || 0);
}
return false;
}
+3 -2
View File
@@ -13,8 +13,9 @@ description: 当前版本已实现但仍需人工验证的变更项
- Agent 顶部栏:标题只显示垂直居中的「Agent」,连接、对话、历史、日志、新对话和收起操作应位于同一行;面板较窄时标签仍可横向滚动且操作不应错位。
- Agent Markdown 样式:右侧 Agent 回复中的代码块应为横向占满消息区域、无语言标题和无双层边框的紧凑代码条,单行内容不应保留纵向大面积空白,复制操作仅在悬停时弱化显示;行内代码、链接在浅色和深色主题下应清晰;点击外部链接后应显示中文紧凑确认弹窗,长路径能够正常换行且复制、继续打开和关闭操作可用;点击 `/Users/`、`/home/` 等本地绝对文件路径时应改为提示在系统文件管理器中定位,支持复制路径,且浏览器地址不应跳转为 localhost 文件路径。
- Agent 工具确认模式:右侧面板标题栏不应再显示「工具确认」开关;对话输入框左下方应显示确认模式选择,默认选中「自动确认」,画布写入工具应直接执行;切换为「手动确认」后,画布写入工具应在对话中展示等待确认卡片,并支持批准或拒绝。
- Canvas Agent Codex 升级:启动 Canvas Agent 后应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,实际运行的 Codex CLI 版本应为 0.145.0运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。
- Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应按“时间 级别 消息 详情”的纯文本单行格式输出日志并显示日志文件路径`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。
- Canvas Agent Codex 升级:启动 Canvas Agent 后应输出 Canvas Agent、内置 Codex 和本机 Codex 版本,实际运行的内置 Codex 应为 0.146.0;内置与本机版本不一致、未安装本机 Codex,或 npm 存在更新版本时应显示对应升级提醒,npm 检查失败不应阻止服务启动;Agent 应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。
- Canvas Agent Debug普通启动应通过公共 logger 输出 Info、Warn 和 Error 日志;使用 `npx -y @basketikun/canvas-agent --debug` 启动后还应输出 Debug 日志并显示日志文件路径,终端统一采用“`YYYY-MM-DD HH:mm:ss` 级别 消息 详情”的纯文本单行格式,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;日志中不应出现连接 token 或图片 Data URL 原文。
- Canvas Agent Codex 日志:Codex app-server 输出带颜色或样式控制符及 UTC 时间的 stderr 时,本地日志和网页诊断日志应只显示一份 `YYYY-MM-DD HH:mm:ss` 本地时间和干净文本,不应出现重复 ISO 时间或 ``、`` 等 ANSI 转义内容。
- Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。
- Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { App, Button, Tooltip } from "antd";
import dayjs from "dayjs";
import { Bot, History, MessageSquare, PanelRightClose, PlugZap, Plus, Terminal } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
@@ -673,7 +674,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
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 });
pushEventLog({ id: `${Date.now()}-${Math.random()}`, time: dayjs().format("YYYY-MM-DD HH:mm:ss"), title, text: value, raw });
};
const upsertActivityMessage = (item: AgentChatItem) => {