Craft Agents v0.9.6 版本解析:多窗口标题、Markdown 预览与凭据生命周期修复
2026/9/17 16:13:47 网站建设 项目流程

Craft Agents v0.9.6 版本解析:多窗口标题、Markdown 预览与凭据生命周期修复

【免费下载链接】craft-agents-oss项目地址: https://gitcode.com/GitHub_Trending/cr/craft-agents-oss

本文是 Craft Agents 桌面端 v0.9.6 的技术发布说明深度解析。该版本围绕"多窗口窗口状态在自动更新后幸存"这一核心痛点,同时引入了markdown-preview代码块用于在聊天中内联渲染.md文件,并修复了 API 来源凭据过期、URL 安全、Anthropic prompt cache TTL 排序、移动端 WebUI 布局等一批与日常使用强相关的问题。读完本文,你将理解这些修复背后的底层机制(electron-updater 窗口销毁时序、凭据取值时机、URL 消毒链路、cache_control 块处理顺序),并掌握markdown-preview的完整配置方式。

新特性:多窗口下窗口标题显示工作区名称

v0.9.6 之前,所有 Craft Agents 窗口的标题都固定为应用名。当用户同时打开多个工作区窗口时,在 Cmd-Tab、Mission Control 或 Windows 任务栏中很难区分它们。

本次更新引入了一套窗口标题策略:单窗口时标题保持应用名("Craft Agents"),打开第二个窗口后,每个窗口的标题切换为其所属工作区的名称。若窗口的工作区无法解析(例如 onboarding 窗口),则回退到应用名。

该策略由主进程统一驱动,实现在 window-manager.ts 的refreshWindowTitles()方法中:方法遍历所有受管窗口,当this.windows.size > 1时尝试通过getWorkspaceByNameOrId(workspaceId)解析工作区名称并调用window.setTitle(title)

关键细节是渲染进程的静态<title>标签被抑制createWindow()中禁用了渲染进程的page-title-updated事件,否则 HTML 里的静态标题会在主进程setTitle()之后再次覆盖标题。该策略会在窗口创建(注册后)、窗口关闭(移除后)以及窗口内工作区切换等时机反复应用,确保标题始终反映当前窗口数量与工作区归属。

新特性:markdown-preview代码块

与现有预览块家族对齐

v0.9.6 新增了markdown-preview代码块类型,与已有的html-previewpdf-previewimage-preview形成完整的"磁盘文件预览"家族。它们遵循同一原则:文件已在磁盘上,只需引用绝对路径,无需提取步骤

块类型适用内容渲染方式
markdown-preview.md文件(规格、草稿、README)通过共享 Markdown 渲染器内联渲染
html-preview邮件、简报、带样式的 HTML沙箱 iframe + 完整 CSS
pdf-previewPDF 文档、报告首屏内联,全屏内完整导航
image-preview截图、捕获画面内联图片 + 全屏查看器
datatable/spreadsheet结构化数据可交互的排序/过滤表格

基本用法

单文件模式(src为必填,title可选):

```markdown-preview { "src": "/absolute/path/to/file.md", "title": "Spec draft" } ```

多文件模式(items数组,标签栏切换):

```markdown-preview { "title": "Spec drafts", "items": [ { "src": "/path/to/v1.md", "label": "v1" }, { "src": "/path/to/v2.md", "label": "v2" }, { "src": "/path/to/final.md", "label": "Final" } ] } ```

配置字段

字段必填类型说明
src是*string磁盘上.md文件的绝对路径(单文件模式)
titlestring头部栏显示的标题(默认 "Markdown Preview")
items是*arraysrc与可选label的条目数组(多文件模式)
items[].srcstring.md文件的绝对路径
items[].labelstring标签文本(默认 "Item 1"、"Item 2"…)

*src(单文件)与items(多文件)至少提供一个;两者同时存在时items优先。

底层实现:解析与归一化

该功能的 JSON 规格解析与归一化逻辑被抽取为纯函数,便于脱离 React 进行单元测试,位于 markdown-preview-helpers.ts:

  • parseMarkdownPreviewSpec(code):解析 JSON 字符串,返回MarkdownPreviewSpec。JSON 非法、或src缺失且items为空时返回nullitems数组中会过滤掉缺少非空字符串src的条目。
  • normalizePreviewItems(spec):把单文件规格包装为单元素数组,使组件可以统一迭代;items存在时优先于src,与html-preview/pdf-preview等兄弟块保持一致的规格形状。

