# devtoys-rs **Repository Path**: liux1224/devtoys-rs ## Basic Information - **Project Name**: devtoys-rs - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-12 - **Last Updated**: 2026-09-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DevToys RS > Windows 桌面开发者工具箱 —— 39 个本地工具 + 本地优先 AI > 技术栈:Tauri v2 + React 19 + TypeScript + Rust | 平台:Windows 10/11 x64 ## 这是什么 一个**从零构建**的 Windows 桌面开发者工具箱:**所有数据处理都在本机完成**, AI 为可选增强且**默认不出网**。 本项目的设计出发点不是"复刻某个已有工具箱",而是回答一个问题: > **给定 Tauri v2 + Rust + TypeScript 这套技术栈,一个开发者工具箱应该长什么样?** 因此每一个设计决策、每一个工具的实现层归属,都由**技术栈的能力边界**推导得出, 而不是照搬任何既有产品的功能列表。 --- ## 三个核心设计 **1. 工具契约是单一事实来源** 一个工具只写一份 zod schema,同时派生出 UI 选项表单、AI Function Calling 参数、 MCP 工具描述、六段式使用说明。详见 [工具契约规范](./docs/TOOL-CONTRACT.md)。 这是工具内核选 TypeScript 而非 Rust 的**唯一但决定性**的理由—— 只有 TS 能做到"一份定义、四处处消费"的类型贯通。 **2. 本地优先,不是"本地可选"** 网关按 `敏感内容 → 用户配置 → 本地可用 → 云端` 顺序路由;敏感内容**永不离机**; 本地模型不可用时请求失败并解释原因,不提供"忽略并上云"的按钮。 详见 [AI 网关设计](./docs/AI-GATEWAY.md)。 **3. 按能力分配实现层** 不是所有逻辑都塞进同一层,而是谁擅长谁做: | 能力 | 归属 | 理由 | |------|------|------| | 文本处理 / 格式化 / 解析 | **TypeScript**(npm 生态) | prettier、sql-formatter、jsonpath-plus 等成熟库直接承担,自研代码极少 | | 大文件 / CPU 密集 / 加密 | **Rust** | 放进 WebView 必然卡顿或 OOM;Rust + rayon 是数量级优势 | | 富文本渲染(Diff / Markdown / 高亮) | **WebView2** | CodeMirror、Shiki、Canvas 是浏览器原生强项 | | 系统集成(凭据 / 剪贴板 / 任务栏) | **Windows 原生 API** | 平台专属能力,不做跨平台抽象 | --- ## 技术栈 | 层 | 选型 | |----|------| | 外壳 / 系统层 | Tauri v2(Rust),WebView2 | | UI | React 19 + TypeScript(strict) + Tailwind v4 + shadcn/ui | | 编辑器 | CodeMirror 6(高亮 / 行号 / Diff merge view) | | 代码高亮 / Markdown | Shiki + markdown-it + DOMPurify | | 工具内核 | TypeScript 纯函数 + **Zod**(契约单一事实来源) | | AI 编排 | Vercel AI SDK(经本地 OpenAI 兼容网关) | | AI 网关 / MCP | Rust:axum + `rmcp` | | 本地向量 | fastembed + sqlite-vec | | 测试 | Vitest(界面用 `react-dom/server` 静态渲染冒烟,不依赖 DOM) | 选型论证、能力矩阵与依赖锁定版本见 [TECH-STACK.md](./docs/TECH-STACK.md)。 --- ## 文档 | 文档 | 内容 | |------|------| | [TECH-STACK.md](./docs/TECH-STACK.md) | 为什么是这套栈、能力矩阵、依赖锁定版本 | | [ARCHITECTURE.md](./docs/ARCHITECTURE.md) | 五层架构、目录结构、数据流、Windows 集成点 | | [TOOL-CONTRACT.md](./docs/TOOL-CONTRACT.md) | **工具契约规范**(最重要)、示例、CI 强制约束 | | **[TOOL-INVENTORY.md](./docs/TOOL-INVENTORY.md)** | **39 个工具权威清单**(id / 分类 / 阶段 / 技术栈依据) | | [AI-GATEWAY.md](./docs/AI-GATEWAY.md) | 本地优先路由、隐私规则、Function Calling、MCP、审计 | | [UI-SPEC.md](./docs/UI-SPEC.md) | 布局规范、尺寸、控件映射、主题与对比度、快捷键 | | [IMPLEMENTATION-PLAN.md](./docs/IMPLEMENTATION-PLAN.md) | S0–S4 施工顺序、验收标准、依赖锁定 | --- ## 开发 ```bash npm install npm run tauri dev # 开发 npm run tauri build # 打包(NSIS,Windows) ``` 环境要求:Node ≥22(AI SDK 7 要求)、Rust ≥1.77、Windows 10/11 + WebView2。 其他命令: | 命令 | 作用 | |------|------| | `npm run typecheck` | `tsc --noEmit`,前端 + 工具内核一起检查 | | `npm run test` | Vitest:内核用例 + 界面冒烟用例 | | `npm run tools:export` | 由 zod 契约生成 `src/generated/tools.json`(对外宿主的工具定义) | | `npm run mcp:bundle` | 把工具内核打成不依赖 Node 的 ESM 多分块(`src-tauri/mcp-bundle/`),供 MCP sidecar 的嵌入式引擎执行 | | `npm run check:contrast` | 遍历深浅两套语义色,逐对断言 WCAG 2.1 AA,不达标即失败 | | `npm run check:bundle` | 首屏(入口 + modulepreload)gzip 预算 240KB,超了报出最大的几块 | | `npm run build` | 依次执行上面全部 + `vite build` | > 首次 `npm install` 若卡住,多为默认源网络问题,可临时换源: > `npm install --registry=https://registry.npmmirror.com`。 --- ## 当前状态 **S2 / S3 全部完成,S4.1 / S4.2 完成,S4.3 进行中(10/17)**:累计 **32 个工具**, 接上了编辑器与两种专用输出视图,落地了剪贴板智能推荐,AI 网关的骨架 / 本地模型探测 / chat 转发 / 隐私路由 / 凭据保管 / 聊天面板 / 工具调用与审批 / 审计日志全部就绪, 并交付了 **MCP Server sidecar**(`devtoys-mcp.exe`,可脱离主程序被 Claude Desktop 一类外部 AI Host 调用)与 **MCP Client**(App 侧可列出并调用外部 Server 的**只读**工具)。 - 转换:`json-yaml` / `timestamp` / `number-base` / `cron-parser` / `json-csv` / `text-case` / `json-to-code` - 编解码:`base64-text` / `url-codec` / `html-entity` / `base64-image` / `gzip` / `jwt-decode` / `cert-decoder` - 格式化:`json-format` / `sql-format` / `xml-format` - 生成:`uuid-generator` / `hash-generator` / `password-generator` / `lorem-ipsum` - 文本:`text-diff` / `escape` / `list-comparer` / `text-analyzer` / `markdown-preview` - 测试:`regex-tester` / `json-path` / `xml-validator` - 图形:`qr-code` / `color-blindness` - 安全:`password-strength` 契约层在这几批里长出了几个新字段,界面继续完全由契约推导: | 字段 | 作用 | |------|------| | `input` 三形态 | `z.string()` 单输入框 / `z.object({...})` 多栏输入 / `z.object({})` 无输入 | | `outputView` | `text`(只读代码)/ `diff`(并排·统一·补丁三态)/ `html-preview`(净化渲染 + 源码切换) | 渲染层是**渐进增强**的:首屏渲染 textarea,挂载后才把 CodeMirror 6 换进来。 语法包、`prettier`、`markdown-it`、Shiki 语法全部按需加载 —— 首屏 gzip **226.6KB / 240KB**, 而"每个工具都能用"这件事不依赖它们加载成功(失败就退回 textarea)。 **剪贴板智能推荐**(S2.5):在任意程序里复制一段 JSON / XML / SQL / YAML / URL / 时间戳 / Base64 …,切回 DevToys 底部就会浮出一条建议,点一下即打开对应工具并把内容填进输入栏。 - 读取在 Rust(Win32 剪贴板序列号探针,`GetClipboardSequenceNumber` 没变就睡), 识别在工具内核(`tool-kernel/src/detect.ts` 纯函数,11 类内容 + 置信度), 界面只订阅一条事件 —— 详见 [ARCHITECTURE §4.4](./docs/ARCHITECTURE.md); - **只推荐注册表里真实存在的工具**:映射表写的是"这个种类该用哪个工具"(意图), 能不能真推给用户由注册表过滤(事实)。S4.3 正好验证了这条设计 —— `jwt` 从一开始就能被识别, 而 `jwt-decode` 直到这一批才注册进来,**注册的那一刻建议自动生效,`detect.ts` 一行没改** (用例把"工具未实现时不弹"与"注册后弹出"两种状态都钉住了); - 识别不出内容时**界面完全安静**(置信度阈值 0.6),自己复制出去的输出也不会反过来触发建议。 **大文件流式哈希**(S2.6 · Rust 能力已就绪,UI 待接):Rust 侧用 1MB 固定缓冲流式读取, 内存占用与文件大小无关 —— 500MB 零文件端到端 6.8s(debug 构建),并带任务栏进度条。 `sha2` 只是转正依赖(本就是 tauri 的传递依赖)。前端桥接 `lib/file-hash.ts` 已就绪, "选文件"的交互(dialog 插件 vs 拖拽取路径)留到 S3/S4 与文件类工具一起接。 **图片编解码**(S2.6 · Rust 能力已就绪,UI 待接):`image` 0.25.6(按需开 png / jpeg / webp / gif / bmp / ico),解码 / 缩放 / 编码全程只有一份像素缓冲, 不把几十 MB 的像素数据搬进 JS 堆。读用魔数嗅探("看起来是 jpg 的可能是 webp"), 写只认输出路径扩展名(写错在编码前就报错、不留半成品)。无损格式往返像素级一致有单测钉住。 前端桥接 `lib/image.ts` 已就绪,"选文件"交互同上留给 S3/S4。 **AI 网关骨架**(S3.1):axum 回环网关随 App 启动,`GET /health` 与 OpenAI 兼容的 `GET /v1/models` 已就绪(模型列表由 S3.2 的 Ollama 探测填充)。端口由 OS 随机分配、 只绑 `127.0.0.1`(不给防火墙弹窗机会),落到 `%APPDATA%\DevToysRS\gateway.json`; 前端拿端口走 `gateway_info` 命令(IPC 主路径),文件留给外部脚本与降级路径。 净新增依赖只有 axum + axum-core(tokio / hyper / tower / http 都是转正)。 **本地模型探测与首次引导**(S3.2):网关每 5s 探测一次本机 Ollama(`127.0.0.1:11434`, 回环不出网),已装模型进入 OpenAI 兼容的 `/v1/models`(`local: true` 标记)。 空态页挂引导卡,覆盖 AI-GATEWAY §3 的三条路径:未装 Ollama → 下载引导; 已装无模型 → 给出可复制的 `ollama pull` 命令(推荐模型随状态下发); 就绪 → 列出模型。reqwest 按 `default-features = false` 转正(无 TLS 后端, 这段探测代码在结构上出不了网)。一键拉取模型随 S3.5 聊天面板接入。 **OpenAI 兼容 chat 转发**(S3.3):`POST /v1/chat/completions` 已就绪 —— 请求转发到 Ollama 自带的 OpenAI 兼容端点,响应(含 SSE 流)字节级透传。**模型解析权在网关**: 前端传 `auto` 即按本地优先策略选模型,请求体的 `model` 永远被网关覆写; 本地无模型时返回 `503 NO_LOCAL_MODEL` 并说明原因,**绝不静默上云**。 任何 OpenAI 生态 SDK/脚本把 baseURL 指到网关端口即可直接使用(AI SDK 的 `streamText` 验收随 S3.5 聊天面板做端到端确认)。 **隐私路由**(S3.4):网关内执行 6 条内容规则(口令词 / 密钥词 / 私钥块 / 中国大陆手机号 / 身份证 / 长 base64-hex 密钥串),命中即强制本地 —— 本地不可用时以专用码 `PRIVACY_REQUIRES_LOCAL` 失败并解释"不会改用云端", 指定云端模型会被 403 拒绝。规则清单见 `GET /v1/privacy`;命中详情经 `x-devtoys-route` / `x-devtoys-privacy` 响应头回传(**证据全程脱敏**, 绝不在错误信息、响应头或日志里回显原文)。规则 7–8(工具声明类)在**工具执行层**落实: 规则 7 裁在工具表上(`localOnly` 工具不进可能上云的请求),规则 8 由 `privacy` 派生的审批配置 让 `consent` 工具先停下来问用户 —— 见上文的「AI 工具调用与审批」。 **凭据保管**(S3.4):云端 API Key 存 **Windows 凭据管理器**(条目名 `DevToysRS/`),不用我们自己的文件 —— 用户可以打开"凭据管理器 → Windows 凭据" **自己看见、自己删除**,隐私承诺因此可被验证。前端命令只有 `secret_set` / `secret_status` / `secret_delete`,**没有 get**:密钥内容永不回传 WebView, 读取是 Rust crate 的内部函数,只给网关的云端路由注入用。 顺带修掉一个会让上面所有承诺失效的真 bug:`reqwest` 默认读取 `HTTP_PROXY` 环境变量, 于是网关发往 **127.0.0.1** 的请求会走用户机器上的代理 —— 本地对话原文被中途转手, 且没有任何报错。两个 HTTP 客户端(chat 与探测)现在都显式 `.no_proxy()`。 (这个 bug 是本机跑测试时撞出来的:沙箱里恰好有 `HTTP_PROXY`,于是"连不通上游应得结构化 502" 的用例拿到的是代理返回的错误页。) **AI 聊天面板**(S3.5):侧边栏底部「AI 助手」展开右侧面板,可以边看工具输出边向**本地模型**提问。 面板用 AI SDK 7 接网关,三段分开 —— provider(`@ai-sdk/openai`,只说 OpenAI 协议)、 agent(`ToolLoopAgent`,管模型 / 系统提示 / 工具循环上限)、transport (`DirectChatTransport`,让 `useChat` 在**进程内**驱动 agent)。**没有为 SDK 新增任何后端路由**: 网关依然只说 OpenAI 协议。升级到 v7 的几处形态变化记在 [AI-GATEWAY §2.4](./docs/AI-GATEWAY.md)(`useChat` 收 `transport`、工具级 `needsApproval` 已废弃 改为智能体级 `toolApproval`)。 - **面板不进首屏**:AI SDK 三个包约 121KB gzip,用 `lazy()` + 默认关闭隔离 —— 首屏 199.8KB,面板自己一整块(`ChatPanel-*.js`)留在懒加载侧; - **路由决策看得见**:网关在每个响应上带 `x-devtoys-route` / `x-devtoys-privacy`, AI SDK 自己消费响应不交出头,所以在 provider 的 `fetch` 上包一层只读这俩头, 面板据此显示「本地优先」或「仅本地 · 风险类型」; - **模型输出按不可信内容处理**:纯函数先转义为 Markdown,再交 DOMPurify 净化 + Shiki 高亮 (围栏语言名也做白名单,防属性注入); - **网关加了跨源白名单**(`gateway/cors.rs`):回环端口任何网页都能发请求, 只放行 Tauri 与本地 dev server 两个来源,**不放 `*`**,否则等于把"用你本机模型跑任意提示" 开放给用户访问的任何网站; - 模型没装好时面板**禁言并说明原因**(与空态页的引导卡同源判断), 不提供"忽略并发到云端"的选项。 **AI 工具调用与审批**(S3.5b):模型能真正调用工具箱里的工具,而不只是聊。 32 个工具的定义**全部从契约派生**(`tool-kernel/src/ai.ts` → `src/lib/ai-tools.ts`), 所以"写一个工具文件 + 登记一行"依然同时喂给了界面表单、内置 AI 与(S4 的)MCP sidecar。 - **三层职责,每层只做一件事**:内核适配层不认识 AI SDK(只谈名字/描述/JSON Schema/隐私级别); SDK 绑定层负责包成 `tool()`;呈现层不持有会话状态。换 SDK 时只动中间那层; - **参数写错不会崩**:`execute` 永远返回结构化结果,模型收到 `INVALID_ARGS`(带字段名与期望值) 自己改;工具真崩了回 `TOOL_CRASHED`,业务错误码原样透出。抛异常等于让整轮对话断掉, 而模型只看得懂返回值; - **工具卡七态**:生成参数 / 执行中 / 待确认 / 已回执 / 完成 / 失败 / 已拒绝,一个不归并 —— "等你点头"和"你拒绝了"对用户是两件事;参数与结果都在卡片里,结果保留换行; - **规则 8(`consent` 需批准)**:审批配置由 `privacy` 派生,`consent` 工具会让循环**停下来问用户**, 点「允许执行」才跑,回执送达后自动续跑。今天名单为空 —— 28 个已实现工具都是纯文本变换, 参数是模型自己写的,弹窗保护不了它已经读过的东西(判定标准见 [TOOL-INVENTORY §5](./docs/TOOL-INVENTORY.md));第一个真正需要它的会是 `file-renamer` 这类有副作用的工具; - **规则 7(`localOnly` 不得进入可能上云的请求)**:裁在**工具表**上而不是执行时 —— 把仅本地工具摆给云端模型等于邀请它把敏感内容写进参数,那时内容已经在提示词里了; 执行前还有第二道兜底; - **链式调用验的是管线**:脚本化假模型驱动 5 工具链(含一个 `localOnly` 工具), 逐条断言执行顺序、工具确实被执行、**每一步的结果都出现在下一次请求里**、 参数纠错不中断、以及审批时循环确实停住。真模型"选得对不对"需要真机跑起 Ollama 才看得到。 **AI 活动审计**(S3.6):每次 AI 调用在**网关侧**留痕,用户能在设置页里看见并一键清空。 - **为什么记**:隐私承诺如果不可验证,就只是文案。用户能自己看见"这台机器上发生过哪些 AI 调用、 分别走了哪条路由",并且自己删掉 —— 与凭据保管选择 Windows 凭据管理器同一个理由; - **记什么**:时间 / 实际模型 / 是否本地 / 路由原因 / 调用过的工具 / token 用量 / 耗时。 **不记对话原文**(证据在网关侧就已脱敏),工具也只记 id 不记参数 —— 审计不该成为新的泄漏面; - **记在哪**:`%APPDATA%\DevToysRS\audit.db`(SQLite,默认保留 30 天), 记录点在**唯一出网通道**上(网关转发路径),不在 WebView 里 —— 写在被审计者一侧等于没写; - **命令面只有"列出"和"全部清空",没有删单条**:能删单条就能悄悄抹掉某一次调用,审计也就不成立了; - **清空是两步**(先问再清):一次误点抹掉的恰好是用户想核对的东西; - **审计是旁路**:写失败不影响这次对话;没走到模型的失败(如本地模型没装)不留痕 —— 审计记的是"发生过什么",不是"谁试过什么"。 **MCP Server sidecar**(S4.1):`devtoys-mcp.exe` 把 32 个工具开放给外部 AI Host (Claude Desktop / 任何 MCP 客户端),**App 没启动也能用**。 - **工具实体怎么跑**是这一步真正的难点:`tools.json` 只有名字与 JSON Schema,没有实现。 三条路线的量化与选定见 [AI-GATEWAY §6.1.1](./docs/AI-GATEWAY.md):最终把内核打成 不依赖 Node 的 ESM 多分块,装进一个**嵌入式 QuickJS** 执行 —— 内核代码一行没改, 改的是它脚下的地面(`TextEncoder` / `Intl` 子集 / `btoa` / `crypto` 由宿主补); - **交付形态是单文件**:分块用 `include_str!` 嵌进二进制,Claude Desktop 的配置里 只需要一行 exe 路径,不需要"旁边还得有个目录"; - **措辞是真值不是语感**:`秒钟` 不是 `秒`、`-2` 天是 `前天`、零偏移是 `GMT+00:00` 而不是裸 `GMT`。`scripts/icu-host-truth.mjs` 用真实 ICU 打出期望值存进 `src-tauri/tests/fixtures/`,由 Rust 用例逐条比对; - **默认不暴露 3 个 `localOnly` 工具**(`hash-generator` / `password-generator` / `password-strength`):工具参数是模型写的,而 MCP 宿主是不是跑在云端我们无法确认 —— 与规则 7 同一条理由。日志会说明跳过了哪几个、怎么用 `--expose-local-only` 打开; - **stdout 只走协议**:stdio 传输把 stdout 当唯一的 JSON-RPC 邮路,一行"顺便打点日志" 就会变成非法帧。所有人类可读输出一律走 stderr(有用例钉住)。 顺带修掉一个**会让密码悄悄变弱**的真 bug:宿主函数最初按 `array.length` 取随机数, 于是 `Uint32Array(1)` 只拿到 1 个字节、每个 32 位槽恒 ≤ 255。`password-generator` 用的正是 `Uint32Array` —— 密码不会变短、不会报错,只会变弱。改成按字节长度取之后, 用"4096 次取样的最大值必须 > 2^24"把它钉住。 **MCP Client**(S4.2):反过来的一步 —— App 侧也能列出并调用**外部** MCP Server 的工具, 方向与 S4.1 正好相反。首期只开口子给**只读**工具。 - **只读只认服务端自己声明的 `readOnlyHint`**:判据是白名单(`== Some(true)`), **没声明注解的一律不暴露**。MCP 规范写明该字段默认 false,所以"没声明就当只读" 等于把所有写工具一次性放进来; - **一次操作一次会话**,不常驻:spawn → 握手 → 列/调 → 关闭,20s / 30s 两级超时兜底。 代价是每次多一次进程启动,换来的是不把子进程句柄塞进 App 的托管状态; - **核对与调用在同一个会话里**(调用路径自己再过一遍只读判据)—— 拆成两次会话 会留下窗口:核对时它是只读的,真调的时候服务端已经换了工具表; - **配置就是 Claude Desktop 那份**:`%APPDATA%\DevToysRS\mcp.json`, `{ "mcpServers": { "": { command, args, env } } }` 可直接粘贴;条目缺 `command` 会整份报错并点名是哪一条(不静默跳过)。界面上同时显示配置文件路径, 与"实际读的那个文件"是同一个来源; - **命令面只收 `serverId`,不收命令字符串** —— 否则 WebView 里任何一段脚本 就成了任意进程启动器。`env` 也只回变量名不回值(那一栏通常就是 API Key), 与凭据保管同一条原则; - 被挡下的工具会**如实报出去**(而不是安静地少几个),界面才能回答 "我配的 Server 有 12 个工具,怎么只显示 3 个"。 首期**不接进 AI 面板的工具循环**:接进去之后,模型的每次工具选择都可能把对话上下文 送到第三方 Server 手里,那条边界要另外论证。先把"能列出、能调用、只读挡得住"做扎实。 **S4.3 批次 1**(2026-09-16):补上 6 个纯内核工具 —— `text-case`(命名风格)、`json-csv`、 `cron-parser`、`gzip`、`jwt-decode`、`password-strength`,累计 **28 个**。 这批全是无副作用的内容变换,因此都能在 MCP sidecar 的嵌入式引擎里照常执行; `password-strength` 是 `localOnly`(密码样本不该离开本机),默认不暴露的仅本地工具因此从 2 个变 3 个。 - **`cron-parser` 最终没引 `cron-parser`**:原计划用 `cron-parser` + `cronstrue`, 但 sidecar 里的 `Intl` 是**刻意的子集**(宿主只实现三种固定 options 形态), 而 `cron-parser` 依赖的 `luxon` 会在第一次算日期时调用 `new Intl.DateTimeFormat()` —— **空 options,被子集拒绝**。把 shim 装进 Node 复现后,7 个用例**全挂(连不带时区的也挂)**, 所以这不是"某个时区不支持",而是这条依赖在 sidecar 里根本跑不起来。与其为了一个库把 shim 扩成"luxon 需要什么就补什么"(子集的纪律是**只实现内核真正用到的东西**), 不如自己写:一个带边界的 Vixie cron 引擎,`*/c`、区间、列表、名字、`@daily` 一类宏都支持, dom 与 dow 同时出现时按规范取 **OR**,Quartz 的 `L/W/#` 与 6 字段表达式**明确报不支持而不是猜**。 **说明文字与执行时间出自同一次解析**,避免"说明看着对、时间其实是另一回事"。 - 顺带把墙钟时间抽成 `tool-kernel/src/tz.ts`(`timestamp` 与新工具共用)—— 偏移量只经 `timeZoneName: "longOffset"` 取,那是 shim 支持的签名,不是碰巧能用的签名。 - **`zxcvbn` 的 800KB 词表没进首屏**:动态 `import()` 之后它是独立分块,构建产物里能直接核实 (入口包内没有 `frequency_lists`)。它的反馈文案只有英文,工具里做了一份中文映射, 未覆盖的条目显示"(未翻译)"而不是悄悄丢掉。 **这批付出的一点体积代价,记在这里**:6 个工具让首屏从 200.0KB 涨到 **215.0KB / 240KB**(gzip, 约 2.5KB/工具),而 S4.3 还剩 11 个工具 —— 按这个斜率预算会被顶到边上。大额来源是自研 cron 引擎 与各工具的 `guide` 文本,**砍说明文字省不了多少**;真要压,方向是把工具实现本身按需加载 (schema 必须在首屏,`run()` 可以后到)。在那之前,这个数字盯着就好,别把预算悄悄调大了事。 > 自研 cron 引擎的语义依据是 Vixie cron 的规范文字,**没有外部真值夹具** —— > 这一点与 ICU 措辞那套"取真实 ICU 真值逐条比"不同,它的用例是自己和自己对齐。 > 想要更高置信度的话,得在真机上拿系统 cron 或第三方实现跑一遍对照。 验证:`tsc --noEmit` 0 错误 · `vite build` 通过 · `vitest run` **615 个用例全通过**(27 文件)· `cargo test --all-targets` **131/131 通过**(lib 92 + MCP 服务面 17 + 宿主 shim 12 + MCP 客户端 10)· `cargo check --all-targets` 0 条代码警告 · `check:contrast` 82 对达标 · `check:bundle` 首屏 215.0KB / 240KB 预算(gzip)。 **批次 2(4 个工具,累计 32)**:`json-to-code` / `cert-decoder` / `qr-code` / `color-blindness`。 这一步正面撞上本项目最硬的一条约束 —— `run()` 是无副作用的纯函数,三个执行端(WebView / sidecar / 测试)必须给出同一份字节,而 QuickJS 里根本没有 Canvas。结论是 **PNG 在内核里自己编** (`tool-kernel/src/png.ts`:1-bit 灰度给二维码、24-bit RGB 给色块,`fflate.zlibSync` 出 zlib 流)—— 代价是几十行编码器,回报是图形工具第一次有了**可逐像素断言**的输出。另外两处坑只有实跑才暴露: `@peculiar/x509` **v2 起不再自带 `reflect-metadata`**(内部 `tsyringe` 在模块求值期就要用, 故加了 `vendor-x509.ts` 做转发),`qrcode-generator` 默认的 `stringToBytes` 按 Latin-1 截断 (中文会**静默编错**,覆写成 `TextEncoder().encode()` 才对)。 验证:`vitest run` **648 个用例全通过**(28 文件)· `cargo test` **131/131** · `check:bundle` 首屏 **226.6KB / 240KB**(gzip;`quicktype-core` 与 `@peculiar/x509` 实测都落在懒加载分块,首屏未受影响)· MCP 分块 14 → 18 块 / 3.72MB。 > **一个必须盯的数字**:批次 1 的斜率是 2.5KB/工具,批次 2 实测 **2.9KB/工具**(更陡), > 而 S4.3 还剩 7 个、余额只剩 13.4KB —— 按此斜率**会被顶穿**。 > 批次 3 动手前应先评估"`run()` 按需加载"(schema 必须留在首屏),而不是等 `check:bundle` 变红。 > 尚未验证:渲染层、剪贴板链路、AI 面板与设置页只有 SSR 冒烟(开发沙箱内无浏览器、无 GUI), > CodeMirror 按键、Shiki 按需加载、Diff 折叠、剪贴板端到端行为、 > **工具卡的出现与审批按钮的点击**、**设置页里看见审计记录并清空后列表变空**, > 以及"在别的程序里复制 → 切回来看见建议 → 点开工具输入已填入" > 需在首次 `npm run tauri dev` 时过一遍; > 网关的 HTTP 行为有 `tower::oneshot` 路由测试,但真机浏览器直连 `127.0.0.1` 的路径同样待真机过; > **真模型对 32 个工具的选择准确率**(链式验收用的是脚本化假模型,验的是管线, > 不是"该调哪个");**sidecar 侧载进 Claude Desktop**;自研 cron 引擎与系统 cron 的外部对照。 下一步:**S4.3 还剩 7 个工具(批次 3,含 Rust 重活)** —— `rsa-keygen` / `checksum` / `image-convert` / `color-picker` / `duplicate-finder` / `file-splitter` / `file-renamer` (累计 39 个)。**动手前必须先定 `privacy` 单值冲突**(`privacy` 是单值枚举 → `file-renamer` 不可能同时是 `localOnly` 与 `consent`),并先评估上一条里的首屏预算。 随后 S4.4 本地语义搜索、S4.5 Windows 集成与打包。 详见 [IMPLEMENTATION-PLAN.md](./docs/IMPLEMENTATION-PLAN.md)。 ## License MIT — 见 [LICENSE](./LICENSE)