渲染行为与递归防护

  • 文件内容经过与聊天相同的共享 Markdown 渲染器:GFM 表格、语法高亮代码块、标题、列表、引用、行内数学均可用;
  • 内联预览最高 400px,超出部分底部淡出渐变,右上角展开按钮可在原地扩展高度;
  • 渲染文件内部的markdown-preview围栏会退化为普通代码块,不会无限递归;文件中内嵌的其他预览块(mermaid、datatable 等)仍正常渲染——这就是发布说明中提到的disablePreviewBlocks守卫:它只禁用"markdown-preview 内的 markdown-preview",不影响其他嵌套预览块;
  • 渲染出的 Markdown 内链接走与聊天其余部分相同的处理链路:文件路径经 OS 文件管理器打开,URL 在系统浏览器中打开;
  • 多文件模式下内容在切换标签时懒加载,加载后缓存。

路径安全边界

src必须是绝对路径,且与其他预览块走相同的路径校验:用户主目录、系统 tmp 目录、工作区目录内的路径被接受;范围外的任意绝对路径被拒绝。Agent 为预览而写文件时仍需遵守当前权限模式(例如 Explore 模式下写入局限于会话的plansFolderPath/dataFolderPath)。

何时该用、何时不该用

  • 该用:刚写完一个.md文件想展示渲染效果;用户引用了 markdown 文件(README、规格、笔记、计划);想展示含表格/代码/链接/标题的富文本内容。
  • 不该用:内容是 HTML(用html-preview);结构化数据(用datatable/spreadsheet);PDF(用pdf-preview);用户想编辑文件(应使用标准的 Read/Edit/Write 工具)。

常见故障排查

现象原因与对策
一直显示 "Loading..."src不是绝对路径;文件不存在于该路径;路径不在允许目录(home/tmp/workspace)内
预览区空白文件为空;文件虽带.md扩展名但不是文本/Markdown 内容
渲染为原始 JSON 代码块JSON 规格非法;srcitems同时缺失/为空;src不是字符串、items不是含src字段的对象数组
渲染出的链接打不开普通https://走系统浏览器;绝对文件路径走 OS 文件管理器;file://被应用内 URL 安全层拦截(见下文)

更完整的用法、决策树与故障排查指南参见 markdown-preview.md。

关键修复一:多窗口状态在自动更新后幸存

这是 v0.9.6 最重要的修复之一。问题根源:electron-updater(Squirrel.Mac)会在quitAndInstallbefore-quit事件之间销毁所有 BrowserWindow。原有的窗口状态保存逻辑挂在before-quit上,此时拿到的是空快照,于是用{ windows: [] }覆盖了~/.craft-agent/window-state.json——用户每接受一次更新就丢失一次多窗口布局。

修复方案:见 auto-update.ts:

  1. 新增setBeforeUpdateQuitHook(fn)导出,允许主进程注册一个回调;
  2. installUpdate()在调用autoUpdater.quitAndInstall(false, true)之前执行该回调(beforeUpdateQuitHook?.()),此时窗口仍然存在,快照到的窗口状态是完整的;
  3. 延迟的before-quit路径增加空快照守卫,防止预更新保存被后续空快照覆盖。

installUpdate中还记录了诊断日志installUpdate pre-quitelectronWindowCountdownloadStatelatestVersion),用于与before-quit[update-flow]日志做相关性对比:如果两侧窗口数量不一致,就说明 electron-updater 正在销毁窗口,从而确认多窗口恢复问题。

关键修复二:API 来源凭据的会话中刷新与清理

陈旧凭据导致 401

此前,携带 bearer/header/query/basic 认证的来源(source)会在工具创建时把凭据固化为静态字符串。当令牌过期、用户通过source_credential_prompt刷新后,进程内的工具仍然发送旧值(直到完整重启会话都报 401),尽管source_test已确认新令牌可用。

v0.9.6 让非 OAuth 的 API 来源改走凭据 getter:每次调用都从凭据库(vault)读取当前值,与既有的 OAuth / renew-endpoint 路径保持一致。OAuth 与 renew-endpoint 来源不受影响——它们本来就有TokenRefreshManager负责刷新。相关实现见 credential-manager.ts 与 token-refresh-manager.ts,回归测试见 api-tools-credential-freshness.test.ts。

authType 翻转为 'none' 时的孤儿凭据

SourceCredentialManager.getCredentialId()会把'none''header''query'映射到同一个source_apikey槽位。若来源从携带凭据的 authType 切到'none',旧凭据仍残留在该槽位下,重建时可能覆盖defaultHeaders——裸凭据值会被当作 Cookie 头发送,而非新的defaultHeaders.Cookie值。

修复:saveSourceConfig在"新配置为 API 来源且authType:'none'"时,尽力删除source_apikey槽位;清理逻辑永不抛错、永不阻塞配置写入(best-effort)。对应回归测试见 save-source-config-orphan-credential.test.ts。

关键修复三:URL 安全——说明原因 + DOM href 消毒

此前,当 react-markdown 的defaultUrlTransformfile:/javascript:URL 剥离为空时,锚点处理器回退到锚点文本,new URL(text)报 "Invalid URL"——用户只看到一条没有解释的通用 toast。

v0.9.6 的改进:

  1. DANGEROUS_SCHEMES从集合升级为Map<scheme, reason>,见 url-safety.ts。每条危险 scheme 都附带原因,例如file:的说明是"file: URLs are blocked because shell.openExternal can launch local executables on Windows (Electron RCE class)",并建议改用应用内的预览块(html-preview、pdf-preview、image-preview、markdown-preview)或从 OS 文件管理器打开;
  2. 原因字符串随OPEN_URL处理器贯穿server-core与 Electron GUI 两层,错误消息现在形如URL blocked (file:). file: URLs are blocked because…
  3. DOM 的href属性同样经defaultUrlTransform消毒,对危险 scheme 置为undefined——堵住了通过 ElectronsetWindowOpenHandlerwill-navigate的中键/⌘-点击逃逸路径。

该修复对应 issue #807 的 URL 处理部分。

关键修复四:cache_control1h TTL 排序 bug 与误分类修复

启用extendedPromptCache(Anthropic 连接,会话启动时)后暴露出两个相互关联的 bug:

Bug 1 — TTL 排序upgradePromptCacheTtl遍历了 system、messages 与顶层cache_control,却跳过了body.tools。Anthropic 按tools → system → messages的顺序处理块,并拒绝"ttl='1h'出现在ttl='5m'之后"的请求。因此只要任一工具上残留 5m,请求就会报system.0.cache_control.ttl: a ttl='1h' cache_control block must not come after a ttl='5m' cache_control block。修复后,升级路径与禁用剥离路径都先遍历 tools

Bug 2 — 误分类parseError因启发式命中 API 提示字符串中的tools字样,把同一个 400 误判为 "Model Does Not Support Tools"。修复删除了过宽的匹配模式,并新增invalid_request_error / 400分支,把通用 Anthropic 400 路由到invalid_request而非unknown_error

测试侧的证据:针对 TTL 排序错误的 400 响应(提示字符串含tools且消息内容为system.0.cache_control.ttl...)在 errors.test.ts 中有专门的用例;schema 测试 unified-network-interceptor.schema.test.ts 则覆盖了启用时升级 tools 块 TTL、禁用时剥离 tools 块 TTL 的两条路径。

关键修复五:移动端 WebUI 发送按钮保持可见

紧凑底栏的旧布局中,每个左侧元素都是shrink-0且外层行没有溢出保护。当自定义端点模型名很长时,375px 视口下行溢出,把发送按钮挤出屏幕(issue #798)。

修复:CompactModelSelector的触发器改为可收缩,并设置min-w-[64px]的最小点击目标;紧凑底栏的子元素包裹进min-w-0 shrink overflow-hidden组,模型标签优先截断,发送按钮锚定右侧。

关键修复六:Headless 服务器自动重试source_activated

[<slug> activated]的重新发送逻辑移入SessionManager.processEvent,使无头部署(WebUI、docker server)与 Electron 渲染器一样串联来源激活。渲染器侧的auto_retryeffect 被移除。

为防止混合版本滚动发布期间(旧渲染器 + 新服务器)重复发送,引入了基于{content, deadlineMs, committed}槽位的2 秒内容匹配去重窗口:首个匹配的sendMessage(服务器定时器或旧版 RPC)赢得并占用该槽位,窗口内的后续匹配被丢弃。重试定时器与待处理槽位在主进程删除会话路径与分支创建回滚路径两处都会被取消(issue #804)。

其他改进

  • 在线文档覆盖:多窗口标题与自动更新恢复行为已写入 workspaces.mdx(注:该目录在当前仓库快照中未包含,可从应用内文档入口查阅相关说明);新手引导引言措辞收紧;
  • 消息网关文档:澄清 0.9.5 回退修复——progressfinal_only模式在运行以工具调用结束、且没有非中间态text_complete时,都会回退到最近的助手文本(而非留下思考气泡或保持沉默);真正空运行仍保持静默;
  • PR 378 review 加固:对 URL 安全、自动重试、凭据清理相关 PR 的评审意见进行跟进,并移除一个掩盖凭据清理回归的泄漏性 source-test 模块 mock。

兼容性说明

v0.9.6 无破坏性变更,所有改动向后兼容。markdown-preview为新增块类型,不影响已有会话、配置与来源定义;自动更新窗口状态恢复只影响更新安装时的状态保存时序;来源凭据刷新与清理改变的是取值时机与存储清理,不改变来源配置的既有字段语义。

【免费下载链接】craft-agents-oss项目地址: https://gitcode.com/GitHub_Trending/cr/craft-agents-oss

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询