feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54975 -1852

No files matched your search

+7
View File
@@ -9,3 +9,10 @@ release/
.env.local .env.local
.DS_Store .DS_Store
*.local *.local
resources/sandbox/win32-x64/*.exe
resources/sandbox/win32-x64/manifest.json
resources/sandbox/win32-x64/git/
resources/omp/omp.exe
resources/omp/LICENSE
resources/omp/THIRD-PARTY-NOTICES.txt
resources/omp/*.part
+17
View File
@@ -2,3 +2,20 @@
- 调研 OMP 与 Pi(pi agent)差异,产出 `docs/omp与pi-agent对比调研-2026-10-17.md`。 - 调研 OMP 与 Pi(pi agent)差异,产出 `docs/omp与pi-agent对比调研-2026-10-17.md`。
- 关键结论:omp = oh-my-pi(can1357/oh-my-pi,Stencil Labs,Pi 的 fork,上游最新 18.11.2);pi = Mario Zechner 的上游 Pi(repo 已迁至 earendil-works/pi,npm @earendil-works/pi-coding-agent v1.3.0)。两者 RPC 协议同源(NDJSON over stdio),omp 走"全内置"路线(31 工具、Rust 原生核心、子代理/计划/审批/MCP),pi 走极简可扩展路线(4 内置工具、无子代理/计划/内置权限)。YCode 现内核 omp.exe 18.6.3 属 omp,非上游 Pi。 - 关键结论:omp = oh-my-pi(can1357/oh-my-pi,Stencil Labs,Pi 的 fork,上游最新 18.11.2);pi = Mario Zechner 的上游 Pi(repo 已迁至 earendil-works/pi,npm @earendil-works/pi-coding-agent v1.3.0)。两者 RPC 协议同源(NDJSON over stdio),omp 走"全内置"路线(31 工具、Rust 原生核心、子代理/计划/审批/MCP),pi 走极简可扩展路线(4 内置工具、无子代理/计划/内置权限)。YCode 现内核 omp.exe 18.6.3 属 omp,非上游 Pi。
- 设计并落地 YCode 应用图标:沿用 WorkBuddy 的 squircle + 135° 青绿渐变(`#0DC8A6 → #7ED968`)+ 负空间挖洞眼睛,造型改为奶油色小家伙(短耳=Y 的两臂,脸越出下缘形成探头感)。v13 之前试过 12 版,结论:靠手调贝塞尔画有机形状不稳,改用几何基元(胶囊耳+椭圆脸)后才可控。
- 产出:`build/icon.svg`、`build/icon-small.svg`(32px 以下切短耳熊版,阈值 48)、`build/icon.ico`(7 尺寸)、`build/icon.png`、`build/icon-preview.png`、`resources/icon.png`、`scripts/gen-icon.mjs`,命令 `npm run icon:build`。
- 主进程接线:`appIconPath()` + 三个 BrowserWindow 的 `icon` + `app.setAppUserModelId('com.ycode.app')`。`tsc -p tsconfig.node.json` 通过;真机任务栏效果未验证(本机 Electron GUI 起不来,`ELECTRON_RUN_AS_NODE=1`)。
- 项目遗留:`electron-vite@5` 要求 vite ^5–^7,项目装的是 vite 8,任何 `npm install` 都需 `--legacy-peer-deps`;`electron-builder` 仍未配置,`.exe` 文件图标无法落地。
- 细节记录见 `docs/YCode图标设计-2026-10-17.md`。
- 按用户要求把图标底色从青绿改为 `#2440E8 → #0FB9DE`(靛蓝→青),奶油色同步冷调化为 `#FAFDFF → #E4F1FD`,与 WorkBuddy 的青绿拉开差异;对比过 `#2C5BFF→#21D4E8`(偏软)与紫粉方案(偏消费类)。遗留:应用内 UI 主色仍是 `#00c29a`,图标蓝与界面绿并存,是否统一待定。
- 用户对蓝色不满意,最终定为 `#4F2BE8 → #A94BFF`(紫罗兰→品红),奶油色 `#FCFBFF → #EDE7FE`。选它的依据是奶油脸与底色亮端对比约 4:1,优于珊瑚橙 2.8:1。图标现为紫色、界面仍为绿色,尚未统一。
- 用户再要求「偏红色系」,最终定 `#C8102E → #FF4D6A`(绯红→玫红),奶油色 `#FFFAFA → #FFE8EC`;否决 `#9E0B22→#E23A2E`(太严肃)与 `#E03616→#FF8A4C`(红橙,对比仅 2.6:1)。图标红、界面绿(`#00c29a`)的不一致仍未处理,用户已连续改色四轮,若再改可直接给色板编号让他选。
- 最终版:深红 `#C8102E → #7E0817` + 五颗金星 `#FFE79B → #F0B233`(一大四小,人民/党的意象),构图由「脸占满」改为徽章式——小家伙缩 0.8 倍移到右下、星群占左上;小尺寸版保留一大两小作金色锚点。星群刻意不做成国旗精确构图以降低合规风险;文档已注明国旗法对商标/商业用途的限制。
- 修正:五颗星原先做成了近似同尺寸,顺序与比例都错。现按《国旗制法说明》重算:旗面 30×20,大星中心 (5,5) r3,小星 (10,2)(12,4)(12,7)(10,9) r1,大:小=3:1,小星一角尖对准大星中心(atan2 算旋转)。换算 k=6.4px/单位、原点 (-12,4),大星 r19.2、小星 r6.4;脚本 `scripts/star-layout.mjs`。兔子因此缩到 0.70 移到 (84,110),48px 以下用 0.84 版。背景用「多段透明渐变」模拟旗面光泽(模糊滤镜画褶皱在 64px 会糊成色块,已弃用)。
- 最终形态:**移除兔子,只留深红旗面 + 制式五角星**。k=10、原点 (-8,-4),大星 r30、小星 r10,星色 #F7CE72 @0.86(实测 #EEB367),旗面采样 #810B18。兔子与星群抢位、两头都做不干净,是弃用角色方案的原因。`build/icon-small.svg` 已删除,生成器改为单一矢量源(`pick()` 恒等),16px 下只能读出"红底+一颗星"。文档 `docs/YCode图标设计-2026-10-17.md` 已整体重写。
- 用户要求继续放大并右移取景,做到"第一眼认不出是红旗"。最终 k=16、原点 (-66,-16):大星 r48 被三边裁切,四颗小星从上/右/下各露一角,右侧两颗只剩 5px 尖角。放大时原点要同步左移,否则小星整体出框(k=16/20 配旧原点就会全挤出画面)。若要一眼认出国旗,回退 k=8.8 + 原点 (-2,1)。
- 兔子位置反复:0.74/(84,94) 右脸被圆角切掉 → 0.76/(74,90) → 用户选了「底部居中探头」并要更大 → 最终 **1.0 倍 / 中心 (64,106)**,下巴被边框切掉、只露耳朵额头和眼睛。经验:判断"位置怪"要先看是否被圆角切脸,而不是只看坐标。眼睛改为实心 #B0112A(不再用 mask 洞,洞会透出底下金星)。
- 收尾:用户反馈任务栏里图标偏小。量截图得 WorkBuddy 占 24px 槽、我们只占 20px——原版窗口图标是贴边裁的,不是 PNG 里那 80% 留白。解决:整幅内容包一层 `scale(1.2)`,内容区从 80% 提到约 96.6%。另加 `build/sizes/` 输出 13 档备用 PNG(16~1024)。
- 二次调整:95.5% 仍显小,改为 `scale(1.25)` → 内容 99.4%(满幅)。`resources/tray.png`(32px) 一并产出。
- 接线:主进程新增 `createTray()`(图标缺失只 warn 不抛错,左键唤回窗口,右键"显示/退出");`package.json` 加 electron-builder 的 `build` 段(appId com.ycode.app、win.icon=build/icon.ico、extraResources 复制 icon/tray、`npmRebuild:false` 绕开 node-pty 的 Spectre 编译失败)与 `dist` / `dist:installer` 脚本。用户明确**不要我跑打包**,`.exe` 文件图标需他自己执行 `npm run dist` 才有。
+12 -1
View File
@@ -55,11 +55,22 @@ npm run test:persistence # 实际 IPC → 会话 JSON 的顺序与终态
npm run test:sessions # 并发读写、原子替换与冷加载 npm run test:sessions # 并发读写、原子替换与冷加载
npm run test:omp # OMP 回合终态、取消、错误与监听清理 npm run test:omp # OMP 回合终态、取消、错误与监听清理
npm run test:omp-session # OMP 持久会话参数与进程释放(假子进程) npm run test:omp-session # OMP 持久会话参数与进程释放(假子进程)
npm run test:omp:bundled # 内置 OMP 资产、摘要与 --version 启动(--packaged 检查打包产物)
``` ```
## Windows 打包
完整发布流程见 [Windows 分发包 SOP](docs/打包流程SOP-2026-10-18.md)。
~~~powershell
npm run dist:installer
~~~
该命令会先校验并内置 OMP,再构建主进程/预加载/渲染进程并生成 NSIS 安装程序。当前 Windows x64 测试包未签名。
## 首次运行 ## 首次运行
1. OMP 路径通过 `YCODE_OMP_PATH` 或现有安装探测;使用 `ycode` profile。回退循环的上游地址、模型和 Key 在应用设置中配置;设置尚未同步 OMP profile。 1. OMP 按 `YCODE_OMP_PATH` → 随包 `resources/omp/omp.exe` → 本机 Ygent 安装的顺序探测;随包版本由 `resources/omp/runtime.lock.json` 锁定,`npm run omp:vendor` 负责拉取与校验。使用 `ycode` profile。回退循环的上游地址、模型和 Key 在应用设置中配置;设置尚未同步 OMP profile。
2. 选择**工作目录**;默认模式限制路径、写入需确认,命令执行需要明确开启完全访问。 2. 选择**工作目录**;默认模式限制路径、写入需确认,命令执行需要明确开启完全访问。
3. 回到对话区提问,例如「列出工作目录里有哪些文件」 3. 回到对话区提问,例如「列出工作目录里有哪些文件」
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

+54
View File
@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="1024" height="1024">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#CF1230"/>
<stop offset="1" stop-color="#78060F"/>
</linearGradient>
<linearGradient id="sheen" x1="0" y1="1" x2="1" y2="0">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.15"/>
<stop offset="0.18" stop-color="#FFFFFF" stop-opacity="0"/>
<stop offset="0.33" stop-color="#000000" stop-opacity="0.15"/>
<stop offset="0.47" stop-color="#000000" stop-opacity="0"/>
<stop offset="0.60" stop-color="#FFFFFF" stop-opacity="0.16"/>
<stop offset="0.74" stop-color="#FFFFFF" stop-opacity="0"/>
<stop offset="0.86" stop-color="#000000" stop-opacity="0.18"/>
<stop offset="1" stop-color="#000000" stop-opacity="0.03"/>
</linearGradient>
<linearGradient id="fur" x1="0.2" y1="0" x2="0.8" y2="1">
<stop offset="0" stop-color="#FFFFFF"/>
<stop offset="1" stop-color="#FFECE6"/>
</linearGradient>
<path id="star" d="M 0.0000 -1.0000 L 0.2245 -0.3090 L 0.9511 -0.3090 L 0.3633 0.1180 L 0.5878 0.8090 L 0.0000 0.3820 L -0.5878 0.8090 L -0.3633 0.1180 L -0.9511 -0.3090 L -0.2245 -0.3090 Z"/>
<clipPath id="frame">
<rect x="13" y="13" width="102" height="102" rx="23"/>
</clipPath>
</defs>
<g transform="translate(64 64) scale(1.25) translate(-64 -64)">
<rect x="13" y="13" width="102" height="102" rx="23" fill="url(#bg)"/>
<g clip-path="url(#frame)">
<rect x="13" y="13" width="102" height="102" fill="url(#sheen)"/>
<g fill="#F7CE72" opacity="0.86">
<use href="#star" transform="translate(14.0 64.0) scale(48.0) rotate(-90)"/>
<use href="#star" transform="translate(94.0 16.0) scale(16.0) rotate(239.0)"/>
<use href="#star" transform="translate(126.0 48.0) scale(16.0) rotate(261.9)"/>
<use href="#star" transform="translate(126.0 96.0) scale(16.0) rotate(-74.1)"/>
<use href="#star" transform="translate(94.0 128.0) scale(16.0) rotate(-51.3)"/>
</g>
<g transform="translate(64 106) scale(1) translate(-64 -94)">
<g transform="rotate(-5 64 90)">
<rect x="36" y="24" width="25" height="54" rx="12.5" fill="url(#fur)" transform="rotate(-12 48.5 51)"/>
<rect x="69" y="22" width="23" height="54" rx="11.5" fill="url(#fur)" transform="rotate(12 80.5 49)"/>
<ellipse cx="64" cy="92" rx="34" ry="33" fill="url(#fur)"/>
<rect x="45.5" y="76" width="14" height="23" rx="7" fill="#B0112A" transform="rotate(-8 52.5 87.5)"/>
<rect x="68.5" y="76" width="14" height="23" rx="7" fill="#B0112A" transform="rotate(8 75.5 87.5)"/>
<circle cx="48.6" cy="81.2" r="2.1" fill="#FFFFFF" opacity="0.9"/>
<circle cx="71.6" cy="81.6" r="2.1" fill="#FFFFFF" opacity="0.9"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,76 @@
# 19 项验收问题的首轮修复(2026-10-17)
## 本轮范围
主对话规划、制定跨模块接口、集成及最终验收;三个子 agent 并行执行归档、会话状态、设置及专项测试等边界明确的任务。后续本对话继续采用此模式。
本轮落实前次验收汇总 A01~A12 的对应修复,并补上交叉审查发现的运行中重开、失败事务补偿和用量请求兼容问题。YCode 功能回归通过不等于 WorkBuddy 全状态逐像素 1:1 验收。
安装中心、定时任务、资料库、更多中的项目继续只记录,不拆解。云端服务、产品内子代理调度、用户明确不复制的链接不纳入本轮。保留进入本轮时已有的未提交修改。
## 修复结果
| ID | 修复后行为 | 验证范围 |
| --- | --- | --- |
| A01 | 回合开始及带序号的事件发送到所有应用窗口;同任务双窗共享运行状态。重开窗口从内存快照恢复正文、工具状态与停止入口,并按序号去重。 | 真实独立窗口发送、完成、停止、关窗后后台保存、运行中关窗重开、双窗同步:62/62。纯回归覆盖迟到 started、终态快照和 hydration 期间事件。 |
| A02 | 表格引用快照进入备用内核的后续历史及中断输入恢复;附加上下文随用户消息冻结,避免重放时丢失或使用新设置。 | 持久化回归断言下一次实际 wire history 包含原引用 markdown;IPC 回归和表格 UI 48/48。 |
| A03 | 新建会话后首回合启动失败时,只清理本次创建、主进程在同一会话锁内确认无消息且未运行的空会话。草稿、附件、资源引用和空间选择恢复;失败清理或有真实消息时保留会话并报错。 | 真实创建成功后注入一次启动前拒绝,验证恢复、既有会话不变及原草稿重试;新任务专项 51/51。纯回归另覆盖切换竞态和清理拒绝。 |
| A04 | 记忆归一化尊重声明默认值;只修改一项不会连带开启另一项。编辑保存期间禁用相关操作并显示忙碌态。 | 记忆纯逻辑回归;真实开关点击、独立持久化和重开恢复,UI 25/25。 |
| A05 | 侧栏任务分组增加“已归档”入口,可查看列表、恢复及恢复后打开原消息。确认文案指向可访问入口,不再指向已移除的数据管理。 | 归档列表/加载/失败重试/独立任务与空间任务恢复/重复点击保护:25/25;任务菜单 43/43。此入口是适配用户移除数据管理后的 YCode 交互。 |
| A06 | 有产物变更的消息编辑提交先询问回退或保留当前产物;普通回退也支持仅回退对话。取消保留编辑草稿。失败时补偿文件及 YCode 管理的原生转录文件。 | 消息编辑 UI 74/74;IPC 回归覆盖两种产物选择、原生分支失败和 startTurn 写入失败后文件/历史/转录恢复。 |
| A07 | 工作台内打开菜单时先关闭菜单;工作台内全屏时 Escape 只退出全屏;非全屏生成中 Escape 才取消回复。 | 纯决策测试与真实新任务专项中的键盘事件;组合状态下生成不中断,随后正常取消并落库。原版该组合状态的优先级尚未新采集。 |
| A08 | 扫描未完成时明确显示“统计未完成 / 至少已扫描”,区分下界和上界,不把部分数值作为完整统计。 | 存储展示纯逻辑覆盖完整/部分/边界值;通用设置 UI 43/43。真实超大目录的完整 UI 扫描仍未构造。 |
| A09 | 系统文件管理器返回错误字符串时,IPC 明确拒绝,设置页原有错误反馈能显示失败。 | IPC 注入 openPath 错误并断言拒绝;没有对用户机器的权限或真实系统文件管理器制造故障。 |
| A10 | 欢迎语订阅同时响应当前窗口事件和其他窗口的 storage 事件,卸载时正确清理。 | 订阅纯回归、跨窗口通知专项及个性化 UI 31/31;没有额外采集原版活跃提示条的逐帧画面。 |
| A11 | 页面展示的默认人设与用户自定义人设一样进入请求上下文,明确清空时才不注入。 | 个性化提示词纯回归及真实 IPC 提示词冻结链路。保持当前 userText 注入方式,原版指令层级等价性仍待 REA 证据。 |
| A12 | 进入编辑器前预检历史消息的原生分支可用性;已知不可编辑的旧消息提前反馈原因,避免输入后才失败。服务端提交仍再次检查。 | 真实 UI 验证旧消息不进入编辑器并显示原因;IPC 预检回归。预检不承诺消除提交时的磁盘或分支运行错误。 |
## 交叉审查补强
- 原生转录:失败补偿前先停止写入进程,恢复 YCode 以 `--resume` 指定的本会话 JSONL 字节快照;补偿动作逐个尝试,失败由 AggregateError 保留,不能因一项失败跳过另一项。测试以受控故障覆盖主转录文件恢复;没有证明外部 OMP 任意版本的附加副文件或真实磁盘硬故障均能补偿,不宣称完整原生事务等价。
- Chat Completions:请求标准流式 usage,识别 `choices: []` 的最终用量;非法数值不产生假用量。仅当 400/422 的错误正文明确说明 `stream_options/include_usage` 不支持时,才去掉该字段重试一次。普通 400、401、5xx、SSE 开始后的错误均不重试,AbortSignal 继续有效。
- 模型用量:切换到不同模型后清理旧样本;运行快照可携带当前真实 usage。五类上下文占比没有编造数据。
- 事件竞态:事件序号和快照水位线防止正文重复、旧快照覆盖增量、终态后迟到 started 重新激活回复。终态快照直接呈现完成/取消/错误态。
## 最终验证
构建:`npm run build` 通过,含 typecheck。最终 UI 批次运行时已使用最后一次业务代码构建。
21 个专项脚本最终全部通过:20 份结构化报告合计 **710/710** 条检查,另有 Git 检测 **6/6**。测试中所有真实模型请求均使用隔离本地服务和测试数据,不依赖用户远程服务。主要报告:
| 专项 | 最终结果 | 报告 |
| --- | --- | --- |
| 独立任务窗口/同任务双窗/运行中重开 | 62/62 | `.tmp/task-window-stream-V8VDg1/report.json` |
| 新任务切页/失败恢复/草稿重试/Escape | 51/51 | `.tmp/new-task-switch-5kOLZ1/report.json` |
| 用户消息编辑/预检/产物选择 | 74/74 | `.tmp/edit-yrCjoa/report.json` |
| 归档列表与恢复 | 25/25 | `.tmp/archived-tasks-2ufOR1/report.json` |
| 记忆设置 | 25/25 | `.tmp/memory-settings-lZKMSf/report.json` |
| 个性化设置 | 31/31 | `.tmp/personalization-settings-YazCcp/report.json` |
| 设置导航 | 19/19 | `.tmp/settings-nav-p3OfGW/report.json` |
| Git/上下文输入区 | 47/47 | `.tmp/composer-context-X5taI3/report.json` |
其余任务菜单、运行中任务菜单、表格引用、滚动条、侧栏、账户菜单、通用、两套外观、快捷键、智能体、关于帮助均通过。本轮日志和全部报告路径索引:`.tmp/fix19-20261017/final/summary.json`。
以下纯逻辑/IPC 命令也已通过:
```text
npm run test:conversation-ipc
npm run test:chat
npm run test:persistence
npm run test:conversation
npm run test:assistant-footer
npm run test:personalization
npm run test:workbench-escape
npm run test:remediation
```
Responses 网关原有回归另行通过。新增便捷入口 `test:task-window`、`test:archived-tasks` 指向本轮真实窗口专项。
测试维护修正:导航不再把 memory 当禁用;上下文测试使用当前主题卡片;记忆测试通过真实点击和等待配置落盘验证保存;失败注入等到主进程实际注册 IPC;窗口探针同时订阅 run-started 和增量事件。初次失败日志保留,最终通过结果没有覆盖或隐去它们。
## 仍待验收与使用边界
- 没有新增 WorkBuddy 原版采集;复刻文案和原版结论沿用既有 REA 证据。本轮修复 YCode 已确认缺陷,不推断未知的原版行为。
- 全状态同视口像素、字体字形、过渡帧、原版浮层边界与未知提示词层级仍未完整验收。快捷键自定义持久化、富文本编辑、文件修改过程等未接能力仍保留待办。
- 系统登录项、系统通知设置页、真实文件管理器故障和超大目录扫描没有对用户环境制造真实副作用。
- 构建产物已更新;本轮未重启用户正在运行的 YCode。运行中的主进程需要在下次启动时加载本轮修改。
+127
View File
@@ -0,0 +1,127 @@
# 19 项验收与问题汇总(2026-10-17)
> 最新:A01~A12 已落实本轮功能修复,并完成最终 21 个专项复验;详细范围、报告和限制见[修复记录](19项问题修复记录-2026-10-17.md)。本文保留初次审查发现及其历史证据,不应把下方修复前的问题描述重新计为当前未修项。
## 范围与结论
本轮由主上下文协调三个子 agent,审查复刻待办清单 01~17、19、21 共 19 个条目。主上下文负责构建、串行运行专项脚本、复核关键源码及问题分类;子 agent 按模块审查实现、测试有效性和既有 REA 证据。
安装中心、定时任务、资料库、更多中的项目继续只记录,不纳入本轮拆解。云端服务、子代理调度,以及用户明确不复制的外跳链接不列为本轮应修 Bug。三档消息时间和仅保留浅色/深色是用户约定,不回退成原版默认行为。
**当前不能验收为完整 1:1。** 最新构建及大部分专项通过,但发现现有脚本没有覆盖的业务缺陷、测试维护问题和原版证据缺口。下列结论区分源码确认、实际执行、静态风险与待取证状态。
首轮子 agent 阶段负责验收与问题收集。随后按用户要求改为主对话规划、agent 并行修复,主对话集成并统一运行最终验证;修复状态以最新修复记录为准,原版 1:1 未验收项不因功能修复自动完成。
## 分组报告
- [对话与侧栏:01~07](验收问题收集-2026-10-17-对话侧栏.md)
- [发送与设置:08~13](验收问题收集-2026-10-17-发送与设置.md)
- [记忆与其余条目:14~17、19、21](验收问题收集-2026-10-17-记忆与上下文.md)
各报告保留复现前提、文件行号、已有原版 Evidence、测试盲区和下一步验证方法。主审已将原版行为未知项、源码风险与已确认缺陷分开,并下调没有高影响证据的问题严重度。
## 优先问题
以下问题由当前源码复核确认,除特别说明外尚未新增完整 UI 复现。现有专项通过不覆盖这些条件。
| ID | 优先级/分类 | 条件与影响 | 代码位置 |
| --- | --- | --- | --- |
| A01 | P1 业务缺陷 | 在独立任务窗口发起对话,`chat:event` 仍只发往主窗口。发起窗口收不到流式增量和终态;主窗口没有对应运行记录又会丢弃事件。 | `src/main/index.ts:168`、`src/main/ipc.ts:628`、`src/main/ipc.ts:737`、`src/renderer/src/stores/chat.ts:406` |
| A02 | P2 业务缺陷 | 备用内核重建第二轮历史时遗漏 `message.quotes`,第一轮已引用的表格不再进入上游历史。 | `src/main/ipc.ts:772`、`src/main/ipc.ts:816` |
| A03 | 已修复(YCode 回归) | 创建成功后首回合启动失败时,先检查后端会话与消息、后台运行及缓冲事件;仅在确认无持久化消息且无后台回合时删除空会话并回到真实新任务,否则保留真实失败会话。迟到失败不能覆盖较新的任务选择。 | `src/renderer/src/stores/chat.ts:320`;`scripts/chat-store-regression.ts` |
| A04 | P2 偏好误写 | 空配置首次只切换“对话记忆”,会把未操作的“本地记忆与技能”从默认关闭写为开启。归一化默认值与声明默认值不一致。子 agent 已以纯函数复现;没有数据丢失证据。 | `src/shared/memory.ts:30`、`src/shared/memory.ts:35`、`src/main/memory-settings.ts:34` |
| A05 | 已解决(YCode 范围) | 已在任务分组标题增加“已归档”入口;弹窗仅列 `archivedAt` 有值的任务,支持恢复、查看、空间分组恢复、失败重试和重复点击保护。原版弹窗逐像素证据仍缺。 | `src/renderer/src/components/ArchivedTasksDialog.tsx`、`src/renderer/src/components/Sidebar.tsx`、`src/renderer/src/components/task-confirm-dialog.tsx` |
| A06 | P2 交互缺口 | 编辑带产物的消息直接回退文件,没有原版既有源码/词典所描述的回退选择;回退弹窗也没有“不回退产物”分支。本轮没有新运行原版。 | `src/renderer/src/components/ChatPane.tsx:251`、`src/main/ipc.ts:663`、`src/renderer/src/workbuddy/conversation/rewind-labels.ts:19` |
| A07 | 已修复(YCode 回归) | 菜单优先消费 Escape,其次由焦点位于工作台内且处于全屏时退出全屏;事件被阻止继续冒泡,不再同时触发全局停止生成。原版同组合状态的 REA 证据仍未取得。 | `src/renderer/src/workbuddy/workbench/escape-action.ts`、`src/renderer/src/workbuddy/workbench/SourceWorkbench.tsx`;`scripts/workbench-escape-regression.ts` |
| A08 | P2 统计准确性 | 目录扫描超过条目/时间预算后返回 `partial:true`,界面仍把扫描到的字节数显示为完整统计。 | `src/main/general-settings.ts:80`、`src/main/ipc.ts:418`、`src/renderer/src/components/GeneralSettingsPanel.tsx:173` |
| A09 | P2 错误反馈 | 打开目录没有检查 `shell.openPath()` 的错误字符串,目录不存在等失败无法反馈到界面。 | `src/main/ipc.ts:444` |
| A10 | P2 同步缺口 | 欢迎语设置只派发当前窗口的自定义事件,订阅没有监听另一 BrowserWindow 的 storage 事件;已挂载的流式状态栏缺少可靠的即时刷新通知。实际可见延迟取决于其他重渲染。 | `src/renderer/src/personalization-prefs.ts:25`、`src/renderer/src/personalization-prefs.ts:32`、`src/renderer/src/workbuddy/conversation/StreamingFooter.tsx:110` |
| A11 | P2 展示一致性 | 默认人设显示为对回复生效,但等于默认值时不进入个性化请求片段。这是既定实现选择,UI 与实际注入值不一致;是否由其他内置提示补足尚未证明。 | `src/shared/personalization.ts:113`、`src/renderer/src/components/PersonalizationSettingsPanel.tsx:278` |
| A12 | P2 能力校验 | 部分没有 `nativeEntryId` 的旧消息仍可进入编辑器,用户修改提交后才被后端以缺少原生分支拒绝。 | `src/renderer/src/components/MessageBubble.tsx:79`、`src/main/ipc.ts:667` |
其他缺口或风险没有混入上述生产缺陷:
- 备用 Chat Completions 没有解析上游 `usage`,属于已确认的功能缺口;子 agent 用受控 SSE 纯测试确认只输出 text/finish。
- 用量只按会话保存,切模型后立即换分母,旧用量样本没有模型来源标记;列为准确性风险,未新增真实 UI 复现。
- 自定义快捷键保存/删除/恢复、富文本编辑和文件修改过程没有接入,界面已禁用或说明限制。
- 账户菜单实际头部高度、五个占位图标仍有视觉差异;表格引用来源标题必须补原版宿主证据后再决定文案。
- detached HEAD 在后端检测到短 SHA、UI 保守隐藏,是当前实现限制;原版显示方式未知,不能据此判定为已确认的原版差异。
- 深色 native 窗口底色、浮层碰撞处理、首帧加载值列为风险;本轮没有观察到白闪或浮层越界,不宣称已经复现。
## 19 项验收状态
“当前范围通过”只指列明的用户范围与已有覆盖,不等于完整逐像素验收。
| TODO | 条目 | 当前结论 |
| --- | --- | --- |
| 01 | 任务操作菜单 | 归档回访缺口已解决并通过 25 项真实 Electron 专项;独立窗口事件仍待修复,菜单原版全状态像素仍缺证据。 |
| 02 | 表格引用 | 发现历史上下文缺失;已有草稿保留测试未构造非空草稿。来源标题和原版两态画面待采集。 |
| 03 | 编辑已发送消息 | 缺产物处理选择、旧消息可编辑性预检;错误映射和编辑器复杂输入未完整验收。 |
| 04 | 用户消息悬停时间 | 用户约定的三档格式通过;“昨天”不列 Bug。动画帧仍未比对。 |
| 05 | 用户消息回退 | 产物选择、逐产物 fallback、失败反馈和原版弹窗像素未闭环。 |
| 06 | 对话滚动条 | 浅色像素及跟随行为通过;原版深色/hover/周边控件仍缺证据。 |
| 07 | 侧栏最下一行 | 核心浅色布局通过;窄态、深色原版画面、头像与 mock 数据仍有边界。 |
| 08 | 新任务切页与占位 | 主体通过;创建成功后的启动失败状态已修复并有回归;多阶段准备及准备阶段取消尚未闭环。 |
| 09 | 账户菜单 | 指定内容和导航通过;头部实际几何、图标路径仍有差异。账号服务不纳入本轮。 |
| 10 | 通用设置 | 已接功能通过;部分统计和打开目录错误有缺陷;系统副作用未纳入自动化。 |
| 11 | 外观主题 | 浅深稳定态和同步通过;首帧、过渡帧与独立窗口/内容块全表面待验收。 |
| 12 | 快捷键 | 页面及部分真实动作通过;Escape 工作台组合冲突已按本地优先级修复并有回归,多数 supported 行仍没有动作级测试。 |
| 13 | 个性化 | 页面与离线拼装通过;默认人设一致性、跨窗口通知和原版注入层级仍需处理或补证。 |
| 14 | 记忆与进化 | 页面专项通过,但纯函数审计发现默认偏好误写;慢保存、失败与弹窗原版像素未闭环。 |
| 15 | 智能体团队管理 | 偏好持久化等当前范围通过;首帧状态和键盘/失败路径待验收。子代理调度不纳入本轮。 |
| 16 | 移除助理与设备连接 | 当前范围通过;导航专项旧预期需要维护。 |
| 17 | 移除数据管理与应用管理 | 当前范围通过;不再依赖数据管理承接归档,归档入口由任务分组标题提供。 |
| 19 | 关于与获取帮助 | 当前约定范围通过;获取帮助深色和无跳转动作测试仍需补齐。 |
| 21 | Git 与会话上下文 | 正常分支、OMP 用量和浅色主体通过;备用 usage、边界状态、浮层、窄态及分类统计未完整验收。 |
## 实际执行与测试维护问题
- 主上下文 `npm run build`:通过,包含 typecheck。
- 主上下文 `npm run test:conversation-ipc`:通过。
- 子 agent `npm run test:personalization`:通过。
- 全工作树 `git diff --check` 未通过:既有 `ipc.ts`、`sessions.ts`、`Sidebar.tsx`、`types.ts` 的未提交修改含行尾空白/文件末空行;本轮没有修改这些业务文件,未顺手清理。
- 主上下文串行执行 19 个专项脚本:17 PASS,2 FAIL。
- 18 个结构化报告共执行到 585 条检查,其中 582 PASS、3 FAIL;Git 独立脚本另有 6/6 PASS。上下文脚本在后续阶段超时,未把没有执行的断言算入通过。
- 运行日志:`.tmp/audit19-20261017-165014/`;汇总:`summary.json`;每个专项的隔离目录和原报告路径均在汇总内。
| 脚本 | 本轮结果 | 限制/问题 |
| --- | --- | --- |
| `verify-task-menu` | 42/42 PASS | 独立窗口只验 URL,没有发起对话。 |
| `verify-task-running-menu` | 21/21 PASS | 运行中归档/删除的受控场景。 |
| `verify-quote-table` | 48/48 PASS | chat:send 桩不覆盖两回合上游历史;草稿为空。 |
| `verify-user-message-edit` | 67/67 PASS | 缺失败、缺原生分支及产物选择场景。 |
| `verify-conversation-scrollbar` | 24/24 PASS | 当前测试范围内的几何、像素、流式跟随。 |
| `verify-sidebar-footer` | 29/29 PASS | 不代表原版所有主题/宽度像素通过。 |
| `verify-new-task-switch` | 30/30 PASS | 失败用例发生在创建会话前,未覆盖 A03。 |
| `verify-account-menu` | 64/64 PASS | min-height 和图标盒子不能证明真实高度和 SVG path。 |
| `verify-general-settings` | 43/43 PASS | 系统集成隔离;无大目录截断或打开目录失败。 |
| `verify-appearance-settings` | 21/21 PASS | 稳定态。 |
| `verify-desktop-appearance` | 19/19 PASS | 像素比较是首页机器人局部 ROI,不是整个界面。 |
| `verify-shortcuts-settings` | 32/32 PASS | supported 标记不等于所有键位实际执行。 |
| `verify-personalization-settings` | 29/29 PASS | 没有验证主窗口活跃流式内容同步。 |
| `verify-memory-settings` | 24/24 PASS | 没有验证只改第一项时第二项不变。 |
| `verify-agents-settings` | 17/17 PASS | 没有加载首帧/失败/键盘全路径。 |
| `verify-settings-nav` | 16/19 PASS,脚本 FAIL | 仍期望 memory 禁用,3 个断言陈旧;不是导航业务回归。 |
| `verify-about-help-settings` | 15/15 PASS | 获取帮助深色及点击无外跳尚未单独断言。 |
| `verify-git-branch` | 6/6 PASS | 检测模块通过不代表所有状态的 UI 已验收。 |
| `verify-composer-context-info` | 前 41/41 PASS,脚本超时 | 仍等待旧 `.segmented`,现为 `.appearance-card`;后续深色段未执行。 |
建议先维护两处陈旧脚本与关键前提不足的用例,再按 A01 → A02 → A04/A05 → 其余交互及视觉差异推进,防止再次用相同的通过结果覆盖未测试路径。
### A03/A07 修复后的最终验证
- `npm run build`:通过,包含主进程、preload、renderer 与 typecheck。
- `npm run test:chat`:通过;新增创建成功后发送拒绝、持久化消息保留、清理失败保留、早到后台事件阻止删除、迟到失败不覆盖新任务、模型切换清除旧 usage、回退选项透传等断言。
- `npm run test:workbench-escape`:通过;纯逻辑模块覆盖 IME、菜单、全屏、普通焦点和非 Escape 键。
- `node scripts/verify-new-task-switch.cjs`:真实隔离 Electron / preload / IPC / renderer、真实 OMP 与受控本地上游场景 PASS,30/30;报告 `.tmp/new-task-switch-tifYSS/report.json`。
- `node scripts/verify-shortcuts-settings.cjs`:真实 Electron PASS,32/32;报告 `.tmp/shortcuts-settings-Ih8soB/report.json`。
这些结果证明当前 YCode 修复路径,不证明 WorkBuddy 在“生成中 + 工作台全屏 + Escape”和“创建成功后发送失败”两个组合状态下采用完全相同的行为。上述两项仍需正式 REA 运行时对照后才能计入原版 1:1 验收。
## 原版证据与限制
本轮复用各模块文档中既有 WorkBuddy 5.8.2/5.8.3 的 REA 证据,没有重新读取原版安装包或绕过 REA 查看实现。具体版本、Evidence ID 和覆盖范围见三份分组报告及原待办清单。
本轮主上下文经 REA `list_electron_targets` 请求已知 `http://127.0.0.1:9223`,返回 `provider_unavailable`,原因为 `endpoint_unreachable`。这只证明该已知端口当时不可达,不能推断其他端口或整个 REA 能力不可用。没有重启或修改 WorkBuddy。
同状态像素、动画帧、原版未观察状态和提示词层级仍需后续经 REA 补证。历史静态源码、计算样式、本地专项与 OMP 通路通过,均不等于完整 1:1 验收。
+25
View File
@@ -0,0 +1,25 @@
# OMP 内置分发
日期:2026-10-18。按用户要求,将 OMP 从外部依赖改为 Windows x64 安装包内置,并重新生成 v0.01 分发包。
## 实现
- 固定上游 OMP 18.6.3。运行时锁记录发布地址、发布时间、字节数和 SHA-256:`resources/omp/runtime.lock.json`。
- `npm run omp:vendor` 从固定版本恢复资产并逐项校验;支持离线验证和用本地已校验副本导入。
- `npm run dist` / `npm run dist:installer` 在构建前自动执行 `omp:vendor`。
- Electron 打包将 `omp.exe`、MIT LICENSE、第三方声明和运行时锁复制到安装目录 `resources/omp/`。
- 运行时探测顺序:`YCODE_OMP_PATH` 显式覆盖、包内 `resources/omp/omp.exe`、兼容旧环境的 Ygent 安装、内置 HTTP 回退。正常安装包不依赖外部 Ygent。
- 新增路径回归和包内资产验证:`npm run test:omp:bundled`,打包后使用 `node scripts/verify-bundled-omp.cjs --packaged`。
## 验证
- `npm run omp:verify` 通过;OMP、LICENSE、第三方声明的大小和 SHA-256 全部匹配运行时锁。
- `npm run test:omp:bundled` 通过;无 `YCODE_OMP_PATH` 时解析到 `resources/omp/omp.exe`,`omp.exe --version` 返回 `omp/18.6.3`。
- 使用隔离临时 profile 完成真实 RPC 握手,收到 `{"type":"ready","protocolVersion":1,...}`,随后清理临时 profile。
- 安装程序归档包含 `resources/omp/omp.exe`(235,715,072字节)、LICENSE、THIRD-PARTY-NOTICES.txt 和 runtime.lock.json。
- 正式 `release/win-unpacked/YCode.exe` 在空用户数据目录启动;生产 IPC 返回 OMP 可用,实际 `exePath` 为包内 `resources/omp/omp.exe`。10项包内容与启动检查通过,0个未捕获 renderer 异常。报告 `.tmp/release-v001-mfDVCw/report.json`。
- 最终安装程序为 `release/YCode-v0.01-Setup-x64.exe`,246,498,049字节(235.08MiB),SHA-256 `4a8ceaeb1dd12bfc228911c2d586a7db2f4682b02b995c62602e7b14e6830184`。当前未签名。
## 限制
本轮验证使用已安装的正式 Electron 构建并在隔离用户目录启动,没有实际执行 NSIS 安装/卸载。未验收干净 Windows 设备、标准用户权限、硬件加速全路径及所有业务功能。包内 OMP仅覆盖 Windows x64;其他平台仍需提供对应运行时。
@@ -0,0 +1,63 @@
# WorkBuddy 子任务显示位置核对
日期:2026-10-18。本轮只调查与记录,未修改 YCode 产品代码、未重启 WorkBuddy、未发送测试消息。
> 最新位置订正:按用户要求,成员入口现已从回复底部移到输入框上方,详见 [迁移与验证](输入框上方成员入口迁移-2026-10-18.md)。下方保留最初调查及后续补查的原始范围。
> 后续实施:用户随后要求对齐,现已接入回合底部成员入口及右侧任务详情,恢复原版CSS和SVG;实现、验证和剩余限制见 [子任务展示对齐](子任务展示对齐-2026-10-18.md)。下方“YCode差异”保留为本次修复前的调查基线。
> 后续订正:用户指出输入框上方还有任务区域,经同一份5.8.2 REA导出确认,那里还有**团队成员栏和后台任务列表**。此前“两条路径”只覆盖回复内的入口,不能作为全部入口清单。位置、数据来源与两套宿主路径的区别见 [输入框上方任务区域核对](WorkBuddy输入框上方任务区域核对-2026-10-18.md)。YCode尚未实现该区域。
## 结论
原版回复内已确认以下两条展示路径,不能把它们都替换为逐张展示的子 agent 卡片;输入框上方的入口另见上述订正。
| 对象 | 主对话中的入口 | 查看详情 |
| --- | --- | --- |
| 当前回合参与协作的子 agent / 团队成员 | 回复底部的头像组和「{count} 位团队成员」;生成中和结束后均有对应插槽 | 点击入口展开成员列表,再点击某位成员,打开详情面板中的成员任务对话 |
| 普通 `task` / `Task` 工具调用 | 消息事件中的可折叠 Task 工具块 | 行内展开委派 prompt、工具调用情况、简报和最终结果;适用时显示审批按钮 |
子 agent 事件带有 isSubAgent、parentSessionId、memberEvent、parentToolCallId 等信息时,会被划分为 member-internal,并优先交给 ChildAgentProjection;不能把这些事件直接作为主回复正文逐项输出。
## 协作入口与详情
- `RequestCollaborationSlot` 按 requestId 读取当前回合的成员。
- 收起时显示最多4个20px头像和「N 位团队成员」,4是头像预览数,不能当作并发上限。
- 点击后展开列表,显示成员头像、职业/名称、任务序号及运行/完成/失败/中断等状态;带重试信息时另显示重试次数。
- 列表先排主理人,再按状态、manifestOrder和稳定身份排序。
- 选择成员调用 selectTeamMember,激活 DetailPanelSurface,并执行 detail.open({view: 'member', projectionId})。
- 成员详情由 `ConversationMemberDetailPanel` 渲染:头像、职业/名称、「的任务一」等标题和关闭按钮;正文使用对话渲染器展示成员消息、工具及思考。下发任务说明有独立的展开入口,运行时可持续更新,支持向上加载更早请求。
- 入口在 `ConversationStreamingFooter` 和完成后的 `conversation-finished-footer` 中均被挂载,因此不是结束后才可查看。
## 证据与范围
主要依据为既有 **WorkBuddy 5.8.2** REA 导出 `.tmp/rea-runtime-scripts.json`,Evidence ID:
`ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`。
本轮只读取、校验和定位 REA 已采集的脚本,没有直接读取安装目录脚本、连接 CDP/WebSocket 或在原版执行 evaluate。
`contexts-BMeWSUHk.js`,SHA-256:`5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b`。以下位置为 JavaScript 字符偏移:
| 范围/入口 | 字符位置 | 证实内容 |
| --- | --- | --- |
| classifyConversationFrame | 975186附近 | 成员内部事件分类 |
| ChildAgentProjection 分流 | 1193716附近 | 子事件优先投影,不直接流入主对话 |
| ConversationMemberDetailPanel | 5913738起 | 成员任务详情及独立对话渲染 |
| RequestCollaborationSlot及成员列表 | 6187325–6193670附近 | 头像、人数、列表、状态及打开成员详情的回调 |
| 中文文案 | 4944252附近 | 「N 位团队成员」「的任务」「下发任务详情」 |
| 完成态底部挂载 | 6892143附近 | variant=finished |
| 生成态底部挂载 | 6978100–6978708附近 | variant=streaming |
| ConversationDetailSurface Portal | 7222068附近 | 通过独立详情面板宿主显示成员内容 |
`src-DbE1_mgM.js`,SHA-256:`6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4`。1102960附近的 convertTask 和1103490附近的 TaskView证实另一条普通Task工具展示路径。
本轮通过 REA 当前安装包目录证据再次核对了该 core 脚本摘要,仍与既有导出一致。对应 Evidence:`ev_29ec36f6e11c3f4b10bfe6e3f96934b819852ae32614ce346c4fd9ea760fc0c2`。不能由此推断当前安装包所有其他脚本也与5.8.2相同。
同时通过 REA 对任务预览、后台终端输出及 agent-chat-pane 等独立模块进行了限定范围分析,父 Evidence:`ev_91a39ae854fd27bece1298ce9e8fce226c1e8653da3de6acfbf3fdc12353844b`;该有限分析不替代上述主对话源码链路,也不证明动态模块已执行。
## 当前限制与 YCode 差异
- REA 连接已知原版调试端口9223返回 endpoint_unreachable,尚未取得本轮原版真实运行截图和点击演示。因此以上是已采集原版源码中的展示路径,不能称为本轮像素或动态交互验收。
- YCode 最新 mock 对应 `SubagentCard` 的自有行内实现:逐张卡片、独立耗时和停止按钮、展开的事件列表。已接通本地调度,但缺少原版的回合底部成员入口和成员任务详情面板;该 mock 不能作为原版1:1展示。
- 后续对齐应保留现有调度数据,将其适配到成员列表、当前回合归属、选中成员和详情对话;普通 task 工具继续沿独立工具块路径处理。具体实现尚未在本轮开展。
@@ -0,0 +1,76 @@
# WorkBuddy 输入框上方任务区域核对
日期:2026-10-18。对应用户问题:输入框上方是否有列表,展示后台命令和正在运行的子 agent。
> 后续实施:按用户“应当放到输入框上面”的订正,现有成员入口已从回复底部迁移到输入框上方,见 [迁移与验证](输入框上方成员入口迁移-2026-10-18.md)。后台命令列表与MainContentCore横向团队栏的完整1:1仍待补;下方保留本次调查时的基线。
## 结论与原先遗漏
用户记得的位置有源码依据。WorkBuddy 5.8.2 的 MainContentCore 宿主在输入框上方挂载同一个区域,其中包含两种独立组件:
| 内容 | 入口与行为 |
| --- | --- |
| 团队成员 / 子 agent | TeamMemberBarSlot,显示主会话和成员的头像、名称及运行/完成/失败状态;点击成员切换成员任务对话,点击主会话返回。专家团队适用时支持收起为头像组再展开。 |
| 后台命令 | BackgroundTaskCapsule,收起显示「N 个后台任务运行中」,点击展开列表,逐项展示终端图标、任务标题和状态/运行时长。 |
两者位于同一个输入框上方宿主区域,但不是把思考、普通工具事件、子 agent 和后台终端混成同一种事件列表。源码中成员使用团队运行时快照,后台任务使用单独的任务更新订阅。
上一批只实现了另一条渲染路径中的回复底部「N 位团队成员」及右侧成员详情,漏查并漏做输入框上方这两个组件。回复底部入口的存在不能证明上方区域不存在,也不能替代上方区域。
## 挂载链路
以下均为校验摘要后的REA保留脚本字符偏移。
`main-content-core-Dlaq72qt.js`:
- 114100–122900附近:TeamMemberBarInline、TeamMemberBarSlot及收起头像组。成员通过 getTeamRuntimeSnapshot 获取;点击调用 setViewingMemberId,状态包括 running、completed、failed、killed。
- 20906附近:useBackgroundTasks 订阅 onBackgroundTaskUpdate,按当前会话取任务;排除 completed、failed、cancelled、killed 后计算活动数量。
- 279250–282150附近:hasTeamForCurrentSession / hasExpertTeamForCurrentSession 判定成员栏是否出现,成员数据不是由后台命令数组推导。
- 284419–285590附近:qx 输入插槽同时组合团队成员栏与 BackgroundTaskCapsule。专家依赖提示出现时会替换成员栏,后台任务胶囊以 hasActiveTasks 为显示条件。
- 295311附近:将 qx 传给 ChatContainer 的 inputBottomSlot。
- 278600–280000及283600附近:选中成员后组装 agent-member 任务对话并放入成员视图;285608附近有返回主会话动作。这条路径不应直接等同于另一条路径的右侧详情面板。
`src-0FGg1seX.js`:
- 1153035附近:ChatContainer 接收 inputBottomSlot。
- 1157440–1158520附近:input-area-container 中先渲染 inputBottomSlot,后渲染输入控件 lt,证实虽名为 bottomSlot,实际位于输入框之前。
## 另一套 ConversationInputArea 路径
`contexts-BMeWSUHk.js` 还包含以下实现,需与上面的宿主路径分别记录:
- 7183110–7187600附近:ConversationInputArea 在输入框之前挂载 BackgroundTaskCapsule;存在消息队列时移到队列上方的 ConversationInputStatusChips,避免重复挂载。
- 5879189附近:useConversationBackgroundTasks 只读取 getShellTasks;5513204–5517500附近的 createBackgroundTaskStore 只从终端快照投影任务及活动数量。
- 1388110–1390000附近:trackRunningBackground 显式排除 isAgentBackgroundUpdate。agent- 标识或工具名 Agent 不按普通后台终端追踪,不能据此把子 agent 加入该终端列表。
- 6993900–7008100附近:任务列表支持运行/等待/完成/失败/取消状态;这套组件可点击打开 background-task 详情,并在支持时确认停止。4981687附近为终端详情面板。
- 7186000附近:父组件要求有活动任务才挂载胶囊。胶囊自身虽有全部完成文案,不能据此声称父组件在所有任务结束后仍会保留入口。
MainContentCore 自带的较简化 BackgroundTaskItem 只展示图标、标题、状态,没有上述点击详情和停止按钮。两套实现不能不加区分地合并为同一套已运行行为。
## 证据身份
使用既有 **WorkBuddy 5.8.2** REA 导出 `.tmp/rea-runtime-scripts.json`,Evidence:
`ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`。
| 脚本 | SHA-256 |
| --- | --- |
| main-content-core-Dlaq72qt.js | 998ada4969d1bf7af8d2dace15c0bc2a3ca5bf0ee9cf941823d85a66d6967991 |
| src-0FGg1seX.js | f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178 |
| contexts-BMeWSUHk.js | 5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b |
CSS来自REA保留导出 `.tmp/rea-css-recovered/`,Evidence:
`ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`。
- main-content-core-D2cL2TEf.css:SHA-256 `0dcd00c1efcc05d6e06d163ffdf7919fb75e7bc537b6020bd016facec51b299d`。团队栏有水平滚动边缘渐变、收起头像组和状态图标;后台任务区域下留12px。
- background-tasks-DpSXKuU1.css:SHA-256 `61c98370fa4b423530a39b56291f806cce89825ffd9a4f02271c961d98c5f6d8`。另一套后台任务列表面板使用16px圆角、12px 16px内边距、32px任务行;声明中包含.25s展开动效。
## YCode 待补与验收范围
1. 输入框上方的会话级任务宿主及团队成员状态栏,不能仅保留回合底部入口。
2. 后台命令的独立真实状态、输出与生命周期;不能把所有执行命令或子 agent 快照冒充后台终端。
3. 入口收起/展开、切换成员及返回、消息队列共存、会话切换、浅/深色和窄屏行为。
4. 确认用户实际运行场景采用的宿主路径后,按对应路径验收,不混搭两套源码行为。
本轮只核对既有REA证据并订正文档,没有改产品代码或重跑上一批测试。没有直接读取原版安装目录、CDP/WebSocket或执行原版evaluate。上轮原版9223不可达,本轮未重试;当前窗口的具体激活路径与同场景视觉/动效仍未运行复核。上述结论是原版源码中的路径与行为,不是新增运行截图验收。
+110
View File
@@ -0,0 +1,110 @@
# YCode 应用图标设计与落地(2026-10-17)
## 参考基线
- 对象:WorkBuddy 桌面端应用图标,取自安装目录 `Assets/Square150x150Logo.scale-400.png`(600×600 位图)。
- 证据等级:**位图采样,不是 REA Evidence**。若要纳入 1:1 复刻的证据链,需另行走 REA 重新导出该资源。
- 实测结果:
- 内容区 480/600 = **80%**,四周各留 10%;squircle 圆角约 **23%** 边长。
- 背景渐变 **135°**,`#0DC8A6 → #7ED968`。
- 浅色斜带自身也有渐变:左下 `#EFFCF8` → 右上 `#D6F6F0`。
- 两枚**纯白药丸**,长轴平行于斜带,圆角 = 宽/2。
- 全平涂:无描边、无阴影、无高光。
- 沿用下来的只有**容器语言**:squircle、45° 对角渐变。
- **但 80% 的内容区没有沿用到运行时**:实测任务栏里 WorkBuddy 占满 24px 的槽位,而我们的图标只占 20px(截图量得 24 vs 20),因为它的窗口图标是贴边裁的。画布内容先按 `scale(1.2)` 放大到 95.5%,仍显小;最终改成 `scale(1.25)`,内容 **99.4%**(实测),等于满幅。
## 设计决策
- **不照抄**:同构换色会被误认成同一产品的不同版本,任务栏并排时分不清。
- 中途试过「纯旗标」方案(只有星、没有角色)。放弃的原因:一眼看过去只是"一面被裁切的红旗",缺少能让人记住的活物。兔子最终回到前景。
- 最终形态:**放大裁切的旗面 + 白色长耳兔**。旗面只取一角,五颗星各自只露局部;兔子压在前景,允许遮挡星群。
- 兔子参数:**1.0 倍、中心 (64,106)**,从底部探出——脸的下缘被边框切掉,只留耳朵、额头和眼睛。这是刻意的"贴脸"取景:兔子占满下半幅,与左侧的大星形成对峙。
- 眼睛是**实心红** `#B0112A`,不是挖洞:早期用 mask 挖洞做"红瞳",但洞会透出底下颜色——压在金星上眼窝就变金,位置被星群绑死。改成实心色后,兔子可以自由压在星上。
### 配色
- 旗面 **`#CF1230 → #78060F`**(正红 → 深绛),45° 方向与 WorkBuddy 一致但色相完全不同。
- **旗面光泽**:一条多段透明/白/黑的 135° 渐变铺满旗面,模拟绸面受光起伏。曾用 `feGaussianBlur` 画真实褶皱,但在 64px 下会糊成色块,改回渐变实现——只保留"光扫过旗面"的观感。
- 星色 `#F7CE72`、透明度 0.86,压在深红上呈哑光金(实测像素 `#EEB367`),不刺眼。
- 已否决的底色:`#0DC8A6 → #7ED968`(原版青绿)、`#2440E8 → #0FB9DE`(靛蓝)、`#4F2BE8 → #A94BFF`(紫罗兰)、`#C8102E → #FF4D6A`(绯红带玫)、`#FF6A3D → #E02D6E`(珊瑚)、`#2B1B4D → #7C3AED`(深紫夜)。
### 五颗星的几何
按《国旗制法说明》计算,而非目测:
- 旗面 30×20 单位;大星中心 (5,5)、外接圆半径 **3**;四颗小星中心 (10,2)(12,4)(12,7)(10,9)、半径 **1** → **大:小 = 3:1**。
- 四颗小星各有一角尖正对大星中心,旋转角由 `atan2` 算出:**239.0° / 261.9° / −74.1° / −51.3°**。
- 换算到画布:`k = 16 px/单位`,原点 (−66, −16) → 大星半径 **48**、小星半径 **16**。构图刻意**放大并右移**:大星被左、上、下三边裁切,四颗小星分别从上、右(两颗)、下边缘露进一角,右侧两颗只剩 5px 左右的尖角,五颗都只看到局部。
- 这个取景是刻意的:放大到这个程度后,第一眼读到的是"红底 + 金色块面",而不是一面完整的国旗;要看清"一大四小"必须退远看。
- 计算脚本 `scripts/star-layout.mjs`:改 `k` 和原点即可重排,坐标不写死。
- 曾试过 k=8.8(星群完整落在框内,一眼就是国旗)、k=10(完整但偏小)、k=14(五颗都露得较多)。放大的同时原点必须跟着往左移,否则小星会被整体挤出画面——k=16 配原点 (−66,−16) 才让右侧只留下一线。
## 合规提醒
国旗及其图案依法不得用作商标、外观设计和商业广告。本图标为个人自用,且星群在尺寸与位置上按画布重新换算(非制式排版),风险可控;**若要对外分发或上架应用商店,建议把星群进一步抽象化**(例如改为光点、星环,或去掉小星只留一颗)。
## 小尺寸处理
图标已无角色细节,**全尺寸共用同一份矢量源**,不再维护 `icon-small.svg`:
- 16px 下大星约 3.7px,只能读到"红底 + 金色星形"的轮廓;这是纯旗标方案的固有下限。
- 48px 及以上五颗星的关系清晰可辨。
## 文件清单
| 路径 | 作用 |
|---|---|
| `build/icon.svg` | 唯一矢量源,1024 画布 |
| `build/icon.ico` | Windows 图标,内嵌 16/24/32/48/64/128/256 |
| `build/icon.png` | 1024 通用位图 |
| `build/icon-preview.png` | 各尺寸在浅色/深色任务栏下的对照表 |
| `build/sizes/icon-*.png` | 备用位图,13 档:16/20/24/32/40/48/64/96/128/180/256/512/1024 |
| `resources/icon.png` | 运行时窗口图标(512) |
| `scripts/gen-icon.mjs` | 栅格化 + ICO 打包 + 对照表 |
| `scripts/star-layout.mjs` | 五角星制式坐标计算 |
## 生成与接线
```bash
npm run icon:build
```
主进程改动(`src/main/index.ts`):
- 新增 `appIconPath()`:开发态取 `app.getAppPath()/resources/icon.png`,打包后取 `process.resourcesPath/icon.png`。
- 主窗口、设置窗口、任务窗口三处 `BrowserWindow` 补 `icon`。
- `app.setAppUserModelId('com.ycode.app')`——Windows 任务栏分组与固定后的图标依赖它。
### 托盘
`src/main/index.ts` 新增 `createTray()`,在 `app.whenReady()` 里随窗口一起创建:
- 图标取 `resources/tray.png`(32px,打包后取 `process.resourcesPath/tray.png`);
- 左键点击唤回主窗口(窗口被关掉就重建一个),右键菜单为「显示 YCode / 退出」;
- 图标缺失时只打一条 warn,**不抛错**,避免把启动流程带崩。
### 文件图标与打包
`package.json` 新增 `build` 段与两个脚本(`dist` 出免安装目录、`dist:installer` 出 NSIS 安装包):
- `appId: com.ycode.app`(与 `setAppUserModelId` 一致)、`productName: YCode`;
- `win.icon: build/icon.ico`——`.exe` 的图标从这里写进 PE 资源;
- `extraResources` 把 `resources/icon.png`、`resources/tray.png` 复制到打包后的 resources 目录,主进程的 `process.resourcesPath` 分支才有文件可读;
- `npmRebuild: false`:本机的 VS Build Tools 缺 Spectre 库,`node-pty` 重编译会以 MSB8040 失败;该模块在开发态已按同一 Electron 版本构建,跳过重编译可直接复用。
**注意**:`.exe` 的文件图标只在打包后存在——开发态运行的是 `electron.exe`,资源管理器里显示的是 Electron 自己的图标,靠改代码无法替换。
## 验证状态
- **已验证**:`tsc -p tsconfig.node.json` 通过;`build/icon.ico` 可被解析出 7 个尺寸;`icon.png` 为 1024、`resources/icon.png` 为 512;星色 `#EEB367`、旗面 `#810B18` 采样正确;对照表目视通过。
- **未验证**:窗口与任务栏实际图标效果——当前环境 Electron GUI 未能拉起(`ELECTRON_RUN_AS_NODE=1` 且 GUI 启动无输出),未做真机观察。
- **未验证**:打包后的 `.exe` 文件图标。项目当前**没有任何打包配置**,需要 electron-builder 的 `win.icon: build/icon.ico` 才能写进 PE 资源。
- **未做**:托盘图标(当前应用没有 `Tray`)。
## 已知取舍
- **认不出是国旗**:这是放大裁切后的预期结果,不是失误。第一眼是"深红 + 金色块面 + 白兔",第二眼才读出星形。如果需要一眼认出国旗,把 `k` 降回 8.8、原点改到 (−2, 1),但那样兔子就得让到角落。
- 图标里**没有字母 Y**:识别靠"深红 + 金星 + 兔子"这组符号,不靠字面。若日后要补品牌关联,靠应用名和界面承担,而不是继续往图标里塞元素。
- 兔子放到 1.0 倍后,下巴被边框切掉,脸只露到眼睛下方;再放大就要开始丢耳朵,往回收则失去"贴脸"的张力。
- 16px 只能呈现"红底 + 白兔 + 一块金色",五颗星的关系不可辨。
- 应用内 UI 主色仍是 `#00c29a`(沿用自原版)。**图标红、界面绿**并存,如需统一要另行调整主题色,不在本次改动范围内。
+65
View File
@@ -0,0 +1,65 @@
# 定时任务、资料库、项目:UI 续对齐
日期:2026-10-17。范围是用户指出的三模块 UI 大差异。主上下文负责架构、集成、差异审查与最终验证;三个执行 agent 分别修改各自模块。此前未提交的业务代码、图标与构建资源均保留。
本记录延续[首批本地功能](三模块首批开发记录-2026-10-17.md)。下面的尺寸检查只证明列出的稳定布局,不能替代全状态、同数据的逐像素和动效验收。
## 原版依据与观察范围
全部 WorkBuddy 实现和运行时资料均来自 REA;没有通过其他渠道读取原版。
| 范围 | 版本 / REA Evidence | 覆盖与限制 |
| --- | --- | --- |
| 定时任务首页 | 5.8.3 / `ev_386df7d869a651cc6c083205fb10c4264ec8356901bc0b031cab63d64d65c8d5` | 桌面首页、空态、模板卡;云端实例数据不复制 |
| 定时任务新建 | 5.8.3 / `ev_be404f0454dfd7496f91ff79a7f43bb61a54ad587933baee0d1cf77c4db915c5` | 源样式、脚本、布局;布局快照捕获在 opacity=0 / scale(.96) 的入场帧,不能直接把 768×470.406 当稳定尺寸 |
| 资料库首页 | 5.8.3 / `ev_82a9ad8ec38c807e465646d749e2cedbb150fec62e30234c97acd0bb309c349e` | 首页侧栏、标签、表格、介绍卡和 inline SVG |
| 本地产物 | 5.8.3 / `ev_b7fe1b4adac2311b04dfa454126363ca382869d6a29664152d497a920c2e8cad` | 返回导航、全宽表格、列头、任务/文件行;收藏星形 SVG 尚无原版依据 |
| 资料库介绍图 | 5.8.3 / `ev_d650991b41fe1831a4caf5dd225398e29db8e91205d00c2ac3047b00baa1998e` | REA 导出两张已加载原始 SVG;资源身份已确认,采集时可见性为 unknown/no_visible_layout |
| 项目首页 / 详情 | 5.8.3 / `ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9` / `ev_ca0195278ba08b57f2d847e4d96e3a35490f26367b71b810563b01b03470d000` | 桌面深色布局、输入区、配置栏;动态内容与 YCode 测试项目不同 |
| 项目源码与筛选 | 5.8.3 / `ev_8782905c492acd1618e137480a3da8a8d7eba56e8f328e4856b008e1dd7f34ea` | UI scope `all/personal/collaborative`;source `all/manual/schedule/local`;云端来源、角色没有本地等价数据 |
本轮通过 REA 补取了资料库已经加载的两张原始介绍图,之后点回原先选中的 WorkBuddy 任务。恢复记录 `ev_4f3ba3426b6d32b70e1d5acb504575c9ff14803c39ef69055d28ef86d14d11d5` 中点击/等待与恢复帧成功;探针总状态因 neutral-pointer 清理取消为 failed,不能将其表述为整次探针全部成功。没有发送原版消息、提交文件或修改云端资料。
## 本轮改动
- 定时任务:恢复紧凑编辑器、名称与提示词组合器、底部空间/权限、三列时间规则;默认完全访问并在保存前使用实际确认;主编辑器不因遮罩或 Escape 关闭。真实模型选择及思考强度继续写入任务配置;页面每 15 秒静默更新到期/运行边界。模板图标、空态图和确认框图标来自固定 REA 资源。
- 资料库:首页恢复内部 280px 导航和原始介绍图;本地产物进入独立全宽内容区,恢复“名称 / 类型 / 更新人 / 更新时间 / 大小”五列。索引、搜索、筛选、收藏、最近访问和来源文件打开继续使用真实本地数据。没有作者信息时显示“—”,不伪装成当前用户。主上下文复核后修正三点按钮:先打开菜单,再选择打开/收藏;不把三点图标直接绑定为收藏切换。云分享入口禁用并说明未接入,此菜单是本地适配,原版菜单打开态尚未验收。
- 项目:移除额外顶栏,使用 64px 顶栏、撑满可用高度的主区和 320px 配置栏;输入区固定在内容底部。使用已有输入/模型/空间/权限组件,项目首发将思考强度和权限贯通 App 与实际请求;失败继续恢复草稿。
- 模块字体与侧栏:按对应原版范围使用 `"PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif`;原版组件内部各自不同的字体规则仍保留。侧栏深色背景调整为原版 `#1f1f1f`,没有全局改写对话正文字体。
## 已知差异与未完成项
1. 定时任务日期弹层、时间双列/轮盘及完整打开态仍未 1:1。当前日期是显式合法未来日期输入,时间以每分钟的 SourceSelect 提供;没有限制未来日期到一年以内。原版 calendar 的完整交互和动效仍待恢复。
2. 本地模型列表、空间和项目内容与原版数据不同;不能用不同内容的整屏差异得出零像素结论。
3. 个人资料导入/编辑/引用、项目待办/资产/动态发布和完整生命周期尚未完成。当前首页、空态和可用本地链路不能代表这些能力已接入。
4. 项目只存在本地单用户数据:非定时会话归本地来源,关联真实自动化 run 的归定时来源;协作和手动云端来源为真实空集。个人/全部在这一数据范围下结果相同。
5. 原版旧版项目输入区与当前复用输入组件还有未验收的图标、菜单、焦点、窄窗口、动画差异。邀请、语音等未接入入口不宣称真实可用。
6. 云成员协作、远端通知、产品侧子代理调度按既有范围暂后;安全中心另行拆解。
## 验证
最终构建、截图报告与实际业务链路验收均已完成。回归脚本严格区分“布局断言通过”和“完整 1:1”;布局检查包含项目满高、输入区贴底和模态稳定尺寸,避免仅检查节点存在就误报。
1336×902 桌面稳定态测量:
| 项目 | 本轮修复前 | 当前 / 原版依据 |
| --- | --- | --- |
| 定时任务编辑器 | 768×806,表单纵向铺开 | 800×490;名称 752×40;提示词组合器 752×188 |
| 本地产物内容区 | 仍保留 280px 内侧栏,820px 表格溢出 772px 内容区 | 内侧栏移除;表头 x308/y150/1004×47;滚动区 x308/y197/1004×705 |
| 项目主区 / 右栏 | y134、高 296,页面下方大量空白 | y94、高 808;右栏宽 320;顶栏高 64 |
| 项目输入区 | y298、高 108 | y730、高 148,底部留白 24;浅色/深色一致 |
通过的检查:
- `npm run build`:node/web TypeScript 和生产构建通过;日志 `.tmp/module-ui-final-build.log`。
- `npm run test:module-ui`:57 项稳定布局、交互约束和样式断言通过;最终报告 `.tmp/module-ui-parity-WDVIEC/report.json`,包含浅色/深色、下拉、创建和窄窗口截图。主上下文实际查看了项目详情、编辑器、资料库首页/本地产物截图,依据发现继续修了底色、权限位置、图标和导航内边距。
- `npm run test:three-modules`:真实 Electron/preload/IPC/OMP/localhost 模型与工具链路通过;报告 `.tmp/three-modules-ksjYzL/report.json`。包含定时写入、失败与暂停、运行身份、收藏菜单/实际文件、规则编辑删除、项目极致思考与完全访问命令、首发失败草稿恢复。
- `node scripts/verify-project-page.cjs`:最终构建通过,报告 `.tmp/project-page-7OsWPz/report.json`;包含满高几何、真实配置更新、任务来源/个人/协作筛选。
- `node scripts/verify-sidebar-footer.cjs`:通过,报告 `.tmp/sidebar-footer-eN7T6B/report.json`。
- 自动化 19 组、资料库数据、资料库聚焦 UI、项目任务状态回归通过;REA 自动化资源与资料库 10 个 SVG 图标/2 张插画恢复的摘要校验通过。原始 star 图标仍明确 unresolved,静态文件行没有继续使用手绘星形。
保留的中间失败:`module-ui-parity-JJbso8` 点击了新增加载骨架,已将 QA 选择器改为真实可点击卡片;`three-modules-IoidNa/yz3oSE/3zuSOe` 分别是菜单 portal 选择范围、思考菜单两级悬停,以及截短标题当完整消息的 QA 假设错误。修正了测试的实际操作路径与身份断言,没有通过删除功能断言来绕过;最终链路同时验证完整用户消息与真实会话身份。
用户当前 YCode 进程以 `electron .` 启动,没有 9222 调试端口。为避免清空不可读取的未发送草稿,没有强制刷新或终止;生产文件已构建,空闲时重载窗口即可加载本轮 renderer。隔离 QA 不修改用户实际模型/会话数据。
本轮仍未达到完整 1:1。除上述明确未知项,资料库首页原生滚动条占位、同数据下的内容长度/滚动位置、旧版项目输入区的精细字形和原版动画仍需要继续逐项验收。
+137
View File
@@ -0,0 +1,137 @@
# 定时任务、资料库、项目:开发准备与执行计划
日期:2026-10-17。本文最初为三个模块的开发准备计划。用户随后授权开发,当前首批本地链路已实现并通过定向与集成验收;实际范围、测试及未完项见[首批开发记录](三模块首批开发记录-2026-10-17.md)。下文计划项不能视为全部已实现。
本计划是当前三个模块的开工入口;安全中心不在本轮。安装中心是此前误解,已经退出范围。目标仍是通过REA对标WorkBuddy的视觉、交互和功能;云端服务与产品侧子代理调度继续暂缓,不能因此把整体标记为完整1:1。
> 新增联动核对:[对话资料沉淀与任务引用确认](对话资料沉淀与任务引用确认-2026-10-17.md)。已确认产物保存的个人/项目目标区分、space-doc+library技能引用、todo及关联附件;默认项目根指针只在已捕获的teams/cloud发送链路证实,本地adapter路径仍待核对。原版并未被证实每轮自动检索全库,不能把此前快照建议当作原版读取语义。
> 新增触发边界:同记录第8节确认todo/plan事件及权威taskList的卡片投影、交接时创建项目事项并后台上传资料、模板阶段提示。会话计划≠项目事项,合成卡片≠模型实际工具调用,生成文件≠资料入库。未证原版统一复杂度拆解阈值或任务终态自动上传hook,开发不自行硬编码这些策略。
## 一、已有基线和开工条件
| 模块 | 当前已有真实能力 | 需要新增或收口 | 原版依据 |
| --- | --- | --- | --- |
| 定时任务 | 会话、模型、空间、OMP、事件广播可复用;没有自动化页面、仓库或调度器 | 定义/规则、表单、持久化调度、真实执行、运行记录和生命周期 | [专项拆解](定时任务模块拆解-2026-10-17.md):5.8.3页面/表单及静态RRULE证据 |
| 资料库 | 工作区文件读取、右栏预览、当前会话产物提取 | 跨任务产物索引、资料库路由、个人资料存储、最近/收藏/搜索与引用 | [专项拆解](资料库模块拆解-2026-10-17.md):5.8.3首页、菜单、骨架、本地产物REA证据 |
| 项目 | 真实list/create/update、模板、任务归属、指令注入 | 现有缺陷、能力配置编辑、真实任务状态、生命周期、动态/计划/资产及联动 | [专项拆解](项目模块拆解-2026-10-17.md):5.8.3列表、新建、详情、任务和CSS证据 |
代码基线有大量已有未提交修改和新增文件;本轮未重置、提交或覆盖这些修改。开发时从当前工作树继续,测试隔离userData、OMP HOME、模型上游,保留用户草稿与已有配置。上一轮通过记录不等于新增功能通过,本轮也不重新运行无改动的全套测试。
## 二、本次代码核对新增的优先问题
1. **对话启动服务需要抽取。** `src/main/ipc.ts` 的`chat:send`主体封在`registerIpc`中,涉及会话锁、运行表、模型/能力快照、OMP、消息检查点、广播和退出回收;`session:create`也封装了项目/空间/模型校验。调度器不能依赖renderer或伪造Electron事件调用IPC。
2. **项目首次发送的失败补偿未覆盖。** `ProjectDetailPage.sendProjectTask`先创建会话,再切页并调用store.send;store只有自己新建的会话才执行空会话清理。因此发送在返回runId前被拒绝时,可能留下空项目任务,详情局部错误也可能已不可见。
3. **项目运行标识查错键。** `ProjectDetailPage`用`runs[task.id]`查运行态,但chat store按runId存储runs,所属sessionId在值里。任务可能被错误显示为非运行/成功。
4. **项目状态筛选未生效。** `taskStateFilter`虽更新但不参与tasks过滤;不能把“没有在运行”全部认作成功,必须由真实持久化回合状态派生。
5. **详情内保存指令后未更新父列表对象。** 详情保存只调用projects.update并关闭抽屉;父ProjectListPage持有的project对象没有更新,和列表自己的保存逻辑不同,详情文字会停留在旧值。
6. **资料库索引不能由目录扫描代替。** `workbench-state.collectWorkbenchArtifacts`只聚合当前会话;必须把有来源的产物提取复用到主进程,不能把整个项目源码或用户输入附件都算作产物。
以上为源码观察,未在本轮运行缺陷复现;开发时须加对应真实失败场景或状态回归再修复。
## 三、主上下文统一的契约与实现边界
### 3.1 会话与执行服务
计划新增`src/main/chat-turn-service.ts`和`src/main/session-factory.ts`:
- `createManagedSession(context)`集中处理项目、空间、cwd、模型、专家和模式校验;共享现有仓库实例。
- `startChatTurn(payload, origin)`提供普通IPC和自动化共用入口;保留现有对外SendResult,服务内部另外提供run完成Promise和取消/查询能力。
- 终态必须在消息持久化完成后才认定;保存失败不能写成自动化成功。服务释放、取消、会话锁与运行恢复语义保持原有行为。
- 广播端口可注入,后台启动没有sender也能正常执行;renderer仍通过现有IPC订阅,调度器直接调用服务。
- 这是有限抽取,不重写OMP/Gateway,不顺带改消息投影、编辑回退或跨窗口事件算法。抽取后必须先过现有对话回归。
### 3.2 实体关系与存储
以下为YCode工程决策,不是原版后端还原:
| 实体 | 最低字段与关系 | 约束 |
| --- | --- | --- |
| AutomationDefinition | id、revision、名称/提示词、schedule、execution、启用/归档、可选workspaceId/projectId | 修改不覆盖历史运行快照;不与普通session混为一个实体 |
| AutomationRun | runId、definitionId/revision、trigger、scheduledFor、sessionId、起止、状态/错误和执行快照 | 每个触发实例有稳定去重键;执行记录终态来自实际会话服务 |
| LibraryNode | nodeId、collectionId、parentId、kind、标题、受管内容引用、version/hash、时间 | renderer只提交ID;个人资料由主进程在独立受管根读写 |
| ArtifactIndexEntry | id、workspaceId/projectId、sessionId/messageId、来源类型、文件/预览引用、时间和可用状态 | 同任务同路径以最新产物汇总;跨任务保留独立来源,此规则为本地设计待原版复核 |
| LibraryReference | 引用种类、来源ID/URL、标题/类型、原版元数据、可取得的版本和解析结果 | 保留space-doc/project-assets/todo语义;本地快照为适配建议,云资料版本与读取时机待证,不伪造已读取 |
| ProjectAssetLink | projectAssetId、projectId、显式来源链接 | 与LibraryNode分开,具体管理/引用UI先补REA,不直接把资料库节点当项目资产 |
| ConversationPlan / ProjectTodo | 前者从会话工具/plan事件维护;后者有projectId及真实项目创建/状态/附件操作 | 独立身份、显式关联;不能看到TodoWrite就伪造项目事项已创建 |
- 工作空间、项目、资料集合使用不同ID与生命周期。项目任务的project/workspace/cwd沿用现有校验;项目工作空间仍不可直接修改。
- definitions/runs需要事务一致的本地持久化边界,第一版放同一版本化仓库并序列化原子提交;不把定义写一半、记录写另一半。
- 个人资料使用`userData/library/`受管目录,元数据和文件两阶段提交并处理失败补偿;具体文件名对外不作为节点ID。
- sessions新增归属字段须可选、兼容旧历史;主进程验证自动化/项目来源,不能由任意renderer值改变执行权限。
- shared类型分别放`automation.ts`、`library.ts`、`artifact-index.ts`;现有`projects.ts`原地扩展。IPC采用automation/library/projects分域;preload、主进程注册和页面路由由主上下文集成。
### 3.3 调度第一版的明确策略
为了能写出可验证的第一版,下列策略作为**YCode本地开发基线**;原版时区/DST/重试/漏跑未证实,后续通过REA核对后再调整,不能按这些策略宣称原版等价:
- 创建时保存明确的IANA时区与用户规则,下一触发时间用UTC;时间计算为纯函数,可注入时钟。RRULE保留原版已知语法,成熟库的时区行为须专项核验,不能用固定24小时毫秒代替日历周期。
- 同一任务不重叠;计划触发与手动触发经过同一领取/执行入口,队列和去重在主进程。全局并发受控,先保证多模型/多项目不串配置。
- 周期漏跑先记录并跳过,一次任务过期不自动执行;默认不自动重试。异常退出的历史运行核对实际会话后记中断/未知,不未经核对重发模型请求。
- 明确区分暂停、过期、归档、删除和取消运行。运行中触发/删除遵循已有原版静态拒绝分支;编辑是否影响当前run由快照隔离。
- 首版调度随主进程运行;关闭应用后仍执行需要额外后台或系统调度支持,当前未实现、原版行为待证。不能因窗口关闭策略不明而宣称关应用后也会执行。
### 3.4 产物、个人资料与引用
- 共用纯提取器从显式artifact、成功present_files和成功写/编辑事件提取产物;读取工具输出、附件、任意目录文件不自动计入。
- 历史回填和新回合增量共用同一规则;索引是可重建的派生数据,不反写会话原文。归档仍保留来源;删除后按事实清理/标不可用,不误删工作目录文件。
- 打开产物带sourceSessionId,按来源cwd校验;资料库个人文件用nodeId解析受管根,不扩大现有resources:read权限。
- 导入通过主进程文件选择与复制,先临时写/计算大小及hash,再提交节点;失败不能留半个可见文件。
- 不把library ID塞进现有resources:string[]路径字段。新增reference结构贯通SendPayload、ChatMessage、落盘、编辑回退和两种模型历史通路;旧消息不受影响。新增主进程引用解析层,保留原版资源身份与技能/待办元数据,再装载当前实现确实能读取的内容;不得仅画chip却不传资源。
- 个人/项目资料保存目标遵循实际会话归属和权限,和本地产物索引分开;原版工具栏项目会话提供项目资产路径而非个人库路径。每次成功生成文件不等于云端入库,整段聊天也没有已证自动入库规则。
- 项目资料scope、用户显式引用、模型实际读取分开记录;不静默把个人或其他项目全库作为默认上下文。原版本地默认scope和自主检索仍须REA补证,teams/cloud根指针逻辑不能直接外推。
## 四、可直接派发的开发任务
下列路径中的新增文件/命令是计划,不表示当前已存在。每个子任务结束报告文件、设计、验证与遗留;共享文件只由主上下文接线,跨模块依赖通过接口交付。
| 编号/负责人 | 文件边界与任务 | 验收出口 |
| --- | --- | --- |
| C1 主上下文 | `chat-turn-service.ts`、`session-factory.ts`、`main/ipc.ts`、`main/index.ts`:抽取公共执行和生命周期 | 原IPC行为不变;没有renderer也可启动并等待真实持久化终态 |
| C2 主上下文 | shared IPC/types/workspaces、preload、App/Sidebar、chat store、package.json:契约、路由、事件与集成 | 旧数据兼容;新页不破坏草稿/多窗口;API有统一失败语义 |
| C3 主上下文 | 新`shared/context-references.ts`与`main/context-resolver.ts`:资料/todo/项目资产引用及作用域解析,供对话/自动化共用 | 来源与scope正确;显式引用进入实际请求;失败可见;不能将根指针标作全部文档已读取 |
| S1 定时任务agent | 新`main/automations/{repository,schedule}.ts`、`shared/automation.ts`(先经主协调冻结) | 定义/run持久化、七类规则、可控时钟与损坏恢复;失败不清空数据 |
| S2 定时任务agent | 新`renderer/src/automations/**`:列表/记录、模板、新建/编辑和规则控件 | 使用REA已有CSS/图标/文案;真实CRUD、重载;未接通知状态真实 |
| S3 定时任务agent | 新`main/automations/{scheduler,runner}.ts`,消费C1端口 | 到期→领取→实际会话→终态→记录;单飞、重启、漏跑和取消可测 |
| S4 定时任务agent | 记录/启停/手动运行/编辑/删除/归档与项目入口 | 操作和持久化状态一致;记录可打开正确会话;原版缺失状态补REA |
| L1 资料库agent | `shared/artifact-index.ts`、新`main/library/artifact-index.ts`;原提取器改动经主协调集成 | 跨任务正确索引;去重、来源、失效与归属;不扫描源码冒充产物 |
| L2 资料库agent | 新`renderer/src/library/**`与store:内侧栏、最近、本地产物、排序/筛选/收藏 | 原版已有布局直接应用;可打开实际产物;长列表/空/错误可用 |
| L3 资料库agent | 新`main/library/{repository,service}.ts`、`shared/library.ts` | Markdown/CSV/文件夹/链接及导入真实落盘,原子补偿,重启恢复 |
| L4 资料库agent | 搜索、预览/编辑、引用选择与快照;消费C2引用契约 | 请求内容和显示来源一致;历史/编辑/重载不丢引用;编辑器先补REA |
| P1 项目agent | 现有`renderer/src/projects/**`及专项脚本:先修首发补偿、运行点、状态筛选和指令保存同步 | 真实失败不留孤立空任务;进行/完成/失败按事实;详情即刻反映配置 |
| P2 项目agent | 列表/新建/详情壳、指令/能力配置、`main/projects.ts`与`shared/projects.ts` | 可创建、查看、修改、重载;市场引用不冒充已安装能力;配置是否作用执行要另证 |
| P3 项目agent | 项目生命周期、任务行/来源过滤、交接创建待办与附件关联、恢复与配置事件 | 菜单/副作用先补证;交接事项创建与附件上传状态分别留证,部分失败可恢复;不误删会话或目录 |
| P4 项目agent | 动态/计划/资产与S/L模块关联;新增子域先主协调定契约 | 逐页补REA后接真实本地数据;项目/资料/自动化引用可追溯,云成员协作仍单列未接 |
项目指令、专家、技能、连接器的继承/优先级必须核对原版,不自动把项目内所有专家一起激活。现有buildProjectPrompt只证明指令注入,保存能力ID不等于能力在任务中生效。
## 五、执行顺序与每批可见结果
1. **基础批**:主上下文先做C1/C2/C3的最小契约、执行抽取与引用解析边界。并行agent可做S1纯仓库/时间引擎、L1产物提取与索引、P1独立页面修复;不得并行重写同一chat store或IPC。
2. **第一批可用结果**:定时任务至少一次/每日的真实执行和记录;资料库可浏览实际跨任务产物并打开;项目可创建/编辑指令/真实发起任务,首发失败可恢复。已完成状态的UI从这一批就按原版证据实现,不等全部逻辑完成才做视觉。
3. **第二批完整本地链**:补齐其余频率及生命周期、个人资料与引用、项目配置和任务操作;分别做隔离Electron+受控模型回归。第一批的频率切片不作为缩减完整七类频率范围的决定。
4. **第三批页面与关联收口**:补REA后推进资料编辑/导入细节、项目动态/计划/资产及跨模块联动;云协作/远端推送/服务端权限保留独立未接清单。
5. **最终对照**:逐状态验证几何/字体/图标/滚动/菜单/动效、深浅色与窄窗口。视觉、交互、真实功能分别留证;没有证据或没有实现的项保持未完成。
## 六、验证安排
| 变更 | 必须保留的既有验证 | 需要新增的有意义验证 |
| --- | --- | --- |
| C1/C2对话与归属 | `npm run typecheck`、`test:conversation-ipc`、`test:chat`、`test:persistence`、`test:sessions`;集成后`test:omp-session`、`test:task-window` | 主进程直接启动、无窗口、成功/失败/取消、完成Promise和磁盘终态一致;项目首发补偿与草稿恢复 |
| 定时任务 | 共用对话回归及相关UI检查 | 假时钟七类频率/月底/闰年/时区、单飞/领取竞争、重启/漏跑;受控上游实际产生会话与run记录,保存失败不能成功 |
| 资料库 | `test:resources`、`test:artifacts`、`verify:previews`、`test:sessions`;引用变更跑对话持久化 | 来源白名单/历史回填/增量一致、失效文件、跨会话打开、导入失败补偿、伪造nodeId/符号链接;实际模型wire history引用快照 |
| 项目 | `node scripts/verify-project-storage.cjs`、`node scripts/verify-project-page.cjs`、相关对话IPC专项 | 首发拒绝不留空任务、状态筛选/运行点、指令保存即时更新、能力选择持久化、空间/项目隔离、跨窗口更新 |
| 集成与视觉 | 一次最终`npm run build`后串行运行相关Electron专项 | 同数据/视口/主题的REA对照截图和状态序列;明确动态区域、误差与缺口 |
新增测试名称由实现时登记,不能把计划中的脚本写成已通过。Electron专项由主上下文串行运行,agent只在各自目录执行纯逻辑测试,避免窗口/端口和用户数据冲突。
## 七、不会阻塞第一批与必须补证的部分
- 第一批可直接推进:本地仓库、执行服务抽取、定时任务已知表单/规则、产物索引、已有项目缺陷与指令保存。它们有明确数据通路或既有原版证据。
- 与开发并行补REA:频率/有效期弹层、任务与记录有数据状态,资料搜索/导入子菜单/编辑器,项目筛选枚举/任务菜单/能力选择/动态计划资产页面。
- 阻止宣称1:1、但不阻止首批开工:原版后端时间语义、云资料和成员权限、远端通知、全状态像素与动效未验收。
- REA运行态采集统一调度,避免多个agent操作同一个原版窗口。沿用5.8.2有效证据;缺失部分的新采集注明真实版本与Evidence ID,不改用其他通道。
## 八、本轮交付状态
已从准备转入开发:C1/C2 执行服务与路由、S1/S2/S3 的首批本地实现、L1/L2 本地产物浏览、P1 及能力配置编辑已落地,新增 `rrule` 依赖。首批真实模型/工具/Electron 链路已验证;C3、个人资料导入/引用、项目待办/资产/动态/完整生命周期及全状态 1:1 仍未完成。具体以[首批开发记录](三模块首批开发记录-2026-10-17.md)为准,不能把计划表中的全部功能勾为完成。
@@ -0,0 +1,60 @@
# 定时任务、资料库、项目:首批开发与验收记录
日期:2026-10-17。用户“是否可以开发了”之后,按主上下文规划/集成、三个执行 agent 限定文件边界的方式完成首批本地链路。安全中心不在此批。此前未提交的代码、文档和图标改动均保留,未重置工作树,未重启用户正在使用的 YCode。
本记录描述实际实现与验证,不代表三个模块已完整 1:1。原版依据继续沿用各专项拆解中的固定 REA Evidence(5.8.2 有效历史内容、5.8.3 新增模块证据);本轮没有通过其他渠道查看 WorkBuddy,也没有导航其运行中的用户任务。
## 已实现
| 模块 | 首批实际能力 | 入口/存储 |
| --- | --- | --- |
| 定时任务 | 单次、间隔、每天、每周、双周、每月、每年;真实新建/编辑、启停、手动运行、删除、搜索/筛选和运行记录 | 左栏“定时任务”;`userData/automations.json` 同文件保存定义和运行记录 |
| 实际执行 | 按定义快照创建会话,冻结项目/空间/模型/权限/思考强度;运行中回填 sessionId/runId,终态等待真实消息保存;单任务不重叠,停止时取消并等待 | `main/automations/host.ts` 调用统一会话工厂和对话执行服务,不伪造 renderer/IPC sender |
| 资料库 | 跨任务本地产物,空间→任务→文件层级,搜索、类型/空间/收藏筛选、排序、最近访问、打开来源产物 | 左栏“资料库”;索引从会话重建;收藏/最近访问保存到 `userData/library/user-state.json` |
| 产物来源 | 只提取助手显式 artifact、成功 present_files、成功写入/编辑;同任务同路径汇总,跨任务保留独立来源;保留项目/空间/消息/工具来源 | `shared/artifact-index.ts` 为主进程资料库与工作台共用提取器;不会枚举源码目录或把读取/附件当作生成文件 |
| 文件边界 | 索引检查词法根及 realpath,跨根/symlink/junction 越界标为不可访问;打开时继续使用来源会话的资源读取守卫 | 不扩大 `resources:read` 原有范围,不移动或删除用户产物 |
| 项目 | 真实任务首发、失败草稿恢复;运行点按 run 的 sessionId;任务状态按活跃 run/最新助手结果推导,进行中/完成筛选;指令即时同步父对象;配置编辑保存真实能力引用 | “更多→项目”;原有项目仓库与 projectId/workspaceId 校验复用 |
共享执行抽取至 `main/chat-turn-service.ts`,会话校验抽取至 `main/session-factory.ts`。普通 `chat:send` 保持原传输返回值;只有主进程调用方取得内部 completion Promise。后台新建会话的 started 广播会补入侧栏,用户不必重新加载。
关闭窗口的广播访问已加隔离:枚举结果中的窗口、读取 webContents、判断销毁、发送消息均不能通过关闭对象异常中断执行。IPC 纯回归新增“销毁对象访问抛错”的场景,并等待实际回合终态屏障,避免把内存状态已完成误认为落盘及广播已完成。
## 验证
以下为本批实际执行的检查:
- `npm run build`:通过,包含 node/web 类型检查和生产构建。
- `npm run test:automations`:19 个 suite 通过,涵盖七类规则、DST、月底/闰年、持久化/损坏保护/恢复写失败、去重与竞争、后台轮询、长任务不阻塞后续任务、运行中会话关联、关闭时取消/等待和手动 claim 竞态。
- `npm run test:library`:通过,含成功/失败来源白名单、原生小写 OMP write、用户 artifact 排除、跨任务同路径、收藏/最近访问重载、跨根和 symlink/junction 检查;64 个未引用文件未被扫描,只有 3 个显式安全测试条目接受探测。
- `npm run test:project-tasks`、`node scripts/verify-project-storage.cjs`:通过,涵盖真实状态推导、筛选、能力引用 provider 身份、并发存储与损坏文件保护。
- `node scripts/verify-project-page.cjs`:最终生产构建上真实隔离 Electron 通过;报告 `.tmp/project-page-QBihjh/report.json`,包含指令父对象同步、配置编辑和页面几何/深色检查。
- `npm run test:conversation-ipc`、`test:chat`、`test:persistence`、`test:sessions`、`test:resources`、`test:artifacts`、`test:omp-session`:通过。旧 sessions/resources 测试适配了当前配置依赖与 ESM bundling,未通过跳过断言消除失败。
- `npm run test:three-modules`:生产 Electron、真实 OMP、真实 preload/IPC、仅绑定 localhost 的受控 OpenAI 上游通过。最终集成报告见下方更新。
- `node scripts/verify-new-task-switch.cjs`:最终生产构建上通过;报告 `.tmp/new-task-switch-58fWzV/report.json`,验证即时切页、准备占位、成功替换和首发失败恢复。
三模块端到端覆盖:
1. 单次任务自动到期,真实模型请求→宿主写文件→最终回复→消息落盘→自动化成功记录;模型、空间、项目归属正确,任务及时出现在侧栏。
2. 手动运行立即返回运行中记录;回复仍被上游 gate 阻塞时就能读取真实会话身份;重复执行被拒绝。
3. 上游失败写为 failed,实际助手消息为 error;暂停后拒绝手动运行。
4. 资料库只收录真实生成文件,收藏与最近访问可持久化;点击通过来源任务打开真实文件。
5. renderer 重载保留定义、运行记录和收藏;浅色/深色页面及编辑器截图可检查。
6. 可见表单创建/编辑双周规则,修改提示词不移动周期 anchor,页面启停和确认删除真实落盘。
7. 项目输入区首发切到实际对话并收到回复;另用未被 OMP 占用的独立测试空间模拟目录不可用,确认失败恢复草稿且不留下空任务。
截图为 YCode 自身运行时验收,不是原版逐像素差异报告。主题截图等待状态更新及颜色过渡完成,避免把切换中的白色卡片误当最终深色效果。
## 当前限制与下一批
- **资料库尚未完成个人资料存储与引用。** “我的资料/团队空间”明确未接入;本批本地产物索引不等于个人资料库或项目资产库。Markdown/CSV/文件夹/链接导入、资料编辑、显式引用 chip 到实际请求的统一解析(开发计划 C3/L3/L4)仍待开发。
- **项目动态/计划/资产尚未配置。** 项目待办、交接创建事项及资料关联、归档/删除等完整生命周期未接。配置中保存了专家/技能/连接器引用,不等于项目任务已自动激活这些能力。
- **不引入未经证实的自动策略。** 会话计划不自动变项目待办;生成文件不自动上传到个人/项目资料;不按轮数/复杂度硬编码拆解或沉淀;不把整个资料库作为每次请求默认上下文。
- **调度是本地策略。** 主进程运行时每秒检查;默认 60 秒迟到容差,超出后记录 missed 并跳过,不自动重试,重启未完成记录标为中断失败。同一任务不重叠,不代表已实现原版服务端队列/全局并发策略。应用完全退出后不执行。以上包括迟到容差均是 YCode 本地实现,原版时区/DST/漏跑/重试行为仍需 REA 证据验证。
- **视觉与交互未完整 1:1。** 列表与表单复用现有 token 及已回收尺寸依据;编辑器存在本地执行设置扩展、原生选择/日期控件、模板图标和未采集状态等差异。已知下一批还须对齐原版默认 fullAccess 与保存确认、禁止遮罩/Escape 关闭的编辑器约束(本批默认 default-sandbox 且允许关闭)、有效期到期时的页面刷新边界。尚缺同数据/同主题/同视口的原版像素及动效验收,不能把本批功能通过标记为完整复刻。
- 云成员协作、远端通知与产品侧子代理调度继续按用户原授权暂后。安全中心另行拆解。
## 最终报告路径
- 最终全链路:`.tmp/three-modules-x8nrxz/report.json`;截图 `library-light.png`、`library-dark.png`、`automations-light.png`、`automations-dark.png`、`automation-editor-dark.png`。先前通过报告 `.tmp/three-modules-Hf6e02/report.json` 保留。
- 最终构建日志:`.tmp/three-modules-build.log`。
- 独立窗口回归:广播关闭对象访问隔离后,`npm run test:task-window` 通过;报告 `.tmp/task-window-stream-fRrpwz/report.json`。真实 OMP 验证窗口内发送/流式/取消、关闭发送窗口后持久化终态、重开活跃回合及双窗口同步。首轮超时保留在 `.tmp/task-window-stream-Gw2hxF/failure.json`,不抹去失败记录。
@@ -0,0 +1,41 @@
# 上下文用量查询、恢复与分类修复
用户确认开发:修复用量无法获取、保存与时效问题,并显示系统提示词、工具、对话、连接器、技能的使用明细。此前[复核记录](上下文用量获取复核-2026-10-18.md)是诊断阶段,本文记录本轮实现。
## 已实现
- 新增 `session:context-usage` / `window.ycode.session.contextUsage(id)` 查询。优先使用当前消息的合法样本;旧记录可从对应 OMP 原生分支恢复最近请求输入数。查询不会创建、重建或切换 Agent,也不发送模型请求。
- 恢复校验 YCode 当前助手/请求、前一条用户消息的 nativeEntryId、应用自己的规范 transcript 路径及 native parent 链。兄弟分支有歧义、最终请求失败、最新回合尚未形成样本时不拾取更早的数字。扫描上限 16 MiB。
- 上游 input 与缓存 input 合计作为输入占用,output 不混入;工具循环使用最后一份样本,不能累加。未知历史窗口保持未知,仍展示已知 token 数;符合相同会话、模型与输入样本的已运行内核可补 runtime 窗口,读取超时最多约 1.8 秒。
- 恢复样本在原有 session 写锁内再次核对当前助手、请求和运行状态后缓存;不改变任务更新时间和侧栏顺序。查询期间新建回合、切分支或删除任务时,旧结果不能覆盖新状态。
- 增加 OMP `before_provider_request` 观测扩展,采集同一次实际 provider payload 的分类计数;只回传数字和来源元数据,不回传系统提示词正文。备用 Gateway 同样观察最终的 Chat Completions / Responses 请求体,不改变传输内容。
- 分组包含系统提示词、工具声明/调用/返回、对话、MCP、技能,另有非零才显示的“未归类”。来源采用本项目注入上下文和工具元数据;工具未调用时仍计入声明,历史工具结果仍计入当前请求。图像等无法计数的输入标记为部分统计。
- 分类是独立的 **UTF-8 字节估计**,不是供应商分项账单或模型专用 tokenizer 的精确结果。没有强制将分类之和缩放成上游总量。提供商隐藏封装、模型特定分词和多模态仍会造成差异。
- `context_breakdown` 与 `usage` 分别进入助手消息与 checkpoint,保留请求、顺序、采样时间、模型、窗口和来源。下一份准备输入使上一份总量失效;重复或较旧的分类事件不能抹掉较新的真实总量。非法或不属于该消息的持久化样本不复用。
- 界面保留原版 32px 入口和 280px 明细面板,显示分类的 `≈` 标记、来源、采样时间和刷新入口。缺分母显示“未知”;没有真实 usage 但已有分类时显示本地估计;换模型后保留的历史数据标记“上次请求”。查询结果与会话/最新消息/模型/采样身份绑定,防止迟到响应覆盖新输入。
- 每个回合的观测配置在结束后删除。长期保留的是计数快照;没有把 provider payload 或 API Key 写入统计结果。
## 实现边界与依据
新增主进程模块:`context-usage-service.ts`(查询与恢复)、`context-usage-estimator.ts`(来源分类及估计)、`omp-context-observer.ts`(请求前观测)。接入 `chat-turn-service.ts`、OMP 事件适配、备用 Gateway、共享消息投影、preload/IPC 和输入区明细。
扩展能力依据 OMP **v18.6.3** 的[公开扩展类型](https://github.com/can1357/oh-my-pi/blob/v18.6.3/packages/coding-agent/src/extensibility/extensions/types.ts)中 `before_provider_request` / `payload` 和[CLI 参数](https://github.com/can1357/oh-my-pi/blob/v18.6.3/packages/coding-agent/src/cli/args.ts)的扩展加载能力,并通过本机实际 OMP 请求验证。没有将旧 Ygent 文档当作当前实现证据,也没有读取或新采集 WorkBuddy。面板原有几何沿用固定 REA `ev_2569313a7a0aa901c5a3b8dc1d7fd190109b9006c915830109b62195f312376a`;新增来源说明是本地统计功能适配,不宣称全状态逐像素 1:1。
历史记录若未保留当次完整输入,只恢复有证据的总量;不使用今天的提示词、工具或模型窗口反推过去的分类。子 Agent 私有输入及服务端隐藏提示没有被虚构成已知明细。
## 验证
- `npm run test:context-usage`:原生缓存输入合计、最后工具轮次、兄弟分支拒绝、失败终态、非法路径、缺分母、已缓存样本、运行中不恢复、迟到查询与新回合竞争、来源分类、多模态部分统计、独立总量、重复事件及 Chat/Responses 最终请求体观察均通过。
- `npm run test:chat`、`npm run test:persistence`、`npm run test:conversation`、`npm run test:conversation-ipc` 通过。保存测试明确核对来源、请求/模型身份、采样时间与落盘一致。
- `npm run build` 包含 TypeScript 检查,生产构建通过;最终日志 `.tmp/context-usage-build-final.log`。
- 实际隔离 Electron / preload / IPC / OMP + 受控 localhost SSE 的最终专项报告 `.tmp/composer-context-X5SBih/report.json` **60 项通过、0 renderer 错误**,含实际请求前分类、上游用量、两套主题、模型窗口变化、重载、后续无 usage 请求、原生旧记录恢复、查询不创建内核及缓存元数据不重排任务。主上下文已查看明细浅色、深色截图;最终截图 `context-breakdown-light.png`、`context-breakdown-dark.png`、`context-native-recovered.png` 和 `context-usage-restored.png` 留存于同一报告目录。
测试使用隔离用户目录,没有修改用户实际会话、模型配置或向远端供应商发送测试消息。原生窗口查询也不因打开统计而重启内核。
中间测试记录:旧测试曾要求“最新无 usage 时取更早样本”和“缺分母隐藏所有数字”,本轮按修复后的语义更新断言;新 Responses 夹具曾遗漏合法成功终态的 status,修正夹具后双协议测试通过;暗色样式与请求几何仍保留检查,没有移除功能验收以绕过失败。
本轮子代理通道因模型切换后返回任务 sealed 错误,实施、集成与最终验收由主上下文接续完成。保留工作区其它未提交改动,没有新增提交。
## 用户窗口
生产资源已构建,当前用户窗口没有被强制终止,以免清空不可读取的未发送草稿。本次包含主进程、preload 和 renderer 改动,保存草稿后需要完整退出并重启 YCode;仅刷新界面不能加载全部修复。
@@ -0,0 +1,46 @@
# 上下文用量获取、计算与保存复核
> 后续用户授权的实现已完成,见[用量与分类修复记录](上下文用量与分类修复-2026-10-18.md)。下文保留当时的只读发现;“尚未实现”是诊断阶段状态。
用户要求查看当前用量无法获取及保存逻辑,确认是否可以计算。本轮为只读诊断;未修改产品代码、用户数据或发送模型消息。
## 实查结果
1. 用户数据目录最近五个会话的 YCode `sessions.json` 均没有 `contextUsage`,但其用户消息记录的原生 OMP transcript 存在。最近 `hi` 会话的原始助手消息有 input 4009、cacheRead 1152、output 125,最近请求输入占用为 5161,`contextSnapshot.promptTokens` 同为 5161。
2. 将该 transcript 复制到隔离目录,以独立 HOME、仅指向 localhost:1 的无鉴权 profile 启动 OMP,只调用 `get_state`,未调用 prompt,未接触原文件。实测返回 `contextUsage:{tokens:5161,contextWindow:262144,percent:1.9687652587890625}`。报告 `.tmp/context-state-readonly-Fv6wfQ/report.json`。窗口 262144 来自此次隔离 profile(与用户当前配置相同),这不是对历史模型窗口从未修改的证明。
3. 当前实际应用主进程于 2026-10-17 19:51 启动,早于 `chat-turn-service.ts` 20:39 的保存修复;该进程尚未完整重启。不能用它生成的旧数据证明最新持久化字段仍然丢失,也不能认为更新 renderer 已加载主进程修复。
## 已确认的产品缺口
- `session:messages` 只读取 YCode 保存的消息;`lastContextUsage` 只扫描这些消息。没有查询内核状态或从原生记录恢复统计的入口。无旧快照时 UI 提示“请稍后重试”,实际上没有重试动作。
- 新发送未清除/标识上次请求统计;最新助手无 usage 时,恢复函数还会跳过它,继续拾取更早的样本。这会把更早请求的数据呈现为当前用量。
- 现有保存记录只有 usage 与冻结窗口,没有采样请求、原生分支、采样时间及来源标识。它适合作为历史样本,不能单独承担实时上下文查询。
- 新版 usage 事件已能进入助手消息、checkpoint 及磁盘,相关受控回归已通过;主要问题是获取/恢复路径及样本时效语义,不应笼统认定 JSON 保存损坏。
## 改法
建立独立的上下文用量查询,优先读取已绑定当前会话的 OMP `get_state.contextUsage`,无需为了数字再发模型消息,也不累计多次请求。读取必须校验会话/模型/分支身份,不为查询随意切换、重建或重启正在工作的内核。
未运行的旧会话可从当前保留分支的原生记录恢复最近请求真实输入量;恢复需绑定相应助手消息和原生分支,不用当前模型配置冒充未知的历史窗口。若需重新计算,则由内核针对实际输入计算,并将估计来源与上游 usage 区分。单看界面正文不足以精确还原隐藏提示、工具定义、图像与协议封装。
保存保留为缓存/历史依据,增加来源、采样时间、请求及分支身份;新回合、换分支、压缩和换模型时重新核对有效性。无法取到新统计时明确显示“上次统计”或“暂无统计”,不能无提示地复用更旧样本。没有分母时仍可展示已知 token 数,不应将整个数值隐藏为“无法获取”。
分类数值目前没有来源,不由总量分摊伪造。此次验证只证明 OMP 已保存的最近输入用量可恢复,不代表已实现每次编辑草稿后的全输入实时精确分词。
此前七项记录中的 N04 代表新样本保存与指定恢复测试通过;以上旧会话恢复、查询和时效缺口尚未修复,不能将它表述为上下文用量功能完整完成。
## 分类明细追查:系统提示词、工具、对话、连接器与技能
用户进一步询问能否获得这类明细。实测报告 `.tmp/context-breakdown-readonly-6qX2rb/report.json`:在另一隔离副本上仅执行 get_state、set_host_tools(声明一个测试工具,未调用工具)和 get_messages,没有 prompt 或模型请求。
- `get_state.systemPrompt` 可读取当前内核系统提示词,本例为两个字符串的数组;报告只记录类型/数量,不输出正文。
- `get_state.dumpTools` 提供当前工具定义。本例以 `--no-tools` 启动时为空,注册测试工具后返回一个包含 `name/description/parameters` 的对象。
- `get_messages` 返回 user/assistant 等内容块,可识别正文、thinking、toolCall 等;原始助手记录保留 usage 与 `contextSnapshot`。
- 原始 `contextSnapshot` 有 `promptTokens/nonMessageTokens/compactionEpoch`,本例非消息部分为 3306;它不是“系统提示词独占 3306”,不能据此拆出各类。
- 关键时效限制:注册新工具前后 `contextUsage.tokens` 仍是 5161。已证该值在此路径中复用最近请求统计,不是配置变化后即时重算的全输入总量。此前“get_state 负责当前用量”的建议须加来源/采样身份约束,不能把它包装成任意时刻的实时精确分词。
当前 YCode 自有来源也足以做归类:OMP 回合注册实际 tools;`createCapabilitySnapshot` 保有技能 references/注入正文、专家身份及 MCP tool 的 server/tool 元数据;工具调用和返回能按 toolCallId 对应。分类应按来源,不应仅按 role:技能/专家指令可能被注入 user 文本,仍归入其指令来源,避免重复计数。
结论:可以实现这些明细,但当前产品尚未接入分类采集/分词/IPC,内核状态接口也没有直接给出五类 token 账单。可靠实现要捕获同一次实际准备发送的输入(系统提示词、最终工具定义、当前消息分支和实际注入能力),按模型可用分词器独立计数并保留来源/精度标识。工具未被调用时,其定义仍可能占输入;历史工具返回在停用连接器后仍可能留在输入中。提供商隐藏封装及子代理私有上下文不能凭空计入。
总量保留上游实测/原生快照;分类独立标注本地分词计数或估算,合计差异列为未归类/协议开销,不把分类按比例缩放到总量,也不把“总数减已知数”伪装成已证实系统提示词。旧历史缺少当时完整输入时只能显示可恢复部分,不能以当前提示词和工具反推过去。
@@ -0,0 +1,53 @@
# 总上下文预算与压缩阈值(2026-10-18)
## 最新语义
用户订正:模型表单的“上下文”表示输入与输出共用的总窗口;“输出”配置最大输出,输入预算和压缩阈值动态计算。上一轮把500K作为输入、另加128K得出628K的实现已撤销。
- `C`:总上下文,500K快捷项为500,000。
- `O`:最大输出,128K快捷项为128,000。
- `I = min(C - O, 模型目录原始输入上限)`:输入预算;没有单独目录上限时取`C - O`。系统提示词、工具、历史、附件和本轮消息共用该预算。
- 总上下文与输出各自校验目录上限,输出必须小于总上下文;总窗口不会被加上输出后扩大。
界面为“上下文 / 输出”,下方实时显示可用输入和自动压缩阈值。空值采用自动默认,目录默认输入也扣除输出预留。
## 原规则与本次调整
本机OMP CLI为18.6.3。只读查询原默认配置:固定/百分比阈值均为`-1`,`reserveTokens`未设置,自动压缩与回合内检查开启,`keepRecentTokens=20000`。
同版本[压缩源码](https://github.com/can1357/oh-my-pi/blob/v18.6.3/packages/agent/src/compaction/compaction.ts)确认:默认预留为`max(floor(C × 15%), 16384)`,阈值为`C - 预留`,已用上下文严格大于阈值时触发。默认绝对预留对小窗口不可用时,恢复为至少1 token的15%比例预留。
原规则没有自动预留用户配置的完整输出预算。500K总窗口原默认阈值为425K,而128K输出只允许372K输入。本次计算:
```text
P = max(1, floor(C × 15%))
D = max(P, 16384)
如果 D >= C - P,则 D = P // 小窗口恢复
R = max(D, O, C - I)
T = C - R
```
`R`是内核预留,`T`是压缩阈值。常规窗口可写为`T = min(I, C - max(floor(C × 15%), 16384))`。
| 总上下文 | 输出 | 可用输入 | 压缩阈值 |
| --- | --- | --- | --- |
| 500,000 | 128,000 | 372,000 | 372,000 |
| 500,000 | 64,000 | 436,000 | 425,000 |
| 1,050,000 | 128,000 | 922,000 | 892,500 |
| 8,192 | 1,234 | 6,958 | 6,958 |
第三行按GPT-6.1 Sol目录上限。`keepRecentTokens`是压缩时保留近期消息的预算,不从阈值中重复扣除。默认异步压缩可提前预计算,提前量为`clamp(T × 0.125, 8192, 32000)`;500K/128K约从340K开始后台准备,正式阈值仍是372K,见[同版本文档](https://github.com/can1357/oh-my-pi/blob/v18.6.3/docs/compaction.md)。
## 实施与验证
- `src/shared/model-token-limits.ts`统一预算;`src/shared/models.ts`增加`tokenBudgetVersion: 2`,标识总窗口权威、输入派生。
- `src/main/models.ts`保存/运行时共用计算。旧配置原第一项迁移为总窗口,丢弃隐藏相加值;列表读取不改文件或密钥,保存后带版本,重载不重复扣除输出。
- `src/shared/model-metadata.ts`从默认总窗口扣除默认输出,保留原始上限与完整价格档。
- `src/main/omp-model.ts`写入模型总窗口/输出及独立`agent/config.yml`,配置预留、固定阈值和百分比回退;`src/main/omp.ts`通过`--config`传入。预算参与指纹,进行中的回合保持原快照。
- `src/renderer/src/components/ModelSettingsPanel.tsx`接两字段、实时预算说明、500K/128K选项及整数校验。
完整构建与类型检查通过(`.tmp/context-total-build.log`);元数据/预算13项、目录边界和小窗口通过(`.tmp/context-total-metadata.log`);注册/旧628K记录迁移/冷加载/无效写入保护通过(`.tmp/context-total-registry.log`)。OMP取消/错误、持久化、用量回归通过(`.tmp/context-total-omp.log`、`.tmp/context-total-persistence.log`、`.tmp/context-total-usage.log`)。原生多协议14项通过(`.tmp/context-total-protocols.log`)。
真实模型能力22项通过(`.tmp/model-capabilities-M077wK/report.json`)。真实隔离生产应用104项通过、0 renderer错误(`.tmp/custom-model-settings-oHMHAB/report.json`):安装OMP配置读取返回372,000阈值,profile总窗口500,000,实际请求输出128,000,用量分母500,000。实时切换输出、表单重开和越界保护均通过;已查看`custom-editor-500k-128k-light.png`。
验收使用短提示与本地受控上游,没有向收费服务发送500K正文,尚未完成真实长上下文跨阈值压缩产物验收。备用HTTP路径没有OMP完整自动压缩,本次只统一其预算与请求上限。未采集WorkBuddy或新增1:1结论,未重启用户窗口、未修改其模型数据;完整重启后加载本轮代码。
+19
View File
@@ -0,0 +1,19 @@
# YCode v0.01 Windows 分发包
> 可重复执行的完整流程见 [打包流程 SOP](打包流程SOP-2026-10-18.md)。
用户要求制作版本号 v0.01 的客户端分发包。文件名使用 v0.01;npm/Electron/Windows 的内部语义版本为 `0.0.1`,与原 `0.1.0` 开发版本不同。
构建命令:`npm run dist:installer`。输出目录 `release/`,安装程序 `YCode-v0.01-Setup-x64.exe`,展开后的客户端 `win-unpacked/YCode.exe`。按当前工作区构建,包含尚未提交的现有功能;没有创建 Git 提交或发布到外部。
安装程序使用 NSIS,Windows x64、当前用户安装,可选择安装路径,提供桌面和开始菜单快捷方式,卸载不删除用户数据。没有配置代码签名证书,因此属于未签名的本地测试分发版本。
包含应用主进程、preload、两份 renderer、生产依赖、图标、models.dev 元数据、原生沙盒0.7.0、内置Git资源,以及内置 OMP 18.6.3。模型Key、个人会话、项目和用户专家包都在用户数据目录中,不复制到分发包。新用户需自行配置模型;本机已有数据继续沿用。
OMP 文件位于安装目录 `resources/omp/`,包含 `omp.exe`、MIT LICENSE、THIRD-PARTY-NOTICES.txt 和 runtime.lock.json。运行时依次支持 `YCODE_OMP_PATH` 显式覆盖、包内 OMP、旧 Ygent 安装和 HTTP 回退;正常安装不依赖外部 Ygent。新设备未预置本机的五个自定义专家或模型配置。
构建与实际包启动验证结果:OMP资产摘要、版本输出和真实RPC ready握手通过;最终安装程序246,498,049字节(235.08MiB),SHA-256为`4a8ceaeb1dd12bfc228911c2d586a7db2f4682b02b995c62602e7b14e6830184`。安装程序归档确认包含四个OMP文件;实际签名状态NotSigned。
正式YCode.exe包内容与启动共10项检查通过:版本、main/preload/renderer、生产依赖、本机配置与会话排除、模型元数据/内置OMP/沙盒/Git及摘要、真实exe启动和生产IPC、空用户数据隔离、0未捕获renderer异常及正常退出。生产IPC实际选择的OMP路径为包内resources/omp/omp.exe。报告`.tmp/release-v001-mfDVCw/report.json`,汇总`.tmp/release-v001-verification.json`;已查看实际客户端截图`packaged-client.png`。离屏启动使用软件渲染,不据此声称GPU硬件加速路径通过。
交付附`release/使用说明-v0.01.txt`及`release/SHA256SUMS.txt`。验证在隔离用户目录执行,不调用用户模型接口,不改当前开发客户端会话;包内可执行文件启动检查不等于已在干净Windows设备验收完整安装/卸载、GPU兼容和所有功能。
@@ -1,5 +1,7 @@
# 四位 WorkBuddy 专家迁入 YCode # 四位 WorkBuddy 专家迁入 YCode
> 2026-10-18用户定制更新:原四个已安装角色现已改为通用的探索、实现、验收和审查,并新增问题诊断员;完整原包已备份。见[通用开发角色替换](通用开发角色替换-2026-10-18.md)。下方保留2026-10-16原始迁移记录与REA证据,不代表当前定制包仍是原始字节。
2026-10-16,按用户指示将首席 C++ 架构顾问及高级项目经理关德豪、UI 设计师像素君、高级算法工程师小算迁入当前用户 YCode。所有原版包内容、清单和头像/字体读取均通过 REA;安装只使用已经导出并核对 SHA 的副本。 2026-10-16,按用户指示将首席 C++ 架构顾问及高级项目经理关德豪、UI 设计师像素君、高级算法工程师小算迁入当前用户 YCode。所有原版包内容、清单和头像/字体读取均通过 REA;安装只使用已经导出并核对 SHA 的副本。
## 已安装的真实内容 ## 已安装的真实内容
+82
View File
@@ -0,0 +1,82 @@
# 四模块拆解总览(2026-10-17)
> 当前推进范围:用户要求先梳理定时任务、资料库、项目并准备开发,具体契约、文件分工和验收门槛见[三模块开发计划](三模块开发计划-2026-10-17.md)。安全中心仍待后续取证,不混入这轮开发。
用户已明确订正:此前“安装中心”应为“设置 → 安全中心”。当前四模块为**安全中心、定时任务、资料库、更多→项目**;应用安装管理不在本次四模块范围。此前“仅记录、不拆解”限制已由用户授权取代;本轮只交付分析与后续任务,未修改业务代码。云端服务、产品侧子代理调度仍按此前要求暂缓。
## 当前结论
| 模块 | 原版证据现状 | YCode 现状 | 下一步要解决的核心问题 |
| --- | --- | --- | --- |
| [设置→安全中心](安全中心模块拆解-2026-10-17.md) | 范围已订正;实际页面内容与策略尚待REA取证,安装/发现应用证据不适用于本项 | `securityCenter`导航可见但未接入,无页面分支 | 先采原版实际分区/控件与行为,再逐项映射真实能力、接页面与执行策略并验收 |
| [定时任务](定时任务模块拆解-2026-10-17.md) | 页面、新建表单、频率与校验有REA证据;执行记录/生命周期/时间语义不齐 | 侧栏入口占位,缺少调度存储与执行服务 | 不只是复刻表单,还要定义时间规则、任务执行、运行记录、重启恢复及并发行为 |
| [资料库](资料库模块拆解-2026-10-17.md) | 本轮已补首页、类型菜单、个人新增菜单、骨架和本地产物页;完整编辑/导入/引用仍需补采 | 入口占位;可复用真实文件读取、产物预览和消息关联 | 个人资料、团队资料、本地产物分开;建立真实索引、最近/收藏/搜索及引用快照 |
| [更多→项目](项目模块拆解-2026-10-17.md) | 列表、新建、详情、任务页、指令和模板已有REA证据;协作与其余页签证据有限 | 本地项目、持久化、任务归属和指令注入已有通路;动态/计划/资产多为占位 | 补生命周期、配置选择、筛选和任务行;分清本地业务与云协作缺口 |
“有页面/源码证据”“YCode 已有功能”“功能测试通过”“全状态1:1通过”是不同状态。四个模块都没有完整1:1验收结论。
## 每个模块的实施拆分
### A. 设置→安全中心
1. 通过REA进入原版设置的`securityCenter`,取得实际页面分区、字段、默认值、说明与布局。
2. 逐控件核对修改、保存/取消、生效时机、持久化、确认、错误和不可用状态,不预设具体功能。
3. 用REA追踪对应脚本、CSS与宿主契约,区分页面偏好、实际执行策略和远端依赖。
4. 主上下文确定数据与执行边界,基于原版事实映射YCode真实能力;涉及策略时验证其在执行入口实际生效。
5. 验证重启、多窗口同步和错误恢复,再做深浅色、窄屏、键鼠及动效对照。
6. 旧安装中心分析退出本次任务;发现应用与能力中心独立保留,不计入安全中心进度。
### B. 定时任务
1. 列表/空态/模板入口、新建/编辑弹窗及字段校验。
2. 一次、间隔小时、每天、每周、双周、每月、每年及有效期规则。
3. 自动化配置存储、启停、编辑、删除、手动运行的真实状态机。
4. 本地调度服务:下一次触发、系统休眠/恢复、重启、错过触发和重复执行策略。
5. 复用现有模型/OMP/会话链路执行,生成真实运行记录,不以按钮Toast替代执行。
6. 记录详情、失败重试、运行中互斥、会话跳转、归档/删除。
7. 项目级定时任务与通知推送单独接线;远端推送按当前范围暂缓。
第4项为YCode必须做的工程设计,原版服务端的同名行为尚未证实。不能用设计建议冒充原版已采集规则。
### C. 资料库
1. library路由、资料库内侧栏、最近三页签、骨架/空/错误与主题。
2. 个人文档、CSV、文件夹、上传导入、链接的真实存储和操作。
3. 本地产物索引:工作空间→任务→文件;类型筛选、排序、收藏、搜索和失效文件。
4. 文档/表格/网页编辑与预览;先补编辑器证据,复用已有右侧预览宿主。
5. 最近访问、资料搜索、选择资料添加到对话;保存引用来源和内容版本。
6. 个人资料与项目资产关系通过显式关联表达;团队协作/分享/远端配额分开记录。
7. 逐状态几何、字体、图标、滚动、键鼠、响应式与动效比对。
### D. 更多→项目
1. 保留现有项目存储和任务绑定;补契约边界、列表空/加载/失败和创建全状态。
2. 项目卡片菜单与项目重命名、归档/删除等生命周期;副作用先取证后实现。
3. 指令、连接器、专家、技能的完整配置选择和保存,不停留于ID展示。
4. 任务状态/来源筛选、任务行菜单、首发占位、取消/失败和打开任务。
5. 动态、计划、资产分别建模并接真实数据;不能只保留页签和“尚未配置”。
6. 成员、角色、通知、cloud路由与协作作为云端依赖拆出,保持未接状态真实可见。
7. 统一对照四个页签、右配置栏、弹窗、浅深色和窄窗口。
## 共享契约与依赖
以下为YCode目标设计,不能作为原版后端结构结论:
- 工作空间代表本地工作目录;项目代表任务组织与配置;资料库集合代表资料组织。三者有显式关联,ID和生命周期独立。
- 定时任务描述触发与执行配置,一次运行产生独立运行记录,并关联实际会话。
- 专家/技能/连接器沿用已有能力类型与可用状态;项目与定时任务保存能力引用,发送时解析真实可用能力。此依赖不再扩展成应用安装管理任务。
- 对话产物和导入资料都可进入索引,但保留来源;发送到模型的引用冻结内容/版本,历史重载不能悄悄改请求内容。
- 页面状态、持久化、执行状态统一由主进程提供权威值;多窗口订阅更新,避免把定时任务UI计时器当调度器。
## 建议推进顺序与并行边界
1. **补证据并定契约**:主上下文统一数据关系、迁移、来源与状态定义;缺失原版实现只用REA。现有5.8.2有效证据继续使用,新补采明确5.8.3。
2. **三个并行工作面**:定时任务本地执行闭环;资料库本地产物/个人资料闭环;安全中心原版取证与能力映射。安全中心待证据明确后再分派实现;shared/main IPC由主上下文协调集成。
3. **项目收口与联动**:在已有项目实现上补缺,绑定前述能力、资料和自动化;先完成本地真实链路。
4. **逐状态对标**:同视口、同主题、同字体、同数据;截图/计算几何/状态序列与功能回归分别留证。云依赖未接不能计入完整1:1。
本轮不提供虚假的完成百分比或固定工期;各专项文档按证据、实现、占位、未知和验收门槛列出可分派工作。
## 本轮交付与验证边界
当前交付为三份已有模块拆解、安全中心范围订正与待拆解记录、总览和TODO/交接更新。此前安装中心文档保留为误解范围的历史,不计入当前四模块完成度。没有改业务代码、权限策略或用户配置;没有重新跑上一轮已通过的应用回归。
+92 -27
View File
@@ -1,5 +1,15 @@
# WorkBuddy 复刻 TODO(2026-10-17) # WorkBuddy 复刻 TODO(2026-10-17)
> 2026-10-18 表格与附件修复已实现:[实施与验证](表格与附件样式修复-2026-10-18.md)。恢复原版短语标签和19个SVG/31条规则,统一输入、只读、编辑及文件/图片/资源/技能;新增混排持久化、处理/失败、预览与边界验证。最终隔离应用44项、构建、保存及IPC回归通过,补预览暗色背景和编辑资源归属;原版全状态逐像素仍待验收。总对话回归另有沙箱命令旧断言失败,详见记录。当前用户窗口未强制重启,此批含preload/主进程,需完整重启。
> 2026-10-18 表格与附件样式复核:用户截图与保留 REA 宿主源码确认,此前表格选错引用渲染分支;输入外摘要横条应为输入内“会话表格”短标签。文件、图片、资源/技能及发送后的展示也有结构与样式差异,详见[专项核对](表格与附件样式核对-2026-10-18.md)。本轮仅核对、复现和记录,未修改业务代码;02 的相应视觉/组合验收重新打开,不再以旧卡片断言通过作为对齐依据。
> 用户追加七项:见[新增七项修复](新增七项界面修复-2026-10-17.md)。先完成三模块 UI 批次,再修复更多项目浮层、任务右键、通知遮挡、上下文统计、个性化输入、关于图标和权限切换。对应 N01~N07 单独追踪,保留精确像素/动效未知项,不将修复通过等同于完整 1:1;通知服务和沙箱内核仍未接入。
> 当前开发进展:用户授权开发后,定时任务真实执行、资料库本地产物浏览、项目首发/状态/配置修复已完成首批实现与隔离验收,见[首批开发记录](三模块首批开发记录-2026-10-17.md)。[三模块开发计划](三模块开发计划-2026-10-17.md)继续作为分批范围;个人资料/引用、项目待办和资产联动及完整1:1尚未完成,TODO20整体保持未完成。安全中心不在此批。
> 最新范围订正:用户确认第18项应为**设置→安全中心**。当前四模块为安全中心、定时任务、资料库、更多→项目,见[总览](四模块拆解总览-2026-10-17.md)。安全中心待REA采集与正式拆解,不能沿用安装中心分析的进度。此前 A01~A12 修复及21个专项回归见[修复记录](19项问题修复记录-2026-10-17.md),不代表所有原版视觉/动画/未知状态已完成。
本清单记录用户最初提出的 20 项问题及后续追加项,作为后续逐项拆解、实现和验收的工作入口。原始编号保持不变,新增需求顺延编号。本清单的建立与追加仅记录需求,不代表已完成对应采集或实现。 本清单记录用户最初提出的 20 项问题及后续追加项,作为后续逐项拆解、实现和验收的工作入口。原始编号保持不变,新增需求顺延编号。本清单的建立与追加仅记录需求,不代表已完成对应采集或实现。
## 执行约定 ## 执行约定
@@ -18,9 +28,9 @@
- [x] 将侧栏对话条目后的直接删除按钮替换为原版任务操作入口。 - [x] 将侧栏对话条目后的直接删除按钮替换为原版任务操作入口。
- [x] 通过 REA 对齐菜单的入口、位置、顺序、图标、文案、悬停、关闭、禁用和确认状态。 - [x] 通过 REA 对齐菜单的入口、位置、顺序、图标、文案、悬停、关闭、禁用和确认状态。
- [x] 接入:置顶、取消置顶、重命名、独立窗口中打开、打开文件夹、归档、删除任务。 - [x] 接入:置顶、取消置顶、重命名、独立窗口中打开、打开文件夹、归档、删除任务。
- [ ] 验收:置顶顺序和重命名持久化;独立窗口打开正确任务;文件夹指向正确工作目录;归档与删除行为独立;涉及运行中任务时有明确处理。 - [x] 验收:置顶顺序和重命名持久化;独立窗口打开正确任务;文件夹指向正确工作目录;归档与删除行为独立;涉及运行中任务时有明确处理。
状态:主体已实现;运行中任务的处理与逐像素截图比对未完成。 状态:主体、运行中任务处理和归档回访已实现并通过真实运行链路专项验收;原版运行中禁用菜单及确认弹窗仍缺实时 REA 像素帧,不能宣称该状态已完成逐像素 1:1。归档回访由任务分组标题的“已归档”入口承接,弹窗只显示 `archivedAt` 有值的任务,支持“恢复”和“查看(先恢复再打开)”,独立任务与空间任务分别标识;`node scripts/verify-archived-tasks.cjs` 25/25 PASS,`npm run typecheck`、`npm run build`、`node scripts/verify-task-menu.cjs`、`node scripts/verify-sidebar-footer.cjs`、`node scripts/verify-conversation-scrollbar.cjs` 通过。
原版证据:WorkBuddy 5.8.3,REA Evidence `ev_f5e9d71a676c453daab40928acfb33aeabae68ba57ffda342380cead7f1a1fe2`(任务菜单打开态),归档 `.tmp/rea-css-evidence/ev_f5e9d71a….json`。观察范围:任务列表首条任务的菜单容器、9 个条目、2 条分隔线、逐项 SVG 图标、几何与 computed style;未覆盖:菜单在窗口边缘翻转、运行中任务状态、分享/保存到工作空间/批量操作三类条目的行为。 原版证据:WorkBuddy 5.8.3,REA Evidence `ev_f5e9d71a676c453daab40928acfb33aeabae68ba57ffda342380cead7f1a1fe2`(任务菜单打开态),归档 `.tmp/rea-css-evidence/ev_f5e9d71a….json`。观察范围:任务列表首条任务的菜单容器、9 个条目、2 条分隔线、逐项 SVG 图标、几何与 computed style;未覆盖:菜单在窗口边缘翻转、运行中任务状态、分享/保存到工作空间/批量操作三类条目的行为。
@@ -30,18 +40,20 @@
实现:`Sidebar.tsx`(行结构、置顶排序、行内重命名、菜单挂载)、`task-menu.tsx`(菜单容器与条目)、`task-menu.css`(原版规则与深浅色取值)、`task-menu-icons.tsx`(逐项 SVG 原样复刻)、`stores/chat.ts`(setPinned / setArchived / renameSession)、`main/sessions.ts`(pinnedAt / archivedAt 持久化与排序)、`main/ipc.ts`(pin / archive / openFolder / openWindow)、`main/index.ts`(任务独立窗口)、`shared/types.ts`+`shared/ipc.ts`+`preload/index.ts`(契约)。 实现:`Sidebar.tsx`(行结构、置顶排序、行内重命名、菜单挂载)、`task-menu.tsx`(菜单容器与条目)、`task-menu.css`(原版规则与深浅色取值)、`task-menu-icons.tsx`(逐项 SVG 原样复刻)、`stores/chat.ts`(setPinned / setArchived / renameSession)、`main/sessions.ts`(pinnedAt / archivedAt 持久化与排序)、`main/ipc.ts`(pin / archive / openFolder / openWindow)、`main/index.ts`(任务独立窗口)、`shared/types.ts`+`shared/ipc.ts`+`preload/index.ts`(契约)。
验证:`scripts/verify-task-menu.cjs` 在真实隔离 Electron(真实 preload / IPC / 文件系统)中 33 项断言全部通过,覆盖菜单文案与顺序、两条分隔线、逐项几何与配色、锚点偏移、遮罩关闭、置顶与取消置顶持久化、行内重命名持久化、归档保留会话与消息、删除真正移除、打开文件夹指向真实 cwd、独立窗口创建且携带 `window=task&taskId=…`。报告与截图:`.tmp/task-menu-Moulvm/`。`npm run typecheck`、`npm run test:conversation`、`npm run build` 通过;`npm run test:sessions` 在干净 HEAD 上同样报 `yaml` 动态 require 失败,与本次改动无关。 验证:`scripts/verify-task-menu.cjs` 在真实隔离 Electron(真实 preload / IPC / 文件系统)中通过,覆盖菜单文案与顺序、两条分隔线、逐项几何与配色、锚点偏移、遮罩关闭、置顶与取消置顶持久化、行内重命名持久化、归档保留会话与消息、删除真正移除、打开文件夹指向真实 cwd、独立窗口创建且携带 `window=task&taskId=…`,并验证归档与删除均先打开确认弹窗。新增 `scripts/verify-task-running-menu.cjs` / `npm run test:task-running` 在真实隔离 Electron、真实 OMP 和受控 SSE 上游中 21 项断言全部通过:运行中归档在 UI 与主进程 IPC 均被拒绝且保留任务;取消“停止并删除”保留运行;确认后移除会话并终止上游流;480×176 弹窗与共享 24px shell、标题、描述、按钮文案、禁用态和浅色 token 均通过;无 renderer 错误。报告与截图:`.tmp/task-running-yNfEFt/report.json`、`running-menu.png`、`running-delete-confirm.png`。`npm run build`、`node scripts/verify-task-menu.cjs`、`npm run test:task-running` 通过。
遗留:未覆盖运行中任务的置顶/归档/删除副作用与提示;未做原版逐像素截图比对;置顶标识图标取自同批归档的成员图标,原版置顶行的独立截图证据未采集。 遗留:未做原版逐像素截图比对;置顶标识图标取自同批归档的成员图标,原版置顶行的独立截图证据未采集。当前 REA 的 `capture_web_screenshot` 对 WorkBuddy Electron `file://` target 返回 `target_not_allowed`,`capture_browser_scenario` 仅接受 HTTP(S) 起始页,因此运行中禁用菜单、确认弹窗以及鼠标悬停态尚无实时原版像素帧;现有视觉依据为固定 REA Evidence 的 DOM/CSS/几何证据。
### 02. 表格“添加到对话”及引用结果 ### 02. 表格“添加到对话”及引用结果
- [x] 对齐表格上方“添加到对话”按钮的字体、图标、背景、边距、出现条件及交互。 - [x] 对齐表格上方“添加到对话”按钮的字体、图标、背景、边距、出现条件及交互。
- [x] 对齐点击后输入区内引用表格的呈现,包括结构、预览、移除和与已有草稿的组合方式。 - [x] 接入原版表格短语标签,改正此前引用卡片分支,支持移除及文字中间插入;已验证源几何和已有草稿保留。
- [x] 对齐发送后用户消息中的表格引用显示。 - [x] 用户消息/旧记录改用表格图标与“会话表格”短标签;Markdown快照仍作为真实载荷完整保存。
- [x] 验收:引用正确表格内容,不丢失已有草稿;输入区、发送结果及历史重载三种状态一致。 - [x] 验证正确引用、发送、混排原子保存及重启恢复;新增实际隔离应用专项44项通过。
- [x] 统一文件、图片、资源/技能标签、浅深色、折行、粘贴/拖放的处理/失败及只读显示,详见[修复记录](表格与附件样式修复-2026-10-18.md)。
- [ ] 原版同尺寸、同内容的全状态逐像素与动效验收;原生选择器内部逐项准备进度、完整撤销/剪贴板等未验证状态继续追踪。
状态:主体已实现并做了真实窗口回归;仍有两处原版证据缺口(见“遗留”)。 状态:2026-10-18 本轮实现及限定范围验证已完成,原版完整1:1仍未验收。以下保留旧轮次证据、实现和测试历史;旧横条/原始Markdown标签断言已由新专项替换,不能再作为正确视觉依据。
原版证据: 原版证据:
- 渲染器实现:WorkBuddy 5.8.3 渲染器(REA 回收 `.tmp/wb-src-DbE1_mgM.js`)中的 `TableMessageQuoteAction`、`createTableMessageQuoteSourceId`、`getTableMessageQuoteMessageId`、`MessageQuoteAddIcon`、`MessageQuoteTextIcon`、`ChipCloseIcon`、`QuoteReferenceCard`、`InputBannerRail`、`MessageQuotePhraseTag`、`convertUserFrame`/`ContextChipsRenderer`。 - 渲染器实现:WorkBuddy 5.8.3 渲染器(REA 回收 `.tmp/wb-src-DbE1_mgM.js`)中的 `TableMessageQuoteAction`、`createTableMessageQuoteSourceId`、`getTableMessageQuoteMessageId`、`MessageQuoteAddIcon`、`MessageQuoteTextIcon`、`ChipCloseIcon`、`QuoteReferenceCard`、`InputBannerRail`、`MessageQuotePhraseTag`、`convertUserFrame`/`ContextChipsRenderer`。
@@ -152,7 +164,7 @@
1. 回退弹窗沿用 YCode 现有 modal 样式,未采集原版弹窗(宿主 Modal:标题/正文/产物清单/底部按钮)的几何与动画;待后续按原版逐像素对齐。 1. 回退弹窗沿用 YCode 现有 modal 样式,未采集原版弹窗(宿主 Modal:标题/正文/产物清单/底部按钮)的几何与动画;待后续按原版逐像素对齐。
2. 原版失败路径以 toast 呈现 `rewind.error.rollbackFailed`(对话回退失败,请稍后重试。);YCode 当前在弹窗内联展示原始错误文本。 2. 原版失败路径以 toast 呈现 `rewind.error.rollbackFailed`(对话回退失败,请稍后重试。);YCode 当前在弹窗内联展示原始错误文本。
3. 原版逐产物 fallback 状态(回退至 {time} 版本 / 历史版本已被删除,将保持当前版本 / 该产物无历史版本,回退后文件仍会保留在目录中)依赖服务端产物版本历史;YCode 只有工具写入前后快照,暂映射“将被删除 / 回退至历史版本”。 3. 原版逐产物 fallback 状态(回退至 {time} 版本 / 历史版本已被删除,将保持当前版本 / 该产物无历史版本,回退后文件仍会保留在目录中)依赖服务端产物版本历史;YCode 只有工具写入前后快照,暂映射“将被删除 / 回退至历史版本”。
4. 原版编辑提交时会先弹 `rewind.edit.*` 询问是否回退产物历史版本(回退 / 不回退),YCode 编辑提交固定回退产物(`rollbackEditedArtifacts`);差异记入 TODO 03 追补。 4. 已修复原先固定回退产物的差异:编辑带产物消息先选择“回退 / 不回退”,回退也可仅处理对话。真实窗口与 IPC 故障补偿回归见本轮修复记录;原版宿主弹窗像素仍待验收。
5. 原版在 `hasMoreRequests`(分页历史未全部加载)时首条已加载消息也可显示回退入口;YCode 全量加载历史,按 `requestId !== 首条用户消息 requestId` 判定。 5. 原版在 `hasMoreRequests`(分页历史未全部加载)时首条已加载消息也可显示回退入口;YCode 全量加载历史,按 `requestId !== 首条用户消息 requestId` 判定。
### 06. 对话区域滚动条 ### 06. 对话区域滚动条
@@ -252,7 +264,7 @@
遗留: 遗留:
1. 原版「发送后」逐帧等待态(先后顺序、每段时长)无 REA 运行时证据(探针限制见上),本项只对齐可证部分。 1. 原版「发送后」逐帧等待态(先后顺序、每段时长)无 REA 运行时证据(探针限制见上),本项只对齐可证部分。
2. 发送失败时仍会留下一个空会话(`session.create` 已成功、回合未开始);原版在准备失败时是否保留会话未采集。 2. 已修复创建后启动失败的空会话状态:仅由主进程原子确认空闲且无消息的本次新会话可清理,草稿/附件/引用恢复;已有消息或运行时保留并反馈错误。原版在准备失败时是否保留会话仍未采集。
3. 启动阶段目前只使用 `acquiring_runtime` 一条;原版多阶段(expert → … → checking_launch_config)的发出方、顺序与时长未采集,不按字符串表臆造。 3. 启动阶段目前只使用 `acquiring_runtime` 一条;原版多阶段(expert → … → checking_launch_config)的发出方、顺序与时长未采集,不按字符串表臆造。
4. 发送窗口内没有「停止」入口(`停止` 只在回合运行后出现,沿用既有实现);原版在准备阶段是否可直接取消未采集。 4. 发送窗口内没有「停止」入口(`停止` 只在回合运行后出现,沿用既有实现);原版在准备阶段是否可直接取消未采集。
@@ -279,12 +291,12 @@
- `workbuddy/user-menu/account-menu-source.css`(新增):逐条落地回收规则并把 8 个侧栏作用域缺失的 token 按回收取值补齐;所有规则收敛在 `.ycode-account-menu-scope` 下,避免与其它 `.wb-popover` 作用域互相污染。 - `workbuddy/user-menu/account-menu-source.css`(新增):逐条落地回收规则并把 8 个侧栏作用域缺失的 token 按回收取值补齐;所有规则收敛在 `.ycode-account-menu-scope` 下,避免与其它 `.wb-popover` 作用域互相污染。
- `workbuddy/user-menu/account-menu-icons.tsx`(新增):设置/外观/记忆与进化/检查更新/退出登录 5 个 16×16 线性图标,暂用 Lucide(MIT)占位并在文件头注明原版路径待回收。 - `workbuddy/user-menu/account-menu-icons.tsx`(新增):设置/外观/记忆与进化/检查更新/退出登录 5 个 16×16 线性图标,暂用 Lucide(MIT)占位并在文件头注明原版路径待回收。
- `components/Sidebar.tsx`:底部账号触发器改为打开账户菜单(`aria-haspopup="menu"`、`aria-expanded`,记录触发器几何供 popover 落位);收起侧栏时关闭菜单;菜单渲染在 footer 内(portal)。 - `components/Sidebar.tsx`:底部账号触发器改为打开账户菜单(`aria-haspopup="menu"`、`aria-expanded`,记录触发器几何供 popover 落位);收起侧栏时关闭菜单;菜单渲染在 footer 内(portal)。
- 内容与真实性:头部=mock 账号头像与名称(副标题标注「账号为 mock 数据,尚未接入登录」);计费摘要=套餐/余额两格,值显示「未接入」,升级套餐/购买积分按钮 `disabled` 并带「尚未接入账号与计费服务」提示;设置 → `openSettings('model')`、外观 → `openSettings('appearance')`(值跟随当前主题,显示浅色/深色);记忆与进化、检查更新、退出登录标注「待拆解」/「未接入」,点击只提示,不假装成功。 - 内容与真实性:头部=mock 账号头像与名称(副标题标注「账号为 mock 数据,尚未接入登录」);计费摘要=套餐/余额两格,值显示「未接入」,升级套餐/购买积分按钮 `disabled` 并带「尚未接入账号与计费服务」提示;设置 → `openSettings('model')`、外观 → `openSettings('appearance')`(值跟随当前主题,显示浅色/深色)、记忆与进化 → `openSettings('memory')`;检查更新、退出登录仍标注「未接入」,点击只提示,不假装成功。
验证:新增 `scripts/verify-account-menu.cjs`(真实隔离 Electron + 真实 preload/IPC/渲染进程 + 真实设置窗口)63 项断言全 PASS: 验证:新增 `scripts/verify-account-menu.cjs`(真实隔离 Electron + 真实 preload/IPC/渲染进程 + 真实设置窗口)63 项断言全 PASS:
- 几何/样式:popover 320 宽、左边缘与触发器对齐(实测 x=8)、底边距触发器上沿 8px(popover bottom 846 / 触发器 top 854)、padding 8 / gap 8 / radius 16、浅色背景 `rgb(247,247,247)`、边框计算值 1px `rgb(222,222,222)`、双层阴影、`overflow:hidden auto`、`max-width` 1312px、`max-height` 870px(视口 902)、`role=menu`;三张卡片 302 宽 / padding 8 / radius 8 / 白底。 - 几何/样式:popover 320 宽、左边缘与触发器对齐(实测 x=8)、底边距触发器上沿 8px(popover bottom 846 / 触发器 top 854)、padding 8 / gap 8 / radius 16、浅色背景 `rgb(247,247,247)`、边框计算值 1px `rgb(222,222,222)`、双层阴影、`overflow:hidden auto`、`max-width` 1312px、`max-height` 870px(视口 902)、`role=menu`;三张卡片 302 宽 / padding 8 / radius 8 / 白底。
- 头部与计费:min-height 56 / padding 8 / gap 8、头像框 40×40、初始头像圆形 `rgb(0,86,212)`、名称 16/500/24 且等于 mock 账号名;两个计费单元各 141×86、padding `4px 8px 8px`、title 12/400/20、value 14/500/22、动作行 margin-top 8 / 按钮高 24 / `disabled`、文案「升级套餐」「购买积分」;值恰为「未接入」,整份菜单文本不含任何数字(不虚构套餐/余额/积分)。 - 头部与计费:min-height 56 / padding 8 / gap 8、头像框 40×40、初始头像圆形 `rgb(0,86,212)`、名称 16/500/24 且等于 mock 账号名;两个计费单元各 141×86、padding `4px 8px 8px`、title 12/400/20、value 14/500/22、动作行 margin-top 8 / 按钮高 24 / `disabled`、文案「升级套餐」「购买积分」;值恰为「未接入」,整份菜单文本不含任何数字(不虚构套餐/余额/积分)。
- 条目与 footer:4 个条目各 32 高 / `5px 8px` / radius 8 / gap 8 / space-between / 14/400/22 / 图标 16×16 / `role="button"`;设置显示 `Ctrl+,`、外观显示当前主题、未接入行显示「待拆解」「未接入」且 `aria-disabled="true"`;footer margin-top 6 + padding-top 8 + 1px 分隔线(`rgb(242,242,242)`)、退出登录行 32 高。 - 条目与 footer:4 个条目各 32 高 / `5px 8px` / radius 8 / gap 8 / space-between / 14/400/22 / 图标 16×16 / `role="button"`;设置显示 `Ctrl+,`、外观显示当前主题、记忆与进化直接打开真实设置页;未接入的检查更新显示「未接入」且 `aria-disabled="true"`;footer margin-top 6 + padding-top 8 + 1px 分隔线(`rgb(242,242,242)`)、退出登录行 32 高。
- 交互:触发器点击开/再点关、Escape 关闭并把焦点交还触发器(`aria-expanded` 同步为 false)、外部真实鼠标点击关闭且同一次点击不被吞(点通知铃铛 → 菜单关 + 通知面板开)、设置行复用并导航原生设置窗口到模型面板、外观点亮外观面板。 - 交互:触发器点击开/再点关、Escape 关闭并把焦点交还触发器(`aria-expanded` 同步为 false)、外部真实鼠标点击关闭且同一次点击不被吞(点通知铃铛 → 菜单关 + 通知面板开)、设置行复用并导航原生设置窗口到模型面板、外观点亮外观面板。
- 浅色/深色:通过设置窗口切深色后 popover 背景 `rgb(41,41,41)`、卡片 `rgb(36,36,36)`、边框 `rgb(61,61,61)`、阴影切换为深色 token,「外观」行值同步为「深色」;切回浅色后 `documentElement.dataset.theme` 与 `localStorage['ycode:theme']` 均为 light。 - 浅色/深色:通过设置窗口切深色后 popover 背景 `rgb(41,41,41)`、卡片 `rgb(36,36,36)`、边框 `rgb(61,61,61)`、阴影切换为深色 token,「外观」行值同步为「深色」;切回浅色后 `documentElement.dataset.theme` 与 `localStorage['ycode:theme']` 均为 light。
- 失败路径:未接入行点击后只显示提示、不新开窗口。 - 失败路径:未接入行点击后只显示提示、不新开窗口。
@@ -440,23 +452,62 @@
遗留: 遗留:
1. 「显示文件修改过程」尚未接入 YCode 的逐步文件变更渲染,保持原版 off 形态并禁用+「暂未接入」提示。 1. 「显示文件修改过程」尚未接入 YCode 的逐步文件变更渲染,保持原版 off 形态并禁用+「暂未接入」提示。
2. 编辑弹窗用 textarea 承载纯文本;原版是富文本编辑器(`.sc-editor-content`),富文本格式、粘贴与快捷键未复刻。 2. 编辑弹窗用 textarea 承载纯文本;原版是富文本编辑器(`.sc-editor-content`),富文本格式、粘贴与快捷键未复刻。
3. 原版默认人设文本已按 REA 抄录并显示在面板中,但默认值不会注入请求上下文(用户修改后才注入),避免与 YCode 自身身份设定重复冲突。 3. 已修复默认人设展示/生效不一致:默认值与自定义值均进入请求上下文,明确清空才不注入。原版提示词层级仍待 REA 证据。
4. 个性化上下文附加在每轮 `promptText` 末尾(与用户消息同一条 prompt 发送),不是独立的 system 消息;OMP 与自建循环行为一致。 4. 个性化上下文附加在每轮 `promptText` 末尾(与用户消息同一条 prompt 发送),不是独立的 system 消息;OMP 与自建循环行为一致。
5. **采集副作用**:REA 采集时把 WorkBuddy「对你的称呼」保存成了 `CodexTmp`;原版界面不接受空值确认(空字符串与单空格均被拒绝),无法通过可见操作恢复为空。该项需要在 WorkBuddy 内另行处理。 5. **采集副作用**:REA 采集时把 WorkBuddy「对你的称呼」保存成了 `CodexTmp`;原版界面不接受空值确认(空字符串与单空格均被拒绝),无法通过可见操作恢复为空。该项需要在 WorkBuddy 内另行处理。
### 14. 记忆与进化 ### 14. 记忆与进化
- [ ] 记录待拆解:入口、页面、数据、读写机制、会话生效方式及生命周期。 - [x] 通过 REA 拆解入口、页面结构、双开关、导入提示词、管理卡片、编辑与重置状态。
- [x] 对齐浅色、深色、导入弹窗和 480px 重置确认弹窗的结构、几何、字体、按钮与关闭方式。
- [x] “对话记忆”偏好真实控制长期记忆是否进入后续请求;“本地记忆与技能”偏好持久化并明确执行器限制。
- [x] 导入使用原版 zh-CN 提示词原文,校验空值与过短内容,并写入对话实际使用的长期记忆。
- [x] 验收:开关持久化、重开恢复、浅深主题、导入、管理、编辑、重置、Escape/遮罩/关闭与失败反馈。
状态:待拆解,本轮不自动展开实施。 状态:主页面和本地可用流程已实现,并通过真实隔离 Electron 窗口验收(24 项断言);完整 1:1 仍有下节列出的数据与服务边界。
原版证据:
- WorkBuddy 5.8.3 设置窗口 900×680:`ev_ccbabc0d8507e997eba15aab8b5effa66f7e82d6720c6131bfcd62157b9d6896`(浅色页面与 CSS)、`ev_765f35252332546eb751e470e70e32f4c6cd62b82974f0d4653b651ab8b7963d`(开关过渡并恢复)、`ev_646ce86b4da4d8c66c70ce576cb3a6689736757030a57c6fd4df552fbc843c69`(导入弹窗)、`ev_532a122e798a1890ee4b8977619a076a3e2dc5c927f72dbbabecf8dbd4e6f673`(深色页面)、`ev_505aa1d34e1ceba54d5ccd03de1a9bb3dd3c423d281c982fbc56acf7f049dd66`(模块脚本源码)。
- 2026-10-17 当前 WorkBuddy 5.8.3 页面视觉复核:`ev_16220ada6e43823fc8977aebb1d829bc8504b6ba7be8cc18041fbd03082f0c99`(当前深色页面,`.tmp/todo14-wb-current.png`)。
- 页面为 638×620 内容区;双行组 638×128、行 606×46;管理卡空态 638×78;浅色卡 `rgb(255,255,255)`,深色卡 `rgb(31,31,31)`;“对话记忆”默认开启,“本地记忆与技能”源码回退默认关闭。
- 原版模块 CSS `MemorySettingsPanel-qor7TPl3.css` 25,213 bytes、SHA-256 `43e0b0207ff1a91ad6066efc772dad351b2b5e7afe588d38a5ee38537eb01aad`。
- 原版管理态在内容存在时显示“重置 / 编辑 / 导入”;重置调用共享 `Modal`,`variant=confirm`、`width=480`、危险确认按钮,正文由 `.memory-modal__reset-confirm-desc` 与 `.memory-modal__reset-confirm-hint` 组成。
- 观察范围:设置窗口浅/深、空管理卡、双开关、导入弹窗和管理态源码。未覆盖:由于当前 WorkBuddy 记忆为空,真实界面没有“重置”按钮,所以未直接截取原版重置弹窗;其结构来自已回收脚本与共享 Modal/CSS,尚未完成原版运行态逐像素帧。
实现:
- `src/renderer/src/components/MemorySettingsPanel.tsx`:复刻双开关、管理卡、导入和 480px 重置确认弹窗;补齐 Escape、遮罩、关闭、焦点陷阱、初始焦点、异步禁用、错误反馈与重置后折叠。
- `src/renderer/src/components/source-memory-settings.css` 与 `source-memory-settings-recovered.css`:接入 REA 回收模块样式,并只在页面作用域内补齐共享 UI kit 弹窗壳和 token。
- `src/shared/memory.ts`:原版导入提示词、最短长度、偏好归一化和导入追加规则;本地记忆回退默认值修正为关闭。
- `src/main/memory-settings.ts`、`src/main/config.ts`、`src/main/ipc.ts`、`src/preload/index.ts`、`src/shared/ipc.ts`、`src/shared/types.ts`:真实偏好与长期记忆读写 IPC。
- `src/shared/personalization.ts`:只有“对话记忆”开启时才把长期记忆加入每轮提示词。
- `scripts/rea/recover-memory-panel.mjs`:从固定 REA Evidence 幂等回收模块 CSS;探针为 `scripts/rea/probe-todo14-*.json`。
验证:
- `npm run test:memory`(`node scripts/verify-memory-settings.cjs`)在真实隔离 Electron、真实 preload/IPC/renderer/filesystem、900×680 下 24 项断言全通过,覆盖浅/深页面、默认态、偏好持久化、重开恢复、导入提示词哈希、内容过短错误、真实写入、管理卡展开、480×212 重置弹窗、按钮/危险色/焦点、Escape 关闭、确认清空与折叠恢复、无 renderer 错误。
- 报告与截图:`.tmp/memory-settings-i0W3JB/report.json`、`ycode-memory-light.png`、`ycode-memory-import.png`、`ycode-memory-reset.png`、`ycode-memory-dark.png`。
- `npm run test:personalization`、`npm run typecheck`、`npm run build` 通过。
遗留:
1. 原版“重置记忆”弹窗本身尚未在真实 WorkBuddy 运行态取得逐像素 Evidence;当前按原版 `Modal` 源码 `width=480`、confirm 结构与共享 CSS 复刻,并已在 YCode 真实窗口验收。
2. 原版“编辑”通过记忆建议输入并提交服务端更新;YCode 在无账户记忆服务的前提下直接编辑本地长期记忆。功能可用,但提交流转和“更新中/成功 toast”尚未源码级等价。
3. 原版账户侧自动生成、每晚整理、更新时间与更新任务互斥状态没有在 YCode 实现;本地记忆执行器也未接入,界面已明确说明限制。
4. 当前像素验收覆盖 900×680 的浅色页面、深色页面、导入和重置稳定态;打开/关闭过渡、窄窗口、刷新失败和所有错误 toast 尚未逐帧验收。
### 15. 智能体页面:团队管理 ### 15. 智能体页面:团队管理
> 2026-10-18 后续授权:团队管理已接为全部子智能体调用的实际总开关,本地 OMP/HTTP 调度、运行中取消、独立上下文、文件范围、审批、流式展示及用量汇总已实现,核心29项与真实应用27项通过。见[子智能体调度接入](子智能体调度接入-2026-10-18.md)。下方采集与最初实现范围保留为历史基线。
> 2026-10-18 用户范围调整:移除经济模式和浏览器操控,智能体设置仅保留团队管理。已完成界面与专用样式清理,真实隔离窗口 11 项验收通过;以下原版三区块的采集与实现说明保留为历史记录。详见[用途核对与移除记录](经济模式用途核对-2026-10-18.md)。
- [x] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。 - [x] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。
- [x] 根据 REA 证据确认本页只有三个设置区块,没有团队列表、创建、详情或成员管理;接入“开启 / 禁用”偏好的真实存储与重启恢复链路。 - [x] 根据 REA 证据确认本页只有三个设置区块,没有团队列表、创建、详情或成员管理;接入“开启 / 禁用”偏好的真实存储与重启恢复链路。
- [x] 验收:仅展示原版已确定的三个区块;团队管理偏好实际持久化、重启后可恢复;团队管理与真实子代理调度分别记录。 - [x] 验收:设置仅保留团队管理,开关持久化;开启允许本地子智能体调用,关闭拒绝新调度并取消已有子任务,主对话继续;原版采集和本地调度验收分别记录。
状态:已实现并通过真实窗口专项验收。团队管理偏好已持久化;多智能体自动拆分执行器未接入。 状态:已实现并通过真实窗口及本地调度专项验收。主模型可调用独立成员,所有本地子智能体创建均受总开关控制;不据此宣称原版调度等价。
原版证据:WorkBuddy 5.8.3,设置窗口 900×680。REA Evidence `ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883`(浅色布局、DOM、computed style 与 CSS 源)、`ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719`(深色)、`ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd`(团队管理下拉只含“开启 / 禁用”)、`ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8`(最终浅色顶部)、`ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7`(三枚浏览器扩展图标媒体字节)。页面根 `638×503 @232,40`,三个区块依次为智能体团队管理、经济模式、浏览器操控(Beta);浏览器卡片 `638×261`,扩展列表 `606×193`,三行高 `68/68/56`。经济模式开启轨道浅色 `rgb(0,194,154)`、深色 `rgb(34,203,168)`。独立懒加载 CSS 为 `ExtensionsSettingsPanel-BnsXh3oa.css`,sha256 `cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5`。浏览器图标分别为 Chrome SVG `bde4af5a…`、Edge SVG `968cf9d8…`、QQ PNG `186c8282…`。观察范围限当前设置窗口;本页没有团队列表、团队详情、创建或成员管理,未据此推断其他页面。 原版证据:WorkBuddy 5.8.3,设置窗口 900×680。REA Evidence `ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883`(浅色布局、DOM、computed style 与 CSS 源)、`ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719`(深色)、`ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd`(团队管理下拉只含“开启 / 禁用”)、`ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8`(最终浅色顶部)、`ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7`(三枚浏览器扩展图标媒体字节)。页面根 `638×503 @232,40`,三个区块依次为智能体团队管理、经济模式、浏览器操控(Beta);浏览器卡片 `638×261`,扩展列表 `606×193`,三行高 `68/68/56`。经济模式开启轨道浅色 `rgb(0,194,154)`、深色 `rgb(34,203,168)`。独立懒加载 CSS 为 `ExtensionsSettingsPanel-BnsXh3oa.css`,sha256 `cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5`。浏览器图标分别为 Chrome SVG `bde4af5a…`、Edge SVG `968cf9d8…`、QQ PNG `186c8282…`。观察范围限当前设置窗口;本页没有团队列表、团队详情、创建或成员管理,未据此推断其他页面。
@@ -503,15 +554,19 @@
状态:已实现并通过真实窗口回归(与 16 同一次改动,代码见上)。 状态:已实现并通过真实窗口回归(与 16 同一次改动,代码见上)。
原版证据、实现、验证:同 16。`dataManagement`、`buddyApps` 与 `claw`、`deviceConnections` 一并进入 `REMOVED_SETTINGS_TABS`,走同一份 normalize 回退与 `visibleTabs` 渲染;验收脚本同时断言这两项(以及 `claw`、`deviceConnections`)不在 DOM、标签和搜索元数据中,且 `openSettings('dataManagement')` 深链接落到模型面板而不是空白页。未触碰本地会话数据、安装中心与任何已有用户内容。 原版证据、实现、验证:同 16。`dataManagement`、`buddyApps` 与 `claw`、`deviceConnections` 一并进入 `REMOVED_SETTINGS_TABS`,走同一份 normalize 回退与 `visibleTabs` 渲染;验收脚本同时断言这两项(以及 `claw`、`deviceConnections`)不在 DOM、标签和搜索元数据中,且 `openSettings('dataManagement')` 深链接落到模型面板而不是空白页。未触碰本地会话数据、已有能力与任何用户内容。
遗留:无新增;数据管理/应用管理的原版页面内容不再复刻(用户指定仅移除入口)。 遗留:无新增;数据管理/应用管理的原版页面内容不再复刻(用户指定仅移除入口)。
### 18. 安装中心 ### 18. 设置 → 安全中心
- [ ] 记录待拆解:入口、页面结构、安装来源、安装/更新/卸载流程及与其他能力中心的关系。 - [ ] 通过REA采集安全中心真实页面、字段、说明、默认状态及对应源码/样式。
- [ ] 逐项拆解控件行为、持久化与生效时机、宿主/服务依赖;原版未证实的策略不预设。
- [ ] 将已有真实能力接入安全中心,逐项验证功能与实际执行效果,并做浅/深色、窄屏和全状态视觉/交互验收。
状态:待拆解,本轮不自动展开实施。 状态:用户已订正名称与范围,待REA采集与正式拆解。YCode仅有`securityCenter`可见导航,尚未加入`supportedTabs`,没有对应页面或可用功能。详见[安全中心范围与待拆解项](安全中心模块拆解-2026-10-17.md)。
范围纠正:此前按应用安装管理拟定的目录、安装、更新、卸载和授权任务全部撤出本项;原[安装中心分析](安装中心模块拆解-2026-10-17.md)仅保留历史。已经实现的发现应用浮层、相关REA证据和44项专项验收记录仍保留在历史文档,不计为安全中心进度。用户此前要求移除设置“应用管理”的决定继续有效。
### 19. 关于与获取帮助 ### 19. 关于与获取帮助
@@ -549,11 +604,21 @@
### 20. 定时任务、资料库、更多中的项目 ### 20. 定时任务、资料库、更多中的项目
- [ ] 定时任务:待拆解入口、页面、任务配置、触发/执行记录及生命周期。 开发入口:[三模块开发计划](三模块开发计划-2026-10-17.md)。已完成公共执行服务与首批三路本地链路,实际交付和待完项见[首批开发记录](三模块首批开发记录-2026-10-17.md);后续推进个人资料/引用与项目待办/资产联动,再做全状态对照,不把首批通过等同完整1:1。
- [ ] 资料库:待拆解入口、资料组织、添加/读取/检索及会话引用方式。
- [ ] 更多 → 项目:待拆解入口、项目组织、工作区/任务关系及管理操作。
状态:三个模块均待拆解,本轮不自动展开实施。 2026-10-17 最新:用户已授权四模块拆解。本项拆分、依赖、优先顺序和验收要求见[总览](四模块拆解总览-2026-10-17.md)、[定时任务](定时任务模块拆解-2026-10-17.md)、[资料库](资料库模块拆解-2026-10-17.md)、[项目](项目模块拆解-2026-10-17.md)。下方原有证据和实现记录继续保留,旧“待拆解”表述为此前状态;本次只拆解,不新增业务实现。
- [ ] 定时任务:入口、列表/空态、新建/编辑字段、触发频率/时间/有效期、执行记录及暂停/删除生命周期。
- [ ] 资料库:待拆解入口、资料组织、添加/读取/检索及会话引用方式。
- [ ] 更多 → 项目:项目组织、工作区/任务关系、筛选/协作/管理操作及全状态验收。
状态:三项已进入实现,完整模块仍保持未勾选。本地定时调度/真实执行与记录、跨任务本地产物索引、项目首发/配置/指令已接入并通过实际链路,见[首批开发记录](三模块首批开发记录-2026-10-17.md)。本轮继续修正编辑器、资料库层级/表格/原图、项目满高/输入区/配置栏和字体,见[UI续对齐](三模块UI续对齐-2026-10-17.md)。个人资料导入/编辑/引用、项目动态/计划/资产及完整生命周期、原版日期面板与全状态像素/动效仍未完成。YCode 时区/DST/迟到策略已做本地回归,但原版服务端行为尚未证明等价。
定时任务首轮运行态证据:WorkBuddy 5.8.3,REA target `41DA5D1989571E35EEB83ADEF03B5B47`;列表/入口 `ev_386df7d869a651cc6c083205fb10c4264ec8356901bc0b031cab63d64d65c8d5`,新建弹窗 `ev_be404f0454dfd7496f91ff79a7f43bb61a54ad587933baee0d1cf77c4db915c5`。观察范围为 1336×902 深色界面的定时任务页及空白新建表单。**尺寸订正:布局捕获在 opacity=0、scale(.96) 的入场帧,768×470.406 是动画中尺寸;按源 CSS 稳定态为 800×490,内部表单圆角 16,名称输入 752×40。** 有任务名称、提示词、工作空间/权限、重复频率/时间/有效期、企业微信推送和取消/确定入口;空白状态下确定按钮禁用。截图:`.tmp/rea-todo20-scheduled-page.png`、`.tmp/rea-todo20-scheduled-create.png`。捕获整体为 `policy_filtered`:53 个越界脚本、2 个未归属来源脚本及 14 个资源被排除;本次所需 automation 脚本与样式表正文仍包含在 Evidence 中。
定时任务静态源码证据:REA `ev_625e3664a4da2b25291494bd5b985d16b09d1654d5f8170d8af6325d2019c159`,父分析 `ev_c58c2aea390e169637d9233032f33bfccc0b62bb96014a003fbd39803a178241`,WorkBuddy `app.asar` SHA-256 `3e50fcb5b060deba4f1ea9a8c293cf955bf12d7e52d971985de914350afd9077`;仅分析 `renderer/assets/automation-qDy4JNlv.js`(SHA-256 `687f7b062b0df0f353dafd1f17b00cb939685da5a12c8d3ce865b920b94d6710`),来源覆盖为部分,不代表其他 11,388 个文件已分析。样式表 `automation-C36RYsTC.css`(SHA-256 `28c6dc5e1a8d83d7b955d00a604723898a296f0ece4a3bbe4d6db26fc3bbbda5`)来自同次 REA 页面 Evidence。
静态观察:重复方式为间隔小时、一次、每天、每周、双周、每月、每年;周期规则含本地时间,周/月/年可细化日期;一次任务需选择未来日期时间。默认草稿为 ACTIVE、每日执行、fullAccess 权限;名称与提示词必填,提示词上限 100,000 字符;有效期可设置起止或长期有效。页面分别读取本地/云端任务及运行记录,通过 `Ne.automations` 服务调用 create/update/delete/trigger 和记录归档/删除;运行中的任务会被拒绝触发或删除。上述为静态 AST 观察,不证明服务端执行、时区/DST、记录刷新与生命周期运行行为;本轮尚未操作频率弹层或保存原版任务。整包分析采用 `record-and-continue`,ASAR 其他条目存在完整性声明与文件字节不一致,未把相关字节作为可信证据。
## 追加需求 ## 追加需求
@@ -604,14 +669,14 @@
4. 原版对 detached HEAD、无首次提交、非 Git 目录、检测失败的标签表现未采集;YCode 在这些情况下不渲染标签(无提交但有分支时按 git 真实结果仍显示分支名)。 4. 原版对 detached HEAD、无首次提交、非 Git 目录、检测失败的标签表现未采集;YCode 在这些情况下不渲染标签(无提交但有分支时按 git 真实结果仍显示分支名)。
5. 用量环进度颜色:原版通过未回收的变量注入,本轮截图像素证明该实例用的是回退色 `#9ca3af`;是否随用量分段变色未验证。 5. 用量环进度颜色:原版通过未回收的变量注入,本轮截图像素证明该实例用的是回退色 `#9ca3af`;是否随用量分段变色未验证。
6. 刷新时机未采集:YCode 在切换会话、窗口重新聚焦以及每 20s 轮询时重新读取分支;用量只在收到真实 `usage` 时更新。 6. 刷新时机未采集:YCode 在切换会话、窗口重新聚焦以及每 20s 轮询时重新读取分支;用量只在收到真实 `usage` 时更新。
7. 备用(非 OMP)内核走自建网关时用量来自 Responses `response.completed.usage`;Chat Completions 上游路径仍未解析 `usage`。 7. 已补 Chat Completions 流式 usage 解析及不支持 stream_options 的单次兼容重试;Responses 原有通路保留。切模型清理旧用量,见本轮修复和受控上游回归记录。
8. 用量环只在会话输入区渲染(本轮首页基线的 REA 证据里首页输入区确实没有该入口),首页/新建任务时不显示。 8. 用量环只在会话输入区渲染(本轮首页基线的 REA 证据里首页输入区确实没有该入口),首页/新建任务时不显示。
## 推进顺序 ## 推进顺序
1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。 1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。
2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;11(外观)、12(快捷键)、13(个性化)、19(关于与获取帮助)、15(智能体)均已实现并完成专项验收。 2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;11(外观)、12(快捷键)、13(个性化)、19(关于与获取帮助)、15(智能体)均已实现并完成专项验收。
3. 14、18、20 保留为待拆解,后续单独展开。 3. 14 已接入;18已订正为设置→安全中心,待REA采集与正式拆解;20已有三个模块的首轮拆解,后续依[总览](四模块拆解总览-2026-10-17.md)逐批实现与验收,尚未勾选完成。
## 每项交付记录模板 ## 每项交付记录模板
+45
View File
@@ -0,0 +1,45 @@
# 子任务展示对齐
日期:2026-10-18。按 [WorkBuddy 子任务显示位置核对](WorkBuddy子任务展示核对-2026-10-18.md) 的已采集 5.8.2 证据实施。
> 最新位置订正:用户要求放到输入框上面,现有成员入口已迁移,回复底部重复入口已移除;见 [迁移与验证](输入框上方成员入口迁移-2026-10-18.md)。下方“当前行为”保留为前一批实施记录。
> 范围订正:本批只实现回合底部成员入口和右侧详情,未覆盖输入框上方的团队成员栏及后台任务列表。用户指出后已从同一份REA证据确认这两个入口,见 [补充核对](WorkBuddy输入框上方任务区域核对-2026-10-18.md);它们是明确待补项,不能将本批称为全部子任务展示1:1完成。
## 当前行为
- 主回复移除逐张子 agent 卡片;生成中和结束后均在本回合底部显示头像组及「N 位团队成员」。收起时最多预览4个头像,展开列表可查看所有实际成员。
- 列表显示任务名称、固定任务序号和运行/排队/完成/失败/中断状态。状态排序不会改变任务身份,不虚构本地调度器没有的主理人、职业或头像。
- 点击成员在既有右侧详情区域显示成员任务;保留原有文件/产物标签,打开这些标签或子任务中的产物会退出成员视图。
- 下发任务说明可独立展开。成员正文通过现有 MessageBubble、思考和专用工具渲染器展示,使用真实工具参数及结果;完整结果替换流式尾部,避免重复展示。
- 当前成员的事件和状态持续更新;停止按钮保留在详情标题栏,真实调用已有 cancelAgent IPC。主对话、其他成员继续运行。
- 选择按会话及父消息归属;切换会话、重新水合、结束和回退会正确恢复或清理。关闭详情回到父会话,不终止主任务。
- 单轮及成员已报告 token 用量继续保留,未推算费用或增加模型请求。
## 原版资源与适配
使用既有 REA 导出,没有直接读取安装目录或通过其他入口采集原版。
- 运行时 Evidence:`ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`。
- CSS Evidence:`ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`。
- 恢复53条成员列表/详情原版样式,原声明保存在 `src/renderer/src/workbuddy/subagents/source.css`;宿主按钮、滚动和布局适配独立放置。
- 从固定 `src-DPDj7_qD.js` 恢复7个原版 SVG:下箭头、右箭头、运行状态、完成、错误、关闭及任务说明。原路径、viewBox和摘要记录在 icons.json。
- 成员详情复用对话的主题作用域和字体变量,包含思考/工具的深色样式。原版 header 高度为 spacing-10 + spacing-2,本次对应52px,未误用48px。
- 恢复脚本及 manifest 留在 `scripts/rea/recover-subagent-display.mjs`、`scripts/rea/recover-subagent-icons.cjs` 和 `src/renderer/src/workbuddy/subagents/manifest.json`。
与原版的明确适配:YCode 额外显示真实排队状态,保留标题栏中的单独停止;当前本地调度器只提供名称和有限事件快照,因此没有伪造专家职业、市场头像或未采集的历史分页接口。原版普通 Task 工具块继续独立处理。
## 验证
- `npm run test:subagents:display`:15项,覆盖成员归属、状态排序与固定序号、真实工具投影、流式尾部去重、旧记录、面板隔离及资源切换。
- `npm run test:subagents:ui`:14项,隔离真实 Electron/正式 renderer,以明确 mock IPC 快照验证生成态入口、成员选择、说明展开、浅/深色、深色思考、实时更新、窄窗口抽屉、切换及重新水合、父任务取消和回退清理;0 renderer 错误。
- UI 报告 `.tmp/subagent-display-pt4PXH/report.json`;已实际查看该目录的 subagents-expanded.png、subagents-dark.png、subagents-narrow.png。
- `npm run test:subagents:app`:30项,真实 Electron/preload/生产 IPC、安装的 OMP 和回环模型验证,成员入口、工具详情、单独停止、开关、权限、用量、跟进、落盘和清理均通过。最终报告 `.tmp/subagent-app-qt49gP/report.json`,0 renderer 错误。
- `npm run test:subagents`:29项;`npm run test:conversation`、`npm run test:assistant-footer` 通过。
- 两个 REA 恢复脚本的 `--check` 通过;完整 `npm run build` 通过,日志 `.tmp/subagent-display-build.log`。
UI mock 不等于真实调度验证;真实调度由独立应用专项覆盖。所有模型测试均使用回环服务,没有请求用户付费 API。未强制重启用户当前窗口,重新启动后加载最新构建。
## 尚未验收
原版已知9223调试端口不可达,本轮没有新采集原版运行截图或操作。因此,以上是已采集结构、CSS/SVG恢复和 YCode 真实链路验证;原版同场景的全状态像素与动效验收仍待补。成员快照既有80条事件、单事件4000字符的保留上限继续适用,截取时显示提示,不宣称完整原生历史分页。
@@ -0,0 +1,62 @@
# 子智能体调度接入
日期:2026-10-18。用户确认将“智能体团队管理”接为全部子智能体调用的总开关。以下为 YCode 本地实现和验收,不是 WorkBuddy 调度行为的 1:1 等价结论;本轮未采集 WorkBuddy。
## 开关与职责
- 开启时,主模型可以分派具有明确目标、范围和验收要求的任务;简单任务仍自行完成,不强制每轮创建子智能体。
- 关闭时,不向新的模型请求提供创建、补充指令工具,宿主也拒绝通过旧工具名创建或继续调度。当前运行和排队的子任务收到取消信号,主对话继续。可以收取已有状态及结果。
- 去掉页面中的“仅保存偏好”和“专家团队任务不受此设置影响”。单个专家仍是对话角色;所有实际创建本地子智能体的行为均受开关限制。
- 主智能体负责规划、分工、结果整合、验证及最终回复。子智能体是执行成员,其输出不构成新的用户授权。
## 运行方式
新增本地主进程调度器;OMP 与备用 HTTP 使用同一套宿主调度工具:
| 工具 | 行为 |
| --- | --- |
| spawn_agent | 创建并排队子任务,返回当前回合的子智能体 ID |
| list_agents | 查看当前主回合的状态与已完成结果 |
| wait_agent | 等待指定或全部子任务并收取结果,单次最多30秒 |
| send_agent_message | 补充指令;运行中排队,已完成时复用独立上下文继续 |
| cancel_agent | 取消一个子任务,不自动回滚已产生的修改 |
全进程最多并发5个子任务(用户后续要求由3个调整为5个),超出上限排队;每个主回合最多创建16个。每个子智能体最多接受8轮初始/补充指令,最多同时排队4条;每次执行默认10分钟超时。暂不提供嵌套调度或独立模型选择,继承父回合冻结的自定义模型、思考配置和权限上限。规划/问答模式不会开放调度工具。
子智能体只接收任务与明确提供的必要背景,不复制主对话历史。每个成员有独立的 OMP 实例及原生记录,或独立的 HTTP 历史;成员不能读取另一个主回合的调度状态。主模型准备结束时,如果仍有子任务或未收取结果,宿主先等待,再将候选结果送回主模型整合,不能直接将未完成的调度作为完成终态。
主任务结束、取消、删除或应用退出都会清理执行成员及其浏览器等资源。原生记录按父会话目录存放于 userData/subagent-sessions/,删除父会话时一并清理;应用重启不自动恢复运行中的子任务,历史恢复为已取消。
## 权限和文件修改
默认子任务只读。需要修改时必须指定 access=write 和1至32个明确的工作区内文件路径;拒绝目录、通配符和越界路径。读写限制同时应用于提供给模型的工具列表和宿主执行入口。
第一版子智能体可读取文件、加载/读取已有技能资源,并在声明范围内使用写文件工具。命令执行、浏览器、连接器和专家包修改由主智能体处理,不把难以约束写入范围的工具开放给成员。子智能体遇到需要用户决策时向主智能体报告。
文件范围被独占保留,阻止同名兄弟任务及主任务同时覆盖;主任务执行命令或连接器时也检查工作区内的写任务。默认权限下的子任务写入仍走现有审批,审批在主对话显示并标明成员名称;取消或关闭开关可取消尚未回答的审批。
取消后会等待已开始的本地文件操作结束,再释放文件占用,保留实际产生的 before/after 记录。主对话编辑/回退时合并主、子写入记录,并按完成顺序校验恢复,避免漏掉子任务的文件变更。
## 界面与用量
对话中显示每个子任务的名称、状态、耗时、模型和文件范围;运行中即可展开步骤、思考和工具输出,并可单独停止。结束后仍保留结果及事件记录。主回合结束事件在子任务清理及最终快照之后发布,序号保持递增,避免流式页面停留在生成状态。
上游实际报告的 token 按主智能体、子智能体、合计分别展示;这是请求累计,不能拿来当作当前上下文占用。未报告的用量不估算,不显示推算费用。成员事件最多80条,单项展示文本最多4,000字符,结果最多24,000字符,模型工具回传也有完整 JSON 大小预算;截取时标记,不截掉文件回退所需的数据。
## 并发传输修复
并发验收曾出现一次浏览器截图 OMP JSONL 解析异常。检查同时发现上下文观测扩展和 OMP 内核独立写同一 stdout,存在大帧被插入观测数据的竞争路径。现将观测计数改为独立的原子文件通道,由宿主监测并在请求结束前读取;stdout 保留给内核协议。只写计数和身份,不落盘模型请求正文;观测失败不阻断模型执行。
新增7项通道测试,覆盖不写 stdout、不保存正文、用量前读取、去重和停止观察。随后同时运行真实子智能体应用验收与60项浏览器回归通过。单次原始错误帧未完整保留,因此不据此声称已经验收所有高压并发场景。
## 验证与交付
后续并发上限由3改为5:更新后的核心29项通过,包含5个运行、第6个排队、跨主会话共用上限及5个运行实例的取消清理;完整构建通过,日志 .tmp/subagent-limit-5-build.log。此次未重跑真实应用专项,下方应用报告为初版3并发配置的验收记录。
- npm run test:subagents:29项通过,覆盖开关、宿主拒绝、跨回合与全局并发、文件边界和占用、父子取消、超时、补充指令、最终整合、回退及大结果预算。
- npm run test:subagents:app:27项通过,真实 Electron、preload/IPC、已安装 OMP、HTTP 工作成员和回环模型端点验证;检查流式展开、界面停止、终态序号、写入排空、审批、补充指令、用量、持久化与清理。报告 .tmp/subagent-app-3IxrIT/report.json,0 renderer 错误;已查看该目录的 agents-light.png 和 agents-dark.png。
- 智能体设置浅色/深色与开关保存11项通过,报告 .tmp/agents-settings-BrgdVb/report.json;已查看实际对话的浅色和深色子任务截图。
- npm run test:context-observer、npm run test:context-usage、npm run test:conversation、npm run test:conversation-ipc、npm run test:omp、npm run test:chat、npm run test:persistence、npm run test:custom-model-native 通过。
- 完整 npm run build 通过,日志 .tmp/subagent-build-final.log。浏览器最终并发60项通过,报告 .tmp/browser-automation-KOnj4K/report.json,日志 .tmp/browser-subagents-concurrent-final.log。变更文件的 git diff --check 通过。
所有模型端点均为本机回环测试服务,没有请求用户付费 API;原生运行使用本机实际安装的 OMP。当前用户窗口保留,未强制重启,新主进程重启后生效。保留工作区已有修改,未提交代码。
@@ -0,0 +1,30 @@
# 设置 → 安全中心:范围订正与待拆解项(2026-10-17)
## 用户订正
用户明确订正:原TODO第18项“安装中心”应为**设置中的安全中心**。当前四模块范围为安全中心、定时任务、资料库、更多→项目。
此前按应用安装管理拟定的目录、安装、更新、卸载、应用registry与授权方案撤出本项任务;原“安装中心模块拆解”仅留作误解范围的历史记录,不是安全中心方案。现有发现应用浮层与能力中心功能保留,不计入安全中心进度,也不因本次订正移除已实现功能。
## 当前可确认状态
- YCode导航key为 `securityCenter`,位于 `SOURCE_WINDOWS_SETTINGS_TABS`,不在用户要求移除的四项导航中。
- `source-settings-navigation-reference.ts` 保留“安全中心”标签、搜索词和图标;这些仅证明当前导航素材存在。
- `SettingsDialog.tsx` 的 `supportedTabs` 不含 `securityCenter`,点击显示“该设置尚未接入”;没有对应页面渲染分支。
- 因此YCode安全中心当前为**可见但未接入的入口**,没有可验收的安全中心页面或功能。
- 既有原版设置导航REA证据可复用(WorkBuddy 5.8.3,`ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`,相关引用见SettingsDialog注释及设置导航历史验收记录)。本轮只读核对YCode,不新增原版采集;该证据不代表安全中心内容已采集。
## 后续拆解任务
1. **REA页面基线**:进入原版设置的 `[data-settings-tab="securityCenter"]`,取得实际标题、分区、所有字段、默认值、提示、图标、间距和滚动布局;已有有效5.8.2证据继续复用,补采注明实际版本。
2. **逐项行为**:核对每个实际控件的点击、展开、保存/取消、即时生效、持久化、确认、错误与不可用状态;不预设页面包含哪些安全功能。
3. **实现追踪**:通过REA追踪对应已加载脚本/CSS和宿主契约;分清显示偏好、真实权限策略、本地能力和远端服务依赖。不得套用应用安装方案。
4. **YCode接入方案**:基于取证结果逐项映射当前真实能力,主上下文确定shared/main/preload契约;策略若影响执行,应在真实执行入口生效,不能只保存UI开关。
5. **功能验收**:按实际字段验证读取、修改、恢复、重启、多窗口同步与错误恢复。具体策略的执行用受控测试场景验证,未接项如实列出。
6. **视觉与交互验收**:同视口/主题/字体对照;覆盖浅/深色、窄窗口、hover/focus/disabled、弹层、滚动和动效。功能与像素分别留证。
## 当前限制
原版安全中心的实际内容、数据来源、策略规则与副作用尚未完成取证。状态为**范围已订正,待REA采集与正式拆解**;不能沿用此前安装中心“部分拆解/部分实现”的进度。
本次仅新增本文件、更正当前TODO/总览/交接记录,并给旧安装中心文档加失效说明。未改业务代码、用户配置或权限策略,未运行构建与应用测试。
+761
View File
@@ -0,0 +1,761 @@
# 安装中心模块拆解(2026-10-17)
> **范围订正:本文件不再是当前实施任务。** 用户已明确第18项是“设置 → 安全中心”,此前按应用安装管理理解有误。本文件保留历史证据与分析,不作为安全中心的需求、实现方案或完成度。当前入口见[安全中心范围订正与待拆解项](安全中心模块拆解-2026-10-17.md)。已有发现应用功能保留;下文安装/更新/卸载等批次不再列入本次四模块计划。
## 文档范围与结论
本文只做模块拆解、证据归档和分批实施方案,不实现业务代码,不触发安装、更新、卸载、授权或保存。本文形成后通过 REA 对 WorkBuddy 5.8.3 做了一次只读 target 枚举、一次无操作的声明式探针,以及一次打开设置窗口的声明式探针尝试;没有执行产品写操作,也没有修改 REA 配置。
**当前结论:**
- 既有 REA 证据已经证明 WorkBuddy 5.8.3 侧栏“发现应用”浮层的入口、稳定打开态、主要几何、CSS 和客户端请求/授权逻辑。
- 既有证据**没有**采集“设置 → 应用管理/安装中心”的懒加载主页面。现有脚本清单只出现 `BuddyAppsPanel-vC0qX_Oo.js` 和 `BuddyAppsPanel-OzkcFEe0.css` 的文件名,没有这两个资源的正文。
- 2026-10-17 的最新 REA 枚举只返回主窗口 `1CAD78026104DDABDB43F9EB6BA7635D`,没有设置窗口 target。一次无操作声明式探针成功(`ev_a3ca56f518f5c5101d2ae6e64f24794a564956da643c11e622c56b5cbb245000`),但没有导航。
- 打开设置窗口的声明式探针被 REA 以 `invalid_request / invalid_input` 拒绝,未产生 Evidence ID、设置 target 或 `data-settings-tab="buddyApps"` 页面。这次结果只记为工具输入失败,不能反推 WorkBuddy 没有应用管理页,也不能反推 REA 不支持该动作。
- “发现应用”是应用切换/发现浮层,不能据此推定安装中心主页面的列表、已安装状态、版本、更新、卸载、授权按钮和失败恢复。
- YCode 当前只有同壳适配的本地“发现应用”浮层,映射到专家、技能、连接器三个本地能力入口;没有云应用目录、应用详情、安装/更新/卸载、授权、依赖安装或应用管理主页。
- 因此 TODO 18 只能保持“部分实现”。本轮不能把它标记为完成,也不能把浮层通过当作安装中心 1:1 验收。
本文证据标签:
| 标签 | 含义 |
| --- | --- |
| `REA-RUNTIME` | REA 运行时观察到的页面、DOM、布局、视觉或声明式输入结果 |
| `REA-STATIC` | REA 保存的 renderer script/CSS/manifest 正文静态结论 |
| `YCODE-SOURCE` | 当前 YCode 源码事实 |
| `INFERENCE` | 基于证据的工程建议,不能当成原版事实 |
## REA 证据清单
目标版本均为 WorkBuddy 5.8.3,Windows,Chrome 138 / Electron 37.10.3。主要观察在暗色主题、`1336×1240` 主窗口或 `900×680` 设置窗口下完成。
| 用途 | Evidence ID | 保存位置 | 时间(UTC) | 观察范围与边界 |
| --- | --- | --- | --- | --- |
| 主窗口基线 | `ev_ab93f4313af45f559597bfa886f218257edb9b8c1c1c056c446b2ed26a2352e9` | `.tmp/rea-todo18-main-visual.json`、`.tmp/rea-todo18-main/` | `2026-10-17T05:44:14.389Z` | 暗色主窗口、1336×1240、无浮层的基线。截图 SHA-256 `b561129cfa37a5b6220e449fd13de7eadc2bc88b68cf48b5c647052aff4a8591`。不能证明打开态。 |
| 设置窗口基线 | `ev_5b635ae361c0988fa459b5d58897d40ab52ae2e814394b8c21080d1eb9bcb6a1` | `.tmp/rea-todo18-settings.json`、`.tmp/rea-todo18-settings/` | `2026-10-17T05:44:00.912Z` | 暗色设置窗口、900×680,停在“记忆与进化”。截图可见“数据与安全”分组下的“应用管理”导航,但右侧不是应用管理页。截图 SHA-256 `5fe5eb779151e04e1415198cd5e3a2958f02395381cc743713f3cddf9961bc3c`。 |
| 打开后复核 | `ev_43e8726c4cfac93184509ea30ae0d1305f4ad9c4a186f6ce7a20a7b16810316c` | `.tmp/rea-todo18-discover-after.json`、`.tmp/rea-todo18-discover-after/` | `2026-10-17T05:45:42.851Z` | 截图 SHA 与主窗口基线完全相同,画面未出现浮层。它不能作为“发现应用”打开态证据。 |
| 键盘打开过程 | `ev_d3527391f609e4a8cb4d194a24ee17dcbbdfc0a2d58db240ef3686119694d85f` | `.tmp/rea-todo18-discover-key.json`、`.tmp/rea-todo18-discover-key/` | `2026-10-17T05:49:07.439Z` | REA 对 `.industry-template-switcher__trigger` 执行 Enter,动作完成并等待 500ms;`frame-000.png` 显示浮层。最终截图 SHA `65f7ce3cd1d81f4f01f9fabe745b50c0236831f55e69b2012f0fe4e2135271cc`。只证明入口可打开,不证明搜索、选择、滚动到底和授权。 |
| 稳定打开态 | `ev_bd22e46c95d3887daba16dbe319389ad74e10afea8f71af9a7639e8bce95ebb4` | `.tmp/rea-todo18-discover-state.json`、`.tmp/rea-todo18-discover-state/` | `2026-10-17T05:49:49.145Z` | 暗色浮层稳定截图 SHA-256 `f5284085280418769f8c6f022c1e2451f167815783227373c2a90d766ea2bd40`。含布局树,但 REA 布局不含文本/input value,应用名称与描述需结合同次截图。 |
| renderer 脚本源 | `ev_2b8db4eb73c59a8d3d80c3e37db5ea9b5bf1d13390d5238d81ff2cc53d11dc09` | `.tmp/rea-todo18-main-scripts.json`;恢复件 `.tmp/rea-todo18-matches/contexts-DgzzFjnf.js` | 采集于 2026-10-17 | REA 捕获 321 个当前已加载脚本。`contexts-DgzzFjnf.js` 为 7,553,508 bytes,SHA-256 `8d0c375f141cf724c7f77233ee1d54dd21c9cc86286b79d119711f34a6236746`。其中包含发现浮层、API、登录、consent、依赖和缓存逻辑。 |
| 样式源 | `ev_7eb66ea8d5949eb7d872f1a93a97894a811b3beb7a02eabd81a500409f1045e4` | `.tmp/rea-todo18-styles-current.json`、`.tmp/rea-todo18-current-contexts.css`、`.tmp/rea-todo18-industry.css` | 采集于 2026-10-17 | `contexts-BkjwYSZT.css` 为 1,600,789 bytes,SHA-256 `50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380`。恢复出的行业浮层规则文件为 21,085 bytes,SHA-256 `ab1771d8109a137d566e19d24901fa088f893354553900172e5719684de58a5d`,只是原 CSS 的子集。 |
| 当前 target 枚举 | `ev_2504239bed6d570c1e1e274465ffb9989058ba81ea503f7b295db6add4e380f1` | `.tmp/rea-four-modules-install-targets.json` | 采集于 2026-10-17 | WorkBuddy 5.8.3 / Electron 37.10.3;只枚举到主窗口 target `1CAD78026104DDABDB43F9EB6BA7635D`,没有设置窗口。 |
| 无操作探针确认 | `ev_a3ca56f518f5c5101d2ae6e64f24794a564956da643c11e622c56b5cbb245000` | `.tmp/rea-four-modules-install-probe-minimal.json` | `2026-10-17T10:04:39.891Z` | 使用仅含 wait 的 REA 声明式探针;`probe.status=completed`,0 个探针帧。视觉 viewport 为 `1336×902`,PNG SHA-256 `d6ef3903bee943b69272a003d8ac7753e7bef882d4949d10104ccac06f70115f`。没有点击、导航或设置窗口证据。 |
| 打开设置探针失败 | 无;工具拒绝后未生成 Evidence ID | `.tmp/rea-four-modules-install-open-settings.json`、`.tmp/rea-four-modules-install-open-settings-retry.json` | 2026-10-17 | 两个文件均 295 bytes;返回 `code=invalid_request`、`category=invalid_input`、`message=Analysis input is invalid. Check the arguments and try again.`。没有证据表明点击已执行;失败后停止重试。 |
### 证据边界
1. 没有安装中心主页面 DOM、视觉、CSS 或 lazy chunk 正文。
2. 没有“应用管理”导航的实际点击记录;当前截图只证明导航文字可见。
3. 没有真实 catalog API 响应、账号/企业 header、登录跳转结果或 consent 页面截图。
4. 没有安装、更新、卸载、重装、取消、失败、重试或部分成功的产品运行态。
5. 没有浅色主题下的原版“发现应用”稳定截图;当前浅色验收来自 YCode 自身。
6. REA DOM/layout 会省略文本和输入值,不能从布局节点单独证明中文文案。
7. 运行时截图与 DOM/layout 是顺序观察,不是同一原子帧。
8. 最新打开设置窗口探针在 REA 参数校验阶段失败;没有设置 target、`buddyApps` active 状态或主页面截图。
9. `BuddyAppsPanel-*` 是否已在当前窗口加载仍未证明;没有打开主页面就不能把“未观察到”写成“不存在”。
## 可用入口与选择器
### 设置主页面入口
`REA-STATIC` 已从保存的 renderer 正文确认设置导航项稳定带有 `data-settings-tab`:
```css
[data-settings-tab="buddyApps"]
.settings-navigation__item[data-settings-tab="buddyApps"]
.settings-navigation__item--active[data-settings-tab="buddyApps"]
```
设置窗口外层可用的稳定选择器:
```css
body.settings-window
.settings-window-root
.settings-modal__nav
.settings-navigation
.settings-modal__content
.settings-modal__panel
```
现有证据截图对应“应用管理”的导航行,但页面未打开。主协调补采时建议:
1. 选择设置窗口自己的 REA target。历史设置 target 为 `8E8A75786E23EA4A57CD049223F7099D`;当前主窗口 target `1CAD78026104DDABDB43F9EB6BA7635D` 不能默认代表设置窗口。
2. 点击 `button[data-settings-tab="buddyApps"]`。
3. 等待 `.settings-navigation__item--active[data-settings-tab="buddyApps"]`。
4. 再等待 500–1000ms,确保 `BuddyAppsPanel-vC0qX_Oo.js` 与其 CSS 懒加载完成。
5. 因主页面根 class 尚未知,先使用 `.settings-modal__panel`、`.settings-modal__content` 和 `.settings-modal__panel > *` 记录页面边界,不要先假定 `.buddy-apps-panel`。
6. 同一次 lifecycle 同时采集视觉、script sources 和 style sources;只采稳定页面,不点击安装、更新、卸载或授权动作。
### 发现应用浮层
`REA-RUNTIME` 与 `REA-STATIC` 共同确认的稳定选择器:
```css
.industry-template-switcher__trigger
.industry-template-switcher__trigger--general
.industry-template-switcher__trigger--application
.industry-template-switcher__reset-button
.industry-template-switcher__popover[role="dialog"]
.industry-template-switcher__panel
.industry-template-switcher__search-header
.industry-template-switcher__search
.industry-template-switcher__search .wb-input__inner
.industry-template-switcher__list
.industry-template-switcher__group
.industry-template-switcher__group-items
.industry-template-switcher__item[data-template-id]
.industry-template-switcher__item-action-btn
.industry-template-switcher__retry
.industry-template-switcher__more
.industry-template-switcher__back
.industry-template-switcher__recent-grid
.industry-template-switcher__empty
```
说明:
- `data-template-id` 在运行时实际承载 `applicationId` 值。
- 选中项在原版是 `role="option"` 的非按钮 `div`,未选中项是可点击 `button`。
- 不应依赖样式哈希类名 `src-*` 或 `_tag_*`,这些类名跨构建不稳定。
## 原版可证行为
### 1. 功能开关、登录与入口
`REA-STATIC`:
- 设置路由 `buddyApps` 由 feature flag `buddyAppEnabled` 控制是否可见。
- 发现应用根据账号、API route 和 feature flag 决定是否出现。
- 未登录时点击打开入口会先走登录,不会直接展示云目录。
- API route 分个人与企业两种;企业路线把 enterprise id 编入路径。
`REA-RUNTIME`:
- 暗色主窗口侧栏顶部存在“发现应用”胶囊入口。
- 当前截图中的入口是通用态,文案“发现应用”,不是已选中某个业务应用的状态。
### 2. 浮层结构与分组
`REA-RUNTIME` 稳定打开态:
| 结构 | 实测 |
| --- | --- |
| 触发按钮 | `[173.28125,48,98,28]`;12/500/20;暗色背景 `rgb(46,46,46)`;圆角 100px |
| 浮层 | `[31,82,240,420]`;固定定位;padding 8;圆角 16;暗色背景 `rgb(36,36,36)` |
| 内面板 | `[40,91,222,402]` |
| 搜索区 | `[40,91,222,33]`,输入框 `[40,91,222,28]` |
| 列表视口 | `[40,128,222,365]`;gap 4;纵向滚动 |
| 推荐组 | 标题高 24,条目列表高 960;按 48px 行高可容纳 20 项后继续滚动 |
| 条目 | 218×48;padding 8;gap 12;圆角 8 |
| 图标 | 24×24;列表中圆形,暗色背景 `rgb(41,41,41)` |
| 标题/描述 | 13/16、11/14;描述为 50% 暗色文字 |
当前截图可见的推荐样例为“通达信、企鹅教师助手、美图设计室、腾讯自选股、腾讯电子签AI合同助手、Alpha派 Lite、广发证券”。这些是随账号/服务变化的运行时数据样例,**不得写死为 YCode 目录契约**。
### 3. 列表、搜索和分页
`REA-STATIC`:
- `recent` 与 `recommended` 各按场景请求 20 项。
- 默认只展示 recent 前 6 项;recommended 会过滤掉已在 recent 中出现的 `applicationId`。
- recent 超过 6 项或 `hasMore` 时有“更多”,进入独立 recent 网格视图并可返回。
- 非搜索模式滚动到距底部 80px 内时调用对应场景的 `loadMore()`。
- 搜索输入有 250ms debounce;搜索模式隐藏 recent/recommended 分组,只显示搜索结果。
- 搜索有 loading、error/retry、空结果非搜索和搜索无结果状态。
- 请求使用 owner key 隔离个人/企业账号;旧请求有 generation guard。
`REA-RUNTIME` 只覆盖默认非空推荐列表;上述分页、搜索、错误和空态属于代码事实,未逐态截图。
### 4. 条目身份与选择
列表 summary 的核心字段来自 `REA-STATIC`:
```ts
interface BuddyApplicationSummary {
applicationId: string
name: string
description?: string
icon?: string
operationTag?: {
title: string
description: string
}
}
```
条目行为:
- 未选中行悬停/聚焦显示“进入”动作;选中行显示品牌色对勾与“退出”。
- 选中项由当前应用/模板选择状态决定,不由列表自带 installed 布尔值决定。
- 点击未选中 summary 后,浮层先关闭,再请求 detail。
### 5. 应用详情、激活与授权
`REA-STATIC` 证明的 detail 字段:
```ts
interface BuddyApplicationTemplate {
templateId: string
applicationId: string
name: string
templateName: string
description?: string
category?: string
operationTag?: { title: string; description: string }
icon?: string
schemaVersion: number
templateVersion: number
versionNo: number
requiredScopes: string[]
allowedOrigins: string[]
templateConfig: unknown
releaseNote?: string
publishedAt?: string
}
```
上面的 `templateConfig` 在实际代码里还有 schema 校验;这里保留为 `unknown`,避免把未经原文逐项恢复的完整 schema 当成 YCode 契约。
选择流程:
1. 请求 detail。
2. 等待本地选择状态 hydrated,最长 8s。
3. 校验账号在异步过程中没有切换。
4. 调登录/授权状态接口,取得 `authorized`、application、redirect URI 和 granted scopes。
5. 已授权则直接提交本地模板上下文。
6. 未授权则显示 consent;用户可批准或跳过跳转。
7. consent 后获得一次性 code 和 granted scopes。
8. 根据模板配置安装/连接 MCP 依赖,并可能打开外部 authorization URL。
9. 成功后提交模板选择、上报 usage、触发 onboarding,并返回主页。
10. 退出当前应用时清空选择,并尝试移除该应用的 MCP dependency。
全局同一时间只允许一个应用切换流程,避免并发授权。
### 6. 远程 API 事实
`REA-STATIC` 中可确认的个人账号端点:
| 功能 | 方法 | 路径/参数 |
| --- | --- | --- |
| 场景列表 | GET | `/api/v1/open-platform/buddy_application?limit={limit}&scene={recent\|recommended}[&cursor=...]` |
| 搜索 | POST | `/api/v1/open-platform/buddy_application/search`,body `{ keyword }` |
| 详情 | POST | `/api/v1/open-platform/buddy_application/detail`,body `{ application_id }` |
| 使用上报 | POST | `/api/v2/open-platform/asset-usage/report`,body `{ asset_id, asset_type: "buddy_app" }` |
| 入口可用性 | GET | `/api/v2/open-platform/buddy/entrance` |
企业账号使用 `/v2/enterprises/{enterpriseId}` 前缀和对应子路径。
列表响应可确认字段包括 `applications`、`has_more`、`next_cursor`。请求会按需附加 `X-GPU-DEVICE-NAME`。
**目前没有证据证明以下接口:**应用包下载、安装、更新、卸载、版本检查、安装进度、取消、回滚、权限撤销、已安装列表。不能用上述 discovery/usage 端点臆造完整安装 API。
### 7. 设置页中的应用管理
`REA-STATIC` 已确认:
- 设置 route id 为 `buddyApps`。
- 入口是 `button[data-settings-tab="buddyApps"]`,中文截图显示“应用管理”。
- 页面是懒加载 `BuddyAppsPanel-vC0qX_Oo.js`,配套 `BuddyAppsPanel-OzkcFEe0.css`。
- route 可用性受 `buddyAppEnabled` 控制。
**尚不能确认:**
- 主页面标题、分组、搜索、筛选和排序。
- 是否显示市场目录、已安装应用或两者共存。
- installed 的判定规则,以及安装与应用激活是否同一概念。
- 版本、更新提示、自动更新、更新检查、发布说明。
- 安装、更新、卸载、打开、停用、退出、重试、取消的入口和确认方式。
- loading、empty、error、partial、offline、unauthorized、dependency-missing 状态。
- 主页面的所有几何、深层深浅主题和动画。
这是主协调下一步最优先的 REA 补采目标。
2026-10-17 补采结果:
- 设置窗口 target 不在当前 REA endpoint 的 target 列表中,只有一个主窗口。
- 计划中的 `open-account-menu → open-settings-window` 探针没有进入动作生命周期;REA 在输入校验阶段返回 `invalid_request / invalid_input`,因此没有 Evidence ID 和声明式动作步骤。
- 没有采集 `button[data-settings-tab="buddyApps"]`、`.settings-navigation__item--active[data-settings-tab="buddyApps"]` 或 `.settings-modal__panel` 的主页面内容。
- 本专项按用户要求停止重复尝试。安装中心主页面的布局、状态、版本更新、卸载和错误恢复仍为明确未知,等待后续设置窗口 target 和新 Evidence 补证。
## YCode 当前状态
### 已实现
| 能力 | 状态 | 主要源码 |
| --- | --- | --- |
| “发现应用”入口与浮层 | 已实现可交互壳 | `src/renderer/src/components/AppDiscoverPopover.tsx` |
| 浮层几何、搜索、空态、键盘、外部点击、焦点恢复 | 已实现本地版本 | 同上、`app-discover-popover.css` |
| 最近使用 | 已实现 localStorage 版,最多 6 个本地能力 ID | `ycode.app-discover.recent.v1` |
| 应用选择 | 映射到 experts / skills / connectors 三个本地能力页 | `Sidebar.tsx`、`App.tsx`、`CapabilityCenter.tsx` |
| 选中态退出 | 从本地能力页回到聊天 | `AppDiscoverPopover` 的 `onExit` |
| 本地能力 | 专家、本地技能、自定义 MCP 已有各自页面和数据层 | `src/renderer/src/capabilities/` |
### 占位
| 能力 | 现状 |
| --- | --- |
| 技能市场 | 技能页对未连接目录显示本地空态,可导入本地技能 |
| 连接器目录 | 当前以自定义 MCP 为主,云目录和第三方 OAuth 未接 |
| 最近使用 | 只保存本地三个 capability ID,不是原版账号级应用使用历史 |
| 应用图标 | 使用本地 SVG,不是远端应用图标和失败 fallback |
### 缺失
| 能力 | 缺口 |
| --- | --- |
| 安装中心主页面 | 完全缺失;设置导航中 `buddyApps` 还被用户明确移除 |
| 云应用目录 | 无列表、搜索、detail、最近、推荐和分页 API |
| 应用身份模型 | 无 `applicationId/templateId/version/scopes` 数据 |
| 安装状态机 | 无 installed/installing/updating/uninstalling/failed |
| 安装来源 | 无市场、企业、URL、本地包或内置来源模型 |
| 更新与版本 | 无版本比较、release note、更新检查 |
| 卸载与恢复 | 无卸载确认、部分失败、重试、回滚 |
| 授权 | 无 login、consent、scope grant、redirect URI |
| 应用依赖 | 无应用级 MCP dependency 安装/移除 |
| 使用上报/onboarding | 无 usage report、show observer、onboarding 状态 |
| 管理入口 | `SettingsDialog.tsx` 的 `REMOVED_SETTINGS_TABS` 包含 `buddyApps` |
### 需要先决定的产品边界
用户此前要求移除“系统设置 → 应用管理”入口。若后续恢复安装中心,不应静默推翻该决定。可由主协调确认采用下列之一:
1. 保留设置入口移除,只在侧栏“发现应用”中增加管理/浏览入口。
2. 恢复“应用管理”设置页,同时保留“发现应用”作为切换器。
3. 将两者合并为独立安装中心页面,设置与侧栏都指向同一路由。
本拆解建议先按“两个表面、一个状态源”设计:发现浮层负责快速发现/切换,安装中心管理页负责目录、安装、版本、授权和卸载;两侧共享同一 registry,避免出现两套 installed 状态。
## 建议数据契约
以下是 `INFERENCE`,用于 YCode 后续实现;字段名尽量贴近 REA 静态事实,但不冒充原版完整 schema。
```ts
type BuddyAppSource = 'builtin' | 'marketplace' | 'enterprise' | 'local'
type BuddyAppAvailability = 'available' | 'unavailable' | 'entitlement-required'
type BuddyAppLifecycle =
| 'not-installed'
| 'installing'
| 'installed'
| 'active'
| 'updating'
| 'update-available'
| 'uninstalling'
| 'failed'
| 'unauthorized'
| 'dependency-missing'
interface BuddyAppSummary {
applicationId: string
templateId?: string
name: string
description?: string
iconUrl?: string
operationTag?: { title: string; description?: string }
source: BuddyAppSource
availability?: BuddyAppAvailability
}
interface BuddyAppDetail extends BuddyAppSummary {
name: string
versionNo: number
templateVersion?: number
schemaVersion?: number
category?: string
requiredScopes: string[]
allowedOrigins: string[]
templateConfig: unknown
releaseNote?: string
publishedAt?: string
}
interface BuddyAppRegistryEntry {
applicationId: string
templateId?: string
installedVersion?: number
availableVersion?: number
lifecycle: BuddyAppLifecycle
source: BuddyAppSource
installedAt?: number
lastOpenedAt?: number
lastError?: string
operationId?: string
dependencies: BuddyAppDependencyState[]
}
interface BuddyAppDependencyState {
kind: 'mcp' | 'connector'
key: string
state: 'missing' | 'installing' | 'connected' | 'needs-auth' | 'error'
error?: string
}
interface BuddyAppOperation {
operationId: string
applicationId: string
kind: 'install' | 'update' | 'uninstall' | 'activate' | 'deactivate'
phase: 'queued' | 'downloading' | 'verifying' | 'applying' | 'authorizing' | 'connected' | 'done' | 'failed' | 'cancelled'
progress?: number
message?: string
startedAt: number
finishedAt?: number
}
```
工程约束:
- `applicationId` 与 `templateId` 分开;现有原版列表以 `applicationId` 去重。
- 版本、scope、origin、redirect URI 必须保存快照,避免更新过程中配置变化。
- installed、active、authorized、dependency-ready 必须分开。
- operationId 用于 UI 去重和恢复;不能只靠 loading 布尔值。
- token、authorization code、secret 不进入 renderer 持久状态或日志。
## 本地功能与远端依赖边界
| 能力 | 本地可完成 | 依赖远端/宿主 |
| --- | --- | --- |
| 浮层/主页面布局 | 是 | 否 |
| 列表缓存、最近使用、当前选择 | 是 | 初次目录数据需要远端 |
| 本地 registry 和 operation journal | 是 | 状态变更需要服务端确认 |
| 目录列表、搜索、详情 | 否 | 是 |
| 登录、consent、scope grant | 否 | 是 |
| 应用包下载、校验、安装、更新、卸载 | 否 | 是;若 YCode 自建安装包生态则另立本地协议 |
| 应用模板 config 和 allowed origin | 可缓存 | 更新源来自远端 |
| MCP dependency 本地注册 | 是 | 授权/远端 connector 仍可能需要服务端 |
| usage report、show/click telemetry | 可本地排队 | 上报需要远端 |
| entitlement、企业策略、配额 | 否 | 是 |
| 错误文案和恢复 UI | 是 | 具体 reason code 来自宿主/服务端 |
| 安装 provenance/checksum | 可实现本地校验器 | 可信摘要/签名来源需可信配置 |
原则:没有真实安装结果时,UI 只能显示“未接入/等待服务”,不能显示“安装成功”。
## 建议模块拆分
### A. `buddyAppCatalog`
职责:
- recent/recommended/search/detail。
- 500ms 内搜索 debounce 的内部实现可沿用 250ms 事实,但需要统一 abort/generation。
- 账号、企业、feature flag 作为请求 key。
- summary/detail 校验、去重、缓存和 stale 响应丢弃。
不负责安装状态。
### B. `buddyAppRegistry`
职责:
- 已安装列表、版本、来源、最近打开时间。
- selected application/template。
- install/update/uninstall/active 状态。
- 操作日志、错误和恢复。
本地 registry 必须可以独立启动;远端 catalog 失败不能让已安装应用消失。
### C. `buddyAppInstaller`
职责:
- 下载、完整性校验、解包/安装、原子切换、失败回滚。
- operation progress/cancel。
- 安装来源差异。
此模块是否存在原版真实包安装流程目前未知;不得把“安装 MCP dependency”改名为“安装应用”。
### D. `buddyAppAuthorization`
职责:
- login/authorized 状态。
- consent、scope diff、grant。
- redirect/MCP 授权的衔接。
- 账号切换取消未完成操作。
### E. `buddyAppUi`
包含两个 surface:
- switcher:快速发现、最近、推荐、搜索、切换/退出。
- manager:已安装、目录、版本、更新、卸载、授权、依赖、错误恢复。
两者共享 Registry/Store,但各自保留 surface-specific UI state。
### F. `buddyAppObservability`
- 展示曝光、点击、进入、consent、授权失败、usage report。
- 事件失败不能破坏业务主流程。
- 脱敏 app id/version,不记录 token/code/secret。
## 分批任务与验收标准
### 批次 0:补采应用管理主页面
范围:
- 主协调通过现有 REA lifecycle,在设置窗口点击 `[data-settings-tab="buddyApps"]`。
- 同次 capture 开启 scripts、styles、visuals。
验收:
- 捕获到 `BuddyAppsPanel-vC0qX_Oo.js` 正文或其新的 build hash。
- 捕获到 `BuddyAppsPanel-*.css` 正文。
- 新 Evidence ID、target、captured_at、script/CSS SHA-256、viewport、theme 全部记录。
- 产出主页面 DOM/layout/稳定截图,至少覆盖默认稳定态。
- 明确没有触发安装、更新、卸载、授权等有副作用动作。
当前状态:**已尝试,未完成。** 最新 target 枚举只有主窗口;打开设置窗口的探针在 REA 参数校验阶段失败,没有设置 target。批次 0 未通过,阻塞条件正是设置页面 source/lifecycle,而不是已有静态结论不足。
取消条件:
- 只采到导航 active class,未加载 lazy chunk。
- 只采到 `.settings-modal__panel` 但内容仍为其他设置页。
### 批次 1:数据契约与只读 registry
范围:
- 建 `BuddyAppSummary/Detail/RegistryEntry/Operation/Dependency` 类型。
- 建立按账号/企业隔离的本地 registry。
- 无远端时显示真实“目录未连接”,已安装本地条目仍可展示。
验收:
- ID 身份稳定;同 application 不会因 template 版本重复成多张卡。
- 本地 registry 跨重启保留。
- account/enterprise 切换不会串数据。
- 没有真实 installed 数据时不伪造 installed 状态。
### 批次 2:应用管理页壳与本地状态
范围:
- 安装中心主页面骨架、搜索、筛选、分组。
- 列表、空态、加载态、错误态和 operation banner。
- 配置入口决定:独立页或恢复设置入口由主协调拍板。
验收:
- 在 900×680 设置窗口和主窗口布局无溢出。
- 浅色/深色、正常/窄宽、空/错/loading 可稳定渲染。
- `[data-settings-tab="buddyApps"]` 或替代入口的导航语义明确。
- 不依赖云端即可打开本地 manager shell。
### 批次 3:目录列表、搜索和详情
范围:
- recent/recommended/search/detail adapter。
- summary 去重、分页、stale guard、请求取消。
验收:
- 空查询、250ms 搜索、无结果、失败重试、下一页、末尾停止。
- recent 与 recommended 去重。
- account/enterprise 切换取消旧请求。
- 远端不可达时返回可恢复错误,不显示空目录冒充成功。
### 批次 4:安装/激活状态机
范围:
- `not-installed → installing → installed → active` 等状态。
- 同一 application 的并发操作锁。
- failure、retry、cancel、app restart 恢复。
验收:
- 双击安装只产生一个 operation。
- UI 进度来自 operation 状态,不由定时器模拟。
- 失败不会部分标记 installed。
- 重启后未完成操作可查询、恢复或明确标记未知。
- “安装应用”与“安装 MCP dependency”在产品文案和状态上分开。
### 批次 5:更新、卸载与恢复
范围:
- 版本比较、release note、确认流程。
- 卸载对 active 应用、MCP dependency、云端授权和本地数据的影响。
验收:
- 同版本、旧版本、新版本、降级、unknown version均有明确状态。
- 更新失败回滚到旧版本和旧 selected template。
- 卸载前明确是否保留本地数据与授权。
- 卸载失败可重试,不把卡片直接删掉。
### 批次 6:登录、consent、scope 和依赖授权
范围:
- login/authorization adapter。
- consent summary、scope diff、取消、拒绝、跳转失败、回到桌面。
- MCP/builtin/custom/joint auth 分支。
验收:
- 账号切换时取消旧流程。
- redirect URI、scopes、template version 在授权期间发生变化则拒绝提交。
- 取消/失败不会写入 ready 状态。
- code/token 不落盘、不打印。
- 依赖失败与应用安装状态可区分。
### 批次 7:像素与全状态验收
范围:
- switcher 与 manager 的原版同数据、同主题、同尺寸对照。
验收:
- 入口、popover、manager 默认态在浅/深色完成截图和几何比对。
- 搜索、loading、error、empty、scroll、load-more、hover、focus、selected、disabled。
- 安装、更新、卸载、授权、依赖、失败、重启恢复。
- 键盘、focus trap、Escape/外部点击、IME 和窄窗。
- 不以 YCode 专项 PASS 代替 WorkBuddy 同状态 1:1。
## 精确未知项
### 主页面与产品信息架构
1. 设置“应用管理”和侧栏“发现应用”是否共享同一个应用实体、选择状态和 installed 列表。
2. 主页面是否只管理已安装应用,还是也展示市场目录。
3. 主页面标题、分组、筛选、排序、搜索和详情进入方式。
4. 是否支持企业目录、内置应用、本地应用和第三方 market 的混合列表。
### 安装语义
1. “安装”是否下载本地包,还是只创建账号授权/云端绑定。
2. 安装与应用“进入/激活”是否同一操作。
3. 安装成功后是否立即成为当前应用,还是仍展示“进入”。
4. 同一 application 是否可安装多个 template/version。
### 更新语义
1. 更新源、版本比较规则和自动/手动检查时机。
2. 更新通知显示位置、release note 来源和强制更新规则。
3. 更新中能否使用当前版本,失败时是否回滚。
### 卸载语义
1. 卸载按钮的确认文案和危险色层级。
2. 本地数据、云端 grant、MCP dependency、recent snapshot 是否一起清除。
3. active 应用或 dependency-disconnected 状态下的卸载限制。
### 状态与错误
1. 完整状态枚举和 reason codes。
2. 进度是否支持百分比、字节数、阶段文案。
3. 取消、超时、断网、配额不足、企业策略、权限撤销、依赖失败的实际文案。
4. 失败后 operation 是否保留、恢复或可重试次数。
### 数据与持久化
1. 本机 registry 的 storage key、schema version、账号/企业隔离。
2. 已安装包位置、完整性校验、签名和升级目录。
3. recent/show/usage 的服务端与本地双层缓存。
4. 应用图标 URL、缓存、CSP 和失败 fallback。
### 视觉
1. “应用管理”主页面完整 DOM/CSS、默认数据量和空数据量。
2. 浅色稳定态和深色稳定态。
3. 安装/更新/卸载/授权弹窗与 toast。
4. 菜单、hover、disabled、focus、loading、滚底、分页。
5. 900×680 设置窗口之外的独立页面尺寸。
## 补采检查表
主协调下一次统一补采应至少保存:
- 设置 target id、窗口 URL 和 viewport。
- `[data-settings-tab="buddyApps"]` 点击前后稳定帧。
- 主页面 `.settings-modal__panel` 的 DOM、layout、可见文本和截图。
- `BuddyAppsPanel-vC0qX_Oo.js`/新 hash 的完整 REA script source。
- `BuddyAppsPanel-OzkcFEe0.css`/新 hash 的完整 REA style source。
- 默认列表数量及至少一个已安装、一个未安装、一个有更新条目的字段。
- 主操作的 DOM selector、文案、disabled 条件和确认弹窗。
- 浅色与深色稳定态;若窗口主题由设置控制,记录切换证据。
- 只读网络/状态来源证据;不在主页面直接执行真实安装或卸载。
建议的探针起点:
```json
[
{
"kind": "click",
"selector": "[data-settings-tab=\"buddyApps\"]",
"frames": 0
},
{
"kind": "wait",
"duration_ms": 750,
"frames": 1
}
]
```
探针完成后以 `.settings-navigation__item--active[data-settings-tab="buddyApps"]` 确认路由,再检查 `.settings-modal__panel` 内是否出现新模块内容。
## 决策
1. **不把发现浮层等同于安装中心。** 两者共享可能的数据,但当前证据将它们拆为 switcher 与 manager 两个 surface。
2. **不在主页面证据缺失时实现安装按钮。** 后续先建立 registry/operation 状态,再接真实能力。
3. **把 installed、active、authorized、dependency-ready 分开。** 现有原版已证明选择/授权/依赖是独立步骤。
4. **应用 ID 使用 `applicationId` 作为首选去重键。** `templateId`、version 和 schema 另存。
5. **远端目录失败不等于空目录。** 本地已安装 registry 必须离线可见,远端错误必须可恢复。
6. **保留用户移除设置入口的决定。** 安装中心的最终入口位置需要主协调明确,不能擅自恢复 `buddyApps` 设置项。
7. **sample app 名称只作截图样例。** 通达信等运行时条目不得写死。
## 本轮核对命令
本轮没有运行测试或构建。除下方明确列出的 REA target 枚举和两次声明式探针外,其余核对均为静态读取。
```text
rg --files docs | rg '安装|应用|发现|中心|模块拆解'
Get-ChildItem .tmp -Recurse | Where-Object FullName -match 'rea-todo18'
rg -n '安装中心|发现应用|Discover|app discover|install' docs src scripts
Get-Content docs/复刻待办清单-2026-10-17.md
Get-Content src/renderer/src/components/AppDiscoverPopover.tsx
Get-Content src/renderer/src/components/app-discover-popover.css
Get-Content src/renderer/src/capabilities/CapabilityCenter.tsx
Get-Content scripts/verify-app-discover.cjs
Get-Content .tmp/rea-todo18-BuddyAppDiscoverPanel.js
Get-Content .tmp/rea-todo18-BuddyAppIcon.js
Python 解析 .tmp/rea-todo18-*.json 的 evidence_id、target、captured_at、viewport、artifact SHA-256、limitations
Python 解析 .tmp/rea-todo18-main-scripts.json 的 contexts-DgzzFjnf.js source artifact
Python 解析 .tmp/rea-todo18-styles-current.json 的 contexts-BkjwYSZT.css source artifact
Python 解析 .tmp/rea-todo18-discover-state/layout.json 的浮层几何
Python 解析 .tmp/rea-todo18-settings/layout.json 的设置导航结构
Python 在保存的 contexts-DgzzFjnf.js 中检索 API、selection、consent、dependency、settings route
```
2026-10-17 新增的 REA 读取与失败记录:
```text
node .tmp/rea-css-runtime/scripts/rea.mjs list-electron-targets http://127.0.0.1:9223
REA inspect-electron-page:主窗口 + 仅 wait 的 visual_probe
REA inspect-electron-page:主窗口 + 打开账户菜单/设置窗口的 visual_probe
结果:target list 成功;无操作探针成功;设置打开探针 invalid_request,未生成 Evidence ID
保存:.tmp/rea-four-modules-install-targets.json
.tmp/rea-four-modules-install-probe-minimal.json
.tmp/rea-four-modules-install-open-settings.json
.tmp/rea-four-modules-install-open-settings-retry.json
```
## 风险
1. **把浮层当完整安装中心**:会严重高估完成度。
2. **把 MCP 依赖安装冒充应用安装**:原版代码明确区分 dependency 与 application 选择。
3. **恢复已移除设置入口而不确认**:与用户此前要求冲突。
4. **远端服务不可用时伪造状态**:可产生虚假 installed/update 结果。
5. **账号/企业隔离错误**:可能把企业目录、授权或用量混进个人数据。
6. **授权并发和账号切换**:可能出现旧 consent 写入新账号。
7. **安装/更新破坏用户内容**:需要原子操作、备份、回滚和明确确认。
8. **敏感信息泄漏**:authorization code、token、secret 和完整远端 URL 不应进入日志/普通本地状态。
9. **运行时动态条目被写死**:sample app 名称、顺序和描述会随账号/服务变化。
10. **主页面像素误判**:当前没有该页面任何 REA 视觉,不能从浮层几何推导。
## 交接结论
本轮安装中心专项到此收口:已有足够证据拆出“发现浮层”和“安装中心主页面”两个 surface,以及目录、选择、授权、依赖和未知安装生命周期边界;但安装中心主页面仍没有 REA 运行时证据。下一步需要先获得可操作的原版设置窗口 target,再点击 `[data-settings-tab="buddyApps"]` 并同次采集 DOM、视觉、脚本和样式。补采成功后,优先恢复 `BuddyAppsPanel` 的 DOM/CSS 和页面状态,再决定 manager 的真实信息架构。YCode 已有浮层壳可以保留,但其数据层仍是本地能力适配,必须和后续云应用 registry 解耦。
+171
View File
@@ -0,0 +1,171 @@
# 定时任务模块拆解(2026-10-17)
> 开发执行入口:[三模块开发计划](三模块开发计划-2026-10-17.md)。新增公共执行服务抽取、时钟/持久化策略、首批分工和验证门槛;下文保留原版事实与未知边界,工程基线不等于原版规则已证实。
当前状态:完成首版页面、表单、频率模型、执行关系与实施批次拆解。本文只使用已保存的 WorkBuddy 5.8.3 REA Evidence 和当前 YCode 源码事实;没有启动原版或 YCode,没有构建、测试、导航或创建任务。
本轮目标是把“定时任务”从单个占位入口拆成可实现的本地闭环。页面复刻、任务定义、运行记录、调度器、会话执行和系统恢复必须分别建模,不能用 UI 计时器或 Toast 冒充调度成功。
## 证据边界
| 范围 | Evidence / 工件 | 已确认 | 限制 |
| --- | --- | --- | --- |
| 定时任务页 | `ev_386df7d869a651cc6c083205fb10c4264ec8356901bc0b031cab63d64d65c8d5`;`.tmp/rea-todo20-scheduled-page.json`、`rea-todo20-scheduled-page.png` | 1336×902 深色页面、导航、空态、模板卡、工具栏和布局 | 固定账号、主题、窗口与空数据状态 |
| 新建表单 | `ev_be404f0454dfd7496f91ff79a7f43bb61a54ad587933baee0d1cf77c4db915c5`;`.tmp/rea-todo20-scheduled-create.json`、`rea-todo20-scheduled-create.png` | 标题、名称、提示词、工作空间、权限、重复、时间、有效期、企业微信和按钮 | 仅空白表单静止态;没有提交、切换频率或打开弹层 |
| 静态实现 | `ev_625e3664a4da2b25291494bd5b985d16b09d1654d5f8170d8af6325d2019c159` | `automation-qDy4JNlv.js`、`automation-C36RYsTC.css`、路由和同次页面资源 | 来源覆盖部分;不代表原版全部脚本、服务端和状态 |
`app.asar` SHA-256 为 `3e50fcb5b060deba4f1ea9a8c293cf955bf12d7e52d971985de914350afd9077`。
`automation-qDy4JNlv.js` SHA-256 为 `687f7b062b0df0f353dafd1f17b00cb939685da5a12c8d3ce865b920b94d6710`,131,537 字节。`automation-C36RYsTC.css` SHA-256 为 `28c6dc5e1a8d83d7b955d00a604723898a296f0ece4a3bbe4d6db26fc3bbbda5`,56,516 字节。
证据可复核位置:两份运行态归档的 `normalized_result.scripts.items` / `styles.items`;只读导出在 `.tmp/todo20-scheduled-source/`。频率锚点为 `getScheduleMode`、`toIntervalRrule`、`toPeriodicRrule`、`ScheduleControls`、`ValidityField`;关闭约束为 `AutomationEditorSurface` 的 `closeOnOverlayClick:false` / `closeOnEscape:false`;权限确认为 `shouldConfirmAutomationFullAccess`、`AutomationFullAccessConfirmModal`、`requestSaveDraft`;页签与记录为 `AutomationToolbar`、`AutomationWorkspacePage`、`RecordsTab`、`RecordRow`、`AutomationRecordsFilterBar`。路由辅助源 `automation-D9XYSeN0.js` 以及中文文案 `zh-cn-C7BTHfrQ.js` 同为既有REA捕获正文的导出;本轮没有直接读取原版包。
## 原版页面结构
侧栏“定时任务”是独立一级入口,选中时使用该页导航态。主区在 Windows 原生标题栏之下从 `y=30` 开始,宽约 `1051.7`,高 `872`。
顶部工具栏高 `64`,左右内边距 `24`,标题为“定时任务”,右侧是“新建”分体按钮。静态实现还暴露“手动新建、从任务新建、从模版新建”入口;当前运行证据只显示主按钮与下拉箭头。
静态实现包含“定时任务 / 运行记录”两个页签,以及来源筛选、关键词搜索、批量管理和状态筛选;运行记录页包含状态、来源、任务筛选。当前运行截图只覆盖空列表,尚未逐一运行这些分支。
空态包含插画、“暂无定时任务”和“试试这些常用任务”。固定证据显示 12 张模板卡:每日 AI 新闻推送、每日 5 个英语单词、每日儿童睡前故事、每周工作周报、经典电影推荐、历史上的今天、每日一个为什么、父母联系提醒、体检预约提醒、面试准备提醒、会议前准备、可爱萌宠手机壁纸。
模板卡为三列,实测单卡约 `326.56×82`,圆角 `16`,内边距 `16px 20px`,列间约 `12`、行间约 `12`。静态点击处理会生成预填的自动化草稿;本轮未运行模板点击流程。
## 新建/编辑表单
固定弹窗宽 `768`、高约 `470.4`,内容面板圆角 `16`,遮罩为 `rgba(0,0,0,.8)`。静态组件配置禁止遮罩点击和 Escape 直接关闭,提供右上角按钮或“取消”;这些关闭行为尚未运行态验证。
标题为“新建定时任务”或“编辑定时任务”。底部“取消 / 确定”位于右对齐页脚,页脚内边距 `24`、按钮间距 `12`。
名称输入框实测约 `721.94×38.41`,圆角 `8`,占位“输入任务名称”。提示词使用富文本编辑器,实测区域约 `721.94×180.48`,占位“添加提示词”,最大 100,000 字符。
提示词编辑器接入共用输入能力,包括模型、专家、连接器、工作空间、权限和可选技能。执行配置至少包含:
- `cwd` / `workspaceScope`
- `modelId`、`modelThinking`、`reasoningEffort`、`contextWindow`
- `expertId`、`expertMarketplace`
- `connectorIds[]`、`skills[]`
- `permissionMode`
- `pushToWecomBot`、`wecomBotSource`
默认权限为 `fullAccess`。静态提交分支包含首次创建或从较低权限切换到完全访问时的确认弹窗,说明文件、终端和互联网能力,并要求确认后保存;该分支尚未运行态验证。
调度行实测为三个等宽字段,约 `230.4×38.4`,字段间距 `16`:重复、时间、有效期。单次任务隐藏有效期字段。
名称或提示词为空、提示词超过上限时“确定”禁用。提交时还会校验名称必填、提示词必填和提示词长度。
## 七类频率及规则
运行态默认显示“重复:每天,时间:当前时间后约 5 分钟”。静态实现确认七类分支:
| 频率 | 模型规则 | UI 行为 |
| --- | --- | --- |
| 间隔 | `FREQ=HOURLY;INTERVAL=N`,可选工作日限制 | 显示“每隔 N 小时”;静态解析也兼容 `FREQ=MINUTELY`,但 UI 会换算成小时 |
| 单次 | `type=once` + ISO `scheduledAt` | 选择日期和时间;必须晚于当前时间,隐藏有效期 |
| 每天 | `FREQ=DAILY;BYHOUR;BYMINUTE` | 每日同一时刻 |
| 每周 | `FREQ=WEEKLY;BYDAY=...;BYHOUR;BYMINUTE` | 多选周一至周日;全选显示“每天”,工作日显示“工作日”,周末显示“周末” |
| 双周 | `FREQ=WEEKLY;INTERVAL=2;BYDAY=...` | 每两周的所选星期 |
| 每月 | `FREQ=MONTHLY;BYMONTHDAY=...;BYHOUR;BYMINUTE` | 支持一个或多个月内日期,静态上限为 31 日 |
| 每年 | `FREQ=YEARLY;BYMONTH;BYMONTHDAY=...;BYHOUR;BYMINUTE` | 选择月份、日期和时间 |
有效期使用 `validFrom`、`validUntil`;二者为空时显示“长期有效”,支持起始、截止或起止区间。日历不允许早于当天,清空后恢复长期有效。
云任务只开放间隔、单次、每天三类分支。YCode 当前范围先做本地任务,不把这条云端限制冒充本地限制。
## 列表、生命周期和运行记录
本节为上述REA静态证据的客户端分支观察,未创建原版任务,也未验证服务端实际执行和状态变化。
任务定义状态至少为 `ACTIVE`、`PAUSED`,单次任务过期后呈现 `EXPIRED` 语义。操作包括编辑、手动运行、暂停、开启、删除、归档和批量删除。
运行记录使用独立列表,列为任务名称、状态、运行时间、耗时、任务类型和操作。状态筛选至少包含全部、运行中、成功、失败;记录可归档、取消归档和删除。
来源至少包含本地、项目、云端。项目任务只允许回到项目查看/管理;云端和移动端记录可能存在只读状态。YCode 首轮只实现本地,项目和云端保留明确未接态。
手动运行不是普通按钮反馈。静态实现等待任务产生真实 `sessionId`,成功后打开对应会话;运行中重复触发会被拒绝,删除运行中的任务也会被拒绝。
## 当前 YCode 现状
`src/renderer/src/components/Sidebar.tsx` 已有定时任务图标映射,但没有定时任务业务页面。
`src/renderer/src/App.tsx` 当前主路由仍是 `chat | capabilities | projects`,没有可达的 `scheduled` 页面。
`src/renderer/src/components/SourceHome.tsx` 有“定时任务”快捷动作,`src/renderer/src/mock.ts` 仍把定时任务标为 `implemented:false`。
`src/renderer/src/components/GeneralSettingsPanel.tsx:336` 只保留禁用态的“定时任务产物通知”,没有通知执行链路。
当前没有自动化定义、运行记录、RRULE 解析、持久化仓库、IPC、主进程调度器、重启恢复或真实会话触发实现。
## 建议数据契约
以下是 YCode 本地实现设计,不是对原版服务端结构的断言:
```text
AutomationDefinition 1 ── 0..n AutomationRun
AutomationRun 0..1 Session
AutomationDefinition 0..1 Workspace / Project
```
`AutomationDefinition`:`id`、`name`、`prompt`、`status`、`schedule`、`execution`、`origin`、可选 `workspaceId/projectId`、`createdAt`、`updatedAt`、可选 `archivedAt`。
`AutomationSchedule`:`type:'once'|'recurring'`、可选 `scheduledAt`、可选 `rrule`、`validFrom?`、`validUntil?`,并建议增加明确 IANA `timeZone`。
`AutomationExecution`:目录/空间、模型与思考配置、上下文窗口、专家、连接器、技能、权限、通知目标和模板来源。
`AutomationRun`:`id`、`automationId`、`trigger:'scheduled'|'manual'|'retry'`、计划时间、排队时间、开始/结束时间、`status`、`sessionId`、工作目录、名称快照、失败摘要、归档时间。
定义和记录必须原子落盘。提交保存时冻结任务名称、提示词、调度与执行配置;修改任务只影响后续运行,不篡改历史记录。
## 本地调度与执行关系
主进程是调度权威。渲染进程只负责表单、列表和倒计时展示,不能依赖页面存活触发任务。
持久化后维护最小触发时间堆或有序索引。启动、系统恢复、时区变化和配置更新时重新计算下一触发点。
到期后先原子创建 `queued` 运行记录并领取任务,再调用现有会话创建、OMP/模型执行和产物链路。一次运行创建或恢复一个会话,会话保存 `automationId`、`runId` 标签。
运行事件继续复用现有 chat 事件总线并广播到所有窗口。运行记录状态由主进程写入;会话失败、用户取消、应用退出和迟到事件都要落到明确终态。
同一自动化默认禁止重叠运行。全局并发需要上限;重复 timer、窗口重启和系统唤醒后的旧任务必须用运行 ID 与唯一约束去重。
错过触发、休眠恢复、失败重试、最大重试次数和退避目前均未从原版证实。YCode 可以先定义本地策略,但必须在 UI 和文档中标注为本地设计。
## 分批实施与验收
### 批次 A:契约与持久化
建立 shared 类型、RRULE 纯函数、仓库文件、错误码和迁移策略。覆盖创建、更新、暂停、归档、非法输入、原子写、损坏文件保护和重启恢复。
### 批次 B:页面与表单
接入侧栏路由、任务/记录双页签、空态、12 个恢复模板、新建/编辑弹窗、权限确认和深浅主题。先复刻已观察静止态,再补频率和时间弹层。
### 批次 C:调度引擎
在主进程实现下一触发计算、持久化队列、单实例领取、系统恢复后的重算、取消和过期。测试必须使用可控时钟,禁止按真实小时等待。
### 批次 D:会话执行闭环
把一次运行接到真实本地模型与工作区,创建独立会话并持续写入运行记录。验收至少覆盖成功、模型错误、用户取消、工作目录失效、应用退出重开和跨窗口同步。
### 批次 E:列表操作与记录
完成启停、手动运行、编辑、删除、归档、批量删除、运行记录筛选、打开会话和只读来源。失败路径必须有真实错误反馈,不以 Toast 代替状态。
### 批次 F:逐状态 1:1
用同视口、同主题、同字体和同数据比对列表、模板、表单、频率弹层、记录表、空态、错误态、Hover/Focus 与过渡。每个状态保留 REA Evidence ID 和本地截图。
## 待补 REA 证据
- 重复、时间、有效期三个弹层的完整 DOM、计算样式、键盘和边界翻转。
- 每周、双周、每月、每年选择器的实际交互及“工作日/周末”切换。
- 企业微信未连接、URL 重绑定、成功推送和失败提示。
- 任务卡 Hover、运行中、暂停、过期、失败、归档及批量管理状态。
- 运行记录的空、加载、分页、失败详情、归档和删除确认。
- 名称长度上限、精确错误文案、日期跨度限制和编辑单次任务的边界条件。
- 时区来源、夏令时重复/跳过时间、错过触发、休眠恢复、并发和重试策略。
- 原生服务端接口、持久化格式、通知服务、项目任务和云端/移动端只读语义。
这些未知项不能用当前 YCode 行为反推为原版事实。完成既有证据覆盖的本地闭环后,才进入完整 1:1 验收。
@@ -0,0 +1,40 @@
# 对话无头浏览器接入
日期:2026-10-18。范围:YCode 自身对话运行链路;本次不采集 WorkBuddy,不据此声明原版功能或视觉等价。
## 原因与实现
原有 `browser-host.ts` 仅向右侧手动浏览面板提供网页容器;对话的宿主工具列表未注册浏览器,OMP 又以 `--no-tools` 启动。因此移除浏览器设置项后,对话仍没有页面读取与操作入口。
现注册一个内置 `browser` 工具,支持 `open`、`navigate`、`snapshot`、`click`、`type`、`press_key`、`scroll`、`wait`、`screenshot`、`list`、`close`。使用 Electron 内置 Chromium 的隐藏离屏窗口,不需要安装扩展或额外浏览器。原有经济模式和浏览器操控设置保持移除,不新增开关。
- 浏览器归属于会话与工作目录,独立内存存储,不共享个人浏览器登录态。每个作用域最多 4 个标签,进程最多 16 个。
- 页面快照返回可见正文、可交互元素及稳定 ref;导航后旧 ref 失效。也可用唯一 CSS selector 定位。点击、输入与按键经过 Chromium 真实输入通道;不向模型开放任意 JavaScript 执行。
- 仅固定 DOM 操作在隔离世界中运行;页面禁用 Node 与 preload,阻止下载和权限请求,限制顶层 HTTP(S) 导航。页面弹窗仍留在受控窗口中。
- 服从现有模式及权限机制:规划模式不能调用;默认权限下打开、导航、点击、输入、按键请求批准,完全访问按该模式直接执行。读取、截图、等待、滚动及标签管理不额外询问。
- 支持取消、操作超时、同标签串行执行;归档、删除会话、关闭最后一个普通窗口或退出应用会清理后台浏览器。
## 对话及图片链路
OMP 和备用 HTTP 均调用相同宿主工具。截图压缩为不超过 500 KiB 的 JPEG,适应 OMP 单帧 1 MiB 限制;截图作为消息图片保存,文本工具记录不内嵌完整图片数据。
支持图片输入的模型收到实际截图;未启用图片输入的模型仍可读取页面快照,会话仍显示截图,但不将图片送入模型请求。备用 HTTP 在该轮所有工具结果之后插入截图,维持工具调用与结果的配对顺序;后续轮次的兼容历史会按模型能力重放截图。
主要文件:`src/main/browser-automation.ts`、`src/main/agent/browser-dom.ts`、`src/main/agent/tools/browser.ts`;配套更新工具注册、主进程初始化、会话清理、OMP 结果、HTTP 循环、历史重放和模型图片能力传递。
## 已验证
- 完整 `npm run build` 通过,含沙盒原生组件、TypeScript、Electron 主进程及页面构建。日志:`.tmp/browser-automation-full-build.log`。此前经济模式移除记录中的沙盒编译错误为当时状态,本轮构建已无该阻塞。
- `npm run test:browser-automation` 通过 60 项。报告:`.tmp/browser-automation-3wMm3p/report.json`。使用真实隐藏 Chromium 与本机回环网页,覆盖真实可信点击、替换/清空/追加输入、键盘、异步等待、滚动、截图、导航旧 ref、权限拒绝/批准、作用域隔离、取消和清理。
- 同一测试验证备用 HTTP、实际安装 OMP、二者的无图片输入模型,以及构建后正式桌面应用的 preload/IPC → 对话服务 → OMP → 宿主浏览器 → 模型续接;截图与工具记录真实写入 `sessions.json`,删除会话清理浏览器。模型端为可控的本机回环服务,未请求用户付费 API。
- 检查 Responses、Anthropic、Gemini 的请求转换保留截图输入;不将请求转换验证等同于三个远程供应商实测。
- `npm run test:conversation`、`npm run test:omp`、`npm run test:conversation-ipc`、`npm run test:custom-model-native` 全部通过。
- `node scripts/browser-host-regression.cjs` 通过,原有手动浏览器导航、隔离、尺寸与生命周期回归通过。
- 已查看 `.tmp/browser-automation-3wMm3p/browser-actual.jpg`:实际隐藏页面截图,输入和页面变化可见。
- 本次文件的 `git diff --check` 通过。保留工作区其他修改,未提交代码。
## 使用及限制
重启 YCode 主进程后,在支持工具调用的模型、Craft 模式下直接要求打开 URL 并操作即可。不需要恢复已移除的浏览器设置。当前用户窗口尚未重启:其本地调试端口不可连接,无法核实未发送草稿,因此保留窗口和现有用户数据。
已实现主文档操作;跨域 iframe 遍历、任意脚本执行、外部浏览器接管、文件上传/下载暂不支持。验证码或登录可能需要用户协助,目前未提供把后台标签切为手动登录窗口的入口。浏览器内存会话在应用重启后消失,已存会话截图保留。右侧手动面板与自动化标签当前独立,尚未接入同屏展示。
@@ -0,0 +1,177 @@
# 对话资料沉淀与任务引用:源码核对(2026-10-17)
用户确认问题:对话如何沉淀到资料库,普通资料库与项目资料库如何区分,以及对话/任务是否自动以资料库为依据。
本轮使用reverse-engineer-anything技能复核既有REA捕获正文。没有上传、修改或创建原版资料;发现主窗口有运行中会话后仅做被动观察,没有导航或发送测试消息。以下业务结论均为固定源码观察,不是完整运行态或后端行为验收。
## 1. 证据
原版均为WorkBuddy **5.8.3**。主要归档:
- 资料库及共用已加载脚本:`ev_82a9ad8ec38c807e465646d749e2cedbb150fec62e30234c97acd0bb309c349e`,`.tmp/rea-four-modules-library-stable-20261017.json`。
- 主窗口脚本:`ev_2b8db4eb73c59a8d3d80c3e37db5ea9b5bf1d13390d5238d81ff2cc53d11dc09`,`.tmp/rea-todo18-main-scripts.json`。
- 项目详情及发送逻辑:`ev_ca0195278ba08b57f2d847e4d96e3a35490f26367b71b810563b01b03470d000`,`.tmp/rea-todo20-project-detail.json`;任务页补充 `ev_8782905c492acd1618e137480a3da8a8d7eba56e8f328e4856b008e1dd7f34ea`。
- 本轮只读主窗口:`ev_87e1797f654b717af1b300cbf1c971ade495e48b5a43e1f254903defd6e0b23c`,`.tmp/rea-library-task-link-baseline-20261017.json` / `.png`。只证明当前画面,不用于证明自动检索或上传行为。
页面捕获有policy_filtered范围,未归属脚本/资源被排除。下面以实际包含的模块正文为边界;缺失工具/后端行为不能按负面搜索当成全产品不存在。
## 2. 三类资料不能混为同一保存动作
| 类型 | 当前证据确认 | 不能据此推出 |
| --- | --- | --- |
| 本地产物 | 成功write/edit/present_files/imagegen等工具事件被投影为任务产物;有内部artifact-index路径 | 云端入库、整段对话被保存、所有项目源文件都成为资料 |
| 普通资料库 | 个人/团队文档空间;由fetchSpaces取得,选择器按个人/团队展示 | 对任意账号都可见、默认全库全文进入每轮模型请求 |
| 项目资料范围 | lockProjectId时取得该项目的space.id并锁定树;另有项目NetDrive资产选择/上传路径 | 两个入口必然共用或必然分离后台存储、可跨项目访问 |
项目资料库首先体现**项目归属和选择范围**。`space`文档系统和项目NetDrive在客户端有不同路径;不能只靠名称合并成同一后端,也不能反推成两套数据库。
## 3. 对话产物如何保存
### 自动归集为本地产物
源`contexts`中的artifact规则与分支解析,以及`artifact-DiRw8UKk.js:isHiddenArtifactPath`,表明本地产物索引和云文档写入是两条链路。索引不是把聊天原文上传到资料库。
### 产物工具栏的显式保存
`selection-quote-conversation-input-eBPyMR25.js:resolvePersonalUploadTargets`(字符偏移3799附近,SHA-256 `80904295f5a938dbfacf8e2e5bceab978eaf9d1d3a467c4e8d4076f639bee271`)按能力开关、canSave、是否项目会话和服务可用性决定入口:
- `libraryUpload`路径要求非项目会话;旧NetDrive个人库路径也检查非项目会话。
- `projectAsset`路径要求项目会话、可保存及项目NetDrive能力可用。
- 这是该工具栏的入口策略,不代表用户通过其他产品入口永远不能跨空间保存。
`detail-panel-wrapper-BZPa078h.js:uploadArtifactToPersonalLibrary`(偏移33344,SHA-256 `004a480f8014fd9eb68fe3df85f0557480b60765514ff0f6ee9fd55821b4049c`)读取产物、准备上传、调用backend.uploadToKnowledge。已有节点/内容hash可复用,变更时可能按nodeBlockId更新,并处理容量/格式等失败。
同模块`project-asset`分支(偏移61647附近)校验项目角色、解析url/sandbox/conversation中的projectId,初始化该项目NetDrive,然后打开上传选择流程;不是自动把产物上传任意项目。
### 模型工具写库
`src-DbE1_mgM.js:convertLibraryUpload`(偏移1049061,SHA-256 `6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4`)及`LibraryUploadView`证明存在LibraryUpload工具和结果/错误展示。
因此写库不只可能由人工按钮完成,也可能通过模型工具;但何时由模型调用、工具完整参数与个人/团队/项目目标选择仍缺后端/受控运行证据。不能说“每次对话自动写库”,也不能说“只能手动按钮写库”。
`local-docs-facade`的autoSaveContext/saveContext是本地Office编辑上下文保存,不等于聊天入库或资料库上传。
## 4. 普通资料与项目资料如何被引用
`space-library-picker-iy0rD3_T.js`(SHA-256 `648cf973cd4f7b2b7ae8cd7a0db26755d8671583ead81a62bcfa185491901ea3`):
- 普通模式fetchSpaces请求`/space/api/view/query/spaces`,分个人和团队;查询搜索由选择器关键词触发。
- lockProjectId模式调用`fetchProjectSpace(projectId)`→`POST /space/api/view/query/projectspace`,从space.value取space.id,再按该ID读取records。隐藏普通tab/搜索,只构造当前项目树。
`space-library-DZ_aI7LI.js:buildSpacePickerBlocks`(偏移4608;SHA-256 `91bbbd1b9b1602088e8e19d495f29b44da25d8df20028c663163baa8db3d5cbd`)在用户选中资料后生成隐藏的`skill://library`引用及space-doc块,保留docId、URL、kind、source等元数据。发送转换会把所选URL带入本轮输入。
这证明**显式选择资料→引用与所需技能→本轮发送**的客户端通路;尚未证明library技能实际读取过程、全部检索工具、内容解析时机及是否全量读取目录。
## 5. 对话如何使用项目任务/待办
`use-project-local-chat-config-DqSrAK4B.js`(SHA-256 `e6e153194a543b5cdc52c5cac1f96727571aa2ca9b2ab78bf6c79bc35d05f636`)的useLocalTaskAggregatedProvider同时提供待办计划、项目NetDrive资料和本地文件候选;todo候选默认过滤已完成项。
选中待办后,`create-local-todo-chip-CS01etNd.js:createLocalTodoChip`(SHA-256 `0496cea90c780a2480ef063f79dd8555929295aada161d0c38ba63a920d48d5a`)产生todo://id引用,包含projectId、状态、标题/描述/摘要、snapshotSessionId和tags。这是对待办的引用,不是把其他所有会话都并入上下文。
项目详情已捕获的teams/cloud发送链路中:
- extractTodoContext识别所选todo,构造todo-reference。
- collectTodoNetdriveAttachments按projectId+todoId读取待办,把其明确关联的netdisk附件加入tdrive-attachments。
- 因而有“选中待办后自动补关联附件”的逻辑,不等于检索整个项目资料库。
本批证据未观察到独立task://chip,不能据此断言全产品不存在。用户说的“任务”须区分项目待办引用、实际对话任务和定时任务执行。
## 6. 自动上下文的准确边界
在`project-detail-page-DdYKdEkr.js:resolveAndSendPrompt`(SHA-256 `a5630ae6ad4e053e8d085a569a67c102c4226a14c7f27a3e62e69601d0f8d2c1`)的**teams/cloud**发送链路,原版在可解析根信息时加入:
```xml
<tdrive-root id="…" dir_name="…" scope="project" />
```
还包含项目指令、连接器状态、所选todo/附件、工作总结和当前时间等,固定块有首轮/变更去重。根指针不包含目录全文或所有文件正文。
同链路默认可走backend.resolveMessage,解析显式blocks和项目上下文,失败/超时可回退本地拼装;后端算法与实际检索范围未知。
**不能把上述结论外推到所有本地项目会话。** local的createConversation→adapter.prompt已确认携带projectId、promptContentBlocks、projectResources;后者只有connectorSkills/mcpServers/projectSkills,没有资料列表。本地adapter/内核是否另注入根或自主检索仍待证。
因此当前可确认的层级是:
1. 按项目或个人/团队范围选择资料:已证。
2. 显式引用资料/待办,并在部分发送链路补关联上下文:已证。
3. teams/cloud路径默认携带可解析的项目根指针:已证。
4. 模型每次任务都自动list/search/read全库,或全库正文自动注入:未证。
5. 原版定时任务默认自动检索资料库:未证,需另核对持久化引用与触发执行链路。
自动化列表中的dataSource会参与local/cloud来源与开关能力判断,不能据该字段名断言“阅读资料源”。中文资源另有外部数据源同步到协作项目的文案,也不能仅凭词条证明当前定时执行会读取资料库。
## 7. YCode当前与开发计划修订
- 当前YCode自动组装能力快照、非空项目指令、个性化/记忆;显式quotes/attachments和工作目录路径随消息进入请求。没有资料库读写/检索服务,也没有项目资产模型。
- 普通文件工具可以读取工作目录,这不代表资料库自动接入;项目指令注入也不代表资产全文进入上下文。定时任务尚未实现。
- 三模块开发必须包括对话入口:本地产物归集;个人/项目目标明确的入库动作;保留类型与来源的资料/todo引用;按作用域解析和实际读取记录。
- 不把资料库引用全部降成普通路径或一段拼接文本,也不能仅显示引用chip而模型请求没有对应资源。
- 先前计划中的内容快照是YCode本地适配建议,**不是已证原版全文冻结行为**。现在应先保留原版引用身份、URL/ID、类型和元数据;云资料何时解析/读哪一版仍待证。本地可增加快照保障历史重建,但必须单列为适配决定。
- 项目资料范围应显式绑定projectId;不因有projectId就静默注入个人/其他项目全部资料。
本轮只新增确认记录、更新开发计划链接与引用语义说明;没有修改业务功能。尚未完成上传/引用/模型检索的受控运行验收。
## 8. 追加确认:何时拆解任务、何时沉淀资料
### 8.1 对话内计划与项目事项分别看待
已捕获的`adapters-DF2aWDY_.js`(SHA-256 `c4529f43704680c595661fc20578ee813cac20912a9f2324e9b03c33e04ea0c4`,位于项目详情REA归档)中:
- `PlanToolAdapter`识别plan_create/plan_update等实际工具数据。
- `syncTodoStatusFromMessage`在消息中出现todo_write/TodoWrite工具调用后,提取结果并更新对话的todo状态;merge为false时重置当前列表。
- `handlePlanSessionUpdate`在收到ACP plan事件时同步计划条目。
另一路不能遗漏:`contexts-DgzzFjnf.js`(SHA-256 `8d0c375f141cf724c7f77233ee1d54dd21c9cc86286b79d119711f34a6236746`)的`createTaskPlanCard` / `projectTaskTimelineMessages`可根据已存在的权威taskList合成todo_write显示卡。它是展示投影,不证明本轮模型真实调用过TodoWrite,更不证明列表由前端自动拆出。
这些是**事件到达之后的展示/状态同步**,不证明renderer会识别用户任务复杂度并主动要求拆解。现有证据没有统一的“超过几步/多少字/多少文件就拆”阈值,也没有证明每次发消息、回复结束或任务完成时都会创建计划。
对话出现计划卡片或TodoWrite结果,不能直接当作项目数据库里的待办已创建;项目事项的持久化必须另有项目创建命令或已证交接链路。
### 8.2 项目计划页还有尚未采到的远程业务
`plan-host-CQg0zimX.js:ProjectPlanFederationHost`(SHA-256 `501ea6feabfa9458df2585f636b3b765e70701546e3ebd71a60f032778b37a16`)挂载远程project-plan模块,并向它提供`chat.addTodosToChat`、`quoteCommentToChat`等接口。这里可确认选择事项后引用到对话,不能从host反推远程计划模块的“AI拆解/批量创建”逻辑。
计划页新建待办的完整点击处理、AI拆解的入口/条件、后台批量创建和模型系统提示词均未完整取得。因此“复杂任务自动生成项目待办”的规则仍待证,不用通用AI习惯补成原版结论。
### 8.3 已证实的交接触发:创建事项并上传关联资料
`task-transfer-popover-CuSX2nzk.js`(SHA-256 `9b701f16edd2645655b85ca9d278f50958e7051e54351faed7ae84e61b4bec71`,同项目详情REA归档):
1. 交接表单提交路径调用`createLocalTaskTodo`→`planCommands.createTodo(projectId, {...source:'handoff'})`,包含标题、摘要、负责人、时间、标签和状态等。
2. 存在表单携带的附件、本地文件或产物时,后台调用`finalizeLocalTaskAttachments`。
3. 该函数读取实际产物、`uploadTodoAttachmentsToProjectNetdisk`上传,再通过`addTodoAttachment`关联到刚创建的事项。
4. **事项创建成功提示先于后台附件上传完成。** 上传可能部分失败,源码有单独错误反馈。因此交接成功不等于资料已全部上传成功。
触发条件是明确的交接动作及其携带的数据,不是普通任务终态自动发生。代码中待办创建与上传关联可串联,尚未完整验收其服务端/权限和远程UI流程。
### 8.4 模板给出了业务阶段提示,不是自动落库定时器
模板依据为REA `ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9`,由`recover-project-templates.cjs`校验正文hash后静态恢复为`project-templates.json`。
已确认的示例:
- 产品需求模板:帮助拆解研发任务、子任务与依赖;PRD完成后提醒上传项目资料库并创建评审事项;评审结论可沉淀,待办问题拆成事项。
- 调研模板:阶段完成后主动提醒上传资料、分析表和报告;重要洞察创建评审事项。
- 交付模板:拆里程碑、负责人、期限与依赖;验收后上传结论和交付文档,将遗留问题创建后续事项。
- 知识库模板:引导将经验/会议结论/SOP/FAQ整理成结构化文档,提醒沉淀与维护。
关键词是“帮助”“提醒用户”“可以创建/上传”。当前代码将这些文字作为项目instructions提供给模型,没有证据证明客户端解析这些中文句子后自动创建事项或上传文件。它们说明模型应在哪些业务阶段建议拆解/沉淀,不能替代真实工具调用或用户提交的执行证据。
### 8.5 触发矩阵
| 时机/动作 | 拆解/事项 | 资料沉淀 | 证据层级 |
| --- | --- | --- | --- |
| 用户仅发送一条需求 | 可能引导模型规划,是否调用计划工具未证 | 无已证整段聊天自动入库 | agent决策规则待证 |
| agent发出todo/plan调用、plan事件或取得已有权威taskList | 同步/投影对话计划与状态,卡片可能为合成展示 | 不因该事件自动入库 | adapter及task timeline静态已证 |
| 成功写/编辑/展示产物 | 不自动等同创建项目待办 | 自动归集本地产物,不等同云库上传 | artifact规则静态已证 |
| 产物工具栏保存/上传 | 不必创建待办 | 按个人/项目目标执行上传 | 保存分支静态已证 |
| agent调用LibraryUpload | 不必拆任务 | 执行一次工具入库,目标和实际成功另验 | 工具展示/转换已证,后端决策待证 |
| 用户确认任务交接且携带产物/附件 | 创建项目handoff待办 | 后台上传并关联附件,可能部分失败 | 交接客户端静态已证 |
| 达到模板描述的阶段 | 提示模型帮助拆解/提醒创建 | 提示模型提醒上传/沉淀 | 提示词已证,实际决策待证 |
| 会话/所有待办完成 | 未证自动新建事项或再拆解 | 未证统一自动上传hook | 不标已实现或原版规则 |
### 8.6 YCode开发约束
YCode当前`executeConversationTool(todo_write/create_plan)`根据工具参数构造PlanContent作为工具结果;生产链路主要保存tool_call/tool_result,由renderer从工具参数/结果生成计划卡,没有独立项目待办服务。ChatEvent虽支持plan part,当前未确认生产plan事件发射器;TaskContent也不能视为已有项目任务创建能力。资料库入库与项目交接仍未实现。开发时区分会话计划、项目事项和产物/资料三种对象,并保存可追溯关联。
不能自行加入“复杂就拆”“回复结束就上传”“全部完成就入库”等固定策略再称1:1。已证自动部分可先实现事件投影与状态同步;模型何时调用工具依赖完整工具/系统指令,远程计划业务需REA另取。交接动作应分别跟踪事项创建、上传中、关联完成、部分失败和恢复。
+350
View File
@@ -0,0 +1,350 @@
# YCode Windows 分发包 SOP
版本:2026-10-18
适用:Windows x64、Electron 37、NSIS、内置 OMP 18.6.3
最后一次验证产物:YCode v0.01(内部版本 0.0.1)
## 1. 目标与发布边界
本流程用于生成可分发的 Windows 安装程序和免安装目录包,并确保内置 OMP、沙盒、Git、模型元数据和第三方许可完整。
发布包不得包含:
- 当前用户的模型 Key 或其他凭据
- %APPDATA%\YCode 下的会话、项目、资料库或设置
- 用户个人专家包、备份目录或临时测试目录
- .tmp、release 中间文件、源码仓库元数据
个人模型、会话和专家保留在安装后的用户数据目录中,不随安装包分发。
## 2. 版本规则
YCode 的展示版本和 npm 内部版本分开管理:
| 项目 | 当前值 | 位置 |
| --- | --- | --- |
| 分发版本 | v0.01 | 文件名、发布记录 |
| 内部 SemVer | 0.0.1 | package.json、package-lock.json、Windows ProductVersion |
| 安装程序 | YCode-v0.01-Setup-x64.exe | package.json 的 build.win.artifactName |
发版前同时更新以下内容:
1. package.json 顶层 version。
2. package-lock.json 顶层和根包 version。
3. package.json 的 build.win.artifactName。
4. 发布说明中的文件名、大小和 SHA-256。
内部版本必须符合 SemVer。显示为 v0.01 时,内部版本对应 0.0.1,不能直接写成字符串 0.01。
## 3. 构建环境
最低要求:
- Windows 10/11 x64
- Node.js 24.x 和 npm
- PowerShell 5.1 或 PowerShell 7
- 本仓库依赖已安装
- 至少 2 GiB 可用磁盘;推荐 4 GiB
- 构建期间不要同时运行多个 electron-builder
- 不要让 win-unpacked 中的测试客户端保持运行,避免文件锁
进入仓库根目录并检查工具:
~~~powershell
Set-Location 'C:\Users\Administrator\code\YCode'
node --version
npm --version
Get-Command node,npm
~~~
干净环境和 package-lock.json 变更后使用:
~~~powershell
npm ci
~~~
仅日常沿用现有 node_modules 时,可先执行:
~~~powershell
npm ls --depth=0
~~~
## 4. 发版前检查
### 4.1 源码和变更范围
确认工作区中的改动都属于本次发布;当前项目允许保留未提交修改,但发布记录必须注明构建来源。
~~~powershell
git status --short
git diff --stat
~~~
不要覆盖或回退其他并行任务的修改。
### 4.2 静态检查
~~~powershell
npm run typecheck
~~~
### 4.3 OMP 资源与许可
内置资源位于 resources\omp,必须包含:
- omp.exe
- LICENSE
- THIRD-PARTY-NOTICES.txt
- runtime.lock.json
校验固定版本和摘要:
~~~powershell
npm run omp:verify
npm run test:omp:bundled
~~~
通过标准:
- runtime.lock.json 中的平台为 win32-x64
- OMP 版本为锁定版本,本批为 18.6.3
- omp.exe、LICENSE、THIRD-PARTY-NOTICES.txt 的大小和 SHA-256 全部匹配
- omp.exe --version 返回 omp/18.6.3
- 无 YCODE_OMP_PATH 时,detectOmpPath 指向 resources\omp\omp.exe
- 真实 RPC 启动能收到 type=ready
如果资源刚清空,先从固定上游恢复:
~~~powershell
node scripts/vendor-omp.mjs
~~~
如果本机已有经过校验的 Ygent 安装,可只导入二进制:
~~~powershell
node scripts/vendor-omp.mjs --from "$env:LOCALAPPDATA\Ygent"
node scripts/vendor-omp.mjs --verify
~~~
离线构建时,应先从上一个已验收安装包复制完整的 resources\omp 目录,再执行 --verify。不得绕过大小或 SHA-256 校验。
## 5. 下载镜像与缓存
当前环境访问 GitHub 可能超时。首次构建在干净网络下可直接运行;出现 ETIMEDOUT 时设置 electron-builder 镜像:
~~~powershell
$env:ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/'
$env:ELECTRON_BUILDER_BINARIES_MIRROR = 'https://npmmirror.com/mirrors/electron-builder-binaries/'
~~~
测试版没有证书时关闭证书自动发现:
~~~powershell
$env:CSC_IDENTITY_AUTO_DISCOVERY = 'false'
~~~
镜像只用于下载构建工具,不改变应用运行时地址。不要在源码或脚本中写死个人代理。
常见缓存位置:
~~~text
%LOCALAPPDATA%\electron\Cache
%LOCALAPPDATA%\electron-builder\Cache
~~~
electron-builder 的生产依赖收集可能需要数分钟。期间不要重复启动第二个打包进程或误判为卡死。
## 6. 构建安装程序
标准入口:
~~~powershell
npm run dist:installer
~~~
该命令依次执行:
1. omp:vendor:恢复并校验内置 OMP。
2. npm run build:
- 构建原生沙盒
- TypeScript node/web 检查
- electron-vite main/preload/renderer 构建
3. electron-builder --win nsis:
- 生成 release\win-unpacked
- 复制 OMP、模型元数据、沙盒和 Git extraResources
- 生成 NSIS 安装程序
仅需免安装目录时:
~~~powershell
npm run dist
~~~
构建成功必须同时满足:
- 命令退出码为 0
- release\win-unpacked\YCode.exe 存在
- release\YCode-v0.01-Setup-x64.exe 存在
- release\YCode-v0.01-Setup-x64.exe.blockmap 存在
- release\win-unpacked\resources\omp\omp.exe 存在
自动化执行器如果自身有 5 分钟超时,应使用独立后台进程并把 stdout、stderr 和退出码写入文件;不能用工具超时或终端断开判断构建失败。
## 7. 签名门槛
### 7.1 测试包
测试包可以不签名,但发布记录必须明确写出 NotSigned。CSC_IDENTITY_AUTO_DISCOVERY=false 只用于测试包。
### 7.2 正式发布包
正式对外包必须配置组织证书并签名。不要把证书路径和密码写入仓库、脚本或日志。构建完成后必须验证:
~~~powershell
Get-AuthenticodeSignature 'release\YCode-v0.01-Setup-x64.exe' |
Select-Object Status,StatusMessage,SignerCertificate
~~~
正式发布通过条件为 Status 为 Valid。
builder 日志出现 “signing with signtool.exe” 不等同于文件已签名;必须以 Authenticode 检查结果为准。
## 8. 产物验收
### 8.1 包内 OMP
~~~powershell
node scripts/verify-bundled-omp.cjs --packaged
~~~
该命令检查四个 OMP 文件、大小、SHA-256 和 --version。
### 8.2 归档内容
如果系统有 7z,可列出安装程序内容:
~~~powershell
7z l release\YCode-v0.01-Setup-x64.exe |
Select-String 'resources\\omp'
~~~
必须看到:
~~~text
resources\omp\omp.exe
resources\omp\LICENSE
resources\omp\THIRD-PARTY-NOTICES.txt
resources\omp\runtime.lock.json
~~~
### 8.3 实际客户端启动
至少执行一次隔离用户目录启动:
~~~powershell
$env:YCODE_USER_DATA_DIR = Join-Path $env:TEMP 'ycode-release-smoke'
New-Item -ItemType Directory -Force $env:YCODE_USER_DATA_DIR
& '.\release\win-unpacked\YCode.exe'
~~~
通过标准:
- 主窗口出现,composer 可用
- 设置中的版本为 0.0.1
- 模型、会话和专家为空,不读取开发用户数据
- 打开“关于”或调用生产 IPC,OMP 路径指向安装目录 resources\omp\omp.exe
- renderer 无未捕获异常
验证完成后关闭客户端,并清理临时用户目录。
### 8.4 哈希
~~~powershell
$installer = Get-Item 'release\YCode-v0.01-Setup-x64.exe'
$hash = (Get-FileHash $installer.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
$hash
"$hash $($installer.Name)" |
Set-Content -Encoding ascii 'release\SHA256SUMS.txt'
~~~
记录安装包大小、ProductVersion、Authenticode 状态和 SHA-256。
## 9. 交付物
至少交付:
- release\YCode-v0.01-Setup-x64.exe
- release\YCode-v0.01-Setup-x64.exe.blockmap
- release\SHA256SUMS.txt
- release\使用说明-v0.01.txt
- release\win-unpacked\ 仅作为免安装包;分发时必须保留整个目录
安装包不携带用户数据。卸载程序保留 %APPDATA%\YCode,这是当前产品行为。
## 10. 失败处理
### GitHub ETIMEDOUT
设置 ELECTRON_MIRROR 和 ELECTRON_BUILDER_BINARIES_MIRROR 后重试。已有 Electron ZIP 可先校验,不要手工删除缓存。
### OMP SHA 或大小不符
停止发布。重新运行 omp:vendor,或从已验收安装包复制完整的 resources\omp 后执行 omp:verify。不得直接使用任意版本 omp.exe。
### 安装程序只剩几百 KB
这通常是 NSIS 最后装配被中途终止。删除以下两个明确的半成品后重新运行 electron-builder:
~~~powershell
Remove-Item -LiteralPath 'release\YCode-v0.01-Setup-x64.exe' -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath 'release\YCode-v0.01-Setup-x64.__uninstaller.exe' -Force -ErrorAction SilentlyContinue
npm run dist:installer
~~~
不要递归删除 release 目录,也不要删除 release\win-unpacked 之外的构建缓存。
### 文件被占用
关闭正在运行的 release\win-unpacked\YCode.exe 和残留 electron-builder 进程后重试。不要结束正在发送消息的开发客户端。
### 构建通过但 OMP 不可用
检查:
~~~powershell
node scripts/verify-bundled-omp.cjs --packaged
Test-Path 'release\win-unpacked\resources\omp\omp.exe'
~~~
如果包内文件存在但仍选择外部 Ygent,检查 detectOmpPath 的优先级和 YCODE_OMP_PATH 是否被当前会话设置。
## 11. 发布记录模板
~~~text
发布日期:
发布版本:v0.01
内部版本:0.0.1
Git 分支/提交或工作区状态:
Node/Electron 版本:
OMP 版本与 SHA-256:
安装包大小:
安装包 SHA-256:
签名状态:
构建命令与日志:
OMP RPC ready:
包内 OMP 路径:
正式客户端启动结果:
renderer 异常数:
未覆盖项:
~~~
## 12. 当前已知限制
- 当前安装程序为测试包,NotSigned。
- 本批未执行干净 Windows 设备上的实际安装和卸载。
- 离屏启动验收使用软件渲染,未覆盖全部 GPU 驱动。
- 内置 OMP 仅覆盖 Windows x64。
- OMP 没有模型凭据;用户仍需在设置中配置模型。
- 包内首次启动不会自动安装本机五个自定义专家。
最后更新:2026-10-18
+78
View File
@@ -1,5 +1,83 @@
# YCode 接手状态(2026-10-15) # YCode 接手状态(2026-10-15)
> 2026-10-18 v0.01 Windows测试分发包:[构建与限制](分发包-v0.01-2026-10-18.md)。用户指定文件名版本v0.01,内部SemVer改0.0.1;NSIS按用户安装、可选目录、保留卸载数据,产物`release/YCode-v0.01-Setup-x64.exe`(140.3MiB),附说明和SHA256SUMS。完整dist:installer及实际包内容/正式exe隔离启动10项通过,0未捕获renderer异常,已查看截图;报告`.tmp/release-v001-M4MP5b/report.json`。未签名、未测干净设备安装卸载/GPU全路径;OMP仍外部探测,个人配置/专家不随包供应。保留其他工作区修改,未提交、未外部发布。
> 2026-10-18 沙盒 Git 一次性对齐:[完整实现与限制](沙盒Git完整接入-2026-10-18.md)、[功能验收矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。组件升级至 0.7.0 / protocol 1:内置 82 文件 MinGit,接入宿主普通仓库/linked worktree 历史和索引审批导入、本地常用 Git、项目+`.git` 原生组事务写回/恢复、HTTPS/SSH、DPAPI scoped 凭证、clone/fetch/pull/push/reconcile、维护 CLI 与生产 IPC。覆盖拒绝、并发、真实主进程死亡恢复、unknown push、TLS/host-key 校验、取消/策略撤销;本地29/29、安全25/25、远程40/40、生产 Electron/OMP 52/52、最终包内 Git20/20、旧兼容专项314项重新回归;`npm run build` 通过,总计480项。构建验证摘要见 `.tmp/git-complete-validation.json`,代码/日志保留在本工作区,未提交、未改用户真实仓库/远程或生产沙盒状态、未重启用户窗口。明确限制仍有标准用户/安装发布/独立安全审查、Git 大仓库/部分高级特性和原版 Git 运行时 1:1 证据。
> 2026-10-18 专家详情错位与头像修复:[详情](通用开发角色替换-2026-10-18.md#详情布局与头像修复)。修复avatar与fallback误用同一外容器导致590px头像挤掉标题的问题,本地/参考详情共用固定72px头像层;角色指令改Markdown排版、正文独立滚动,并补加载态及键盘焦点覆盖。五个角色新增代码原生统一机器人SVG/256px PNG,已备份后部署真实用户包2.1.0;隔离幂等/真实冷读取及正式Electron界面17项通过、0 renderer错误,已查看浅/深/窄屏和召唤截图。完整构建通过;另仅改两条既有Git诊断文案,避开electron-vite错误的import正则。报告`.tmp/general-agents-validation-ttZ4Gw/detail-ui-report.json`。未重启当前用户窗口,布局需重新加载;未请求模型API、未新采原版,保留并行修改,未提交。
> 2026-10-18 专业角色改为通用职责:[配置、备份及验收](通用开发角色替换-2026-10-18.md)。按用户要求,将已安装四位原专业专家改为代码探索员、实现工程师、测试验收员、代码审查员,并新增问题诊断员;双语元数据、快捷提问、实际指令及默认技能同步更新,启动专项技能为0。保留旧内部ID/Agent文件名,完整原包和SHA清单备份在用户数据capabilities/expert-backups/general-development-070c2a39-3f03-4576-bda4-82e54e242af2。定义resources/agents/general-development.json,维护脚本scripts/configure-general-agents.mjs;隔离替换/幂等、真实用户冷读取和实际Electron界面9项通过,0 renderer错误,已查看截图。当前是可手动召唤角色,未新增调度器自动角色绑定或工具权限;未调用模型API、未重启用户窗口。用户重新进入“我的专家”刷新配置。委派通道仍因封存任务不可读而失败,主上下文完成验证;保留并行修改,未提交。
> 2026-10-18 成员入口移到输入框上方:[迁移与验证](输入框上方成员入口迁移-2026-10-18.md)。按用户位置订正,将生成态/完成态成员入口统一移到输入框之前,移除回复底部重复入口;当前回合优先,新回合无成员时不显示旧团队,切换/回退清理。保留展开、实时详情和单独停止。投影19、真实renderer17、真实Electron/OMP/回环模型专项及对话回归、类型检查和完整构建通过;0 renderer错误,已查看浅/深色/窄屏截图。报告 .tmp/subagent-display-rFYnKp/report.json、.tmp/subagent-app-ErrF75/report.json。构建另修复沙盒Git错误文案触发electron-vite静态import正则误匹配的问题,仅调整文案。横向团队栏完整视觉、后台命令状态源和原版同场景像素仍待补;未重启用户窗口或调用付费API,保留并行修改,未提交。
> 2026-10-18 输入框上方任务入口订正:[REA核对与待补项](WorkBuddy输入框上方任务区域核对-2026-10-18.md)。同一份5.8.2保留证据确认,MainContentCore输入框上方同时有团队成员状态栏和后台任务胶囊/列表,数据来源独立;另一套ConversationInputArea后台列表只接终端,显式排除Agent事件。上一批仅覆盖回复底部成员入口及右侧详情,上方两个入口尚未实现。已订正原调查和实施范围,未改产品代码;当前激活路径、全状态像素/动效未运行复核。
> 2026-10-18 子任务展示对齐:[实现、原版资源及验收](子任务展示对齐-2026-10-18.md)。改为回复底部头像组/成员列表,点击成员打开右侧任务详情,按会话隔离、实时更新并保留单独停止;回退清理过期选择,普通Task独立保留。使用既有5.8.2 REA证据,恢复53条原版CSS及7个SVG,复用对话主题与消息/工具渲染。核心29、投影/状态15、真实UI14、真实Electron/OMP/回环模型30项与对话/模型信息回归、完整构建通过;最终报告 .tmp/subagent-display-pt4PXH/report.json、.tmp/subagent-app-qt49gP/report.json,0 renderer错误,已查看浅/深色/窄窗口截图。原版9223不可达,未新增同场景像素/动效验收;未请求用户付费API或重启用户窗口,保留并行修改,未提交。
> 2026-10-18 长期沙盒工作区:[实现、使用与恢复边界](沙盒长期工作区接入-2026-10-18.md)。组件 0.6.0 / protocol 1,显式 persistent 命令跨命令保留同一物理草稿与会话私有 Git,支持三方同步/冲突、查看/审批提交/归档丢弃/恢复;宿主专用原生 ACL 封存和不可变检查点保护保留内容。项目/会话/根身份/策略绑定,真实主进程死亡恢复及宿主已提交但草稿元数据失败已测;生产 main/preload 只读状态由保存会话推导根,busy 不等待审批队列。长期专项51、真实生产Electron/OMP42及兼容回归共11组356项、完整构建通过,组件源码/二进制摘要一致,汇总 `.tmp/sandbox-persistent-validation.json`。普通文件工具仍作用宿主;进程/home/temp/稳定绝对路径、可写依赖、宿主Git元数据/远程Git、联网/MCP/终端/安全中心和完整原版等价继续未完成。本批未构建安装包,未改生产状态或重启用户窗口,保留并行修改。
> 2026-10-18 子智能体并发上限调整:按用户要求从3个增加至5个,全进程共用,第6个排队;每回合最多创建16个的限制不变。调度器与模型工具说明已同步,更新后的核心29项及完整构建通过,构建日志 .tmp/subagent-limit-5-build.log。下方初版应用验收使用原3并发配置,本次未重跑应用专项;用户窗口未重启,重启主进程后加载新上限。
> 2026-10-18 子智能体调度接入:[开关、权限、界面与验收](子智能体调度接入-2026-10-18.md)。团队管理现为实际总开关:开启允许主模型分工,关闭拒绝新调度并取消成员,主对话继续。OMP/HTTP共用本地调度,独立上下文、全进程并发3、禁止嵌套、指定文件写入与父对话审批、收取结果后整合;流式卡片可展开/停止,分别统计已报告用量,回退包含子任务写入。核心29项、真实应用27项(0 renderer错误)、设置11项、上下文通道7项及相关回归和完整构建通过,已查看浅/深色截图。最终应用报告 .tmp/subagent-app-3IxrIT/report.json,浏览器同时60项通过 .tmp/browser-automation-KOnj4K/report.json。观测器计数已改独立原子文件通道,避免共用stdout的大帧竞争。测试使用回环模型服务及实际安装OMP,不是用户付费API或原版1:1验收。当前用户窗口未重启,重启主进程后生效;保留并行修改,未提交。
> 2026-10-18 对话无头浏览器已接入:[实现、验收与限制](对话无头浏览器接入-2026-10-18.md)。原先仅有右侧手动网页容器,现新增会话隔离的后台 Chromium `browser` 工具,支持打开/读取/点击/输入/按键/滚动/等待/截图及标签管理;OMP、备用 HTTP、图片能力与历史重放均接通。真实离屏浏览器与正式生产 IPC 专项60项、对话/OMP/IPC/多协议及旧手动浏览器回归、完整构建通过,已查看实际截图。测试模型为本机回环服务,真实安装 OMP;未测用户付费 API。默认权限接现有审批,规划模式禁用,设置开关保持移除。当前用户窗口未重启,新主进程重启后生效;跨域iframe、外部登录态与同屏展示尚未接通,非原版1:1验收。保留并行修改,未提交代码。
> 2026-10-18 原生写回事务:[实现、恢复与边界](沙盒原生写回事务-2026-10-18.md)。组件 0.5.0 / protocol 1,默认文件工具、命令写回、补偿及恢复使用本地 NTFS 原生相对目录句柄;v3 记录根身份、隐式父目录和独立操作日志,审批期间同内容换根也拒绝。文件替换为禁止覆盖的两步交接,存在短暂缺名窗口;不是单次原子交换。原生专项32、目录13、基础52、策略38、Git43、内置分发20、读取桥21、真实生产Electron/OMP31项(共250)与完整构建通过,组件源码/二进制摘要复核一致。长期工作区、完整文件 shim、元数据/断电耐久性/标准用户等继续未完成。未改生产状态或重启用户窗口,保留并行修改。
> 2026-10-18 智能体设置范围调整:[用途核对与移除记录](经济模式用途核对-2026-10-18.md)。按用户要求移除经济模式和浏览器操控,页面仅保留团队管理,清理对应控件和样式导入;现有 OMP 压缩不受影响。TypeScript、Electron 页面构建、真实隔离浅色/深色及持久化 11 项通过,已查看截图。完整构建被并行沙盒 `runner.cpp:392` C4244 编译错误阻断,未修改该模块、未重启用户窗口。YCode 对话未内置无头浏览器工具;当前默认用户目录也未配置 MCP。右侧手动网页浏览器不等于模型已能操控页面。
> 2026-10-18 Git 改为默认内置:[供应、打包与验证](沙盒内置Git分发-2026-10-18.md)。组件 0.4.0 / protocol 1,固定官方 MinGit 2.56.0.windows.2,裁剪后 46 文件/约7.2MB,保留许可证与源码/构建来源;归档和确定性运行时清单双摘要锁定。新用户数据无需注册 Git,未扫描宿主 PATH;高级导入覆盖保留,损坏覆盖或内置资源均拒绝执行。完整构建/类型检查、实际目录包构建通过,包内资源/离线复现及供应目录保护21、Git工作流43、真实生产无Git注册31、Node/Python工具链24、策略38项通过。使用包内资源验证真实 AppContainer,目录包为本地未签名验证,不替代 NSIS、标准用户、公开发布许可/源码交付等 A16 验收。未改生产状态、未重启用户窗口,保留并行修改。
> 2026-10-18 总上下文语义订正:[实现、公式与验收](上下文预算与压缩阈值-2026-10-18.md)。第一项改“上下文”总窗口,输入动态扣除输出;500K/128K为500,000总窗口、372,000输入,撤销上一轮628,000误算。原OMP默认按15%/16,384预留,现另覆盖完整输出预算和模型输入上限;500K/128K压缩阈值372,000,生成独立profile配置并由安装OMP读取确认。旧配置原第一项迁移为总窗口,版本2保存/冷加载防止重复扣除。完整构建、预算13项、原生14项、能力22项、OMP/持久化/用量回归及真实隔离应用104项(0 renderer错误)通过,已查看界面。未验真实500K跨阈值压缩产物,备用HTTP无完整自动压缩;未改用户模型数据或重启窗口,保留无关修改。
> 2026-10-18 Git 沙盒接入:[实现、维护与限制](沙盒Git接入-2026-10-18.md)。组件 0.3.0 / protocol 1,显式 Git x64 PE/DLL 导入与摘要校验、只读运行时、跨命令保存的沙盒专用仓库已接通;支持本地状态/差异/暂存/提交/分支,审批绑定文件与候选仓库身份,策略变化阻止旧历史加载。初始基线为过滤快照并保留原始字节,宿主历史/索引/凭证/hook 不导入,宿主 `.git` 不写回;远程 Git 仍禁用。Git43、原生52、策略38、工具链24、进程20、真实生产 Electron/OMP30项通过,完整构建/类型检查通过;维护 CLI 的含空格/中文/`^` 参数问题已修复并实测。宿主仓库安全导入/元数据事务、长期工作区、自动清理和原版等价继续未完成。未修改生产沙盒状态、未重启用户窗口,保留并行修改。
> 2026-10-18 500K/128K 预算适配:[实现与验证](自定义模型与多协议接入-2026-10-18.md)。输入新增500,000、输出新增128,000的快捷项;保留原始目录上限/完整价格档,手动大输入同步扩展隐藏总上下文,GPT-6.1 Sol组合为628,000。前后端共用预算解析、越界保存保护及缩小预算后恢复低价档;实际OMP profile、Responses输出预算、上下文统计已验证同步。完整构建、元数据12项、模型注册/冷加载、上下文回归、原生HTTP14项及真实隔离应用99项(0 renderer错误)通过,已查看新增快捷项截图。未调用实际收费服务、未重启用户窗口;本轮子agent因后端封存任务不可读取而失败,主上下文完成审查和验收,保留并行修改。
> 2026-10-18 OpenAI 协商优先级调整:[模型接入记录](自定义模型与多协议接入-2026-10-18.md)。新建与重新测试默认 Responses 优先,服务不支持时回退 Chat,显式完整 Chat 地址保持优先;重测保留已协商自定义根路径,普通对话沿用保存或新验证的路由,旧协议配置继续兼容。完整构建、模型注册专项、多协议14项及真实隔离应用78项(0 renderer错误)通过;未重启用户窗口,加载最新代码后新建/重新测试生效。
> 2026-10-18 模型设置界面续调整:[当前行为与验证](自定义模型与多协议接入-2026-10-18.md)。配置自定义模型统一进入列表,元数据更新移到关于;移除免密开关与提示,支持空 Key 和已有密钥保留/清空;模型改为同框搜索、单选和手填,下拉与输入完整对齐。移除匹配/参考价格文案及“总上下文窗口”输入,后台自动匹配和限制继续生效。最终构建、真实隔离应用78项(0 renderer错误)、关于布局21项、设置生命周期26项通过,已查看浅色表单、深色列表及关于截图。本次未强制重启用户当前窗口,重新启动加载最新构建;未采集原版或新增完整1:1结论,保留沙盒等并行修改。
> 2026-10-18 沙盒只读桥与运行时续接:[实现与验证](沙盒只读文件桥接入-2026-10-18.md)、[Node/Python 兼容修复](沙盒Node子进程与临时目录修复-2026-10-18.md)。新增原生 NTFS 句柄读取、按 Job 绑定的同步/异步 Node 桥、不可变分块、受限队列/截止、先落盘后回复及真实 main/preload 查询;另补私有 TEMP、显式 Node 同步进程捕获与 Python 0o700 私有目录适配。组件 0.2.0,二进制/三个适配器均校验;读取21、协议15、进程20、原生52、策略38、工具链24、旧stdio10和真实生产整应用26项通过,完整构建/类型检查通过。宿主写入句柄代理、逐后代认证、未适配 Node 管道/异步子进程、Git/长期工作区、受控出站、MCP/ConPTY、安全中心及原版等价继续未完成。未重启用户窗口、未修改生产策略/运行时,保留并行修改。
> 2026-10-18 自定义模型与多协议接入:[实现与验证](自定义模型与多协议接入-2026-10-18.md)。供应商仅自定义,支持域名自动协商 OpenAI Chat/Responses、Anthropic、Gemini,列表获取/单选/手填、原生鉴权和正式对话已接通。保存 models.dev 最新快照(232供应商/9041模型),自动能力、思考、上下文与低价档默认值;保留手动覆盖、处理过期响应/旧缓存/路由覆盖。最终完整构建、原生与备用协议、模型档案、对话/持久化/用量回归通过;真实隔离生产应用72项、模型能力22项和设置性能26项通过。免密Gemini因OMP占位Key改走原生HTTP且已验证;未调用用户实际收费API、未重启用户窗口,main/preload需完整重启生效;不新增WorkBuddy全状态1:1结论。
> 2026-10-18 沙盒完整接入首批:[实现与验证](沙盒完整接入首批实施-2026-10-18.md)、[验收矩阵](沙盒接入验收矩阵-2026-10-18.md)。已接入版本化策略/CAS/内容指纹、审批目标与归属绑定、撤销交互、执行队列、真实能力/自检/结果查询,受控 Node/npm、Python/pip、工作区只读依赖层,以及 v2 目录事务和恢复。策略38项、工具链/依赖24项、目录13项、stdio有限回显10项、基础原生52项和最终生产整应用23项通过,完整构建通过。命名管道原型未连通;替代stdio仅回显,真实操作 broker、Git/长期工作区、受控联网、MCP/ConPTY、安全中心和原版等价等保持未完成。未重启用户应用或修改其生产沙盒状态,保留并行修改。
> 2026-10-18 设置性能修复:[实现与验证](设置打开性能修复-2026-10-18.md)。设置独立入口、按需子页、关闭隐藏复用/卸载临时表单、重开刷新和生命周期竞态已修复,普通设置入口默认“通用”,显式添加模型仍进模型页。初始JS约637KB、共享CSS约230KB;生产通用内容首开191ms、重开17~21ms,独立开发模式277ms/35~44ms。生产/开发生命周期各26项、设置各专项与引用附件回归通过,18组修改前后样式快照零差异;完整构建/类型检查通过。未强制重启用户窗口,含main/preload,需完整重启生效。
> 2026-10-18 设置打开延迟:[实测与原因](设置打开延迟诊断-2026-10-18.md)。隔离真实 Electron 两轮11次新窗口打开,内容首次绘制327~708ms,平均474ms;窗口复用show约1ms。每次关闭销毁、设置共用主界面5.02MB JS/3.42MB CSS是主要启动成本,首屏配置/模型IPC单项约0.01~1.17ms。用户窗口精确1秒未直接复现;本轮仅诊断、未改生产代码或重启用户窗口。后续建议独立设置入口/子页按需加载、窗口复用与首次窗口壳。
> 2026-10-18 表格与附件修复:[实施与验证](表格与附件样式修复-2026-10-18.md)。表格改为框内“会话表格”短标签,文件/图片/文件夹/技能共用原图标和样式;混排、发送/历史/编辑、逐项状态和预览已接入,布局独立于真实载荷并落盘恢复;补附件预览深色背景及编辑资源归属。最终完整构建、保存、IPC、消息状态回归及真实隔离应用44项通过、0 renderer错误;总对话回归仍有沙箱默认命令旧断言失败,未改动该并行实现。原版9223仍不可达,全状态像素/动画及部分编辑/二进制预览状态未验收。用户当前窗口未强制重启,此批含main/preload,保存草稿后完整重启生效。
> 2026-10-18 沙盒完整接入计划:[阶段、架构与验收](沙盒完整接入计划-2026-10-18.md)。在已验首版上按 P0~P8 推进:证据/机制原型、统一策略授权、工具链与依赖缓存、文件/目录代理和安全删除、联网/程序/注册表、本地 MCP/内核/终端、安全中心、维护发布与原版同场景验收。工程接入、边界验证、原版等价分别记录完成状态;首批从高风险机制可行性和 shared 契约开始。本轮仅制定文档计划,未实施新功能、未运行构建或应用测试、未新增原版取证;保留既有并行修改。
> 2026-10-18 自有沙盒首版:[实施与维护基线](自有沙盒首版实施-2026-10-18.md)。新增本仓库可构建的 Windows AppContainer/Job 执行器,默认命令接工作区副本、网络禁用、真实审批后事务写回;敏感路径/链接检查、独立备份、审计、自检、恢复和遗留租约清理均有源码与维护入口。原生 52 项与真实生产 Electron/OMP/localhost 审批/拒绝/取消 10 项通过,源码编译与生产构建通过。保留并行上下文用量/引用 UI 修改,未重启用户窗口。当前仅隔离模型命令入口;工具链/依赖缓存、MCP/用户终端进程隔离、目录元数据、安全中心 UI、原版同场景 1:1 和独立安全审查仍待完成,不能将首版通路成功标记为完整复刻。
> 2026-10-18 表格与附件核对:[差异与修复范围](表格与附件样式核对-2026-10-18.md)。用户截图及保留 REA 宿主正文证实,表格实际为 displayAsPhrase 的“会话表格”短标签,先前错误接入输入外 QuoteReferenceCard;文件/图片/资源/技能和发送后的标签也未统一对齐。隔离真实 YCode 五组状态复现、0 renderer 错误;本轮只记录和重新打开 TODO02 相应验收,未修改业务代码。原版已知9223端口当前不可达,未知交互帧与全状态像素仍待采集,不能复用旧48项回归结论宣称1:1。
> 2026-10-18 用量与分类修复完成:[实施与验收](上下文用量与分类修复-2026-10-18.md)。新增真实 provider 请求前分类观测、旧原生分支用量恢复、可操作刷新、来源/时间/估计标识及失效校验。最新无 usage 时不复用更早总量,重载、换模型和分支恢复保留身份;缺分母仍展示已知 tokens,元数据缓存不改变任务顺序。最终生产构建与纯回归通过,实际隔离 Electron/OMP/localhost 上游 60 项通过、0 renderer 错误。分类为 UTF-8 字节估计(≈),不当作供应商精确分项;原版全状态 1:1 未验收。用户当前窗口未强制重启,此批含主进程/preload,完整重启后生效。
> 2026-10-18 用量获取复核:[诊断记录](上下文用量获取复核-2026-10-18.md)。最近用户会话没有 YCode 用量快照,但 OMP 原始记录有真实统计;隔离副本仅调用 get_state 已成功恢复 5161 tokens,未发送模型消息或改用户数据。发现当前缺少按需查询/旧记录恢复、最新无样本时误拾更早统计及来源/时效标识;旧进程也尚未加载主进程保存修复。本轮仅诊断,尚未实现上述补齐,不把此前 N04 的局部验收视为完整完成。
> 2026-10-18 新建项目背景异常已修复:[遮罩修复记录](新建项目遮罩修复-2026-10-18.md)。根因为内联弹窗的遮罩没有独立层级,sticky 标题穿到遮罩上方;只增加项目弹窗 overlay 层级与隔离,保留原版主题颜色。生产构建及项目页面真实链路通过,并新增浅色/深色截图像素遮罩检查。用户窗口未被强制重载,此次 renderer 修改重载后生效。
> 2026-10-17 用户新增七项修复:[问题与验收记录](新增七项界面修复-2026-10-17.md)。更多→项目透明、会话右键菜单、通知遮挡、上下文用量持久化/历史模型窗口、个性化输入、关于导航图标和原版权限面板已落实。最终生产构建通过;权限/浮层 88 项、任务菜单 47 项、上下文真实链路 52 项、设置三专项 20+34+20 项及三模块八场景通过。已实际查看设置和权限菜单截图;保留未知原版状态和文案适配记录,不宣称完整1:1。通知仍为示例数据,未接入 OS 沙箱。当前用户窗口未被强制重载,此批包含主进程更改,保存草稿后完整重启才可加载全部修复。
> 2026-10-17 最新 UI 续对齐:按三个 agent 分工、主上下文集成/截图审查推进定时任务、资料库、项目。编辑器稳定态 800×490(已纠正原版 .96 入场帧误读);本地产物去掉多余内侧栏并恢复全宽五列表格/原始 SVG;项目顶栏 64、主区随视口满高、配置栏 320、输入区贴底,深色底色/权限排列/禁用发送/面包屑均继续修正。项目输入的思考强度/权限已进入实际首发。生产构建、模块布局截图和真实模型/工具链路通过;具体报告和剩余日期面板、菜单/图标及全状态差异见[UI续对齐记录](三模块UI续对齐-2026-10-17.md)。此次经 REA 补取资料库已加载原图并恢复原选中任务;保留既有未提交修改。用户当前 YCode 后来以无调试端口方式启动,尚未强制刷新其窗口,避免丢失未发送草稿。
> 2026-10-17 最新开发:用户授权后已完成定时任务、资料库、项目的首批本地链路,见[首批开发记录](三模块首批开发记录-2026-10-17.md)。定时任务共用真实对话执行与持久化终态;资料库浏览实际跨任务产物;项目首发、失败草稿恢复、状态筛选、指令同步和能力配置已接入。生产构建、纯回归及隔离Electron实际模型/工具链路已通过;个人资料导入/引用、项目待办/资产联动和全状态1:1仍未完成。未重启用户当前YCode,未操作WorkBuddy当前任务,保留全部既有未提交修改。
> 2026-10-17 拆解/沉淀触发补充:见[对话资料沉淀与任务引用第8节](对话资料沉淀与任务引用确认-2026-10-17.md)。已证收到todo/plan工具或plan事件才同步会话计划;交接提交可创建项目待办并后台上传关联资料;项目模板在阶段完成后引导提醒沉淀。未证统一复杂度自动拆解阈值、计划卡片自动变项目事项或任务终态统一自动入库。会话计划/项目事项/产物/资料分别建模;交接创建成功不等于上传全成功。本轮仅核对固定REA正文和补文档,未操作原版或改业务代码。
> 2026-10-17 对话与资料联动确认:[对话资料沉淀与任务引用](对话资料沉淀与任务引用确认-2026-10-17.md)。固定REA源码确认本地产物归集、个人/项目保存入口、space-doc+library技能、todo及关联附件。默认项目资料根指针只在teams/cloud发送路径证实,本地adapter自动注入/自主检索未知;不可宣称自动全库依据。YCode现仅有项目指令和普通资源/附件,尚缺统一引用解析层,已加入三模块开发计划C3。本轮未改业务代码,未导航运行中的WorkBuddy会话。
> 2026-10-17 当前开发准备:[定时任务、资料库、项目开发计划](三模块开发计划-2026-10-17.md)。用户要求先梳理这三项,安全中心留在后续。三个agent只读核对模块,主上下文确定公共会话执行服务、归属/持久化/引用契约及分批文件边界;新增项目首发补偿、运行点索引、筛选和指令保存同步问题记录。下一步按基础批与第一批任务推进;本轮未改业务代码、未跑构建/应用测试、未新采原版。现有未提交修改须全部保留。
> 2026-10-17 最新范围订正:用户确认原第18项“安装中心”应为**设置→安全中心**。当前四模块为安全中心、定时任务、资料库、更多→项目,见[总览](四模块拆解总览-2026-10-17.md)。此前应用安装管理方案撤出当前任务,仅保留历史,发现应用既有功能保留。安全中心当前仅有禁用导航,待REA采集与正式拆解,详见[安全中心记录](安全中心模块拆解-2026-10-17.md);其余三个模块首轮拆解继续有效。主上下文协调、agent执行的模式不变,本次仅改文档;云端服务与产品侧子代理调度仍暂缓,完整1:1未验收。
> 2026-10-17 功能修复轮次:[19 项问题修复记录](19项问题修复记录-2026-10-17.md)。本对话按主对话规划、agent 并行执行、主对话集成与最终验收推进。A01~A12 已落实功能修复,另补跨窗口运行恢复、序号去重、事务失败补偿和网关兼容;最后构建及 21 个专项全部通过(710 条结构化检查 + Git 6 条)。该修复轮次未拆解四个记录模块,之后的新授权与拆解见上方;全状态像素/动画及原版未知行为不因修复通过而标记完整 1:1。用户运行中的 YCode 在该修复轮次没有重启,最新主进程代码在下次启动加载。
> 2026-10-17 用户新增 20 项对话、侧栏和设置对齐要求,已建立后续主清单:[复刻待办清单](复刻待办清单-2026-10-17.md)。本轮仅记录 TODO,尚未开展对应的新 REA 采集和实现;编号与用户原始问题一致,记忆与进化、安装中心、定时任务、资料库、更多中的项目保留待拆解。后续逐项记录证据、实现与验收,不能仅凭构建通过勾选完成。 > 2026-10-17 用户新增 20 项对话、侧栏和设置对齐要求,已建立后续主清单:[复刻待办清单](复刻待办清单-2026-10-17.md)。本轮仅记录 TODO,尚未开展对应的新 REA 采集和实现;编号与用户原始问题一致,记忆与进化、安装中心、定时任务、资料库、更多中的项目保留待拆解。后续逐项记录证据、实现与验收,不能仅凭构建通过勾选完成。
> 2026-10-16 工具动作标题线上复核:此前截图取自 23:12 的旧构建;23:35 启动的旧调试进程仍占用 9237,导致先前重启检查没有连到新构建。重新构建并清理旧调试进程后,以 PID 86372 在 9237 启动最新版本;重新打开原来没有 `intent` 的真实会话 `2f93304e-a8d6-40ab-96d4-9e4b45511352` 并展开,DOM 与截图均显示 `List contents of src directory`、`Read file ... (lines ...)` 等动作标题。验证截图:`.tmp/ycode-tool-intent-session-2f-expanded.png`。`npm run test:conversation` 与 `npm run build` 通过。当前这项修复仅验证到历史工具记录和静态回归;流式进行中的视觉刷新仍由既有工具事件回归覆盖,未在本轮真实模型请求中复测。 > 2026-10-16 工具动作标题线上复核:此前截图取自 23:12 的旧构建;23:35 启动的旧调试进程仍占用 9237,导致先前重启检查没有连到新构建。重新构建并清理旧调试进程后,以 PID 86372 在 9237 启动最新版本;重新打开原来没有 `intent` 的真实会话 `2f93304e-a8d6-40ab-96d4-9e4b45511352` 并展开,DOM 与截图均显示 `List contents of src directory`、`Read file ... (lines ...)` 等动作标题。验证截图:`.tmp/ycode-tool-intent-session-2f-expanded.png`。`npm run test:conversation` 与 `npm run build` 通过。当前这项修复仅验证到历史工具记录和静态回归;流式进行中的视觉刷新仍由既有工具事件回归覆盖,未在本轮真实模型请求中复测。
@@ -0,0 +1,67 @@
# 用户新增七项界面问题
日期:2026-10-17。用户要求完成当前三模块工作后处理;当前三模块批次的构建与验收已记录在[UI续对齐](三模块UI续对齐-2026-10-17.md),本批开始推进。
原则:原版实现只通过 REA 或已保存的 REA Evidence 查看;已知行为按 WorkBuddy 对齐,不确定项单独记录并留给用户决策。沙盒内核不在本批接入范围,不把视觉复刻当作安全隔离已实现。
| 编号 | 问题 | 执行边界 | 状态 |
| --- | --- | --- | --- |
| N01 | 点击更多,项目浮层透明 | 侧栏 / 项目入口样式 | 已修复,浅色/深色运行验收通过 |
| N02 | 左侧会话右键与三点菜单不一致 | 共用真实任务菜单,保留目标会话身份与状态 | 已修复,实际菜单与目标身份验收通过 |
| N03 | 左下角通知内容被遮挡 | 通知浮层层级、裁切、定位 | 已修复,浅色/深色运行验收通过;数据仍为示例 |
| N04 | 对话输入区上下文用量有问题 | 数据语义、模型窗口、未知值与展示 | 已修复,真实用量持久化/重载/切换/回退验证通过 |
| N05 | 个性化称呼与名字输入框样式不一致 | 设置个性化原版控件 | 已修复,浅色/深色及真实保存验收通过 |
| N06 | 设置关于缺少图标 | 关于导航原版信息图标及恢复器 | 已修复,真实 SVG/几何/生成幂等校验通过 |
| N07 | 默认沙箱入口逻辑与样式不一致 | 原版权限面板 / 完全访问开关 / 风险确认 | 已修复并通过开关全流程;文案适配见待决策 |
主上下文负责 N07、跨模块数据契约、集成和最终验收;三个 agent 分别执行 N01–N03、N04、N05–N06。不得并行启动整应用 Electron QA,最终构建与运行验收由主上下文串行完成。
## 修复与验证
- N01:body portal 自带完整浅色/深色背景、边框、阴影变量,避免脱离父级主题作用域后变透明;保留菜单翻转和键盘操作。
- N02:右键、Shift+F10/菜单键与三点按钮复用 `TaskContextMenu`,使用同一个目标会话状态和操作链路;右键不先选中另一任务。
- N03:通知通过 body portal 呈现,固定定位并按视口边界翻转、夹紧;关闭、Escape、点击外部和焦点返回均检查。通知服务尚未接入,示例标注保留。
- N04:不累计多个工具轮次的 usage,取最后一次有效输入用量。新增逐条助手消息 `contextUsage`,冻结该回合的模型窗口并写入 checkpoint;重载、模型配置变化、切换会话、回退和删除都同步恢复或清除。未知旧记录不借用当前模型窗口。格式按源实现显示一位小数与 K,如 `20.0% · 12.8K / 64.0K`;超窗文字保留真实比例,圆环仅限制在 0–100%。分类统计没有真实数据时保持 `—`。
- N05:两处身份编辑共用原版 `wb-input--medium`,输入 177×32、编辑行 251×34,统一边框/圆角/焦点/悬停及取消确认按钮。恢复原始关闭/勾选路径;编辑弹窗关闭按钮 24×24、图标 16×16。真实输入、取消、保存、重开及浅色/深色均验证。
- N06:缺失的是设置左侧“关于”的信息圆图标;固定 Evidence 的 SVG path 被旧恢复器遗漏了 `d`。现在从 about 组 `svg_nodes` 恢复原根属性与 534 字符路径,恢复脚本支持 `--check`,不会在重新生成时丢失。关于正文按原版没有新增品牌图标。另修正原页脚分隔符字形和 44px 高度。
- N07:恢复原版 200px 权限面板、说明/分隔线、单一完全访问开关及 480px 风险确认框。勾选风险项才可确认;取消确认保留权限面板;确认成功关闭面板;关闭完全访问直接恢复默认。原始 SVG、开关、checkbox 和按钮禁用态均从固定 REA 资源恢复。移除额外外壳 padding/边框,面板距触发器 4px;确认层级 100001。风险确认不再写成“以后跳过”设置。
已通过的最终运行报告:
- `.tmp/seven-ui-fixes-otsHxW/report.json`:88 项,浅色/深色项目浮层、通知、权限面板和确认/取消/恢复默认全流程;无 renderer 错误。
- `.tmp/task-menu-qSFOyj/report.json`:47 项,含真实右键与三点菜单等价、任务身份及实际操作。
- `.tmp/composer-context-pJn2OU/report.json`:52 项,真实 Electron/preload/IPC/OMP + 受控 localhost 模型;含真实 usage、主进程落盘、renderer 重载,以及当前模型窗口改大后仍使用历史冻结窗口。
- `.tmp/three-modules-3qKM4N/report.json`:三模块八条真实模型/工具集成场景,包含项目极致思考/完全访问、首发立即切页和失败草稿恢复。
- `.tmp/settings-nav-24jVrf/report.json`:20 项,包含关于信息图标真实路径及 16×16 尺寸。
- `.tmp/personalization-settings-3HS4Ks/report.json`:34 项,包含原版身份编辑尺寸、两处真实保存、弹窗关闭图标及浅色/深色;留存 `identity-edit.png`、`personalization-editor.png` 和两套主题截图。
- `.tmp/about-help-settings-DQudf7/report.json`:20 项,含关于/帮助布局、原始外链箭头、页脚 44px 与分隔符字形;留存 `about-light.png`、`about-dark.png`。主上下文实际查看了身份编辑和关于深色截图。
- `npm run typecheck`、`npm run build`、`npm run test:chat`、`npm run test:persistence`、`npm run test:conversation-ipc` 已通过。权限恢复校验 `node scripts/rea/recover-permission-control.cjs --check`:3 个图标 / 97 条样式。
这些是已覆盖状态的功能、几何和样式检查,不能替代所有状态的逐像素验收。隔离 QA 不修改用户真实会话/模型,未向 WorkBuddy 发送消息。
## 原版依据
按用户要求复用已有 5.8.2 REA 依据,固定设置运行快照中已补采的 5.8.3 项分别保留来源;未重新整批采集。上下文原版依据 `ev_2569313a7a0aa901c5a3b8dc1d7fd190109b9006c915830109b62195f312376a`;任务菜单 `ev_f5e9d71a676c453daab40928acfb33aeabae68ba57ffda342380cead7f1a1fe2`。
权限源恢复只读取保留的 REA runtime `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb` 与固定 CSS:`src-DbE1_mgM.js` SHA256 `6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4`;`src-CyXHA0IC.css` SHA256 `a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe`;`src-Bh3YZ35H.css` SHA256 `88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a`。恢复脚本校验身份、摘要及未解析 JSX 引用。
个性化身份编辑/图标 `ev_7b37285056df4d32125272daa6819c1efb18cf7ef3ca269cd9da6872c6f5483f`;编辑弹窗 `ev_523e07353a9fd7a003136f8e07c72d5ba87a05f893dd4b4c4adb2d7ed5a7430f`;关于正文 `ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c`;关于导航 `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`。导航路径 SHA256 `d84f185b0d52e3b7eb61c96efc21a34887c213746b12132ddd3595d33e332287`,连续生成检查无漂移。
上述 Evidence 的页面 user agent 可核对版本:个性化两份、关于正文为 5.8.3;关于导航为 5.8.2。runtime 导出未包含可独立确认版本的 user agent,沿固定脚本身份/摘要使用,不将当前运行版本推断为该脚本的采集版本。
本轮原版被动截图 `ev_176b4a0369c4836f9498bd4a9c7c9dff10022defcbddaad6dd0080442de78ee9` 表明当前原版在专家搜索页面;未为权限菜单取证而改动用户页面,权限结论来自已有固定源码证据。
## 待决策 / 未确定内容
1. 未实现 OS 沙箱,因此默认说明中的“安全沙箱”暂用“默认权限”;确认框“所有任务”暂用“当前发送的任务”,与本地实际权限范围一致。界面结构、交互按原版恢复,文案适配记录待用户决定。
2. 原版上下文动态 `maxAllowedSize` 与自定义模型声明窗口的等价关系尚未证实;当前冻结真实本地模型 `maxInputTokens`,不估算虚假分类。
3. 通知原版打开态的精确尺寸/动效与右键锚点的原版独立帧尚未补采;当前验证解决遮挡和行为等价,不宣称这些状态零像素差。
4. 通知后端、项目更多中的云入口、沙箱内核仍未接入。
## 用户窗口与中间问题
生产构建完成,但用户当前以无调试端口方式运行的 YCode 未被强制终止或重载,以保留无法读取的未发送草稿。本批包含主进程持久化改动,完整加载需要保存草稿后退出并重新启动 YCode;仅重载 renderer 不足。
中间验收捕获权限 SVG 恢复遗漏 `$s` JSX 别名引发白屏,现已修复生成器并增加未解析引用检查。上下文测试更新模型遗漏 `previousId`、导航测试把 16px 几何误写为布尔预期均已修正,最终报告通过;不把中间失败当作最终通过。
设置验收还捕获编辑弹窗 SVG 被通用按钮样式缩到 12×12、关于页脚分隔符导致高度 46px 的两处差异;依据固定 REA 修正覆盖后,最终图标为 16×16、页脚为 44px,未通过放宽预期掩盖差异。
@@ -0,0 +1,19 @@
# 新建项目背景与遮罩修复
用户反馈:新建项目的背景导致显示异常。
已在真实隔离 YCode 生产应用复现。弹窗容器、标题、正文和底栏的背景均不透明,颜色也符合当前主题;错误在遮罩的层叠关系。`collab-modal__overlay` 使用 `z-index:auto`,没有形成独立的层叠上下文。侧栏 sticky 分组标题等定位元素会绘制到遮罩颜色上方,出现亮条。透明 backdrop 仍可以拦住点击,因此只检查 `elementFromPoint` 或弹窗自身背景不能发现这个问题。
修复位于 `src/renderer/src/projects/project-layout.css`:只对项目新建弹窗的 overlay 设置 `z-index:1000; isolation:isolate`,高于页面顶栏与侧栏,同时低于已有模板下拉 portal 的 1100 层级。不改原始恢复 CSS,不改原版浅色 0.6 / 深色 0.8 的遮罩颜色。
原版参照沿用 WorkBuddy 5.8.3 固定 REA `ev_bc0f5546bb714cad4634168969a115be8f34e307bc09f51caf1457f5d3ed61ec`:原版在 body 下挂载 portal,overlay 仅声明 fixed/inset 和遮罩背景,portal 宿主为 display:contents,没有提供额外层叠上下文。本地目前内联挂载与原版不同,1000 是本地宿主层级适配,不声称为原版测得数值。本轮未重新采集或操作 WorkBuddy。
验证:
- `npm run build` 通过,包含 node/web TypeScript 检查;日志 `.tmp/project-dialog-background-build.log`。
- `node scripts/verify-project-page.cjs` 通过;报告 `.tmp/project-page-3682qD/report.json`、`dialog-surfaces.json`,包含浅色/深色截图。
- 补充了弹窗四个区域的不透明背景、全视口遮罩、实际点击遮挡和背景像素合成检查。浅色同点修复前 `[247,247,247]` 未受遮罩影响,修复后 `[99,99,99]`,与 0.6 黑色遮罩预期完全一致;深色稳定帧 `[31,31,31]` 变 `[6,6,6]`,符合 0.8 遮罩。
- 既有创建/保存、模板选择及切换保留项目名、能力选择、重载恢复和项目详情几何检查继续通过;无 renderer 错误。
- 主上下文实际查看浅色、深色修复截图。测试曾错误采到深色主题过渡的中间背景帧,现等待有限动画结束后比较稳定像素;没有放宽像素预期。
生产资源已构建,未强制重载用户窗口,避免清空未发送草稿。此次产品改动仅在 renderer;重载窗口后生效。原版其他弹窗状态与全状态 1:1 不属于此次验收结论。
+81
View File
@@ -0,0 +1,81 @@
# 沙盒 Git 完整接入(2026-10-18)
状态:**工程接入已完成(Windows x64 验收范围内)**。本批集中交付完整支持子集,不再把分散的局部增量当作 Git 对齐完成。沙盒全量目标和 WorkBuddy 1:1 等价仍未完成;见本文件的明确边界和[专用验收矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。基线组件升级为 0.7.0 / protocol 1,保留 0.6.0 私有快照 Git 和会话草稿作为兼容路径。
## 本批验收范围
1. 应用携带可校验、可离线复现的 Git 及远程传输依赖,无需宿主 Git/PATH。
2. 支持宿主普通仓库和 linked worktree 的历史、真实 HEAD、索引、分支、标签、stash、忽略规则;导入先获明确审批,不导入 hook、凭证 helper、任意配置程序或 alternates 外部对象路径。
3. 对话中的本地 Git 在 AppContainer/Job 内运行;验证 status/diff/add/commit、分支/切换、merge/冲突处理、rebase/cherry-pick/revert/reset、stash/tag 和 detached HEAD。保留跨命令状态。
4. 显式宿主提交同时处理项目文件、索引/引用/对象等仓库变更;绑定原根身份、策略、版本和审批,拒绝宿主外部冲突。复用原生事务,组事务记录提交/补偿/恢复结果。
5. 远程查询、clone/fetch/pull/push 通过专用宿主控制面代理,限定结构化参数、固定 Git 和净化环境。每次远程操作有目标/引用/内容审批;沙盒普通命令继续禁网。凭证显式提供并限定目标,不自动使用宿主 SSH/全局 Git/凭证管理器。
6. 工具、OMP/备用内核、维护 CLI、状态/结果查询和错误说明一致;不在当前用户真实仓库或远程仓库上做写入测试。
7. 用真实临时仓库/远程服务验证成功、拒绝、取消、冲突、传输失败、篡改和恢复;构建、兼容回归、资源身份和文档一起验收。
交互式编辑器、任意用户 hook/filter/helper、LFS/外部扩展及子模块各自的执行/凭证权限不得通过普通 Git 参数绕过沙盒。测试报告仅记录工程侧行为;未知原版行为不会自动算已对齐。
## 原版依据
参考目录仍为材料声明的 WorkBuddy 5.8.6,版本未独立证明,目录摘要 `d87c4cab25fb1feec426332fe01b1680538577405cf82476cb6f5b1b14d94b84`。本批通过 REA 复核程序策略和文件代理两个 CJS,父 Evidence `ev_3f45718a91d5c7f20b8940aff699cf62e42467e1c60df9448b8ef9ff4b4be0ab`,摘要视图 `ev_8e62a51a397ca733c115e7bb42d5925b9fbdda1be19ab33b8ff4cbf8b525638c`。仅有静态职责线索,无完整 Git 宿主/远程运行证据;本批工程验收与原版同场景验收分开记录。
## 架构决定
- 宿主历史通过审批导入为会话私有镜像,Git 命令继续在隔离 stage 使用副本;不直接给 AppContainer 开放宿主 `.git`。
- 普通项目文件与 Git 元数据使用分别绑定根身份的原生事务,另有组事务记录跨根提交和补偿;不承诺跨根/断电原子性。
- 原始配置仅由宿主控制面读取和合并白名单数据;沙盒执行配置和传输配置均重新生成,不运行宿主 alias/hook/filter/fsmonitor/credential helper。
- 远程代理是有网络能力的受信控制面,不向命令 Job 添加网络 capability,也不接受任意 shell/可执行文件/环境变量。
## 实施结果
### 接入能力
- 应用内置并校验官方 MinGit 2.56.0.windows.2;HTTPS、SSH、HTTP/HTTPS helper、OpenSSH、必要 DLL 和许可证作为同一固定归档/清单交付。SHA-256:归档 `da35e72aa21c005a5a0d298cfbae110bc1609a815730ea0dde84b01a1b3cd3be`,运行时清单 `17cbe59d1fdd9230e528be9ec6402b870637d5e7148215f278bbaff01b92ed01`;82 个文件、30,197,068 bytes。普通构建离线复核,不扫描宿主 PATH。
- `sandbox_git` 接入对话工具,`sandbox_workspace` 的 commit/discard/recover 统一转到 Git 管理路径;受控 Git argv 仍在 AppContainer/Job 执行。状态、远程持久操作结果、会话/项目 owner 只读 IPC 和维护 CLI 已接线。
- 本地常用流程包含 status/diff/add/commit、分支/标签/switch、merge 冲突继续/abort、rebase、cherry-pick、revert、reset、stash、detached HEAD;非交互 Git editor 只用于已允许的继续操作。
- `import` 先逐次审批,把宿主普通仓库或现有 linked worktree 的真实历史、HEAD、索引、引用及安全白名单配置导入当前会话镜像;不执行导入的 hook/helper/filter,也不带入凭证或外部对象路径。源内容在审批期间变化即拒绝。
- `init` 创建真正 unborn 私有仓库。`clone` 读取远程默认分支/全部引用,包含空远程和 SHA-256;首次发布新仓库时先单独审批创建 `.git`,再审批工作区和 Git 元数据组提交。
- 只有显式批准的宿主提交会同步项目文件及 `.git` 元数据;使用原生目录身份、写前摘要、Git 锁协调和组事务恢复。Linked worktree 按工作树私有目录、common Git 目录拆分提交阶段。对象先发布、引用后发布,宿主对象只追加。
- 远程操作包括 `ls-remote`/`clone`/`fetch`/`pull`/`push`。HTTPS 校验证书、禁止重定向,SSH 使用显式私钥和 pinned `known_hosts`;凭证经用户 DPAPI 保护并限定到精确 URL。审批指定 endpoint、远端引用和对象 ID;push 使用原子更新,force-with-lease 需用户提供期望旧 OID。普通命令及 AppContainer 仍不获通用网络权限。
- pull 默认 ff-only,另有显式 merge/rebase;拒绝/取消/策略变化会终止对应作业。Push 响应丢失作为 `unknown` 保存,不自动重试;用户可单独批准只读 reconcile,核对当前引用状态。
- 控制 Job 使用原生 Git runner 固定 argv/环境、父进程监控、期限和进程树限制。异常控制目录由维护入口仅在 owner/native lease 都无活进程时清理。
### 维护者 CLI
所有带状态的命令都要求显式 `--state-root` 及绝对路径;需要向宿主项目或远程写入的操作必须额外写 `--approve`。请求文件只承载 argv 或结构化远程参数,不使用 shell 拼接:
```powershell
npm run sandbox:maintain -- git-host-status --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox'
npm run sandbox:maintain -- git-host-import --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox' --approve
npm run sandbox:maintain -- git-host-commit --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox' --approve
npm run sandbox:maintain -- git-host-recover --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox'
npm run sandbox:maintain -- git-run --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox' --request 'C:\requests\git-argv.json'
npm run sandbox:maintain -- git-remote --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox' --request 'C:\requests\fetch.json' --approve
npm run sandbox:maintain -- git-remote-operation OPERATION_UUID --workspace 'C:\Project' --session SESSION_ID --state-root 'C:\YCodeData\sandbox'
npm run sandbox:maintain -- git-credential-add --url 'https://example.invalid/team/repo.git' --credential-file 'C:\secure\credential.json' --state-root 'C:\YCodeData\sandbox'
npm run sandbox:maintain -- git-credentials --state-root 'C:\YCodeData\sandbox'
npm run sandbox:maintain -- git-cleanup --state-root 'C:\YCodeData\sandbox'
```
`git-argv.json` 为 JSON 字符串数组,例如 `["status", "--short"]`;`fetch.json` 是 `{"action":"fetch","url":"https://example.invalid/team/repo.git","branch":"main","credentialId":"CREDENTIAL_UUID"}`。HTTPS 凭证文件格式为 `{"type":"https","username":"...","secret":"...","caCertificate":"可选 PEM"}`,SSH 凭证则提供 PEM private key 和 exact-host `knownHosts`。凭证导入经过 DPAPI 加密,清单只显示 id/URL/type;不要把真实 token 写入命令行、请求或日志。
### 最终验证
- `npm run build`:通过;包含 Windows x64 原生重建、两套 TypeScript 检查、生产 main/preload/renderer 构建。实际构建日志:`.tmp/git-complete-final-build.log`。
- 整批新专项:本地完整流程 29/29、安全/并发/事务/崩溃恢复 25/25、HTTP/HTTPS/SSH 真实 loopback remote 40/40、真实生产 Electron + OMP + localhost 模型 52/52;最终目录包内 Git 20/20。
- 兼容回归:工作区 51、基础沙盒 52、既有 Git 43、策略 38、目录 13、原生写回 32、内置 Git 20、读取桥 21、进程 20、工具链 24;314 项全通过,总计 480 项。
- 目录包:Windows x64 `win-unpacked` 已构建;包内 native/adapter/manifest 与 82 个 Git 文件核对 SHA-256,并使用包内资源跑 Git 专项。只验未安装/未签名编辑目录包,不代表 NSIS 安装、升级/卸载、公开签名链或标准用户验收。
- 当前机器 Windows 11 x64、提升权限。本批未对用户真实仓库或远端执行写操作;所有仓库、HTTP/HTTPS/SSH 服务、凭证、用户数据和模型服务均为隔离测试夹具/loopback。未重启用户窗口、未改生产沙盒状态、未提交代码。
### 已知不支持 / 未验证
- 只验 Windows x64 与当前本地 NTFS/提升权限;标准用户、支持 Windows 版本/文件系统矩阵和独立安全审查仍未验。macOS/Linux/ARM64 不支持。
- 元数据仅支持 files ref backend;reftable 显式拒绝。Git alternates、partial clone/promisor 必须先明确物化;不跟随外部对象路径。
- 任意 hook/alias/filter/fsmonitor/credential helper、交互式编辑器、GPG/SSH agent/GCM、LFS、子模块的独立程序/网络执行、项目任意远程 helper/扩展均未开放。宿主配置只读安全白名单。
- 上限包括 30,000 项元数据、256 MiB 仓库总量、128 MiB 单文件;不承诺大于这些上限的 monorepo/历史。
- 新仓库 `.git` 骨架初始化与后续组提交是两个显式审批/事务;多根 linked worktree 组提交可暴露中间态,不承诺跨根/断电原子性。原生覆盖仍采用两次 rename,有短暂缺名窗口。
- 远端 TLS/SSH 行为在真实 loopback fixtures 验证;未对公网托管、企业代理、真实供应商账号/产品仓库做生产凭证验证。代理是受信宿主控制面,不等同于普通命令受控联网。
- 工程实现覆盖常用 Git,而不是完整 Git 子命令/平台语义。版本兼容范围、长历史资源/性能与所有并发时序仍需扩大测试。
### 原版等价范围
参考材料目录 SHA-256 `d87c4cab25fb1feec426332fe01b1680538577405cf82476cb6f5b1b14d94b84`,材料声明 WorkBuddy 5.8.6,版本未独立证明。本批仅通过 REA 静态查看 `shims/broker-program-policy-check.cjs` 与 `shims/node-brokered-fs-shim.cjs`;Evidence parent `ev_3f45718a91d5c7f20b8940aff699cf62e42467e1c60df9448b8ef9ff4b4be0ab`,summary `ev_8e62a51a397ca733c115e7bb42d5925b9fbdda1be19ab33b8ff4cbf8b525638c`。没有原版 Git 导入、命令、审批、远程或 UI 的同场景执行证据,故这里只宣称 YCode 的工程集成/实测,不宣称 WorkBuddy 1:1 等价。
+84
View File
@@ -0,0 +1,84 @@
# 沙盒 Git 接入(2026-10-18)
当前组件版本为 **0.4.0 / protocol 1**。Git 现在默认随 YCode 提供,普通用户无需安装或注册;内置来源、构建和打包验证见[内置分发记录](沙盒内置Git分发-2026-10-18.md)。已接入真实 Git for Windows 运行时和跨命令保存的**沙盒专用仓库**。本记录不表示完整 P2、宿主仓库集成或 WorkBuddy 等价验收完成。
## 当前可用范围
- `execute_command` 显式选择 `runtimes: ['git']` 后,可执行本地 `status`、`diff`、`add`、`commit -m`、`log`、分支、`switch` 等操作;也可与 Node/Python 运行时组合。
- 首次选择 Git 时,从本次过滤后的工作区文件生成一个合成提交,分支名为 `ycode-sandbox`。基线包含所有通过快照过滤的文件,不复用宿主 Git 的跟踪、暂存或忽略状态。后续 `git add` 使用沙盒内的 Git 忽略规则。
- 合成基线保留快照原始字节,初始化的私有 `info/attributes` 默认禁用 text/filter/ident/eol/working-tree-encoding 自动转换,避免项目 `.gitattributes` 让首次 CRLF/编码文件出现虚假差异;这不复刻宿主 clean/smudge 语义。用户在沙盒内主动改动这些规则仍受原生隔离约束。
- `status` / `diff` 比较当前快照与沙盒索引/提交。后续宿主编辑会作为工作树差异出现;恢复文件事务后,Git 也会看到相应工作树差异。
- 暂存、提交、分支等仓库元数据发生变化时,需要审批保存。仅查询状态/差异/日志不触发审批;关闭可选锁及 diff 自动刷新索引,避免缓存刷新产生审批。
- 宿主文件仍使用现有文件/目录事务;沙盒仓库修订在 `stateRoot/git` 下独立保存。命令结果、审计和 `sandboxOperation` 查询分别显示 `writeback` 与 `git.persistence`。
**宿主 `.git`、历史、索引、远程配置、凭证及 hook 均不导入,宿主 `.git` 不写回。** 因此,首次沙盒状态为干净并不代表宿主仓库干净,沙盒 `log` 也不表示宿主历史。工具描述、能力状态、结构化结果和文本结果都明确标注这一差别。宿主 worktree 的 `.git` 文件同样排除。
## 运行时与边界
`toolchains.ts` 默认使用应用资源中的固定版本 Git;维护者仍可显式导入 `cmd/git.exe`、`bin/git.exe` 或实际的 MinGW/UCRT x64 `git.exe` 覆盖它。导入器只读取 PE 导入表,递归复制该主程序实际依赖的本地 DLL;不在宿主执行选中的程序。PE 依赖分析使用已经复制的同一份字节。导入失败不会替换已有注册项。
每次执行重新校验清单及全部文件摘要,复制到原生只读 ACL 约束的工具目录。不复制 Git Bash、网络辅助程序、SSH、签名程序或凭证管理器,也不继承宿主 PATH 和环境。Git 不是依赖缓存类型;`dependencies` 继续只允许 Node/Python。
原生环境指定私有 HOME、空系统/全局配置、禁止交互凭证提示、空模板/默认 hook 目录,以及默认关闭 hook、fsmonitor、凭证助手、提交/标签签名和自动维护。默认禁止全部 Git 传输协议,包括本地 file 传输。上述 Git 配置属于默认兼容策略,沙盒内命令主动修改参数或环境仍受同一 AppContainer/Job 约束;真正的宿主访问和联网边界来自原生隔离。没有新增网络 capability。
`GIT_DIR` 指向当前 stage 的 `repository`,`GIT_WORK_TREE` 指向 `workspace`。需要实际工作区 `.git` 目录或宿主绝对仓库路径的第三方工具尚未兼容。未执行的 Git 子命令、Git Bash 脚本、submodule、LFS、原生插件、交互编辑器及版本矩阵不因本轮测试通过而声称可用。
## 保存、策略与恢复
仓库修订位于 `stateRoot/git/<workspace-sha256>/objects/<uuid>`,清单绑定规范工作区、策略内容指纹及每个文件的摘要。活动引用原子替换;同工作区执行队列之外,再使用跨维护进程的 `repository.lock` 防止重复操作。工作区之间不共享仓库对象。
首次合成基线只写沙盒内部状态。用户命令产生的候选修订先保存独立文件及清单,并在运行目录写入 `git-candidate.json`,然后将候选身份和文件变更摘要共同绑定审批。保存前核对活动引用、策略和完整初始工作区快照。审批期间外部编辑、策略变化、索引/运行时损坏均拒绝旧操作。命令失败、超时、取消或审批拒绝,不发布该命令的仓库变更。
新策略可能禁止旧历史中的某些文件,因此任何策略指纹变化都会拒绝加载旧仓库,要求维护者先查看 `git-state`,再显式 `git-reset`。不会自动丢弃原有历史。`git-reset` 归档活动引用;此前修订仍留在状态目录,新的基线重新按当前策略过滤。
单次仓库修订上限:30,000 项、256 MiB,总清单 8 MiB,单文件 32 MiB;链接、硬链接、特殊文件、ADS/设备路径、外部 alternates、commondir/gitdir 及遗留 `.lock` 文件拒绝保存。这里不是 OS 磁盘硬配额;旧修订、未发布候选及运行记录尚无自动保留清理,长期磁盘管理仍待补齐。
**宿主文件事务与私有 Git 引用发布是两个持久化步骤,不保证跨步骤原子性或断电级耐久性。** 文件事务成功而 Git 发布失败时,结果保留真实 `writeback: committed`、事务 ID,并单独报告 `git.persistence: failed`。此时可查询运行/候选记录,并按现有事务恢复文件;不能根据原生命令退出码推断两者都已保存。进程异常退出可能留下 `repository.lock`;先核实记录中的 PID 已退出,再由维护者处理该锁,当前不做自动抢锁。
## 维护入口
以下命令供维护者查询或覆盖版本使用,普通用户无需执行 Git 导入。`C:\YCodeData\sandbox` 是示例,应替换为该应用实际 userData 下的 `sandbox`。验证只使用隔离夹具,没有修改用户生产配置或重启窗口。
```powershell
npm run sandbox:maintain -- toolchain-add git --source "C:\Program Files\Git\cmd\git.exe" --state-root "C:\YCodeData\sandbox"
npm run sandbox:maintain -- toolchains --state-root "C:\YCodeData\sandbox"
npm run sandbox:maintain -- git-state --workspace "C:\Project" --state-root "C:\YCodeData\sandbox"
npm run sandbox:maintain -- git-reset --workspace "C:\Project" --state-root "C:\YCodeData\sandbox"
```
模型工具参数示例:
```json
{"command":"git status --short && git diff","runtimes":["git"]}
```
Node 捕获 Git 子进程输出时,继续按已有显式适配器规则执行:
```json
{"command":"node --require \"%YCODE_SANDBOX_PROCESS%\" script.cjs","runtimes":["node","git"]}
```
## 验证与遗留工作
专项入口为 `npm run test:sandbox:git`,使用独立 userData/state/workspace、真实 AppContainer 和本机 Git for Windows 2.55.0.windows.5。夹具宿主仓库包含曾提交后删除的假 `.env`,验证其内容及宿主历史未进入沙盒;所有宿主 Git 初始化/提交均只作用于生成的夹具,未操作 YCode 自身历史。
真实生产 Electron/preload/main → OMP RPC → 本地确定性模型 → `execute_command` → 原生 Git → 聊天审批 → 文件事务/仓库保存 → 后续读取及持久结果查询,也有独立验收。构建/报告及摘要汇总见 `.tmp/sandbox-git-integration-validation.json`。
0.3.0 首批接入时完整构建/类型检查通过,6 组共 207 项通过;0.4.0 内置版本的新增与复核结果见内置分发记录:
| 专项 | 通过项 | 报告 |
| --- | --- | --- |
| Git | 43 | `.tmp/sandbox-git-YDNhf3/report.json` |
| 原生隔离 | 52 | `.tmp/sandbox-regression-wZty7E/report.json` |
| 策略/授权 | 38 | `.tmp/sandbox-policy-6AgDYV/report.json` |
| Node/Python 工具链及依赖 | 24 | `.tmp/sandbox-toolchain-mH9fP5/report.json` |
| Node/Python 进程兼容 | 20 | `.tmp/sandbox-process-IPJvWP/report.json` |
| 真实生产 Electron/OMP | 30 | `.tmp/sandbox-app-pBc7pP/report.json` |
另实测维护 CLI 的 Git 导入、列举、仓库查询与重置。将 `sandbox:maintain` 改为单一 Node 启动器,避免 npm 因前置 cmd shim 给参数双重转义;安装路径、状态目录和工作区含空格、中文及字面量 `^` 的场景通过。测试期间未接触生产沙盒配置,也未重启用户窗口。
尚未接入:宿主原有仓库的安全只读历史/索引导入、宿主 `.git` 的审批事务、远程协议及授权凭证、持久可写工作区、完整 Git 子命令/多版本兼容、自动清理、标准用户/安装包矩阵、原版同场景 REA 比较。后续导入原仓库时必须解决对象库/packfile 中的受保护历史,不能直接复制整个 `.git`。
## 0.7.0 后续增量(2026-10-18)
以上是 0.3.0/0.4.0 历史记录,保留当时的范围和报告,不再代表当前缺口。宿主 Git 导入、`.git` 元数据组事务、长期 Git 草稿、远程 HTTP(S)/SSH 和凭证现在已在[完整接入记录](沙盒Git完整接入-2026-10-18.md)中完成工程实现;当前 0.7.0 验收矩阵、限制和 29/25/40/52 新专项结果见[专用矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。
@@ -0,0 +1,27 @@
# 沙盒 Git 接入验收矩阵(2026-10-18)
本矩阵只统计 Windows x64 工程侧验证,不代表 WorkBuddy 1:1 等价、完整 Git 兼容或独立安全审查。
| 区域 | 结果 | 证据 |
| --- | --- | --- |
| 内置 Git | 通过;MinGit 2.56.0.windows.2,82 文件、30,197,068 bytes,归档/运行时清单锁定,空 PATH 可运行,损坏覆盖拒绝 | `scripts/sandbox-bundled-git-regression.ts`:20/20;`resources/sandbox/git-runtime.lock.json` |
| 最终目录包内 Git | 通过;最终 `win-unpacked/resources/sandbox/win32-x64` 的 native/runtime/82 个 Git 文件摘要匹配,并实际运行包内 Git | `scripts/sandbox-bundled-git-regression.ts`(`YCODE_TEST_BUNDLED_RESOURCES`):20/20;`.tmp/git-complete-validation.json` |
| 宿主导入 | 通过;普通仓库、真实历史/HEAD/index、分支、标签、stash、linked worktree,审批期间变化拒绝;hook/helper/filter/凭证/外部 alternates 不导入 | `scripts/sandbox-git-complete-regression.ts`:29/29;`scripts/sandbox-git-safety-regression.ts`:25/25 |
| 本地工作流 | 通过;status/diff/add/commit、switch/merge/冲突继续、rebase、cherry-pick、revert、reset、stash、detached HEAD、非交互 editor | `scripts/sandbox-git-complete-regression.ts`:29/29 |
| 宿主写回 | 通过;项目文件与 Git 元数据组事务、索引/HEAD/ref/object 顺序、锁身份、补偿、已提交后元数据失败、真实主进程死亡恢复、pack 大于普通文件上限 | `scripts/sandbox-git-safety-regression.ts`:25/25 |
| 新仓库 | 通过;unborn 私有仓库、两次审批创建宿主 `.git` 后发布真实首提交;不生成合成基线 | `scripts/sandbox-git-complete-regression.ts`:29/29 |
| 远程 HTTP/HTTPS | 通过;loopback smart HTTP、TLS 校验/显式 CA、精确 URL 凭证、撤销、fetch/clone/pull/push、dry-run、force-with-lease、删除 ref、unknown/reconcile | `scripts/sandbox-git-remote-regression.ts`:40/40 |
| 远程 SSH | 通过;显式 key、pinned known_hosts、自有 SSH proxy、fetch/push、错误 host key 拒绝 | `scripts/sandbox-git-remote-regression.ts`:40/40 |
| clone 语义 | 通过;默认分支、全部远程分支、空远程、SHA-256、原始 remote ancestry、维护 CLI 复用 | `scripts/sandbox-git-remote-regression.ts`:40/40 |
| 生产接线 | 通过;真实 Electron main/preload/renderer、OMP、loopback model、session owner、审批/拒绝/取消、Git durable result/query | `scripts/verify-sandbox-app.cjs`:52/52 |
| 兼容回归 | 通过;长期工作区 51、基础 52、既有 Git 43、策略 38、目录 13、原生写回 32、内置 Git 20、读取桥 21、进程 20、工具链 24 | 314/314 |
| 构建/包 | 通过子集;原生/TypeScript/Electron 构建通过,目录包产物已生成并检查资源布局 | `.tmp/git-complete-final-build.log`、`.tmp/git-complete-package/` |
## 明确限制
- 仅 Windows x64、本地 NTFS、当前提升权限环境;标准用户、其他平台、升级/卸载/NSIS、独立安全审查未验。
- files ref backend;reftable、alternates、partial clone/promisor、LFS、子模块网络、任意 hook/helper/filter/GPG/交互编辑器未开放。
- 30,000 entries / 256 MiB 总仓库 / 128 MiB 单文件边界;宿主对象追加保留,不自动做破坏性 GC。
- linked worktree/新仓库跨根发布不是断电原子事务;两步 rename 有短暂缺名窗口。
- 远程只验证 loopback fixtures;受信宿主 remote broker 不等于普通命令获得网络能力。
- 原版材料只有 REA 静态 Evidence,没有完整原版 Git 运行证据;本矩阵不能证明 1:1。
@@ -0,0 +1,36 @@
# 沙盒 Node 子进程与临时目录修复(2026-10-18)
## 已确认的问题与修复
继续实现[只读文件桥](沙盒只读文件桥接入-2026-10-18.md)时,真实测试发现三个独立问题:
1. Windows 把 AppContainer 内的 TEMP 派生为私有 `LOCALAPPDATA/Packages/<profile>/AC/Temp`;最初只创建了 `stage/tmp`,派生目录不存在,Node 临时文件创建返回 ENOENT。runner 现已在启动和授予 ACL 前创建对应私有目录,仍随本次 stage 清理。
2. 当前受测 Node 的默认同步管道捕获会在实际 AppContainer 中挂起,甚至其自身 timeout 尚未有效结束该启动;最终由外层 Job 时限终止。继承 stdio 和传入文件描述符的同步捕获均可用。提供显式预加载的自有适配器,用私有临时文件描述符交给原版 Node spawn 实现,保持原 token/Job/网络约束。
3. Python 3.13 起的 Windows `mkdir(0o700)` 创建显式 user/admin DACL,没有 AppContainer SID;真实 Python 3.14.7 的 TemporaryDirectory 因此创建后立即不可访问。新增只读 `python-sitecustomize.py`,仅对当前私有 stage 内的 0o700 建目录改为继承原生私有父 DACL;stage 外仍用原行为,访问继续由系统 ACL 拒绝。
未将该问题归因于已证明的 Windows/Node 内部机制:当前证据只证明上述允许/失败路径。未修改用户安装的 Node、libuv 或全局设置,也未回退宿主执行。
## 使用与维护
```json
{
"command": "node --require \"%YCODE_SANDBOX_PROCESS%\" build.cjs",
"runtimes": ["node"]
}
```
源文件是 `native/sandbox/node-process.cjs`。随 `sandbox:build` 复制到资源目录,manifest 记录源码和成品摘要;每次选择 Node 时校验、复制到只读 tools 目录。适配器通过显式 `--require` 或 require 启用,不更改宿主 Node 设置,不自动覆盖所有项目的运行方式。
启用后覆盖该 Node 进程公开的 `spawnSync`、`execFileSync`、`execSync`,并同步 ESM 内建命名导出。子进程依然由 Node 原生启动,AppContainer/Job 继承和系统 ACL 继续生效。默认捕获及 `stdio: 'pipe'` 使用私有输入/输出文件,其他显式 stdio 配置交给原实现。私有文件句柄在操作后关闭,文件和空目录尽力移除;最终 stage 清理负责收尾。
输入与 maxBuffer 各限制为最多 16 MiB;默认 maxBuffer 为 1 MiB。适配器按上限读取捕获内容,超限返回 ENOBUFS。这里是结果缓冲限制,不是子进程运行中的磁盘硬限额;超限检测发生于子进程返回后,不能由 ENOBUFS 推断进程在产生超量输出时已被终止。timeout 继续交给 Node 原生实现,整个进程树另由外层 Job 管理。
## 当前验证
`npm run test:sandbox:process` 最终 20/20 通过,报告 `.tmp/sandbox-process-9EyXTm/report.json`。测试使用隔离状态与运行时,真实执行 Node 同步二进制/stdin/错误/cwd/env/超时、继承 stdio、ESM 导出、缓冲上限、临时文件清理、原生 ACL、宿主文件拒绝,Python tempfile/subprocess/0o700 和宿主路径拒绝,以及三个资源被篡改时安装状态/命令共同拒绝。
已保留的原始诊断:`.tmp/sandbox-spawn-compat.log`(默认捕获超时)、`.tmp/sandbox-spawn-compat-default-fixed-temp.log`(创建 TEMP 后默认捕获仍超时)、`.tmp/sandbox-spawn-compat-files-fixed-temp.log`(文件描述符捕获成功)、`.tmp/sandbox-spawn-compat-adapter.log`(显式适配后 Node/宿主专用读取器的捕获与拒绝均成立)。
该适配器不是完整 child_process 复刻。异步 spawn/fork/IPC、流式事件/背压、额外 fd、所有 stdio/平台版本组合及输出磁盘硬限额继续待验;不启用适配器的默认 Node 管道仍有上述已知问题。P2/P5 不因此标记完成,也没有原版等价结论。
Python 适配器通过受控 PYTHONPATH 加载,具有清单摘要和只读 tools ACL。它不提供宿主目录授权,不修改宿主 Python;`-I/-S` 等绕过 site/PYTHONPATH 的方式也会绕过兼容修复。这里保留父 DACL 的私有范围,不宣称 POSIX mode/元数据完全兼容。原生行为依据是 [Python os.mkdir 文档](https://docs.python.org/3/library/os.html#os.mkdir) 对 Windows 0o700 及 3.13 变更的说明;该行为与 AppContainer 的组合失败及修复效果由隔离实测记录。
+65
View File
@@ -0,0 +1,65 @@
# YCode 内置 Git 分发(2026-10-18)
组件 **0.4.0 / protocol 1** 已将 Git 改为应用默认携带。普通用户不需要在电脑上安装 Git,也不需要执行 `toolchain-add git`。工具使用 `runtimes: ['git']` 选择内置运行时,首次使用会按[已有仓库规则](沙盒Git接入-2026-10-18.md)生成沙盒专用基线。
## 固定来源和文件
当前固定版本为 Git for Windows **2.56.0.windows.2**,来源为[官方 MinGit 发布](https://github.com/git-for-windows/git/releases/tag/v2.56.0.windows.2)。上游 ZIP 为 `MinGit-2.56.0.2-64-bit.zip`,39,806,486 字节,GitHub release asset 的 SHA-256 与下载实测一致:
```text
da35e72aa21c005a5a0d298cfbae110bc1609a815730ea0dde84b01a1b3cd3be
```
`resources/sandbox/git-runtime.lock.json` 同时固定版本、官方地址、上游归档摘要,以及确定性生成的运行时清单摘要。构建脚本从 ZIP 读取实际 UCRT/MinGW x64 PE 导入表,只保留 Git 主程序及其所需 DLL,不在宿主执行下载的程序。当前内置子集含 46 个文件、7,204,871 字节(约 7.2 MB),其中包括许可证、版权文本、上游包版本、来源和构建入口说明。Git Bash、SSH、凭证管理器及远程辅助程序不随这个子集提供。
生成资源为 `resources/sandbox/win32-x64/git/{manifest.json,files/**}`,不提交生成的二进制。`extraResources` 的 `git/**/*` 将整个子集带到应用的 `resources/sandbox/win32-x64/git`,保留在 asar 之外。主程序 manifest 固定该 Git 清单的摘要;启动时可查询来源 `source: bundled` 和版本,每次执行继续核对全部文件摘要并复制到受限只读工具目录。
`LICENSE.txt`、`licenses/**`、`THIRD_PARTY_NOTICES.txt`、`SOURCES.json` 和 `package-versions.txt` 随资源一起打包。保留这些材料不等于已验收公开再分发时的全部源码提供义务;对应源码及构建材料的实际交付和发布许可检查仍属于 A16 发布验收。本轮只生成本地验证包,没有对外发布,也不据此宣称 YCode 的许可证结论。
## 默认行为及高级覆盖
1. 未导入替代 Git 时,直接读取应用内置资源,不创建用户 Git 注册项,不扫描宿主 PATH。
2. 维护者主动执行 `toolchain-add git` 后,使用该替代版本,并在状态中标注 `source: imported`。损坏的显式覆盖会报错,不能悄悄切换到内置版本。
3. 内置清单或文件缺失、被修改时,Git 执行被拒绝;不会回退到系统 Git。Git 单项失效也不会让普通 shell 命令改走宿主执行。
4. 网络、宿主配置/凭证、工作区过滤、审批和仓库持久化规则保持已有范围。当前没有宿主历史/索引导入、宿主 `.git` 写回或远程 Git 接入。
## 维护和构建
开发者首次准备源代码工作区时,运行一次明确的依赖供应命令:
```powershell
npm run sandbox:vendor-git
npm run build
```
也可以提供已经下载且摘要匹配的上游 ZIP,完全离线生成:
```powershell
npm run sandbox:vendor-git -- --archive "C:\Downloads\MinGit-2.56.0.2-64-bit.zip" --offline
```
该步骤是维护者准备安装包的构建步骤,不是最终用户注册 Git。普通构建只验证现有资源,不隐式下载。缺失或被篡改时,构建会明确失败。供应脚本先在生成目录完成校验,再替换目标资源;旧版本保留在 `.tmp/vendor-git-build-*` 中。更新上游版本时,需审查并更新锁文件的归档及清单摘要,再重跑离线复现和回归,不自动追随 latest。
验证入口为 `npm run test:sandbox:bundled-git`。设置 `YCODE_TEST_MINGIT_ARCHIVE` 可追加离线确定性重建检查;设置 `YCODE_TEST_BUNDLED_RESOURCES` 可直接验证打包后的 `resources/sandbox/win32-x64`,避免只验证源码目录。
## 验证范围
已完成空用户数据、空宿主 PATH 下的真实 AppContainer Git、跨命令提交、中文和空格安装路径、高级覆盖及损坏拒绝、资源/清单损坏、无系统回退、构建摘要检查、许可证/来源随包和离线可复现检查。真实生产 Electron/preload/main/OMP 测试移除了 Git 注册步骤,验证默认内置来源与实际运行版本一致,并保持聊天审批和持久化查询。
实际目录包使用本机已有的 Electron 37.10.3 构建,避免构建工具另行下载同版本 Electron;目录包测试只说明本地未签名资源布局和执行子集,不能替代 NSIS 安装、标准用户、升级/卸载、签名和公开发布验收。具体报告与资源/源码摘要汇总见 `.tmp/sandbox-bundled-git-validation.json`。
完整构建及类型检查通过,最终复核 5 组共 157 项通过:
| 专项 | 通过项 | 报告 |
| --- | --- | --- |
| 实际打包资源内置 Git / 离线复现 / 拒绝覆盖无关目录 | 21 | `.tmp/sandbox-bundled-git-kB0ET9/report.json` |
| Git 本地工作流(使用固定版本 2.56.0.windows.2) | 43 | `.tmp/sandbox-git-hKqvGy/report.json` |
| 生产 Electron/OMP,无 Git 注册步骤 | 31 | `.tmp/sandbox-app-8ou6LQ/report.json` |
| Node/Python 工具链及依赖 | 24 | `.tmp/sandbox-toolchain-h859sL/report.json` |
| 策略/授权 | 38 | `.tmp/sandbox-policy-iUnn9M/report.json` |
目录包位于 `.tmp/sandbox-bundled-git-package/win-unpacked`,最终打包日志为 `.tmp/sandbox-bundled-git-package.log`。构建命令使用 `electronDist=node_modules/electron/dist` 和专用输出目录;初次尝试由打包器下载 Electron 时网络超时,最终使用本机相同版本重新构建成功。包内 Git 的清单、46 个文件及所有许可证均与源资源一致;主应用和沙盒执行器的摘要也纳入汇总。没有改动用户生产策略/运行时注册或重启现有窗口。
### 0.7.0 供应增量
本记录的 0.4.0 统计保留历史口径;当前运行时清单已扩展为 82 文件/30,197,068 bytes,以覆盖 HTTPS、SSH、helpers 和完整 DLL closure。宿主 Git 导入/远程传输的工程验收不再由本文件单独代表,统一见[沙盒 Git 完整接入](沙盒Git完整接入-2026-10-18.md)与[验收矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。
@@ -0,0 +1,82 @@
# 沙盒原生写回事务(2026-10-18)
> 后续增量:0.6.0 已接入[长期会话草稿](沙盒长期工作区接入-2026-10-18.md),提交继续复用本文 v3 原生宿主事务。下方 0.5.0 测试和“下一阶段”保留为当时记录;长期草稿的已实现子集及剩余限制见新文档。
## 交付范围
组件升级为 0.5.0 / protocol 1。默认 `write_file` / `edit_file`、命令产物提交、失败补偿和显式事务恢复均使用自有原生文件事务后端。支持范围是 Windows x64、本地固定 NTFS 卷;后端不可用时拒绝写入,不退回普通宿主文件写入。
本批没有新增 WorkBuddy 取证。它是 YCode 自有边界实现,不代表 P3 全部完成或原版等价验收。
| 文件 | 职责 |
| --- | --- |
| `native/sandbox/filesystem-write.h` | 宿主专用事务通道;固定根和目录句柄,按父目录句柄打开、创建、重命名和删除目标 |
| `native/sandbox/runner.cpp` | `--workspace-write` 宿主入口;AppContainer 内调用直接拒绝 |
| `src/main/sandbox/filesystem-writer.ts` | 私有串行通道、帧/时限限制、身份检查和生命周期 |
| `src/main/sandbox/native-component.ts` | 命令和事务共用组件摘要、普通文件身份及事务协议校验 |
| `src/main/sandbox/transactions.ts` | v3 根身份日志、独立 before/after 备份、逐操作意图、提交/补偿/恢复 |
| `service.ts` / `agent/tools/index.ts` | 执行或审批前捕获工作区身份;原有授权消费和策略撤销继续有效 |
## 实际边界与文件语义
1. 根目录从本地卷逐级打开并保留祖先句柄。后续目标由 `NtCreateFile` 的 `RootDirectory` 和单个名称解析,启用 `OBJ_DONT_REPARSE` / `FILE_OPEN_REPARSE_POINT`;重命名用 `NtSetInformationFile` 的父目录句柄。不会在检查后重新用绝对路径遍历整个祖先链。
2. 根和目录句柄不共享删除,固定名称和身份。根祖先允许写共享以兼容原生目录操作,但目标解析仍基于已持有的目录对象,校验 reparse 属性和最终名称。文件目标不共享写入/删除;多重硬链接、reparse、设备、既有可写句柄和只读目标均拒绝,失败后不放宽共享模式重试。
3. 普通命令在快照前捕获工作区卷/文件 ID,默认文件工具在审批前捕获;提交前重新核对。即使同一路径被换成内容相同的新目录,旧审批也不能向新根写入。v3 恢复同样绑定原根身份。
4. 读取备份和检查现有内容发生在原生固定句柄下。文件写入、删除和目录删除由原生句柄执行,保留摘要冲突检查;目录最终删除还会由文件系统检查是否为空,外部新文件不会被递归删除。
5. 本批不直接向沙盒进程开放写入桥。项目命令继续修改副本,由原有整批审批触发宿主事务;已有 Node 读取桥继续只读。新通道仅由宿主主进程持有,不是逐后代 PID 认证协议。
文件替换采用两步、不覆盖的名称交接:先刷新新内容的临时文件,在保持原文件句柄固定时把原文件移到预先记录的专用临时名,再把新文件移动到目标名;两次移动均不覆盖既有名称。若竞争者创建了目标,发布失败并保留竞争者内容,随后执行补偿或标记需恢复。
因此,这不是单次原子交换,两个移动之间有短暂缺名窗口。普通读取者可能观察到 ENOENT;本批不宣称无中间状态。Windows 通用绝对路径重命名和 POSIX 替换在固定句柄条件下实测发生共享冲突,最终生产实现使用上述相对句柄交接,不通过释放目标锁重试来绕过它。
## 日志和恢复
- v3 manifest 保存根身份、完整 before/after 摘要、目录增删、调用/策略/审批归属。原文件备份、目标备份和逐操作意图均在宿主变更前刷新文件缓冲。
- 每次操作意图独立保存于 `transactions/<id>/mutations/<nonce>.json`。恢复会合并独立记录和 manifest,避免主进程中断后漏掉尚未汇总的操作;正常执行不为每个文件重写整个批次 manifest。
- 默认文件工具隐式创建的父目录也进入事务,恢复时可移除其未变化的目录树。
- `.ycode-sandbox-` 是保留名称前缀,不进入正常命令快照,模型文件操作不能访问。恢复仅清理日志 nonce 对应且内容摘要一致的临时文件;外部新增或变更的内容继续拒绝清理。
- 后端死亡或回复丢失时不推断“未执行”,未能补偿的批次保留 `recovery-required`。新的原生会话可根据独立备份恢复;交接中断造成的缺名状态可按已记录意图恢复,不能覆盖后来的外部编辑。
- v1/v2 历史记录仍可读取,在原生边界内恢复并升级为 v3。历史记录原本没有根身份,不能追溯补上当时的身份保证。
```powershell
npm run test:sandbox:write
npm run sandbox:maintain -- restore TRANSACTION_UUID --workspace 'C:\absolute\workspace' --state-root 'C:\absolute\userData\sandbox'
```
## 验证
全部测试使用独立 `.tmp` 工作区/状态目录和隔离 Electron userData;不修改生产策略,不重启用户窗口。
| 范围 | 结果 / 最终报告 |
| --- | --- |
| 原生写入专项 | 32/32;`.tmp/sandbox-write-GuLFv7/report.json` |
| 目录事务 | 13/13;`.tmp/sandbox-directories-Y8KMq0/report.json` |
| 基础隔离/宿主工具 | 52/52;`.tmp/sandbox-regression-diTTJm/report.json` |
| 策略/授权 | 38/38;`.tmp/sandbox-policy-UMZshP/report.json` |
| Git 本地工作流 | 43/43;`.tmp/sandbox-git-5Z9639/report.json` |
| 内置 Git 分发/资源 | 20/20;`.tmp/sandbox-bundled-git-CzL0UX/report.json`;本次未额外提供离线归档重建参数 |
| 读取桥 | 21/21;`.tmp/sandbox-read-bridge-nsjAQ4/report.json` |
| 真实生产 Electron/OMP | 31/31;`.tmp/sandbox-app-RVG4C7/report.json` |
| 完整生产构建 | `npm run build` 通过,包含原生 `/W4 /WX`、主进程/渲染器类型检查与 main/preload/renderer 生产产物;本次不另作安装包验收 |
最终 8 组共 250 项通过。组件源码/二进制和构建产物身份、各报告摘要及限制汇总于 `.tmp/sandbox-native-write-validation.json`;组件 manifest 源码摘要与当前源码逐项一致,原生 `--version` 与 manifest 均为 0.5.0。
专项覆盖:固定根和祖先、junction/硬链接/ADS/敏感路径、二进制和空文件、既有可写句柄、只读文件、AppContainer 调用宿主入口拒绝、审批期间根替换、外部编辑和目录新增、默认文件工具、隐式父目录恢复、实际后端死亡后的批次恢复、回复通道失效拒绝降级、专用临时文件的冲突保护,以及实际维护 CLI 的中文/空格/`^` 路径和 v1/v2 恢复升级。
“交接中断缺名并留下 old/new 临时文件”的用例是可复现磁盘状态夹具;不能把它写成已经在每个原生指令位置强制断电/终止的验收。另有真实终止原生后端的批次恢复测试,两者证据范围分开。
## 剩余限制与下一步
尚未验收断电级目录耐久性;文件刷新不等于 NTFS 命名操作和完整事务已经断电原子化。写入中断造成未完成的临时内容、临时文件被外部修改、损坏的意图记录等情况会拒绝自动清理,需继续保留恢复状态并人工核对。
尚未完成完整描述符/追加/语言 shim、ACL/时间戳等元数据、回收站、逐程序和逐后代规则、磁盘硬配额、长期工作区、标准用户/支持 OS/特殊 NTFS 配置矩阵及独立安全审查。当前验证不能推广到其他文件系统或平台。
下一阶段优先实现长期工作区的身份、策略失效、外部编辑冲突和提交/丢弃规则,再接宿主 Git 元数据事务与受控依赖更新。
## 原生 API 依据
- [ZwCreateFile / RootDirectory 与共享规则](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-zwcreatefile)
- [NtSetInformationFile / FileRenameInformation](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntsetinformationfile)
- [FILE_RENAME_INFORMATION](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information)
以上官方接口资料用于解释执行点,支持范围以自有测试和列出的限制为准。
+103
View File
@@ -0,0 +1,103 @@
# 沙盒参考代码与 YCode 接入评估(2026-10-18)
> 后续实施已完成自有沙盒首版,见[实施与维护基线](自有沙盒首版实施-2026-10-18.md)。本文保留此前评估时的代码状态,不代表后续沙盒仍未接入。
## 结论
可以据此完善 YCode,尤其是统一策略、文件操作代理、安全删除、调用归属与审计。不过,`C:/Users/Administrator/code/沙盒` 是逆向分析资料和部分客户端脚本的集合,不是可以直接安装的完整 Windows 沙盒工程。
当前 YCode 已有工作区路径检查和文件写入审批,但尚未接入模型命令的系统级隔离。最合适的实施方式是保留 OMP 与备用内核共用的宿主工具入口,在其下增加策略服务、文件事务和独立的 Windows 执行后端;参考包里的 shim 放在这套后端之上接入。仅复制 shim、设置环境变量或取消当前命令限制,不能补齐系统沙盒。
本轮完成材料核对、YCode 执行路径审查和临时目录中的权限基线探测;**未修改业务代码,未启用新沙盒,未连接或重启 WorkBuddy/YCode 的用户进程**。
## 证据与版本范围
材料 README 将对象标为 WorkBuddy 5.8.6 / Windows x64,采集日期为 2026-10-17。这是资料作者的版本声明;随包可导入的 REA Evidence 未单独证明该版本号,不能与项目历史的 5.8.2/5.8.3 原版基线混为一谈。
本轮所有原版脚本实现的核对均通过 REA,不直接读取或执行所附原版 shim。README 和 `runtime_facts.txt` 作为分析者说明阅读,其原生机制、运行实况与具体数值仍需对应的 REA 证据复核。未直接审读附带的 `.c` 反编译正文。
| 证据 | 本轮核对结果与范围 |
| --- | --- |
| 原随包 `evidence/rea_evidence.json` | REA 成功导入 2 条记录,0 条 unknown;该数字只表示本包包含的记录,不代表不存在实现未知项 |
| `ev_74bf702462bc37612d2a68ea2e1c93b30b77dd5d99eaf1e961f1ab36d626c5e3` | 原安装位置 `cli/vendor/shim` 的静态 JavaScript 分析,8 个 JS 文件;不是进程运行验证,也没有附带原生分析记录 |
| `ev_2860ca8fafaf310ee1adc2ebe31530c868da0ecbb41a6f451e8e8e86b447a078` | 本轮 REA 分析所给目录中的 8 个 CJS 文件和 `rules/tsbx_rules.json`;8 个 JS 均解析成功,源码正文解析失败数为 0 |
| `ev_3c27ca6a1b13c90777b729eac969ce3aa20a232145bfd342d6e13e76b4ac8c9d` | 本轮目录静态分析摘要;应用关系图 partial、语义图 unknown,保留 2699 项语义未知;规则 JSON 只确认解析与顶层字段,未据此宣称策略生效 |
| 两份静态分析中的文件身份 | 按 basename 对照的 8 个 CJS SHA-256 全部相同,确认提供的 JS 副本与随包 REA 记录中的字节身份一致;不证明运行环境等价 |
本轮 REA 输入目录摘要为 `d87c4cab25fb1feec426332fe01b1680538577405cf82476cb6f5b1b14d94b84`。选择范围不包含材料中的 Python、Shell、Ghidra 伪代码正文;REA 当前 JavaScript 入口不支持将这些作为源码选择项。调用返回明确的 `source_paths` 不支持错误后,已缩小到支持范围,未改走其他采集渠道。
本地证据产物(`.tmp` 为本机验证产物,不随业务代码发布):
- `.tmp/sandbox-assessment-20261018-evidence.json`:REA 导出的 7 条记录,包括导入记录、本轮分析和选定视图,约 33.8 MB。
- `.tmp/sandbox-assessment-20261018-identities.json`:8 个 JS 文件的原/副本摘要对照。
- `.tmp/sandbox-assessment-20261018-probe.json`:YCode 当前权限基线的 11 项受控检查。
- `.tmp/sandbox-assessment-probe-20261018.mjs`:可复现探测脚本;执行命令为 `node .tmp/sandbox-assessment-probe-20261018.mjs`。
## 当前 YCode 的实际边界
| 执行位置 | 已实现 | 与完整沙盒的差距 |
| --- | --- | --- |
| `src/main/agent/tools/index.ts` | `realpath` 后的工作区检查;默认模式拒绝越界;写入前审批并再次检查路径/内容;原子替换文件 | 只覆盖这些宿主文件工具,没有统一敏感文件策略、通用删除代理或任意进程文件隔离 |
| 同文件 `execute_command` | 默认模式立即拒绝;只有 `full-access` 才调用普通 `spawn(shell: true)`,Windows 取消使用 `taskkill /T` | 未实现受限进程启动、系统级文件/注册表/网络规则;`cwd` 只是启动位置;该命令分支未显式净化继承的环境变量 |
| `src/main/agent/conversation-tools.ts` / `omp-turn.ts` | OMP 与备用循环共用宿主工具与审批;OMP 启动参数含 `--no-tools` | 可以接入统一沙盒服务,无需为此更换 OMP;OMP 自身进程目前也没有系统级隔离 |
| `src/main/capabilities/mcp.ts` / `runtime.ts` | MCP 配置摘要信任、连接代次、每次调用审批;HTTP origin 保护 | 本地 stdio MCP 启动没有接系统沙盒;调用获批不等于插件进程已隔离。远程 MCP 的外部副作用需独立标明权限范围 |
| `src/main/terminal-host.ts` | 用户终端使用 ConPTY/PowerShell,限制窗口归属和资源数量,过滤部分敏感环境变量 | 独立于模型命令权限,当前没有系统沙盒;用户手动终端与模型自动执行应明确各自边界,不能误算成模型工具已隔离 |
| `src/main/workspace-revert.ts` / `src/shared/revert.ts` | 对有完整 before/after 的 `write_file` / `edit_file` 实现回滚并检查外部改动 | 不覆盖任意命令、MCP、副进程或删除产生的所有变化;不能替代通用文件备份事务 |
| 权限菜单与安全中心 | 已有默认/完全访问状态和确认;安全中心导航保留 | 权限菜单刻意将原版“安全沙箱”描述适配为“默认权限”;设置安全中心尚无页面分支,不能只接 UI 宣称后端已启用 |
## 参考材料中可以利用的部分
以下源位置均指 REA 静态图返回的行号与符号,不代表本轮执行过这些原版函数。
| 材料 | 已取得的静态事实 | 对 YCode 的用途 |
| --- | --- | --- |
| `shims/broker-ipc-client.cjs` | `sendBrokerRequest`(151 行)、`sendBrokerRequestSync`(209 行)、队列优先级/低优先级淘汰/超时相关函数 | 参考 IPC 客户端的同步/异步分工、背压、错误分类;服务端认证、协议上限、取消和断线策略需另行完整定义 |
| `shims/node-brokered-fs-shim.cjs` | 请求构造 `buildBrokerHostOperationRequest`(195 行)、`buildBrokerFileTokenRequest`(213 行)、`buildBrokerFileWriteCompletedRequest`(231 行);默认命令字可见 `HostFileOperation`、`FileTokenRequest` | 建立“受控操作/申请授权/报告完成”的文件操作生命周期,统一归属与审计;token 的真实授权语义与原生侧消费方式仍需补证 |
| 同文件 | `sessionId`、`toolCallId`、`brokerTraceId` 字段相关静态节点;`createBrokerOutcomeUnknownError`(260 行)、`loadBrokerNativeAddon`(143 行) | 将审批绑定到具体调用,并对结果未知建模;不能在 IPC 结果不明时假定成功或直接重做非幂等操作。原生 addon 仍是依赖项 |
| `shims/safe-delete-broker-delete.cjs` / `safe-delete-bulk-guard.cjs` / `node-safe-delete-shim.cjs` | 删除请求构造、同步/异步代理、批量删除检查与批准/拒绝的函数及参数节点 | 参考安全删除、批次审批和审计;回收站执行器、阈值、重放和恢复成功条件尚未由本轮证据证明 |
| `shims/broker-program-policy-check.cjs` | `checkProgramPolicy`(14 行)及程序/runtime/options 参数 | 增加程序启动策略入口;不能用程序名检查替代进程隔离或实际可执行文件身份验证 |
| `rules/tsbx_rules.json` | REA 确认解析成功,包含 `file_rules`、`process_rules`、`network_policy`、`registry_rules` 等顶层字段 | 可作为后续规则语义分析的起点;本轮没有完整规则值或实际判定证据,不直接把此 JSON 当作 YCode 已生效策略 |
因此可以复用已经确认的职责划分和协议线索;实际引入脚本前还要补齐完整正文、导出接口、依赖、平台分支与失败分支的 REA 证据。现有静态图足以支撑接入方向判断,不足以保证逐函数搬迁后行为一致。
## 这份目录还缺少什么
目录没有配套的 `sandbox-center.exe`、`sandbox-cli.exe`、`sandbox_ffi.dll`、`tsbx*.dll`、native addon 可执行产物,也没有这些组件可复现构建所需的完整工程。README 描述的 Rust 策略中枢不在所给源码集合中。
`decompiled/tsbx*.c` 按资料自身说明属于 Ghidra 伪代码;它们不等于包含原始类型、初始化、链接依赖和完整构建配置的 C/C++ 项目。本轮未尝试把伪代码当源码编译。
README 提到的受限令牌、Job、注入钩子、命名管道、网络双引擎、备份事务,以及具体 verdict 位与 registry/network TODO 等均是**待 REA 复核的分析结论**。随包仅有 shim 的两条正式 REA 记录,不能直接把这些原生说明提升为已验证事实,更不能据此声称 Windows 沙盒已复原。
当前还未证明 shim 对 Windows、macOS 和 Python 的完整适用范围;没有观察它们在 YCode 的 Node/Electron/子进程环境中的实际行为。语言层拦截即使成功工作,也不能凭静态函数存在证明任意其他程序受到同样限制。
## 建议接入顺序与验收条件
### 第一批:统一策略、审批与文件事务
在主进程增加独立的沙盒服务边界,输入至少包含会话、run、tool call、真实工作区路径、权限模式和操作;输出区分允许、待审批、拒绝与结果未知。沿用 `executeTurnTool` 的公共路由,让 OMP 和备用内核得到同一决策。
先覆盖宿主文件读写、敏感路径规则、安全删除和独立文件事务日志。既有消息 before/after 可继续用于展示,事务记录应能在消息保存失败或进程重启后追查与恢复。权限 token 应绑定主体、真实目标和操作并有有效期;不能只信任请求 JSON 中自报的 session ID。
验收:越界、junction、凭证样本保护、审批前后文件变化、审批取消、删除恢复、批量操作、重启恢复、多会话交叉请求。此批完成只能称“应用层权限与文件保护”,不启用默认模式 Shell,也不宣称任意进程已隔离。
### 第二批:独立 Windows 执行后端
通过 REA 补齐原版原生组件身份、初始化/配置/启动/停止、策略 IPC、审批回调及实际允许/拒绝行为,再决定原生后端的接入实现。无论采用哪种内核,都需由 YCode 管理自己的实例、会话和生命周期,不能借用用户正在运行的 WorkBuddy 策略进程。
在 `execute_command` 下加入受控启动适配器,输出真实能力状态。后端不可用或握手失败时继续拒绝默认模式命令,不能回退到普通 `spawn`。将环境净化、子进程继承、进程树停止、会话退出和审计纳入这个入口;当前终端的环境过滤可供参考,但不是完整凭证隔离保证。
验收:受控临时目录中的允许/拒绝写入;命令和后代进程越界尝试;程序真实身份;受控本地网络策略;取消/超时/主进程退出后的子树清理;IPC 断线与策略失败;原版同场景比较。注册表及其他声称具备的能力须分别验证。只有这些边界通过,才可在默认模式开放命令。
### 第三批:语言适配、连接器和安全中心
原生边界建立后,再依据补齐的 REA 证据引入 Node/Python/Shell 适配,验证实际运行时、同步/异步接口、rename/copy/文件描述符、恢复与错误处理。明确哪些本地 MCP 进程、用户终端和其他辅助进程处于隔离范围;配置被信任不等于进程被沙盒化。
安全中心继续按项目约定通过 REA 采集原版页面与行为。页面读取真实后端健康、策略、审计和审批状态,分别标明已实现、未实现与不可用能力;原版对应功能和视觉/交互仍按独立 1:1 场景验收。
## 本轮验证结果
`node .tmp/sandbox-assessment-probe-20261018.mjs`:**11/11 通过**。使用唯一临时目录、假 `.env` 内容和自建 junction;不读取真实凭证,不发送模型请求,不执行完全访问命令。
已验证当前行为:工作区内可读取假 `.env`(说明当前没有敏感文件策略);默认模式拒绝越界读写和 junction 逃逸;默认写入需批准,拒绝不落文件,批准后写入成功;预取消写入不创建文件;默认 Shell 被拒绝;完全访问仅在自有临时样本上验证可读取工作区外文件。
这些检查只验证 YCode 现有宿主工具的常规路径,未验证并发路径替换等竞争条件,也没有验证系统沙盒、网络、原生钩子、MCP/终端隔离或 WorkBuddy 等价行为。本轮没有业务代码变化,因此未额外运行生产构建;全部既有未提交修改保持原样。
@@ -0,0 +1,98 @@
# 沙盒只读文件桥接入(2026-10-18)
> 后续增量:宿主产物提交/补偿/恢复已在组件 0.5.0 接入[原生写回事务](沙盒原生写回事务-2026-10-18.md)。本文件保留读取桥交付时的范围与证据;读取桥本身仍只读,宿主原生事务不等于完整文件写入调用桥已经完成。
## 当前交付
在[首批接入](沙盒完整接入首批实施-2026-10-18.md)上,P1/P3 已新增真实的工作区只读文件桥:模型可显式选择,Node 同步/异步客户端通过已绑定 Job 的 stdio 调用原生文件读取,操作结果在回复前落盘,断线后可以查询。完整目标继续按[计划](沙盒完整接入计划-2026-10-18.md)及[验收矩阵](沙盒接入验收矩阵-2026-10-18.md)推进。
本批另修复了 AppContainer 私有 TEMP 目录,并补充[显式 Node 同步子进程适配器](沙盒Node子进程与临时目录修复-2026-10-18.md)。该适配器不依赖读取桥,也不增加宿主文件/网络权限。
这是一套 YCode 自有实现。本批没有新增 WorkBuddy 取证,也没有原版等价结论。`filesystem-broker` 整体能力仍不可用;独立的 `workspace-read-bridge` 只声明下述读取子集。
## 调用与执行点
```json
{
"command": "node inspect.cjs",
"runtimes": ["node"],
"bridge": "workspace-read"
}
```
```js
const workspace = require(process.env.YCODE_SANDBOX_BRIDGE).connectSync()
try {
const bytes = workspace.readFileSync('src/example.ts')
const stat = workspace.statSync('src/example.ts')
const entries = workspace.readdirSync('src')
console.log(bytes.length, stat.kind, entries.length)
} finally {
workspace.close()
}
```
异步使用 `connect()` 的 `readFile`、`stat`、`readdir`、`query` 和 `close`。一个 Job 同时只应有一个 stdio 客户端;客户端独占该进程 stdin,不能同时作为交互输入。它不覆盖 Node 标准 `fs`,也不会将普通绝对路径自动映射到宿主。`stat`/`readdir` 返回桥的有界 JSON 元数据,不声称兼容完整 Node Stats/Dirent。
| 源码 | 职责 |
| --- | --- |
| `native/sandbox/filesystem-read.h` | 宿主专用原生读取器;持有祖先/目标句柄,校验卷、工作区身份和最终路径;读取、元数据、句柄目录枚举 |
| `native/sandbox/runner.cpp` | 拒绝容器内部调用宿主读取器;继续保持 AppContainer/Job 与默认禁网;转发有界 stdio 回复 |
| `native/sandbox/node-bridge.cjs` | 自有同步/异步客户端、分块摘要检查、异步队列和退出清理 |
| `src/main/sandbox/filesystem-reader.ts` | 策略及拒绝路径、实际原生调用、取消/时限、报告解析与最终名称复核 |
| `src/main/sandbox/operation-broker.ts` | 严格帧、按 Job 绑定的归属、请求/快照上限、状态日志、结果查询与协议失败清理 |
| `src/main/sandbox/maintenance.ts` | 重启/断线后的持久结果查询;未完成记录返回 unknown |
客户端随原生组件构建、打包;manifest 记录 runner/header/client 源码摘要及客户端摘要。选择文件桥时,客户端摘要不符会拒绝启动,不查找其他宿主模块。
## 实际边界
1. 当前仅支持 Windows x64、本地固定 NTFS 卷。读取器从卷根开始逐级打开目录,保留整个祖先链,并拒绝 reparse point、设备和多重硬链接。句柄只共享读取,已有可写句柄导致 EBUSY;读取期间禁止对应对象的改写、删除或重命名,不放宽共享模式重试。
2. 每个 Job 固定工作区卷/文件 ID。每次调用重新核对根身份;同路径替换成另一个目录不会取得原授权。目标最终规范名称来自句柄,与根的 volume GUID 路径比较,并再次经过敏感名称/拒绝前缀规则。宿主状态目录和副本排除目录不向桥开放。
3. 相对路径严格拒绝 `..`、空组件、绝对路径、ADS/UNC/设备、尾部空格/点及敏感文件。目录列表会过滤敏感项和链接。普通 `fs` 及原生后代仍受原有 AppContainer 权限约束。
4. 调用归属来自主进程保存的 session/run/tool-call、策略版本/内容指纹及本次 sandboxRunId。子进程不能提交替代归属。该权限范围属于整个 Job;它没有独立的逐后代 PID 认证,不能据此声称更细粒度的进程规则已完成。
5. 单次读取先在原生句柄下取得完整有界内容,再以 16 KiB 块回复。后续块来自同一只读内存快照,宿主外部编辑不会拼接出混合版本。每块核对身份/摘要,客户端完成后再核对完整 SHA-256。
6. 桥不实现宿主写入/删除/联网。普通命令产物仍由副本差异、审批和原有事务写回;原有事务的宿主目标约束没有因本批读取器而自动升级。
| 限制 | 当前值 |
| --- | --- |
| 请求帧/回复值 | 8 KiB / 24 KiB |
| 每 Job 并发/总请求 | 8 / 1024 |
| 异步客户端总排队 | 32,向宿主最多发送 8 项 |
| 单文件读取 | min(策略 fileBytes, 8 MiB) |
| 读取快照 | 最多 8 个,总容量 min(策略 bytes, 16 MiB),空闲 60 秒失效 |
| 目录枚举 | 最多 128 个原始条目;序列化超限则拒绝,不截断后冒充完整结果 |
| 原生读取器/代理截止 | 4 秒 / 5 秒;原生另监控父进程退出 |
## 操作生命周期和查询
请求包含 version、UUID requestId、operation、args。未知/伪造归属字段、重放、超限或截断控制帧导致整个通道失败并取消 Job;无权操作和路径错误返回有界错误。普通命令输出可继续流式传递,不使用控制帧上限截断普通长行。
每个操作保存 pending/running 和最终 succeeded/denied/failed/cancelled 状态。最终记录在回复之前写入并刷新;只记录身份和元数据,不保存文件内容或活跃快照 token。回复丢失不会把已经成功的读取改成未执行;在运行中中断而没有最终状态的记录查询为 unknown。
```powershell
npm run sandbox:maintain -- broker-operation RUN_UUID REQUEST_UUID --state-root 'C:\absolute\userData\sandbox'
```
真实 preload 提供 `window.ycode.app.sandboxBrokerOperation(runId, requestId)`;仅允许自有顶层应用页面,校验 UUID、记录类型以及 session/run/tool-call/策略归属。不能由另一个 Job 的 requestId 替换目标。查询接口不接受任意路径或 stateRoot。
## 验证与未完成项
测试使用隔离工作区、状态目录和 Electron userData,不改用户生产策略/运行时,不重启用户当前窗口。
| 验证 | 已确认结果 | 报告 |
| --- | --- | --- |
| 实际读取桥 | 21/21;同步/异步、二进制分块、根替换/junction 竞争、后代绕过拒绝、缓存一致性、归属/重放和策略撤销 | `.tmp/sandbox-read-bridge-fcJNOa/report.json` |
| 协议故障 | 15/15;长普通输出/分帧、快照/并发上限、伪造/重放/超限/截断、取消、丢失回复和 unknown 结果 | `.tmp/sandbox-read-protocol-faEL5f/report.json` |
| 运行时兼容 | 20/20;Node 同步进程和 Python TEMP/0o700、真实子进程、原生拒绝及三个资源被篡改时 fail closed | `.tmp/sandbox-process-9EyXTm/report.json` |
| 既有基础 | 原生 52/52、策略 38/38、旧 stdio 10/10 | `.tmp/sandbox-regression-GJmnRy/report.json`、`.tmp/sandbox-policy-0ZW6xX/report.json`、`.tmp/sandbox-stdio-AsODUy/report.json` |
| 工具链/依赖 | 24/24;Node/npm、Python/pip 和工作区绑定的只读依赖层仍可用 | `.tmp/sandbox-toolchain-IvxSbF/report.json` |
| 生产整应用 | 26/26;真实 main/preload/renderer、OMP RPC、localhost 模型;显式同步进程预加载、审批写回、读取桥及其持久查询 | `.tmp/sandbox-app-1LP378/report.json` |
最终 8 组共 206 项通过,完整生产构建/类型检查通过。原生资源更新为 0.2.0,协议仍为 1;三个适配资源均纳入安装状态校验,缺失或摘要不符时不显示 installed。源码/组件身份与报告摘要另存 `.tmp/sandbox-read-integration-validation.json`。文件格式检查已按本仓库 Windows CRLF 配置验证;并行模块未做全库换行重写。
尚未完成:宿主写入的原生事务代理、描述符/追加等完整文件语义、Python/Shell 桥、逐后代认证、标准用户与其他 Windows/文件系统矩阵、安全中心页面及原版 1:1。目录/文件元数据写回、磁盘硬限额、长期工作区、Git、受控出站、MCP/ConPTY 等继续按总计划推进。
## 原生 API 依据
[CreateFileW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew) 规定共享模式及打开 reparse 对象的行为;[GetFinalPathNameByHandleW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew) 提供句柄规范路径和 volume GUID 形式;[FILE_ID_BOTH_DIR_INFO](https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_both_dir_info) 定义句柄目录枚举记录。这些文档用于解释具体执行点;当前支持程度由自有实际攻击/故障测试记录限定,不由 API 名称推定完整文件代理安全。
+313
View File
@@ -0,0 +1,313 @@
# YCode 沙盒完整接入计划(2026-10-18)
> 最新增量:组件 0.6.0 已接入[长期会话草稿](沙盒长期工作区接入-2026-10-18.md):显式 persistent 命令保留同一物理工作区及会话私有 Git,支持三方同步、查看/审批提交/归档丢弃/中断恢复和所属会话只读状态。默认快照路由保留,普通文件工具仍作用宿主;长期进程/可写依赖、宿主 Git 元数据、完整文件代理和阶段整体验收仍未完成。[0.5.0 原生宿主事务](沙盒原生写回事务-2026-10-18.md)继续用于提交/补偿/恢复;两步替换有短暂缺名窗口。
> 以上是 0.6.0 时期的历史摘要。当前 Git 已在 0.7.0 一次性接入宿主仓库、远程及原生提交事务;Git 结论以[完整接入记录](沙盒Git完整接入-2026-10-18.md)和[专用矩阵](沙盒Git接入验收矩阵-2026-10-18.md)为准。
状态:总体沙盒接入仍在实施;本批 **Git 工程接入已完成 Windows x64 验收范围**,见[完整 Git 接入](沙盒Git完整接入-2026-10-18.md)与[Git 专用矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。P0/P1、P2 离线工具链/只读依赖层、Git 宿主导入/写回/远程和 P3 目录事务/原生只读桥/原生宿主写回事务已有工程实现;非 Git 的联网/程序/注册表、MCP/终端、安全中心 UI、标准用户/安装发布和原版等价仍按本文目标推进。首版实现和既往测试结果见[维护基线](自有沙盒首版实施-2026-10-18.md),原版材料的证据范围见[参考评估](沙盒参考代码评估-2026-10-18.md)。
## 1. 目标、范围与完成口径
在现有自有 AppContainer/Job 后端上,完成 Windows x64 下的工具链、文件代理、权限策略、连接器、交互终端、安全中心和维护发布接入。YCode 自行构建、发布、诊断和恢复,不依赖 WorkBuddy 的安装、服务或二进制。
完整接入分开记录三个验收结果:
| 结果 | 完成条件 |
| --- | --- |
| 工程接入完成 | 本计划的运行时、文件、联网、程序/注册表策略、本地扩展、终端、UI 和维护入口均接真实后端;支持范围和宿主可信组件逐项可查询 |
| 边界验证完成 | 每项允许/拒绝规则在真实进程及后代中成立;故障、并发、恢复和安装包场景通过;专项安全审查的问题已关闭或列明未解决项 |
| 原版等价验收完成 | 实际版本、REA Evidence ID、同场景输入/结果和 UI 证据完整;未知原版行为已补证;差异逐项关闭 |
前两项不能替代第三项。仍有必要场景未验证、后端缺失、只返回 mock 的功能时,相应项目保持未完成。
本轮主平台为 Windows x64。Windows 版本、最低支持版本、标准用户/管理员权限的能力差异在 P0 固定。macOS、Linux 和 ARM64 作为独立后端列入平台矩阵,在完成各自原生后端前继续明确返回不可用,不能由 Windows 的验收结果代替。
可信控制组件包括 Electron main、策略/文件代理和负责供应商通信的受控内核部分。模型生成的命令、项目代码、安装脚本、本地 MCP 程序及其子进程按不可信执行对象处理。OMP 的凭证/联网需求要单独拆解;仅保留 `--no-tools` 不能证明内核整体已隔离。
## 2. 当前可复用基线与缺口
| 能力 | 当前基线 | 本计划补齐目标 |
| --- | --- | --- |
| 原生启动 | 独立 AppContainer、SID 验证、Job、环境净化、超时/取消/父进程退出清理 | 支持明确的运行时与长期进程配置,启动前能力检查和分项自检 |
| 工作区 | 默认每命令快照;显式会话草稿保留物理身份、冲突检查和提交/丢弃/恢复 | 可写依赖层、长期进程/稳定执行路径、完整文件工具草稿路由及明确路径映射 |
| 文件写回 | 文件增改删、审批、摘要冲突检查、独立事务备份与恢复 | 原生文件代理、目录与元数据语义、批量删除、调用中授权和结果查询 |
| 工具链 | cmd/Windows PowerShell、Node/npm、Python/pip、内置 Git、只读依赖层;Git 0.7.0 已接宿主历史/索引、元数据事务和受批远程 | Git 仍需完整命令/版本/平台兼容;可写依赖、普通命令网络和受控构建工具链独立验收 |
| 网络 | 模型命令统一禁网 | 默认禁网、按范围授权联网、域名/端口/地址约束、受控本地开发服务 |
| 程序/注册表 | 依赖隔离容器及系统权限;没有自有细粒度规则引擎 | 明确的程序/运行时/注册表规则及可证明的执行点 |
| 其他入口 | MCP、手动终端、OMP 各有已有信任或调用流程 | 统一能力与权限声明,适用的本地执行进入同一隔离后端 |
| UI/维护 | 状态 API、自检、审计、事务恢复、孤儿清理 CLI | 安全中心真实数据与操作、多窗口同步、自动保留清理、升级恢复及安装包验收 |
首版记录的原生 52 项和整应用 10 项是计划制定时的基线;最新复核与新增专项见首批实施记录。新增能力需增加对应实测,测试数量增加本身不作为完成条件。
## 3. 接入架构与共用契约
```mermaid
flowchart TD
A[会话工具 / 本地 MCP / 交互终端] --> B[主进程权限与执行路由]
U[权限菜单 / 安全中心] --> B
B --> P[版本化策略与授权]
B --> R[自有原生执行器]
R --> W[隔离工作区与受控运行时]
W --> F[经过认证的文件/网络代理请求]
F --> P
P --> T[原生目标校验与文件事务]
B --> J[运行审计 / 自检 / 恢复与清理]
T --> J
J --> U
```
### 3.1 主上下文先固定的决定
1. 延续 AppContainer/Job 和副本执行基线。工具链先在副本中离线可用,联网和调用中访问宿主由独立能力逐项开放。普通宿主 shell 继续对应显式的完全访问模式。
2. 策略由主进程持有,绑定工作区、会话、run、tool call、进程实例和策略版本。Renderer 和子进程提交的标识作为待核对字段,授权以主进程保存的归属与经过认证的连接为准。
3. 宿主真实文件变更统一进入事务;命令、文件工具、代理和 UI 恢复入口共用同一事务语义。允许写副本不等于批准写回宿主。
4. 语言 shim 负责兼容与请求转发。文件、进程和网络边界必须由宿主代理或原生约束实现,并用未加载 shim 的原生探针验证。
5. 全局默认策略、工作区授权和单次授权分层存储。规则修改生成新版本;旧审批不得自动扩大到新策略、替换后的程序或新目标。
6. 原版的功能与交互通过 REA 对齐。注入器、驱动或其他系统组件是否需要引入,由证据和可行性原型决定,单独记录构建、部署和维护代价。
### 3.2 先定义的共用数据
以下是待设计契约,尚未加入当前代码:
| 契约 | 必需内容 |
| --- | --- |
| 能力与健康状态 | 组件身份、协议、平台、文件/程序/网络/注册表/终端能力、每项自检结果与时间、失败原因、是否需要额外系统组件 |
| 执行配置 | 入口类型、真实工作区、会话/run/tool call、运行时身份、输入与缓存声明、策略版本、时限和资源上限 |
| 决策与授权 | allow/ask/deny、原因、目标、操作、范围、过期时间、一次性 nonce、策略/进程代次、撤销状态 |
| 操作与结果 | request ID、事务 ID、目标身份、等待/执行/已提交/已拒绝/已取消/结果未知/需恢复;支持查询已提交结果 |
| 运行事件 | 有序序号、入口归属、阶段、审批与恢复关联、脱敏错误;支持主进程重启后的记录重建 |
`installed` 继续只说明组件校验通过;实际可用性来自分项自检。IPC 断线发生在提交之后时,应允许查询持久化结果,结果未知的非幂等操作不自动重试。
## 4. 分阶段实施
### P0:固定证据、能力清单与高风险可行性
目标:明确完整范围和系统实现前提,尽早暴露会改变架构的阻碍。
- 建立验收矩阵,逐项记录当前实现、拟接入入口、原版证据、平台、原生执行点与测试场景。
- 沿用已有 REA 静态记录;补采原版安全中心、shim 完整接口与失败分支、策略 JSON 语义、原生程序/网络/注册表/删除行为。记录实际版本及文件身份,资料标注的 5.8.6 不直接当作已证版本。
- 对四项机制做隔离原型:原生文件目标校验与并发替换防护、允许代理但拒绝旁路的联网、本地交互终端受限启动、进程树内的程序/注册表规则。原型输出必须说明普通用户权限下的支持程度。
- 明确磁盘硬限额、系统组件、签名与升级要求;如某能力需额外安装或管理员权限,形成单独部署方案,不能用配置字段假装已实现。
- 测量现有副本、启动和清理开销,固定小/中/大项目夹具和后续性能阈值。
交付:能力/证据/测试矩阵、原型报告、协议设计、依赖与阻碍清单。原版证据缺失时可继续独立的 YCode 工程工作,相应等价项保持未验证。
验收:每项声称的能力有真实执行点、支持范围与验证办法;高风险机制至少有一组允许、一组拒绝和一组故障测试。
### P1:统一策略、授权、生命周期与 IPC
目标:所有入口使用一致且可审计的权限语义,为后续模块固定接口。
- 将固定在 `policy.ts` / `service.ts` 中的默认项整理为版本化策略,增加输入验证、确定性规则优先级、配置迁移与安全默认值。
- 扩展 shared 类型和主进程路由;将现有 `InteractionBroker` 接为授权界面,覆盖取消、超时、撤销、策略变化和跨窗口恢复。
- 定义原生代理的认证与协议:连接绑定实际进程身份/实例,限制消息大小、并发和队列;同步/异步请求都具备背压、截止时间、断线与结果查询语义。
- 隔离运行的状态、租约和事务记录由宿主管理;记录包含启动前、运行、审批、提交、清理与恢复阶段。
文件边界:`src/shared/sandbox.ts`、`src/main/sandbox/{configuration,policy,service}.ts`、拟新增 `policy-store.ts` / `protocol.ts` / `broker.ts`;shared IPC、preload 和 main 接线由主上下文统一修改。
验收:越权窗口/会话、伪造归属、过期授权、授权重放、并发撤销、请求洪泛及提交后断线均得到明确结果;原生组件不可用时默认执行继续拒绝。
### P2:工具链、依赖缓存与项目工作区
目标:首先让真实 Node、Python 和 Git 工作流在禁网沙盒内可用。
当前增量:0.6.0 已实现显式长期草稿、项目/会话/根身份/策略绑定、三方同步、审批提交、丢弃与恢复,以及草稿专用私有 Git。正常成功命令保持同一物理目录;失败恢复可重建。只读依赖临时注入可用,可写依赖、稳定绝对执行路径和长期进程仍未完成;普通文件工具没有切换为草稿视图。验证范围见[接入记录](沙盒长期工作区接入-2026-10-18.md),P2 整体保持进行中。
- 增加运行时清单:版本、来源、摘要、架构、许可、运行所需文件及环境变量;执行前校验真实文件身份,避免依赖宿主任意 PATH。
- Node/npm、Python/pip、Git 分批接入;构建器、编译器、原生模块、虚拟环境和子进程按同样标准增加支持矩阵。
- 依赖按工作区与锁文件、运行时/ABI、架构和策略版本隔离。共享下载缓存、每项目依赖层和运行中可写目录分别管理;包内容始终作为不可信输入。
- 增加沙盒自有依赖层与受控复制/复用方案;校验链接、敏感内容和完整性,避免直接给原工作区或全局包目录增加访问权限。
- 定义命令间持续存在的沙盒工作区、同步边界和变更查看/提交/丢弃流程;排队处理同工作区命令,并保留外部编辑冲突检测。
- 明确副本路径、工作区路径、临时目录、缓存、产物与错误输出的映射;实际绝对路径兼容逐个验证,暂不以字符串全局替换代替。
- Git 覆盖只读状态/差异、受控本地变更及仓库数据的专门规则;宿主 Git 凭证、全局配置和任意 hook 不自动进入沙盒。
文件边界:拟新增 `src/main/sandbox/toolchains.ts`、`dependency-cache.ts`、`workspace.ts`;`native/sandbox/runner.cpp` 的启动协议及 `service.ts` 适配由主上下文集成。
验收:离线 Node 项目构建/测试、Python 导入/测试、Git 工作流、运行时后代程序、中文及空格路径、依赖变化与损坏、跨项目缓存隔离、敏感环境变量和宿主路径拒绝。联网安装在 P4 完成后补验。
### P3:原生文件代理、目录事务与安全删除
目标:补齐副本外受控文件操作和完整的文件生命周期。
- 建立原生文件代理,以已授权目录/文件身份约束实际操作;P0 原型验证通过后再选择句柄与路径处理方法,并验证最终目标没有逃逸。现有 realpath/摘要检查继续作为普通冲突保护。
- 对齐读取、写入、追加、截断、rename、copy、文件描述符和同步/异步错误语义;逐步加入自有 Node 适配和 Python/Shell 桥接。原版源码只按 REA 取得的接口和证据分析。
- 扩展事务类型,覆盖空目录、递归目录、目录删除/重命名、二进制和文件所需元数据;定义只读属性、链接、ACL、时间戳、ADS 等的支持或明确拒绝行为。
- 增加单项及批量删除预览、实际清单摘要、阈值策略、授权绑定和独立备份;大批次审批期间清单变化使授权失效。
- 提供事务内删除恢复;Windows 回收站行为作为单独的执行器与验收项,具体交互和语义由 REA 补证。
- 将聊天“恢复此版本”与独立事务建立映射,避免旧 before/after 和新日志对同一变更重复恢复;远端副作用不混入本地文件恢复成功结果。
文件边界:`native/sandbox/` 内新增代理模块,`src/main/sandbox/transactions.ts` / `policy.ts`、拟新增 `filesystem-broker.ts`、自有语言适配目录;`src/main/agent/tools/index.ts` 和 `src/main/workspace-revert.ts` 由主上下文对接。
验收:目录增删改/恢复、批量删除、部分失败补偿、审批期间外部修改、并发替换路径、junction/符号链接/硬链接、设备/ADS/UNC 路径、跨工作区授权混用、断线后结果查询和跨进程恢复。纯 JS 路径校验通过不能代替原生边界测试。
### P4:受控联网、程序与注册表规则
目标:在保持默认拒绝的前提下,完成细粒度授权和真实系统执行约束。
- 网络策略覆盖协议、目标主机、端口、地址范围、代理和授权时效;按 P0 原型选定能约束原生进程及后代的执行方案。
- 增加受控出站代理/下载入口;验证未走代理、直连 IP、DNS/重定向、IPv4/IPv6、localhost/局域网和非 HTTP 程序的行为。只设置代理环境变量不算网络策略完成。
- 在受控联网环境完成 npm/pip 安装;生命周期脚本仍在隔离进程内运行。缓存发布必须完成完整性/路径校验,私有仓库令牌按最小范围使用并脱敏保存。
- 支持绑定特定沙盒实例的本地开发服务和预览;限定端口、来源和生命周期。预览页面不能取得主进程 IPC 或模型凭证。
- 程序策略区分可信工具链和项目生成的待执行产物,绑定实际可执行文件、参数、脚本/运行时与策略版本;覆盖后代启动,不能只检查主进程程序名称。
- 注册表建立逐项允许/拒绝矩阵,包括程序所需系统读取、容器内状态与宿主用户配置保护;宿主读写通过明确授权的代理实施,直接调用路径用原生探针验证。
文件边界:拟新增 `src/main/sandbox/{network,program-policy,registry-policy}.ts` 及对应原生模块;浏览器预览入口和包管理器接线由主上下文集成。
验收:获准请求真实到达受控服务、未获准请求在接收端没有流量;改变代理/目标/程序身份后授权失效;安装脚本、编译产物和原生网络程序均不能绕过限制;注册表测试只使用自有测试键并清理。
### P5:MCP、内核与交互终端接入
目标:将模型可触发的本地执行纳入统一策略,明确可信组件和远端边界。
- 本地 stdio MCP 通过自有执行适配器启动,继承对应工作区、运行时、资源和网络策略;保持 stdio 协议、连接代次、工具清单和取消语义。需要凭证的服务逐项声明最小授权。
- MCP 长期进程明确状态持久化和文件提交时机;配置/信任变化后停止旧进程树,旧连接和旧授权不能继续使用。
- 远端 MCP/连接器保留服务端外部副作用边界,核验 origin、凭证范围、逐次审批和取消;结果未知时不自动重试。界面分别显示本地隔离与远端调用权限。
- 审计 OMP 启动环境、扩展、配置、会话文件、网络和子进程。将凭证/供应商通信与项目代码执行分离;能受限运行的部分进入执行器,必须保留宿主权限的部分列出职责和允许路径/网络。
- 增加隔离终端配置和明确的宿主完全访问终端配置;隔离终端的长期工作区、写回审批和后代清理复用现有服务。
- 基于 P0 原型接入受限 ConPTY:输入/输出、resize、信号、会话迁移、窗口退出及恢复均需实际验证。界面显示终端权限和未提交变更,避免静默切换。
- 枚举技能/专家包、项目操作、预览及其他模型可达的本地执行路径,检查是否存在绕过公共策略的宿主 spawn。
文件边界:`src/main/capabilities/{mcp,runtime}.ts`、`src/main/omp.ts`、`src/main/terminal-host.ts`、对应 shared 类型与测试。跨模块路由、终端模式和内核信任划分由主上下文决定。
验收:实际 Node/Python MCP、恶意测试 MCP、配置变化、凭证过滤、连接中取消;真实 OMP 供应商夹具与工具回调;真实交互终端、跨会话/窗口归属、进程树退出和副本提交。现有调用审批不能代替进程隔离验证。
### P6:权限菜单、安全中心与通知
目标:用户能查看真实能力、调整有效策略、处理审批并恢复文件。
- 先通过 REA 补齐原版页面、控件、默认值、持久化、错误和动画证据;最终分区、字段、措辞和交互以已确认的原版为准。
- 定义后台可查询/操作的数据:分项健康与自检、策略与授权、活动进程、运行审计、文件变更/恢复、缓存与遗留资源。后端先具备能力,页面逐项接入。
- 扩展 main/preload/shared IPC,按窗口、会话和工作区校验操作归属;UI 不接受任意 stateRoot 或未经验证的宿主路径。
- 权限菜单、聊天审批、终端权限和安全中心读同一策略版本;变更/撤销实时通知相关窗口,重启后保留。
- 接入真实审批/失败/恢复事件,替换与本功能有关的示例通知;显示不可用原因、提交结果、审计失败及需恢复事务。
- 完成浅/深色、窄窗口、滚动、键盘、hover/focus/disabled、弹层和动效对照;给运维状态提供可操作错误入口。
文件边界:拟新增 `src/renderer/src/components/SecurityCenterSettingsPanel.tsx` 及配套模块;`SettingsDialog.tsx`、权限控件、通知面板、shared/main/preload 接线由主上下文安排。
验收:所有可操作控件都有真实后端效果;修改后立即影响对应测试命令;重启/多窗口同步、审批撤销、恢复冲突与后端不可用可复现;原版视觉与行为单独留 REA 对照证据。
### P7:维护、资源限制、升级与发布
目标:能够持续运行、升级和独立维护。
- 备份、审计、工作区、缓存分别制定容量/保留/清理策略;活动租约、待恢复事务与其备份受保护。清理支持预览和可查询结果。
- 资源限制覆盖进程、内存、时间、输出、存储和并发;区分快照检查、运行时监控和真正的硬限额。磁盘硬限额方案以 P0 可行性结果为前提。
- 固化事务落盘顺序、日志损坏检测与启动恢复;补充提交/补偿中断、早期启动崩溃及清理失败处理。停电级耐久性使用虚拟机断电/重启场景单独验收。
- 协议、状态格式、策略和运行时清单版本化;升级前识别活动实例,迁移失败保留可恢复数据,旧版读取不兼容状态时明确拒绝。
- 发布工件记录源码、构建器、SDK、二进制和第三方运行时身份;完善校验与签名方案。当前 exe 与相邻 manifest 的摘要匹配只是完整性检查,发布真实性需独立验证链。
- 在无 MSVC、无 WorkBuddy 的干净 Windows 机器测试安装包;覆盖标准用户/管理员差异、安装/更新/卸载、路径含中文、组件损坏、系统能力不可用及杀软兼容。
- 完成维护手册、故障诊断、日志脱敏、漏洞响应与专项安全审查。
文件边界:`scripts/build-sandbox.mjs`、`scripts/sandbox-maintain.ts`、`src/main/sandbox/maintenance.ts`、原生资源清理、发布配置和维护文档;打包/升级策略由主上下文集成。
验收:长时间反复运行无 profile/盘符/进程泄漏;故障后可诊断和恢复;升级、回退与保留策略不会误删待恢复数据;安装包可脱离开发机独立工作。
### P8:整应用与原版同场景最终验收
目标:关闭功能、边界、UI 和交接的全部必要项目。
- 使用生产 main/preload/renderer、真实 OMP RPC 和受控模型完成工具链、MCP、终端、联网、审批、恢复与通知链路。
- 在干净安装包和开发构建中复核关键边界,明确测试运行时、OS、权限、版本、策略及证据位置。
- 对 WorkBuddy 逐场景采集 REA 允许/拒绝、审批时机、错误与恢复、设置保存、页面状态;相同输入对照 YCode,逐项记录差异。
- 关闭对功能或安全声明有影响的未解决项;未知或未支持能力保持未完成,形成下一轮明确任务,不能标记全量接入/1:1 完成。
交付:最终能力矩阵、测试与 REA 报告、发布工件身份、维护文档和遗留项。三个完成口径分别给出结论。
## 5. 顺序、依赖与阶段交付
| 阶段 | 前置 | 可交付结果 |
| --- | --- | --- |
| P0 | 首版基线 | 冻结的范围、证据矩阵和高风险原型 |
| P1 | P0 的契约与机制决定 | 共用策略、授权与协议;保持现有默认隔离行为 |
| P2 | P1 | 可运行真实离线项目的沙盒 |
| P3 | P1;与 P2 按稳定接口协作 | 完整文件/目录事务与安全删除 |
| P4 | P0 机制原型、P1、P2、P3 | 可控安装依赖、联网与程序/注册表策略 |
| P5 | P2、P3、P4 | MCP、内核执行边界及交互终端统一接入 |
| P6 | P1 可开始契约/UI准备;完成依赖 P4/P5/P7 与原版页面证据 | 有真实数据与操作的安全中心 |
| P7 | P1 可开始维护工作;完成依赖 P3/P4/P5 | 可升级、可诊断、资源有界的发布版本 |
| P8 | P0~P7 必要项目完成 | 整应用、安装包、边界与原版等价分别验收 |
推进顺序:P0 → P1 → P2/P3 → P4 → P5 → P6/P7 → P8。P6 的原版采集和 P7 的维护设计可提前进行。P2 离线交付不等待联网能力;联网安装通过 P4 后才算对应工作流完成。
## 6. 必测场景与验证入口
| 编号 | 场景与通过条件 | 归属 |
| --- | --- | --- |
| A01 | 缺失/损坏/错误协议后端拒绝默认执行,不普通 spawn 回退 | P1/P7 |
| A02 | 跨会话/窗口、授权重放、策略变化、请求洪泛、取消与断线正确处理 | P1 |
| A03 | Node/Python/Git 与后代在隔离环境真实运行;缺工具链明确报错 | P2 |
| A04 | 依赖隔离、缓存完整性、安装脚本和恶意包不能取得宿主凭证/写权限 | P2/P4 |
| A05 | 文件、目录、rename/copy/追加/描述符语义及二进制写回/恢复 | P3 |
| A06 | 链接/ADS/设备路径、目录竞争替换、审批后变更拒绝越权操作 | P3 |
| A07 | 批量删除清单、部分失败补偿、恢复冲突、提交后断线可查询 | P3 |
| A08 | 网络允许/拒绝有接收端观测;直连、代理绕过、重定向与 IPv6 受约束 | P4 |
| A09 | 程序替换、运行时/参数变化、后代启动与原生注册表探针得到正确决策 | P4 |
| A10 | 本地开发服务只暴露授权入口,退出后关闭,无主进程权限泄漏 | P4 |
| A11 | MCP 连接/调用/取消、配置变化、长期进程状态和进程树清理 | P5 |
| A12 | OMP 的模型通信、会话恢复、扩展和宿主工具回调没有权限旁路 | P5 |
| A13 | 真实交互终端 I/O、resize、窗口/会话迁移、权限显示与事务提交 | P5 |
| A14 | 安全中心每项操作真实生效,多窗口/重启一致,失败和需恢复状态可操作 | P6 |
| A15 | 磁盘/内存/进程限制、崩溃/断电恢复、清理与升级不丢待恢复数据 | P7 |
| A16 | 干净安装包、标准用户与管理员、支持 OS、中文路径及真实整应用链路 | P7/P8 |
| A17 | 同版本/同场景 WorkBuddy REA 对照;视觉、行为、错误与恢复差异已关闭 | P8 |
现有验证入口继续使用:
```powershell
npm run test:sandbox
npm run test:sandbox:app
npm run sandbox:maintain -- status
npm run sandbox:maintain -- self-test
npm run smoke
npm run test:conversation-ipc
npm run test:omp
```
按模块增加策略/代理、工具链、网络、扩展/终端、UI 和打包场景的独立测试入口;命令名在新增 package scripts 时确定。整应用测试已包含生产构建,其他检查按本批修改范围选取。仅文档修改不重复运行整应用测试。
所有破坏性/拒绝/恢复测试使用自有临时工作区、假凭证、受控注册表键和本机服务;原版取证通过 REA,YCode 测试使用真实自己的执行链路。具体报告区分原生实测、整应用实测、UI mock 和尚未验证。
## 7. 实施分工与文件归属
主上下文负责需求/证据结论、架构、shared 契约、跨模块路由、策略优先级、信任划分、package/build 配置、冲突解决与最终验收。保留已有并行 UI、上下文用量和其他用户修改。
接口冻结后可按以下边界委派,实际委派时再给本批精确文件白名单与验证命令:
| 工作包 | 允许边界 | 验收责任 |
| --- | --- | --- |
| A:原生执行与文件代理 | `native/sandbox/` 的指定源文件及原生探针;不改 shared/IPC/业务路由 | 原生 token/Job、文件目标、程序/网络/注册表机制与故障证据 |
| B:运行时与项目工作区 | 指定的 toolchains/cache/workspace 模块与聚焦测试;不改原生协议和通用事务 | 离线/联网项目、依赖身份、跨项目隔离与资源限制 |
| C:安全中心与 UI 验证 | 已固定的页面/样式及 UI 测试;不改 main/preload 或自己定义授权语义 | 已有 REA 视觉交互对照、真实 IPC 消费与全部 UI 状态 |
文件/策略事务与 MCP/终端跨模块改动按阶段再拆包;未冻结协议时由主上下文推进。所有子任务报告修改文件、关键决定、验证命令与结果、剩余限制;主上下文审查后接收。首批因子任务执行环境不可用,由主上下文直接实现和验收,未接收子任务代码。
## 8. 主要依赖与停止条件
| 项目 | 必须解答的问题 | 未解决时的处理 |
| --- | --- | --- |
| 原版证据 | 安全中心内容、原生规则与删除行为的实际版本/运行语义 | 记录所需 REA 输入和场景;独立工程可继续,等价验收保持未完成 |
| 原生文件代理 | 能否在实际操作期间约束目标身份并抵御并发路径替换 | 宿主代理保持关闭,继续已验副本模式,相应能力不发布 |
| 联网/程序/注册表机制 | 原生进程与后代是否可旁路;标准用户部署是否可行 | 使用默认拒绝;补独立系统后端或声明不可用,不伪装为已生效规则 |
| 长期进程与终端 | 容器/ConPTY、stdio、状态与写回是否兼容 | 保留明确的入口权限状态;未通过的隔离配置不开放 |
| 资源/恢复/发布 | 磁盘硬限制、持久性、签名与干净机器是否满足要求 | 区分监控与硬限制;发布和完成结论保留未解决项 |
上述是实施中的能力门槛,不构成本轮额外安装、权限变更或系统配置操作。新组件的部署方式在 P0 形成具体方案。
## 9. 第一批具体工作与本轮状态
下一次实施从以下四项开始:
- [x] P0-01:建立 A01~A17 验收矩阵,登记每项原版证据、平台和真实执行点。
- [ ] P0-02:完成原生文件代理、受控联网、程序/注册表规则与受限终端的可行性报告。
- [ ] P1-01:固定能力状态、策略版本、执行配置、授权和结果查询类型,主上下文审查接口。
- [ ] P1-02:先将现有命令/文件工具接入新契约,复跑原生与整应用基线;通过后再打开 P2/P3。
计划制定轮次完成了方案和交接索引;随后用户批准持续接入,当前实现、验证与剩余项以首批实施和验收矩阵为准。未强制重启用户应用或变更其生产沙盒状态。
## 10. 参考与证据范围
- [首版实现与维护基线](自有沙盒首版实施-2026-10-18.md):当前实现、测试产物、平台与能力限制。
- [参考材料评估](沙盒参考代码评估-2026-10-18.md):REA 来源和原生未知项。沿用 `ev_74bf702462bc37612d2a68ea2e1c93b30b77dd5d99eaf1e961f1ab36d626c5e3`、`ev_2860ca8fafaf310ee1adc2ebe31530c868da0ecbb41a6f451e8e8e86b447a078`;这些是静态脚本证据,不证明原版运行效果。
- [安全中心待拆解](安全中心模块拆解-2026-10-17.md):导航证据来自原版 5.8.3,页面内容与行为仍待 REA 采集,不预设原版具有本计划中的全部维护分区。
- [本仓库原生维护说明](../native/sandbox/README.md):当前 API 使用和维护入口。本计划涉及的新增系统机制仍需 P0 验证。
@@ -0,0 +1,70 @@
# 沙盒完整接入:首批实施(2026-10-18)
> 后续已增加[真实只读文件桥](沙盒只读文件桥接入-2026-10-18.md)。下文是首批历史范围;当前桥已开放有界读取,宿主写入/联网代理仍未开放。
## 本批范围与状态
用户批准[完整接入计划](沙盒完整接入计划-2026-10-18.md)后,已开始 P0/P1,并落地 P2 的离线运行时/只读依赖层和 P3 的目录事务子集。所有阶段的完整目标仍按[验收矩阵](沙盒接入验收矩阵-2026-10-18.md)继续推进;本批不标记 P0/P1/P2/P3 全部完成,也不代表 WorkBuddy 1:1。
当前默认命令仍在自有 AppContainer/Job 和工作区副本中执行,网络禁用;原生组件或策略无效时拒绝执行。未给 MCP、手动终端或 OMP 内核进程追加“已整体隔离”的状态。
## 已落地实现
| 模块 | 当前行为 |
| --- | --- |
| `policy-store.ts` | 严格版本化全局/工作区策略、CAS 与跨维护进程文件锁;支持命令开关、ask/deny 写回、相对拒绝路径、资源/批次上限;工作区不能放宽全局规则 |
| `authorizations.ts` / `InteractionBroker` | 授权绑定具体变更摘要、工作区/调用、策略版本及内容指纹;单次消费、单调时限;取消、过期及策略变更拒绝旧授权;撤销会取消真实待审批交互 |
| `execution.ts` | 同工作区串行,最多 4 个活动工作区、32 个全局排队和每工作区 8 个排队请求;排队期间取消不启动副本 |
| `health.ts` / `self-test.ts` | 已安装与分项能力分开;自检保存时间、二进制/策略身份;身份变化不沿用旧自检结果;当前自检只验命令启动与清理 |
| `toolchains.ts` | 显式导入本机 Node/npm、Python/pip 的独立副本,逐文件摘要清单;源解释器不在宿主执行,凭证配置被过滤;执行时按需验证/复制 |
| `dependency-cache.ts` | 显式导入工作区 `node_modules` / `.venv/Lib/site-packages`,绑定工作区、配置/锁文件和运行时身份;拒绝链接/多重硬链接、篡改与过期层 |
| `runner.cpp` | 固定 PATH 指向受控目录,净化 npm/pip/Python 环境;工具与依赖层原生 ACL 为只读/可执行;继续保持零网络 capability 和精确 SID/Job 检查 |
| `transactions.ts` | 保留 v1 文件事务,增加 v2 目录条目;空目录、递归删除和目录重命名进入同一审批/备份/补偿/恢复;未知目录内容会阻止删除 |
| `maintenance.ts` | 增加操作结果查询;主进程独立保存 outcome,记录缺失时返回 incomplete/unknown,不由原生命令退出推断宿主写回成功 |
| main/preload/shared IPC | 暴露策略读取/CAS 更新、自检、操作查询与变更通知;变更接口校验自有主页面,拒绝 renderer 任意 workspaceRoot |
| 公共工具路由 | OMP/备用循环传递 session/run/tool-call;`execute_command` 新增显式 `runtimes` / `dependencies`,宿主文件工具共用策略与授权 |
| `stdio-broker.ts` / 原生输入转发 | 测试用、按 Job 绑定的 stdio 回显通路;限制帧/总量/并发/时间,拒绝伪造归属和重放。未开放文件/网络操作,模型参数不能开启 |
运行时可用性来自显式注册和每次执行的文件校验。测试中的注册均发生在隔离 stateRoot;没有修改用户正在运行应用的数据。生产状态目录通过维护命令指定后才能注册对应运行时/依赖,安全中心 UI 尚未提供导入入口。
## 调用与维护
模型命令可使用以下参数选择已为当前沙盒注册的运行时与依赖:
```json
{
"command": "npm run build && npm test",
"runtimes": ["node"],
"dependencies": ["node"]
}
```
省略选择时保持系统 shell 配置;缺少运行时/缓存会明确拒绝,不查宿主任意 PATH。Node 依赖保持真实 node_modules 布局,因此已验证 CJS 和 ESM;Python 依赖通过私有 PYTHONPATH 提供。依赖只读,修改源依赖后需要显式重新导入,新的锁文件或运行时身份使旧层失效。
维护命令、格式和明确限制见[原生维护说明](../native/sandbox/README.md)。全局策略 UI API 当前只接真实后台,页面仍待原版 REA 采集和实现。原版安全中心分区、控件和语义不由本批后台功能反向假定。
## 当前验证记录
| 验证 | 已得到的结果 | 范围/报告 |
| --- | --- | --- |
| `test:sandbox:policy` | 38/38 | 策略/CAS、授权单次与目标/工作区绑定、同 revision 内容变更失效、真实交互取消、操作查询、串行与排队取消;`.tmp/sandbox-policy-QaV6xN/report.json` |
| `test:sandbox:toolchains` | 24/24 | 真实 Node/npm 构建测试、Python 标准库/pip/单元测试、只读工具/依赖、CJS/ESM/Python 包加载、锁文件和身份/摘要拒绝;`.tmp/sandbox-toolchain-6feGGZ/report.json` |
| `test:sandbox:directories` | 13/13 | 真实空目录/树删除/重命名、精确二进制恢复、凭证保留、外部新增冲突和补偿;`.tmp/sandbox-directories-lDwsqD/report.json` |
| `test:sandbox:feasibility` | 9 项基本机制通过;实验命名管道未通过连通性门槛 | Windows 10.0.26100 x64、宿主 elevated;假 HKCU 样本和目录锁;`.tmp/sandbox-feasibility-ihY5FY/report.json`。管道拒绝另见 `.tmp/sandbox-feasibility-PtEmPh/report.json`,brokerPipeAvailable 为 false |
| `test:sandbox:stdio` | 10/10 | 真实继承通路回显、伪造/重放/超限拒绝、实际网络仍被拒绝、取消和模型不能开启;`.tmp/sandbox-stdio-7zOaqk/report.json` |
| `test:sandbox` | 52/52 | 基础 token/Job、越界、网络、事务与故障、退出清理等;目录用例已更新为真实支持;`.tmp/sandbox-regression-PoNlsK/report.json` |
| 整应用生产链路 | 最终生产构建与 23/23 通过 | 真实 production main/preload/renderer、OMP RPC、localhost 模型,Node 与策略/自检/查询、真实撤销交互及迟到按钮拒绝;`.tmp/sandbox-app-ZSMl6C/report.json` |
| 类型/基础回归 | 类型检查、smoke、对话、IPC 和 OMP 专项通过 | 旧对话“默认命令恒拒绝”断言改成显式缺失后端拒绝;并行思考强度修改的短暂类型错误已随其更新恢复,未改动该功能 |
测试仅使用自有临时工作区、隔离 userData/OMP home、本机模型夹具和假注册表键;注册表样本最终删除。用户当前窗口未被强制重启,主进程/preload 更改需下一次完整启动加载。所有既有并行 UI/引用/用量修改均保留。
## P0 原型发现与下一批
1. 只打开目录元数据的最初句柄原型未能阻止重命名;改为读取目录内容权限后,本机无删除共享句柄成功阻止替换,关闭后允许。未据此声称完整祖先遍历或文件代理安全。
2. 原生 AppContainer 不能读取/修改自建宿主 HKCU 假值。这只是默认隔离观察,不是自定义注册表规则完成。
3. 本机 named-pipe 原型在实际/预期 SID 相同的情况下仍返回 access denied;LOCAL 路径也未连通。替代的已绑定 stdio 原型已通过 10 项有限回显检查,实际 TCP 仍被拒绝;仅测试/内部调用可启用,真实文件和网络代理继续关闭。
4. P1 的真实操作 broker、逐后代归属、完整同步/异步适配和断线语义仍待实现。当前授权票据是可信主进程内部对象,stdio 回显只证明有限的通道、边界和取消,不是完整代理。
5. P2 仍缺 Git、长期可写工作区、联网安装、依赖更新和清理、pnpm 链接布局与完整编译工具链;P3 仍缺原生句柄代理、完整语言适配、元数据、专用删除阈值/回收站和聊天恢复映射。
6. P4~P8 的出站/程序/注册表规则、MCP/内核/ConPTY、安全中心、硬磁盘限额、升级/发布/独立审查及原版同场景验证按矩阵继续执行。
本轮没有新采 WorkBuddy 原版实现。E1/E2 静态证据和 E3 导航证据仅沿用此前记录,范围和未知项见矩阵。
@@ -0,0 +1,61 @@
# 沙盒接入验收矩阵(2026-10-18)
依据:[完整接入计划](沙盒完整接入计划-2026-10-18.md)、[首版基线](自有沙盒首版实施-2026-10-18.md)、[首批接入记录](沙盒完整接入首批实施-2026-10-18.md)、[只读文件桥](沙盒只读文件桥接入-2026-10-18.md)。矩阵记录功能子集,不将部分测试通过视为整个阶段或原版等价完成。
最新增量:[长期会话草稿](沙盒长期工作区接入-2026-10-18.md),组件 0.6.0 / protocol 1。显式 persistent 命令保留物理工作目录和会话私有 Git,支持同步/冲突、审批提交/丢弃/恢复及只读所属会话状态。[宿主原生事务](沙盒原生写回事务-2026-10-18.md)继续用于提交/补偿/恢复;完整文件代理、长期进程和可写依赖仍未完成。
## 状态与证据口径
- 已验证子集:有真实 YCode/Windows 执行记录;观察范围以对应测试报告为准。
- 进行中:已经有真实代码和部分测试,完整目标仍有必要项目未完成。
- 待实现/取证:尚无对应执行器或足够原版证据。
原版引用统一沿用 E1/E2/E3;这些不代表原生运行效果:
| 引用 | 版本与 Evidence ID | 范围与限制 |
| --- | --- | --- |
| E1 | 材料声明 5.8.6,版本未独立证明;`ev_74bf702462bc37612d2a68ea2e1c93b30b77dd5d99eaf1e961f1ab36d626c5e3` | 8 个 shim 的静态分析;无原生边界与运行比较 |
| E2 | 同一材料副本;`ev_2860ca8fafaf310ee1adc2ebe31530c868da0ecbb41a6f451e8e8e86b447a078` | 文件/删除/程序检查及规则字段的静态线索;图覆盖不完整 |
| E3 | 原版 5.8.3;`ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e` | 设置导航素材;不包含安全中心页面完整内容和策略行为 |
## A01~A17
| 编号 | 当前状态与真实执行点 | 已验证/验证入口 | 完成前仍需补齐 | 原版证据 |
| --- | --- | --- | --- | --- |
| A01 后端失效 | 进行中;`service.ts` 校验 manifest/二进制,`policy-store.ts` 严格解析策略 | `test:sandbox` 的缺失/摘要错误,`test:sandbox:policy` 的策略损坏拒绝执行 | 安装包损坏、迁移错误、后端替换期间运行语义 | E1/E2 无原生失效行为,需 REA |
| A02 授权与取消 | 进行中;`authorizations.ts`、`InteractionBroker`、执行队列、按 Job 绑定的真实读取 broker | 策略版本/内容指纹、单次使用、目标摘要、过期/撤销、交互/队列取消;真实读取、重放/洪泛拒绝、断线结果查询和缓存撤销;策略/读取桥/协议专项 | 写入等操作、逐后代归属、多窗口/重启审批恢复;Job 绑定不等于逐 PID 认证 | E1/E2 有调用归属/IPC 静态线索,无认证生效证据 |
| A03 工具链 | 进行中;Git 0.7.0 已接宿主历史/索引、原生组事务和受控远程;Node/Python 工具链与长期草稿维持各自边界 | Node/npm、Python/pip、内置 Git 离线复现;宿主普通仓库/linked-worktree 导入审批;本地分支/冲突/变基/标签/stash;项目+Git元数据提交、事务/主进程死亡恢复;HTTP/HTTPS/SSH、凭证撤销和unknown恢复;生产 OMP/IPC | 全部 Git 子命令/版本/平台兼容;30,000 项/256 MiB/128 MiB 之外的大仓库;reftable、alternates、partial clone、任意 hooks/helpers、LFS/子模块/GPG/交互编辑器;公网/企业代理、标准用户/安装包矩阵;可写依赖/长期进程/稳定绝对路径 | WorkBuddy Git 无完整运行证据;REA 范围见完整接入记录 |
| A04 依赖与安装 | 进行中;`dependency-cache.ts`、runner 只读依赖目录;草稿执行前注入、封存前卸载 | 工作区/锁文件/运行时绑定、CJS/ESM/Python 加载、篡改与跨项目拒绝;真实长期草稿加载只读依赖并保留项目产物;工具链/长期工作区专项 | 联网安装、受控私有凭证、可写依赖层、虚拟环境/临时目录持久化、pnpm 链接布局及供应链测试 | E1/E2 不证明完整包管理流程 |
| A05 文件语义 | 进行中;`transactions.ts` v3、`filesystem-write.h` 相对句柄事务、stage diff、native 只读桥 | 文件/目录增改删与二进制恢复、隐式父目录恢复;宿主写入专项32及目录专项13;读取桥语义保持 | 完整写入调用桥、追加/描述符/语言适配、ACL/时间戳;替换是两步交接,有短暂缺名窗口 | E2 有 HostFileOperation / FileTokenRequest 线索,消费语义待证 |
| A06 路径与竞争 | 进行中;读取/宿主事务使用 native 句柄链;草稿绑定项目/会话/根身份/策略,`workspace-state.h` 封存 DACL | junction/硬链接/敏感路径、审批期间同内容换根;成功命令保持同一物理目录;外部编辑三方同步/同文件及祖先冲突、草稿根替换拒绝、真实跨会话及同会话私有 blob 拒绝;读/写/长期工作区专项 | 标准用户/支持 OS/特殊 NTFS 配置矩阵、独立审查;稳定绝对执行路径/完整文件工具草稿路由;不据当前测试推定所有竞争时序均已通过 | E1/E2 无完整原生路径代理证据 |
| A07 删除与恢复 | 进行中;整批审批、原生句柄删除、v3 备份/意图;草稿检查点、同步/执行/提交意图与归档丢弃 | 递归删除/补偿、外部新增阻止、实际后端死亡;草稿非零退出/取消/超时回退,真实宿主进程死亡与租约恢复、提交后元数据失败仍保留 committed;确认记录中断及交接缺名为磁盘夹具 | 专用批量删除阈值、回收站、聊天映射、未完成/变更临时文件人工核对、断电耐久性;草稿文件/Git 两步发布不具单个崩溃原子性 | E2 有删除代理/批量防护模块;阈值和回收站待证 |
| A08 联网 | 普通 AppContainer 命令仍默认禁网;Git 仅通过专用可信宿主传输控制面按次审批 HTTP(S)/SSH | 原生 TCP 拒绝;Git loopback HTTP/HTTPS/SSH 的 TLS/host-key、URL/ref/凭证范围、撤销、unknown、取消和策略撤销 | 普通命令通用出站代理、DNS/重定向/IP/IPv6/局域网策略、npm/pip 在线安装、公网/企业代理矩阵 | E2 仅证 `network_policy` 字段;原生双引擎说法待证 |
| A09 程序/注册表 | 进行中;现有 Job/容器与 P0 假注册表探针 | 后代继承、breakaway 拒绝;自建 HKCU 假值不可读写;基础/可行性专项 | 逐程序/参数/身份规则、后代真实执行点、可配置注册表读写 | E2 程序检查入口;registry 字段不代表生效 |
| A10 开发服务/预览 | 待实现 | 当前无受控服务端口暴露验收 | 实例绑定、端口/来源限制、退出回收、预览权限隔离 | 无完整证据,需 REA |
| A11 MCP | 待系统隔离接入;现有 `capabilities/mcp.ts` 信任/连接代次继续有效 | 原有连接与逐次调用审批不作为沙盒验收 | stdio 启动适配、将现有会话草稿接入 MCP、长期进程、凭证、配置变化后的进程树清理 | 现有材料不能证明原版全部 MCP 进程隔离 |
| A12 OMP | 进行中;工具路由按 session/run/tool-call 归属,接入 `sandbox_workspace` / `sandbox_git` | 真实 OMP + localhost 模型;长期草稿、宿主 Git 导入/写回、远程审批结果、所属会话 IPC;生产 Electron Git 专项52项 | 内核自身环境/扩展/配置/通用网络审计和可信控制面拆分;普通文件工具仍访问宿主 | 无原版等价结论;OMP 只是当前内核实现选择 |
| A13 交互终端 | 待系统隔离接入;现有 ConPTY 仍为宿主终端 | 原有终端归属/取消测试不作为隔离验收 | 受限 ConPTY 原型、I/O/resize/迁移、长期变更与写回、权限显示 | 需 REA 同场景行为 |
| A14 安全中心 | 进行中;真实策略/自检/运行/读取操作及所属会话草稿状态 IPC | 生产 main/preload 的 CAS、非法字段/路径/跨 Job 请求拒绝、广播、自检与持久读取结果;草稿根由保存会话推导、未知会话拒绝、busy 不阻塞审批;整应用专项 | 原版页面取证、真实页面控件、恢复/审计/缓存/草稿 UI、多窗口全状态;本批没有新增安全中心页面 | E3 只有导航,页面不能凭设想补齐后声称 1:1 |
| A15 资源/恢复 | 进行中;Job、队列、事务、租约;宿主私有草稿 blob/版本/归档、封存及独占锁;版本清单有发布前 8 MiB 检查 | 进程/内存限制、超时/取消/父退出、崩溃清理、目录恢复;真实死主进程接管与活动租约拒绝、检查点回退 | OS 磁盘硬配额、blob/归档/审计自动保留、早期启动崩溃、断电耐久性、版本清单容量边界专项;长期进程未实现 | 无原版完整证据,需独立 REA/系统测试 |
| A16 发布环境 | 开发/生产构建链路已验子集 | 当前 Windows 11 x64 elevated 环境;生产 Electron 整应用专项 | 无 Build Tools/WorkBuddy 的安装包、标准用户、支持 OS、升级/卸载/签名链 | 原版安装版本和发布行为需分别取证 |
| A17 原版等价 | 未验收 | E1/E2/E3 只用作静态/导航参照 | 相同场景下允许/拒绝、审批、错误、恢复、视觉/动画、持久化的 REA 对照 | 全部未知项须带实际版本、Evidence ID 和范围 |
## P0 机制原型清单
| 机制 | 当前观察 | 允许、拒绝和故障的后续验收 |
| --- | --- | --- |
| 文件目录句柄 | 读取桥和宿主事务均固定根/祖先及实际目标;写入通过 NT 相对目录句柄操作,默认工具/命令和恢复已接入。历史原型失败记录保留 | 补平台、权限、安全审查和中断时序;两步替换有缺名窗口,完整写入调用桥/元数据及断电级耐久性未完成 |
| 代理通信 | named-pipe 实验未连通;stdio 回显原型仍保留,新增模型显式选择的真实只读协议、分块/背压/日志与查询,未增加网络 capability | 逐后代认证、宿主写入、Python/Shell 等完整适配;现有 Job 通道不将完整 broker 标完成 |
| 出站网络 | 命令 Job 零网络 capability 的 TCP 拒绝已实测;Git 专用宿主代理另有 loopback HTTPS/SSH 允许通路 | 普通命令及通用代理仍需验允许通路与绕过拒绝;Git 已验目标/凭证/证书/取消/策略撤销失败关闭 |
| 程序与注册表 | Job 继承已测;本机自建假注册表键在 AppContainer 内不可读写,已清理样本 | 允许指定目标并拒绝替换程序/后代旁路;授权撤销和后端故障;自建注册表允许/拒绝/恢复,不能仅依赖名称匹配 |
| ConPTY | 尚无原型结论 | 真实受限 token/Job 的 shell 启动、I/O/resize/退出;拒绝越界/网络/脱离 Job;窗口消失、父退出及协议断线 |
| 硬磁盘限额 | 快照/写回大小上限和执行队列已实现;不是 OS 硬限额 | 受控卷/目录实际达到硬限额并被阻止;容量耗尽不影响宿主系统卷;异常退出、重启和卸载正确释放资源 |
命名管道原型保持在测试 probe 中,不打包到生产沙盒。Microsoft 的 [CreateNamedPipe 文档](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea) 对 UWP/AppContainer 的支持范围有限定;[管道权限文档](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights) 说明通用写权限可能包含创建管道实例权限。本轮原型已改用单项访问位,但连通性仍未通过;这些文档不证明当前自有原型可以工作。
## 后续顺序
1. Git 常用工程子集已一次性接入并独立验收;继续扩展 Git 全命令/平台兼容,并在取得 WorkBuddy Git 的 REA 同场景运行证据后再作等价结论。
2. 接入可写依赖层/受控更新与版本兼容,明确长期进程/稳定执行路径和普通文件工具的草稿视图;补保留清理、两步交接中断/竞争时序。完整语言适配、逐后代认证和完整协议验收继续未完成。
3. 在原生边界可靠后实现 P3 代理操作,再接 P4 受控联网/程序/注册表;维持默认拒绝。
4. 按计划推进 MCP/ConPTY、安全中心、发布和原版同场景验收;各行分开更新证据和完成状态。
@@ -0,0 +1,130 @@
# 沙盒长期工作区接入(2026-10-18)
> 当前状态:本文记录 0.6.0 长期草稿历史范围;0.7.0 已扩展宿主 Git 导入/写回/远程。本文后部关于“宿主 Git 尚未接通/下一步”的内容是当时状态,不代表当前;请以[完整 Git 接入](沙盒Git完整接入-2026-10-18.md)和[验收矩阵](沙盒Git接入验收矩阵-2026-10-18.md)为准。长期草稿的其他限制仍有效。
## 交付范围
组件升级为 **0.6.0 / protocol 1**,新增 `workspaceState: 1`。同一项目、同一会话可显式选择长期草稿:连续命令保留文件与沙盒私有 Git,检查变更后再审批提交到宿主项目。默认命令仍使用每命令快照;本批没有切换用户生产配置。
这是 P2 长期工作区的已验证子集。没有新增 WorkBuddy 取证,不代表完整 P2/P3、安全中心或原版等价验收。
| 入口 | 当前行为 |
| --- | --- |
| `execute_command` 不传 `workspace` | 沿用当前权限路由;默认权限使用快照,成功变更经审批写回 |
| `execute_command` 显式 `workspace: "snapshot"` | 强制使用隔离快照,包括原本为完全访问的调用上下文 |
| `execute_command` 显式 `workspace: "persistent"` | 强制使用当前会话隔离草稿;成功命令保存草稿,不自动写宿主 |
| `sandbox_workspace` | `status`、`changes`、`commit`、`discard`、`recover`;宿主根据工具上下文确定项目/会话,不接受模型指定任意归属或路径 |
| `window.ycode.app.sandboxWorkspace(sessionId)` | 只读状态;主进程根据保存的会话推导项目根,检查受信顶层页面,拒绝未知会话 |
| 维护 CLI | 指定绝对项目根、状态根和会话;提交需要显式 `--approve` |
普通 `read_file`、`write_file`、`edit_file`、`list_dir` 仍作用于宿主项目。读取或修改未提交草稿必须使用 `workspace: "persistent"` 的命令。只读宿主文件桥仍读取绑定的宿主项目,也没有自动重定向到草稿。
## 使用方式
模型工具调用示例:
```json
{"command":"echo draft-value>draft.txt","workspace":"persistent"}
```
```json
{"command":"type draft.txt","workspace":"persistent"}
```
```json
{"action":"changes"}
```
```json
{"action":"commit","expectedRevision":"已观察到的版本 UUID"}
```
后两项调用 `sandbox_workspace`。`expectedRevision` 可省略;提供时版本变化会拒绝操作。`status` 只查看已保存状态,`changes` 才同步未冲突的宿主外部修改并计算待提交变更。`commit` 复用现有对话审批和原生 v3 宿主事务;拒绝审批会保留草稿。规划模式不能调用草稿管理工具。
私有 Git 示例:
```json
{"command":"git add . && git commit -m draft","workspace":"persistent","runtimes":["git"]}
```
Git 默认由应用携带,无需用户注册。此处提交只保存当前会话的沙盒私有历史,不触发宿主写回审批,不写宿主 `.git`,也不代表已经提交到用户原仓库。Node/Python 仍需维护者先导入;只读依赖通过已有 `dependencies` 参数显式选择。
```powershell
npm run sandbox:maintain -- workspace-status --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- workspace-changes --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- workspace-commit --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox' --draft-revision REVISION_UUID --approve
npm run sandbox:maintain -- workspace-discard --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- workspace-recover --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
```
`discard` 归档旧草稿和私有 Git 引用,然后从当前宿主/策略建立新基线;它不是永久删除历史的保留清理。若宿主提交结果尚未确定,必须先 `recover`,避免把已发生的宿主写入当作未执行。
## 保存、隔离与冲突
状态位于 `stateRoot/workspaces/<项目与会话摘要>/`。活动引用指向摘要校验的不可变版本清单,文件内容保存在 SHA-256 blob 中。blob 先写临时文件并刷新,再发布到摘要名称;版本清单发布前限制为 **8 MiB**。默认文件/总容量/条目上限继续有效;保留历史没有自动回收,也不是操作系统磁盘硬配额。
正常成功命令之间保留**同一个实际 NTFS 工作目录身份**:执行前将 `worktree` 移入本次私有 stage,执行结束并清理 Job/profile/映射后移回。没有每次重新复制整个项目。恢复、丢弃可以重建物理目录并产生新身份。
新增宿主专用 `--seal-workspace`:使用原生相对目录句柄递归检查并设置受保护 DACL,只允许宿主用户和 SYSTEM。它拒绝 AppContainer 调用、链接、多重硬链接和过深/过量目录。保留文件移回前去除旧 AppContainer 权限;blob/版本存储由宿主独占,后续同会话 Job 也不能直接读取。运行时只向本次 stage 开放当前 Job 所需访问,宿主项目 ACL 不改变。
版本绑定规范项目根、宿主根卷/文件身份、会话、策略内容指纹和草稿 revision。换根、策略变更、物理草稿被替换、摘要损坏均拒绝自动复用;失效状态不返回旧文件名。明确 `discard` 后才按当前策略建立新草稿。
执行与管理操作复用项目队列,持有独占草稿锁。`status` 走队列外的只读检查,命令运行或等待提交审批时及时返回 `busy`。宿主外部修改在下一次同步时做三方合并:不相关修改进入草稿;同文件、目录祖先冲突停止执行/提交,保留双方内容。当前不进行行级文本合并;文件/目录互换在发布新版本前拒绝。
## 中断与提交结果
- 非零退出、取消、超时不发布新文件版本,恢复最近的已保存草稿;未成功内容归档在宿主私有存储。
- `checkout.json` 记录执行归属,`mutation.json` 记录同步意图;未完成记录阻止普通复用。显式恢复检查保存的 run 请求、会话、主进程锁和原生租约,活动进程或未清理租约继续拒绝接管。
- 草稿提交先保存 `commit-intent.json`,授权绑定 revision、宿主根身份和完整变更摘要。原生宿主事务仍使用 v3 根身份/逐操作日志与独立备份。
- 宿主已经提交、但随后草稿元数据失败时,结果仍报告 `writeback: committed`,并保留需恢复状态。`recover` 根据授权和会话匹配原生事务,再推进草稿基线;结果未知时不自动重试宿主写入。
- 私有 Git 使用独立的项目/会话命名空间,与原有项目级快照 Git 分开。文件检查点和 Git 发布是两个步骤;Git 失败单独报告,不能声称两者为单个崩溃原子事务。失败命令和真实宿主进程死亡的用例已验证旧 Git 版本不被替换。
恢复支持进程中断,不承诺逐原生指令断电耐久性。原有宿主文件替换仍为禁止覆盖的两次重命名,有短暂缺名窗口,见[原生写回事务](沙盒原生写回事务-2026-10-18.md)。
若未确定的宿主提交同时遇到策略或宿主根身份变化,当前自动恢复会拒绝继续,丢弃也不会绕过该记录。此组合需要维护者核对原事务;尚无跨策略/换根自动迁移恢复入口。
## 源码与验证
| 文件 | 职责 |
| --- | --- |
| `native/sandbox/workspace-state.h`、`runner.cpp` | 原生宿主封存入口、目录句柄与 DACL |
| `src/main/sandbox/workspace.ts` | 物理草稿、blob/版本、三方同步、锁、检查点与恢复 |
| `src/main/sandbox/workspace-service.ts` | 管理动作、审批、宿主提交意图与结果恢复 |
| `src/main/sandbox/workspace-files.ts` | 快照/变更扫描共用实现,由原 service 提取 |
| `src/main/sandbox/service.ts`、`git-repository.ts` | 命令模式路由、草稿生命周期、私有 Git 命名空间 |
| `src/main/agent/tools/sandbox-workspace.ts`、`tools/index.ts` | 模型工具与显式工作区参数 |
| main/preload/shared IPC、`scripts/sandbox-maintain.ts` | 所属会话只读状态与维护入口 |
`npm run build` 已通过,包含原生 `/W4 /WX`、TypeScript 和 main/preload/renderer 生产构建,日志 `.tmp/sandbox-persistent-final-build.log`。专项命令为 `npm run test:sandbox:workspace`;生产整应用为 `npm run test:sandbox:app`。测试期间不能重建正在使用的原生 exe;批量回归直接运行同一次构建对应的独立测试 bundle。
最终报告与摘要汇总见 `.tmp/sandbox-persistent-validation.json`。所有测试使用独立 `.tmp` 项目/状态和隔离 Electron userData;生产 OMP 使用实际已安装内核及本机回环模型,没有调用用户付费 API。
最终 11 组共 **356 项通过**:
| 专项 | 结果 | 最终报告 |
| --- | --- | --- |
| 长期会话草稿 | 51/51 | `.tmp/sandbox-workspace-b84whI/report.json` |
| 真实生产 Electron/OMP | 42/42 | `.tmp/sandbox-app-ZWTjR8/report.json` |
| 基础隔离/宿主工具 | 52/52 | `.tmp/sandbox-regression-XPTm0T/report.json` |
| Git 本地工作流 | 43/43 | `.tmp/sandbox-git-mDlqPV/report.json` |
| 策略/授权 | 38/38 | `.tmp/sandbox-policy-zkcBxS/report.json` |
| 目录事务 | 13/13 | `.tmp/sandbox-directories-mKwa11/report.json` |
| 原生宿主写回 | 32/32 | `.tmp/sandbox-write-eZDjBV/report.json` |
| 内置 Git 分发 | 20/20 | `.tmp/sandbox-bundled-git-EmYmSY/report.json`;本次未提供离线归档重建参数 |
| 只读文件桥 | 21/21 | `.tmp/sandbox-read-bridge-22EY7p/report.json` |
| Node/Python 进程兼容 | 20/20 | `.tmp/sandbox-process-QOd5lW/report.json` |
| 工具链/依赖 | 24/24 | `.tmp/sandbox-toolchain-FUo7VU/report.json` |
组件 manifest 中全部原生源码摘要、适配器摘要和二进制摘要已与实物逐项核对;原生 `--version` 与 manifest 均为 0.6.0。汇总另保存 main/preload 产物和测试报告摘要。本批没有重新构建安装包,不能用之前的 0.4.0 目录包验收替代当前 0.6.0 发布验收。
专项包含真实独立 Node 宿主进程死亡及原生租约恢复、真实 AppContainer 跨会话/同会话私有内容拒绝、只读依赖注入/卸载、原生宿主提交、提交后元数据故障、策略撤销、中文/空格/`^` 路径和 CLI 新进程重载。同步/检查点已发布但确认记录未移除的用例是可复现磁盘状态夹具;不把夹具当作每个写入指令位置强杀或断电测试。
## 尚未完成
长期草稿目前保留受过滤的项目文件和私有 Git。进程、home、TEMP、工具和盘符映射仍为每命令生命周期;实际执行绝对路径不稳定,原项目绝对路径不重定向,后台服务不会跨命令继续运行。
`node_modules`、`.venv`、`venv`、`.tmp`、`__pycache__` 等不作为可写持久层保存。受控只读依赖可临时注入,Node 依赖在封存前移除;联网安装、可写依赖缓存和链接布局仍未完成。宿主 Git 历史/索引导入、元数据事务和远程 Git 尚未接通。
完整文件 shim、元数据/回收站、受控出站、逐程序/逐后代规则、MCP/交互终端系统隔离、安全中心页面、自动保留清理、磁盘硬限额、安装升级、标准用户/平台矩阵、断电耐久性及独立安全审查继续按[完整计划](沙盒完整接入计划-2026-10-18.md)推进。当前用户窗口未重启;主进程/preload 新入口在完整重启后加载。
下一步优先处理宿主 Git 的安全导入与元数据事务,然后完善可写依赖和受控联网;相应验收仍逐项记录,不将本批长期草稿通过扩大为完整沙盒完成。
@@ -0,0 +1,45 @@
# WorkBuddy 经济模式用途核对
核对日期:2026-10-18。用途结论来自既有 WorkBuddy 5.8.3 REA 页面截图;不将当前安装包的静态目录分析视为运行验证。
> 后续决定:2026-10-18 用户要求移除经济模式,并随后要求移除下方浏览器操控。YCode 智能体设置现仅保留团队管理;下方原版用途保留为调查记录。
## 已确认的用途
原版设置 → 智能体 → 经济模式的说明为:
> 自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗
因此,已确认的产品用途是自动上下文压缩和减少长对话的重复输入成本。该说明没有提供具体阈值、压缩算法或节省比例。
REA Evidence:`ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883`,操作 `inspect_electron_page`,画面采集于 2026-10-17T03:21:59.120Z,900×680。本轮重新查看了证据中的原版截图,PNG SHA-256 为 `d0d60a274e4e02690f471bde07717019f1734516ca5d7c98adb707a9fa8e4a7f`。画面中的开关为开启状态;单张画面不能证明新安装默认值。
## 解释与未确认项
- 从文案可解释为:降低后续轮次重复携带历史内容造成的输入开销。
- 不能据此确定其采用摘要、工具输出清理还是其他压缩策略。
- 尚未确认触发阈值、保留多少历史、是否后台准备,以及关闭后接近硬上限时的处理。
- 尚无证据表明该开关会切换廉价模型、改变思考强度或调整输出上限;这些行为不能自行加入原版结论。
## 本轮新增采集范围
通过 REA 检查当前安装包及 `ExtensionsSettingsPanel-DBPXrq2V.js` 的目录、摘要、导入和静态关系。安装包 SHA-256:`3e50fcb5b060deba4f1ea9a8c293cf955bf12d7e52d971985de914350afd9077`。相关 Evidence:`ev_45c7257e193365d9b0e981235ad36233971a26883b004a90f923a3e1adc00f56`、`ev_434b27ec757cadc649caa02849e81b860a89cd9213ccf09b2ef5d3fb0b38d358`。所选脚本摘要与安装包目录记录一致,但静态结果没有建立经济模式的完整配置到压缩执行链路,因此不据此宣称当前安装版本已完成源码级核对。
本轮 REA 连接 `http://127.0.0.1:9223` 返回 `endpoint_unreachable`。未重启 WorkBuddy、切换设置或发送测试消息。安装包分析按 `record-and-continue` 保留若干 unpacked 资源的完整性差异,不能将全包视为全部通过完整性校验。
## YCode 当前状态
经济模式与浏览器操控配置已按用户要求移除,包括说明、开关、安装按钮、扩展列表及专用样式/资源导入。OMP 已有的自动压缩继续按模型上下文和输出预算运行;原版压缩算法与执行策略尚未完成等价验证。
初次调查未修改产品代码;后续按用户指令移除 YCode 配置,并更新现有智能体页面验收。TypeScript 检查和 `electron-vite build` 通过,真实隔离应用浅色/深色、移除项无残留、团队管理保存/重开恢复等 11 项检查通过,0 renderer 错误。报告:`.tmp/agents-settings-P8GbjD/report.json`,已查看两张页面截图。完整 `npm run build` 在沙盒原生编译阶段被 `native/sandbox/runner.cpp:392` 的 C4244(警告视为错误)阻断;未修改该并行模块,未重启用户窗口。
## 对话中的无头浏览器能力
> 后续实现:用户随后要求修复对话中的浏览器操控,现已内置 `browser` 并接通 OMP/HTTP;60 项实际浏览器及生产 IPC 检查与完整构建通过,见 [对话无头浏览器接入](对话无头浏览器接入-2026-10-18.md)。以下是接入前的调查基线,不再代表最新代码。原版 REA 结论保持原有观察范围。
- `src/main/browser-host.ts` 提供右侧手动网页浏览器,基于 Electron `WebContentsView`,IPC 覆盖创建、导航、前进/后退、刷新和显示位置。
- `src/main/agent/tools/index.ts` 的内置工具没有页面读取、点击、输入、截图或无头浏览器生命周期工具。
- `src/main/agent/omp-turn.ts` 只注册 YCode 工具与能力快照;`src/main/omp.ts` 通过 `--no-tools` 关闭 OMP 内置工具。因此不能由 OMP 自身支持哪些工具推断 YCode 已开放浏览器操控。
- `src/main/capabilities/runtime.ts` 支持引入已连接并授权的 MCP 工具。检查当前默认用户数据目录时,`mcp-servers.json` 不存在,未配置浏览器 MCP;未读取或输出凭据。
结论:YCode 当前普通对话尚未内置或通过当前配置开放无头浏览器自动化。完全访问模式下执行外部脚本是另一条有条件的执行路径,不能视为已接入浏览器能力。
@@ -0,0 +1,94 @@
# 自定义模型与多协议接入(2026-10-18)
## 实施结果
本轮完成用户提出的五项模型设置调整。按用户定制需求实现,未查看或采集 WorkBuddy,不新增原版等价或全状态 1:1 验收结论。保留当前工作区的设置性能、附件、上下文用量和沙盒并行修改,未重启用户正在使用的 YCode。
| 需求 | 当前行为 |
| --- | --- |
| 供应商仅自定义 | 添加、编辑和列表统一显示“自定义”;已有配置和密钥继续可用。原供应商资源仍供其他界面的模型图标使用。 |
| 多协议 | 自动协商、OpenAI 兼容、Anthropic、Gemini;OpenAI 内含 Chat Completions 和 Responses 两种 API。正式对话和备用 HTTP 网关均按协议构造请求。 |
| 仅填写域名 | 自动补全 HTTPS,localhost/127.0.0.1/IPv6 回环默认 HTTP;保留完整地址兼容,协商常用版本路径和根路径。原来未声明协议的配置保持旧 OpenAI 路由。 |
| 获取模型 | 按当前地址、协议和新填/已有 Key 请求模型列表,支持分页、去重、单选、搜索和手动输入。Gemini 排除不支持生成内容的模型。空列表、没有列表 API 时仍可手动配置。 |
| 模型元数据 | 内置完整 models.dev 快照,按精确 ID、供应商前缀及明确的 canonical ID 匹配,填入能力、思考、总上下文和输出;输入预算动态扣除输出,原始上限继续用于校验。用户手动配置在编辑和重新打开后保留;未知 ID 不猜测能力。 |
新连接在保存前发送一次简短生成探测,表单已说明该行为。模型列表成功只能证明列表 API 可用,不能代替生成协议验证;正式路由保存验证结果。获取列表不会覆盖原来已验证的 Responses 等生成路由。迟到的列表、元数据或测试响应不能覆盖后续编辑。
## 最新总上下文语义订正
用户确认第一项是总“上下文”,第二项是“输出”;输入预算和压缩阈值动态计算,见[总上下文预算与压缩阈值](上下文预算与压缩阈值-2026-10-18.md)。500K/128K现为500,000总窗口、128,000输出、372,000输入预算,压缩阈值372,000;上一轮628,000的相加逻辑已修正。已接真实OMP独立配置,安装内核读取阈值正确。完整构建、预算13项、原生14项、模型能力22项、旧配置迁移/冷加载及真实隔离应用104项通过,未重启用户窗口。
## 本日后续界面调整
- 对话、项目和定时任务中的“配置自定义模型”统一打开模型列表;只有明确的添加操作打开添加表单。
- “更新元数据”移动到“设置 → 关于”,显示来源及快照日期,调用既有真实更新服务并反馈成功或失败。模型列表不再显示更新入口。
- 移除“此接口无需 API Key”开关和相关提示。新建时 Key 为空即按免密接口处理;编辑已有密钥时只显示圆点占位,未编辑则保留,清空后可改为免密。
- 模型名称使用可编辑搜索下拉框,同一个输入框支持筛选、键盘单选及手填;下拉框与整个输入框左边缘和宽度对齐。修复浮层定位与入场动效冲突,以及获取列表后过早打开的竞态。
- 移除“已匹配 models.dev…”及最低参考价格等说明。最新表单仅保留“上下文”和“输出”两项,输入预算动态计算;后台继续匹配元数据、选择价格档并校验原始上限。
- OpenAI 兼容的新协商默认优先 Responses,成功后不再探测 Chat;Responses 不受支持时继续尝试 Chat。自动协商中的 OpenAI 候选也采用该顺序,明确填写 `/chat/completions` 的地址仍优先 Chat。重新测试已有 OpenAI 配置时也按新顺序探测,并保留协商出的自定义根路径;普通对话继续使用保存的接口或更新的成功探测结果,未声明新协议的旧模型仍沿用原兼容逻辑。
最终 `npm run build` 通过。最新真实隔离应用验证 **78 项通过、0 renderer 错误**,报告 `.tmp/custom-model-settings-EeOtQT/report.json`;四种协议的正式调用、空 Key、保留/清空已有密钥、搜索/手填、浮层宽度/位置/稳定态可见性、移除文案和字段、关于页实际更新服务均覆盖。关于与帮助布局 21 项通过(`.tmp/about-help-settings-Hk3bkH/report.json`),设置生命周期 26 项通过、0 renderer 错误(`.tmp/settings-performance-JpGssO/report.json`)。已查看最新浅色参数表单、深色展开列表及关于页截图。
本次界面调整后未强制重启用户正在运行的 YCode,重新启动后加载最新构建。本轮未采集 WorkBuddy,不新增原版逐像素验收结论。下方验证表保留本日原功能实现时的基线。
Responses 优先调整后的完整构建通过(`.tmp/responses-priority-build.log`)。模型注册回归验证两种接口都支持时只请求 Responses、仅 Chat 时回退、显式 Chat 地址、旧 Chat 自定义根路径重新测试、历史协议及原有加密/持久化/错误处理(`.tmp/responses-priority-registry.log`);原生多协议回归 14 项通过(`.tmp/responses-priority-protocols.log`),真实隔离应用 78 项通过、0 renderer 错误(`.tmp/custom-model-settings-EciOBY/report.json`)。未调用用户实际 API 或重启用户当前窗口。
## 500K快捷项首轮实现(历史,已由上方订正)
- 输入新增 **500K = 500,000 tokens**,输出新增 **128K = 128,000 tokens**。新项使用明确标签与精确值,避免按 1024 生成 131,072 而超出模型目录的 128,000 输出上限;原有快捷项数值保持兼容。
- 元数据同时保留模型原始输入/输出/总上下文上限及完整价格档。自动匹配仍优先低价档;手动输入超出该窗口时,总上下文同步为足以容纳输入和输出预算的值。以 GPT-6.1 Sol 为例,选择这两项后保存 **500,000 输入 / 128,000 输出 / 628,000 总上下文**,参考价格切换至对应长上下文档。
- 前端与主进程使用同一预算解析。目录不支持的快捷项禁用,手填超出单项或总上限时阻止保存;已保存值不会被失败操作覆盖。缩小大预算后可恢复自动低价档与窗口。
- 编辑已有配置时更新原始元数据,但保留手动输入、输出与能力。保存、重新打开及新的主进程加载均保留预算;新回合的 OMP profile、真实请求输出预算和上下文统计使用同一配置。
本轮完整构建通过(`.tmp/model-budget-build.log`)。元数据与预算 12 项通过(`.tmp/model-budget-metadata.log`),模型注册/冷加载/无效写入保护通过(`.tmp/model-budget-registry.log`),上下文用量回归通过(`.tmp/model-budget-context.log`),原生 HTTP 多协议 14 项通过(`.tmp/model-budget-native.log`)。真实隔离生产应用 **99 项通过、0 renderer 错误**,报告 `.tmp/custom-model-settings-D1Jj9H/report.json`;实际读取 OMP profile 确认 `contextWindow: 628000`、`maxTokens: 128000`,受控 Responses 请求确认 `max_output_tokens: 128000`,统计分母为 628,000。已查看 `custom-editor-500k-128k-light.png`,两列新增选项完整显示且选中样式正常。
验收使用短提示和本地受控上游,验证配置、参数与统计链路;没有发送 500K 内容到用户实际收费服务,不能据此断言供应商实际限额。本轮未重启用户当前窗口、未采集 WorkBuddy,重启加载最新构建后可选择新项。
## 元数据快照与价格选择
- 来源:[models.dev API](https://models.dev/api.json)、[公开仓库](https://github.com/anomalyco/models.dev)。数据和 MIT 许可证保存在 `resources/model-metadata/`,包含打包资源。
- 获取时间:北京时间 **2026-10-18 06:29:14**(UTC `2026-10-17T22:29:14.523Z`)。
- 数量:232 个供应商,9,041 个模型,5,796,868 字节。
- SHA-256:`5980b554323ea4f118b06b960dde1eeb193f5bfa4722d6127fb3966d77195a1c`。
- 获取时仓库 revision:`8deeec23540f4c0e2c473be7f08130366e662c78`。记录的是获取时仓库状态,不将其当作 API JSON 与该提交的一一构建映射。
- “设置 → 关于”的“更新元数据”会校验并原子缓存新快照;更新失败保留可用数据。启动时在有效缓存与随包快照中选较新的版本,避免旧缓存覆盖新安装包。
- 维护命令:`npm run models:sync-metadata`。
同一模型优先采用其原供应商元数据,不为更低的代理报价切换供应商。上下文阶梯的 `tier.size` 按下限处理;默认上下文不越过所选低价档的上界。例如 GPT-5.4 默认为 272,000 总上下文和 128,000 输出上限,输入预算派生为 144,000,参考输入/输出价格为每百万 token $2.5/$15。
若某档输入更便宜、输出更贵,另一档相反,则不存在与 token 比例无关的“最便宜档”。此时默认最小上下文档,并在内部元数据中保存选择依据。所有价格均为目录参考值,不能保证自定义服务实际收费;页面已按用户要求移除这些说明。
预算型思考模型使用已验证的 `minimal`(极低)档作为可行默认时,对应原生预算为 1,024 tokens;只有开关、没有离散强度的模型采用自动强度。选择器与请求共用强度解析,禁止把旧模型不支持的强度传入新模型,也不会关闭目录声明为必须开启的思考模式。
## 正式调用、鉴权与持久化
- 主进程保存协议、协商路径、能力、限制和元数据来源。密钥继续使用已有的安全存储,预加载和界面只返回脱敏信息;编辑时未触碰密钥框则保留,明确清空则按免密保存。
- OMP 原生 Anthropic 自定义域名默认会附加 Bearer。现在通过受支持的环境变量请求头配置发送原生 `x-api-key`,同时关闭自动 Bearer;密钥不会写入 YAML 或命令行。
- Gemini 使用 `x-goog-api-key` 和 `generateContent` / SSE `streamGenerateContent`,不要求用户拼接查询字符串。
- OMP 的免密 Gemini 分支会附加 `N/A` 占位 Key,因此这种配置明确走原生 HTTP 网关。真实生产应用已验证免密 Gemini 成功对话且没有鉴权头。
- 同源路径协商不跟随跨源重定向携带凭证。模型列表和生成探测均有时间与响应大小上限。
- 备用网关处理原生图片、工具、思考、usage 与同一工具循环中的签名;截断、输出上限和流错误不会被当作可执行的完整工具调用。
- 总上下文进入原生 profile 和用量分母;原生 Gemini 请求也接入已有的系统/工具/消息分类观测。
## 验证
最终完整 `npm run build` 通过,含原生沙盒编译、Node/Web 类型检查和 Electron 构建。当前任务相关文件的 `git -c core.whitespace=cr-at-eol diff --check -- <files>` 通过;全工作区仍有并行 `src/main/sessions.ts` 的末尾空行提示,未为此修改他人文件。
| 验证 | 结果与证据 |
| --- | --- |
| 实际 UI → preload/IPC → 正式内核/HTTP → 受控上游 | **72 项通过,0 renderer 错误**,`.tmp/custom-model-settings-vu41wD/report.json`。覆盖四种 API 获取列表、自动参数、严格鉴权、保存及正式对话;核对实际请求强度和上下文分类;覆盖免密 Gemini、旧密钥查询、手动覆盖、空/无列表、过期响应以及浅色/深色布局。 |
| 真实 OMP 原生协议 | `npm run test:custom-models`:14 项通过,`.tmp/custom-model-core-final.log`。Anthropic API Key/免密、Gemini API Key 实际回合通过;免密 Gemini 明确使用备用通道。 |
| 原生 HTTP 网关 | `npm run test:custom-model-native`:14 项通过,`.tmp/custom-model-native-final.log`。分页、签名/工具续接、用量、错误与截断、免密、现代 OpenAI 输出预算和 Gemini 原生强度参数。 |
| 元数据服务 | `npm run test:model-metadata`:7 项通过,`.tmp/model-metadata-final.log`。新旧缓存、校验失败回退、更新落盘/失败保留、canonical 匹配、交叉价格和强度解析。 |
| 既有模型档案 | `scripts/model-registry-regression.ts` 通过,`.tmp/model-registry-final.log`。迁移、加密 CRUD、并发写入、选择/重载、空密钥及探测失败脱敏。 |
| 既有 Responses 网关 | `scripts/gateway-responses-regression.ts` 30 项通过,`.tmp/custom-model-responses-final.log`。旧 Chat 路由和参数保持兼容。 |
| 既有原生模型能力 | 22 项通过,`.tmp/model-capabilities-gYrRP5/report.json`。 |
| 设置性能与生命周期 | 26 项通过,`.tmp/settings-performance-OujUBI/report.json`。默认通用内容首开 283ms、重开 25~53ms;当前轮并行测试环境,不将其作为用户机器固定耗时。 |
| 对话与持久化 | `test:omp`、`test:chat`、`test:persistence`、`test:conversation-ipc`、`test:conversation`、`test:context-usage` 通过。旧测试替身补齐强度设置契约,并断言非思考模型确实在 prompt 前关闭思考。 |
| 定时任务兼容 | `test:automations` 19 组通过;允许保存新增的 auto/minimal 强度。 |
截图位于 `.tmp/custom-model-settings-vu41wD/`:浅色/深色表单、能力与上下文、400px 模型列表。已实际查看最新构建的高级参数和深色模型列表截图,确认可滚动到高级参数、完整显示模型 ID、主题与输入排版正常。
验收使用隔离 userData、构建快照、真实 Electron 和已安装 OMP,只请求本地受控模型服务。未调用用户内网 API 或收费云端模型;自定义服务的非标准路径需手动填写完整地址,任意厂商私有参数、备用网关跨回合签名历史和生产云端全状态仍需针对对应服务验收,不能由本轮协议契约测试推断全部等价。
本轮含 main/preload 修改。保存草稿后完整退出并重新启动 YCode,才会加载全部改动;没有强制关闭当前用户窗口。
@@ -0,0 +1,88 @@
# 自有沙盒首版实施与维护基线(2026-10-18)
> 后续完整接入已开始,见[首批实施与当前差距](沙盒完整接入首批实施-2026-10-18.md)。本文保留首版验收时的范围;后续策略、运行时/依赖及目录事务状态以新记录和维护 README 为准。
## 当前结论
已建立可独立构建、修改、测试和诊断的 YCode Windows x64 沙盒首版。原生执行器、策略、文件事务、审计和维护工具全部在本仓库维护,不依赖 WorkBuddy 的 exe、DLL、策略进程或安装目录。
默认权限的 `execute_command` 已接入自有原生后端:在工作区副本中执行,禁用网络,成功后的文件变更经真实会话审批后写回。原生组件缺失、摘要不匹配或隔离启动失败时拒绝执行,不回退到普通宿主 Shell。
这满足本轮“可以开始维护自己的沙盒”的阶段目标;**不代表 WorkBuddy 沙盒或安全中心已完成 1:1,也不表示所有 YCode 子进程已经隔离**。具体差距与下一批工作见下文。
## 实现归属
| 位置 | 职责 |
| --- | --- |
| `native/sandbox/runner.cpp` | Windows AppContainer、精确 SID 检查、Job、受控 stdio、环境白名单、父进程监控、超时/取消、临时盘符、租约与资源释放 |
| `native/sandbox/probe.cpp` | 仅测试使用的真实 Win32 文件、网络和子进程探针,不打包发布 |
| `native/sandbox/README.md` | 构建、协议、维护操作、安全边界、限制和微软 API 来源 |
| `scripts/build-sandbox.mjs` | 自动定位本机 MSVC/SDK,静态运行库编译,生成源码/产物 SHA-256 清单 |
| `src/main/sandbox/policy.ts` | 真实工作区路径、敏感名称、junction/符号链接、多重硬链接、设备名称及 ADS 路径检查 |
| `src/main/sandbox/service.ts` | 工作区快照、原生产物校验、命令生命周期、文件变更、审批与审计 |
| `src/main/sandbox/transactions.ts` | 独立备份、批量校验、提交补偿、冲突保护与恢复 |
| `src/main/sandbox/maintenance.ts` / `scripts/sandbox-maintain.ts` | 状态、自检、运行/事务记录、恢复与遗留 profile/盘符清理 |
| `src/main/agent/tools/index.ts` / `conversation-tools.ts` | 宿主文件工具与默认命令接入;OMP 和备用循环共享入口及 run/tool-call 归属 |
| main / preload / shared IPC | 配置真实 userData/resources 路径;应用退出取消原生执行;提供 `window.ycode.app.sandboxStatus()` |
`package.json` 增加独立构建、原生回归、整应用回归和维护命令,生产构建先编译原生执行器。Electron 打包资源只选择 `ycode-sandbox.exe` 和 `manifest.json`,不包含探针。生成的 exe/manifest 被 Git 忽略,源码与构建入口留在仓库。
## 执行边界
每次命令使用独立 AppContainer 身份。受限进程先暂停启动,校验其 AppContainer 标记与指定 SID,再加入禁止 breakaway 的 Job,之后才执行。Job 限制 32 个活动进程与 1 GiB 总提交内存,根命令退出、取消或超时后终止后代,并等待进程树静止。
原生执行器监视实际启动父进程。Node 启动它时使用 Windows detached 进程模式,使父进程突然退出后,原生监视器仍能完成 Job/profile/盘符清理;这项行为已通过独立父进程退出探测。
工作区副本通过一个独立临时盘符呈现,解决 cmd/PowerShell 查询驱动根目录时被拒绝的兼容问题。盘符分配有互斥,清理按精确目标匹配,不修改原工作区 ACL。
子进程只继承允许的 stdio 句柄与固定环境变量;模型供应商凭证不继承。未授予网络 capability,已通过本机 TCP listener 实测确认没有建立连接。
## 文件、审批和恢复
默认宿主文件工具增加敏感路径与链接检查,写入转入独立事务。命令执行前过滤受保护文件、VCS、依赖目录和链接,执行后校验正常文件并计算 create/update/delete。成功命令产生变更时,由现有 `InteractionBroker` 发出真实审批,批准后才提交。
所有变更在提交前校验原文件摘要;审批期间被外部编辑会拒绝覆盖。先持久化 before/after 备份,再应用变更。中途失败尝试补偿;补偿不完整会报告事务 ID 和 `recovery-required`。成功提交后即使运行审计保存失败,也保留真实的 committed 结果并返回审计失败标记,避免误报未写入而重复操作。
事务记录独立于聊天消息,维护工具可在另一进程中恢复。恢复拒绝覆盖后续外部改动,重复恢复不产生额外变化。快照副本在结束后清理,租约、清理状态、审计和事务备份保留在 `userData/sandbox`。
## 维护入口
```powershell
npm run sandbox:build
npm run test:sandbox
npm run test:sandbox:app
npm run sandbox:maintain -- status
npm run sandbox:maintain -- self-test
```
有状态操作必须显式提供实际 `userData/sandbox` 的绝对路径:
```powershell
npm run sandbox:maintain -- audit --state-root 'C:\实际数据目录\sandbox'
npm run sandbox:maintain -- transactions --state-root 'C:\实际数据目录\sandbox'
npm run sandbox:maintain -- restore 事务UUID --workspace 'C:\实际工作区' --state-root 'C:\实际数据目录\sandbox'
npm run sandbox:maintain -- cleanup --state-root 'C:\实际数据目录\sandbox'
```
修改规则时从 `policy.ts` 入手,补对应的对抗性测试,再运行原生与整应用两组回归。改原生协议时同步 runner、manifest、shared 类型与 service。`sandboxStatus` 的 installed 仅表示组件校验通过,系统实际隔离能力以自检/命令执行为准。
## 验证
- `npm run test:sandbox`:最终 **52/52** 通过,报告 `.tmp/sandbox-regression-EDmDWK/report.json`。覆盖真实 token/Job、目录与 PowerShell、文件越界读写、后代隔离、breakaway 拒绝、敏感文件、TCP、审批/拒绝/外部冲突、文件增改删与恢复、Unicode、取消/超时、父进程退出、硬杀后的租约清理、审计故障、组件缺失/摘要篡改、快照清理及完全访问的链接写入兼容性。
- `npm run test:sandbox:app`:最终完整链路 **10/10** 通过,报告 `.tmp/sandbox-app-0ljMle/report.json`。此命令先执行完整生产构建(原生编译、全项目 typecheck、Electron 三端构建),再以实际 production main/preload/renderer、真实 OMP RPC、仅 localhost 的受控模型验证审批、拒绝、取消、消息落库及真实工作区结果。
- `npm run sandbox:maintain -- self-test` 通过;维护 CLI 在独立新进程中恢复测试事务,确认新建文件被恢复移除。遗留租约清理命令亦在自有测试状态目录验证成功。
- `npm run smoke`、`npm run test:conversation-ipc`、`npm run test:omp` 通过。OMP 历史测试 mock 缺少现有 `powerSaveBlocker` 导出,已仅补齐测试替身。
- 全项目类型检查已在并行 UI 修改完成后恢复通过;最终生产构建和整应用复核均通过,日志 `.tmp/sandbox-app-final-verification.log`。
所有测试使用自有临时目录/隔离 userData;未发送真实模型请求,未对用户真实工作区运行测试命令或写回测试产物,未操作 WorkBuddy 运行状态。用户当前 YCode 未被强制重启,新主进程代码在下次完整启动时加载。保留用户授权的并行上下文用量与引用 UI 修改。
## 当前限制与下一批工作
1. 当前实现 Windows x64。其他平台拒绝默认命令。第三方语言解释器、npm 等工具链和受控依赖缓存尚未自动接入;默认可使用 cmd 与系统 Windows PowerShell。
2. 快照上限为 10,000 项、128 MiB、单文件 8 MiB;默认超时 120 秒,输出每路保留 2 MiB。这些文件容量限制是快照/写回检查,尚非 OS 磁盘配额。
3. 当前同步文件增改删及文件所需父目录;空目录创建和目录删除的写回显式拒绝。大型依赖项目、目录/元数据语义、工具链兼容性需继续完善。
4. OMP 进程本身、用户手动终端、本地 MCP 服务进程和远端连接器仍沿用原有信任/审批模型,不能算作已被此后端整体隔离。
5. AppContainer 按 Windows ACL 允许读取部分共享系统资源;没有实现任意读取路径的全局白名单、DLP、原版网络域名/注册表/注入钩子规则。对抗具有相同用户权限的宿主进程并发替换路径,需要进一步实现原生句柄相对文件代理。
6. 事务备份/审计的保留与自动压缩清理、停电级持久性、安全中心 UI、独立安全审查以及原版相同场景比较仍需逐项验收。
本轮没有新采 WorkBuddy 实现。设计线索沿用上轮 REA `ev_74bf702462bc37612d2a68ea2e1c93b30b77dd5d99eaf1e961f1ab36d626c5e3` 与 `ev_2860ca8fafaf310ee1adc2ebe31530c868da0ecbb41a6f451e8e8e86b447a078`;新增的 Windows 原生实现使用微软公开 API,来源与边界见 `native/sandbox/README.md`。未将本轮原生/OMP 通路成功计作完整 1:1。
@@ -0,0 +1,68 @@
# 表格引用与附件样式修复(2026-10-18)
## 本轮结果
对应[此前核对报告](表格与附件样式核对-2026-10-18.md)的表格、文件、图片、资源/技能、发送后展示和混合状态,已完成本轮实现及限定范围验证。表格现在使用输入框内的“表格图标 + 会话表格”短标签;文件/图片/文件夹/技能共用原版标签规则;发送后与编辑态复用同一呈现。
本轮没有向模型发送真实消息、改变用户会话或强制重启用户应用;隔离验收的模型响应为受控数据。由于包含主进程与 preload 变更,用户当前窗口需保存草稿后完整重启,不能只依赖旧进程内的 renderer 重载。
## 原版来源与观察范围
- 沿用保留的 REA `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`(既有记录为 WorkBuddy 5.8.3)与 CSS `ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`(既有 WorkBuddy 5.8.2 CSS);未重新采集已有材料。
- 渲染器 `src-DbE1_mgM.js` SHA-256 `6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4`、宿主 `contexts-BMeWSUHk.js` SHA-256 `5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b`。
- CSS `src-CyXHA0IC.css` SHA-256 `a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe`。
- `scripts/rea/recover-reference-tags.mjs` 校验固定 Evidence 与摘要,仅从保留正文恢复19个纯 SVG 函数及31条相关样式,保留原选择器及上级条件;不读取安装目录、连接目标协议或执行原版初始化器。
- 表格实际宿主路径为 `createTableMessageQuoteReference` → `createEmbeddedReference` → `displayAsPhrase`;原 `InputBannerRail` 会过滤这些显示块,不能使用 `QuoteReferenceCard` 作为该表格的输入态。
- 本轮再由 REA 检查原有 `9223` 端口,仍为 `provider_unavailable / endpoint_unreachable`;没有擅自导航或重启 WorkBuddy。用户提供的短标签截图可作为结构对照,不能证明所有主题、动画和交互状态均已逐像素一致。
## 已实施
1. **正确标签及原版资源。** 新增 `workbuddy/references/ContentReferenceTag.tsx`、`SourceReferenceIcons.tsx`、`source-reference-tags.css`。文件类23px高、100px圆角、padding `1px 8px`、13px/500/21px、14px图标;表格使用真正的表格 SVG,标签固定为“会话表格”。历史 `preview` 中的 Markdown 不再进入标签、ARIA或 tooltip。
2. **共同内容区。** `ComposerControls` 移除三组框外附件/引用容器,把标签放到圆角 `.cr-input-container` 的内容编辑区;文件、图片、资源和技能不再各占一条外部横栏。原输入框圆角、工具栏、分支/权限/模型区域保留。
3. **混排与键盘。** 使用 Slate 的 inline void 节点承接带引用的内容,支持在当前光标位置插入、文本与多个标签混排、换行、IME保护、选择和键盘删除;最后一个标签移除后回到普通输入框时恢复光标和焦点。原文本输入兼容路径继续保留。富文本区域沿用此前输入高度上限200px,长文件名按源规则截断、折行。
4. **真实内容与显示分离。** 新增 `UserContentBlock`、`shared/user-content.ts` 和可选 `ChatMessage/SendPayload.userContent`,仅记录文本与实际引用的排列;附件、表格快照、资源和技能仍使用原来的真实载荷。主进程按实际集合校验身份、去重、拒绝虚构引用;布局不能替换提示词。发送、失败草稿恢复、编辑、重新生成、磁盘保存和重启恢复保留排列。裁剪首尾空白时同步调整布局。
5. **发送后与编辑。** `UserInlineContent` 在用户气泡内渲染同一套只读标签;移除气泡外文件名、常驻图片和原始 Markdown 标签。`UserMessageEditor` 支持带引用的编辑,取消不改变原消息,提交只移除用户选中的引用;旧消息无需迁移数据即可恢复短标签。普通无引用消息的原有编辑验证仍通过。
6. **悬停与图片。** 默认隐藏移除按钮,悬停时在图标位置替换关闭图标;图片使用类型图标和名称,预加载成功后显示上限300×300的悬停预览,点击接既有图片查看器。资源使用文件/文件夹图标、名称和完整路径提示;技能接相应图标与同类标签。
7. **逐项状态。** 粘贴/拖放时立即插入每个文件的 processing 标签,显示原加载图标及`.7`透明度;失败保留失败标签和原因。完成后在原位置替换为真实附件 ID;取消后即使暂存晚到也会清理,不进入发送载荷;失败项移除前禁止发送。
8. **可用预览入口。** 新增 `attachments.preview(id)`,按已登记附件读取实际内容;右栏后续的完整 Markdown/代码读取仅允许这个已登记路径,重新核对附件 ID 与真实保存路径。其他工作区外路径继续拒绝。原生文件选择保留只读显示用途的 `sourcePath`,缓存路径不冒充原始来源显示在标签提示里;粘贴文件没有来源路径时使用文件名。
9. **发送边界。** 修正只有资源或技能时被渲染器/主进程“空消息”判断静默忽略的问题;仍使用实际能力与资源校验。
10. **错误验收纠正。** 原 `verify-quote-table.cjs` 变为新专项的兼容入口,不再断言“36px摘要横条/强调线/标签必须包含Markdown”为原版正确表现;保留表格菜单几何、来源、加入高亮、移除、IPC快照与历史验证。
11. **预览主题与编辑归属。** 截图复核发现深色附件预览为浅字白底;原版工作台正文暗色规则只识别 `root.dark/cb-dark`,YCode使用`data-theme`。在工作台局部补相同规则的主题标志映射及`--wb-sidebar-bg`绑定,浅深色实际内容均可见;编辑中的资源分类同时传入所属会话,避免工作目录不同导致文件夹被误认成未知文件。
## 验证
最终隔离应用报告:`.tmp/reference-content-tufTSn/report.json`,**44项通过,renderer错误0**。使用实际构建的 YCode、preload、文件系统与正常附件暂存;仅模型回复、两个受控失败/延迟文件为测试替身。
覆盖:原表格菜单与高亮、23px/14px标签几何、输入框内边界、悬停移除、移除后重新添加、键盘删除及焦点、文字中间插入、文件/图片粘贴、拖放、文件夹识别、图片悬停、失败、处理中、取消后晚到清理、浅深色、长文件名与窄窗口折行、发送后的排列与完整引用快照、真实右栏附件预览、图片查看器、编辑取消/提交及旧数据兼容。
重要截图均已实际检查:
- `.tmp/reference-content-tufTSn/table-draft-light.png`
- `.tmp/reference-content-tufTSn/mixed-light.png`
- `.tmp/reference-content-tufTSn/mixed-narrow-dark.png`
- `.tmp/reference-content-tufTSn/file-preview.png`
- `.tmp/reference-content-tufTSn/file-preview-light.png`
- `.tmp/reference-content-tufTSn/edit-dark.png`
代码与主进程验证:
- `npm run build`:最终通过(含后来工作区新增的原生沙箱构建、typecheck及Electron构建),日志 `.tmp/reference-build-final.log`。此前一次构建因并发占用原生exe出现LNK1104,后来占用解除后通过,没有改动沙箱实现或回退并行工作。
- `npm run test:references`:通过,覆盖多行/Unicode/相邻引用顺序、元数据校验、裁剪、历史标签、只读与源图标族。
- `npm run test:chat`:通过;新增编辑裁剪后排列与资源/技能单独发送验证。
- `npm run test:persistence`:通过。
- `npm run test:conversation-ipc`:通过;新增真实原子保存/新实例恢复排列、表格快照保留、附件预览、虚构ID拒绝、元数据路径覆盖拒绝和未登记工作区外路径拒绝。
- `node scripts/verify-user-message-edit.cjs`:通过,报告 `.tmp/edit-5P9kn7/`。
- 专项消息SSR、流式状态栏与完成折叠回归通过。`npm run test:conversation` 总命令仍失败于本轮未修改的沙箱工具运行断言:`execute_command('echo SHOULD_NOT_RUN')` 预期拒绝、当前返回成功;日志 `.tmp/reference-conversation-final.log`。不能宣称整个对话总回归已通过,也没有为通过测试而改掉沙箱断言。
- 本轮相关已跟踪文件的 `git -c core.whitespace=cr-at-eol diff --check` 通过。
## 保留限制
- **完整1:1未验收。** 原版调试端口当前不可达,未取得同尺寸、相同内容的全状态逐像素帧;跨版本静态声明、用户截图及本地专项通过不能替代这项验收。
- 图片、文本/Markdown/代码预览已实测;Office/PDF等二进制专用查看器继续受现有预览能力限制,不能以标签图标对齐宣称这些查看器已复刻。
- 粘贴/拖放的逐文件处理状态已验证;当前原生选择器接口一次返回已暂存的附件列表,其内部每个文件的准备进度未改成事件流。
- 带引用的全文撤销/重做、富格式剪贴板携带原引用块、多窗口草稿和原版全部动画尚未做完整等价验收。
- 本轮新增的 Slate四个包均锁定版本。安装时使用 `--legacy-peer-deps` 是为保留工作区已有 `electron-vite@5` 与 `vite@8` 的 peer 冲突;未强制更换现有构建栈。新增包为 `slate@0.143.0`、`slate-react@0.142.0`、`slate-dom@0.142.0`、`slate-history@0.140.0`,已通过实际应用及构建验证;不声称原版使用相同包版本。
## 文件范围与交接
主要文件:新增 `src/shared/user-content.ts`、`workbuddy/references/*`;调整 `ComposerControls`、`ChatPane`、`MessageBubble`、`MessageQuoteTag`、`UserMessageEditor`、工作台局部主题adapter、chat store、共享类型/IPC、主进程回合服务、附件服务与preload;新增恢复脚本和引用专项,扩充chat store/IPC回归。本轮未提交代码,保留全部原有和并行未提交修改。
@@ -0,0 +1,67 @@
# 表格引用与附件样式核对(2026-10-18)
> 后续用户已授权实施,本轮修复与验证见[修复记录](表格与附件样式修复-2026-10-18.md)。下文保留修复前的核对结论,不代表当前实现。
## 结论与本轮范围
用户提供的两张截图确实反映了结构差异:原版是圆角输入框内的“表格图标 + 会话表格”短标签,YCode 是输入框外的 Markdown 摘要横条。文件、图片、文件/文件夹引用以及发送后的展示也尚未对齐。
本轮是核对与问题收集,没有修改业务代码、重启用户应用或调用模型。以前的表格引用专项通过只能证明当时实现的功能与选定 CSS 声明符合测试,不能证明选中了原版正确的渲染路径。TODO02 的输入态、发送态及组合验收重新打开。
## 证据与限制
- 用户原版截图:`C:/Users/Administrator/AppData/Local/Temp/codex-clipboard-5d262c85-84aa-4796-a383-21057ffec352.png`;用户 YCode 截图:`C:/Users/Administrator/AppData/Local/Temp/codex-clipboard-51238fe6-c4f4-4202-b9c7-47578d46d5d3.png`。截图本身不证明版本号、悬停、点击、上传中或失败状态。
- 沿用已留存的 REA 证据,不直接读取 WorkBuddy 安装文件或连接目标协议。运行时脚本索引 `.tmp/rea-runtime-scripts.json` 的来源为 `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`,原有记录标注 WorkBuddy 5.8.3。
- 上述索引内 `src-DbE1_mgM.js` SHA-256:`6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4`;`contexts-BMeWSUHk.js`:`5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b`。本轮重新计算正文摘要,与 REA 记录一致。
- 样式沿用既有 WorkBuddy 5.8.2 REA CSS 证据 `ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`,`src-CyXHA0IC.css` SHA-256:`a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe`。与稍后宿主脚本合用说明结构和声明;不把跨版本静态证据当作当前所有运行态的像素验收。
- 表格菜单几何另有 `ev_38cca1ac66606ab07fb495035e3509f5c4d8ec3e5e2b74d0fdcfcfadb120da3b`,它不能单独证明“添加后显示了哪个组件”。
- 本轮 REA `list_electron_targets` 请求原有 `http://127.0.0.1:9223` 返回 `provider_unavailable / endpoint_unreachable`,没有新采原版页面,也没有为核对而重启原版。原版当前悬停、图片预览、窄窗口、失败和编辑等实时画面仍未补验。
## 原版渲染路径已经查清
1. `installConversationMessageQuoteEvents` 的 table 分支调用 `createTableMessageQuoteReference`;中文 `messageQuote.tableLabel` 的值是“会话表格”。
2. `createEmbeddedReference` 创建 `payload.displayBlocks`,其中 `buildEmbeddedDisplayBlock` 写入 `displayAsPhrase: true`、`displayAsContext: false`、表格 SVG、短标签、来源和表格 Markdown 快照。
3. `InputBannerRail` 明确过滤掉 `displayBlocks` 全部为 `displayAsPhrase` 的引用。因此不能因为原版 bundle 里存在 `QuoteReferenceCard` 就让所有表格进入该卡片。
4. 文件、图片和文件夹引用也生成短语内容块,由共同的标签渲染器显示。`createAttachmentBlock` 设置文件/图片图标和文件名;`createReferenceFileBlock` 设置文件/文件夹图标、`displayText: name`,完整路径保留在元数据。
5. 标签渲染器根据类型、图标、只读态、处理状态和上下文选择样式。普通文件类标签命中 `._fileTag_1krka_111`:23px 高、100px 圆角、padding `1px 8px`、13px/500、21px 行高、14px 图标。短标签内部 gap 为 4px;正文内短语与独立上下文行还有不同的包裹和间距,不能只复制一个高度。
6. 图片在标签上提供 `ImagePreviewTooltip`,图像预加载成功才展示预览,源码上限 300×300;不是直接把缩略图固定塞进标签。悬停移除会隐藏原图标,在同一位置显示关闭图标。
7. 发送后,`convertUserFrame` 分开处理纯上下文块与用户内容块;`UserContentRenderer` 使用只读内容编辑器保留短语块。表格和附件不能统一迁到气泡外,也不能把表格改成普通文本引用图标。
## 已确认差异
| 项 | YCode 当前表现与定位 | 原版依据 / 应修方向 |
| --- | --- | --- |
| 表格输入态 | `ComposerControls.tsx:424` 统一选 `MessageQuoteCard`;引用在 `.cr-input-container` 外。`TableMessageQuoteAction.tsx` 把整个 Markdown 压缩进 `preview`。实测卡片 36px 高、圆角10,并带强调竖线。 | 使用表格短语标签,显示“会话表格”和表格 SVG,放进输入内容区;Markdown 仍作为完整引用载荷保存。 |
| 普通文件 | `ComposerControls.tsx:430` 与 `composer.css:236` 自定义附件卡。实测 34px 高、圆角6、12px/18px,前方直接显示“文件”两个字,关闭按钮常驻右侧。 | 共用来源标签的文件图标、23px 高胶囊样式、13px 字体和图标位置的悬停关闭规则。 |
| 图片 | `ComposerControls.tsx:433` 与 `composer.css:249` 使用24×24常驻缩略图,并沿用34px自定义卡;没有该标签的图片预览 tooltip。 | 图片图标与文件名短标签,加原版图片悬停预览;保留真实图片载荷及发送能力。 |
| 资源引用与能力标签 | `ComposerControls.tsx:443` 使用另一套 `.wb-composer__resources`,实测32px高、圆角6、12px/18px、完整路径、没有文件/文件夹图标。`ChatPane.tsx` 的引用技能也使用此自定义资源样式。 | 将文件/文件夹/技能按各自类型接共同标签规则;名称、完整路径提示、图标、正文内位置分别处理。技能的所有交互态本轮未做原版运行验证。 |
| 发送后与历史 | `MessageQuoteTag.tsx:37` 使用文本引用 T 图标和 Markdown 摘要,实测22px高/圆角6。`MessageBubble.tsx:75` 将附件放在气泡外;非图片只有文件名文本。 | 复用正确表格/附件短标签,按原始内容块组织用户气泡与只读态;保留来源跳转和附件预览。当前表格快照/来源已持久化,不应为修样式而丢失。 |
| 混合布局及状态 | 表格、文件/图片、资源分别成行并都在输入框外,内容越多越抬高消息区域。附件 `stageFiles` 完成后才加入列表;处理中无逐项标签,失败为区域级文案。 | 对应原版内容区布局、折行和正文顺序;按文件保留 processing/failed 状态,以加载图标、`.7`透明度、失败颜色和提示呈现。当前运行画面的动效/错误色仍待 REA 补验。 |
现有 `textarea` 与分离的 `quotes / attachments / resources` 数组还能维持传输功能,但不能表达“文字中间插入引用”的光标位置及混合顺序。完整还原混排、选择、键盘删除和编辑恢复时,需要内容块编辑/只读投影;只把三组 div 移进边框不能算全部对齐。
## YCode 实际画面核对
本轮运行 `.tmp/audit-attachment-styles-20261018.cjs`,使用已构建 YCode 的真实主进程、preload、附件暂存和粘贴/资源引用链路,独立 userData/工作目录;未调用模型,历史消息为测试数据。
结果:退出码0、renderer错误0。五组状态:表格草稿、表格+文件、表格+文件+图片、混合资源浅色、混合资源深色。报告包含元素实际边界、是否位于输入框内、字体/行高/圆角/背景、图片尺寸与关闭按钮。
- 报告:`.tmp/attachment-style-audit-8rW9bB/report.json`
- 浅色混合:`.tmp/attachment-style-audit-8rW9bB/mixed-references-light.png`
- 深色混合:`.tmp/attachment-style-audit-8rW9bB/mixed-references-dark.png`
- 历史完整页面:`.tmp/attachment-style-audit-8rW9bB/history-full-dark.png`
以上是缺陷复现和测量,不是修复结果,也不是两边同状态像素对比。普通文件、图片、资源的边界均测得 `insideComposer: false`。
## 后续修改与验收顺序
1. 修正表格引用的分支和标签数据:标签文案/图标与 Markdown 载荷分开,旧数据按 kind 恢复正确显示。
2. 恢复共同标签组件与输入内容区布局,让表格、文件、图片、资源及技能使用有类型的渲染规则,包含悬停关闭、名称截断、路径提示和图片预览。
3. 把发送后、历史重载及编辑态接同一内容块投影;保留本地附件路径、引用快照、来源、清理与重试逻辑。
4. 接逐项处理/失败状态;验收多附件折行、长文件名、文字混排、浅深色、删除、图片预览及恢复。
5. 修正旧 `scripts/verify-quote-table.cjs` 的错误视觉期望。其现有卡片36px/圆角10/强调条断言,以及历史标签包含 `| 项目 |` 的断言,正好把本次错误写成了“原版正确值”。保留真实引用内容、来源、草稿和 IPC 验证。
6. 对原版已知证据可直接恢复的部分先实现;未知交互用 REA 补帧,记录版本和 Evidence ID,再做同尺寸同状态像素比较。
本轮只更新本记录、总清单状态及接手记录;未运行构建和业务回归,因为没有修改业务源码。主工作树的既有修改全部保留。
技能使用:沿用 `reverse-engineer-anything` 的固定证据/未知项边界及 `ui-design` Audit 的先报告原则。读取了两份 `SKILL.md`、REA `runtime-observation.md`,以及 UI Audit 的 `feature-playbooks.md`、`ship-readiness.md`、`output-adapters.md`;没有加载设计改版指导或引入新配色/字号体系。本记录是定向原版一致性核对,不是全站发布就绪审计。
@@ -0,0 +1,34 @@
# 设置打开延迟诊断(2026-10-18)
用户反馈点击设置后约 1 秒才显示。本轮只诊断,未修改生产代码、未重启或注入用户当前 YCode、未读取/改写真实账户配置。没有查看 WorkBuddy,此任务不需要原版取证。
当前代码会在首次打开和每次关闭后重新创建设置 BrowserWindow,再加载主界面使用的同一份 index.html;设置关闭即销毁。renderer 的条件分支发生在 App 与 SettingsWindow 静态导入后,因此设置窗口仍会解析、执行包含主界面依赖的公共入口。当前生产入口 JS 为 5,016,896 字节,公共 CSS 为 3,424,320 字节。相关位置:`src/main/index.ts:268`、`src/renderer/src/main.tsx:3`、`src/renderer/src/components/SettingsDialog.tsx:81`。
诊断使用 GPU 开启的隔离真实 Electron 实例,加载现有 out 构建、真实 preload/IPC、本地独立数据目录。两轮不启用 tracing 的 11 次新窗口打开(包含首次打开、关闭重开、不同设置页和真实账户菜单“设置”点击)结果如下。时间从发起打开到对应事件,FCP 是浏览器首次内容绘制,不能等同所有控件和数据全部就绪。
| 指标 | 实测 |
| --- | --- |
| 原生窗口 show | 288~652 ms,平均 399 ms |
| 首次内容绘制 FCP | 327~708 ms,平均 474 ms |
| 真实账户菜单“设置”点击→FCP | 452 ms;菜单点击后约 14 ms 请求已发出并开始加载 |
| 已打开窗口重复 show/IPC 返回 | 约 0.7~1.1 ms;不代表切换页内容已经完成绘制 |
| 首屏 config/workspace/appInfo/models/workspaces 的 IPC 处理 | 单项约 0.01~1.17 ms;未阻塞窗口创建及页面加载 |
另一次开启 tracing 的诊断用于定位阶段,不能与未启用 tracing 的样本混算。首个设置渲染进程中,入口模块编译约 63 ms、模块执行约 98 ms,公共 CSS 解析约 19 ms;其余还有渲染进程/上下文启动、文件加载、布局及绘制。trace 中 V8 的细分事件有嵌套,不能将所有 Parse/Compile 行相加当作总耗时。原生 ready-to-show/show 还会早于 React 内容的首次绘制,存在几十至一百多毫秒的无内容窗口阶段。
结论:主要延迟是每次重走完整页面启动链路,且入口携带不属于设置的代码与样式。首屏数据读取不是本次隔离测量中的主要瓶颈;菜单中没有人为等待 1 秒的逻辑。用户所述精确 1 秒未在其当前窗口直接复现,隔离样本有明显波动,不能将 1 秒当作固定测量结果。
建议修复边界:
1. 设置使用独立 renderer 入口,仅加载设置需要的 React、基础主题和导航样式;分离主界面的聊天、Markdown、工具及工作台依赖,设置子页按需加载。
2. 调整设置窗口生命周期,关闭后可复用,必要时在主窗口空闲后预加载。复用时同步最新配置与导航目标,明确清理临时编辑/错误状态,避免旧数据或意外保留未保存表单。保留显式退出时销毁与现有 IPC 归属校验,并相应调整当前“关闭后新建窗口”的测试约定。
3. 首次加载保留可立即绘制的窗口壳/占位;不能只提前 show 一个白色窗口来声称优化完成。配置读取继续异步,不阻塞窗口壳。
证据位置:
- `.tmp/settings-latency-CicVg0/report.json`:第一轮未开启 tracing,5 次新窗口、1 次已有窗口复用。
- `.tmp/settings-latency-R7szKf/report.json`:第二轮未开启 tracing,6 次新窗口(含菜单入口)、1 次已有窗口复用。
- `.tmp/settings-latency-H2wSPl/trace.json`、`analysis.json`、`report.json`:独立跟踪样本。
- `.tmp/measure-settings-latency.cjs`、`.tmp/analyze-settings-latency.cjs`:隔离诊断与 trace 聚合脚本,仅用于 YCode。
本轮只运行隔离诊断,没有生产实现变更,因此未重跑构建或业务回归。最终优化效果需修改后以同样口径重新测量首开、关闭重开、导航切换和实际内容绘制,并验证表单状态、主题和设置数据刷新。
@@ -0,0 +1,48 @@
# 设置打开性能修复(2026-10-18)
用户确认修复设置打开慢,并补充:普通“设置”入口默认应进入“通用”。本轮已完成实现和隔离应用验证;当前用户窗口没有被强制重启,主进程/preload 更新需要完整重启 YCode 才能加载。
设置窗口改为独立 `settings.html` / `settings.tsx` 入口,首次使用创建,关闭后隐藏并复用同一渲染进程。设置子页按需加载并缓存代码;关闭时卸载页面,清理搜索、未保存模型表单、密码输入和快捷键录制状态,重新打开时读取最新配置。主窗口关闭时销毁隐藏缓存窗口,正常退出仍销毁窗口。
普通设置入口(账户菜单、标题菜单、Ctrl+,、未传 section 的 IPC 和内嵌后备入口)默认“通用”;显式 `model/new` 仍进入添加模型。设置渲染完成后通过带请求 ID 的信号唤起窗口,连续请求以最后一个目标为准,关闭后到达的就绪信号不会重新显示窗口。新信号只接受所属设置窗口主 frame;沙盒策略来源验证已适配独立设置入口,没有放宽到任意页面。
初始 HTML 有可绘制的主题占位,页面加载期间保留设置导航和内容占位。共享按钮、开关和下拉样式显式随窗口壳加载;专项验证发现通用页此前隐含依赖模型页导入的 Select CSS,已修复直接打开通用页时下拉高度变成 21px 的回归,恢复为 32px。按需加载没有采用会引入约 300ms fallback 延时的 Suspense 页面切换,改为代码缓存和明确加载状态。
主要文件:
- `src/main/index.ts`:窗口缓存、导航/就绪/关闭竞态、退出清理、独立入口和策略校验。
- `src/preload/index.ts`、`src/shared/ipc.ts`:设置窗口生命周期信号及请求 ID。
- `electron.vite.config.ts`、`src/renderer/settings.html`、`src/renderer/src/settings.tsx`:renderer 多入口。
- `src/renderer/src/components/SettingsDialog.tsx`、`settings-panel-loader.tsx`、`src/renderer/src/styles/settings-entry.css`:窗口壳、共享样式、按需子页、刷新与清理。
- `src/renderer/src/App.tsx`、`src/renderer/src/workbuddy/user-menu/AccountMenu.tsx`:默认进入通用。
- `scripts/verify-settings-performance.cjs`、`package.json`:新增 `npm run test:settings-performance`,测量真实页面内容和后续绘制帧,验证生命周期和导航;使用本次构建副本,避免其他任务重建 out 时丢失哈希资源。
- `scripts/verify-settings-nav.cjs`、`scripts/verify-account-menu.cjs`:更新默认页面和缓存窗口的验收约定。
验证结果:
| 项目 | 结果 / 证据 |
| --- | --- |
| 完整生产构建、最终 typecheck、相关 diff --check | 通过;`.tmp/settings-build-final.log`、`.tmp/settings-typecheck-final.log` |
| 生产真实窗口性能与生命周期 | 26 项通过、0 renderer 错误;`.tmp/settings-performance-plMGVs/report.json` |
| 独立 Vite 开发模式 + 真实原生窗口 | 26 项通过、0 renderer 错误;`.tmp/settings-performance-vmVayL/report.json`;测试服务器已关闭 |
| 导航与账户菜单 | 20 / 64 项通过;`.tmp/settings-nav-uFrgQk/`、`.tmp/account-menu-UyqRJA/` |
| 通用、个性化、记忆与进化 | 43 / 34 / 25 项通过;`.tmp/general-settings-h6QOeC/`、`.tmp/personalization-settings-QRlcKp/`、`.tmp/memory-settings-bRs1wp/` |
| 外观、快捷键、智能体 | 21 / 32 / 17 项通过;`.tmp/appearance-settings-QxvT5P/`、`.tmp/shortcuts-settings-LpJa3s/`、`.tmp/agents-settings-P9eimH/` |
| 关于与获取帮助 | 20 项通过;`.tmp/about-help-settings-shGlBh/` |
| 共享样式对对话引用/附件的回归 | 44 项通过;`.tmp/reference-content-Mcp7jt/` |
| 修改前后浅色/深色 9 页计算样式 | 18 个状态、162 组选择器快照零差异;`.tmp/settings-visual-final-diff.json`。已人工查看最终浅色通用和深色模型截图 |
性能口径:以前诊断记录中的 FCP 为浏览器首次内容绘制。此次进一步记录所选页面真实控件出现(contentMs),以及它们出现后两帧(frameMs),避免将占位或 IPC 返回误当作加载完成;不同口径不直接相减计算精确提升百分比。
| 指标 | 生产构建实测 | 独立开发模式实测 |
| --- | --- | --- |
| 第一次原生窗口 show | 101 ms | 185 ms |
| 第一次通用页内容出现 | 191 ms | 277 ms |
| 第一次通用页内容后两帧 | 231 ms | 351 ms |
| 缓存窗口重开:原生 show | 9~16 ms | 21~29 ms |
| 缓存窗口重开:通用内容出现 | 17~21 ms | 35~44 ms |
| 缓存窗口重开:内容后两帧 | 23~28 ms | 50~61 ms |
初始静态脚本从 5,016,896 字节降到 637,012 字节,基础与共享控件 CSS 从 3,424,320 字节降到 230,103 字节;其后仅加载目标子页代码和必要样式。首次打开仍有创建原生窗口/渲染进程的成本;缓存保留一个渲染进程及代码,页面本身在关闭时卸载。表中数据是隔离实例测量,设备负载会带来波动,当前用户旧进程中的新效果尚未验收。
测试同时覆盖显式添加模型、未保存编辑器清理、跨窗口模型保存后的隐藏/可见刷新、启动中的连续目标切换、最小化恢复、加载中关闭和重开、独立入口的沙盒策略读取授权以及主窗口关闭时清理隐藏缓存。测试没有发送真实模型请求,也没有操作 WorkBuddy 或新增原版证据;这是 YCode 性能与回归验收,不能据此宣称 WorkBuddy 全状态 1:1 验收完成。
+92
View File
@@ -0,0 +1,92 @@
# 资料库模块拆解(2026-10-17)
> 开发执行入口:[三模块开发计划](三模块开发计划-2026-10-17.md)。首批以跨任务真实产物索引与浏览为交付,后续接个人资料、编辑和引用;共享接口与文件边界由主上下文统一。
> 新增源码核对:[对话资料沉淀与任务引用](对话资料沉淀与任务引用确认-2026-10-17.md)。普通/项目选择树、显式space-doc+library技能、个人/项目保存入口及todo关联已有固定源码证据;原版自动检索、解析时机和底层存储关系仍未知。下文“未拆解引用”等为上次观察边界,本次补充以新记录为准。
本轮用户授权拆解此前仅记录的四个模块。本文件只记录原版证据、YCode 差异和后续任务,不表示实现或 1:1 验收完成。原版观察全部通过 REA;没有新建、上传、修改、分享或删除原版资料。
## 1. 原版证据与边界
本轮原版为 WorkBuddy **5.8.3**、Windows、深色、1336×902。5.8.2 已有有效证据继续复用,不以新版本证据覆盖旧版本结论。
| 证据 | 归档 | 覆盖与限制 |
| --- | --- | --- |
| `ev_3d653093d5a6c22b41cf5cf7a8e39e1b618fd4b11ae32ac9620c7655503d5ff6` | `.tmp/rea-four-modules-launch-20261017.json` | 既有本地 REA lifecycle provider 启动安装的 WorkBuddy;启动前没有同名进程,没有关闭用户任务。只证明进程创建。 |
| `ev_ea3b4d82afab5f6ee4f8451b1debb2a2b6a2bfbdb34d948d495f83a1e0d76582` | 本会话 REA target list | 确认端点 9223、WorkBuddy/5.8.3、target `1CAD78026104DDABDB43F9EB6BA7635D`。 |
| `ev_f3929b82c6249f5eaa4f7162b87364ceae077a22b204c66fd7f12e3912b033ca` | `.tmp/rea-four-modules-baseline-20261017.json` / `.png` | 导航前的定时任务页,资料库为左栏第五个入口。 |
| `ev_fa8e4adddc7304748c872d3a42e8ee7c51dc7cd71a479a58bffbf4a16cbbf7ee` | `.tmp/rea-four-modules-library-20261017.json`;`library-after` PNG | REA 声明式点击资料库并等待 1s;action frame 为加载骨架,不能拿初始 artifact 当导航后页面。 |
| `ev_82a9ad8ec38c807e465646d749e2cedbb150fec62e30234c97acd0bb309c349e` | `.tmp/rea-four-modules-library-stable-20261017.json` / `.png` | 稳定首页、DOM/计算样式/已归属脚本和 CSS。整体 `policy_filtered`:32 资源、15 越界脚本、2 未归属脚本排除;未取得完整资料库业务源码。 |
| `ev_1d7ecab64cb98e4ce1f1c584dc9f5abb64af0a6ae9fea4d62891bbb4aefe6766` | `.tmp/rea-four-modules-library-controls-20261017.json`;按 step 分开的 PNG | 类型菜单、个人新增菜单、本地产物页;搜索 action 返回 completed,但立即采帧未显示搜索界面,不能宣称搜索交互已拆完。 |
| `ev_b7fe1b4adac2311b04dfa454126363ca382869d6a29664152d497a920c2e8cad` | `.tmp/rea-four-modules-library-local-20261017.json` | 本地产物稳定态 DOM/布局/脚本,整体 `policy_filtered`,34 资源、15 越界脚本和2未归属脚本排除。 |
可复跑探针:`scripts/rea/probe-four-modules-library-20261017.json`、`probe-four-modules-library-controls-20261017.json`。选择器顺序仅对应本次原版布局;复跑前核对当前页面和 target,不能盲用历史 target。
历史 5.8.2 静态证据 `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb` 中 `space-library-picker-D2BL85X8.js` 已确认存在 `fetchSpaces`、`fetchProjectSpace`、`searchSpaceNodes`。这证明文档空间选择器的数据调用入口存在,不证明其后端实现与权限规则。详见 `assessment-space-tools-2026-10-15.md`。
本轮 REA 首页证据包含 `space-library-DZ_aI7LI.js`(4995 bytes,SHA-256 `91bbbd1b9b1602088e8e19d495f29b44da25d8df20028c663163baa8db3d5cbd`)、`space-library-picker-iy0rD3_T.js`(61585 bytes,SHA-256 `648cf973cd4f7b2b7ae8cd7a0db26755d8671583ead81a62bcfa185491901ea3`)及 `library-target-B9F6xlSn.js`(1399 bytes,SHA-256 `5b9beba2df38c87cec4393b8c7b54ec950221269b4cf660970800713a79d3ba1`)。本轮未做这些模块的完整语义追踪,不能凭文件名推出全部资料库能力。
## 2. 已观察的页面与交互
| 区域 | 原版实际观察 | 尚未证实 |
| --- | --- | --- |
| 资料库内侧栏 | 标题、通知图标、搜索、最近、本地产物;我的资料、团队空间两组树和加号;底部容量与升级入口 | 通知内容、团队新增流程、树节点菜单/拖拽/权限和容量结算 |
| 最近首页 | 最近访问 / 我分享的 / 与我共享;列表列为名称、所有者、位置、最近访问;有介绍卡片 | 分享页空/有数据/撤回状态,分页与更新策略 |
| 类型筛选 | 全部类型、文档、表格、网页;当前项有对勾 | 切换后的查询、持久化和组合条件 |
| 我的资料加号 | 新建文档 (.md)、新建表格 (.csv)、新建文件夹;上传和导入(有子菜单箭头)、添加链接 | 保存位置、命名规则、导入子菜单、限制、覆盖和失败恢复 |
| 本地产物 | 返回资料库;全部类型、搜索文件/任务/工作空间、我的收藏;按工作空间 → 任务 → 文件分组 | 收藏持久化、类型全集、搜索匹配规则、缺失文件和打开行为 |
| 本地产物表格 | 名称、类型、更新人、更新时间、大小;名称与更新时间有排序图标;真实文档/代码条目 | 排序切换顺序、跨任务重复文件如何合并、产物生成与磁盘修改的关联规则 |
| 加载态 | 首次进入资料库曾出现标题、正文与卡片形状的骨架 | 骨架最短显示时长、错误重试、离线/无权限状态 |
本轮观察不包含:文档编辑器、表格编辑器、网页发布、团队协作、成员权限、上传/下载结果、资料发送到对话后的实际上下文。不能把列表截图视为编辑/检索闭环已拆完。
## 3. 已取得的几何基线
同一 1336×902 深色视口,主应用侧栏宽约284.28;资料库内侧栏 `[284.28,30,280,872]`,右侧内容从 x≈564.28 开始。标题区高56;搜索按钮 `[296.28,86,256,30]`;最近/本地产物导航高30;资料树行高32。首页页签组 `[608.28,146,248,32]`,三个按钮各80宽;类型筛选100×32;列表宽约679.70,表头36高、正文行48高。
本地产物页返回按钮 `[308.28,47,107,30]`;内容包装 `[308.28,150,1003.70,752]`,滚动列表 `[308.28,197,1003.70,705]`。这些是单一视口实测值;后续必须恢复布局约束、浅色 token、窄窗口和长文本行为,不能写死这些坐标充当自适应1:1。
## 4. YCode 当前状态
- `src/renderer/src/mock.ts` 的 `library` 导航仍为 `implemented:false`;当前 `App.tsx` / `Sidebar.tsx` 只有 chat、capabilities、projects 页面路由,没有资料库页面。
- `src/shared/ipc.ts` 没有 library 集合、资料树、最近访问、收藏或资料检索的专用契约。
- `src/shared/types.ts` 已保留消息的 artifact parts、attachments、resources、quotes,可作为本地产物关联来源;这不是完整产物索引。
- `src/shared/resources.ts`、`src/main/resources.ts` 及 renderer resource store 已支持真实目录/文本/图片读取和产物预览,可复用到打开资料的宿主侧。
- 现有本地工作目录的空间、项目中的资产,以及资料库文档空间必须分别建模;当前没有证据证明原版三者使用同一存储或同一权限语义。
- 没有持久化资料库、跨任务索引、全文检索、资料编辑、分享/团队协议。已有预览与 Markdown 渲染只覆盖显示能力。
## 5. 建议的数据与实现边界(YCode 设计建议)
以下是建议,不是原版字段恢复;最终契约由主上下文统一决定。
1. `LibraryCollection`:个人资料/团队资料的容器。与本地 `WorkspaceRecord` 使用不同 ID 域;保存来源、所有者、权限能力,不用当前账户信息伪造团队。
2. `LibraryNode`:所属集合、父节点、类型(文件夹/Markdown/CSV/网页)、标题、内容引用、创建/更新时间、来源版本。路径和内容读取在主进程处理,renderer 使用受控节点 ID。
3. `LocalArtifactEntry`:关联 workspaceId/sessionId/messageId/产物或文件引用、显示名、类型、大小、可用状态。先从真实产物事件与现有历史回填,不把项目全部源文件无条件当作任务产物。
4. `LibraryUserState`:最近访问、收藏、展开与排序;独立持久化,重启恢复,文件失效有可读状态。
5. `LibraryReference`:引用节点ID、版本、标题、选区或内容快照及来源;发送时冻结,使编辑原资料后已发送上下文不被暗改,恢复历史时仍能正确重建。
6. 用适配层区分本地与云端资料提供者。云端协作、配额/购买、远端权限仍按当前范围暂缓;不能复制原版账户的5GB容量或把本地数据标成原版团队内容。
本地可先做:页面外壳、真实个人文档/表格文件、文件夹、导入、链接记录、最近访问、收藏、搜索、本地产物聚合和对话引用。文档/表格编辑器的功能范围需先补证据,不能把可编辑 textarea 等同于原版编辑器。
## 6. 后续分批任务与验收
| 批次 | 具体任务 | 交付门槛 |
| --- | --- | --- |
| L0 补证据 | 搜索稳定态、上传导入子菜单、文档/CSV/网页详情、节点菜单;浅色及窄屏 | 每项记录REA截图/结构/状态/来源;不能取得的业务源码列明原因 |
| L1 页面骨架 | library 路由、内侧栏、最近页、类型菜单、骨架/空/错误、主题和滚动 | 同数据同视口截图与计算布局对照;切页不丢当前会话草稿 |
| L2 个人资料 | 节点存储与主进程IPC;新建/命名/文件夹/本地导入/链接 | 重启恢复;取消不写入;失败可重试;重复命名和失效来源处理有证据或明确设计记录 |
| L3 本地产物 | 跨任务真实产物索引、工作空间/任务分组、筛选/排序/收藏/失效文件 | 展示只来自真实事件/关联;打开正确来源;切空间/归档/删除后不串归属 |
| L4 编辑与预览 | 据补采接 Markdown/CSV/网页宿主,预览复用已有右侧栏 | 编辑保存/取消/失败保留草稿;类型对应正确预览;不能仅展示成功toast |
| L5 检索与引用 | 最近、搜索、选取资料入对话、已发引用与历史恢复 | 查询结果与内容一致;引用冻结版本;请求上下文与UI来源一致 |
| L6 完整对照 | 原版全状态、深浅色、窄屏、长列表/长标题/空与错误、键鼠与动效 | 功能和视觉分开验收;云端未接范围单列,不给完整1:1完成标记 |
## 7. 阻碍与所需证据
- 当前 REA 已能进入资料库、截图、取DOM/计算样式。阻碍不再是“不能采画面”。
- 资料库页面的完整业务脚本和样式有部分未被当前 file-page 归属规则收录;应由 REA 的精确模块/协议支持补足,禁止直接CDP/evaluate或手写提取器绕过。
- 当前仅有深色单视口。未证实搜索、编辑、导入、团队权限、错误恢复和实际引用行为;后续 L0 需要补采。
- 云端资料持久化、权限、协作及配额服务不在本轮范围。本地替代链路可以做,但必须和原版服务等价性分开验收。
## 8. 本轮验证
只读核对 `mock.ts`、`App.tsx`、`Sidebar.tsx`、shared IPC/types/resources 和 main resources;解析REA归档确认Evidence ID、版本、过滤范围、action steps和PNG来源。新增两个声明式探针及本文件;未改业务代码,未运行构建或应用测试,未声称资料库已实现。
@@ -0,0 +1,31 @@
# 输入框上方成员入口迁移
日期:2026-10-18。用户订正:成员任务入口应放在输入框上面。
## 已实现
- 移除生成态、完成态回复底部的成员入口,在 ChatPane 输入框之前统一挂载一次;与输入区共用宽度,位于消息滚动容器之外。
- 保留收起头像组、展开成员列表、状态实时更新、点击打开右侧成员详情和单独停止。任务完成后,最新一轮的成员仍可查看。
- 活动回合按 assistantId 选取;空闲时只读取最后一条助手回复。新回合尚无成员、活动消息尚未加载、切换到无成员会话或回退移除成员时,不借用旧回合的成员。
- 按会话及助手消息身份重置展开状态,避免新任务继承旧任务的展开列表。
- 输入控件和成员入口放入同一个 input-stack,消除宿主 flex gap 叠加;恢复原版输入任务宿主的12px下间距,未修改已恢复的原版成员列表CSS。
位置依据为同一份WorkBuddy 5.8.2 REA保留证据,完整身份、摘要和源码偏移见 [上方任务区域核对](WorkBuddy输入框上方任务区域核对-2026-10-18.md)。未新增原版采集。
本次是现有成员入口的位置与归属修复,继续使用已恢复的成员列表和既有右侧详情。MainContentCore横向团队栏的完整视觉/主区域成员切换、后台终端列表的真实状态源仍需分别对齐;不能将此次迁移称为上方两个组件全部1:1完成。
## 验证
- `npm run test:subagents:display`:19项通过,含活动回合优先、新回合无成员、缺失消息及最新完成回合归属。
- `npm run test:subagents:ui`:17项通过,正式renderer与隔离IPC快照验证位置/宽度、回复无重复、展开、实时更新、会话隔离、重新水合、新回合替换、取消及回退;0 renderer错误。报告 `.tmp/subagent-display-rFYnKp/report.json`。
- 已实际查看 `subagents-expanded.png`、`subagents-dark.png`、`subagents-narrow.png`;位置在输入框上方,窄屏详情沿用抽屉。
- `npm run test:subagents:app`:真实Electron/preload/OMP及localhost模型服务通过,含生成中从输入框上方打开成员、单独停止和主对话继续、落盘、跟进、审批与清理;0 renderer错误。报告 `.tmp/subagent-app-ErrF75/report.json`。
- `npm run test:conversation`、`npm run typecheck`及完整`npm run build`通过。构建日志 `.tmp/composer-team-build.log`。
没有调用用户付费API,没有重启用户当前窗口。重新加载renderer后显示最新位置;包含下述主进程文案修复的构建在主进程重启后加载。
## 构建阻断修复
构建过程中发现 electron-vite 的 esm-shim 插件用正则定位静态import,会把沙盒Git错误字符串末尾的`before import`误识别为导入语句,并将CommonJS兼容代码插入后一条字符串,导致生成的index.mjs报Unterminated string。已用本地插件同一个正则复现误匹配。
只将 `src/main/sandbox/git-host-source.ts` 中该错误文案改为`before importing the repository`,保持校验分支不变;完整构建恢复成功。未修改打包器或沙盒Git行为。
@@ -0,0 +1,59 @@
# 通用开发角色替换
日期:2026-10-18。用户要求将之前加入的专业角色改成通用、职责不同的智能体。本次是用户定制修改,不继续将修改后的包称为WorkBuddy原始专家包的1:1副本。
## 详情布局与头像修复
用户反馈点开错位、头像缺失。实际Electron复现确认:详情头像外容器同时应用`avatar`和`avatarFallback`,后者的100%宽高覆盖72px固定尺寸,在1440px窗口里头像宽590px、标题可用宽度为0。已拆成固定头像外容器与内部回退层;本地详情和缓存参考详情同步修复,图片加载失败仍保持头像尺寸。简介、标签和角色指令沿用现有主题;指令使用已有Markdown组件排版,长内容在正文区域滚动,头部保持固定。加载时显示提示,切换读取清理旧内容,焦点循环覆盖正文滚动区域和链接。
五个通用角色新增统一机器人头像,分别使用绿、蓝、金、紫、粉色和职责标记。代码原生SVG与256px PNG保存在`resources/agents/avatars/`,可用`scripts/generate-general-agent-avatars.mjs`重建;没有调用图像或模型服务。定义版本2、专家包版本2.1.0。安装脚本检查PNG大小、尺寸、SHA和生产激活后的data URL;配置变更前保留完整包备份,文本通过原注册事务提交,PNG随后单独原子写入。两步不是整个包的单次原子事务,脚本中途失败需重跑或用备份恢复。
本轮备份:`C:/Users/Administrator/AppData/Roaming/YCode/capabilities/expert-backups/general-development-20e82549-fb1b-4972-bdff-93d3d52fcec3/`。已应用真实用户五个包;独立进程冷读取、`--check`及隔离重复执行均通过,重复执行变更数0,原五类指令和技能配置未改。
验证:`npm run build`通过(含原生沙盒构建和两份TypeScript检查);真实正式Electron/main/preload/renderer专项17项通过、0 renderer错误,覆盖五个详情、卡片图片解码、浅/深色、420px/320px窗口、长文滚动、键盘循环/关闭/焦点恢复、无头像回退、详情召唤后的输入框头像及缓存参考详情。报告`.tmp/general-agents-validation-ttZ4Gw/detail-ui-report.json`;构建日志`.tmp/expert-detail-build.log`。已查看`general-agents-with-avatars.png`、`expert-detail-light.png`、`expert-detail-dark.png`、`expert-detail-narrow.png`、`expert-detail-320.png`、`general-agent-avatar-summoned.png`。这些是YCode自身缺陷验证,没有新增WorkBuddy采集,也不作为完整1:1验收。
构建另发现electron-vite的ESM shim正则误把两个既有Git诊断文案末尾的`before import`当成导入语句,导致shim被插入`.git`字符串中。只调整`src/main/sandbox/git-host-source.ts`两条诊断文案,未改沙盒执行逻辑。
当前运行用户窗口没有调试监听端口,本轮未强制重启或清除草稿;重新进入我的专家可读取新头像,布局需要重新加载YCode界面。当前工作区其他并行改动保留,未提交。
## 已应用到用户YCode
| 原角色 | 当前通用角色 | 保留的包ID |
| --- | --- | --- |
| 高级项目经理/关德豪 | 代码探索员 | senior-project-manager |
| 首席C++架构顾问/C++超级专家 | 实现工程师 | cpp-super-expert |
| UI设计师/像素君 | 测试验收员 | ui-designer |
| 高级算法工程师/小算 | 代码审查员 | senior-algorithm-expert |
| 新增 | 问题诊断员 | development-debugger |
已同步修改双语名称与职业、简介、标签、三条快捷提问、默认提问、实际Agent指令及README;版本改为2.0.0。保留原四个内部ID、agentName和Agent文件名,既有会话和选择引用继续可解析。过去消息中已保存的角色身份不改写。
共同指令不预设语言、框架、行业、目录和外部服务,明确委派边界、证据要求和验证报告。探索与审查默认只读;验收不擅自改业务代码,诊断默认先复现与定位,获准后再修复;实现遵守指定文件范围。
移出原专项技能声明和旧文本资源,实际回合不再自动预加载cpp-mastery、minimax-docx、impeccable、frontend-dev、browser-use或algorithm-engineering-playbook。现有API在编辑时保留二进制资源,原头像和字体等未引用二进制可能仍留在包目录,但不再注册或作为角色资源加载。初次替换使用通用回退头像;本轮已改为上方记录的五张通用角色头像,不复用原专业人物头像。
定义保存在 `resources/agents/general-development.json`,维护方法见同目录README。`scripts/configure-general-agents.mjs`通过生产专家包API完成编辑、校验、注册、激活和回合上下文检查;`--check`只核对配置并写本地验证报告,不修改专家包。
实际数据目录:`C:/Users/Administrator/AppData/Roaming/YCode/plugins/marketplaces/my-experts/`。
完整原包、原marketplace及逐文件SHA清单备份在:
`C:/Users/Administrator/AppData/Roaming/YCode/capabilities/expert-backups/general-development-070c2a39-3f03-4576-bda4-82e54e242af2/`。
备份在首次写入前完成并校验逐文件摘要;安装逐个通过生产注册事务,未覆盖无关目录条目。原REA导出和原始证据文件未改动,也未查看或修改WorkBuddy。
## 验证
- 先复制原四份YCode专家包到隔离目录,实际执行替换、校验模式和重复执行;五个角色全部通过,重复执行无改动。报告 `.tmp/general-agents-validation-ttZ4Gw/report.json`。
- 真实用户目录已替换并重新启动独立校验进程读取:五个角色均可列出、激活,15份目标文本一致;新提示词确实进入expertTurnContext,启动技能均为0。部署报告 `.tmp/general-development-agents-rZCsZ0/report.json`;冷读取校验 `.tmp/general-development-agents-2zULma/report.json`。
- 隔离真实Electron、正式main/preload/renderer通过9项界面检查:五卡片、旧显示名消失、五次召唤绑定对应角色及默认提问、0持久化对话、0 renderer错误。报告 `.tmp/general-agents-validation-ttZ4Gw/ui-report.json`。
- 已实际查看 `general-agents.png` 及 `general-reviewer-selected.png`,显示通用职责卡片和输入框中的代码审查员。
- 脚本语法与JSON检查通过。未改应用运行时代码,本批没有重复构建或运行无关回归;验证未调用模型API、未重启用户窗口,未发送消息。
用户当前已打开页面的内存列表未强制刷新;重新进入“我的专家”会重新读取最新包内容。
## 范围与限制
当前改动对应“我的专家”中可手动召唤的通用Agent配置,没有新增spawn_agent的role参数或将这些包自动注入子任务调度器。工具权限和5并发保持现状;职责提示词不是宿主层的额外权限策略。模型实际回答质量不由配置和界面校验代替。
按既有委派模式尝试了边界明确的只读角色审查,但工作通道因后端封存任务不可读取而失败,未产生候选结果;主上下文完成职责边界审查、部署与最终验证。没有将失败的子任务声称为通过。
+326
View File
@@ -0,0 +1,326 @@
# 更多 → 项目:REA 拆解进度
> 当前开发执行入口:[三模块开发计划](三模块开发计划-2026-10-17.md)。先收口现有项目链路:首次发送失败补偿、运行标识、真实状态筛选、详情指令保存同步,再接配置与后续数据页。本轮为源码核对与准备,未修复这些问题。
> 新增资料/待办核对:[对话资料沉淀与任务引用](对话资料沉淀与任务引用确认-2026-10-17.md)。已证显式todo引用和项目资料上传/选择范围;默认tdrive-root仅在teams/cloud发送链路证实,本地路径是否由adapter/内核补入仍待证。不可把scope指针当全量资料读取,也不能按名称断言项目资产与资料库的后台存储关系。
当前状态(2026-10-17 系统拆解轮):已实现并验证“本地单用户项目 + 项目任务关系 + 项目指令注入”的基础闭环;项目协作、动态、计划、资产、成员权限和云传输仍是占位或未实现。属于主待办 TODO 20,整个模块保持未完成、未做完整 1:1 验收。
说明:以下历史章节按时间记录取证和实现过程,其中“尚未实现”“仍待接线”等语句反映当时状态,不代表本文件末尾“本轮系统拆解”的最新结论。
## 本地存储基础
- `src/shared/projects.ts`:独立项目 ID、本地 transport、名称、指令、模板 ID、专家/技能来源及连接器关联;不将空间记录冒充项目。
- `src/main/projects.ts`:`ProjectRepository` 串行创建、临时文件原子替换、重新读取持久化配置、拒绝覆盖损坏配置。项目名称 trim 后非空且最多 15 个 UTF-16 单元;能力引用保留来源与 provider,并去除同身份重复引用。
- 创建表单源码 `CreateProjectDialog` 使用 15 字输入限制、trim 非空条件;提交区分 customExperts/customSkills 与市场 experts/skills,连接器采用 personal auth。YCode 当前契约仅保存引用,尚未实现授权及能力解析,不能称这些能力已生效。
- `node scripts/verify-project-storage.cjs` 通过:12 次并发创建无丢失、ID 唯一、实例重建后恢复、指令/能力关联保存、非法输入阻断、失败后队列可恢复、损坏文件保持原内容。`npm run typecheck` 通过。
- 下一步接入主进程 IPC、preload 和原版页面;项目指令进入真实对话及项目删除/任务历史关系仍需单独实现和验收。
### IPC 与样式接线准备
- 已新增 `YCodeApi.projects.list/create`、preload 对应 `projects:list/create` 调用,以及主进程 `ProjectRepository(userData/projects.json)` handlers。`npm run typecheck`、`npm run build` 和存储回归通过;尚未进行真实 renderer → preload → IPC 的窗口回归,不能据此称创建交互已验收。
- `scripts/rea/recover-project-styles.cjs` 从固定弹窗 REA Evidence 中恢复 `collab-YMJYRnW_.css`,源 SHA `4737f6a8f8f993de933e5ebae0d94b96cb39788e160470490f607508537e5377`。705 条规则限定在 `.yc-project-surface` 后代,保留媒体条件和 keyframe 正文;生成到 `src/renderer/src/projects/source-projects-recovered.css`。生成及 `--check` 一致性检查通过。该 CSS 暂未挂载,不能当作页面视觉已完成。
- 下一步恢复项目页面 DOM、图标和插图,挂载上述规则并接 IPC;然后验证真实创建、重载、筛选与错误状态,继续项目会话关联。
### 项目页插图
- 原版加载图像经 REA `ev_d740e73903dd69444105e2509b238e5912f2c93d3adaa183b05ec7221512ef7a` 采集,`.tmp/rea-todo20-project-hero.json`;源文件名 `landing-hero-B6659kdy.png`。
- `scripts/rea/recover-project-hero.cjs` 只消费该证据的图像正文,校验 PNG、629281 字节长度及 SHA `b382831ec55658c051fed7031302c37e9af8c9199a4becdf8277f962419d1b47`,恢复至 `src/renderer/src/projects/assets/landing-hero.png`;生成与 `--check` 均通过。
- 采集时新建弹窗仍在,REA 标记插图可见性 covered;本记录仅证明加载资源字节恢复,页面上的稳定可见外观以前述项目首页证据为准。
### 页面组件第一批(尚未挂载)
- `ProjectListPage.tsx` 已接真实列表、名称搜索、创建 IPC 与失败信息;新建弹窗保留原版主结构、名称计数及指令字段。类型检查通过。
- 仍缺模板、三类能力选择、项目图标、列表操作和详情/任务关联,组件暂未挂入 App 路由,不能称用户端已交付或创建交互已验收。加载/空态与部分文案仍待按原版恢复。
- 样式生成器补回同一 REA Evidence 内的 26 条共享 modal 规则(逐份 SHA 验证),现共 731 条;修正 body/html 主题选择器被错误放到页面内部导致无法匹配的问题。生成与 `--check` 通过。
### 模板与图标恢复
- `scripts/rea/recover-project-templates.cjs` 静态解析固定项目首页 REA Evidence 的 `project-list-page-DpHRTtk8.js` 与 `zh-cn-C7BTHfrQ.js`,逐份校验正文 SHA;不执行原模块。五类模板的 ID、中文标题/描述、完整指令和专家/技能/连接器关联恢复到 `project-templates.json`;生成与 `--check` 通过。
- 首页模板卡片现按原版 `landing-templates-head/list` 结构生成,点击预填完整指令,创建载荷携带 templateId 与能力引用。引用尚未解析/授权,不代表这些市场能力在本地存在或可调用;尤其连接器必须在后续选择器接入时按实际 registry 过滤,不能将模板 ID 当作已安装连接器。
- `scripts/rea/recover-project-icon.cjs` 从同份首页 layout/svg_nodes 恢复项目 SVG 至 `ProjectIcon.tsx`,每个组件使用独立 clip ID,避免重复卡片引用冲突;生成、`--check` 与 `npm run typecheck` 通过。
- 当前仍未挂载 App 路由,未进行窗口视觉或真实点击创建验收。
### 首页挂载与真实创建回归
- App 增加 projects 路由,Sidebar 普通/窄屏复用 `ProjectMenu`,悬停入口及点击项目进入真实列表;当前菜单仅包含项目,原版其余外部应用入口尚未接入,菜单几何和关闭时序仍是待对齐的适配实现。
- `node scripts/verify-project-page.cjs` 在隔离 Electron、真实 preload/IPC/文件系统中通过:五模板显示、空名称禁用提交、填写名称后创建、磁盘一条真实项目、重载完成后重新导航可恢复项目、renderer 错误为零。生产构建通过。报告 `.tmp/project-page-aaCZyO/report.json`,截图 `project-page.png`。
- 修正测试重载等待:必须等待 `did-finish-load` 后操作,避免旧 DOM 断言成功却采到首页的竞态;最终截图人工检查确为项目页。
- 当前截图显示共享 UI kit 按钮/输入框样式和文字/背景 token 尚未完整进入此作用域;页面顶部几何、项目卡图标/时间、模板弹层/能力选择、详情操作仍未对齐。因此本轮只证明功能入口和创建持久化,不能称视觉 1:1 或整个项目模块完成。
### 共享控件样式修复
- `recover-project-controls.cjs` 从固定弹窗 Evidence 的 `tokens-Df-i-zsO.css` 与 `src-Bh3YZ35H.css` 恢复作用域 token 和按钮/输入框/textarea 规则,共 301 顶层区块;逐份源 SHA 与生成 `--check` 通过。保留浅色默认及 `[data-theme=dark]` 覆盖。
- 首轮仅选含正文颜色的 token 区块导致按钮独立 token 缺失;已改为恢复全部相关 root/dark 自定义属性区块,截图确认新建按钮与搜索输入框恢复尺寸、底色与边框。
- `npm run build` 与真实项目页回归再次通过,最终报告/截图 `.tmp/project-page-Waid6s/`;仅验证浅色功能及画面,深色/精确几何/卡片元信息和全模块仍待验收。
### 顶栏与项目卡片结构
- 固定首页 Evidence 显示 workbuddy-collab 内先有 64px workbuddy-topbar,之后 landing,后者内部顶部 padding 56px。已补回空顶栏 DOM,恢复相应共享规则;共享默认 56px 与项目实测 64px 不同,`project-layout.css` 按运行态明确项目变体为 64px,而非给内容追加偏移。
- 项目卡片补回 36px 图标容器与恢复 SVG,描述改为根据真实 createdAt 生成“添加于…”相对时间,沿用源分钟/小时/天/30天月的分段;不再用项目指令填充该行。
- `verify-project-page.cjs` 新增顶栏64px、header 相对 topbar+64+56、卡片80px、图标36px、添加时间断言。首轮断言发现默认56px,修正后生产构建与真实窗口创建/重载/几何检查通过,报告 `.tmp/project-page-3HpYwT/report.json`。仍未进行同主题逐像素比对或深色验收。
### 深色及能力配置行
- 已接原版深色 hero 声明到 YCode html[data-theme] 主题归属;真实窗口测试新增深色卡片、插图反色/opacity、640px 深色弹窗和15字符输入检查,并输出深色首页/弹窗截图。最终生产构建及回归通过,报告 `.tmp/project-page-CMmzDh/report.json`。
- 创建弹窗补连接器、专家、技能三行及可移除引用;添加从真实 MCP、专家、技能 IPC 列表取得本地能力,可勾选并写入项目引用。模板市场引用明确 title 标注尚未本地执行;模板连接器按实际 registry 过滤。目录加载中禁止提交,读取失败且仍有连接器时阻断提交,避免绕过 registry 校验。
- 本地选择器当前为行内复选列表,尚未复刻原版独立选择窗口,且未完成带真实能力的点击/重载专项回归;不能称选择器 1:1 或能力运行接通。原版模板切换浮层、modal 精确几何与图标、市场引用解析、项目对话接入仍待完成。
### 连接器选择回归与模板切换
- 真实窗口回归先通过 MCP IPC 保存禁用的 stdio 连接器(不连接、不执行),在创建弹窗点击选择、移除、再次选择并提交,断言项目磁盘记录的 connector ID 等于实际 registry ID。最终构建及回归通过,报告 `.tmp/project-page-kf1YKd/report.json`;此证据覆盖引用选择和持久化,不证明工具执行。
- 已补模板切换,沿用现有 REA 恢复 SourceSelect 控件:选择模板替换指令/能力,切换自定义清空指令和能力,保留项目名称;连接器仅保留实际目录可匹配 ID。SourceSelect 与原版此处 Dropdown 是不同组件,当前尚未逐像素等价,也未做模板切换全路径真实窗口专项验收。
- `recover-project-templates.cjs --check` 再次通过;不修改源模板指令以适配本地能力缺失。
### 模板切换实际交互验收
- `verify-project-page.cjs` 新增真实 renderer 选择器交互:等待 registry 就绪后展开六项列表,键盘选择产品需求模板,确认完整角色指令及专家 chips 出现;再选择自定义,确认指令和全部 chips 清空,两次切换项目名称均保留。
- 与既有真实连接器选择/移除/重选/落盘、创建重载及深浅主题回归一同通过,最终报告 `.tmp/project-page-Q8G5qN/report.json`。本测试使用实际页面 Select 状态机,未改写 React 状态或伪造模板保存。
- 此更新关闭“模板切换全路径未经实际交互验收”缺口;仍不关闭原版 Dropdown 外观差异、独立能力选择窗口、项目详情/任务关联及能力执行的缺口。
### 项目详情取证及任务归属
- WorkBuddy 5.8.3 项目详情稳定页 REA `ev_ca0195278ba08b57f2d847e4d96e3a35490f26367b71b810563b01b03470d000`,`.tmp/rea-todo20-project-detail.json` / `.png`:动态、计划、任务、资产四页签;右侧项目配置含指令、连接器、专家、技能和定时任务。未创建原版任务或修改项目。
- 任务页稳定证据 `ev_beb6214ec8bd412a2fe93beefdead0c4416f37dd4246d964f9c5f65aee240f69`,`.tmp/rea-todo20-project-tasks.json` / `.png`:全部任务、全部来源筛选与任务搜索;底部共用输入区,本地任务及工作空间选择在下方。
- 详情源码 `project-detail-page-DdYKdEkr.js` SHA `a5630ae6ad4e053e8d085a569a67c102c4226a14c7f27a3e62e69601d0f8d2c1`(上述固定详情 Evidence)确认本地 `createConversation(cwd,{projectId,...})`,工作目录和项目 ID 为独立字段。
- YCode `SessionContext` / `SessionMeta` 增加 projectId,贯通 session:create、列表、上下文及更新结果投影;创建时核对真实项目 registry,拒绝不存在项目。保留空间/目录语义,不用 projectId 替换 workspaceId。
- 生产构建与真实窗口回归通过,新增 IPC 创建项目任务及列表重载 projectId 一致、无效项目 ID 被拒绝,报告 `.tmp/project-page-SKUgRA/report.json`。仅完成任务归属数据通路;详情 UI、项目任务发消息、指令/能力进入内核仍未完成。
### 项目指令请求接线
- 每轮 chat:send 根据 session.projectId 读取真实项目,`buildProjectPrompt` 将项目名称和完整指令附加到现有 promptText;OMP 与备用通路共用该请求文本。市场能力引用不会被当成执行指令或声称已加载。此为 YCode 宿主适配,尚未证明与 WorkBuddy 内核注入方式等价。
- session:update-context 拒绝改变现有项目归属,目录/模型等更新保留 projectId。缺失项目在发送前报错。
- `verify-project-storage.cjs` 增加按ID读取/缺失项目、空指令及完整指令构造断言,通过。
- `test:conversation-ipc` 新增项目→任务→chat:send→OMP adapter prompt 回归,确认 PROJECT-CONTEXT 指令抵达受控内核替身、用户消息正文仍保持原文、跨项目改归属拒绝。全套回归通过;没有发送真实远程模型请求,不能据此声称真实模型遵循指令或完整内核等价。
### 项目配置更新通路
- `ProjectRepository.update` 与 projects:update IPC/preload 接通名称、指令、模板及能力引用的部分更新;串行读改写、完整输入校验、拒绝未知字段和不存在项目,保留 projectId、transport、createdAt。
- 存储回归验证并发改名/改指令合并、重新实例化后恢复、能力保留、非法更新不落盘。对话 IPC 回归新增更新指令后的下一轮请求,确认实际 OMP 适配器提示包含 UPDATED-PROJECT-CONTEXT。两套回归均通过。
- 项目指令编辑 UI 的下一步集成见下节;详情任务页、配置其他字段与原版像素不属于上述 repository 回归覆盖范围。
### 项目指令编辑入口
- `ProjectInstructionDrawer.tsx` 复用已从 REA 恢复的项目指令抽屉结构:800×560、说明、textarea、字符计数、取消/保存。打开入口暂放在项目卡片更多位置,保存调用正式 `projects.update`,成功刷新列表记录,失败保留输入并显示错误。
- 主进程统一限制 instructions 不超过 10,000 字符。`npm run typecheck` 通过;本轮未运行真实窗口交互回归,编辑保存 UI 及深浅色像素仍待验收。
## 原版运行态
- WorkBuddy 5.8.3 / Electron 37.10.3,REA target `7BBA9C2FB14CBDC4404347707DA76722`。
- Escape 关闭发现应用并等待稳定帧:`ev_1fe35225981ea05e20e4b7d6fa99981830ce9b5342045b22c5ca41b17463f1eb`,归档 `.tmp/rea-todo20-project-closed.json` / `.png`。
- 更多菜单打开帧:`ev_d1a438b97437b2f6a98f5dddcc5c3385184e24a439c5a351365fbbefc39e457d`,归档 `.tmp/rea-todo20-more-open.json` / `.png`。可见项目(BETA)、我的邮箱、腾讯文档、ima、乐享知识库、灵感社区。
- 限制:打开及等待步骤成功,最终 neutral-pointer 清理因操作取消失败;不能把整个探针称为成功。布局快照未包含打开后的浮层,截图证明打开态,不能由本快照推导浮层 computed style。
- 可复跑探针:`scripts/rea/probe-todo20-close-discover.json`、`probe-todo20-open-more.json`。
## 已回收源码结论
来源:REA `ev_2b8db4eb73c59a8d3d80c3e37db5ea9b5bf1d13390d5238d81ff2cc53d11dc09`,`.tmp/rea-todo18-main-scripts.json`。
- `contexts-DgzzFjnf.js` 的 SidebarNavigation 使用 `conversation-list-more-dropdown`,右侧起点定位、hover 触发、portal 到 body。点击不是唯一触发方式,YCode 应保留悬停交互。
- `conversation-route-redirect-9iisDdJA.js`(正文 SHA-256 `85ba8228584ffe57a6c1343ea26f6ed98090f9e1099aa578ba745c55c4e04b97`)的 `resolveProjectConversationRoute`:要求 conversationId 和 projectId;cloud transport 返回 `/projects?projectId=…&collabTaskId=…` 且不恢复本地 session;本地 transport 返回 `/task/…` 且需要恢复 session。
- 路由清单引用 `projects-75rC88eE.js`、`project-BAAnDO1E.js`、`use-project-local-chat-config-Bal9cCUM.css`。这里只证明引用存在,不代表已取得模块正文或已观察页面行为。
- 项目具有独立 ID、传输类型与会话关系,不能直接把 YCode 工作空间列表更名来代替项目实现。具体项目创建、配置与删除语义仍待取证。
## 项目详情和任务页补采
- 项目详情稳定运行态:REA `ev_ca0195278ba08b57f2d847e4d96e3a35490f26367b71b810563b01b03470d000`,WorkBuddy 5.8.3。显示「动态 / 计划 / 任务 / 资产」四页签,右侧项目配置含指令、连接器、专家、技能和定时任务;指令抽屉尺寸 800×560、textarea 750×358、计数上限 10,000,保存按钮 76×36。项目指令抽屉组件已按此接入主进程更新接口,类型检查通过,但无窗口保存回归。
- 任务页稳定运行态:REA `ev_beb6214ec8bd412a2fe93beefdead0c4416f37dd4246d964f9c5f65aee240f69`,完整 DOM/样式补采 `ev_8782905c492acd1618e137480a3da8a8d7eba56e8f328e4856b008e1dd7f34ea`。任务页 bounds `[289.28,142,721.70,572]`;toolbar 48px,两个 94×32 筛选按钮、任务私密提示、240×32 搜索框;任务行 691.70×57,前导图标、标题/状态点、协作者头像组、相对时间和 24px 更多入口;输入区约 172px 固定在内容底部。证据截图 `.tmp/rea-todo20-project-tasks-full.png`。
- `scripts/rea/recover-project-detail-styles.cjs` 根据 REA 样式正文 SHA 恢复372条作用域规则到 `source-project-detail-recovered.css`。
- YCode 新增详情页:四页签外观、任务过滤与本地标题搜索、按 `projectId` 筛选真实会话、项目任务输入区、项目配置栏与指令编辑;创建会话时校验项目归属并使用项目工作空间,发送走现有聊天通路。项目归属及项目指令进入受控 OMP 请求已有 IPC 回归。
- 当前差异:动态、计划、资产为未接数据占位;筛选状态选项尚未与运行/终态数据打通;成员邀请、协作者头像、卡片更多菜单和项目完整配置选择尚未实现。项目详情真实窗口全状态、任务输入框几何及项目路径系统性回归本轮未执行,不能标为1:1。
## 下一步与当前限制
1. 恢复 WorkBuddy 调试运行态后,通过 REA 重新采集更多菜单稳定布局,进入项目列表。
2. 回收项目页面及关联配置模块,确认项目、空间、本地目录、任务的数据关系及管理操作。
3. 再确定 YCode 持久化契约、页面实现与真实窗口验收。
本轮后续 REA 复采返回 `provider_unavailable / endpoint_unreachable`;同时 `Get-Process WorkBuddy` 未找到进程。失败记录 `.tmp/rea-todo20-more-stable.json`。这不影响已保存证据的使用,也不证明项目页面不可复刻。
## 调试恢复后的补采
- REA lifecycle `ev_86650169a0ebc57f6a06e45c136f957e24e47233f49166ca4730d567fafbc7d7` 启动 PID 111900,端口 9223;REA target list `ev_24e56f79198d0fe7757d6c4ccc53d7da3168fd91aa77a6f372ed8a7dd5fecec6` 确认 WorkBuddy 5.8.3,新 target `41DA5D1989571E35EEB83ADEF03B5B47`。此前端点阻塞已解除。
- 项目导航动作 `ev_64bd69418b96a024c53141f7a26624ed513746a0fa3dadd95643026a95e766b3`:悬停更多 → 等待 → 点击首个 `.conversation-list-more-dropdown .wb-dropdown__item` → 等待,四步成功。
- 独立稳定页采集 `ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9`:`.tmp/rea-todo20-project-page.json` / `.png`。页面包含项目标题、副标题、新建项目、我的项目、搜索项目与五类模板(产品需求全流程、市场调研与竞品分析、团队知识库、项目交付、Bug 跟踪/测试验收)。本次没有创建或删除原版项目。
- 从上述 REA 证据正文导出 `project-list-page-DpHRTtk8.js`(51964 字符,SHA `2576827ec29d651f1ff1325ccd7f76f4ab62a3466afb8e341f4595f7bd3ef3b0`)和 `use-project-local-chat-config-DqSrAK4B.js`(45898 字符,SHA `e6e153194a543b5cdc52c5cac1f96727571aa2ca9b2ab78bf6c79bc35d05f636`),本地片段保存在 `.tmp/todo20-<模块名>`。未执行回收模块。
- 新建项目弹窗独立稳定证据 `ev_bc0f5546bb714cad4634168969a115be8f34e307bc09f51caf1457f5d3ed61ec`:`.tmp/rea-todo20-new-project-dialog.json` / `.png`。容器 640×560(1336×902 视口),项目名称输入 590×34、计数 0/15,指令 textarea 590×122;模板选择以及连接器、专家、技能三个可选选择区;取消和确定按钮。下一步分析校验、模板与能力关联,再实现本地项目契约。仍未验收整个 TODO 20。
## 本轮系统拆解(2026-10-17,只读核对)
### 口径
- 本轮没有启动 WorkBuddy、没有操作原版运行态、没有读取 WorkBuddy 安装包,也没有直连 CDP。原版事实仅来自项目内已保存的 REA Evidence、其 DOM/CSS/脚本导出和据此生成的静态恢复物。
- YCode 事实来自当前工作树的页面、共享契约、主进程、preload 和专项脚本的只读核对。本轮没有运行 build、Electron 或 UI 验收。
- “原版已观察”只表示固定版本和固定场景下有一条可追溯证据;“原版未知”不能由现有 YCode 行为反推。
- “YCode 已实现”只表示当前代码存在真实数据通路;“已验收”只引用文件中已有的专项结果。二者都不等于完整 1:1。
### 原版事实与证据账本
固定版本为 **WorkBuddy 5.8.3**。本轮复用的主要 Evidence 如下:
| 范围 | Evidence | 已保存证据/用途 | 明确覆盖与限制 |
| --- | --- | --- | --- |
| 更多菜单 | `ev_d1a438b97437b2f6a98f5dddcc5c3385184e24a439c5a351365fbbefc39e457d` | `.tmp/rea-todo20-more-open.json` / `.png` | 观察到菜单打开帧及“项目(BETA)”等入口;最终 neutral-pointer 清理失败,且布局快照不含打开后的完整浮层 computed style。 |
| 项目列表页 | `ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9` | `.tmp/rea-todo20-project-page.json` / `.png` | 观察到一个稳定列表页;包含项目标题、副标题、新建入口、我的项目、搜索框和五类模板。未覆盖空态、加载失败、搜索无结果、卡片全部操作和响应式。 |
| 新建项目弹窗 | `ev_bc0f5546bb714cad4634168969a115be8f34e307bc09f51caf1457f5d3ed61ec` | `.tmp/rea-todo20-new-project-dialog.json` / `.png` | 观察到 640×560 弹窗、名称、指令、模板、连接器/专家/技能选择区及底部按钮;未逐一操作三个选择器,未验证提交失败、重名和网络/权限错误。 |
| 项目详情 | `ev_ca0195278ba08b57f2d847e4d96e3a35490f26367b71b810563b01b03470d000` | `.tmp/rea-todo20-project-detail.json` / `.png` | 观察到“动态 / 计划 / 任务 / 资产”四页签及右侧配置栏;脚本静态恢复中可确认本地 `createConversation(cwd,{projectId,...})`。未创建原版任务,未操作非任务页签。 |
| 项目任务页 | `ev_beb6214ec8bd412a2fe93beefdead0c4416f37dd4246d964f9c5f65aee240f69` | `.tmp/rea-todo20-project-tasks.json` / `.png` | 观察到全部任务、全部来源、搜索、项目私密提示、任务行和底部输入区。只覆盖当前账号/数据/主题/窗口下的稳定画面,不证明所有筛选、菜单或状态。 |
| 任务页完整 DOM/CSS | `ev_8782905c492acd1618e137480a3da8a8d7eba56e8f328e4856b008e1dd7f34ea` | `.tmp/rea-todo20-project-tasks-full.json` / `.png` 及 `source-project-detail-recovered.css` | 恢复 372 条项目详情作用域规则;只负责已加载样式所覆盖的选择器,不证明未出现状态。 |
| 项目路由/传输源码 | `ev_2b8db4eb73c59a8d3d80c3e37db5ea9b5bf1d13390d5238d81ff2cc53d11dc09` | `.tmp/rea-todo18-main-scripts.json` | 已保存脚本正文中的 `resolveProjectConversationRoute` 表明项目对话路由同时要求 `conversationId` 与 `projectId`;cloud 路由带 `projectId`/`collabTaskId`,local 路由返回本地 task 并恢复 session。只覆盖该路由函数及其调用可见信息。 |
| 项目插图资源 | `ev_d740e73903dd69444105e2509b238e5912f2c93d3adaa183b05ec7221512ef7a` | `.tmp/rea-todo20-project-hero.json` | 仅证明 `landing-hero-B6659kdy.png` 资源字节及 SHA,不单独证明页面运行态可见性。 |
原版页面和交互清单,按“已观察”与“仅源码/静态恢复”分开:
| 区域 | 原版已观察或固定证据可确认 | 当前仍是未知 |
| --- | --- | --- |
| 更多入口 | “项目(BETA)”位于更多菜单;菜单可打开。 | BETA 徽标条件、菜单键盘/Hover 时序、边缘翻转、点击后窗口/路由切换过程。 |
| 项目列表 | 标题、副标题、新建按钮、我的项目、搜索、五类模板。 | 空列表、无搜索结果、加载、错误、重试、卡片 Hover/焦点、卡片更多菜单、排序/分页、项目数量上限。 |
| 新建项目 | 项目名 15 字计数;完整指令;模板;连接器、专家、技能选择;取消/确定。 | 重名策略、空白/超限错误文案、三个选择器完整交互、能力授权失败、模板切换副作用、保存成功/失败反馈。 |
| 项目详情 | 动态、计划、任务、资产四页签;右侧配置可见指令、连接器、专家、技能和定时任务。 | 项目重命名、删除/归档、分享、邀请成员、成员角色与权限、配置各字段编辑流程。 |
| 任务页 | “全部任务/全部来源”筛选、任务搜索、私密提示、任务行、相对时间、协作者头像组、24px 更多入口、底部输入区。 | 每个筛选的真实枚举和语义、任务状态定义、更多菜单动作、任务归属变更、云/本地来源差异、无权限态。 |
| 任务创建 | 静态源码可确认本地任务创建时 `cwd` 与 `projectId` 为独立字段。 | 新任务标题生成、工作目录选择、首个发送前后的状态、失败补偿、项目与空间冲突时的原版反馈。 |
| 动态 | 页签存在。 | 发布留言、评论、成员动态、与我相关、编辑/删除、空/加载/错误和实时刷新。 |
| 计划 | 页签存在。 | 计划对象、状态、负责人、时间、与任务/资产关系、创建和变更流程。 |
| 资产 | 页签存在;任务输入提示可引用资产文件、项目待办或技能。 | 上传/导入/预览/删除、文件来源、项目资料库关系、权限、版本、空态/无数据状态。 |
| 成员与协作 | 任务行可见协作者头像组。 | 邀请、接受、移除、角色、负责人/关注人、可见范围、账号/企业策略和通知。 |
原版数据关系的已证实部分:
- 项目拥有独立 `projectId`,还区分 local/cloud transport;项目关系不能用一个“空间”记录替代。
- 项目任务仍是一条 conversation/task,并同时携带项目归属与工作目录。项目 ID 和 `cwd` 不能合并成同一字段。
- local 项目的任务路由回到本地 task;cloud 项目路由可能使用 `projectId + collabTaskId`。这证明至少存在两条传输语义,尚不能据此列出全部云 API。
- 固定详情与任务页表明项目详情至少聚合项目配置、任务和协作者信息;动态、计划、资产的具体存储关系尚未取得。
### YCode 当前实现真相
页面和入口:
| 能力 | 当前状态 | 代码/验证依据 | 明确边界 |
| --- | --- | --- | --- |
| 侧栏“更多 → 项目” | 真实现 | `ProjectMenu.tsx`;点击进入 `projects` 路由 | 当前只有“项目 BETA”一项;保留 Hover、点击、外部点击、Escape 和 resize 关闭,几何/时序未与原版全状态比较。 |
| 项目列表 | 真实现 | `ProjectListPage.tsx`、`projects:list` | 真实读取 `userData/projects.json`;名称搜索;卡片可在 Enter/Space 打开详情。无专用空列表/无搜索结果态,加载中与实际空数组可能视觉上难区分。 |
| 五类模板 | 真实现为固定恢复配置 | `project-templates.json`;`recover-project-templates.cjs` | 5 个模板的标题、描述、完整中文指令、专家/技能/连接器引用来自固定 Evidence。市场能力只保存引用,不保证本机已安装或可执行。 |
| 新建项目 | 真实现 | `projects:create`、创建弹窗、`ProjectRepository.create` | 真实持久化;名称 trim 后 1–15 个 UTF-16 单元;指令最多 10,000;模板切换会更新指令和能力;当前界面没有显式项目/工作空间选择。 |
| 连接器选择 | 真实现为本地 registry 引用 | MCP `list/save` 与创建弹窗 | 选择并保存真实本地 connector ID;测试使用禁用的 stdio 连接器,证明引用/持久化,不证明项目任务实际启用或调用该连接器。 |
| 专家/技能选择 | 真实现为本地引用 | experts/skills IPC 与创建弹窗 | 当前行内复选列表,不是原版独立选择窗口;模板中的 market 引用标题标注“尚未接入本地执行”,没有能力解析或执行证明。 |
| 项目详情框架 | 真实现 | `ProjectDetailPage.tsx` | 四页签、面包屑、邀请按钮、右栏配置存在;邀请按钮禁用。 |
| 动态页 | 占位 | `ProjectDetailPage.tsx` activity 分支 | 发布留言、与我相关、成员动态按钮均禁用;固定显示“暂无项目动态”。无动态数据、IPC 或云服务。 |
| 计划页 | 占位 | `ProjectDetailPage.tsx` plan 分支 | 固定显示“项目计划尚未配置”;无 plan 类型、存储或交互。 |
| 任务页 | 部分真实现 | `ProjectDetailPage.tsx`、`session:list/create`、`chat:send` | 按 `SessionMeta.projectId` 展示任务,标题搜索可用,任务可打开;状态筛选状态值未参与过滤,来源筛选的 `all` 与 `local` 在当前本地数据上等价。 |
| 任务行 | 部分实现 | `ProjectDetailPage.tsx` | 标题、相对时间、运行中/非运行中点可显示;没有原版协作者头像组和任务行更多菜单。 |
| 项目任务输入 | 部分真实现 | `ProjectDetailPage.tsx`、会话与对话 IPC | 输入会先创建带 `projectId/workspaceId` 的会话,再打开任务并发送首条消息;失败会回显错误。完整输入区能力、准备阶段、取消和真实远端模型尚未专项验收。 |
| 资产页 | 占位 | `ProjectDetailPage.tsx` assets 分支 | 固定显示“项目资产尚未配置”;没有上传、引用、预览或数据模型。 |
| 项目标题/副标题/英雄图 | 部分实现 | 列表页和恢复 CSS/PNG | 页头结构和插图已挂载,已有真实窗口浅/深色、64px topbar 与基础几何回归;没有完成原版同数据整页像素比较。 |
| 项目卡片更多 | 适配占位 | `ProjectListPage.tsx` | 唯一动作是“项目指令”;没有原版卡片完整更多菜单,也不能从该入口改名、删除、归档或分享项目。 |
| 项目指令抽屉 | 真实现,专项验收未闭环 | `ProjectInstructionDrawer.tsx`、`projects:update` | 800×560 结构、10,000 字符限制、取消/保存、错误和保存中状态已接;本轮未运行窗口保存回归,不能把静态结构当成交互完成。 |
| 项目右栏配置 | 部分实现 | `ProjectDetailPage.tsx` | 指令可打开抽屉保存;连接器/专家/技能只显示已有 ID 列表,配置卡无编辑动作;定时任务未渲染。 |
| 成员/协作者 | 未实现 | 邀请按钮禁用;无 member API/type | 没有项目成员、角色、关注人、头像、通知或权限模型。 |
| 项目生命周期 | 未实现 | projects API 只有 list/create/update | 没有项目删除、归档、恢复、重命名 UI、复制、分享或导出;`update` 后端支持改名但未暴露入口。 |
项目详情目前由列表组件内的 `detailProjectId` 状态打开,没有独立 URL、刷新恢复或原版式深链。任务输入区位于四页签共享的详情底部并始终渲染;原版在动态、计划、资产页签是否保留同一输入区、各页签动作是否不同,现有证据尚未确认。
数据与契约:
```text
Space/Workspace 1 ── 0..n Project
Project 1 ── 0..n Task(Session)
Project 1 ── 0..n CapabilityRef(connector | expert | skill)
Task 1 ── 0..n Message
Task n ── 1 Workspace/CWD
```
建议的目标关系如下,表内除已证实项目/任务关系外均为设计建议,不是从原版运行态推得的事实:
| 实体 | 建议关系 | 当前状态/未知 |
| --- | --- | --- |
| 成员 Member | User n ── n Project,成员可拥有 owner/admin/member 等角色 | 原版有协作者头像迹象,角色与权限未知;YCode 未建模。 |
| 动态 Activity | Activity n ── 1 Project,可关联 Task/User/Asset | 页签存在但不清楚事件种类和存储;YCode 为占位。 |
| 计划 Plan | Plan n ── 1 Project,是否关联 Task/Asset/时间轴待证 | 页签存在但对象语义未知;YCode 为占位。 |
| 资产 Asset | Asset n ── 1 Project,可被 Task/Message/Plan 引用 | 原版输入提示出现“资产文件”,具体实体和引用关系未知;YCode 未建模。 |
| 配置 Config | 1 Project ── 1 ProjectConfig,含指令与连接器/专家/技能引用 | YCode 已保存指令和三类能力引用;原版配置接口和权限限制未知。 |
| 定时任务 | ProjectConfig 1 ── 0..n Schedule 或独立服务记录 | 原版右栏出现入口;YCode 未建模,不能与普通项目任务混为一类。 |
- `ProjectRecord` 当前字段:`projectId`、`transport:'local'`、可选 `workspaceId`、`name`、`instructions`、可选 `templateId`、`experts[]`、`skills[]`、`connectors[]`、`createdAt`、`updatedAt`。
- 项目创建时缺省采用当前选中的 workspace;主进程先解析真实 workspace root,再写入项目。项目一旦创建,`projects:update` 不允许更新 `workspaceId`。
- 项目任务通过 `SessionMeta.projectId` 和 `SessionRecord.projectId` 关联项目;同时保存 `workspaceId` 与 `cwd`。项目任务创建会拒绝不存在的 `projectId`,也会拒绝项目 workspace 与传入 workspace 不一致的组合。
- 项目任务不能通过 `session:update-context` 改到另一项目;跨项目必须新建任务。切换 workspace/cwd 也沿用“新建任务”约束。
- 项目指令通过 `buildProjectPrompt()` 进入每轮用户消息的 `capabilityContext/promptContext`,名称为“项目 X”加完整指令。该项目上下文会被冻结进消息,后续历史重建可复用。当前只注入指令,不自动加载项目的专家、技能或连接器。
- 项目配置把连接器、专家、技能作为引用保存;local/market 来源不同,但当前没有统一的“项目能力运行时”把这些引用转换成真实工具或专家激活。
- 成员、动态、计划、资产、项目级定时任务目前都没有 YCode 数据类型、持久化容器或 IPC。
已有验证事实:
- `scripts/verify-project-storage.cjs`:验证串行并发创建、重载、原子写、ID、名称/指令校验、能力引用、更新合并、非法更新不落盘和损坏文件不被覆盖。它验证 repository 纯存储,不验证页面。
- `scripts/verify-project-page.cjs`:历史上在隔离 Electron 中验证侧栏进入、五模板、创建、真实 connector 选择/移除/重选、磁盘落盘、项目任务 `projectId`、重载恢复、基础几何、深色和模板切换;没有覆盖详情四页签、任务发送、筛选、成员、动态、计划、资产或指令抽屉保存。
- `test:conversation-ipc` 已历史覆盖项目→任务→`chat:send` 的项目指令到达受控 OMP adapter,以及跨项目改归属拒绝。它不是远端模型遵循指令的验收。
- 本轮按要求没有重跑上述脚本;这些是已有工件和代码路径事实。
### 云端与外部依赖
| 依赖面 | 为什么需要单独建模 | 当前可做范围 | 未实现前不得声称 |
| --- | --- | --- | --- |
| 成员、邀请、角色与权限 | 原版任务行已显示协作者头像组,路由存在 cloud transport;固定证据尚未给出权限矩阵。 | 本地 owner-only 项目的明确只读/禁用态。 | 多人协作、共享可见、负责人/关注人、企业策略。 |
| 动态流 | 原版有不依赖固定数量的动态/成员动态入口;存储和服务语义未观察。 | 本地占位或明确空态。 | 评论、提及、实时更新、通知。 |
| 云端计划与资产 | 页签存在,但无法从固定画面证明它们是本地文件、云文档还是项目服务对象。 | 本地 plan/asset 自有 schema,或继续明确占位。 | 上传、预览、版本、权限和跨端同步。 |
| 市场模板能力 | 模板中的 experts/skills/connectors 多带平台 ID 和远程图标;当前设备未必安装。 | 保存引用、按本地 registry 过滤 connector、显著标记不可执行引用。 | 模板创建后所有能力自动可用。 |
| 云项目与 `collabTaskId` | `resolveProjectConversationRoute` 明确存在 cloud 分支。 | 以 local transport 实现清楚的项目/任务关系。 | 本地 `/task` 路由等价于 cloud project route。 |
| 项目定时任务 | 原版右栏配置可见定时任务,但项目级任务模型未采集。 | 先作为未知项,或接 YCode 自有 automations 后另证。 | 已实现项目自动化。 |
### 未知项与下一轮 REA 取证要求
以下均为原版未知,需要协调者统一通过 REA 操作原版运行态补采;本轮明确不操作:
1. **项目生命周期**:非常规命名、重名、15 字边界、删除/归档/恢复/分享、运行中任务存在时的限制与确认框。
2. **列表全状态**:空项目、搜索无结果、加载、失败/重试、卡片 Hover/Focus/更多菜单、排序与长列表。
3. **新建弹窗全交互**:模板切换前后、三个能力选择器、选择后芯片、取消/返回、保存成功与各类失败。
4. **详情与配置**:页签切换、指令/连接器/专家/技能/定时任务配置、保存/取消/失败、项目重命名、邀请按钮。
5. **任务筛选与行**:两个下拉的完整枚举、筛选组合、任务搜索无结果、运行/完成/失败、协作者头像、任务更多菜单和私密提示。
6. **项目输入区**:首条任务创建前后的占位/加载/错误、工作空间选择、附件/资产/技能/项目待办引用、发送与取消。
7. **动态**:发布、评论、筛选、成员动态、与我相关、空/错误/滚动加载、实时更新。
8. **计划**:对象字段、状态、负责人、时间、与任务/资产关系、创建/编辑/删除和空态。
9. **资产**:来源、上传、预览、引用、版本、删除、权限、失败和空态。
10. **成员与权限**:邀请方式、成员列表、角色、移除、owner/admin/member 能力、企业限制和通知。
11. **云/本地传输**:项目 route 参数、cloud project 列表/详情/任务/成员 API、本地项目同步边界、`collabTaskId` 生命周期。
12. **视觉全状态**:浅/深色、1336×902 以及窄/矮窗口、Hover/Focus/Disabled、菜单/弹窗 collision、图片和字体、动效时序。
每组证据至少应包含:固定版本、Evidence ID、输入和前置数据、观察窗口、DOM/layout、可见截图、交互步骤、失败/未知状态。只有截图而没有可操作流程,不足以验收交互;只有源码分支而没有运行态,不足以判定状态外观。
### 推荐任务分批与验收
以下编号和分批是本轮的设计建议,不是原版事实。
| 批次 | 目标 | 主要交付 | 验收门槛 |
| --- | --- | --- | --- |
| P0 契约冻结 | 先消除空间、项目、任务、成员和能力的歧义 | 完整 `ProjectRecord` schema;get/update/delete/archive 契约;成员/活动/计划/资产是明确本地 schema 还是云占位;迁移与损坏恢复 | 单元回归覆盖一对一/一对多关系、不可跨项目改任务、删除/归档不误删任务与目录;新字段有兼容迁移;不与 workspace 混用。 |
| P1 列表与创建 1:1 | 完成“更多→项目→列表→新建”主链 | 空/加载/失败/无搜索结果;卡片操作;新建弹窗和能力选择;模板切换;浅深色和响应式 | 真实 Electron 逐状态覆盖;同一 REA 输入下截图像素与几何有报告;键盘、Escape、外部点击和焦点闭环。 |
| P2 详情骨架 | 四页签、右栏、响应式、配置入口 | 项目 breadcrumb;页签状态;右栏所有配置卡;指令抽屉保存;标题/邀请按钮状态 | 真实窗口覆盖四页签切换、指令保存/取消/失败、深色和窄窗口;未实现配置不能伪装可点击。 |
| P3 项目任务 | 项目内任务真实闭环 | 状态/来源筛选;搜索;运行/完成状态;协作者;行更多菜单;打开任务;输入区创建并发送 | 新建任务携带正确 project/workspace/cwd;项目指令进入模型请求;筛选跨刷新稳定;停止/失败/重载不串项目;至少一条受控模型端到端。 |
| P4 成员与动态 | 协作主链 | 成员查询/邀请/接受/角色;动态发布、评论、筛选与通知策略 | 本地 owner-only 与云可用态清楚;权限拒绝有明确 UI;成员操作不破坏任务归属;若云服务不可接,必须保留可验证禁用态而非 mock 成功。 |
| P5 计划与资产 | 补齐两个数据页 | plan/asset schema、列表、详情、创建/删除、任务引用 | 计划与任务、资产与任务/消息的引用可追踪;权限、空态、失败、重载、跨项目隔离通过。 |
| P6 传输与云适配 | 分离 local/cloud transport | local repository adapter;cloud adapter/gateway 接口;route 解析和迁移 | same-project、cross-project、cloud task ID、断网/401/403/超时、重试和回滚均有契约测试;不能把 cloud mock 记为真实云验收。 |
| P7 全状态视觉验收 | 最终 1:1 收口 | 所有固定状态和过渡状态截图、几何、样式、键盘、动效 | 每项有 REA Evidence 对照;0 容差优先;任何容差、mask 和动态排除都逐项说明;所有专项与相关回归通过后再勾选 TODO 20。 |
建议先做 P0 和 P1;P2/P3 可并行但共享项目详情契约;P4–P6 需要先决定自建本地能力还是等待原版云接口证据。若继续按“云端暂不管”,P4 的成员/动态和 P6 的 cloud adapter 应明确停留在只读禁用或占位,并不得勾选模块完成。
### 本轮核对命令与限制
只读核对命令包括:
```powershell
Get-Content -LiteralPath 'docs\项目模块拆解-2026-10-17.md' -Raw
Get-Content -LiteralPath 'src\shared\projects.ts' -Raw
Get-Content -LiteralPath 'src\main\projects.ts' -Raw
Get-Content -LiteralPath 'src\renderer\src\projects\ProjectListPage.tsx' -Raw
Get-Content -LiteralPath 'src\renderer\src\projects\ProjectDetailPage.tsx' -Raw
Get-Content -LiteralPath 'scripts\verify-project-storage.cjs' -Raw
Get-Content -LiteralPath 'scripts\verify-project-page.cjs' -Raw
```
并对 `.tmp/rea-todo20-project-page.json`、`new-project-dialog.json`、`project-detail.json`、`project-tasks-full.json` 的顶层 Evidence、覆盖率、限制和页面节点做只读解析;未执行其中脚本。
限制:
- 没有新的 WorkBuddy 运行态证据,不能把“未观察到”填成“原版没有”。
- 没有执行 build、Electron、UI 专项或代码修改;当前测试结论只引用历史工件。
- 现有固定页面证据主要覆盖一个当前账号、一种数据状态和有限窗口/主题,无法证明所有用户、企业策略、空态、错误态和响应式。
- 动态、计划、资产和成员的具体服务端语义仍未知;本文列出的数据关系和分批方案是工程建议。
@@ -0,0 +1,242 @@
# 08~13 验收问题收集(2026-10-17)
本文只验收 `复刻待办清单-2026-10-17.md` 的 08~13:
1. 新建任务发送后的立即切页与占位
2. 账户菜单与页面
3. 通用设置
4. 外观与浅色/深色主题
5. 快捷键页面与真实功能
6. 个性化页面与真实功能
本轮不修改业务代码、测试或 TODO,不启动 Electron/build。WorkBuddy 原版结论只沿用项目内既有 REA Evidence;本文没有新采集、驱动或修改 WorkBuddy。
## 本轮专项结果
主上下文最新构建后的专项脚本结果如下:
| 条目 | 脚本 | 结果 | checks |
| --- | --- | --- | ---: |
| 08 | `verify-new-task-switch` | PASS | 30/30 |
| 09 | `verify-account-menu` | PASS | 64/64 |
| 10 | `verify-general-settings` | PASS | 43/43 |
| 11 | `verify-appearance-settings` | PASS | 21/21 |
| 11 | `verify-desktop-appearance` | PASS | 19/19 |
| 12 | `verify-shortcuts-settings` | PASS | 32/32 |
| 13 | `verify-personalization-settings` | PASS | 29/29 |
| 13 | `npm run test:personalization` 离线纯测试 | PASS | 全部断言 |
主上下文报告最新构建通过,并已单独通过 `npm run test:conversation-ipc`。这些结果只覆盖对应构建与断言,不替代本文列出的原版证据和全状态验收。
报告:
- `.tmp/new-task-switch-CskK4m/report.json`
- `.tmp/account-menu-1pttNZ/report.json`
- `.tmp/general-settings-it8Zlb/report.json`
- `.tmp/appearance-settings-3Jyy3m/report.json`
- `.tmp/desktop-appearance-hM7qdy/report.json`
- `.tmp/shortcuts-settings-Q8JoQ3/report.json`
- `.tmp/personalization-settings-0eAA5x/report.json`
- `.tmp/audit19-20261017-165014/conversation-ipc.log`
19 个串行脚本中 17 个通过、2 个失败。`verify-settings-nav` 仍把已接入的“记忆”断言为禁用;`verify-composer-context-info` 仍使用旧 `.segmented` 主题控件,外观页改为主题卡后等待超时。两处均为陈旧测试预期,不推断为业务失败,本报告不把它们计作 08~13 的缺陷。
## 优先复核的实际问题
### P2:会话已创建、回合未启动时失败,会留下“空会话 + 新任务界面”的双重状态
- 复现:新任务发送后,让 `session:create` 成功,随后让 `chat:send` 在运行环境启动、附件提交、资源校验或 `startTurn` 任一阶段失败。例如发送过程中删除暂存附件,或让 OMP 启动失败。
- 实际:`newSession()` 已把 `currentSessionId` 设为新会话;后续 `chat:send` 失败时,渲染层只删除乐观消息,没有删除会话或恢复 `currentSessionId=null`。`ChatPane` 因 `messages.length===0` 显示新任务首页,但空间选择仍因 `currentId!==null` 被禁用。
- 期望:首回合启动失败后,要么删除刚创建但从未启动的会话并回到真正的新任务状态,要么保留失败会话和完整错误上下文,不能出现“看起来是新任务、实际已绑定旧会话”的状态。
- 影响:侧栏产生无消息任务;重试会继续使用这个会话;用户无法再切换该“新任务”的工作空间,后续上下文归属容易出错。
- 代码:`src/renderer/src/stores/chat.ts:250-259` 设置新 `currentSessionId`;`src/renderer/src/stores/chat.ts:313-360` 失败路径只清消息;`src/renderer/src/components/ChatPane.tsx:171` 把空消息当首页;`src/renderer/src/components/ChatPane.tsx:371` 用非空 `currentId` 禁用空间切换。
- 当前证据:本轮 30 项脚本没有覆盖它。`scripts/verify-new-task-switch.cjs:175-203` 的失败场景先清空模型注册表,使 `session:create` 在 `src/main/ipc.ts:535-538` 的模型解析阶段就失败,所以此时根本没有创建会话。测试名“发送失败后回到首页”通过,但对应的是“创建会话前失败”。
- 下一步:新增“`session:create` 成功、`chat:send` 拒绝”的回归;断言会话列表、`currentSessionId`、空间选择可用性和草稿恢复同时一致。原版准备失败是否保留空会话仍缺 REA 运行时证据。
### P2:通用设置的磁盘统计被超时截断时,仍按完整值展示
- 复现:让 `app.getPath('userData')` 包含超过 200,000 个文件,或目录扫描超过 1.5 秒。
- 实际:扫描返回 `partial:true`,但进程只把这个值放到 `dataUsagePartial`;设置页完全不读取它,仍把已扫描字节当成完整“已用”值,并据此绘制进度条。
- 期望:显示“统计未完成/继续扫描”,或异步完成后刷新;不能把下界当作精确总量。
- 影响:存储占用和磁盘进度会偏低;用户会误以为目录已清理或容量不足。
- 代码:`src/main/general-settings.ts:80-101` 在 200,000 条目或 1500ms 时截断;`src/main/ipc.ts:418-419` 返回 `dataUsagePartial`;`src/renderer/src/components/GeneralSettingsPanel.tsx:173-178`、`:235-257` 只消费 `dataBytes`,没有读取 partial。
- 当前证据:`verify-general-settings` 使用很小的隔离数据目录,43 项全部通过;没有构造条目上限或耗时预算耗尽。
- 下一步:增加大目录/低预算纯函数测试,并在界面提供部分统计状态或继续扫描。
### P2:右侧工作台全屏且任务生成中,按 `Esc` 会同时取消生成和退出全屏
- 复现:打开右侧工作台并全屏,启动一个生成中的回复,把焦点留在工作台内后按 `Esc`。
- 实际:App 的全局监听取消当前运行;`SourceWorkbench` 的独立监听也退出全屏。两个监听都挂在 `window` 上,工作台监听没有检查 `event.defaultPrevented`,也没有阻止同一事件继续传递。
- 期望:明确一个优先级。快捷键表只声明“生成中 Esc 停止生成”,因此至少不应同时触发未声明的第二个动作;若原版在全屏时另有优先级,应以 REA 运行时证据为准。
- 影响:一次按键产生两个业务状态变化,容易打断用户查看工作台。
- 代码:`src/renderer/src/App.tsx:295-316` 的生成中 Esc;`src/renderer/src/workbuddy/workbench/SourceWorkbench.tsx:59-65` 的全屏 Esc。
- 当前证据:`verify-shortcuts-settings` 只测试录制态 Esc,不测试生成中 Esc,也不测试全屏组合状态。
- 下一步:增加“生成中 + 全屏 + 工作台焦点”的冲突回归,再按原版优先级收敛。
### P2:默认人设的页面说明与实际默认注入状态不一致
- 复现:保持“人设设定”为页面初始值,不编辑,直接发送消息。
- 实际:页面展示完整工作人设并说明“对所有回复生效”,但 `buildPersonalizationPrompt()` 发现它等于默认人设后主动跳过,所以该字段本身不会从个性化模块进入本回合上下文。这是既定实现选择,不等于行为必然错误;YCode 内置提示词是否承担了等价语义尚未证明。
- 期望:页面应准确说明默认人设由哪一层生效;如果内置提示词与展示的人设不等价,则应注入展示内容或调整说明。行为是否与原版等价需要 REA 证据判断。
- 影响:用户看到的人设说明与模型实际收到的个性化字段不一致,容易误判当前人设是否已经生效。
- 代码:`src/shared/personalization.ts:108-116`,尤其 `:113` 明确排除等于 `DEFAULT_PERSONA` 的值;该值在 `PersonalizationSettingsPanel.tsx:278-285` 作为默认人设展示。
- 当前证据:`scripts/personalization-prompt-regression.ts` 反而断言默认人设不得进入提示词,证明这是既定实现选择;尚无证据证明 YCode 内置提示词与展示内容行为等价。
- 下一步:用 REA 补原版默认人设的生效层级和内容证据,再决定注入展示内容、证明内置替代,或修正 UI 文案。
### P2:欢迎语跨原生设置窗口更新时,主窗口的现有流式状态栏没有可靠刷新信号
- 复现:在主窗口保持一个尚未产生正文的慢请求;打开原生设置窗口切换“欢迎语”;观察主窗口当前加载提示。
- 实际:设置窗口写共享 `localStorage` 后只派发当前 renderer 的自定义 `ycode:personalization-change` 事件。主窗口是另一个 BrowserWindow,只会收到浏览器 `storage` 事件;`personalization-prefs.ts` 没监听它。当前组件除非因其他状态再次渲染,否则不会收到变更。
- 期望:配置修改后,所有窗口中的当前流式状态栏即时同步。
- 影响:开关似乎已保存,但当前会话仍显示或隐藏欢迎语;新请求通常会在挂载时读到新值,导致“有时生效、有时要等下一次”的不稳定现象。
- 代码:`src/renderer/src/personalization-prefs.ts:8` 定义自定义事件;`:25` 只在设置窗口派发;`:32` 只订阅自定义事件;`src/renderer/src/workbuddy/conversation/StreamingFooter.tsx:110` 用这个订阅驱动 `useSyncExternalStore`。
- 当前证据:`scripts/verify-personalization-settings.cjs:210-217` 只在设置窗口读取 `localStorage`,没有检查主窗口正在挂载的 `StreamingFooter`。
- 下一步:监听 `window.storage` 或通过主进程广播配置变更;增加双窗口、活跃流式请求的同步用例。
### P2:账户菜单头部实际高度仍比原版多约 4.19px,现有测试只比较了 `min-height`
- 复现:打开账户菜单,测量 `.user-menu-header` 的 `getBoundingClientRect().height`。
- 实际:头部由两行(名称和新增的 mock 说明)撑高,已知结果约 60.19px;原版头部为 56px。测试只断言 CSS `min-height:56px`,不会发现内容把容器撑高。
- 期望:保留用户要求的头像与名称内容,同时由明确布局把头部控制到原版 56px;若副标题必须保留,应把它移到不改变原版几何的位置。
- 影响:计费卡和后续条目整体下移,不能宣称账户菜单逐像素完成。
- 代码:`src/renderer/src/workbuddy/user-menu/AccountMenu.tsx:90-98` 增加副标题;`account-menu-source.css:119-126` 使用 `min-height:56px`;副标题样式在 `:178-185`。
- 当前证据:`scripts/verify-account-menu.cjs:178` 只检查 `minHeight='56px'`。
- 下一步:补实际高度与各 section 的 y 坐标断言,再对照固定 REA 几何修正。
### P2:账户菜单 5 个条目图标仍是 Lucide 占位,不是原版路径
- 复现:对照原版菜单和当前菜单的设置、外观、记忆、更新、退出图标。
- 实际:源码明确写明原始 SVG 尚未回收,当前用 Lucide 同族图标占位。
- 期望:使用 REA 回收的原版 SVG path、viewBox 和线宽。
- 影响:容器尺寸一致,但字形不一致,不能完成像素级验收。
- 代码:`src/renderer/src/workbuddy/user-menu/account-menu-icons.tsx:4-7` 和后续图标定义。
- 当前证据:本轮只验证 16×16 盒子,不验证 path。
- 下一步:用 REA 针对 5 个条目补 `svg_nodes`,再替换和做像素比对。
### P2:深色主题下 native 窗口底色仍固定为白色,重载/空白首帧白闪仅为未观察风险
- 复现:深色主题下重载主窗口、打开独立任务窗口或原生设置窗口,并捕获页面绘制前帧。
- 实际:三个 BrowserWindow 都写死 `backgroundColor:'#ffffff'`;主题在 renderer React effect 中才写到 `documentElement`,窗口底色不会随主题更新。本轮没有捕获到白闪,也没有在重载帧中观察到白闪,因此不能写成已发生。
- 期望:窗口 native background 与持久化主题一致,首帧和重载帧不出现白色闪屏。
- 影响:稳定态截图可全通过,但启动、刷新、窗口树重建时仍会出现浅深混色。
- 代码:`src/main/index.ts:33`、`:103`、`:178`;`src/renderer/src/useSharedTheme.ts:9-13` 在 effect 中设置主题。
- 当前证据:`verify-appearance-settings` 和 `verify-desktop-appearance` 都等待稳定态后才采样,没有首帧/重载帧证据。
- 下一步:补充首帧捕获;主进程根据持久化主题设置窗口 `backgroundColor`,或使用首屏内联主题引导。
### P2:本地将个性化上下文作为 `userText` 拼接;原版注入层级尚未取证
- 复现:设置回复风格、用户指令或长期记忆后发送一轮,并检查实际内核输入。
- 实际:个性化段落被拼接到用户 `text`,最终作为 `userText` 交给内核;它没有作为独立消息角色发送。这证明了本地实现路径,但尚不能据此断言原版一定使用或一定不使用某个特定角色。
- 期望:先用原版 REA 证据确定个性化字段的角色层级、优先级和合并顺序,再证明本地 `userText` 拼接是否等价。
- 影响:用户级拼接可能影响偏好的优先级;在取得原版证据前,这一行为与原版是否等价仍属未知。
- 代码:`src/main/ipc.ts:774-781` 把 `personalizationPrompt` 拼进 `promptText`,随后作为 `userText` 交给内核;`src/shared/personalization.ts:108-116` 排除默认人设。
- 当前证据:纯函数测试只验证字符串拼装,没有通过真实 `chat:send`/上游捕获 system 与 user 两个 roles。
- 下一步:用 REA 取原版请求构造层级,再补真实 IPC/受控上游的角色断言。
## 08. 新建任务发送后的立即切页与占位
结论:**立即切页、乐观消息、单一启动占位和取消主链已实现,30/30 本轮断言通过;完整准备阶段、创建后失败回滚和准备阶段取消仍未闭环。**
- 已验证:约 500ms 内切页/显示用户消息/启动占位;真实占位替换无重复;运行中切换任务;停止后 `cancelled` 落库。
- 实际缺陷:见 P1“空会话 + 新任务界面”双状态。
- 功能未接:只输出 `acquiring_runtime`。`src/renderer/src/workbuddy/conversation/prepare-phases.ts:6-24` 明确说明主进程是单阻塞阶段;其余 9 个已经恢复的文案没有真实阶段发送方。
- 功能未接/待原版确认:发送窗口内没有停止入口。当前 Composer 在 `sending || running` 期间显示发送禁用态,只有真实 run 建立后才出现停止按钮。
- 证据缺口:WorkBuddy 发送后的逐帧时序未获得;原版准备失败是否保留空会话、是否允许准备阶段取消均未获得 REA 运行时证据。
- 测试盲区:未覆盖创建成功后 `chat:send` 失败、进程在启动窗口退出、准备阶段长时间停留、所有准备阶段依次推进。
## 09. 账户菜单与页面
结论:**当前用户指定内容、打开/关闭、设置导航、浅深色和错误/未接提示已实现,64/64 本轮断言通过;仍不能称为逐像素完成。**
- 已验证:头像、名称、套餐、余额、升级套餐、购买积分、设置、外观、记忆与进化、检查更新、退出登录;原生设置窗口复用;浅深色 token。
- 实际视觉差异:头部高度约 60.19px 对原版 56px。
- 实际视觉差异:5 个图标为 Lucide 占位。
- 用户范围边界:账户、套餐、余额、计费、更新和退出登录服务未接,云端服务本轮不列为缺陷;用户指定的内容范围差异也不列为缺陷。
- 证据缺口:原版深色运行时像素未采集;滚动、窗口失焦等关闭路径未采集。
- 测试盲区:只测 `min-height`,没有测真实 header height;“退出登录”只验证存在/禁用语义,没有点击并断言提示;没有 SVG path 对照。
## 10. 通用
结论:**布局、5 个真实设置和 11 个明确占位已实现,43/43 本轮断言通过;存储统计的 partial 状态和打开目录错误处理存在实际缺陷。**
- 已验证:防止休眠偏好、开机启动偏好持久化、真实数据目录、磁盘容量、工作区根目录持久化、通知设置入口、浅深色、未接控件保持禁用。
- 实际缺陷:部分目录统计仍按完整值展示。
- 实际缺陷:`src/main/ipc.ts:444-447` 忽略 `shell.openPath()` 返回的错误字符串。工作区目录被删除、权限异常或系统文件管理器启动失败时,设置页不会显示错误。现有测试只检查 API 函数存在,没有实际点击“打开目录”。
- 功能未接:语言、链接打开方式、字体大小、锁屏运行、网络代理、自动安装可信技能、技能自动更新、套件自动更新、通知提示音、定时任务产物通知、体验优化计划。代码在 `GeneralSettingsPanel.tsx:185-215` 和 `:280-305` 显式禁用,未伪装成功。
- 证据缺口:“系统缓存目录”实际统计整个 `userData`,包含配置、会话和缓存;原版目录语义及是否同样包含这些数据尚需 REA/运行环境证据。
- 测试盲区:`verify-general-settings.cjs:29` 设置 `YCODE_DISABLE_SYSTEM_INTEGRATION=1`,没有验证真实登录项、Windows 通知设置页和系统文件管理器;`:267-277` 直接调用 `changeWorkspaceRoot(path)`,没有点击“更改”并操作系统目录选择器;没有让真实任务运行时验证 `powerSaveBlocker` 是否实际启停,也没有覆盖并发任务计数。
## 11. 外观与浅色/深色主题
结论:**用户限定的浅色/深色、跨窗口同步和重启持久化已实现;两套本轮外观脚本均通过。稳定态通过不等于全状态和首帧通过。**
- 已验证:外观面板几何、两张主题卡、即时切换、主窗口/设置窗口同步、稳定态浅深色可读性、重启持久化、首页机器人 ROI 零差异。
- 源码风险:三个 BrowserWindow native 底色固定白色,重载或首帧“可能”出现白闪;本轮未观察到该现象。
- 用户范围边界:原版另外 7 张主题卡不接入,不列为当前缺陷。
- 证据缺口:原版主题切换过渡动画因 `target_changed` 未连续采集;YCode 只验证了稳定深浅两态。
- 测试盲区:稳定态采样只有设置壳和主窗口 5 个表面;未覆盖独立任务窗口、账户/模型/加号弹层、代码块、表格、工具结果、对话框、滚动条、首帧、重载帧和动画中间帧。`verify-desktop-appearance` 的像素比较范围只有首页机器人 120×78 ROI,不能外推到整个界面。
- 变量缺口:原版 tokens 中不存在、但旧 YCode 样式仍引用的变量保留 fallback,尚未做原版运行时逐项核对。
## 12. 快捷键页面与真实功能
结论:**24 行展示、搜索、录制/取消、4 个全局快捷键和浅深色已通过;14 个“已绑定”声明中存在明显测试假阳性,并发现 Esc 双动作冲突。**
- 已验证:Ctrl+B、Ctrl+K、Ctrl+Shift+B、Ctrl+N;24 行文案/键位;搜索和空态;录制态 Esc 取消;深色布局。
- 实际缺陷:生成中 + 工作台全屏时,Esc 同时停止生成和退出全屏。
- 测试假阳性:`scripts/verify-shortcuts-settings.cjs:204-207` 所谓“supported rows match the shortcuts YCode really binds”只是拿界面 `data-shortcut-supported` 与静态目录比较,没有执行动作。真正执行主窗口键位的断言在 `:151-172`,只覆盖 Ctrl+B、Ctrl+K、Ctrl+Shift+B、Ctrl+N。
- 未执行的真实绑定:Ctrl+,、Enter 发送、Shift+Enter 换行、生成中 Esc 停止、Ctrl+=/Ctrl+Shift+= 放大字号、Ctrl+- 缩小、Ctrl+0 重置、Ctrl+T 新标签、Ctrl+W 关标签。
- 静态源码显示这些处理器存在:`App.tsx:271-319`、Composer 输入键盘处理和 `SourceWorkbench.tsx:59-65`,但没有专项运行断言;尤其全屏 Esc 已暴露冲突。
- 功能未接:自定义键位保存、单条删除、整表恢复默认;语音录制、对话内搜索、上/下一个任务、定位任务、F11 全屏、唤起/隐藏主窗口、@ 选择器、/ 命令、Ctrl 强制外部打开只展示未执行。
- 证据缺口:空搜索文案为本地文案;深色 toolbar 控件为本地适配;原版自定义键位持久化格式和命令注册表实现仍未完整对齐。
- 下一步:逐个发送真实键位并断言 DOM/持久化/内核状态;先把 Esc 冲突定为单动作,再补 Ctrl+T/W、字号和发送键盘回归。
## 13. 个性化页面与真实功能
结论:**页面主体、回复风格、身份编辑、长文本保存、欢迎语持久化和浅深色已通过;默认人设、提示词层级、富文本编辑器、文件修改过程和跨窗口即时同步尚未闭环。**
- 已验证:五张卡、8 种回复风格、身份 inline 编辑、失焦丢弃、长文本弹窗、配置持久化、重开恢复、深色布局。
- P2 一致性问题:默认人设展示但既定个性化逻辑不会把该默认值注入请求;内置提示词是否提供等价行为仍待证明。
- 实际风险:欢迎语通过 localStorage 的进程内自定义事件同步,主窗口没有可靠接收另一 BrowserWindow 的变更。
- 功能未接:“显示文件修改过程”在 `PersonalizationSettingsPanel.tsx:287` 始终禁用。
- 功能未接:编辑器实际是 `<textarea>`,见 `PersonalizationSettingsPanel.tsx:306`;原版 `.sc-editor-content` 的富文本格式、粘贴、快捷键和内容模型未复刻。
- 架构差异:个性化段落被拼入用户消息而不是 system/developer 指令,见 `src/main/ipc.ts:774-781`。
- 证据缺口:原版默认人设与宿主提示的关系、个性化字段真正的角色层级仍未知。
- 测试盲区:`verify-personalization-settings.cjs:210-217` 只在设置窗口验证 localStorage 和配置,不验证主窗口当前加载提示;纯测试只验证字符串函数,不验证真实 `chat:send` 后的角色和内核输入;没有测试富文本粘贴、超长内容的 UI 反馈或真实上游收到的个性化内容。
- 外部副作用:既有 REA 操作曾把 WorkBuddy“对你的称呼”保存为 `CodexTmp`,原版空值确认无法直接恢复。该问题属于 WorkBuddy 共享运行态清理,不在 YCode 业务代码中;本轮未操作。
## 跨条目测试覆盖矩阵
| 场景 | 已有覆盖 | 结论 |
| --- | --- | --- |
| 创建前失败,如模型不存在 | `verify-new-task-switch` 覆盖 | 通过 |
| 创建成功后、回合启动前失败 | 未覆盖 | 会留下空会话,需补回归 |
| 原版完整准备阶段推进 | 未覆盖 | 当前只接一条占位 |
| 账户菜单 header 实际高度 | 仅 `min-height` | 仍多约 4.19px |
| 账户菜单 SVG path | 未覆盖 | Lucide 占位 |
| 通用目录超大/扫描超时 | 未覆盖 | partial 被当成完整值 |
| 打开目录失败 | 未覆盖 | 错误被吞掉 |
| 真实系统登录项/通知 | 隔离掉 | 仅验证偏好与 IPC 可调用 |
| 深色首帧/重载 | 未覆盖 | native 背景为白色;白闪风险未观察确认 |
| 深色全表面 | 仅局部稳定态 | 独立窗口、弹层、内容块未覆盖 |
| 快捷键 supported 声明 | 静态目录对照 | 14 行中仅 4 个唯一动作真实执行 |
| Esc + 生成 + 全屏 | 未覆盖 | 源码存在双动作 |
| 欢迎语跨两个 BrowserWindow | 未覆盖 | 当前流式状态栏可能不即时更新 |
| 默认人设真实注入 | 未覆盖,且离线测试明确禁止个性化模块注入默认值 | 页面说明与实际字段不一致,行为等价待证 |
| 个性化真实请求 roles | 未覆盖 | 只测字符串拼装 |
## 建议处理顺序
1. 修复 08 创建后失败状态机,并增加成功建会话后拒绝 `chat:send` 的回归。
2. 修复 10 的 partial 展示和 `openFolder` 错误返回。
3. 明确 12 的 Esc 优先级,并把 14 个 supported 声明逐个做成行为断言。
4. 先用原版证据确认 13 默认人设与个性化提示词层级,再处理 UI 一致性和真实请求回归;同时修跨窗口欢迎语同步。
5. 补 09 账户 header 实高与图标 REA 证据。
6. 最后补 11 首帧、重载、独立窗口及弹层/内容块主题矩阵。
## 跨模块依赖(不计入本文问题数)
- 独立任务窗口的 `chat:event` 路由问题已由主上下文确认。它会扩大 08 中“切换任务后运行状态仍正确”的适用面,但本文不重复计项。
- 备用网关 history 遗漏 `quotes` 已由主上下文确认,属于对话投影/网关链路,本文不重复计项。
- memory 默认偏好误写已由主上下文确认,属于 14 记忆模块,本文只提示其可能影响跨设置持久化验收,不重复计项。
完成这些后仍需用 WorkBuddy 对应版本的 REA Evidence 做同视口、同状态、同输入对比;本轮专项 PASS 只能证明列出的断言范围,不等于 08~13 已全部 1:1。
@@ -0,0 +1,408 @@
# 对话区与侧栏验收问题收集(2026-10-17)
## 范围与结论
本报告只审查 TODO 01~07,不实现修复,不修改业务代码,不启动新的 Electron 或 WorkBuddy 实例。审查依据为当前工作树源码、现有测试和主 agent 在 `2026-10-17 16:50:14` 串行执行的真实隔离 Electron 报告。
当前结论不是“01~07 已全部 1:1 完成”。本轮 6 个专项脚本全部通过,但源码审查确认仍有 4 个高优先级行为缺口:
1. 从独立任务窗口发起的流式对话收不到后续 `chat:event`,窗口会停在流式态。
2. 表格引用只在当前回合进入提示词,备用内核后续回合重建历史时丢失引用内容。
3. 编辑带产物变更的消息时,没有原版的“是否回退产物历史版本”选择,而是直接回退。
4. 回退产物时没有原版“不回退”动作,也没有逐产物缺失版本/保留当前版本的分支。
另有归档入口不可回访、引用来源标题功能尚缺且原版宿主标题待取证、原生分支不可用时错误在提交后才暴露等中优先级问题。04、06、07 未发现新的核心代码缺陷,但仍分别存在用户约定差异、像素证据缺口和占位依赖。
本轮没有新调用 WorkBuddy。主 agent 通过 REA 请求已知 `9223` 端口得到 `provider_unavailable/endpoint_unreachable`,这只能证明该已知端口当前不可达,不能扩大为“REA 整体不可用”。已有 REA Evidence ID 只作为历史原版证据引用。
### 七项逐项状态
| TODO | 当前状态 | 结论摘要 |
| --- | --- | --- |
| 01 任务操作菜单 | 发现问题 | 独立窗口流式事件断开;归档后无可回访入口;另有像素/键盘证据缺口。 |
| 02 表格引用 | 发现问题 | 备用内核下一回合丢失引用快照;来源标题是待取证的功能缺口;草稿保留测试无效。 |
| 03 编辑消息 | 发现问题 | 缺少编辑前产物回退选择;原生分支缺失在提交后才报错;错误码映射不完整。 |
| 04 悬停时间 | 当前用户约定范围通过 | 三档时间规则正确;“昨天”是已确认的用户约定差异,不是 Bug;动画帧仍缺证据。 |
| 05 回退消息与产物 | 发现问题 | 缺少“不回退”动作;缺失版本 fallback、失败 toast 和弹窗像素未实现。 |
| 06 滚动条 | 当前范围通过 | 浅色像素及跟随行为通过;深色/hover/周边 affordance 仍缺运行时证据。 |
| 07 侧栏最下一行 | 核心布局当前范围通过 | 普通浅色几何通过;深色、窄态、真实头像和 mock 功能仍有依赖/证据缺口。 |
## 分类口径
| 分类 | 含义 |
| --- | --- |
| 实际缺陷 | 当前 YCode 行为会在用户可触发的条件下失败、丢信息或明显偏离已确认交互。 |
| 占位/依赖 | 入口或视觉已存在,但真实数据、服务或运行链路由其他待办项提供,当前范围只保留占位。 |
| 证据缺口 | 当前实现和测试没有失败,但缺少与原版同状态、同输入、同环境的像素、时序或运行时证据。 |
| 用户约定差异 | 与原版不同是用户已经明确要求或接受的范围差异,不应由本报告重新判成 Bug。 |
| 测试盲区 | 测试 PASS,但断言没有覆盖验收声明的关键条件,或断言本身没有构造前置条件。 |
编辑前的产物回退选择与回退弹窗“不回退”动作有已回收的原版源码或词典证据,本轮没有新跑对应原版画面。行为分支缺失仍属于实际缺陷;缺少逐帧动画或截图只记为证据缺口,不单独算业务 Bug。
## 本轮执行证据
主 agent 的批次目录为 `.tmp/audit19-20261017-165014/`,其中 `results.json` 记录以下脚本退出码为 0。子 agent 没有自行启动 Electron。
| 专项 | 本轮结果 | 实际报告 |
| --- | --- | --- |
| `verify-task-menu` | PASS,42/42 | `.tmp/task-menu-cI3qQG/report.json` |
| `verify-task-running-menu` | PASS,21/21 | `.tmp/task-running-AzKLph/report.json` |
| `verify-quote-table` | PASS,48/48 | `.tmp/quote-table-M1X5lY/report.json` |
| `verify-user-message-edit` | PASS,67/67 | `.tmp/edit-Sf68uQ/report.json` |
| `verify-conversation-scrollbar` | PASS,24/24,本轮没有 skip | `.tmp/scrollbar-6P7oPm/report.json` |
| `verify-sidebar-footer` | PASS,29/29 | `.tmp/sidebar-footer-Edo50t/report.json` |
| `npm run test:conversation-ipc` | PASS,输出 `all checks passed` | 日志 `.tmp/audit19-20261017-165014/conversation-ipc.log` |
主 agent 本轮还确认 `npm run build` 通过。构建通过和上述脚本通过只证明已覆盖路径,不证明以下未覆盖状态。
---
## 01. 任务操作菜单
**结论:发现问题。**
### 01-01 独立任务窗口可以打开任务,但不能正常接收流式回复
- 类别:实际缺陷。
- 优先级:高。
- 复现条件:在主窗口对一个会话选择“在独立窗口中打开”,在任务窗口发送一条会流式输出的消息。
- 当前行为:任务窗口的 `chat.send()` 能取得 `runId` 并建立本地 `running` 状态,但主进程后续只向 `getWindow()` 返回的主窗口发送 `chat:event`。任务窗口收不到增量、工具或终态事件;主窗口即使收到,也因为本窗口没有对应的 `runs[runId]` 而在 `applyEvent` 中丢弃。用户看到任务窗口一直处于流式/停止状态。
- 代码证据:
- `src/main/index.ts:168` 注册 IPC 时固定传入 `() => mainWindow`。
- `src/main/ipc.ts:628` 在 `chat:send` 中取得 `win = getWindow()`。
- `src/main/ipc.ts:737`~`src/main/ipc.ts:741` 只向该窗口发送后续事件。
- `src/renderer/src/stores/chat.ts:406`~`src/renderer/src/stores/chat.ts:412` 对未知 `runId` 的事件只尝试进入待定队列,否则直接返回。
- 测试盲区:`scripts/verify-task-menu.cjs:143`~`scripts/verify-task-menu.cjs:150` 只检查独立窗口数量及 URL 中的 `taskId`,随后销毁窗口,没有在任务窗口发送或接收任何消息。
- 建议验收:真实打开任务窗口,在两个窗口分别发起和观察同一回合;要求增量正文、工具事件、取消和终态都到达发起窗口,主窗口侧栏状态也能同步,不出现“主窗口无运行、任务窗口永不停”。
### 01-02 归档恢复入口(已解决)
- 类别:实际缺陷,并与 TODO 17 的用户范围叠加。
- 优先级:中高。
- 当前结论:YCode 的产品入口缺口已解决;原版归档弹窗的同状态像素与动画证据仍缺。
- 复现条件:对空闲任务选择“归档”并确认。
- 修复前行为:任务从默认侧栏消失,`session:list-archived` IPC 和预加载接口存在,但渲染器没有任何 `listArchived` 调用或归档列表入口。确认框却明确提示用户前往“系统设置-数据管理”查看,而数据管理已按用户要求移除。普通用户没有路径查看或取消归档。
- 代码证据:
- `src/renderer/src/components/task-confirm-dialog.tsx:21`~`src/renderer/src/components/task-confirm-dialog.tsx:27` 保留“系统设置-数据管理”文案。
- `src/renderer/src/components/SettingsDialog.tsx:23`~`src/renderer/src/components/SettingsDialog.tsx:32` 明确移除 `dataManagement`。
- `src/preload/index.ts:170` 当前只暴露归档列表 API;产品渲染源码没有消费该接口。
- 修复:侧栏任务分组标题新增“已归档”入口;`ArchivedTasksDialog` 显式过滤 `archivedAt !== undefined`,支持“恢复”和“查看”,其中“查看”会先恢复再打开原任务;空间任务恢复后回到原空间分组;确认文案改为“确认将该任务归档吗?归档后可在侧栏「已归档」中查看并恢复。”,不再指向已移除的数据管理。
- 验证:`node scripts/verify-archived-tasks.cjs` 25/25 PASS,覆盖加载态、只列归档任务、独立/空间标识、恢复与查看、快速重复点击、空间分组回归、消息保留、空态、载入失败、重试恢复、Escape 关闭和无渲染器错误;报告 `.tmp/archived-tasks-8VDzsw/report.json`。`npm run typecheck`、`npm run build`、`verify-task-menu`、`verify-sidebar-footer`、`verify-conversation-scrollbar` 均通过。
- 剩余范围:本轮解决 YCode 的可发现与可恢复性,没有通过 REA 采集原版“已归档”列表的同状态画面,因此不能把该弹窗宣称为原版逐像素 1:1。
### 01-03 已确认的原版证据缺口
- 类别:证据缺口。
- 未覆盖:运行中禁用菜单、归档/删除确认弹窗、鼠标悬停态的原版实时像素;置顶行独立图标。
- 当前依据:固定 REA Evidence `ev_f5e9d71a676c453daab40928acfb33aeabae68ba57ffda342380cead7f1a1fe2` 的 DOM/CSS/几何,以及运行源码中“任务进行中禁用归档”的规则。
- 测试盲区:菜单没有 ArrowUp/ArrowDown/Home/End 键盘导航及初始焦点断言;现有脚本只覆盖 Escape/点击关闭等已经实现的部分。
### 01-04 用户约定差异
- 云端的“分享任务”“保存到工作空间”“批量操作”当前不渲染。用户已要求云端服务暂不处理,因此这不是本阶段阻塞项;但它导致菜单高度、条目数量和整卡像素不可能与原版 9 项菜单相同,不能宣称该状态逐像素 1:1。
---
## 02. 表格“添加到对话”及引用结果
**结论:发现问题。**
### 02-01 引用表格只进入当前回合,备用内核下一回合重建历史时丢失
- 类别:实际缺陷。
- 优先级:高。
- 复现条件:当 OMP 不可用、走备用 Gateway/Chat Completions 路径时,第一回合引用一张表并发送;第二回合继续追问表中的数据。
- 当前行为:第一回合的 `promptText` 包含引用 markdown,消息对象也持久化了 `quotes`。但第二回合构建历史时,用户消息只重建正文、附件、资源和能力上下文,没有追加该消息的 `quotes.markdown`。上游收到历史时看不到第一回合引用过的表格。
- 代码证据:
- `src/main/ipc.ts:771`~`src/main/ipc.ts:781` 当前回合把 `quoteText` 拼入 prompt。
- `src/main/ipc.ts:816`~`src/main/ipc.ts:820` 重建历史时遗漏 `message.quotes`。
- 测试盲区:`verify-quote-table` 替换了 `chat:send`,只检查 IPC 载荷;`scripts/conversation-ipc-regression.ts` 没有引用载荷及两回合上游历史断言。因此 48 项 PASS 不覆盖“后续会话仍能看到引用内容”。
- 建议验收:使用备用内核连续两回合,第一回合引用表,第二回合要求模型基于该表回答;在受控上游记录中确认两回合的历史包含引用快照。
### 02-02 输入区卡片没有来源标题,原版宿主标题仍待取证
- 类别:功能缺口 + 证据缺口。
- 优先级:中。
- 当前行为:原版组件预留“作者 + 分隔符 + 摘要”,YCode 组件也支持 `quote.title`,但创建表格引用时从未设置 `title`。当前卡片只显示被压缩的 markdown 摘要。
- 代码证据:
- `src/renderer/src/workbuddy/conversation/TableMessageQuoteAction.tsx:24`~`src/renderer/src/workbuddy/conversation/TableMessageQuoteAction.tsx:33` 只写 `markdown`、`preview`,不生成 `title`。
- `src/renderer/src/workbuddy/conversation/MessageQuoteCard.tsx:29` 只在 `quote.title` 存在时渲染作者标题。
- 证据缺口:TODO 已记录原版标题由宿主 `message.quote.add` 处理端生成,尚未采集实际标题格式。
- 建议验收:通过 REA 取得至少一个真实表格引用的宿主结果和几何,再实现标题;不能猜固定文案。
### 02-03 “不覆盖已有草稿”没有真正被测试
- 类别:测试盲区。
- 代码实现本身未发现丢草稿问题:`ChatPane.quoteTable` 使用当前草稿展开新对象,只追加 `quotes`。
- 测试问题:`scripts/verify-quote-table.cjs:155` 的断言名是“不覆盖已有草稿”,但预期值是空字符串 `''`,且在此前没有向输入框写入任何草稿。该断言只证明“空草稿仍为空”,不能证明已有正文被保留。
- 建议验收:先输入一段正文,再点击“添加到对话”,检查正文不变、引用追加;随后移除引用,正文仍应保留。
### 02-04 原版运行时证据缺口
- 类别:证据缺口。
- 未覆盖:原版点击后输入区卡片与发送后消息标签的实际截图/几何;品牌 token 的运行时定义;来源标题。
- 当前依赖:REA Evidence `ev_38cca1ac66606ab07fb495035e3509f5c4d8ec3e5e2b74d0fdcfcfadb120da3b` 和样式表 `src-CyXHA0IC.css`,不是两态运行截图。
- 本轮 PASS 只覆盖 YCode 自身几何/交互和 IPC 载荷,不替代原版逐像素验收。
---
## 03. 编辑已发送消息
**结论:发现问题。**
### 03-01 编辑带产物的消息时没有询问是否回退历史版本
- 类别:实际缺陷。
- 优先级:高。
- 复现条件:选择一条用户消息,其后续回复通过 `write_file`/`edit_file` 改动过文件;点击编辑并提交。
- 当前行为:编辑器直接调用 `send(..., action: 'edit')`,主进程在分支前固定执行 `rollbackEditedArtifacts`。用户没有机会选择“基于旧版本编辑”或“从最新版本开始编辑”。
- 代码证据:
- `src/renderer/src/components/ChatPane.tsx:251`~`src/renderer/src/components/ChatPane.tsx:254` 直接提交编辑。
- `src/main/ipc.ts:663`~`src/main/ipc.ts:664` 固定回退产物。
- `src/renderer/src/workbuddy/conversation/rewind-labels.ts:19`~`src/renderer/src/workbuddy/conversation/rewind-labels.ts:22` 已回收原版询问文案,但产品代码没有使用。
- 证据:REA 文案 Evidence `ev_e91787b1d4dea651582c6cf12827775b9068b0ee8b80c68ba08d4c1d87d94e90`;行为接线见 TODO 已记录的 `rewind.edit.*`。
- 测试盲区:`verify-user-message-edit` 覆盖直接回退成功和分支失败撤销,却没有验证提交前询问及“不回退”分支。
- 建议验收:有历史产物时,提交编辑先出现原版询问;选择回退与不回退分别得到正确文件内容和对话前缀。
### 03-02 UI 对任何用户消息显示编辑入口,但后端可能没有原生分支
- 类别:实际缺陷。
- 优先级:中。
- 复现条件:打开旧 YCode 会话或导入会话,在 OMP 可用但目标用户消息没有 `nativeEntryId` 时点击编辑并提交。
- 当前行为:编辑器先完整打开并允许输入;提交后主进程才报“当前 OMP 会话的分支记录”错误。对于非最后一条消息,代码没有重建分支的回退路径。
- 代码证据:
- `src/renderer/src/components/MessageBubble.tsx:79`~`src/renderer/src/components/MessageBubble.tsx:81` 只要会话空闲就显示编辑能力。
- `src/main/ipc.ts:667`~`src/main/ipc.ts:676` 对缺少原生分支的消息拒绝提交,只有最后一条 cancelled/error 消息可能现场恢复。
- 测试盲区:真实窗口测试使用带 `nativeEntryId` 的种子消息;IPC 回归也以原生分支可用为前提,没有覆盖“编辑器打开后后端明确不可编辑”的 UI。
- 建议验收:在打开编辑器前判定可编辑性,或在支持历史重建后再显示入口;不能等用户输入完毕才报分支缺失。
### 03-03 提交错误仍被压缩为两类文案
- 类别:实际缺陷。
- 优先级:中低。
- 当前实现:只按“正在运行/尚未结束/in progress”正则区分 `retryLater` 和 `sendFailed`。
- 原版证据:`resend-settle-timeout`、`cancel-barrier`、`send-in-progress` 等错误码有映射表,但 YCode 没有逐错误码处理。
- 代码证据:`src/renderer/src/workbuddy/conversation/UserMessageEditor.tsx:82`~`src/renderer/src/workbuddy/conversation/UserMessageEditor.tsx:93`。
- 测试盲区:本窗口测试的 `chat:send` 总是延迟后成功,没有触发任一错误码。
- 建议验收:至少模拟 `send-in-progress`、`resend-settle-timeout`、`cancel-barrier` 和未知错误,逐项校验文案与编辑器保持状态。
### 03-04 编辑器交互和时序仍有盲区
- 类别:证据缺口/测试盲区。
- 编辑器使用原生 `contentEditable` 并由 `innerText` 取值,而原版为 Slate 编辑器。现有测试通过程序写入 `innerText` 和合成 Enter,不覆盖富文本粘贴、IME composition、Shift+Enter 多行、Undo/Redo 和换行 DOM 归一化。
- 工具条淡入 0.2s、默认 0.15s 延迟到 hover 后 0 秒的逐帧表现未采集。`verify-user-message-edit` 只验证静止计算值。
- 原版 `userMessage.edit.resume` 的“恢复到此版本”状态未实现,英文/中文文案在 `source-labels.ts` 中已有,但产品没有该状态。
---
## 04. 用户消息悬停时间
**结论:当前用户约定范围通过,仍有证据缺口。**
### 04-01 时间三档规则已按用户确认落地
- 当天:`HH:mm`
- 本年非当天:`M月D日 HH:mm`
- 非本年:`YYYY年M月D日 HH:mm`
- 代码:`src/renderer/src/components/AssistantMessageDetails.tsx:4`~`src/renderer/src/components/AssistantMessageDetails.tsx:15`。
- 本轮 `verify-user-message-edit` 67/67 通过,覆盖四种日期情形、会话切换与历史重载;`test:assistant-footer` 也覆盖无效时间。
### 04-02 “昨天 HH:mm”是有意保留的用户约定差异
- 类别:用户约定差异。
- 原版 `formatMessageTime` 有 `昨天 HH:mm` 分支;用户已经确认当前三档时间规则,因此当前实现不构成需修复 Bug。
- 原版证据:REA 回收 `ToolbarRenderer`、`src-CyXHA0IC.css`;运行时 Evidence `ev_245ea1740cde21869500348e47cf9663a1ea72943ccfe79c163a6838a153bfd3`。
### 04-03 工具条入场时序仍是证据缺口
- 当前测试只校验 `opacity/pointer-events` 的 hover 前后值和 `transition` 声明,没有验证 0.15s 延迟退出、hover 立即进入及 tooltip 同步出现。
- 原版未覆盖:默认状态向 hover 态过渡的逐帧画面、tooltip 出现延迟和隐藏延迟。
---
## 05. 用户消息回退操作与图标
**结论:发现问题。**
### 05-01 产物回退弹窗缺少“不回退”动作
- 类别:实际缺陷。
- 优先级:高。
- 复现条件:回退点之后有可回退文件,点击回退按钮。
- 当前行为:弹窗只有“取消”和“回退”,没有原版的“不回退”。用户不能保留当前文件、只回退对话。
- 代码证据:
- `src/renderer/src/components/ChatPane.tsx:451`~`src/renderer/src/components/ChatPane.tsx:455` 只渲染两个动作。
- `src/main/ipc.ts:608`~`src/main/ipc.ts:622` 有变更时总是先恢复文件,没有 skip 参数。
- `src/renderer/src/workbuddy/conversation/rewind-labels.ts:32` 已回收 `rewind.action.skipRewind = 不回退`,但未接线。
- 原版证据:`ev_e91787b1d4dea651582c6cf12827775b9068b0ee8b80c68ba08d4c1d87d94e90`。
- 测试盲区:`verify-user-message-edit` 只测取消和确认;IPC 回归测成功、冲突和失败,没有“skip artifacts”模式。
### 05-02 缺失/被删除的历史版本没有原版 fallback 行为
- 类别:实际缺陷/部分占位。
- 优先级:中高。
- 复现条件:消息之后创建或修改文件,随后在 YCode 外删除该文件;再从对应消息回退。
- 当前行为:预览基于工具记录显示“回退至历史版本”或“将被删除”;确认时 `fs.realpath`/读取当前文件失败,整次回退报错。没有原版的“历史版本已被删除,将保持当前版本”“该产物无历史版本,回退后文件仍会保留”等逐产物状态。
- 代码证据:
- `src/renderer/src/components/ChatPane.tsx:444`~`src/renderer/src/components/ChatPane.tsx:448` 只映射“将被删除/回退至历史版本”。
- `src/main/workspace-revert.ts:15`~`src/main/workspace-revert.ts:26` 要求每个当前文件存在且内容精确等于 `after`。
- 原版文案已在 `rewind-labels.ts:23`~`rewind-labels.ts:29` 回收,说明这些是可区分的产品状态。
- 测试盲区:IPC 回归覆盖外部修改冲突,但只验证“失败且不破坏文件”,没有验证用户可见 fallback 或继续回退其他可处理产物。
### 05-03 失败仍以内联原始错误显示,而非原版 toast
- 类别:实际缺陷。
- 优先级:中。
- 当前行为:`chat:revert` 的原始异常直接显示在回退弹窗中。
- 代码:`src/renderer/src/components/ChatPane.tsx:450`、`src/renderer/src/components/ChatPane.tsx:454`。
- 原版应显示 `rewind.error.rollbackFailed` 的 toast;当前已有文案 `rewind-labels.ts:35`,未接线。
- 测试盲区:真实窗口脚本没有制造回退失败;IPC 回归只看 Promise rejection 和文件状态。
### 05-04 弹窗不是源码级视觉实现
- 类别:证据缺口。
- 当前 `ChatPane` 复用通用 `.overlay/.dialog/.btn`,没有接入原版 Modal/产物清单的逐项规则。
- 未覆盖:弹窗几何、遮罩动画、标题/正文排版、产物行密度、按钮布局和深浅色。
- `hasMoreRequests` 分页首条消息显示规则无法按原版直接验证;YCode 当前全量加载,因此按“非首条用户消息”近似。
---
## 06. 对话区域滚动条
**结论:当前范围通过,完整 1:1 验收仍有缺口。**
### 06-01 已通过的本轮证据
- `verify-conversation-scrollbar` 24/24,PASS,本轮没有 skip,报告 `.tmp/scrollbar-6P7oPm/report.json`。
- 浅色真实像素:6px 滑块位于 `x=1328..1333`,颜色合成 `rgb(204,204,204)`,透明轨道和槽位位置与原版截图一致。
- 行为:初始贴底、流式跟随、用户上滚停止跟随、回到底部恢复跟随、短会话稳定槽位、两个窗口尺寸,均通过。
- 未启用 `hideScrollbarWhenIdle`,与原版运行源码一致。
### 06-02 深色和 hover 仍不是运行时像素验收
- 类别:证据缺口。
- `scripts/verify-conversation-scrollbar.cjs:208`~`scripts/verify-conversation-scrollbar.cjs:209` 只检查 CSSOM 中 hover token 规则。
- `scripts/verify-conversation-scrollbar.cjs:214`~`scripts/verify-conversation-scrollbar.cjs:217` 临时切换 `data-cr-theme=dark` 后只读取 `scrollbar-color`,没有截深色滑块,也没有 hover 渲染像素。
- 原版同样未采集 hover/深色运行时像素,因此当前视觉依据是固定 CSS 与 token,而不是同状态帧。
### 06-03 未纳入的滚动条周边能力
- 类别:证据缺口。
- 原版滚动条附近的“回到底部”等 affordance 尚未确认,因此本项不能证明所有滚动行为 1:1。
- 本轮覆盖 1336、1100、1600 三种宽度;未覆盖 480 最小宽度、系统缩放/高 DPI、macOS/Linux 原生滚动条行为。
---
## 07. 侧栏最下一行
**结论:核心布局当前范围通过;深浅色、窄态和真实账号仍有依赖/证据缺口。**
### 07-01 已通过的本轮证据
- `verify-sidebar-footer` 29/29,PASS,报告 `.tmp/sidebar-footer-Edo50t/report.json`。
- 1336 宽窗口下 footer 为 57 高,padding `8 12 8 8`,1px 顶边,账号/通知/小程序三项顺序正确,底部无收起入口。
- 账号触发器、头像框、姓名、通知角标、hover token,以及顶栏收起/折叠轨道展开入口均通过。
### 07-02 深色和窄态没有运行时验收
- 类别:证据缺口。
- `verify-sidebar-footer` 没有深色截图、计算值或切换断言;脚本只运行在 1336×902 浅色主窗口。
- 窄窗口走左侧 drawer 的产品路径没有在本脚本中执行。TODO 写的是“普通/窄侧栏、浅色/深色”,本轮 29 项 PASS 只覆盖普通浅色与折叠轨道。
- 原版深色 footer 也没有运行时像素证据,当前只有回收 token。
### 07-03 真实头像与内描边仍由账号能力阻塞
- 类别:占位/依赖。
- 当前使用 `#0056d4` 字面首字母头像,不渲染原版真实图片及 `--wb-avatar-fallback-*` 的 1px 内描边。
- 这与 TODO 09 的账号数据尚未接入相关;在本项可视为依赖,但“头像 1:1”尚未完成。
### 07-04 通知与小程序仍是占位
- 类别:占位/依赖,属于 TODO 09 和后续待拆解项。
- 通知角标是 mock 数据;小程序按钮点击显示未实现提示。它们不影响本轮 footer 几何,但不能宣称功能 1:1。
### 07-05 其余轻微差异
- 侧栏整数宽 284px,原版运行时为 284.28px,存在 0.28px 理论残差;本轮几何断言四舍五入后一致。
- 原版 footer 的独立深浅色像素、边缘翻转/窄宽挤压及图标资源仍缺同状态证据。
---
## 23 条原始遗留逐项归档
此前对“19 项”的汇总按 TODO 条目分组;01~07 全文实际包含下列 23 个遗留子项。本表用于确认没有只读每段前四条。
| ID | 原始遗留 | 本次判断 |
| --- | --- | --- |
| 01-A | 菜单/确认弹窗/hover/运行中无原版实时像素 | 证据缺口 |
| 01-B | 置顶行图标缺独立原版证据 | 证据缺口 |
| 02-A | 输入区卡片和发送后标签缺原版截图/几何 | 证据缺口 |
| 02-B | 品牌 token 只有兜底值 | 证据缺口 |
| 02-C | 引用来源标题未采集且当前未实现 | 功能缺口 + 原版具体标题待取证 |
| 03-A | 工具条淡入和 tooltip 时序未逐帧采集 | 证据缺口 |
| 03-B | 原版提交错误码集合未完整映射 | 实际缺陷 |
| 03-C | `userMessage.edit.resume` 未实现 | 实际缺陷/未接状态 |
| 03-D | 原版“昨天”时间分支 | 用户约定差异 |
| 04-A | “昨天”分支 | 用户约定差异 |
| 04-B | 工具条延迟切换未逐帧采集 | 证据缺口 |
| 05-A | 回退弹窗几何与动画未对齐 | 证据缺口 |
| 05-B | 回退失败 toast 未接 | 实际缺陷 |
| 05-C | 逐产物历史缺失/保留状态未实现 | 实际缺陷 |
| 05-D | 编辑前产物回退选择未实现 | 实际缺陷 |
| 05-E | `hasMoreRequests` 首条回退规则无法直接复刻 | 当前全量加载下的近似/证据缺口 |
| 06-A | hover 和深色无运行时像素 | 证据缺口 |
| 06-B | 槽位宽度只能由像素推断 | 证据缺口;本轮 YCode 像素一致 |
| 06-C | 回到底部等周边 affordance 未覆盖 | 证据缺口 |
| 07-A | 0.28px 宽度残差 | 证据缺口,实际可见差异为 0 |
| 07-B | 真实头像和 1px 内描边未接 | 占位/依赖 |
| 07-C | 深色 footer 无运行时像素 | 证据缺口 |
| 07-D | 通知角标和小程序为 mock | 占位/依赖 |
此外,本次源码审查新增了 TODO 原文未拆出的四项高优先级问题:跨窗口事件、引用历史丢失、原生分支不可用的后置报错、归档入口不可回访。它们应进入下一轮修复或补充 TODO,不应用“专项脚本 PASS”覆盖。
## 建议修复与验收顺序
1. 先修 `chat:event` 的窗口路由。事件应到达发起窗口,并按需要同步其他窗口;至少不得使用全局 `mainWindow` 作为所有对话事件的唯一目标。
2. 在备用内核重建历史时追加每条用户消息的引用快照,并增加两回合上游请求断言。
3. 将编辑提交前产物询问和回退弹窗“不回退”接线;同时补齐逐产物缺失版本/保持当前版本状态。
4. 将原生分支能力作为编辑/回退入口的前置条件,或在编辑器打开前明确不可用。
5. 修复归档的可回访产品路径与已移除“数据管理”的冲突。
6. 先通过 REA 取得原版宿主生成的引用来源标题,再按证据补齐;同时完成逐错误码编辑文案、失败 toast 和真实窗口错误态回归。
7. 最后做原版同状态像素与动画验收:菜单 hover/运行中、引用两态、编辑失败、回退弹窗、滚动条 hover/深色、footer 深色/窄态。
## 建议命令
以下命令由主上下文串行运行,避免多个 Electron 并行抢焦点:
```powershell
npm run build
node scripts/verify-task-menu.cjs
node scripts/verify-task-running-menu.cjs
node scripts/verify-quote-table.cjs
node scripts/verify-user-message-edit.cjs
node scripts/verify-conversation-scrollbar.cjs
node scripts/verify-sidebar-footer.cjs
npm run test:conversation-ipc
npm run test:chat
npm run test:conversation
```
建议新增纯测试或受控集成测试:
```text
独立任务窗口发起消息并流式完成
备用内核第二回合仍携带前一轮 quote markdown
已有正文时添加/移除表格引用不丢正文
编辑有产物和无产物消息时出现正确回退选择
回退时选择 skip artifacts、文件已删除、外部修改和部分产物可回退
深色滚动条及 hover 的真实截图
深色 footer、窄 drawer footer、真实头像 fallback
```
## 报告限制
- 本报告没有修改或构建业务代码。
- 本报告没有启动 Electron;六个专项结果是主 agent 在本轮串行批次的运行结果。
- 本轮没有直接操作 WorkBuddy,也没有读取安装包、导出 Evidence JSON 或其他绕过 REA 的渠道。
- 已有 REA 结论均来自项目文档中记录的固定 Evidence;不能替代缺失的运行时状态。
- 已知 `9223` 端口不可达不影响“通过其他 REA target/静态分析继续取证”的总体能力,但本报告没有把该失败视为 WorkBuddy 功能结论。
@@ -0,0 +1,317 @@
# 验收问题收集:记忆、智能体、导航、关于、Git 与上下文
日期:2026-10-17
范围:复刻待办清单 14、15、16、17、19、21
约束:不改业务代码、测试或 TODO;WorkBuddy 结论只引用现有 REA Evidence,不把已知 9223 `endpoint_unreachable` 推断成 REA 全部不可用。
## 本轮结论
| 条目 | 当前结论 | 需要关注的内容 |
| --- | --- | --- |
| 14 记忆与进化 | **P2 实际问题** | 首次只切换“对话记忆”会错误把“本地记忆与技能”偏好写成开启;本地执行器未接,不把偏好错误写成记忆内容丢失。 |
| 15 智能体团队管理 | **当前范围主体通过** | 持久化、重开恢复、深浅主题、布局均通过;子代理调度和云端服务按用户要求不计 Bug。另有首帧默认值闪烁和键盘交互测试缺口。 |
| 16 移除助理与设备连接 | **当前范围通过** | 四个移除入口均不在导航、搜索和渲染页面中;旧深链接回退有效。`settings-nav` 的失败是脚本仍把 memory 当未接入。 |
| 17 移除数据管理与应用管理 | **当前范围通过** | 与 16 同一实现和回归;没有发现业务缺陷。 |
| 19 关于与获取帮助 | **当前范围通过,有证据缺口** | 布局、版本、深浅主题和“无外跳链接”通过;独立更新窗口未实现,但外跳/更新服务按本轮约束不计 Bug。获取帮助深色态和按钮点击无动作缺直接断言。 |
| 21 Git 分支与会话上下文 | **不能验收为 1:1 完成** | 有 P2 功能缺口、准确性风险、实现限制、布局风险和证据缺口:Chat Completions usage 未接、切模型样本来源未绑定、detached HEAD 保守不渲染、五类占比和窗口控件未实现、紧凑分支未启用、浮层碰撞未验收、刷新与状态未验收。 |
## 分类口径
- **实际问题**:源码行为可确定地产生错误、错误状态或与验收条件冲突。
- **功能未接**:界面已存在但能力没有实现;若是用户本轮明确排除的云端、子代理调度或外跳链接,不列为 Bug。
- **证据缺口**:原版行为或视觉尚未取得 REA 证据,当前不能宣称 1:1。
- **验收脚本维护缺陷**:产品行为符合当前基线,但专项脚本仍检查旧 DOM、旧支持列表或旧默认状态。
- **测试盲区**:已有断言通过,但没有覆盖验收条件中的关键状态或副作用。
## 14. 记忆与进化
### 14-1 [P2 实际问题] 首次切换“对话记忆”会顺带把“本地记忆与技能”偏好写成开启
触发条件:
1. 新用户配置或 `config.json` 中尚无 `memoryPreferences`。
2. 打开“设置 → 记忆与进化”,页面正确显示“对话记忆”开启、“本地记忆与技能”关闭。
3. 只切换“对话记忆”。
源码原因:
- `src/shared/memory.ts:30` 把两个默认值定义为 `conversationMemory: true`、`localMemorySkills: false`。
- `src/shared/memory.ts:38` 却用 `value?.localMemorySkills !== false` 归一化;传入 `undefined` 时返回 `true`,与声明的默认值不一致。
- `src/main/memory-settings.ts:34` 在更新前直接调用 `normalizeMemoryPreferences(await getStoredMemoryPreferences())`,没有先合并 `DEFAULT_MEMORY_PREFERENCES`。
- 因此第一次只更新任一项偏好时,`localMemorySkills` 会被归一化成 `true`,随后写入 `config.json`。
- `src/renderer/src/components/MemorySettingsPanel.tsx:26` 的初始视图也使用同一个函数;在真实数据返回前,“本地记忆与技能”会短暂显示开启。
影响:
- 用户没有操作“本地记忆与技能”,其本地偏好却被持久化修改。
- 现有 `.tmp/memory-settings-poBX32/report.json` 虽然 PASS,但没有在“只切换对话记忆”后断言第二项仍为关闭。
边界:
- 当前“本地记忆与技能”自动记录执行器尚未接入,因此没有证据表明本次偏好误写已经造成记忆正文、工作记录或技能数据丢失。
- 本项定级为 P2 偏好写入副作用,不做 P1 数据损坏判断。
独立复核结果:
```json
{"empty":{"conversationMemory":true,"localMemorySkills":true},"after":{"conversationMemory":false,"localMemorySkills":true}}
```
建议回归:
- 空配置加载后断言两项分别为 `true`、`false`。
- 只切换 `conversationMemory`,读取 IPC 返回值和 `config.json`,断言 `localMemorySkills` 仍为 `false`。
- 反方向只切换 `localMemorySkills`,断言 `conversationMemory` 不变。
- 在延迟 IPC 下断言首帧不会显示错误的本地记忆开关状态。
### 14-2 [P2 验收缺口] 编辑保存没有忙碌态、重复提交保护和成功反馈
`MemorySettingsPanel.tsx:368` 的 `saveEdit` 没有独立 busy 状态,保存按钮不改变文案也不禁用;双击可产生并发写入。TODO 也已记录原版“更新中/成功 toast”没有源码级等价。该问题不影响现有专项 24 项 PASS,但未覆盖真实慢写入和失败后重复点击。
### 14-3 [证据缺口] 原版重置弹窗、自动记忆和服务流转仍未验证
- 重置弹窗目前按原版共享 Modal 源码复原,未取得原版运行态逐像素证据。
- 账户侧自动生成、每夜整理、更新时间和服务端编辑流转未接入。按用户“云端服务先不管”的边界,这些不列为本轮 Bug,但当前页面仍展示“每晚自动整理更新”等表述,不能对外宣称该能力已实装。
- 打开/关闭动画、窄窗口、刷新失败和所有错误 toast 还没有逐帧验收。
## 15. 智能体页面:团队管理
### 15-1 [P3 实际问题] 已存禁用值在首帧仍按“开启”渲染
`AgentsSettingsPanel.tsx:151` 使用 `view?.teamManagement ?? true`。当持久化值为“禁用”且 IPC 返回较慢时,页面会先显示“开启”,返回后再切成“禁用”;测试在固定等待后读取,未观察首帧。该问题不会覆盖持久化结果,但属于可见状态闪烁,也不利于逐像素验收。
建议回归:
- 人为延迟 `agents:get`,断言加载前不显示错误的“开启”值,或使用已确认的原版加载骨架。
- 加载完成前禁用下拉,避免用户基于默认值误操作。
### 15-2 [测试盲区] 当前专项覆盖面有限
- `.tmp/agents-settings-YLCzhi/report.json` PASS,当前实际验证了布局、三枚图标、下拉持久化、重开恢复、禁用标注和深浅主题。
- 没有验证下拉的 Escape、外部点击关闭、键盘选择、加载态、重复快速更新和 IPC 失败后的最终值。
- `teamManagement` 只在设置页持久化,代码库中没有消费者;这符合“子代理调度先不管”的明确排除条件,不列 Bug。
- 经济模式只还原开关外观,浏览器扩展按钮只是禁用的本地入口。前者属于功能未接;后者涉及外跳/安装服务,按当前约束不列 Bug,但都不能计入完整 1:1 功能验收。
## 16. 移除助理与设备连接
### 16-1 [当前范围通过]
- `SettingsDialog.tsx:29` 的移除列表包含 `claw`、`deviceConnections`、`dataManagement`、`buddyApps`。
- `SettingsDialog.tsx:32` 依据该列表生成 12 项可见导航。
- `SettingsDialog.tsx:35` 对旧深链接统一回退到模型页。
- `SourceSettingsNavigation.tsx:17` 的搜索结果先按 `visibleTabs` 过滤,因此移除项不会通过搜索重新出现。
本轮 `settings-nav` 实际产品状态符合要求:助理、设备连接、数据管理、应用管理均不在 DOM、标签或搜索结果中;旧深链接 `claw`、`dataManagement` 正确回退。布局差异仅是移除条数后导航不再溢出,条目宽度从原版溢出态的 190px 变为当前内容宽 200px;现有 CSS 没有人为补滚动条槽位,符合已有证据。
### 16-2 [P2 验收脚本维护缺陷] `verify-settings-nav` 仍把 memory 当“暂未接入”
当前 `.tmp/settings-nav-grP9jC/report.json` 为 19 项检查、3 项失败。失败项全部来自旧期望:
- `scripts/verify-settings-nav.cjs:128` 构造 `expectedDisabled` 时仍包含 `memory`。
- 脚本期望启用项时仍排除 `memory`;实际产品已启用并渲染 `MemorySettingsPanel`。
- 实际导航为 9 个启用项:`settings`、`appearance`、`keyboardShortcuts`、`personalization`、`memory`、`extensions`、`models`、`helpFeedback`、`getHelp`;未接入项只剩 `account`、`subscriptionUsage`、`securityCenter`。
结论:这是脚本断言陈旧,不是 16/17 产品或 14 记忆页回归。更新专项时应把 memory 从 disabled 期望移到 enabled 期望。
## 17. 移除数据管理与应用管理
### 17-1 [当前范围通过]
16 与 17 共用同一份移除列表、深链接归一化、搜索过滤和真实窗口回归。没有发现数据管理或应用管理残留入口;没有删除本地会话、安装内容或其他用户数据。
### 17-2 [证据缺口]
`verify-settings-nav` 对旧深链接实际执行了 `claw` 和新窗口 `dataManagement`,对 `deviceConnections`、`buddyApps` 只通过同一个 `REMOVED_SETTINGS_TABS.includes()` 分支静态覆盖。实现本身没有分叉,风险低;若要把四个入口逐项纳入验收,可补两个启动参数场景。
## 19. 关于与获取帮助
### 19-1 [当前范围通过]
`.tmp/about-help-settings-WFvFHM/report.json` PASS。当前实现满足:
- 两页 638×620 内容区、顶部卡片、按钮、二维码占位和页脚主几何通过。
- 版本来自真实 `window.ycode.app.info().version`。
- 页面没有 `<a href>`,所有会跳向 WorkBuddy 的入口均被禁用并说明未配置。
- 深浅主题 token 已在关于页直接验证。
### 19-2 [非本轮 Bug,功能未接] “检查更新”只保留禁用外观
`AboutHelpSettingsPanel.tsx:69` 没有接更新通道,原版独立软件更新窗口也未复刻。用户本轮要求不复刻外跳链接,并明确排除外部服务;因此不列生产 Bug。若未来需要完整功能 1:1,需要单独接入 YCode 的更新源、版本检查、下载/重启状态和错误恢复。
### 19-3 [测试盲区]
- 当前专项仅直接测了“关于”深色;“获取帮助”深色由其共享样式推导,未单独断言。
- 专项读取了 `aria-disabled` 和标题,但没有真实点击并断言窗口 URL、窗口数量、设置页状态均不变。
- “检查更新”原版独立窗口只有“已是最新版”的 REA 观察,没有完整更新流程证据。
## 21. 对话框 Git 分支与会话上下文信息
### 21-1 [P2 文档验收缺陷] checklist 已勾选“完成”,但同一段明确写明关键状态未采集
`docs/复刻待办清单-2026-10-17.md:616` 勾选了“核对 detached HEAD、无首次提交、非仓库、检测失败”,同文件 `:632` 和 `:657` 又明确说原版这些状态未覆盖。`:619` 勾选了“浅色/深色及窄窗口布局”,`:655-656` 又说明五类占比、窗口控件和紧凑分支未实现。当前完成标记高于实际证据,不能作为 1:1 验收依据。
### 21-2 [实现限制/原版证据待补] detached HEAD 短 SHA 在数据层已算出,UI 层采用保守隐藏
- `src/main/git.ts:51` 在 detached HEAD 时执行 `rev-parse --short HEAD`,并在 `:52` 返回 `head`。
- `scripts/verify-git-branch.cjs:78` 只验证了这个后端返回值。
- `ComposerContextInfo.tsx:62` 只保留 `info.branch`。
- `ComposerContextInfo.tsx:63` 在 branch 为空时直接返回 `null`,完全没有使用 `info.head`。
结果:真实 Git 检测通过,但界面在 detached HEAD 下不展示短 SHA。源码注释和 TODO 已明确这是原版状态未采集时的保守策略,因此不能认定为生产业务 Bug。可以确定的问题是:`head` 回退目前未被 UI 使用,属于实现限制;要实现原版等价,仍需 REA 补 detached HEAD 的运行时表现。
### 21-3 [P2 功能缺口] 非 OMP 的 Chat Completions 回退未解析上游 `usage`
`src/main/gateway/index.ts:120` 到 `:159` 只解析文本、reasoning、工具调用和 finish reason,从未读取 SSE payload 中的 `usage`。因此 OpenAI 兼容 `/chat/completions` 即使返回:
```json
{"usage":{"prompt_tokens":128,"completion_tokens":2,"total_tokens":130}}
```
也不会产生 `GatewayDelta { type: "usage" }`,用量环保持“无法获取”。
独立纯测试实际输出:
```json
[{"type":"text","text":"ok"},{"type":"finish","reason":"stop"}]
```
现有 `.tmp/composer-context-Jbp2ol/failure.json` 在失败前 41/41 检查通过,但它走的是真实 OMP;OMP 自己从助手消息读取 usage,不能证明备用网关已解析。TODO `:658` 虽记录了 Chat Completions 缺口,但 checklist `:619` 仍被勾选,且当前专项没有网关回退用例。
### 21-4 [P2 准确性风险] 切换模型后未绑定 token 样本来源,可能沿用旧样本并换用新模型分母
- `src/renderer/src/stores/chat.ts:41` 的 `sessionUsage` 只按 sessionId 存储。
- `chat.ts:421` 收到 usage 后只写 `sessionUsage[sessionId]`。
- `chat.ts:164` 到 `:169` 切换现有会话模型时没有清除或标记旧用量。
- `ChatPane.tsx:208` 立即用新模型的 `maxInputTokens` 作为分母。
静态触发路径:模型 A(64K 窗口)跑一轮得到 12,800 tokens,切换到模型 B(128K 窗口),不发送新消息。按当前状态关联可推导界面会把 A 的 12,800 除以 B 的 128K,显示 10%,而不是保留 A 的 20%,也不是显示“新模型尚无真实用量”。
边界:本轮没有真实 UI 复现该切换时序,也不能仅凭现有 REA 证据断言原版在相同情况下采用何种状态。当前只能确定为 P2“用量样本未绑定模型/窗口”的准确性风险,需补真实 UI 用例和原版对照后才能判断是否存在行为差异。
### 21-5 [P2 功能/验收缺口] 重启后已有会话没有真实 usage 可恢复
`sessionUsage` 仅是 renderer 内存状态,初始值在 `chat.ts:126` 为空对象,代码库没有将它写入 `sessions.json` 或在打开会话时恢复。应用重启后,历史消息仍在,但下一次 usage 事件前无法展示上一轮真实用量。
现有验收没有重启应用或重新载入会话。若原版也要求新回合才能显示,此处属于证据缺口;若“已有会话”验收要求保留真实 context 信息,则这是实际功能缺口。
### 21-6 [P2 功能未接] 五类上下文占比固定为“—”
`ComposerContextInfo.tsx:172` 到 `:177` 渲染五个分类,但每项 value 都是固定字符串 `—`。TODO `:655` 明确承认分类统计无法获得。圆环总用量可以是真实的,但浮层的系统提示词、工具及子智能体、对话消息、MCP、技能拆分并没有实现,不能将整个浮层计为 1:1 数据完成。
### 21-7 [P2 功能未接] `__window-control` 只有 CSS,没有 JSX
`src/renderer/src/workbuddy/composer/context-info/source.css:330` 保留了 `.cr-context-usage-popover__window-control` 样式,但 `ComposerContextInfo.tsx:149` 的浮层内容没有该节点。原版内容也未采集,因此目前缺少一整块可能的上下文窗口控制/说明界面。
### 21-8 [P2 功能未接] 紧凑分支标签没有触发条件
`source.css:48` 定义了 `._branchLabelCompact_jdvlk_274` 的 32px 紧凑形态,`ComposerContextInfo.tsx:65` 始终只输出正式类名,没有根据宽度、容器空间或窗口状态添加 compact 类。窄窗口下不会进入原版紧凑形态;原版触发阈值仍是证据缺口。
### 21-9 [P2 布局风险/待验收] 浮层使用固定上方右对齐,未见 collision flip/shift
`source.css:144` 到 `:157` 把 280px 浮层固定为 `bottom: calc(100% + 8px); right: 0`。TODO `:654` 说明原版由 floating-ui 定位,YCode 没有逐像素核对。
风险:矮窗口、窄窗口、窗口边缘或输入区上方空间不足时,固定浮层存在越出窗口的可能。当前只通过静态 CSS 确认没有 flip/shift 处理,尚未真实复现裁切,因此不算已确认越界。当前专项只在 1336×902 的正常位置测量,没有覆盖边界翻转和横向平移。
### 21-10 [P2 测试盲区] 分支刷新时机和关键 Git 状态没有 UI 验收
`ComposerContextInfo.tsx:27`、`:47`、`:48` 使用 20 秒轮询和 window `focus` 刷新,原版刷新时机未采集。测试只验证初始分支,没有:
- 在外部/同一窗口终端执行 checkout 后,观察标签是否在预期时间内刷新;
- 切换两个不同 `cwd` 的会话,确认没有旧请求串线;
- 覆盖 detached HEAD、无提交仓库、非 Git 目录、git 缺失或超时的 UI 行为;
- 新建任务从无 `sessionId` 到首条消息创建会话后的标签出现时机。
后端 `.tmp/audit19-20261017-165014/verify-git-branch.log` 当前 6/6 PASS;它验证的是 `readGitBranch`,不能替代上面的 UI 验收。
### 21-11 [P2 验收脚本维护缺陷] `verify-composer-context-info` 等待已不存在的 `.segmented`
当前专题在 `.tmp/composer-context-Jbp2ol/failure.json` 中保存了失败前 41 项检查,41 项全部 PASS,随后在 `phase: dark-theme-via-native-settings` 超时:
```text
Timeout: appearance panel
scripts/verify-composer-context-info.cjs:330
```
原因:
- 脚本 `:330` 等待 `.segmented`。
- 脚本 `:331`、`:346` 仍点击 `.segmented__item`。
- 当前 `AppearanceSettingsPanel.tsx:47-53` 使用 `.appearance-card-grid` 和带 `aria-label="深色"/"浅色"` 的 `.appearance-card`。
- 其他新脚本 `verify-appearance-settings.cjs` 与 `verify-desktop-appearance.cjs` 已使用当前主题卡选择器。
结论:这是验收脚本维护缺陷,不是 21 的产品回归。失败前已真实覆盖分支标签、复制、32×32 用量环、真实 OMP usage、tooltip、浮层、五类占位和浅色状态;未完成当前构建的深色段。历史 `.tmp/composer-context-lEIWRF/report.json` 曾完成 47 项并 PASS,但不能代替本轮构建完成深色复跑。
### 21-12 [测试盲区] 当前专项未覆盖模型切换、重启和回退网关
现有通过路径是“单个 Git 仓库 + 单个会话 + 真实 OMP + Chat Completions 形态的受控上游”。它没有覆盖:
- OMP 不可用后的自建网关;
- Responses 协议回退;
- 切模型后 usage 与分母的配对;
- 应用重启后的历史 usage;
- 五类真实占比;
- 窗口控件;
- 紧凑分支;
- 矮/窄窗口浮层定位;
- 外部 branch change;
- detached/unborn/non-Git UI。
因此“真实 OMP 主路径 PASS”不能推导出 21 已全量 1:1。
## 本轮专项验收结果
逐项 checks 与 report 路径汇总:`.tmp/audit19-20261017-165014/summary.json`
运行顺序与退出码汇总:`.tmp/audit19-20261017-165014/results.json`
| 脚本 | 结果 | 说明 |
| --- | --- | --- |
| `verify-memory-settings` | PASS | `.tmp/memory-settings-poBX32`;没有覆盖 14-1 的默认值副作用。 |
| `verify-agents-settings` | PASS | `.tmp/agents-settings-YLCzhi`;当前范围主链路通过。 |
| `verify-settings-nav` | FAIL:3 个旧断言 | `.tmp/settings-nav-grP9jC/report.json`;实际移除入口与回退均通过,memory 旧期望错误。 |
| `verify-about-help-settings` | PASS | `.tmp/about-help-settings-WFvFHM`。 |
| `verify-git-branch` | PASS | 6/6;只测后端 Git 检测。 |
| `verify-composer-context-info` | 未完成当前构建深色段 | 前 41/41 checks PASS;旧 `.segmented` 选择器超时,不是业务回归。 |
全量运行结果为 19 个脚本中 17 通过、2 失败;两个失败均已定位为验收脚本维护缺陷,没有因此误报生产功能坏。
本轮可直接确认的检查数:
- `verify-memory-settings`:24/24 PASS,但没有覆盖“只切换对话记忆时不得修改本地记忆偏好”的事务副作用。
- `verify-agents-settings`:17/17 PASS。
- `verify-about-help-settings`:15/15 PASS。
- `verify-git-branch`:6/6 PASS,仅覆盖主进程 Git 检测。
- `verify-settings-nav`:16/19 PASS;其余 3 项是 memory 支持状态更新后的旧预期,16/17 的移除、搜索和深链接回退检查均通过。
- `verify-composer-context-info`:当前构建在失败前 41/41 PASS;深色主题段因旧选择器未执行,不能写成当前构建 47/47 完成。
分类汇总:
- **已确认且本轮发生**:14-1 偏好误写;Chat Completions 回退不解析 usage。
- **静态可证、尚需真实 UI 复现**:切模型后的 token/分母来源未绑定。
- **保守实现或原版证据待补**:detached HEAD、无提交、非 Git、检测失败的展示。
- **已知功能未接**:五类占比、窗口控件、紧凑分支。
- **仅静态布局风险**:浮层 collision flip/shift。
- **验收脚本维护缺陷**:memory 旧期望和 `.segmented` 旧选择器。
## 建议的后续验收用例
1. 记忆默认值事务:空配置、只更新 conversationMemory、只更新 localMemorySkills、慢 IPC 首帧。
2. 智能体加载态:已存 `teamManagement=false` 时延迟 get,首帧不得显示“开启”;下拉补 Escape、外部点击、键盘和失败恢复。
3. 导航脚本更新:把 memory 加入 enabled 期望,从 disabled 与“暂未接入”计数中移除。
4. 关于/帮助:补获取帮助深色页;真实点击所有禁用动作,断言无新窗口、URL 不变、无 IPC 调用。
5. Chat Completions usage:强制 OMP 不可用,由备用网关返回带 usage 的 SSE,断言渲染环更新。
6. Model switch:模型 A 产生真实 usage 后切到不同窗口大小的模型 B,断言旧样本失效或明确绑定原始模型。
7. Existing session restart:保存真实 usage 后重启,断言原版要求的显示状态;若不能恢复,必须显示“暂无本模型用量”。
8. Git UI states:detached、unborn、non-Git、git unavailable 分别截图并断言文案/显示。
9. Git refresh:运行中外部 checkout,记录标签更新延迟和旧会话隔离。
10. Responsive context UI:至少 1336×902、900×680、窄窗口和矮窗口,覆盖 compact 分支、浮层 flip/shift、tooltip 边界。
11. 上下文浮层:补 `window-control` 与五类真实占比的原版证据和实现,不能继续用固定 `—` 计完成。
12. 更新专项选择器:将 `.segmented` / `.segmented__item` 改为 `.appearance-card[aria-label="深色"]` / `.appearance-card[aria-label="浅色"]`,再复跑当前构建 47 项。
## 验证与限制
- 本轮未启动 Electron;全部 UI 专项由主 agent 串行执行。
- 已独立运行 `node scripts/verify-git-branch.cjs`,6/6 PASS。
- 已独立构建并执行 `src/shared/memory.ts` 的纯函数审计,复现 14-1。
- 已独立构建 `src/main/gateway/index.ts` 并用带 usage 的 Chat Completions SSE 调用,确认 usage 未进入输出事件。
- 未重新观察 WorkBuddy。当前端口 9223 `endpoint_unreachable` 只代表该已知入口不可达,不代表 REA 全部不可用;需要的新证据已在第 21 段逐项列出。
- 工作树原有 dirty 修改全部保留;本轮只新增本文档。
+1 -1
View File
@@ -49,7 +49,7 @@ export default defineConfig({
} }
}, },
build: { build: {
rollupOptions: { input: resolve(root, 'src/renderer/index.html') } rollupOptions: { input: { index: resolve(root, 'src/renderer/index.html'), settings: resolve(root, 'src/renderer/settings.html') } }
} }
} }
}) })
+387
View File
@@ -0,0 +1,387 @@
# YCode sandbox, protocol 1
This directory contains YCode's Windows x64 execution backend. The implementation
uses public Windows APIs and the Microsoft SDK. WorkBuddy binaries, services,
profiles, hooks and IPC pipes are not runtime or build dependencies.
## Build and verify
Prerequisites: Windows x64, Node/npm, Visual Studio C++ Build Tools, and a Windows
10/11 SDK. The build discovers the installed compiler with `vswhere`; it does not
install or modify the toolchain.
```powershell
npm run sandbox:build
npm run test:sandbox
npm run sandbox:maintain -- status
npm run sandbox:maintain -- self-test
npm run test:sandbox:app
npm run test:sandbox:policy
npm run test:sandbox:toolchains
npm run test:sandbox:directories
npm run test:sandbox:write
npm run test:sandbox:workspace
npm run test:sandbox:feasibility
npm run test:sandbox:stdio
```
`sandbox:build` compiles `runner.cpp` with MSVC C++17, `/W4 /WX /MT /Brepro`.
Generated artifacts are `resources/sandbox/win32-x64/ycode-sandbox.exe` and
`manifest.json`. The manifest records the source/binary SHA-256, compiler, SDK,
architecture and protocol. Generated binaries are ignored by Git. `npm run build`
builds the runner before checking TypeScript and building Electron. Packaging
copies the runner, pinned adapters/manifests and bundled Git; test-only probe
executables are excluded.
`test:sandbox` executes real native processes in test-owned temporary directories.
`test:sandbox:app` builds and runs the actual production main/preload/renderer,
real OMP RPC, and a deterministic localhost model. It uses separate home/userData,
hides its own windows, and exercises approval, rejection and cancellation.
## Ownership and execution
- `runner.cpp`: AppContainer profile, SID/token verification, explicit stdio
handle inheritance, restricted child environment, Job, timeout/parent monitor,
temporary drive, lease and native result protocol.
- `src/main/sandbox/policy.ts`: workspace containment, sensitive names,
link/device/alternate stream checks.
- `src/main/sandbox/service.ts`: executable identity, bounded workspace snapshot,
native process lifecycle, diff, approval and audit.
- `workspace-state.h` / `workspace.ts` / `workspace-service.ts`: host-only ACL
sealing, persistent session drafts, immutable checkpoints, synchronization,
approval-based commit and explicit recovery.
- `src/main/sandbox/transactions.ts`: independent before/after backups, batch
conflict checks, compensation and explicit recovery.
- `src/main/sandbox/maintenance.ts`: lease validation, orphan cleanup and record
inventory.
- `policy-store.ts` / `authorizations.ts`: versioned global/workspace policy,
compare-and-swap updates, single-use approval and policy revocation.
- `toolchains.ts` / `dependency-cache.ts`: explicit content-verified runtime imports
and immutable workspace/lock/runtime-bound dependency layers.
- `execution.ts`: bounded admission and sequential execution per workspace.
- `src/main/agent/conversation-tools.ts`: common OMP and fallback execution path.
Default `execute_command` creates a fresh snapshot and unique
`YCode.Run.<32 hexadecimal characters>` AppContainer profile. No network
capabilities are granted. The runner verifies the suspended child's AppContainer
flag and exact SID, assigns the Job, then resumes it. Only stdio handles are
inherited. The Job forbids breakaway, limits active processes to 32 and aggregate
committed memory to 1 GiB, and kills remaining descendants when the root finishes.
The native runner also watches its launch parent and accepts `cancel\n` on stdin.
A free per-user DOS drive maps to this generated stage. This makes drive-root
queries by cmd and PowerShell operate on the scratch root. The allocator has a
named mutex and uses exact-target removal. The mapping and profile are removed
before a successful native result. The original workspace ACL is not changed.
The child can modify its scratch copy. Only a successful command can propose host
file changes. A real conversation approval precedes writeback. All targets are
checked against the initial hashes and rechecked while committing. Independent
backups are persisted before the first host write. Rejected, failed, cancelled,
conflicting or invalid changes are not committed. A failed compensation is
reported with its transaction ID and `recovery-required` state. Audit failure
after a successful commit preserves the actual committed outcome.
## State and maintenance
Production state is under `app.getPath('userData')/sandbox`. The actual app API
`window.ycode.app.sandboxStatus()` reports installed/unavailable backend identity;
`installed` means the binary manifest is valid. A native self-test or execution
is required to verify that the OS can start the isolation boundary.
Each `runs/<uuid>/` contains request metadata, native `lease.json`/`result.json`,
`audit.json` or `failure.json`, and `cleanup.json`. Raw stdout/stderr and environment
credentials are excluded from the audit. Scratch stages are removed after runs;
cleanup status records a failure if the OS still holds a file. The `transactions/`
directory retains the manifest and independent `.before`/`.after` contents.
Stateful maintenance requires an explicit absolute state directory. It does not
guess or switch to another application's storage.
```powershell
npm run sandbox:maintain -- audit --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- transactions --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- restore TRANSACTION_UUID --workspace 'C:\absolute\workspace' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- cleanup --state-root 'C:\absolute\userData\sandbox'
```
Recovery checks the expected workspace, backup hashes, and current file hashes.
It refuses to overwrite a later external edit. Interrupted prepared/applying
transactions remain inspectable and recoverable. Restoring an already restored
transaction is harmless. `cleanup` checks a lease's run identity and PID, skips
live runners, and removes an orphaned profile/drive only for the recorded exact
target. It preserves transaction backups and audit history.
## Policies, runtimes and dependencies
Policy defaults preserve the original snapshot boundary. Maintenance can tighten
entry/file/byte limits, disable commands, deny writeback and add relative denied
paths. Workspace overrides cannot relax global restrictions. Updates require the
current revision and an exclusive policy lock. Corrupt/unknown policies fail
closed. Policy changes cancel active commands and pending conversation approvals;
an approved ticket is scoped to its exact change batch and can be consumed once.
Bindings include a content fingerprint as well as the revision, so manual edits
that keep a revision do not preserve old permissions or self-test observations.
```powershell
npm run sandbox:maintain -- policy --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- policy-set --expected-revision REVISION --rules 'C:\absolute\rules.json' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- toolchain-add node --source 'C:\absolute\node.exe' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- toolchain-add python --source 'C:\absolute\python.exe' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- toolchain-add git --source 'C:\Program Files\Git\cmd\git.exe' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- git-state --workspace 'C:\absolute\project' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- git-reset --workspace 'C:\absolute\project' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- dependency-add node --workspace 'C:\absolute\project' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- dependency-add python --workspace 'C:\absolute\project' --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- operation RUN_UUID --state-root 'C:\absolute\userData\sandbox'
```
Import selects explicit local runtime bytes; it never executes the source
interpreter on the host. Node includes the installed sibling npm distribution
when present. Python includes its version DLL, standard library, extension DLLs
and installed pip when present. Sensitive configuration and test/cache files are
filtered; no global PATH or credentials are inherited. Imported runtimes have
independent file-count/size limits, manifests and pinned hashes. Registration is
metadata availability; every requested file is verified before actual execution.
Model commands select `runtimes: ['node']`, `['python']` or `['git']`. A dependency import
reads only that workspace's `node_modules` or `.venv/Lib/site-packages`, rejects
links and hard-linked files, and binds the layer to workspace, project/lockfile
hashes and runtime identity. Commands opt in with `dependencies: ['node']` or
`['python']`. Cached dependencies are copied to the private stage and protected by
native read/execute ACLs. Node keeps the actual node_modules layout for CommonJS
and ESM. Python uses a private PYTHONPATH. Dependency contents are never promoted
as ordinary generated changes; cache updates require explicit import. A changed
lockfile/runtime or a corrupted cache rejects execution. Original dependency
trees may change after import; the pinned layer changes only on explicit reimport.
The following paragraph records the original 0.4.0 Git-only snapshot scope;
host-Git import/writeback and transport were added in component 0.7.0 below.
Git is bundled by default; users do not install or register it. Maintainers run
`npm run sandbox:vendor-git` once when preparing a source checkout. The pinned
official MinGit archive and deterministic runtime manifest are checked before
an offline normal build; licenses and source-provenance notices ship with it.
An optional explicit import overrides the bundle, without silent fallback if
that override is damaged. The importer reads the real MinGW/UCRT x64 image and
its PE DLL dependency closure without executing source programs on the host.
In the original 0.4.0 scope, Git used a persistent private repository
whose initial commit contains the filtered workspace snapshot. Host `.git`,
history, index, credentials and hooks are never imported or written back. Thus
private status/diff/log do not describe the original host repository. Repository
changes require approval, bind the exact candidate and policy, and reject
approval-time workspace edits. Read-only status/diff/log do not save stat caches.
Git metadata uses its own versioned state outside the worktree; normal file
writeback retains its existing independent transaction and recovery record.
`git.persistence` and durable operation queries report private publication
separately: the two steps are not one crash-atomic transaction. All protocols are
disabled by default and the native Job retains zero network capabilities.
This historical snapshot path remains available as a separate mode. See
`docs/沙盒Git接入-2026-10-18.md` for the original subset.
Bundling, copied/packaged-resource and offline reproducibility checks use
`npm run test:sandbox:bundled-git`; details are recorded in
`docs/沙盒内置Git分发-2026-10-18.md`.
### Git host and remote integration (component 0.7.0)
The complete common Git path is documented in `docs/沙盒Git完整接入-2026-10-18.md`
and its acceptance matrix. `sandbox_git` can explicitly import a real host
repository (including an existing linked worktree), run the supported local Git
workflow in the AppContainer draft, and publish project files plus safe Git
metadata through coordinated native transactions. `sandbox_workspace` routes
commit/discard/recover through the same host-Git binding when one exists.
Import is approval-bound and preserves real objects, HEAD, index, refs, tags,
stash state and allowed configuration without executing host hooks, aliases,
filters, credential helpers or external object paths. New repositories remain
unborn until a real first commit. Clone/fetch/pull/push use the trusted host
transport control plane with fixed Git, explicit HTTPS/SSH credentials, TLS and
known-host verification, per-target approvals and durable unknown outcomes.
The ordinary command Job still has no network capability. `git-cleanup` removes
only abandoned control roots after checking both the host owner record and native
lease. This is a Windows x64 engineering acceptance, not a claim of complete
Git subcommand compatibility, crash-atomic multi-root publication, or WorkBuddy
1:1 equivalence.
### Persistent session drafts
Component 0.6.0 adds `workspaceState: 1`. Commands explicitly select
`workspace: 'persistent'` to retain a project/session-bound draft. A successful
command saves private files without writing the host. `sandbox_workspace`
supports `status`, `changes`, `commit`, `discard` and `recover`, with an optional
`expectedRevision`. Commit uses the existing conversation approval and native v3
host transaction. Explicit `snapshot` or `persistent` selection always confines
the command, even in a full-access tool context. Omitting the selector retains
the existing permission behavior.
Normal successful commands retain the same physical NTFS worktree identity by
moving it into and out of the run stage. The host-only `--seal-workspace` entry
uses relative native directory handles to remove former AppContainer access
before retention. Private hash-addressed blobs and revision manifests remain
host-only, including for later Jobs from the same session. Manifests are capped
at 8 MiB before publication. Root identity and policy changes make old drafts
stale. Three-way synchronization imports unrelated host edits and rejects file
or ancestor conflicts; file/directory type swaps are currently rejected.
Failed, cancelled and timed-out commands restore the saved checkpoint. Explicit
recovery checks the saved run owner, dead host lock and native lease. Commit
intents distinguish an interrupted private update from actual host writes:
metadata failure after host commit still reports `committed`. The read-only
status path returns `busy` during execution or approval instead of queueing.
Persistent Git uses its own project/session namespace. Local private Git commits
need no host approval and do not modify host `.git`. File checkpoint and private
Git publication are separate steps; publication failure is reported separately.
Discard archives both the draft and its active private Git reference. Scoped
stale Git locks are checked during explicit draft recovery. The existing
project-wide snapshot Git behavior is unchanged.
```powershell
npm run sandbox:maintain -- workspace-changes --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
npm run sandbox:maintain -- workspace-commit --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox' --draft-revision REVISION_UUID --approve
npm run sandbox:maintain -- workspace-recover --workspace 'C:\absolute\project' --session SESSION_ID --state-root 'C:\absolute\userData\sandbox'
```
The CLI also accepts `workspace-status` and `workspace-discard`. Preload exposes
read-only `app.sandboxWorkspace(sessionId)`; main resolves the root from the saved
session and requires the trusted top-level app page. Ordinary file tools and the
read bridge still access the host project; use persistent commands for draft
file access. Processes, home, temp, tools and absolute execution paths are not
persistent. Mutable dependencies and online installs remain pending; imported
read-only dependencies detach before checkpointing. See
`docs/沙盒长期工作区接入-2026-10-18.md` for verification and recovery limits.
Windows derives an additional private AppContainer TEMP directory from
LOCALAPPDATA. The runner provisions that profile-specific path before launch;
Node/Python temporary files remain inside the private stage and are cleaned with it.
Python 3.13+ gives mkdir(0o700) a user/admin-only DACL, excluding the package SID.
The pinned, read-only Python site adapter uses inherited native private ACLs for
that mode only within the owned stage. Outside paths retain Python's behavior and
OS denial. Isolated/no-site Python flags can bypass this compatibility adapter.
For synchronous Node subprocess capture, explicitly preload the owned adapter:
```text
node --require "%YCODE_SANDBOX_PROCESS%" build.cjs
```
The manifest-pinned `node-process.cjs` replaces public spawnSync/execFileSync/
execSync (including ESM named exports) with a file-descriptor capture adapter.
Subprocesses still use Node's own spawn and inherit the same token/Job. Inputs
and maxBuffer are capped at 16 MiB; maxBuffer defaults to 1 MiB. Buffer overflow
is detected after completion, not an OS hard disk quota or a claim of early
termination. Scratch handles/files are closed/removed after each call. Explicit
non-pipe stdio configurations pass through. Default Node synchronous pipes still
hang on the tested AppContainer without the adapter; async spawn/fork/IPC and
comprehensive API compatibility remain separate work. Run
`npm run test:sandbox:process` for the bounded Node/Python regression.
Native operation outcomes are saved independently of verbose audit and chat
messages. The operation query returns an unknown/incomplete outcome when no
durable host result exists; native process completion does not imply host commit.
Self-tests persist their timestamp and exact binary/policy identity. A new binary
or policy invalidates old observations. The current self-test verifies command
startup/isolation and cleanup, not every network/filesystem/registry capability.
The internal `stdioProbe` mode remains experimental and accepts bounded echo
requests only. It uses the already bound suspended/verified child and its Job's
inherited pipes, rejects caller-supplied identities, repeated IDs and excessive
frames/concurrency, and forwards replies as bounded base64 control records.
Model-facing tool arguments cannot enable it. It grants no host file/network
operation and is not individual descendant PID authentication. Normal commands
retain NUL stdin. Named-pipe connectivity remains unverified on this host; run
`npm run test:sandbox:feasibility -- --probe-pipe` to record the experimental
observation, or add `--require-pipe` to require actual success. An unavailable
transport is reported as unavailable, not as a completed broker capability.
### Native read-only workspace bridge
Commands may explicitly select `bridge: 'workspace-read'` together with
`runtimes: ['node']`. The manifest-pinned client is available at
`process.env.YCODE_SANDBOX_BRIDGE`; `require(...).connectSync()` supplies
`readFileSync`, `statSync`, `readdirSync`, `querySync` and `close`. `connect()`
supplies asynchronous equivalents and an eight-request dispatch queue. Close the
client after use. One Job should have one active stdin consumer. This is an
explicit JSON bridge, not a replacement for every Node fs or child_process API.
`filesystem-read.h` runs only on the host, requires a fixed local NTFS volume,
pins all ancestors/targets with read sharing only, rejects reparse/multiple-link
files, and compares root file identity and final normalized volume paths. The
host checks policy before and after reads and again for every cached chunk.
Reads are limited to 8 MiB or the tighter file policy; immutable snapshots are
served in 16 KiB chunks, with at most eight snapshots/16 MiB per Job, a 60-second
idle lifetime and a full client SHA-256 check. Directory listing is bounded to
128 raw entries. Native readers monitor their parent and have a four-second
deadline. No writable handle is returned to sandbox code.
The strict protocol caps requests at 8 KiB, replies at 24 KiB, eight concurrent
operations and 1024 total requests per Job. Replay, forged ownership, oversized
or incomplete frames cancel the channel/Job. Ordinary output is streamed. Final
operation metadata is flushed before replying; contents/tokens are not persisted.
Completed results remain queryable after a broken reply. Interrupted pending
records are unknown, never inferred successful:
```powershell
npm run test:sandbox:read-bridge
npm run test:sandbox:read-protocol
npm run sandbox:maintain -- broker-operation RUN_UUID REQUEST_UUID --state-root 'C:\absolute\userData\sandbox'
```
Preload also exposes `app.sandboxBrokerOperation(runId, requestId)` to the owned
top-level application page. Ownership and record IDs are checked on the host.
The bridge grants a read scope to the whole bound Job, not to individually
authenticated descendant PIDs. Full host write/file-descriptor shims, Python and
Shell adapters remain pending. See the implementation and test record in
`docs/沙盒只读文件桥接入-2026-10-18.md`.
To maintain a policy, edit the relevant module, add an adversarial case to
`scripts/sandbox-regression.ts`, run the native suite and application integration,
then rebuild. Keep protocol changes synchronized between runner, manifest,
service and tests. Do not enable ordinary host spawn as a default-mode fallback.
## Current scope and limits
- Windows x64 is implemented. Other platforms reject default-mode commands.
- The model command path is isolated. The OMP sidecar, manually operated terminal,
local MCP server processes and remote connectors retain their existing trust
and approval models; their process confinement is separate work.
- Input excludes credentials/protected names, `.git`, `.ycode`, `.tmp`,
`node_modules`, `.venv`, `venv`, `__pycache__` and symbolic links. Multi-link files are rejected. Limits are
10,000 entries, 128 MiB of copied/generated file data, 8 MiB per file, 120 seconds
by default, and 2 MiB retained per stdout/stderr stream.
- File-count/size limits are snapshot and writeback checks, not an OS disk quota.
Hard disk quotas and automatic audit/backup retention are pending work.
- Cmd and Windows PowerShell work in the mapped stage. Explicitly imported
Node/npm and Python/pip run offline with controlled optional dependency layers;
Git supports the private filtered repository described above.
Runtimes and package managers are not automatically copied or granted access.
Commands must use relative scratch paths; actual original workspace absolute
paths are not redirected. Persistent filtered drafts are opt-in as described
above; persistent processes/home/temp, mutable dependency layers, host Git
history/index integration and metadata writeback, remote Git, network installs,
pnpm link layouts and comprehensive native-addon compatibility remain pending.
- File creation/update/deletion, empty directories and directory tree deletion or
rename are supported through versioned transactions. Deletion preflight refuses
unknown/excluded host contents; recovery refuses later external additions.
Directory metadata, ACLs, timestamps, arbitrary reparse behavior and binary
compatibility with every program remain outside this first acceptance set.
- AppContainer can read shared Windows resources and other objects permitted by
Windows ACLs. It does not implement a universal read-path allowlist or DLP.
A workspace credential filter is an additional application policy.
- Registry/domain policies, configurable network egress, connector isolation,
security-center UI and WorkBuddy equivalence remain separate acceptance tasks.
- Read-bridge operations pin native handles. Host commit/compensation/recovery
now use the host-only NTFS transaction channel in `filesystem-write.h`, with
root identity, relative directory-handle operations and a v3 intent journal.
Replacement uses a no-overwrite two-rename handoff, with a brief absent-name
window rather than one atomic swap. Changed/unknown scratch content refuses
automatic cleanup. Full shims/metadata and power-loss acceptance remain pending;
see `docs/沙盒原生写回事务-2026-10-18.md`.
- The recovery journal is intended for process interruption. Power-loss durability
and independent security review require additional validation.
## Primary API references
- [AppContainer isolation](https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation)
- [CreateAppContainerProfile](https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-createappcontainerprofile)
- [Process attributes, including security capabilities and handle lists](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute)
- [Job objects and process trees](https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects)
+174
View File
@@ -0,0 +1,174 @@
#pragma once
#include <memory>
#include <sstream>
#include <iomanip>
#include <cwctype>
// This host-only reader never grants a workspace handle to the sandbox.
namespace workspace_read {
struct Lifetime {
Handle parent{parentProcess()}, done{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
std::thread monitor;
explicit Lifetime(DWORD timeout = 4000) {
require(done.value != nullptr, "Create reader lifecycle event");
monitor = std::thread([this, timeout]() {
HANDLE events[] = {parent.value, done.value};
const DWORD state = WaitForMultipleObjects(2, events, FALSE, timeout);
if (state != WAIT_OBJECT_0 + 1) TerminateProcess(GetCurrentProcess(), 125);
});
}
~Lifetime() { SetEvent(done.value); monitor.join(); }
};
static bool component(const std::wstring& name) {
if (name.empty() || name == L"." || name == L".." || name.back() == L'.' || name.back() == L' ') return false;
for (wchar_t ch : name) if (ch < 32 || std::wstring(L"<>:\"/\\|?*").find(ch) != std::wstring::npos) return false;
auto stem = name.substr(0, name.find(L'.'));
std::transform(stem.begin(), stem.end(), stem.begin(), [](wchar_t ch) { return static_cast<wchar_t>(towupper(ch)); });
if (stem == L"CON" || stem == L"PRN" || stem == L"AUX" || stem == L"NUL") return false;
return !(stem.size() == 4 && (stem.substr(0, 3) == L"COM" || stem.substr(0, 3) == L"LPT") && stem[3] >= L'1' && stem[3] <= L'9');
}
static std::vector<std::wstring> parts(std::wstring value) {
std::replace(value.begin(), value.end(), L'/', L'\\');
std::vector<std::wstring> result;
size_t start = 0;
for (;;) {
const auto end = value.find(L'\\', start);
const auto name = value.substr(start, end == std::wstring::npos ? end : end - start);
if (!component(name)) throw std::runtime_error("Invalid workspace path component");
result.push_back(name);
if (end == std::wstring::npos) return result;
start = end + 1;
}
}
static std::wstring finalPath(HANDLE handle) {
std::vector<wchar_t> buffer(32768);
const DWORD count = GetFinalPathNameByHandleW(handle, buffer.data(), static_cast<DWORD>(buffer.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_GUID);
require(count > 0 && count < buffer.size(), "Resolve workspace handle");
std::wstring path(buffer.data(), count);
while (path.size() > 1 && path.back() == L'\\') path.pop_back();
return path;
}
static BY_HANDLE_FILE_INFORMATION information(HANDLE handle) {
BY_HANDLE_FILE_INFORMATION result = {};
require(GetFileInformationByHandle(handle, &result) != FALSE, "Inspect workspace handle");
if (result.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE)) throw std::runtime_error("Workspace reparse point or device denied");
if (!(result.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && result.nNumberOfLinks != 1) throw std::runtime_error("Workspace hard link denied");
return result;
}
static std::string identity(HANDLE handle) {
FILE_ID_INFO id = {};
require(GetFileInformationByHandleEx(handle, FileIdInfo, &id, sizeof(id)) != FALSE, "Identify workspace handle");
std::ostringstream text;
text << std::hex << std::setfill('0') << std::setw(16) << id.VolumeSerialNumber << ':';
for (BYTE byte : id.FileId.Identifier) text << std::setw(2) << static_cast<unsigned int>(byte);
return text.str();
}
struct Target {
std::vector<std::unique_ptr<Handle>> held;
fs::path path;
std::wstring rootFinal;
std::string rootIdentity;
HANDLE open(bool directory) {
// Read sharing only pins names against rename/delete and excludes writable handles,
// including handles used to retarget a reparse point. All ancestors stay open.
auto handle = std::make_unique<Handle>(CreateFileW(path.c_str(), directory ? FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES : GENERIC_READ,
FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
require(handle->value != INVALID_HANDLE_VALUE, "Open pinned workspace target");
const auto info = information(handle->value);
if (directory && !(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) throw std::runtime_error("Workspace ancestor is not a directory");
const auto result = handle->value;
held.push_back(std::move(handle));
return result;
}
explicit Target(const std::wstring& root) {
if (root.size() < 3 || root.size() > 1024 || !iswalpha(root[0]) || root[1] != L':' || root[2] != L'\\') throw std::runtime_error("Workspace requires a local drive path");
path = root.substr(0, 3);
if (GetDriveTypeW(path.c_str()) != DRIVE_FIXED) throw std::runtime_error("Workspace requires a fixed local volume");
HANDLE last = open(true);
wchar_t filesystem[32] = {};
require(GetVolumeInformationByHandleW(last, nullptr, 0, nullptr, nullptr, nullptr, filesystem, 32) != FALSE, "Inspect workspace volume");
if (_wcsicmp(filesystem, L"NTFS") != 0) throw std::runtime_error("Workspace reader currently requires NTFS");
if (root.size() > 3) for (const auto& name : parts(root.substr(3))) { path /= name; last = open(true); }
rootFinal = finalPath(last);
rootIdentity = identity(last);
}
HANDLE resolve(const std::wstring& relative) {
if (relative == L".") return held.back()->value;
const auto names = parts(relative);
HANDLE last = nullptr;
for (size_t index = 0; index < names.size(); ++index) { path /= names[index]; last = open(index + 1 < names.size()); }
return last;
}
std::wstring relative(HANDLE handle) const {
const auto actual = finalPath(handle);
if (_wcsicmp(actual.c_str(), rootFinal.c_str()) == 0) return L".";
if (actual.size() <= rootFinal.size() || _wcsnicmp(actual.c_str(), rootFinal.c_str(), rootFinal.size()) != 0 || actual[rootFinal.size()] != L'\\') throw std::runtime_error("Workspace handle escaped root");
return actual.substr(rootFinal.size() + 1);
}
};
static std::string base64(const std::vector<BYTE>& bytes) {
static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result; result.reserve((bytes.size() + 2) / 3 * 4);
for (size_t index = 0; index < bytes.size(); index += 3) {
const unsigned int value = (static_cast<unsigned int>(bytes[index]) << 16) |
(index + 1 < bytes.size() ? static_cast<unsigned int>(bytes[index + 1]) << 8 : 0) | (index + 2 < bytes.size() ? bytes[index + 2] : 0);
result += alphabet[value >> 18]; result += alphabet[(value >> 12) & 63];
result += index + 1 < bytes.size() ? alphabet[(value >> 6) & 63] : '=';
result += index + 2 < bytes.size() ? alphabet[value & 63] : '=';
}
return result;
}
static int run(const std::wstring& operation, const std::wstring& root, const std::wstring& relative, const std::string& expected, DWORD limit) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace reader is host-only");
Lifetime lifetime;
if (operation != L"identity" && operation != L"read" && operation != L"stat" && operation != L"list") throw std::runtime_error("Invalid workspace reader operation");
if (limit == 0 || limit > 8 * 1024 * 1024) throw std::runtime_error("Invalid workspace reader limit");
Target target(root);
if (operation != L"identity" && target.rootIdentity != expected) throw std::runtime_error("Workspace root identity changed");
const HANDLE handle = target.resolve(relative);
const auto before = information(handle);
const bool directory = (before.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
const auto size = (static_cast<ULONGLONG>(before.nFileSizeHigh) << 32) | before.nFileSizeLow;
std::string result = "{\"protocol\":1,\"rootIdentity\":\"" + target.rootIdentity + "\",\"identity\":\"" + identity(handle) +
"\",\"path\":\"" + jsonText(target.relative(handle)) + "\",\"kind\":\"" + (directory ? "directory" : "file") + "\",\"size\":" + std::to_string(size);
if (operation == L"read") {
if (directory) throw std::runtime_error("Workspace target is a directory");
if (size > limit) throw std::runtime_error("Workspace file exceeds read limit");
std::vector<BYTE> bytes(static_cast<size_t>(size)); DWORD read = 0;
if (!bytes.empty()) require(ReadFile(handle, bytes.data(), static_cast<DWORD>(bytes.size()), &read, nullptr) && read == bytes.size(), "Read pinned workspace file");
const auto after = information(handle);
if (after.nNumberOfLinks != 1 || after.nFileSizeLow != before.nFileSizeLow || after.nFileSizeHigh != before.nFileSizeHigh || CompareFileTime(&before.ftLastWriteTime, &after.ftLastWriteTime) != 0) throw std::runtime_error("Workspace file changed while reading");
result += ",\"data\":\"" + base64(bytes) + "\"";
} else if (operation == L"list") {
if (!directory) throw std::runtime_error("Workspace target is not a directory");
result += ",\"entries\":[";
std::vector<BYTE> buffer(65536); bool first = true; DWORD count = 0;
for (;;) {
if (!GetFileInformationByHandleEx(handle, FileIdBothDirectoryInfo, buffer.data(), static_cast<DWORD>(buffer.size()))) {
if (GetLastError() == ERROR_NO_MORE_FILES) break;
require(false, "Enumerate pinned workspace directory");
}
size_t offset = 0;
for (;;) {
const auto entry = reinterpret_cast<FILE_ID_BOTH_DIR_INFO*>(buffer.data() + offset);
if (entry->FileNameLength > buffer.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName)) throw std::runtime_error("Invalid directory record");
const std::wstring name(entry->FileName, entry->FileNameLength / sizeof(wchar_t));
if (name != L"." && name != L"..") {
if (++count > limit) throw std::runtime_error("Workspace directory exceeds entry limit");
if (!first) result += ',';
first = false;
result += "{\"name\":\"" + jsonText(name) + "\",\"kind\":\"" + ((entry->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ? "link" : (entry->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? "directory" : "file") + "\"}";
}
if (entry->NextEntryOffset == 0) break;
if (entry->NextEntryOffset < sizeof(FILE_ID_BOTH_DIR_INFO) || entry->NextEntryOffset > buffer.size() - offset - sizeof(FILE_ID_BOTH_DIR_INFO)) throw std::runtime_error("Invalid next directory record");
offset += entry->NextEntryOffset;
}
}
result += ']';
}
result += "}\n";
DWORD written = 0;
require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), result.data(), static_cast<DWORD>(result.size()), &written, nullptr) && written == result.size(), "Write workspace reader result");
return 0;
}
}
+332
View File
@@ -0,0 +1,332 @@
#pragma once
#include <bcrypt.h>
#include <map>
#include <winternl.h>
// Host-owned transaction channel. No workspace handles cross into an AppContainer.
namespace workspace_write {
static size_t fileLimit = 8 * 1024 * 1024;
using CreateFileFn = NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, ULONG, ULONG, ULONG, PVOID, ULONG);
using SetFileFn = NTSTATUS(NTAPI*)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS);
using ErrorFn = ULONG(NTAPI*)(NTSTATUS);
template<class Function> static Function nativeFunction(const char* name) {
const auto address = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), name);
require(address != nullptr, "Resolve native transaction API");
Function result = nullptr;
static_assert(sizeof(result) == sizeof(address)); memcpy(&result, &address, sizeof(result));
return result;
}
static bool nativeOk(NTSTATUS status) {
if (status >= 0) return true;
SetLastError(nativeFunction<ErrorFn>("RtlNtStatusToDosError")(status));
return false;
}
static HANDLE openRelative(HANDLE parent, const std::wstring& name, ACCESS_MASK access, bool directory, bool create = false) {
UNICODE_STRING text = {}; text.Buffer = const_cast<PWSTR>(name.data());
text.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t)); text.MaximumLength = text.Length;
OBJECT_ATTRIBUTES attributes = {}; attributes.Length = sizeof(attributes); attributes.RootDirectory = parent;
attributes.ObjectName = &text; attributes.Attributes = 0x1040; // OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE
IO_STATUS_BLOCK status = {}; HANDLE handle = INVALID_HANDLE_VALUE;
const ULONG options = 0x00200000 | 0x20 | (directory ? 1 : 0); // OPEN_REPARSE_POINT, synchronous, optional directory
const auto result = nativeFunction<CreateFileFn>("NtCreateFile")(&handle, access | SYNCHRONIZE, &attributes, &status, nullptr,
FILE_ATTRIBUTE_NORMAL, directory ? FILE_SHARE_READ | FILE_SHARE_WRITE : FILE_SHARE_READ, create ? 2 : 1, options, nullptr, 0);
return nativeOk(result) ? handle : INVALID_HANDLE_VALUE;
}
struct Root {
std::vector<std::unique_ptr<Handle>> held;
fs::path path;
HANDLE handle = nullptr;
std::wstring rootFinal;
std::string rootIdentity;
explicit Root(const std::wstring& input) {
if (input.size() < 3 || input.size() > 1024 || !iswalpha(input[0]) || input[1] != L':' || input[2] != L'\\') throw std::runtime_error("Workspace requires a local drive path");
path = input.substr(0, 3);
if (GetDriveTypeW(path.c_str()) != DRIVE_FIXED) throw std::runtime_error("Workspace requires a fixed local volume");
auto first = std::make_unique<Handle>(CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
require(first->value != INVALID_HANDLE_VALUE, "Pin transaction volume");
wchar_t filesystem[32] = {};
require(GetVolumeInformationByHandleW(first->value, nullptr, 0, nullptr, nullptr, nullptr, filesystem, 32) != FALSE, "Inspect transaction volume");
if (_wcsicmp(filesystem, L"NTFS") != 0) throw std::runtime_error("Workspace transaction requires NTFS");
handle = first->value; held.push_back(std::move(first));
if (input.size() > 3) for (const auto& name : workspace_read::parts(input.substr(3))) {
auto next = std::make_unique<Handle>(openRelative(handle, name, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
require(next->value != INVALID_HANDLE_VALUE, "Pin transaction root ancestor");
workspace_read::information(next->value);
handle = next->value; held.push_back(std::move(next)); path /= name;
}
rootFinal = workspace_read::finalPath(handle); rootIdentity = workspace_read::identity(handle);
}
std::wstring relative(HANDLE target) const {
const auto actual = workspace_read::finalPath(target);
if (actual.size() <= rootFinal.size() || _wcsnicmp(actual.c_str(), rootFinal.c_str(), rootFinal.size()) != 0 || actual[rootFinal.size()] != L'\\') throw std::runtime_error("Transaction handle escaped root");
return actual.substr(rootFinal.size() + 1);
}
};
static std::string hex(const BYTE* bytes, size_t size) {
static const char* digits = "0123456789abcdef";
std::string result; result.reserve(size * 2);
for (size_t index = 0; index < size; ++index) { result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15]; }
return result;
}
static std::vector<BYTE> unhex(const std::string& text, size_t limit) {
if (text.size() % 2 || text.size() > limit * 2) throw std::runtime_error("Invalid transaction field length");
std::vector<BYTE> result; result.reserve(text.size() / 2);
auto digit = [](char ch) -> BYTE {
if (ch >= '0' && ch <= '9') return static_cast<BYTE>(ch - '0');
if (ch >= 'a' && ch <= 'f') return static_cast<BYTE>(ch - 'a' + 10);
throw std::runtime_error("Invalid transaction hex field");
};
for (size_t index = 0; index < text.size(); index += 2) result.push_back(static_cast<BYTE>((digit(text[index]) << 4) | digit(text[index + 1])));
return result;
}
static std::wstring pathField(const std::string& text) {
const auto bytes = unhex(text, 4096);
const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), nullptr, 0);
if (size <= 0 || size > 1024) throw std::runtime_error("Invalid transaction path encoding");
std::wstring result(size, 0);
require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), result.data(), size) == size, "Decode transaction path");
return result;
}
static std::wstring key(std::wstring value) {
std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) { return static_cast<wchar_t>(towlower(ch)); });
return value;
}
static std::string hash(const std::vector<BYTE>& bytes) {
BCRYPT_ALG_HANDLE algorithm = nullptr;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0) throw std::runtime_error("Open transaction SHA256");
BYTE result[32] = {};
const auto status = BCryptHash(algorithm, nullptr, 0, const_cast<BYTE*>(bytes.data()), static_cast<ULONG>(bytes.size()), result, sizeof(result));
BCryptCloseAlgorithmProvider(algorithm, 0);
if (status < 0) throw std::runtime_error("Hash transaction contents");
return hex(result, sizeof(result));
}
static std::vector<BYTE> contents(HANDLE handle) {
const auto info = workspace_read::information(handle);
const auto size = (static_cast<ULONGLONG>(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
if ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || size > fileLimit) throw std::runtime_error("Transaction file type or size denied");
std::vector<BYTE> bytes(static_cast<size_t>(size)); DWORD count = 0;
if (!bytes.empty()) require(ReadFile(handle, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr) && count == bytes.size(), "Read pinned transaction file");
return bytes;
}
static void reply(const std::string& text) {
const auto line = text + '\n'; DWORD count = 0;
require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), line.data(), static_cast<DWORD>(line.size()), &count, nullptr) && count == line.size(), "Write transaction response");
}
struct Session {
struct MissingParent : std::runtime_error { MissingParent() : std::runtime_error("Transaction parent missing") {} };
Root root;
std::map<std::wstring, std::unique_ptr<Handle>> directories;
std::map<std::wstring, std::string> known;
explicit Session(const std::wstring& path, const std::string& expected) : root(path) {
if (expected != "-" && expected != root.rootIdentity) throw std::runtime_error("Workspace root identity changed");
}
void validate(HANDLE handle, const std::wstring& relative, bool directory) {
for (const auto& ancestor : root.held) workspace_read::information(ancestor->value);
const auto info = workspace_read::information(handle);
if (directory != ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)) throw std::runtime_error("Transaction target type changed");
if (_wcsicmp(root.relative(handle).c_str(), relative.c_str()) != 0) throw std::runtime_error("Transaction final name changed");
}
HANDLE parent(const std::vector<std::wstring>& names) {
HANDLE parentHandle = root.handle; std::wstring relative;
workspace_read::information(parentHandle);
for (size_t index = 0; index + 1 < names.size(); ++index) {
if (!relative.empty()) relative += L'\\'; relative += names[index];
const auto name = key(relative);
if (directories.count(name)) { parentHandle = directories[name]->value; workspace_read::information(parentHandle); continue; }
if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit");
auto handle = std::make_unique<Handle>(openRelative(parentHandle, names[index], FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
if (handle->value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) throw MissingParent();
require(handle->value != INVALID_HANDLE_VALUE, "Pin transaction ancestor");
validate(handle->value, relative, true);
parentHandle = handle->value;
directories.emplace(name, std::move(handle));
}
return parentHandle;
}
void unpin(const std::wstring& relative) {
const auto prefix = key(relative);
for (auto entry = directories.begin(); entry != directories.end();) {
if (entry->first == prefix || entry->first.compare(0, prefix.size() + 1, prefix + L'\\') == 0) entry = directories.erase(entry);
else ++entry;
}
}
std::string execute(const std::vector<std::string>& fields) {
if (fields.size() != 5) throw std::runtime_error("Invalid transaction frame");
const auto& operation = fields[0]; const auto relative = pathField(fields[1]);
const auto names = workspace_read::parts(relative);
const auto name = key(relative);
HANDLE directory = nullptr;
try { directory = parent(names); }
catch (const MissingParent&) {
if (operation == "cleanup") return "{\"ok\":true}";
if (operation == "read" || operation == "stat") return "{\"ok\":true,\"kind\":\"missing\"}";
throw;
}
const auto absolute = names.back();
const auto& nonce = fields[4];
if (operation == "write" || operation == "cleanup") {
if (nonce.size() != 32 || nonce.find_first_not_of("0123456789abcdef") != std::string::npos) throw std::runtime_error("Invalid transaction mutation identity");
}
const auto temporaryName = L".ycode-sandbox-" + std::wstring(nonce.begin(), nonce.end());
const auto temporary = temporaryName + L"-new.tmp";
const auto previous = temporaryName + L"-old.tmp";
if (operation == "cleanup") {
const auto separator = fields[2].find(':');
if (separator == std::string::npos) throw std::runtime_error("Invalid transaction cleanup hashes");
const std::pair<fs::path, std::string> candidates[] = {{previous, fields[2].substr(0, separator)}, {temporary, fields[2].substr(separator + 1)}};
for (const auto& candidate : candidates) {
Handle stale(openRelative(directory, candidate.first.wstring(), GENERIC_READ | DELETE, false));
if (stale.value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) continue;
require(stale.value != INVALID_HANDLE_VALUE, "Open transaction recovery scratch");
validate(stale.value, (fs::path(relative).parent_path() / candidate.first.filename()).wstring(), false);
if (hash(contents(stale.value)) != candidate.second) throw std::runtime_error("Transaction scratch changed externally");
FILE_DISPOSITION_INFO disposition = {TRUE};
require(SetFileInformationByHandle(stale.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove transaction recovery scratch");
}
return "{\"ok\":true}";
}
if (operation == "mkdir") {
Handle created(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true, true));
require(created.value != INVALID_HANDLE_VALUE, "Create pinned transaction directory");
validate(created.value, relative, true);
const auto identity = workspace_read::identity(created.value);
directories[name] = std::make_unique<Handle>(created.value); created.value = nullptr;
known.erase(name);
return "{\"ok\":true,\"identity\":\"" + identity + "\"}";
}
if (operation == "rmdir") unpin(relative);
const bool mutation = operation == "write" || operation == "unlink" || operation == "rmdir";
Handle target(openRelative(directory, absolute, GENERIC_READ | (mutation ? DELETE : 0), operation == "rmdir"));
const DWORD error = target.value == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
const bool missing = error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
if (target.value == INVALID_HANDLE_VALUE && !missing) { SetLastError(error); require(false, "Open pinned transaction target"); }
if (operation == "read" || operation == "stat") {
if (missing) { known.erase(name); return "{\"ok\":true,\"kind\":\"missing\"}"; }
const bool isDirectory = (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
validate(target.value, relative, isDirectory);
const auto identity = workspace_read::identity(target.value); known[name] = identity;
if (isDirectory && !directories.count(name)) {
if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit");
auto pinned = std::make_unique<Handle>(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
require(pinned->value != INVALID_HANDLE_VALUE, "Pin inspected transaction directory");
validate(pinned->value, relative, true);
if (workspace_read::identity(pinned->value) != identity) throw std::runtime_error("Inspected transaction directory identity changed");
directories[name] = std::move(pinned);
}
if (operation == "read" && isDirectory) throw std::runtime_error("Transaction target is a directory");
std::string result = "{\"ok\":true,\"kind\":\"" + std::string(isDirectory ? "directory" : "file") + "\",\"identity\":\"" + identity + "\"";
if (operation == "read") result += ",\"data\":\"" + workspace_read::base64(contents(target.value)) + "\"";
return result + '}';
}
if (operation == "list") {
if (missing) throw std::runtime_error("Transaction directory missing");
validate(target.value, relative, true);
std::string result = "{\"ok\":true,\"entries\":["; bool first = true; size_t count = 0;
std::vector<BYTE> buffer(65536);
for (;;) {
if (!GetFileInformationByHandleEx(target.value, FileIdBothDirectoryInfo, buffer.data(), static_cast<DWORD>(buffer.size()))) {
if (GetLastError() == ERROR_NO_MORE_FILES) break;
require(false, "Enumerate transaction directory");
}
size_t offset = 0;
for (;;) {
const auto entry = reinterpret_cast<FILE_ID_BOTH_DIR_INFO*>(buffer.data() + offset);
if (entry->FileNameLength > buffer.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName)) throw std::runtime_error("Invalid transaction directory record");
const std::wstring child(entry->FileName, entry->FileNameLength / sizeof(wchar_t));
if (child != L"." && child != L"..") {
if (++count > 10000 || !workspace_read::component(child)) throw std::runtime_error("Transaction directory entry denied");
if (!first) result += ','; first = false;
result += "\"" + jsonText(child) + "\"";
}
if (entry->NextEntryOffset == 0) break;
if (entry->NextEntryOffset < sizeof(FILE_ID_BOTH_DIR_INFO) || entry->NextEntryOffset > buffer.size() - offset - sizeof(FILE_ID_BOTH_DIR_INFO)) throw std::runtime_error("Invalid transaction directory offset");
offset += entry->NextEntryOffset;
}
}
return result + "]}";
}
if (!mutation) throw std::runtime_error("Unknown transaction operation");
if (!missing) {
validate(target.value, relative, operation == "rmdir");
if (operation != "rmdir" && (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_READONLY)) throw std::runtime_error("Transaction read-only file denied");
const auto identity = workspace_read::identity(target.value);
if (known.count(name) && known[name] != identity) throw std::runtime_error("Transaction target identity changed");
if (operation == "rmdir") {
if (fields[2] != identity) throw std::runtime_error("Transaction directory identity changed");
} else if (fields[2] != hash(contents(target.value))) throw std::runtime_error("Transaction contents changed");
} else if (operation != "write" || fields[2] != "-") throw std::runtime_error("Transaction target disappeared");
if (operation == "unlink" || operation == "rmdir") {
FILE_DISPOSITION_INFO disposition = {TRUE};
require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Delete pinned transaction target");
known.erase(name);
return "{\"ok\":true}";
}
const auto bytes = unhex(fields[3], fileLimit);
Handle output(openRelative(directory, temporary, GENERIC_WRITE | DELETE | FILE_READ_ATTRIBUTES, false, true));
require(output.value != INVALID_HANDLE_VALUE, "Create transaction replacement");
bool published = false, moved = false;
auto renameHandle = [directory](HANDLE handle, const fs::path& destination) {
const auto name = destination.wstring();
std::vector<BYTE> storage(offsetof(FILE_RENAME_INFO, FileName) + name.size() * sizeof(wchar_t));
auto rename = reinterpret_cast<FILE_RENAME_INFO*>(storage.data());
rename->ReplaceIfExists = FALSE;
rename->RootDirectory = directory; rename->FileNameLength = static_cast<DWORD>(name.size() * sizeof(wchar_t));
memcpy(rename->FileName, name.data(), rename->FileNameLength);
IO_STATUS_BLOCK status = {};
return nativeOk(nativeFunction<SetFileFn>("NtSetInformationFile")(handle, &status, rename, static_cast<ULONG>(storage.size()), static_cast<FILE_INFORMATION_CLASS>(10)));
};
try {
DWORD count = 0;
if (!bytes.empty()) require(WriteFile(output.value, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr) && count == bytes.size(), "Write transaction replacement");
require(FlushFileBuffers(output.value) != FALSE, "Flush transaction replacement");
validate(output.value, (fs::path(relative).parent_path() / fs::path(temporary).filename()).wstring(), false);
const auto replacementIdentity = workspace_read::identity(output.value);
// A no-overwrite handoff keeps the original handle exclusively pinned.
// A competing basename creation fails publication instead of losing data.
if (!missing) { require(renameHandle(target.value, previous), "Stage pinned transaction original"); moved = true; }
require(renameHandle(output.value, absolute), "Publish pinned transaction replacement");
published = true;
known[name] = replacementIdentity;
if (moved) {
FILE_DISPOSITION_INFO disposition = {TRUE};
require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove pinned transaction original");
}
} catch (...) {
FILE_DISPOSITION_INFO disposition = {TRUE};
if (!published) SetFileInformationByHandle(output.value, FileDispositionInfo, &disposition, sizeof(disposition));
if (moved && !published && !renameHandle(target.value, absolute)) throw std::runtime_error("Transaction handoff needs recovery; competing target preserved");
throw;
}
return "{\"ok\":true}";
}
};
static int run(const std::wstring& root, const std::string& expected, bool gitMetadata = false) {
if (gitMetadata) fileLimit = 128 * 1024 * 1024;
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace transaction is host-only");
workspace_read::Lifetime lifetime(600000);
Session session(root, expected);
reply("{\"protocol\":1,\"rootIdentity\":\"" + session.root.rootIdentity + "\"}");
std::string line; size_t requests = 0; char buffer[16384]; DWORD count = 0;
while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &count, nullptr) && count) {
for (DWORD index = 0; index < count; ++index) {
if (buffer[index] != '\n') {
if (line.size() >= fileLimit * 2 + 16384) throw std::runtime_error("Transaction frame limit");
line += buffer[index]; continue;
}
if (++requests > 100000) throw std::runtime_error("Transaction request limit");
std::vector<std::string> fields; size_t start = 0;
for (;;) { const auto end = line.find('\t', start); fields.push_back(line.substr(start, end == std::string::npos ? end : end - start)); if (end == std::string::npos) break; start = end + 1; }
try { reply(session.execute(fields)); }
catch (const std::exception& error) {
std::string message;
for (char ch : std::string(error.what())) { if (ch == '\\' || ch == '"') message += '\\'; message += ch; }
reply("{\"ok\":false,\"error\":\"" + message + "\"}");
}
line.clear();
}
}
if (!line.empty()) throw std::runtime_error("Truncated transaction frame");
return 0;
}
}
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <wincrypt.h>
#pragma comment(lib, "crypt32.lib")
namespace git_control {
static int ssh(int argc, wchar_t** argv) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git SSH control is host-only");
BOOL inJob = FALSE; require(IsProcessInJob(GetCurrentProcess(), nullptr, &inJob) != FALSE && inJob, "Git SSH requires its control Job");
wchar_t module[32768] = {};
require(GetModuleFileNameW(nullptr, module, 32768) > 0, "Locate Git SSH control");
const auto tools = fs::path(module).parent_path(), root = tools.parent_path().parent_path(), executable = tools / L"ssh.exe";
for (const auto& path : {executable, root / L"ssh-config", root / L"ssh-key", root / L"known-hosts"}) nonReparse(path);
std::vector<std::wstring> args = {L"-F", (root / L"ssh-config").wstring(), L"-o", L"BatchMode=yes", L"-o", L"IdentitiesOnly=yes", L"-o", L"IdentityAgent=none",
L"-o", L"ProxyCommand=none", L"-o", L"ProxyJump=none", L"-o", L"CanonicalizeHostname=no", L"-o", L"ClearAllForwardings=yes", L"-o", L"PermitLocalCommand=no",
L"-o", L"StrictHostKeyChecking=yes", L"-o", L"UpdateHostKeys=no", L"-o", L"UserKnownHostsFile=" + (root / L"known-hosts").wstring(),
L"-o", L"GlobalKnownHostsFile=" + (root / L"ssh-config").wstring(), L"-i", (root / L"ssh-key").wstring()};
for (int index = 1; index < argc; ++index) args.push_back(argv[index]);
auto command = quote(executable.wstring()); for (const auto& arg : args) command += L" " + quote(arg);
if (command.size() > 32000) throw std::runtime_error("Git SSH argv exceeds limit");
Handle input, output, error;
require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_INPUT_HANDLE), GetCurrentProcess(), &input.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stdin");
require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_OUTPUT_HANDLE), GetCurrentProcess(), &output.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stdout");
require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), GetCurrentProcess(), &error.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stderr");
AttributeList attributes; HANDLE handles[] = {input.value, output.value, error.value};
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Git SSH handle allowlist");
STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup); startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = input.value; startup.StartupInfo.hStdOutput = output.value; startup.StartupInfo.hStdError = error.value; startup.lpAttributeList = attributes.list;
PROCESS_INFORMATION process = {};
require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, nullptr, root.c_str(), &startup.StartupInfo, &process) != FALSE, "Create Git SSH process");
Handle child(process.hProcess), thread(process.hThread); WaitForSingleObject(child.value, INFINITE);
DWORD exit = 125; require(GetExitCodeProcess(child.value, &exit) != FALSE, "Read Git SSH exit"); return static_cast<int>(exit);
}
static int credential(bool protect) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git credentials are host-only");
std::string input; char buffer[4096]; DWORD count = 0;
while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &count, nullptr) && count) {
input.append(buffer, count);
if (input.size() > 131072) throw std::runtime_error("Git credential exceeds limit");
}
auto bytes = workspace_write::unhex(input, 65536);
DATA_BLOB source = {static_cast<DWORD>(bytes.size()), bytes.data()}, output = {};
const BOOL result = protect ? CryptProtectData(&source, L"YCode Git credential", nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)
: CryptUnprotectData(&source, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output);
require(result != FALSE, "Protect/unprotect Git credential with user DPAPI");
const auto encoded = workspace_write::hex(output.pbData, output.cbData);
SecureZeroMemory(output.pbData, output.cbData); LocalFree(output.pbData);
SecureZeroMemory(bytes.data(), bytes.size());
workspace_write::reply("{\"data\":\"" + encoded + "\"}");
return 0;
}
static std::wstring text(const std::string& encoded) {
const auto bytes = workspace_write::unhex(encoded, 1024 * 1024);
if (bytes.empty()) return L"";
const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), nullptr, 0);
if (count <= 0) throw std::runtime_error("Invalid Git control UTF-8");
std::wstring value(count, 0);
require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), value.data(), count) == count, "Decode Git control text");
if (value.find(L'\0') != std::wstring::npos) throw std::runtime_error("NUL in Git control text");
return value;
}
static std::vector<std::wstring> fields(const fs::path& path) {
nonReparse(path);
Handle file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
require(file.value != INVALID_HANDLE_VALUE, "Open Git control fields");
const auto bytes = workspace_write::contents(file.value);
if (bytes.size() > 1024 * 1024) throw std::runtime_error("Git control fields exceed limit");
std::string input(bytes.begin(), bytes.end()); std::vector<std::wstring> result; size_t start = 0;
for (;;) {
const auto end = input.find('\n', start);
if (end == std::string::npos) break;
if (result.size() >= 256) throw std::runtime_error("Git control field count exceeds limit");
result.push_back(text(input.substr(start, end - start))); start = end + 1;
}
if (start != input.size()) throw std::runtime_error("Incomplete Git control fields");
return result;
}
static int run(const std::wstring& input, DWORD timeout) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git control is host-only");
if (timeout < 100 || timeout > 600000) throw std::runtime_error("Invalid Git control timeout");
const fs::path stage = fs::absolute(input);
nonReparse(stage);
const auto executable = stage / L"tools/git/git.exe";
nonReparse(executable);
auto command = quote(executable.wstring());
for (const auto& arg : fields(stage / L"arguments")) command += L" " + quote(arg);
if (command.size() > 32000) throw std::runtime_error("Git control argv exceeds Windows limit");
auto environmentFields = fields(stage / L"environment");
std::sort(environmentFields.begin(), environmentFields.end());
std::vector<wchar_t> environment;
for (const auto& entry : environmentFields) {
const auto separator = entry.find(L'=');
if (!separator || separator == std::wstring::npos || entry.substr(0, separator).find_first_not_of(L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") != std::wstring::npos) throw std::runtime_error("Invalid Git control environment");
environment.insert(environment.end(), entry.begin(), entry.end()); environment.push_back(0);
}
environment.push_back(0);
Handle parent(parentProcess()), job(CreateJobObjectW(nullptr, nullptr));
writeJson(stage / L"native-lease.json", "{\"version\":1,\"pid\":" + std::to_string(GetCurrentProcessId()) + ",\"root\":\"" + jsonText(stage.wstring()) + "\"}\n");
require(job.value != nullptr, "Create Git control Job");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_JOB_MEMORY;
limits.BasicLimitInformation.ActiveProcessLimit = 32; limits.JobMemoryLimit = static_cast<SIZE_T>(1024) * 1024 * 1024;
require(SetInformationJobObject(job.value, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) != FALSE, "Git control Job limits");
SECURITY_ATTRIBUTES inherited = {sizeof(inherited), nullptr, TRUE};
Handle stdinFile(CreateFileW((stage / L"input").c_str(), GENERIC_READ, FILE_SHARE_READ, &inherited, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
require(stdinFile.value != INVALID_HANDLE_VALUE, "Open Git control input");
Handle output, error;
require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_OUTPUT_HANDLE), GetCurrentProcess(), &output.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Duplicate Git stdout");
require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), GetCurrentProcess(), &error.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Duplicate Git stderr");
AttributeList attributes;
HANDLE handles[] = {stdinFile.value, output.value, error.value};
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Git control handle allowlist");
STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup); startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = stdinFile.value; startup.StartupInfo.hStdOutput = output.value; startup.StartupInfo.hStdError = error.value;
startup.lpAttributeList = attributes.list;
PROCESS_INFORMATION process = {};
require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
environment.data(), (stage / L"workspace").c_str(), &startup.StartupInfo, &process) != FALSE, "Create Git control process");
Handle child(process.hProcess), thread(process.hThread);
if (!AssignProcessToJobObject(job.value, child.value)) { TerminateProcess(child.value, 125); throw std::runtime_error("Assign Git control Job"); }
require(ResumeThread(thread.value) != static_cast<DWORD>(-1), "Resume Git control process");
HANDLE monitored[] = {child.value, parent.value};
const DWORD waited = WaitForMultipleObjects(2, monitored, FALSE, timeout);
DWORD exit = waited == WAIT_TIMEOUT ? 124 : 130;
if (waited == WAIT_OBJECT_0) require(GetExitCodeProcess(child.value, &exit) != FALSE, "Read Git control exit");
TerminateJobObject(job.value, exit); WaitForSingleObject(child.value, 5000);
return static_cast<int>(exit);
}
}
+134
View File
@@ -0,0 +1,134 @@
'use strict'
const fs = require('node:fs')
const { randomUUID, createHash } = require('node:crypto')
const prefix = '\x1eYCODE_WORKSPACE_READ_V1 '
let selected = false
function error(code, message) { return Object.assign(new Error(message), { code }) }
function frame(operation, args, id) { return '\n' + prefix + JSON.stringify({ version: 1, requestId: id, operation, args }) + '\n' }
function send(operation, args, id) {
const bytes = Buffer.from(frame(operation, args, id))
let offset = 0
while (offset < bytes.length) offset += fs.writeSync(1, bytes, offset, bytes.length - offset)
}
function unpack(line, id) {
let response
try { response = JSON.parse(line) } catch { throw error('EPROTO', 'Invalid file bridge reply') }
if (response.version !== 1 || response.requestId !== id || typeof response.ok !== 'boolean') throw error('EPROTO', 'Unexpected file bridge reply')
if (!response.ok) throw error(response.error?.code || 'EACCES', response.error?.message || 'Workspace read denied')
return response.value
}
function claim() {
if (selected) throw error('EBUSY', 'Choose one bridge client and mode per process; stdin belongs to that client')
selected = true
}
function decode(part, initial, offset) {
if (!part || part.snapshotId !== initial.snapshotId || part.sha256 !== initial.sha256 || part.size !== initial.size || part.offset !== offset || typeof part.data !== 'string') throw error('EPROTO', 'Inconsistent read snapshot')
const bytes = Buffer.from(part.data, 'base64')
if (bytes.length > 16384 || !bytes.length && offset < initial.size || offset + bytes.length > initial.size || part.eof !== (offset + bytes.length === initial.size)) throw error('EPROTO', 'Invalid read chunk')
return bytes
}
function complete(chunks, initial, encoding) {
const bytes = Buffer.concat(chunks)
if (bytes.length !== initial.size || createHash('sha256').update(bytes).digest('hex') !== initial.sha256) throw error('EPROTO', 'Read snapshot digest mismatch')
return encoding ? bytes.toString(encoding) : bytes
}
function connectSync() {
claim()
let buffered = Buffer.alloc(0), closed = false
function call(operation, args, id = randomUUID()) {
if (closed) throw error('EPIPE', 'File bridge client closed')
send(operation, args, id)
for (;;) {
const end = buffered.indexOf(10)
if (end >= 0) {
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
return unpack(line, id)
}
if (buffered.length > 32768) throw error('EPROTO', 'File bridge reply exceeds limit')
const bytes = Buffer.alloc(32768), length = fs.readSync(0, bytes, 0, bytes.length, null)
if (!length) { closed = true; throw error('EPIPE', 'File bridge disconnected') }
buffered = Buffer.concat([buffered, bytes.subarray(0, length)])
}
}
return {
call,
readFileSync(path, encoding) {
const initial = call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
statSync: path => call('workspace.stat', { path }),
readdirSync: path => call('workspace.readdir', { path }).entries,
querySync: requestId => call('operation.query', { requestId }),
close() { closed = true }
}
}
function connect() {
claim()
const waiting = new Map(), queue = []
let buffered = Buffer.alloc(0), closed = false, failure
function shutdown(reason) {
if (closed) return
closed = true; failure = reason
process.stdin.off('data', receive); process.stdin.off('end', ended); process.stdin.off('error', failed); process.stdin.pause()
process.stdin.destroy()
for (const item of [...waiting.values(), ...queue]) item.reject(reason)
waiting.clear(); queue.length = 0
}
function flush() {
while (!closed && waiting.size < 8 && queue.length) {
const item = queue.shift(); waiting.set(item.id, item)
try { send(item.operation, item.args, item.id) } catch (reason) { shutdown(reason) }
}
if (waiting.size) { process.stdin.ref?.(); process.stdin.resume() }
else { process.stdin.pause(); process.stdin.unref?.() }
}
function receive(bytes) {
buffered = Buffer.concat([buffered, bytes])
for (;;) {
const end = buffered.indexOf(10)
if (end < 0) { if (buffered.length > 32768) shutdown(error('EPROTO', 'File bridge reply exceeds limit')); return }
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
let id
try { id = JSON.parse(line).requestId } catch { shutdown(error('EPROTO', 'Invalid file bridge reply')); return }
const item = waiting.get(id)
if (!item) { shutdown(error('EPROTO', 'Unexpected file bridge reply')); return }
waiting.delete(id)
try { item.resolve(unpack(line, id)) } catch (reason) { item.reject(reason) }
flush()
}
}
const ended = () => shutdown(error('EPIPE', 'File bridge disconnected'))
const failed = reason => shutdown(reason)
process.stdin.on('data', receive); process.stdin.on('end', ended); process.stdin.on('error', failed); process.stdin.pause()
function call(operation, args, id = randomUUID()) {
if (closed) return Promise.reject(failure || error('EPIPE', 'File bridge client closed'))
if (queue.length + waiting.size >= 32) return Promise.reject(error('EBUSY', 'File bridge client queue full'))
return new Promise((resolve, reject) => { queue.push({ id, operation, args, resolve, reject }); flush() })
}
return {
call,
async readFile(path, encoding) {
const initial = await call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = await call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { await call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
stat: path => call('workspace.stat', { path }),
readdir: async path => (await call('workspace.readdir', { path })).entries,
query: requestId => call('operation.query', { requestId }),
close() { shutdown(error('EPIPE', 'File bridge client closed')) }
}
}
module.exports = { connect, connectSync }
+70
View File
@@ -0,0 +1,70 @@
'use strict'
// Explicit preload for synchronous subprocess capture in Windows AppContainers.
// Every subprocess still uses Node's own spawn and inherits the existing token/Job.
const fs = require('node:fs')
const path = require('node:path')
const cp = require('node:child_process')
const { syncBuiltinESMExports } = require('node:module')
const originals = { spawnSync: cp.spawnSync, execFileSync: cp.execFileSync, execSync: cp.execSync }
const maximum = 16 * 1024 * 1024
function captured(method, file, args, options, spawn) {
if (options.stdio !== undefined && options.stdio !== 'pipe') return method()
const limit = options.maxBuffer === undefined ? 1024 * 1024 : options.maxBuffer
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maximum) throw new RangeError('Sandbox process adapter maxBuffer must be between 0 and 16 MiB')
const input = options.input === undefined ? Buffer.alloc(0) : typeof options.input === 'string'
? Buffer.from(options.input, options.encoding && options.encoding !== 'buffer' ? options.encoding : 'utf8')
: ArrayBuffer.isView(options.input) ? Buffer.from(options.input.buffer, options.input.byteOffset, options.input.byteLength) : null
if (!input) throw new TypeError('Sandbox process adapter input must be a string, Buffer or ArrayBuffer view')
if (input.length > maximum) throw new RangeError('Sandbox process adapter input exceeds 16 MiB')
const directory = fs.mkdtempSync(path.join(process.env.TEMP, 'ycode-process-'))
const files = ['stdin', 'stdout', 'stderr'].map(name => path.join(directory, name)), handles = []
const encode = bytes => options.encoding && options.encoding !== 'buffer' ? bytes.toString(options.encoding) : bytes
let answer, failure
try {
fs.writeFileSync(files[0], input, { flag: 'wx', mode: 0o600 })
handles.push(fs.openSync(files[0], 'r'))
handles.push(fs.openSync(files[1], 'wx+'))
handles.push(fs.openSync(files[2], 'wx+'))
const forwarded = { ...options, stdio: handles }; delete forwarded.input
try { answer = method(forwarded) } catch (error) { failure = error }
const read = index => {
const buffer = Buffer.alloc(Math.min(fs.fstatSync(handles[index]).size, limit + 1))
let length = 0
while (length < buffer.length) { const count = fs.readSync(handles[index], buffer, length, buffer.length - length, length); if (!count) break; length += count }
return { bytes: buffer.subarray(0, Math.min(length, limit)), overflow: length > limit }
}
const out = read(1), err = read(2), stdout = encode(out.bytes), stderr = encode(err.bytes)
if (out.overflow || err.overflow) failure = Object.assign(new Error('Sandbox subprocess output exceeds maxBuffer'), { code: 'ENOBUFS', syscall: 'spawnSync', path: file, spawnargs: args })
if (spawn) {
if (failure && !answer) throw failure
return { ...answer, stdout, stderr, output: [null, stdout, stderr], ...(failure ? { error: failure } : {}) }
}
if (failure) {
failure.stdout = stdout; failure.stderr = stderr; failure.output = [null, stdout, stderr]
throw failure
}
return stdout
} finally {
for (const handle of handles) try { fs.closeSync(handle) } catch {}
for (const file of files) try { fs.unlinkSync(file) } catch {}
try { fs.rmdirSync(directory) } catch {}
}
}
function argumentsOf(args, options) {
return Array.isArray(args) ? { args, options: options || {} } : { args: [], options: args == null ? options || {} : args }
}
function spawnSync(file, args, options) {
const normalized = argumentsOf(args, options)
return captured(forwarded => originals.spawnSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, true)
}
function execFileSync(file, args, options) {
const normalized = argumentsOf(args, options)
return captured(forwarded => originals.execFileSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, false)
}
function execSync(command, options = {}) {
return captured(forwarded => originals.execSync(command, forwarded || options), command, [], options, false)
}
if (process.env.YCODE_SANDBOX !== 'appcontainer') throw new Error('The subprocess adapter is for the isolated YCode Node runtime')
Object.assign(cp, { spawnSync, execFileSync, execSync })
syncBuiltinESMExports()
module.exports = { spawnSync, execFileSync, execSync }
+247
View File
@@ -0,0 +1,247 @@
// Test-only executable. Never distributed as the sandbox runner.
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#include <windows.h>
#include <userenv.h>
#include <sddl.h>
#include <cstdio>
#include <string>
#include <vector>
static bool registryId(const std::wstring& value) {
if (value.size() != 36 || value[8] != L'-' || value[13] != L'-' || value[18] != L'-' || value[23] != L'-') return false;
for (size_t index = 0; index < value.size(); ++index) {
if (index == 8 || index == 13 || index == 18 || index == 23) continue;
if ((value[index] < L'0' || value[index] > L'9') && (value[index] < L'a' || value[index] > L'f')) return false;
}
return true;
}
static std::wstring pipeName(const std::wstring& id) { return L"\\\\.\\pipe\\YCodeSandboxTest." + id; }
static int pipeServer(const std::wstring& id, const std::wstring& profile) {
if (!registryId(id) || profile.size() != 42 || profile.substr(0, 10) != L"YCode.Run." || profile.substr(10).find_first_not_of(L"0123456789abcdef") != std::wstring::npos) return 2;
PSID expected = nullptr;
if (FAILED(DeriveAppContainerSidFromAppContainerName(profile.c_str(), &expected))) return 29;
HANDLE token = nullptr; DWORD bytes = 0;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { FreeSid(expected); return 29; }
GetTokenInformation(token, TokenUser, nullptr, 0, &bytes); std::vector<BYTE> user(bytes);
const bool gotUser = GetTokenInformation(token, TokenUser, user.data(), bytes, &bytes) != FALSE; CloseHandle(token);
LPWSTR owner = nullptr, package = nullptr;
if (!gotUser || !ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &owner) || !ConvertSidToStringSidW(expected, &package)) { if (owner) LocalFree(owner); FreeSid(expected); return 29; }
const std::wstring sddl = L"D:P(A;;GA;;;SY)(A;;GA;;;" + std::wstring(owner) + L")(A;;0x00120003;;;" + std::wstring(package) + L")S:(ML;;NW;;;LW)";
std::printf("PIPE_EXPECTED_SID %ls\n", package); std::fflush(stdout);
LocalFree(owner); LocalFree(package);
PSECURITY_DESCRIPTOR descriptor = nullptr;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), SDDL_REVISION_1, &descriptor, nullptr)) { FreeSid(expected); return 29; }
SECURITY_ATTRIBUTES security = {sizeof(security), descriptor, FALSE};
HANDLE pipe = CreateNamedPipeW(pipeName(id).c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1, 4096, 4096, 5000, &security);
LocalFree(descriptor);
if (pipe == INVALID_HANDLE_VALUE) { FreeSid(expected); return 29; }
const bool connected = ConnectNamedPipe(pipe, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED;
ULONG pid = 0; bool authenticated = false;
if (connected && GetNamedPipeClientProcessId(pipe, &pid)) {
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid), clientToken = nullptr;
if (process && OpenProcessToken(process, TOKEN_QUERY, &clientToken)) {
DWORD container = 0, needed = 0;
if (GetTokenInformation(clientToken, TokenIsAppContainer, &container, sizeof(container), &needed) && container) {
GetTokenInformation(clientToken, TokenAppContainerSid, nullptr, 0, &needed); std::vector<BYTE> data(needed);
if (GetTokenInformation(clientToken, TokenAppContainerSid, data.data(), needed, &needed)) authenticated = EqualSid(reinterpret_cast<TOKEN_APPCONTAINER_INFORMATION*>(data.data())->TokenAppContainer, expected) != FALSE;
}
CloseHandle(clientToken);
}
if (process) CloseHandle(process);
}
FreeSid(expected);
char input[8] = {}; DWORD read = 0, written = 0;
bool ok = authenticated && ReadFile(pipe, input, sizeof(input), &read, nullptr) && read == 4 && std::string(input, read) == "PING";
const char response[] = "BROKER_PONG";
if (ok) ok = WriteFile(pipe, response, sizeof(response) - 1, &written, nullptr) && written == sizeof(response) - 1;
if (ok) FlushFileBuffers(pipe);
DisconnectNamedPipe(pipe); CloseHandle(pipe);
std::printf("PIPE_%s\n", ok ? "AUTHENTICATED" : "REJECTED"); return ok ? 0 : 29;
}
static int pipeClient(const std::wstring& id) {
if (!registryId(id)) return 2;
HANDLE token = nullptr;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
DWORD bytes = 0; GetTokenInformation(token, TokenAppContainerSid, nullptr, 0, &bytes); std::vector<BYTE> data(bytes);
if (GetTokenInformation(token, TokenAppContainerSid, data.data(), bytes, &bytes)) {
const PSID sid = reinterpret_cast<TOKEN_APPCONTAINER_INFORMATION*>(data.data())->TokenAppContainer;
LPWSTR text = nullptr;
if (sid && ConvertSidToStringSidW(sid, &text)) { std::printf("PIPE_CLIENT_SID %ls\n", text); LocalFree(text); std::fflush(stdout); }
}
CloseHandle(token);
}
HANDLE pipe = INVALID_HANDLE_VALUE; const auto deadline = GetTickCount64() + 4000;
do {
pipe = CreateFileW(pipeName(id).c_str(), FILE_READ_DATA | FILE_WRITE_DATA | SYNCHRONIZE, 0, nullptr, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, nullptr);
if (pipe != INVALID_HANDLE_VALUE) break;
if (GetLastError() != ERROR_FILE_NOT_FOUND && GetLastError() != ERROR_PIPE_BUSY) { std::printf("PIPE_DENIED %lu\n", GetLastError()); return 29; }
Sleep(10);
} while (GetTickCount64() < deadline);
if (pipe == INVALID_HANDLE_VALUE) { std::printf("PIPE_UNAVAILABLE %lu\n", GetLastError()); return 29; }
DWORD written = 0, read = 0; char response[64] = {};
const bool ok = WriteFile(pipe, "PING", 4, &written, nullptr) && written == 4 && ReadFile(pipe, response, sizeof(response), &read, nullptr);
CloseHandle(pipe);
if (ok) std::fwrite(response, 1, read, stdout);
return ok && std::string(response, read) == "BROKER_PONG" ? 0 : 29;
}
static int stdioBroker(const std::wstring& mode, const std::wstring& wideId) {
if (!registryId(wideId)) return 2;
std::string id;
for (wchar_t ch : wideId) id += static_cast<char>(ch);
if (mode == L"--broker-oversize") {
const std::string frame = "\x1eYCODE_BROKER_V1 " + std::string(20000, 'x') + "\n";
std::fwrite(frame.data(), 1, frame.size(), stdout); std::fflush(stdout); Sleep(5000); return 30;
}
if (mode == L"--broker-flood") {
for (char index = '0'; index < '9'; ++index) {
auto current = id; current.back() = index;
std::printf("\x1eYCODE_BROKER_V1 {\"requestId\":\"%s\",\"operation\":\"echo\",\"payload\":\"probe-echo\"}\n", current.c_str());
}
std::fflush(stdout); Sleep(5000); return 30;
}
for (int attempt = 0; attempt < (mode == L"--broker-replay" ? 2 : 1); ++attempt) {
std::string frame = "\x1eYCODE_BROKER_V1 {\"requestId\":\"" + id + "\",\"operation\":\"" +
(mode == L"--broker-deny" ? "file_read" : "echo") + "\",\"payload\":\"probe-echo\"";
if (mode == L"--broker-spoof") frame += ",\"runId\":\"unrelated-run\"";
frame += "}\n"; std::fwrite(frame.data(), 1, frame.size(), stdout); std::fflush(stdout);
std::string reply; char buffer[1024] = {}; DWORD read = 0;
while (reply.find('\n') == std::string::npos && reply.size() < 16384 && ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &read, nullptr) && read) reply.append(buffer, read);
const bool rejected = mode == L"--broker-deny" || mode == L"--broker-spoof";
if (reply.find("\"requestId\":\"" + id + "\"") == std::string::npos || reply.find(rejected ? "\"ok\":false" : "\"ok\":true") == std::string::npos) return 30;
}
std::puts(mode == L"--broker-deny" || mode == L"--broker-spoof" ? "BROKER_DENIAL_RECEIVED" : "BROKER_ECHO_RECEIVED"); return 0;
}
static int registryProbe(const std::wstring& mode, const std::wstring& id) {
if (!registryId(id)) return 2;
const std::wstring path = L"Software\\YCodeSandboxTests\\" + id;
if (mode == L"--registry-delete") {
const LSTATUS result = RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
std::printf("REG_DELETE %ld\n", result);
RegDeleteKeyW(HKEY_CURRENT_USER, L"Software\\YCodeSandboxTests");
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND ? 0 : 27;
}
HKEY key = nullptr;
const bool read = mode == L"--registry-read";
const LSTATUS opened = read ? RegOpenKeyExW(HKEY_CURRENT_USER, path.c_str(), 0, KEY_QUERY_VALUE, &key)
: RegCreateKeyExW(HKEY_CURRENT_USER, path.c_str(), 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr);
if (opened != ERROR_SUCCESS) { std::printf("REG_OPEN_DENIED %ld\n", opened); return 27; }
DWORD value = mode == L"--registry-create" ? 0x123456 : 99, bytes = sizeof(value), type = 0;
const LSTATUS result = read ? RegQueryValueExW(key, L"fixture", nullptr, &type, reinterpret_cast<BYTE*>(&value), &bytes)
: RegSetValueExW(key, L"fixture", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(key);
if (read) std::printf("REG_READ %ld %lu\n", result, value);
else std::printf("REG_WRITE %ld\n", result);
return result == ERROR_SUCCESS ? 0 : 27;
}
static std::wstring quoted(const std::wstring& value) { return L"\"" + value + L"\""; }
static int writeFile(const wchar_t* path) {
HANDLE file = CreateFileW(path, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) { std::printf("WRITE_DENIED %lu\n", GetLastError()); return 23; }
const char text[] = "native-probe"; DWORD written = 0;
const bool ok = WriteFile(file, text, sizeof(text) - 1, &written, nullptr) != FALSE;
CloseHandle(file); return ok ? 0 : 23;
}
static int spawnChild(const std::wstring& arguments, bool wait, DWORD flags = 0) {
wchar_t executable[32768] = {};
if (!GetModuleFileNameW(nullptr, executable, 32768)) return 25;
std::wstring command = quoted(executable) + L" " + arguments;
STARTUPINFOW startup = {}; startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); startup.hStdError = GetStdHandle(STD_ERROR_HANDLE);
PROCESS_INFORMATION process = {};
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, TRUE, flags, nullptr, nullptr, &startup, &process)) {
std::printf("SPAWN_DENIED %lu\n", GetLastError()); return 25;
}
CloseHandle(process.hThread);
DWORD exit = 0;
if (wait) { WaitForSingleObject(process.hProcess, 10000); GetExitCodeProcess(process.hProcess, &exit); }
CloseHandle(process.hProcess); return static_cast<int>(exit);
}
int wmain(int argc, wchar_t** argv) {
if (argc < 2) return 2;
const std::wstring mode = argv[1];
if (mode.rfind(L"--broker-", 0) == 0 && argc == 3) return stdioBroker(mode, argv[2]);
if (mode == L"--pipe-server" && argc == 4) return pipeServer(argv[2], argv[3]);
if (mode == L"--pipe-client" && argc == 3) return pipeClient(argv[2]);
if (mode == L"--registry-create" || mode == L"--registry-read" || mode == L"--registry-write" || mode == L"--registry-delete") return argc == 3 ? registryProbe(mode, argv[2]) : 2;
if (mode == L"--hold-directory" && argc == 5) {
HANDLE directory = CreateFileW(argv[2], FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
if (directory == INVALID_HANDLE_VALUE) return 28;
FILE_ATTRIBUTE_TAG_INFO tag = {};
if (!GetFileInformationByHandleEx(directory, FileAttributeTagInfo, &tag, sizeof(tag)) || (tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { CloseHandle(directory); return 28; }
if (writeFile(argv[3])) { CloseHandle(directory); return 28; }
const ULONGLONG deadline = GetTickCount64() + 5000;
while (GetFileAttributesW(argv[4]) == INVALID_FILE_ATTRIBUTES && GetTickCount64() < deadline) Sleep(10);
CloseHandle(directory); return 0;
}
if (mode == L"--host-facts") {
HANDLE token = nullptr; TOKEN_ELEVATION elevation = {}; DWORD bytes = 0, container = 0;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return 3;
const bool elevated = GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &bytes) && elevation.TokenIsElevated;
GetTokenInformation(token, TokenIsAppContainer, &container, sizeof(container), &bytes); CloseHandle(token);
std::printf("{\"elevated\":%s,\"appContainer\":%s}\n", elevated ? "true" : "false", container ? "true" : "false"); return 0;
}
if (mode == L"--info") {
HANDLE token = nullptr; DWORD container = 0, bytes = 0; BOOL job = FALSE;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return 3;
GetTokenInformation(token, TokenIsAppContainer, &container, sizeof(container), &bytes); CloseHandle(token);
IsProcessInJob(GetCurrentProcess(), nullptr, &job);
wchar_t secret[128] = {};
const bool inherited = GetEnvironmentVariableW(L"YCODE_FAKE_SECRET", secret, 128) != 0;
std::printf("{\"appContainer\":%s,\"job\":%s,\"secretInherited\":%s}\n", container ? "true" : "false", job ? "true" : "false", inherited ? "true" : "false");
return container && job && !inherited ? 0 : 4;
}
if (mode == L"--write" && argc == 3) return writeFile(argv[2]);
if (mode == L"--delete" && argc == 3) {
const BOOL deleted = DeleteFileW(argv[2]);
std::printf("DELETE_%s %lu\n", deleted ? "OK" : "DENIED", deleted ? 0 : GetLastError());
return deleted ? 0 : 26;
}
if (mode == L"--paths") {
wchar_t cwd[32768] = {}, full[32768] = {};
GetCurrentDirectoryW(32768, cwd); GetFullPathNameW(L".", 32768, full, nullptr);
std::wprintf(L"CWD=%ls FULL=%ls\n", cwd, full);
const DWORD attributes = GetFileAttributesW(L".");
std::printf("ATTR=%lu ERROR=%lu\n", attributes, GetLastError());
WIN32_FIND_DATAW data = {};
HANDLE found = FindFirstFileW(L".\\*", &data);
std::printf("FIND=%s ERROR=%lu\n", found == INVALID_HANDLE_VALUE ? "DENIED" : "OK", GetLastError());
if (found != INVALID_HANDLE_VALUE) FindClose(found);
return 0;
}
if (mode == L"--read" && argc == 3) {
HANDLE file = CreateFileW(argv[2], GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (file == INVALID_HANDLE_VALUE) { std::printf("READ_DENIED %lu\n", GetLastError()); return 22; }
char buffer[256] = {}; DWORD bytes = 0;
const bool ok = ReadFile(file, buffer, sizeof(buffer), &bytes, nullptr) != FALSE; CloseHandle(file);
if (ok) std::fwrite(buffer, 1, bytes, stdout);
return ok ? 0 : 22;
}
if (mode == L"--tcp" && argc == 3) {
WSADATA data = {}; if (WSAStartup(MAKEWORD(2, 2), &data)) return 24;
SOCKET socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket == INVALID_SOCKET) { WSACleanup(); return 24; }
sockaddr_in target = {}; target.sin_family = AF_INET; target.sin_port = htons(static_cast<u_short>(_wtoi(argv[2]))); target.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
u_long nonblocking = 1; ioctlsocket(socket, FIONBIO, &nonblocking);
int connected = connect(socket, reinterpret_cast<sockaddr*>(&target), sizeof(target));
int error = connected == 0 ? 0 : WSAGetLastError();
if (connected != 0 && error == WSAEWOULDBLOCK) {
fd_set ready, failed; FD_ZERO(&ready); FD_ZERO(&failed); FD_SET(socket, &ready); FD_SET(socket, &failed);
timeval timeout = {1, 0};
if (select(0, nullptr, &ready, &failed, &timeout) > 0) {
int length = sizeof(error); getsockopt(socket, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&error), &length);
connected = error == 0 ? 0 : SOCKET_ERROR;
} else { connected = SOCKET_ERROR; error = WSAETIMEDOUT; }
}
std::printf("TCP_%s %d\n", connected == 0 ? "CONNECTED" : "DENIED", error);
closesocket(socket); WSACleanup(); return connected == 0 ? 0 : 24;
}
if (mode == L"--delay" && argc == 4) { Sleep(static_cast<DWORD>(_wtoi(argv[2]))); return writeFile(argv[3]); }
if (mode == L"--spawn-write" && argc == 3) return spawnChild(L"--write " + quoted(argv[2]), true);
if (mode == L"--spawn-delayed" && argc == 3) return spawnChild(L"--delay 1400 " + quoted(argv[2]), false);
if (mode == L"--breakaway" && argc == 3) return spawnChild(L"--write " + quoted(argv[2]), true, CREATE_BREAKAWAY_FROM_JOB);
return 2;
}
+23
View File
@@ -0,0 +1,23 @@
"""Keep private Windows temporary directories accessible to this AppContainer.
Python 3.13+ gives mode 0o700 an explicit user/admin DACL on Windows. That
omits the AppContainer SID. Inside our stage, inherit the native private parent
DACL instead. Kernel permissions and the Job remain the security boundary.
"""
import os
if os.name == "nt" and os.environ.get("YCODE_SANDBOX") == "appcontainer":
_mkdir = os.mkdir
_stage = os.path.normcase(os.path.abspath(os.environ["YCODE_SANDBOX_ROOT"]))
def _private_mkdir(path, mode=0o777, *, dir_fd=None):
if mode == 0o700 and dir_fd is None:
target = os.path.normcase(os.path.abspath(os.fsdecode(path)))
try:
if os.path.commonpath((_stage, target)) == _stage:
mode = 0o777
except ValueError:
pass
return _mkdir(path, mode, dir_fd=dir_fd)
os.mkdir = _private_mkdir
+444
View File
@@ -0,0 +1,444 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <userenv.h>
#include <sddl.h>
#include <aclapi.h>
#include <tlhelp32.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace fs = std::filesystem;
struct Handle {
HANDLE value = nullptr;
Handle() = default;
explicit Handle(HANDLE handle) : value(handle) {}
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};
static void require(bool ok, const char* operation) {
if (!ok) throw std::runtime_error(std::string(operation) + ": win32=" + std::to_string(GetLastError()));
}
static void security(DWORD error, const char* operation) {
if (error != ERROR_SUCCESS) throw std::runtime_error(std::string(operation) + ": win32=" + std::to_string(error));
}
static bool inContainer(HANDLE process) {
Handle token;
require(OpenProcessToken(process, TOKEN_QUERY, &token.value) != FALSE, "OpenProcessToken");
DWORD value = 0, bytes = 0;
require(GetTokenInformation(token.value, TokenIsAppContainer, &value, sizeof(value), &bytes) != FALSE, "TokenIsAppContainer");
return value != 0;
}
static bool matchesContainer(HANDLE process, PSID expected) {
Handle token;
require(OpenProcessToken(process, TOKEN_QUERY, &token.value) != FALSE, "Open child token");
DWORD bytes = 0; GetTokenInformation(token.value, TokenAppContainerSid, nullptr, 0, &bytes);
std::vector<BYTE> buffer(bytes);
require(GetTokenInformation(token.value, TokenAppContainerSid, buffer.data(), bytes, &bytes) != FALSE, "Child AppContainer SID");
return EqualSid(reinterpret_cast<TOKEN_APPCONTAINER_INFORMATION*>(buffer.data())->TokenAppContainer, expected) != FALSE;
}
static std::wstring quote(const std::wstring& value) {
std::wstring result = L"\"";
size_t slashes = 0;
for (wchar_t ch : value) {
if (ch == L'\\') { ++slashes; continue; }
if (ch == L'\"') result.append(slashes * 2 + 1, L'\\');
else result.append(slashes, L'\\');
slashes = 0;
result += ch;
}
result.append(slashes * 2, L'\\');
return result + L"\"";
}
static std::string jsonText(const std::wstring& value) {
const int count = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
std::string bytes(count, 0), escaped;
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), bytes.data(), count, nullptr, nullptr);
for (char ch : bytes) { if (ch == '\\' || ch == '"') escaped += '\\'; escaped += ch; }
return escaped;
}
static void writeJson(const fs::path& target, const std::string& json) {
Handle output(CreateFileW(target.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
require(output.value != INVALID_HANDLE_VALUE, "Create control report");
DWORD written = 0;
require(WriteFile(output.value, json.data(), static_cast<DWORD>(json.size()), &written, nullptr) && written == json.size(), "Write control report");
require(FlushFileBuffers(output.value) != FALSE, "Flush control report");
}
static std::wstring windowsDirectory() {
wchar_t buffer[MAX_PATH] = {};
require(GetWindowsDirectoryW(buffer, MAX_PATH) != 0, "GetWindowsDirectory");
return buffer;
}
static void nonReparse(const fs::path& target) {
auto current = target;
for (;;) {
const DWORD attributes = GetFileAttributesW(current.c_str());
require(attributes != INVALID_FILE_ATTRIBUTES, "GetFileAttributes");
if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) throw std::runtime_error("Reparse point in sandbox control path");
const auto parent = current.parent_path();
if (parent == current || parent.empty()) break;
current = parent;
}
}
static void privateDirectory(const fs::path& path) {
Handle token;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token.value) != FALSE, "OpenProcessToken");
DWORD bytes = 0;
GetTokenInformation(token.value, TokenUser, nullptr, 0, &bytes);
std::vector<BYTE> user(bytes);
require(GetTokenInformation(token.value, TokenUser, user.data(), bytes, &bytes) != FALSE, "TokenUser");
LPWSTR sid = nullptr;
require(ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &sid) != FALSE, "ConvertSidToStringSid");
const std::wstring sddl = L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;" + std::wstring(sid) + L")";
LocalFree(sid);
PSECURITY_DESCRIPTOR descriptor = nullptr;
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), SDDL_REVISION_1, &descriptor, nullptr) != FALSE, "Private DACL");
PACL acl = nullptr; BOOL present = FALSE, defaulted = FALSE;
require(GetSecurityDescriptorDacl(descriptor, &present, &acl, &defaulted) != FALSE, "GetSecurityDescriptorDacl");
const DWORD error = SetNamedSecurityInfoW(const_cast<LPWSTR>(path.c_str()), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, nullptr, nullptr, acl, nullptr);
LocalFree(descriptor);
security(error, "Set private control DACL");
}
static void grantStage(const fs::path& target, PSID sid, bool writable = true) {
if (!writable) privateDirectory(target);
PSECURITY_DESCRIPTOR old = nullptr;
PACL acl = nullptr, next = nullptr;
security(GetNamedSecurityInfoW(target.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &acl, nullptr, &old), "Read stage DACL");
EXPLICIT_ACCESSW access = {};
access.grfAccessPermissions = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
if (writable) access.grfAccessPermissions |= FILE_GENERIC_WRITE | DELETE;
access.grfAccessMode = GRANT_ACCESS;
access.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
access.Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
access.Trustee.ptstrName = static_cast<LPWSTR>(sid);
const DWORD combined = SetEntriesInAclW(1, &access, acl, &next);
if (combined != ERROR_SUCCESS) { LocalFree(old); security(combined, "Build stage DACL"); }
const DWORD applied = SetNamedSecurityInfoW(const_cast<LPWSTR>(target.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, next, nullptr);
LocalFree(next); LocalFree(old);
security(applied, "Grant stage DACL");
PSECURITY_DESCRIPTOR label = nullptr;
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(L"S:(ML;OICI;NW;;;LW)", SDDL_REVISION_1, &label, nullptr) != FALSE, "Stage integrity label");
PACL sacl = nullptr; BOOL present = FALSE, defaulted = FALSE;
require(GetSecurityDescriptorSacl(label, &present, &sacl, &defaulted) != FALSE, "GetSecurityDescriptorSacl");
const DWORD labeled = SetNamedSecurityInfoW(const_cast<LPWSTR>(target.c_str()), SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, nullptr, nullptr, nullptr, sacl);
LocalFree(label);
security(labeled, "Set stage integrity label");
}
struct Profile {
std::wstring name;
PSID sid = nullptr;
explicit Profile(std::wstring value) : name(std::move(value)) {
if (name.size() != 42 || name.substr(0, 10) != L"YCode.Run." ||
name.substr(10).find_first_not_of(L"0123456789abcdef") != std::wstring::npos) throw std::runtime_error("Invalid profile identity");
const HRESULT hr = CreateAppContainerProfile(name.c_str(), L"YCode isolated command", L"YCode per-command workspace snapshot", nullptr, 0, &sid);
if (FAILED(hr)) throw std::runtime_error("CreateAppContainerProfile: hresult=" + std::to_string(static_cast<unsigned long>(hr)));
}
bool remove() {
if (!sid) return true;
FreeSid(sid); sid = nullptr;
return SUCCEEDED(DeleteAppContainerProfile(name.c_str()));
}
~Profile() { remove(); }
};
static bool validProfile(const std::wstring& name) {
return name.size() == 42 && name.substr(0, 10) == L"YCode.Run." && name.substr(10).find_first_not_of(L"0123456789abcdef") == std::wstring::npos;
}
static HANDLE parentProcess() {
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
require(snapshot.value != INVALID_HANDLE_VALUE, "CreateToolhelp32Snapshot");
PROCESSENTRY32W entry = {}; entry.dwSize = sizeof(entry);
require(Process32FirstW(snapshot.value, &entry) != FALSE, "Process32First");
do {
if (entry.th32ProcessID == GetCurrentProcessId()) {
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, entry.th32ParentProcessID);
require(parent != nullptr, "Open parent monitor");
return parent;
}
} while (Process32NextW(snapshot.value, &entry));
throw std::runtime_error("Parent process identity unavailable");
}
struct Drive {
std::wstring name, target;
explicit Drive(const fs::path& stage) : target(L"\\??\\" + stage.wstring()) {
Handle mutex(CreateMutexW(nullptr, FALSE, L"Local\\YCode.Sandbox.DriveAllocator.v1"));
require(mutex.value != nullptr, "Create drive allocator mutex");
const DWORD lock = WaitForSingleObject(mutex.value, 5000);
if (lock != WAIT_OBJECT_0 && lock != WAIT_ABANDONED) throw std::runtime_error("Drive allocator busy");
for (wchar_t letter = L'Z'; letter >= L'D'; --letter) {
const std::wstring candidate = std::wstring(1, letter) + L":";
wchar_t existing[32768] = {};
if (QueryDosDeviceW(candidate.c_str(), existing, 32768)) continue;
if (DefineDosDeviceW(DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, candidate.c_str(), target.c_str())) { name = candidate; break; }
}
ReleaseMutex(mutex.value);
if (name.empty()) throw std::runtime_error("No free drive for sandbox workspace");
}
bool release() {
if (name.empty()) return true;
const bool removed = DefineDosDeviceW(DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE | DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, name.c_str(), target.c_str()) != FALSE;
if (removed) name.clear();
return removed;
}
~Drive() { release(); }
fs::path root() const { return fs::path(name + L"\\"); }
};
static std::vector<wchar_t> childEnvironment(const fs::path& stage) {
const auto win = windowsDirectory();
std::vector<std::wstring> entries = {
L"=" + stage.root_name().wstring() + L"=" + (stage / L"workspace").wstring(),
L"APPDATA=" + (stage / L"home/AppData/Roaming").wstring(), L"COMSPEC=" + win + L"\\System32\\cmd.exe",
L"HOME=" + (stage / L"home").wstring(), L"LOCALAPPDATA=" + (stage / L"home/AppData/Local").wstring(),
L"PATH=" + (stage / L"tools/node").wstring() + L";" + (stage / L"tools/python").wstring() + L";" + (stage / L"tools/git").wstring() + L";" + (stage / L"tools").wstring() + L";" + win + L"\\System32;" + win + L"\\System32\\WindowsPowerShell\\v1.0",
L"NODE_REPL_HISTORY=" + (stage / L"home/node_repl_history").wstring(),
L"NPM_CONFIG_CACHE=" + (stage / L"home/npm-cache").wstring(), L"NPM_CONFIG_OFFLINE=true",
L"NPM_CONFIG_USERCONFIG=" + (stage / L"home/npmrc").wstring(),
L"PYTHONHOME=" + (stage / L"tools/python").wstring(), L"PYTHONNOUSERSITE=1", L"PYTHONDONTWRITEBYTECODE=1",
L"PIP_CONFIG_FILE=NUL", L"PIP_NO_INPUT=1", L"PIP_NO_INDEX=1", L"PIP_DISABLE_PIP_VERSION_CHECK=1",
L"PIP_CACHE_DIR=" + (stage / L"home/pip-cache").wstring(),
L"PYTHONPATH=" + (stage / L"tools/python-adapter").wstring() + L";" + (stage / L"home/python-site").wstring(),
L"GIT_CONFIG_NOSYSTEM=1", L"GIT_CONFIG_GLOBAL=NUL", L"GIT_CONFIG_SYSTEM=NUL", L"GIT_TERMINAL_PROMPT=0",
L"GIT_OPTIONAL_LOCKS=0", L"GIT_PAGER=", L"GIT_EDITOR=" + (stage / L"tools/ycode-git-editor.exe").generic_wstring(), L"GIT_SEQUENCE_EDITOR=" + (stage / L"tools/ycode-git-editor.exe").generic_wstring(), L"GIT_ALLOW_PROTOCOL=",
L"GIT_EXEC_PATH=" + (stage / L"tools/git/empty-hooks").wstring(), L"GIT_TEMPLATE_DIR=" + (stage / L"tools/git/empty-template").wstring(),
L"GIT_CONFIG_COUNT=7", L"GIT_CONFIG_KEY_0=core.hooksPath", L"GIT_CONFIG_VALUE_0=" + (stage / L"tools/git/empty-hooks").wstring(),
L"GIT_CONFIG_KEY_1=core.fsmonitor", L"GIT_CONFIG_VALUE_1=false", L"GIT_CONFIG_KEY_2=credential.helper", L"GIT_CONFIG_VALUE_2=",
L"GIT_CONFIG_KEY_3=commit.gpgSign", L"GIT_CONFIG_VALUE_3=false", L"GIT_CONFIG_KEY_4=tag.gpgSign", L"GIT_CONFIG_VALUE_4=false",
L"GIT_CONFIG_KEY_5=maintenance.auto", L"GIT_CONFIG_VALUE_5=false",
L"GIT_CONFIG_KEY_6=diff.autoRefreshIndex", L"GIT_CONFIG_VALUE_6=false",
L"PATHEXT=.COM;.EXE;.BAT;.CMD", L"SystemRoot=" + win, L"TEMP=" + (stage / L"tmp").wstring(),
L"TMP=" + (stage / L"tmp").wstring(), L"USERPROFILE=" + (stage / L"home").wstring(),
L"WINDIR=" + win, L"YCODE_SANDBOX=appcontainer", L"YCODE_SANDBOX_BRIDGE=" + (stage / L"tools/ycode-bridge.cjs").wstring(),
L"YCODE_SANDBOX_PROCESS=" + (stage / L"tools/ycode-process.cjs").wstring(), L"YCODE_SANDBOX_ROOT=" + stage.wstring()
};
if (fs::is_directory(stage / L"repository")) {
entries.push_back(L"GIT_DIR=" + (stage / L"repository").wstring());
entries.push_back(L"GIT_WORK_TREE=" + (stage / L"workspace").wstring());
}
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; });
std::vector<wchar_t> block;
for (const auto& entry : entries) { block.insert(block.end(), entry.begin(), entry.end()); block.push_back(0); }
block.push_back(0);
return block;
}
static void forward(HANDLE input, HANDLE output) {
char buffer[16384]; DWORD read = 0;
while (ReadFile(input, buffer, sizeof(buffer), &read, nullptr) && read) {
DWORD offset = 0;
while (offset < read) {
DWORD written = 0;
if (!WriteFile(output, buffer + offset, read - offset, &written, nullptr) || !written) return;
offset += written;
}
}
}
static bool brokerInput(const std::string& line, std::string& decoded) {
if (line.size() > 65536 || line.rfind("input:", 0) != 0) return false;
const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const auto encoded = line.substr(6);
if (encoded.empty() || encoded.size() % 4) return false;
unsigned int accumulator = 0; int bits = 0; bool padding = false; size_t paddingCount = 0;
for (size_t index = 0; index < encoded.size(); ++index) {
const char ch = encoded[index];
if (ch == '=') { padding = true; if (++paddingCount > 2 || index < encoded.size() - 2) return false; continue; }
const auto value = alphabet.find(ch);
if (padding || value == std::string::npos) return false;
accumulator = (accumulator << 6) | static_cast<unsigned int>(value); bits += 6;
if (bits >= 8) { bits -= 8; decoded += static_cast<char>((accumulator >> bits) & 255); }
}
return decoded.size() <= 32768 && bits == static_cast<int>(paddingCount) * 2 && (bits == 0 || (accumulator & ((1U << bits) - 1)) == 0);
}
struct AttributeList {
std::vector<BYTE> bytes;
LPPROC_THREAD_ATTRIBUTE_LIST list = nullptr;
AttributeList() {
SIZE_T size = 0; InitializeProcThreadAttributeList(nullptr, 2, 0, &size);
bytes.resize(size); list = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(bytes.data());
require(InitializeProcThreadAttributeList(list, 2, 0, &size) != FALSE, "InitializeProcThreadAttributeList");
}
~AttributeList() { if (list) DeleteProcThreadAttributeList(list); }
};
namespace git_control { static std::vector<std::wstring> fields(const fs::path& path); }
static int run(const std::wstring& name, const fs::path& stage, const fs::path& report, DWORD timeout, bool broker = false) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Runner cannot broker from inside an AppContainer");
if (!stage.is_absolute() || report.parent_path() != stage.parent_path() || report.filename() != L"result.json" ||
stage.filename() != L"stage" || fs::exists(report)) throw std::runtime_error("Invalid sandbox control layout");
nonReparse(stage);
privateDirectory(stage.parent_path());
Profile profile(name);
// Windows derives the AppContainer TEMP path from LOCALAPPDATA even when TEMP
// was supplied explicitly. Provision that private path before applying ACLs.
fs::create_directories(stage / L"home/AppData/Local/Packages" / name / L"AC/Temp");
grantStage(stage, profile.sid);
for (const auto& entry : fs::recursive_directory_iterator(stage)) {
if (GetFileAttributesW(entry.path().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) throw std::runtime_error("Reparse point in stage");
const auto relative = entry.path().lexically_relative(stage).generic_wstring();
const bool tool = relative == L"tools" || relative.rfind(L"tools/", 0) == 0 ||
relative == L"workspace/node_modules" || relative.rfind(L"workspace/node_modules/", 0) == 0 ||
relative == L"home/python-site" || relative.rfind(L"home/python-site/", 0) == 0;
grantStage(entry.path(), profile.sid, !tool);
}
Drive drive(stage);
writeJson(stage.parent_path() / L"lease.json", "{\"version\":1,\"pid\":" + std::to_string(GetCurrentProcessId()) +
",\"profile\":\"" + jsonText(name) + "\",\"drive\":\"" + jsonText(drive.name) + "\",\"stage\":\"" + jsonText(stage.wstring()) + "\"}\n");
Handle parent(parentProcess());
Handle job(CreateJobObjectW(nullptr, nullptr)); require(job.value != nullptr, "CreateJobObject");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_JOB_MEMORY;
limits.BasicLimitInformation.ActiveProcessLimit = 32;
limits.JobMemoryLimit = static_cast<SIZE_T>(1024) * 1024 * 1024;
require(SetInformationJobObject(job.value, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) != FALSE, "Job limits");
JOBOBJECT_BASIC_UI_RESTRICTIONS ui = {}; ui.UIRestrictionsClass = JOB_OBJECT_UILIMIT_ALL;
require(SetInformationJobObject(job.value, JobObjectBasicUIRestrictions, &ui, sizeof(ui)) != FALSE, "Job UI limits");
SECURITY_ATTRIBUTES inherited = {sizeof(inherited), nullptr, TRUE};
Handle outRead, outWrite, errRead, errWrite;
require(CreatePipe(&outRead.value, &outWrite.value, &inherited, 0) != FALSE, "stdout pipe");
require(CreatePipe(&errRead.value, &errWrite.value, &inherited, 0) != FALSE, "stderr pipe");
require(SetHandleInformation(outRead.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "stdout inheritance");
require(SetHandleInformation(errRead.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "stderr inheritance");
Handle input, inputWrite;
if (broker) {
require(CreatePipe(&input.value, &inputWrite.value, &inherited, 65536) != FALSE, "broker child input");
require(SetHandleInformation(inputWrite.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "broker input writer inheritance");
} else input.value = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &inherited, OPEN_EXISTING, 0, nullptr);
require(input.value != INVALID_HANDLE_VALUE, "NUL input");
AttributeList attributes;
SECURITY_CAPABILITIES caps = {}; caps.AppContainerSid = profile.sid;
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &caps, sizeof(caps), nullptr, nullptr) != FALSE, "AppContainer capability attribute");
HANDLE handles[] = {input.value, outWrite.value, errWrite.value};
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Handle allowlist attribute");
STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = input.value; startup.StartupInfo.hStdOutput = outWrite.value; startup.StartupInfo.hStdError = errWrite.value;
startup.lpAttributeList = attributes.list;
auto executable = windowsDirectory() + L"\\System32\\cmd.exe";
std::wstring command = quote(executable) + L" /d /s /c \"\"" + (drive.root() / L"command.cmd").wstring() + L"\"\"";
if (fs::exists(stage / L"git-arguments")) {
executable = (drive.root() / L"tools/git/git.exe").wstring(); command = quote(executable);
for (const auto& arg : git_control::fields(stage / L"git-arguments")) command += L" " + quote(arg);
if (command.size() > 32000) throw std::runtime_error("Isolated Git argv exceeds Windows limit");
}
auto environment = childEnvironment(drive.root());
PROCESS_INFORMATION process = {};
require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
environment.data(), (drive.root() / L"workspace").c_str(), &startup.StartupInfo, &process) != FALSE, "Create AppContainer process");
Handle child(process.hProcess), thread(process.hThread);
if (!AssignProcessToJobObject(job.value, child.value) || !inContainer(child.value) || !matchesContainer(child.value, profile.sid)) {
TerminateProcess(child.value, 125); throw std::runtime_error("Child isolation verification failed");
}
CloseHandle(outWrite.value); outWrite.value = nullptr;
CloseHandle(errWrite.value); errWrite.value = nullptr;
const ULONGLONG started = GetTickCount64();
require(ResumeThread(thread.value) != static_cast<DWORD>(-1), "Resume isolated process");
std::thread stdoutThread(forward, outRead.value, GetStdHandle(STD_OUTPUT_HANDLE));
std::thread stderrThread(forward, errRead.value, GetStdHandle(STD_ERROR_HANDLE));
Handle cancelled(CreateEventW(nullptr, TRUE, FALSE, nullptr));
std::thread controlThread([&cancelled, &inputWrite, broker]() {
char buffer[4096] = {}; DWORD read = 0; std::string pending;
while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &read, nullptr) && read) {
pending.append(buffer, read);
if (pending.size() > 65536) { SetEvent(cancelled.value); return; }
size_t newline = 0;
while ((newline = pending.find('\n')) != std::string::npos) {
const auto line = pending.substr(0, newline); pending.erase(0, newline + 1);
if (line == "cancel") { SetEvent(cancelled.value); return; }
std::string decoded;
if (!broker || !brokerInput(line, decoded)) { SetEvent(cancelled.value); return; }
DWORD written = 0;
if (!WriteFile(inputWrite.value, decoded.data(), static_cast<DWORD>(decoded.size()), &written, nullptr) || written != decoded.size()) { SetEvent(cancelled.value); return; }
}
}
});
HANDLE monitored[] = {child.value, parent.value, cancelled.value};
const DWORD waited = WaitForMultipleObjects(3, monitored, FALSE, timeout);
DWORD exit = 125;
const char* status = "error";
if (waited == WAIT_OBJECT_0) { GetExitCodeProcess(child.value, &exit); status = "completed"; }
else if (waited == WAIT_TIMEOUT) { exit = 124; status = "timeout"; }
else if (waited == WAIT_OBJECT_0 + 1) { exit = 130; status = "parent-exited"; }
else if (waited == WAIT_OBJECT_0 + 2) { exit = 130; status = "cancelled"; }
// No breakaway flags: close/terminate the job also stops still-running descendants.
const bool terminated = TerminateJobObject(job.value, exit) != FALSE;
WaitForSingleObject(child.value, 5000);
bool quiescent = false;
for (int attempt = 0; terminated && attempt < 500; ++attempt) {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting = {};
if (!QueryInformationJobObject(job.value, JobObjectBasicAccountingInformation, &accounting, sizeof(accounting), nullptr)) break;
if (accounting.ActiveProcesses == 0) { quiescent = true; break; }
Sleep(10);
}
if (!quiescent) { status = "error"; exit = 125; }
CancelSynchronousIo(controlThread.native_handle());
controlThread.join();
stdoutThread.join(); stderrThread.join();
const bool cleaned = profile.remove();
const bool unmapped = drive.release();
const auto elapsed = GetTickCount64() - started;
const std::string json = "{\"protocol\":1,\"backend\":\"windows-appcontainer\",\"isolated\":true,\"network\":\"deny\",\"status\":\"" +
std::string(status) + "\",\"exitCode\":" + std::to_string(exit) + ",\"durationMs\":" + std::to_string(elapsed) +
",\"profileRemoved\":" + (cleaned ? "true" : "false") + ",\"driveRemoved\":" + (unmapped ? "true" : "false") + ",\"broker\":" + (broker ? "\"bound-stdio-v1\"" : "null") + "}\n";
writeJson(report, json);
return 0;
}
#include "filesystem-read.h"
#include "filesystem-write.h"
#include "workspace-state.h"
#include "git-control.h"
int wmain(int argc, wchar_t** argv) {
try {
if (_wcsicmp(fs::path(argv[0]).filename().c_str(), L"ycode-git-editor.exe") == 0) return 0;
if (_wcsicmp(fs::path(argv[0]).filename().c_str(), L"git-ssh.exe") == 0) return git_control::ssh(argc, argv);
if (argc == 2 && (std::wstring(argv[1]) == L"--git-protect" || std::wstring(argv[1]) == L"--git-unprotect")) return git_control::credential(std::wstring(argv[1]) == L"--git-protect");
if (argc == 4 && std::wstring(argv[1]) == L"--git-control") {
size_t end = 0; const auto timeout = std::stoul(argv[3], &end);
if (end != wcslen(argv[3]) || timeout > 600000) throw std::runtime_error("Invalid Git control timeout");
return git_control::run(argv[2], static_cast<DWORD>(timeout));
}
if (argc == 3 && std::wstring(argv[1]) == L"--seal-workspace") return workspace_state::run(argv[2]);
if (argc == 4 && (std::wstring(argv[1]) == L"--workspace-write" || std::wstring(argv[1]) == L"--git-metadata-write")) {
const std::wstring expected(argv[3]);
if (expected.find_first_not_of(L"0123456789abcdef:-") != std::wstring::npos) throw std::runtime_error("Invalid workspace identity");
std::string expectedBytes;
for (wchar_t ch : expected) expectedBytes += static_cast<char>(ch);
return workspace_write::run(argv[2], expectedBytes, std::wstring(argv[1]) == L"--git-metadata-write");
}
if (argc == 7 && std::wstring(argv[1]) == L"--workspace-read") {
const std::wstring expected(argv[5]);
if (expected.find_first_not_of(L"0123456789abcdef:-") != std::wstring::npos) throw std::runtime_error("Invalid workspace identity");
size_t end = 0; const auto limit = std::stoul(argv[6], &end);
if (end != wcslen(argv[6]) || limit > 8 * 1024 * 1024) throw std::runtime_error("Invalid workspace limit");
std::string expectedBytes;
for (wchar_t ch : expected) expectedBytes += static_cast<char>(ch);
return workspace_read::run(argv[2], argv[3], argv[4], expectedBytes, static_cast<DWORD>(limit));
}
if (argc == 2 && std::wstring(argv[1]) == L"--version") {
std::puts("{\"protocol\":1,\"backend\":\"windows-appcontainer\",\"version\":\"0.7.0\",\"network\":\"deny\"}"); return 0;
}
if (argc == 3 && std::wstring(argv[1]) == L"--cleanup") {
if (inContainer(GetCurrentProcess()) || !validProfile(argv[2])) throw std::runtime_error("Invalid profile cleanup request");
const HRESULT cleaned = DeleteAppContainerProfile(argv[2]);
return SUCCEEDED(cleaned) || cleaned == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || cleaned == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || cleaned == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) ? 0 : 125;
}
if (argc == 4 && std::wstring(argv[1]) == L"--release-drive") {
const std::wstring drive = argv[2];
const fs::path stage(argv[3]);
if (inContainer(GetCurrentProcess()) || drive.size() != 2 || drive[0] < L'D' || drive[0] > L'Z' || drive[1] != L':' || !stage.is_absolute() || stage.filename() != L"stage") throw std::runtime_error("Invalid drive cleanup request");
const auto target = L"\\??\\" + stage.wstring();
const BOOL removed = DefineDosDeviceW(DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE | DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, drive.c_str(), target.c_str());
return removed || GetLastError() == ERROR_FILE_NOT_FOUND ? 0 : 125;
}
const std::wstring mode = argc > 1 ? argv[1] : L"";
if (argc != 6 || (mode != L"--run" && mode != L"--run-stdio")) throw std::runtime_error("Usage: --run or --run-stdio profile stage result.json timeout-ms");
size_t end = 0; const auto timeout = std::stoul(argv[5], &end);
if (end != wcslen(argv[5]) || timeout < 100 || timeout > 600000) throw std::runtime_error("Invalid timeout");
return run(argv[2], fs::path(argv[3]), fs::path(argv[4]), static_cast<DWORD>(timeout), mode == L"--run-stdio");
} catch (const std::exception& error) { std::fprintf(stderr, "YCODE_SANDBOX_ERROR %s\n", error.what()); return 125; }
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
namespace workspace_state {
struct PrivateDacl {
PSECURITY_DESCRIPTOR descriptor = nullptr;
PACL acl = nullptr;
PrivateDacl() {
Handle token;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token.value) != FALSE, "Open workspace seal token");
DWORD bytes = 0; GetTokenInformation(token.value, TokenUser, nullptr, 0, &bytes);
std::vector<BYTE> user(bytes);
require(GetTokenInformation(token.value, TokenUser, user.data(), bytes, &bytes) != FALSE, "Read workspace seal owner");
LPWSTR sid = nullptr;
require(ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &sid) != FALSE, "Encode workspace seal owner");
const std::wstring sddl = L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;" + std::wstring(sid) + L")";
LocalFree(sid);
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), SDDL_REVISION_1, &descriptor, nullptr) != FALSE, "Build workspace private DACL");
BOOL present = FALSE, defaulted = FALSE;
require(GetSecurityDescriptorDacl(descriptor, &present, &acl, &defaulted) != FALSE && present && acl, "Read workspace private DACL");
}
~PrivateDacl() { if (descriptor) LocalFree(descriptor); }
void apply(HANDLE handle) const {
security(SetSecurityInfo(handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
nullptr, nullptr, acl, nullptr), "Seal workspace handle DACL");
}
};
static void seal(HANDLE handle, const PrivateDacl& dacl, size_t& count, unsigned int depth = 0) {
const auto info = workspace_read::information(handle);
if (++count > 10001 || depth > 64) throw std::runtime_error("Workspace seal entry or nesting limit");
dacl.apply(handle);
if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) return;
std::vector<BYTE> buffer(65536);
for (;;) {
if (!GetFileInformationByHandleEx(handle, FileIdBothDirectoryInfo, buffer.data(), static_cast<DWORD>(buffer.size()))) {
if (GetLastError() == ERROR_NO_MORE_FILES) break;
require(false, "Enumerate workspace seal directory");
}
size_t offset = 0;
for (;;) {
const auto entry = reinterpret_cast<FILE_ID_BOTH_DIR_INFO*>(buffer.data() + offset);
if (entry->FileNameLength > buffer.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName)) throw std::runtime_error("Invalid workspace seal directory record");
const std::wstring name(entry->FileName, entry->FileNameLength / sizeof(wchar_t));
if (name != L"." && name != L"..") {
if (!workspace_read::component(name) || entry->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) throw std::runtime_error("Workspace seal link or invalid name denied");
Handle child(workspace_write::openRelative(handle, name, GENERIC_READ | READ_CONTROL | WRITE_DAC, false));
require(child.value != INVALID_HANDLE_VALUE, "Open workspace seal child");
seal(child.value, dacl, count, depth + 1);
}
if (entry->NextEntryOffset == 0) break;
if (entry->NextEntryOffset < sizeof(FILE_ID_BOTH_DIR_INFO) || entry->NextEntryOffset > buffer.size() - offset - sizeof(FILE_ID_BOTH_DIR_INFO)) throw std::runtime_error("Invalid workspace seal directory offset");
offset += entry->NextEntryOffset;
}
}
}
static int run(const std::wstring& path) {
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace sealing is host-only");
workspace_read::Lifetime lifetime(120000);
workspace_write::Root root(path);
if (root.held.size() < 2) throw std::runtime_error("A volume root cannot be a managed workspace");
const auto names = workspace_read::parts(path.substr(3));
Handle target(workspace_write::openRelative(root.held[root.held.size() - 2]->value, names.back(), GENERIC_READ | READ_CONTROL | WRITE_DAC, true));
require(target.value != INVALID_HANDLE_VALUE, "Open workspace seal root");
if (workspace_read::identity(target.value) != root.rootIdentity) throw std::runtime_error("Workspace seal root changed");
PrivateDacl dacl; size_t count = 0;
seal(target.value, dacl, count);
workspace_write::reply("{\"protocol\":1,\"sealed\":true,\"rootIdentity\":\"" + root.rootIdentity + "\",\"entries\":" + std::to_string(count) + "}");
return 0;
}
}
+2998 -2
View File
File diff suppressed because it is too large. Load diff
+122 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "ycode", "name": "ycode",
"version": "0.1.0", "version": "0.0.1",
"private": true, "private": true,
"description": "YCode - 个人 AI Agent 桌面应用(Electron)", "description": "YCode - 个人 AI Agent 桌面应用(Electron)",
"author": "YCode", "author": "YCode",
@@ -8,13 +8,46 @@
"scripts": { "scripts": {
"dev": "electron-vite dev", "dev": "electron-vite dev",
"dev:renderer": "electron-vite dev --rendererOnly", "dev:renderer": "electron-vite dev --rendererOnly",
"build": "npm run typecheck && electron-vite build", "build": "npm run sandbox:build && npm run typecheck && electron-vite build",
"sandbox:build": "node scripts/build-sandbox.mjs",
"sandbox:vendor-git": "node scripts/vendor-sandbox-git.mjs",
"test:sandbox": "npm run sandbox:build && node scripts/build-sandbox.mjs --probe && esbuild scripts/sandbox-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-regression.mjs --log-level=warning && node .tmp/sandbox-regression.mjs",
"test:sandbox:app": "npm run build && node scripts/verify-sandbox-app.cjs",
"test:sandbox:policy": "esbuild scripts/sandbox-policy-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-policy-regression.mjs --log-level=warning && node .tmp/sandbox-policy-regression.mjs",
"test:sandbox:toolchains": "npm run sandbox:build && esbuild scripts/sandbox-toolchain-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-toolchain-regression.mjs --log-level=warning && node .tmp/sandbox-toolchain-regression.mjs",
"test:sandbox:feasibility": "node scripts/build-sandbox.mjs --probe && esbuild scripts/sandbox-feasibility.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-feasibility.mjs --log-level=warning && node .tmp/sandbox-feasibility.mjs",
"test:sandbox:directories": "esbuild scripts/sandbox-directory-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-directory-regression.mjs --log-level=warning && node .tmp/sandbox-directory-regression.mjs",
"test:sandbox:write": "npm run sandbox:build && esbuild scripts/sandbox-write-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-write-regression.mjs --log-level=warning && node .tmp/sandbox-write-regression.mjs",
"test:sandbox:workspace": "npm run sandbox:build && esbuild scripts/sandbox-workspace-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-workspace-regression.mjs --log-level=warning && node .tmp/sandbox-workspace-regression.mjs",
"test:sandbox:stdio": "npm run sandbox:build && node scripts/build-sandbox.mjs --probe && esbuild scripts/sandbox-stdio-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-stdio-regression.mjs --log-level=warning && node .tmp/sandbox-stdio-regression.mjs",
"test:sandbox:read-bridge": "npm run sandbox:build && esbuild scripts/sandbox-read-bridge-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-read-bridge-regression.mjs --log-level=warning && node .tmp/sandbox-read-bridge-regression.mjs",
"test:sandbox:read-protocol": "esbuild scripts/sandbox-read-protocol-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-read-protocol-regression.mjs --log-level=warning && node .tmp/sandbox-read-protocol-regression.mjs",
"test:sandbox:process": "npm run sandbox:build && esbuild scripts/sandbox-process-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-process-regression.mjs --log-level=warning && node .tmp/sandbox-process-regression.mjs",
"test:sandbox:git": "npm run sandbox:build && esbuild scripts/sandbox-git-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-git-regression.mjs --log-level=warning && node .tmp/sandbox-git-regression.mjs",
"test:sandbox:git:prepare": "npm ci --prefix scripts/fixtures/git-transport --ignore-scripts --no-audit --no-fund",
"test:sandbox:git:complete": "npm run test:sandbox:git:prepare && npm run build && esbuild scripts/sandbox-git-complete-regression.ts scripts/sandbox-git-safety-regression.ts scripts/sandbox-git-remote-regression.ts --bundle --platform=node --format=esm --outdir=.tmp/git-complete-tests --out-extension:.js=.mjs --log-level=warning && node .tmp/git-complete-tests/sandbox-git-complete-regression.mjs && node .tmp/git-complete-tests/sandbox-git-safety-regression.mjs && node .tmp/git-complete-tests/sandbox-git-remote-regression.mjs && node scripts/verify-sandbox-app.cjs",
"test:sandbox:bundled-git": "npm run sandbox:build && esbuild scripts/sandbox-bundled-git-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-bundled-git-regression.mjs --log-level=warning && node .tmp/sandbox-bundled-git-regression.mjs",
"sandbox:maintain": "node scripts/sandbox-maintain.mjs",
"start": "electron-vite preview", "start": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json", "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"test:settings-performance": "node scripts/verify-settings-performance.cjs",
"models:sync-metadata": "node scripts/sync-model-metadata.mjs",
"test:custom-models": "esbuild scripts/custom-model-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/custom-model-regression.mjs --log-level=warning && node .tmp/custom-model-regression.mjs",
"test:custom-model-native": "esbuild scripts/custom-model-native-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/custom-model-native-regression.mjs --log-level=warning && node .tmp/custom-model-native-regression.mjs",
"test:model-metadata": "esbuild scripts/model-metadata-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/model-metadata-regression.mjs --log-level=warning && node .tmp/model-metadata-regression.mjs",
"test:custom-model-settings": "node scripts/verify-custom-model-settings.cjs",
"smoke": "esbuild scripts/smoke.ts --bundle --platform=node --format=esm --outfile=.tmp/smoke.mjs --log-level=warning && node .tmp/smoke.mjs", "smoke": "esbuild scripts/smoke.ts --bundle --platform=node --format=esm --outfile=.tmp/smoke.mjs --log-level=warning && node .tmp/smoke.mjs",
"test:omp": "esbuild scripts/omp-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/omp-regression.mjs --log-level=warning && node .tmp/omp-regression.mjs", "test:omp": "esbuild scripts/omp-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/omp-regression.mjs --log-level=warning && node .tmp/omp-regression.mjs",
"test:omp-session": "esbuild scripts/omp-session-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/omp-session-regression.mjs --log-level=warning && node .tmp/omp-session-regression.mjs", "test:omp-session": "esbuild scripts/omp-session-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/omp-session-regression.mjs --log-level=warning && node .tmp/omp-session-regression.mjs",
"test:chat": "esbuild scripts/message-order-regression.ts scripts/chat-store-regression.ts scripts/chat-stream-committer-regression.ts scripts/tool-output-regression.ts --bundle --platform=node --format=esm --out-extension:.js=.mjs --outdir=.tmp/chat-tests --log-level=warning && node .tmp/chat-tests/message-order-regression.mjs && node .tmp/chat-tests/chat-store-regression.mjs && node .tmp/chat-tests/chat-stream-committer-regression.mjs && node .tmp/chat-tests/tool-output-regression.mjs", "test:chat": "esbuild scripts/message-order-regression.ts scripts/chat-store-regression.ts scripts/chat-stream-committer-regression.ts scripts/tool-output-regression.ts --bundle --platform=node --format=esm --out-extension:.js=.mjs --outdir=.tmp/chat-tests --log-level=warning && node .tmp/chat-tests/message-order-regression.mjs && node .tmp/chat-tests/chat-store-regression.mjs && node .tmp/chat-tests/chat-stream-committer-regression.mjs && node .tmp/chat-tests/tool-output-regression.mjs",
"test:context-usage": "esbuild scripts/context-usage-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/context-usage-regression.mjs --log-level=error && node .tmp/context-usage-regression.mjs",
"test:browser-automation": "node scripts/browser-automation-regression.cjs",
"test:subagents": "esbuild scripts/subagent-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/subagent-regression.mjs --log-level=warning && node .tmp/subagent-regression.mjs",
"test:subagents:app": "node scripts/subagent-app-regression.cjs",
"test:subagents:display": "esbuild scripts/subagent-display-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/subagent-display-regression.mjs --log-level=warning && node .tmp/subagent-display-regression.mjs",
"test:subagents:ui": "node scripts/verify-subagent-display.cjs",
"test:context-observer": "esbuild scripts/context-observer-transport-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/context-observer-transport-regression.mjs --log-level=warning && node .tmp/context-observer-transport-regression.mjs",
"test:references": "esbuild scripts/reference-content-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/reference-content-regression.cjs --log-level=error && node .tmp/reference-content-regression.cjs",
"test:expert-usage": "esbuild scripts/expert-usage-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/expert-usage-regression.mjs --log-level=warning && node .tmp/expert-usage-regression.mjs", "test:expert-usage": "esbuild scripts/expert-usage-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/expert-usage-regression.mjs --log-level=warning && node .tmp/expert-usage-regression.mjs",
"test:markdown": "esbuild scripts/markdown-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/markdown-regression.cjs --log-level=warning && node .tmp/markdown-regression.cjs", "test:markdown": "esbuild scripts/markdown-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/markdown-regression.cjs --log-level=warning && node .tmp/markdown-regression.cjs",
"test:recovered": "esbuild scripts/recovered-parser-regression.ts scripts/source-projection-regression.ts scripts/code-block-regression.tsx scripts/incremental-markdown-regression.ts scripts/math-renderer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/recovered-tests --log-level=warning && node .tmp/recovered-tests/recovered-parser-regression.cjs && node .tmp/recovered-tests/source-projection-regression.cjs && node .tmp/recovered-tests/code-block-regression.cjs && node .tmp/recovered-tests/incremental-markdown-regression.cjs && node .tmp/recovered-tests/math-renderer-regression.cjs", "test:recovered": "esbuild scripts/recovered-parser-regression.ts scripts/source-projection-regression.ts scripts/code-block-regression.tsx scripts/incremental-markdown-regression.ts scripts/math-renderer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/recovered-tests --log-level=warning && node .tmp/recovered-tests/recovered-parser-regression.cjs && node .tmp/recovered-tests/source-projection-regression.cjs && node .tmp/recovered-tests/code-block-regression.cjs && node .tmp/recovered-tests/incremental-markdown-regression.cjs && node .tmp/recovered-tests/math-renderer-regression.cjs",
@@ -22,17 +55,36 @@
"test:persistence": "esbuild scripts/chat-persistence-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/chat-persistence-regression.mjs --log-level=warning && node .tmp/chat-persistence-regression.mjs", "test:persistence": "esbuild scripts/chat-persistence-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/chat-persistence-regression.mjs --log-level=warning && node .tmp/chat-persistence-regression.mjs",
"test:sessions": "esbuild scripts/session-store-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/session-store-regression.mjs --log-level=warning && node .tmp/session-store-regression.mjs", "test:sessions": "esbuild scripts/session-store-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/session-store-regression.mjs --log-level=warning && node .tmp/session-store-regression.mjs",
"shot": "node scripts/shot.mjs", "shot": "node scripts/shot.mjs",
"icon:build": "node scripts/gen-icon.mjs",
"dist": "npm run omp:vendor && npm run build && electron-builder --win dir",
"dist:installer": "npm run omp:vendor && npm run build && electron-builder --win nsis",
"verify:ui": "node scripts/verify-ui.mjs", "verify:ui": "node scripts/verify-ui.mjs",
"wb:capture": "node scripts/wb-capture.mjs 9223 .tmp/wb-capture", "wb:capture": "node scripts/wb-capture.mjs 9223 .tmp/wb-capture",
"test:media": "esbuild scripts/mermaid-regression.ts --bundle --platform=node --format=esm --packages=external --jsx=automatic --loader:.css=empty --outfile=.tmp/media-tests/mermaid-regression.mjs --log-level=warning && node .tmp/media-tests/mermaid-regression.mjs && esbuild scripts/image-renderer-regression.tsx scripts/path-renderer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/media-tests --log-level=warning && node .tmp/media-tests/image-renderer-regression.cjs && node .tmp/media-tests/path-renderer-regression.cjs", "test:media": "esbuild scripts/mermaid-regression.ts --bundle --platform=node --format=esm --packages=external --jsx=automatic --loader:.css=empty --outfile=.tmp/media-tests/mermaid-regression.mjs --log-level=warning && node .tmp/media-tests/mermaid-regression.mjs && esbuild scripts/image-renderer-regression.tsx scripts/path-renderer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/media-tests --log-level=warning && node .tmp/media-tests/image-renderer-regression.cjs && node .tmp/media-tests/path-renderer-regression.cjs",
"test:resources": "esbuild scripts/resource-bridge-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/resource-bridge-regression.mjs --log-level=warning && node .tmp/resource-bridge-regression.mjs", "test:resources": "esbuild scripts/resource-bridge-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/resource-bridge-regression.mjs --log-level=warning && node .tmp/resource-bridge-regression.mjs",
"recover:typography": "node scripts/rea/recover-typography-reference.mjs", "recover:typography": "node scripts/rea/recover-typography-reference.mjs",
"test:font-size": "esbuild scripts/font-size-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/font-size-regression.mjs --log-level=warning && node .tmp/font-size-regression.mjs", "test:font-size": "esbuild scripts/font-size-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/font-size-regression.mjs --log-level=warning && node .tmp/font-size-regression.mjs",
"test:conversation": "esbuild scripts/conversation-renderer-regression.tsx scripts/composer-regression.tsx scripts/conversation-runtime-regression.ts scripts/completed-fold-regression.tsx scripts/streaming-footer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=text --out-extension:.js=.cjs --outdir=.tmp/conversation-tests --log-level=error && node .tmp/conversation-tests/conversation-renderer-regression.cjs && node .tmp/conversation-tests/composer-regression.cjs && node .tmp/conversation-tests/conversation-runtime-regression.cjs && node .tmp/conversation-tests/streaming-footer-regression.cjs && node .tmp/conversation-tests/completed-fold-regression.cjs", "test:conversation": "esbuild scripts/conversation-renderer-regression.tsx scripts/composer-regression.tsx scripts/conversation-runtime-regression.ts scripts/completed-fold-regression.tsx scripts/streaming-footer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=text --out-extension:.js=.cjs --outdir=.tmp/conversation-tests --log-level=error && node .tmp/conversation-tests/conversation-renderer-regression.cjs && node .tmp/conversation-tests/composer-regression.cjs && node .tmp/conversation-tests/conversation-runtime-regression.cjs && node .tmp/conversation-tests/streaming-footer-regression.cjs && node .tmp/conversation-tests/completed-fold-regression.cjs",
"test:workbench-escape": "esbuild scripts/workbench-escape-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/workbench-escape-regression.mjs --log-level=warning && node .tmp/workbench-escape-regression.mjs",
"test:assistant-footer": "esbuild scripts/assistant-message-footer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/assistant-message-footer-regression.cjs --log-level=error && node .tmp/assistant-message-footer-regression.cjs", "test:assistant-footer": "esbuild scripts/assistant-message-footer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/assistant-message-footer-regression.cjs --log-level=error && node .tmp/assistant-message-footer-regression.cjs",
"test:omp-protocol": "esbuild scripts/omp-protocol-probe.ts --bundle --platform=node --format=esm --outfile=.tmp/omp-protocol-probe.mjs --log-level=warning && node .tmp/omp-protocol-probe.mjs", "test:omp-protocol": "esbuild scripts/omp-protocol-probe.ts --bundle --platform=node --format=esm --outfile=.tmp/omp-protocol-probe.mjs --log-level=warning && node .tmp/omp-protocol-probe.mjs",
"omp:vendor": "node scripts/vendor-omp.mjs",
"omp:verify": "node scripts/vendor-omp.mjs --verify",
"test:omp:bundled": "esbuild scripts/omp-bundled-path-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/omp-bundled-path-regression.mjs --log-level=warning && node .tmp/omp-bundled-path-regression.mjs && node scripts/verify-bundled-omp.cjs",
"test:conversation-ipc": "esbuild scripts/conversation-ipc-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/conversation-ipc-regression.mjs --log-level=warning && node .tmp/conversation-ipc-regression.mjs", "test:conversation-ipc": "esbuild scripts/conversation-ipc-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/conversation-ipc-regression.mjs --log-level=warning && node .tmp/conversation-ipc-regression.mjs",
"test:personalization": "esbuild scripts/personalization-prompt-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/personalization-prompt-regression.mjs --log-level=warning && node .tmp/personalization-prompt-regression.mjs", "test:personalization": "esbuild scripts/personalization-prompt-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/personalization-prompt-regression.mjs --log-level=warning && node .tmp/personalization-prompt-regression.mjs",
"test:memory": "node scripts/verify-memory-settings.cjs",
"test:remediation": "esbuild scripts/remediation-pure-regression.ts scripts/chat-completions-usage-regression.ts --bundle --platform=node --format=esm --out-extension:.js=.mjs --outdir=.tmp/remediation-tests --log-level=warning && node .tmp/remediation-tests/remediation-pure-regression.mjs && node .tmp/remediation-tests/chat-completions-usage-regression.mjs",
"test:task-window": "node scripts/verify-task-window-stream.cjs",
"test:archived-tasks": "node scripts/verify-archived-tasks.cjs",
"test:task-running": "node scripts/verify-task-running-menu.cjs",
"test:app-discover": "node scripts/verify-app-discover.cjs",
"test:automations": "esbuild scripts/automation-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/automation-regression.mjs --log-level=warning && node .tmp/automation-regression.mjs",
"test:library": "esbuild scripts/library-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/library-regression.mjs --log-level=warning && node .tmp/library-regression.mjs",
"test:library-ui": "esbuild scripts/library-ui-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --loader:.svg=dataurl --outfile=.tmp/library-ui-regression.cjs --log-level=warning && node .tmp/library-ui-regression.cjs",
"test:module-ui": "node scripts/verify-module-ui-parity.cjs",
"test:project-tasks": "esbuild scripts/project-task-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/project-task-regression.mjs --log-level=warning && node .tmp/project-task-regression.mjs",
"test:three-modules": "node scripts/verify-three-modules.cjs",
"recover:artifact-reference": "node scripts/rea/recover-artifact-reference.mjs", "recover:artifact-reference": "node scripts/rea/recover-artifact-reference.mjs",
"test:artifacts": "esbuild scripts/artifact-diff-regression.ts scripts/artifact-renderer-regression.tsx scripts/specialized-block-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/artifact-tests --log-level=warning && node .tmp/artifact-tests/artifact-diff-regression.cjs && node .tmp/artifact-tests/artifact-renderer-regression.cjs && node .tmp/artifact-tests/specialized-block-regression.cjs", "test:artifacts": "esbuild scripts/artifact-diff-regression.ts scripts/artifact-renderer-regression.tsx scripts/specialized-block-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/artifact-tests --log-level=warning && node .tmp/artifact-tests/artifact-diff-regression.cjs && node .tmp/artifact-tests/artifact-renderer-regression.cjs && node .tmp/artifact-tests/specialized-block-regression.cjs",
"recover:conversation-labels": "node scripts/rea/recover-conversation-labels.mjs", "recover:conversation-labels": "node scripts/rea/recover-conversation-labels.mjs",
@@ -48,8 +100,8 @@
"test:tool-virtualizer": "node scripts/rea/test-recovered-tool-virtualizer.mjs", "test:tool-virtualizer": "node scripts/rea/test-recovered-tool-virtualizer.mjs",
"verify:pierre": "node scripts/verify-pierre-diff.cjs", "verify:pierre": "node scripts/verify-pierre-diff.cjs",
"rea:visual:capture": "node scripts/rea/capture-css.mjs --visuals", "rea:visual:capture": "node scripts/rea/capture-css.mjs --visuals",
"recover:conversation-styles": "node scripts/rea/recover-conversation-styles.mjs && node scripts/rea/recover-composer-styles.mjs && node scripts/rea/recover-question-styles.mjs && node scripts/rea/recover-task-styles.mjs && node scripts/rea/recover-tool-result-styles.mjs && node scripts/rea/recover-artifact-styles.mjs", "recover:conversation-styles": "node scripts/rea/recover-conversation-styles.mjs && node scripts/rea/recover-composer-styles.mjs && node scripts/rea/recover-question-styles.mjs && node scripts/rea/recover-task-styles.mjs && node scripts/rea/recover-tool-result-styles.mjs && node scripts/rea/recover-artifact-styles.mjs",
"recover:streaming-footer": "node scripts/rea/recover-streaming-loading-tips.mjs && node scripts/rea/recover-streaming-footer-styles.mjs", "recover:streaming-footer": "node scripts/rea/recover-streaming-loading-tips.mjs && node scripts/rea/recover-streaming-footer-styles.mjs",
"recover:conversation-icons": "node scripts/rea/recover-action-icons.mjs && node scripts/rea/recover-runtime-icons.mjs && node scripts/rea/recover-question-icons.mjs && node scripts/rea/recover-approval-icon.mjs", "recover:conversation-icons": "node scripts/rea/recover-action-icons.mjs && node scripts/rea/recover-runtime-icons.mjs && node scripts/rea/recover-question-icons.mjs && node scripts/rea/recover-approval-icon.mjs",
"recover:modern-conversation-labels": "node scripts/rea/recover-modern-conversation-labels.mjs", "recover:modern-conversation-labels": "node scripts/rea/recover-modern-conversation-labels.mjs",
"test:modern-conversation-labels": "node scripts/rea/modern-conversation-labels-regression.mjs", "test:modern-conversation-labels": "node scripts/rea/modern-conversation-labels-regression.mjs",
@@ -78,6 +130,11 @@
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "^19.3.0", "react": "^19.3.0",
"react-dom": "^19.3.0", "react-dom": "^19.3.0",
"rrule": "^2.8.1",
"slate": "0.143.0",
"slate-dom": "0.142.0",
"slate-history": "0.140.0",
"slate-react": "0.142.0",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"yaml": "^2.9.1", "yaml": "^2.9.1",
"zustand": "^5.0.15" "zustand": "^5.0.15"
@@ -85,6 +142,7 @@
"devDependencies": { "devDependencies": {
"@babel/generator": "^8.0.6", "@babel/generator": "^8.0.6",
"@babel/parser": "^8.0.7", "@babel/parser": "^8.0.7",
"@resvg/resvg-js": "^2.6.2",
"@types/katex": "^0.16.8", "@types/katex": "^0.16.8",
"@types/markdown-it": "^14.2.0", "@types/markdown-it": "^14.2.0",
"@types/node": "^26.6.5", "@types/node": "^26.6.5",
@@ -94,9 +152,67 @@
"@types/svg-pan-zoom": "^3.3.0", "@types/svg-pan-zoom": "^3.3.0",
"@vitejs/plugin-react": "^6.1.2", "@vitejs/plugin-react": "^6.1.2",
"electron": "^37.10.3", "electron": "^37.10.3",
"electron-builder": "^26.15.3",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"postcss": "8.5.30", "postcss": "8.5.30",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.3.4" "vite": "^8.3.4"
},
"build": {
"appId": "com.ycode.app",
"productName": "YCode",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"out/**/*",
"package.json"
],
"npmRebuild": false,
"extraResources": [
{ "from": "resources/model-metadata", "to": "model-metadata", "filter": ["models.dev.json", "manifest.json", "LICENSE"] },
{
"from": "resources/omp",
"to": "omp",
"filter": ["omp.exe", "LICENSE", "THIRD-PARTY-NOTICES.txt", "runtime.lock.json"]
},
{
"from": "resources/sandbox/win32-x64",
"to": "sandbox/win32-x64",
"filter": [
"ycode-sandbox.exe",
"node-bridge.cjs",
"node-process.cjs",
"python-sitecustomize.py",
"git/**/*",
"manifest.json"
]
},
{
"from": "resources/icon.png",
"to": "icon.png"
},
{
"from": "resources/tray.png",
"to": "tray.png"
}
],
"win": {
"icon": "build/icon.ico",
"artifactName": "YCode-v0.01-Setup-${arch}.${ext}",
"target": [
"dir"
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "YCode"
}
} }
} }
+26
View File
@@ -0,0 +1,26 @@
# 通用开发角色
`general-development.json` 是当前五类可选开发角色的定义来源,包含双语显示信息、触发描述、快捷提问、共同约束与实际角色指令。
| 职责 | 定位 |
| --- | --- |
| 代码探索员 | 只读梳理代码路径、约束与影响范围 |
| 实现工程师 | 在明确接口和文件归属下实施改动 |
| 测试验收员 | 验证实际行为,报告复现、结果及未覆盖项 |
| 代码审查员 | 独立、只读检查真实缺陷与回归 |
| 问题诊断员 | 复现异常、验证假设、定位根因,获准后修复 |
从仓库根目录运行安装/更新脚本,必须明确指定YCode用户数据目录:
```powershell
node scripts/configure-general-agents.mjs --user-data 'C:\Users\Administrator\AppData\Roaming\YCode'
node scripts/configure-general-agents.mjs --user-data 'C:\Users\Administrator\AppData\Roaming\YCode' --check
```
脚本通过生产专家包API校验、注册、激活和构造回合上下文。变更前在该用户数据目录的 `capabilities/expert-backups/` 备份完整原包及目录清单,保留原有四个包ID和Agent文件名,避免破坏已有选择引用。第五个诊断角色使用新ID。
角色默认不预加载专项技能。五个角色各有一张统一机器人风格的头像,SVG源文件和256px PNG保存在`avatars/`;维护源图时运行`node scripts/generate-general-agent-avatars.mjs`,再执行上面的配置更新。安装脚本在备份后注册文本,并单独原子写入已校验的PNG;`--check`同时检查头像字节和激活数据,重复执行不会重装一致的包。
提示词中的只读职责与编辑范围不构成独立的权限隔离;实际工具权限由宿主控制。
这些定义当前对应“我的专家”中的可手动召唤Agent。此脚本不修改子任务调度协议、并发上限、模型配置或工具授权,也不表示调度器已经支持按这里的角色自动选人。
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="debugger-bg" x2=".8" y2="1"><stop stop-color="#F7E9E6"/><stop offset="1" stop-color="#DBADA5"/></linearGradient>
<linearGradient id="debugger-shell" x2=".3" y2="1"><stop stop-color="#FFFFFF"/><stop offset="1" stop-color="#F5DDD6"/></linearGradient>
<linearGradient id="debugger-screen" x2=".7" y2="1"><stop stop-color="#533730"/><stop offset="1" stop-color="#172A31"/></linearGradient>
<clipPath id="debugger-clip"><rect width="256" height="256" rx="48"/></clipPath>
</defs>
<g clip-path="url(#debugger-clip)">
<rect width="256" height="256" fill="url(#debugger-bg)"/>
<circle cx="128" cy="95" r="94" fill="#FFFFFF" opacity=".2"/>
<ellipse cx="128" cy="254" rx="94" ry="20" fill="#B6786C" opacity=".16"/>
<path d="M48 256v-23c0-31 32-49 80-49s80 18 80 49v23" fill="#B6786C"/>
<path d="M62 256v-23c0-24 26-40 66-40s66 16 66 40v23" fill="url(#debugger-shell)"/>
<path d="M106 175h44v24c-11 9-33 9-44 0z" fill="#DBADA5"/>
<path d="M128 55V34" stroke="#B6786C" stroke-width="8" stroke-linecap="round"/>
<circle cx="128" cy="30" r="10" fill="#B6786C"/>
<circle cx="125" cy="27" r="4" fill="#FFFFFF" opacity=".7"/>
<rect x="43" y="97" width="22" height="44" rx="11" fill="#B6786C"/>
<rect x="191" y="97" width="22" height="44" rx="11" fill="#B6786C"/>
<rect x="54" y="55" width="148" height="128" rx="42" fill="#B6786C" opacity=".18" transform="translate(0 5)"/>
<rect x="54" y="51" width="148" height="128" rx="42" fill="url(#debugger-shell)"/>
<path d="M78 72c12-8 25-10 50-10" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<rect x="69" y="87" width="118" height="66" rx="25" fill="url(#debugger-screen)"/>
<rect x="87" y="105" width="16" height="23" rx="8" fill="#F5DDD6"/>
<rect x="153" y="105" width="16" height="23" rx="8" fill="#F5DDD6"/>
<path d="M119 136q9 6 18 0" fill="none" stroke="#F5DDD6" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="79" cy="137" r="5" fill="#DBADA5" opacity=".55"/>
<circle cx="177" cy="137" r="5" fill="#DBADA5" opacity=".55"/>
<path d="M102 225h52" stroke="#DBADA5" stroke-width="7" stroke-linecap="round"/>
<path d="M114 239h28" stroke="#DBADA5" stroke-width="5" stroke-linecap="round" opacity=".65"/>
<circle cx="205" cy="204" r="29" fill="#FFFFFF" opacity=".28"/>
<circle cx="205" cy="203" r="25" fill="#533730"/>
<g fill="none" stroke="#F5DDD6" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M191 204h7l4-10 7 19 4-9h7"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="explorer-bg" x2=".8" y2="1"><stop stop-color="#E4F3ED"/><stop offset="1" stop-color="#9DCCB9"/></linearGradient>
<linearGradient id="explorer-shell" x2=".3" y2="1"><stop stop-color="#FFFFFF"/><stop offset="1" stop-color="#D8EEE4"/></linearGradient>
<linearGradient id="explorer-screen" x2=".7" y2="1"><stop stop-color="#1D3E37"/><stop offset="1" stop-color="#172A31"/></linearGradient>
<clipPath id="explorer-clip"><rect width="256" height="256" rx="48"/></clipPath>
</defs>
<g clip-path="url(#explorer-clip)">
<rect width="256" height="256" fill="url(#explorer-bg)"/>
<circle cx="128" cy="95" r="94" fill="#FFFFFF" opacity=".2"/>
<ellipse cx="128" cy="254" rx="94" ry="20" fill="#558E79" opacity=".16"/>
<path d="M48 256v-23c0-31 32-49 80-49s80 18 80 49v23" fill="#558E79"/>
<path d="M62 256v-23c0-24 26-40 66-40s66 16 66 40v23" fill="url(#explorer-shell)"/>
<path d="M106 175h44v24c-11 9-33 9-44 0z" fill="#9DCCB9"/>
<path d="M128 55V34" stroke="#558E79" stroke-width="8" stroke-linecap="round"/>
<circle cx="128" cy="30" r="10" fill="#558E79"/>
<circle cx="125" cy="27" r="4" fill="#FFFFFF" opacity=".7"/>
<rect x="43" y="97" width="22" height="44" rx="11" fill="#558E79"/>
<rect x="191" y="97" width="22" height="44" rx="11" fill="#558E79"/>
<rect x="54" y="55" width="148" height="128" rx="42" fill="#558E79" opacity=".18" transform="translate(0 5)"/>
<rect x="54" y="51" width="148" height="128" rx="42" fill="url(#explorer-shell)"/>
<path d="M78 72c12-8 25-10 50-10" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<rect x="69" y="87" width="118" height="66" rx="25" fill="url(#explorer-screen)"/>
<rect x="87" y="105" width="16" height="23" rx="8" fill="#D8EEE4"/>
<rect x="153" y="105" width="16" height="23" rx="8" fill="#D8EEE4"/>
<path d="M119 136q9 6 18 0" fill="none" stroke="#D8EEE4" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="79" cy="137" r="5" fill="#9DCCB9" opacity=".55"/>
<circle cx="177" cy="137" r="5" fill="#9DCCB9" opacity=".55"/>
<path d="M102 225h52" stroke="#9DCCB9" stroke-width="7" stroke-linecap="round"/>
<path d="M114 239h28" stroke="#9DCCB9" stroke-width="5" stroke-linecap="round" opacity=".65"/>
<circle cx="205" cy="204" r="29" fill="#FFFFFF" opacity=".28"/>
<circle cx="205" cy="203" r="25" fill="#1D3E37"/>
<g fill="none" stroke="#D8EEE4" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="204" cy="202" r="8"/><path d="m210 208 8 8"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="implementer-bg" x2=".8" y2="1"><stop stop-color="#E7EFFB"/><stop offset="1" stop-color="#A6BCE2"/></linearGradient>
<linearGradient id="implementer-shell" x2=".3" y2="1"><stop stop-color="#FFFFFF"/><stop offset="1" stop-color="#E0EBFA"/></linearGradient>
<linearGradient id="implementer-screen" x2=".7" y2="1"><stop stop-color="#253D60"/><stop offset="1" stop-color="#172A31"/></linearGradient>
<clipPath id="implementer-clip"><rect width="256" height="256" rx="48"/></clipPath>
</defs>
<g clip-path="url(#implementer-clip)">
<rect width="256" height="256" fill="url(#implementer-bg)"/>
<circle cx="128" cy="95" r="94" fill="#FFFFFF" opacity=".2"/>
<ellipse cx="128" cy="254" rx="94" ry="20" fill="#668CC3" opacity=".16"/>
<path d="M48 256v-23c0-31 32-49 80-49s80 18 80 49v23" fill="#668CC3"/>
<path d="M62 256v-23c0-24 26-40 66-40s66 16 66 40v23" fill="url(#implementer-shell)"/>
<path d="M106 175h44v24c-11 9-33 9-44 0z" fill="#A6BCE2"/>
<path d="M128 55V34" stroke="#668CC3" stroke-width="8" stroke-linecap="round"/>
<circle cx="128" cy="30" r="10" fill="#668CC3"/>
<circle cx="125" cy="27" r="4" fill="#FFFFFF" opacity=".7"/>
<rect x="43" y="97" width="22" height="44" rx="11" fill="#668CC3"/>
<rect x="191" y="97" width="22" height="44" rx="11" fill="#668CC3"/>
<rect x="54" y="55" width="148" height="128" rx="42" fill="#668CC3" opacity=".18" transform="translate(0 5)"/>
<rect x="54" y="51" width="148" height="128" rx="42" fill="url(#implementer-shell)"/>
<path d="M78 72c12-8 25-10 50-10" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<rect x="69" y="87" width="118" height="66" rx="25" fill="url(#implementer-screen)"/>
<rect x="87" y="105" width="16" height="23" rx="8" fill="#E0EBFA"/>
<rect x="153" y="105" width="16" height="23" rx="8" fill="#E0EBFA"/>
<path d="M119 136q9 6 18 0" fill="none" stroke="#E0EBFA" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="79" cy="137" r="5" fill="#A6BCE2" opacity=".55"/>
<circle cx="177" cy="137" r="5" fill="#A6BCE2" opacity=".55"/>
<path d="M102 225h52" stroke="#A6BCE2" stroke-width="7" stroke-linecap="round"/>
<path d="M114 239h28" stroke="#A6BCE2" stroke-width="5" stroke-linecap="round" opacity=".65"/>
<circle cx="205" cy="204" r="29" fill="#FFFFFF" opacity=".28"/>
<circle cx="205" cy="203" r="25" fill="#253D60"/>
<g fill="none" stroke="#E0EBFA" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m199 195-7 8 7 8m13-16 7 8-7 8m-7-19-3 22"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="reviewer-bg" x2=".8" y2="1"><stop stop-color="#EDEAF7"/><stop offset="1" stop-color="#B9AADC"/></linearGradient>
<linearGradient id="reviewer-shell" x2=".3" y2="1"><stop stop-color="#FFFFFF"/><stop offset="1" stop-color="#E9E1F6"/></linearGradient>
<linearGradient id="reviewer-screen" x2=".7" y2="1"><stop stop-color="#3D3258"/><stop offset="1" stop-color="#172A31"/></linearGradient>
<clipPath id="reviewer-clip"><rect width="256" height="256" rx="48"/></clipPath>
</defs>
<g clip-path="url(#reviewer-clip)">
<rect width="256" height="256" fill="url(#reviewer-bg)"/>
<circle cx="128" cy="95" r="94" fill="#FFFFFF" opacity=".2"/>
<ellipse cx="128" cy="254" rx="94" ry="20" fill="#8770B5" opacity=".16"/>
<path d="M48 256v-23c0-31 32-49 80-49s80 18 80 49v23" fill="#8770B5"/>
<path d="M62 256v-23c0-24 26-40 66-40s66 16 66 40v23" fill="url(#reviewer-shell)"/>
<path d="M106 175h44v24c-11 9-33 9-44 0z" fill="#B9AADC"/>
<path d="M128 55V34" stroke="#8770B5" stroke-width="8" stroke-linecap="round"/>
<circle cx="128" cy="30" r="10" fill="#8770B5"/>
<circle cx="125" cy="27" r="4" fill="#FFFFFF" opacity=".7"/>
<rect x="43" y="97" width="22" height="44" rx="11" fill="#8770B5"/>
<rect x="191" y="97" width="22" height="44" rx="11" fill="#8770B5"/>
<rect x="54" y="55" width="148" height="128" rx="42" fill="#8770B5" opacity=".18" transform="translate(0 5)"/>
<rect x="54" y="51" width="148" height="128" rx="42" fill="url(#reviewer-shell)"/>
<path d="M78 72c12-8 25-10 50-10" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<rect x="69" y="87" width="118" height="66" rx="25" fill="url(#reviewer-screen)"/>
<rect x="87" y="105" width="16" height="23" rx="8" fill="#E9E1F6"/>
<rect x="153" y="105" width="16" height="23" rx="8" fill="#E9E1F6"/>
<path d="M119 136q9 6 18 0" fill="none" stroke="#E9E1F6" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="79" cy="137" r="5" fill="#B9AADC" opacity=".55"/>
<circle cx="177" cy="137" r="5" fill="#B9AADC" opacity=".55"/>
<path d="M102 225h52" stroke="#B9AADC" stroke-width="7" stroke-linecap="round"/>
<path d="M114 239h28" stroke="#B9AADC" stroke-width="5" stroke-linecap="round" opacity=".65"/>
<circle cx="205" cy="204" r="29" fill="#FFFFFF" opacity=".28"/>
<circle cx="205" cy="203" r="25" fill="#3D3258"/>
<g fill="none" stroke="#E9E1F6" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m205 190 13 5v9c0 8-8 13-13 15-5-2-13-7-13-15v-9z"/><path d="m199 203 4 4 8-9"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="verifier-bg" x2=".8" y2="1"><stop stop-color="#FAF0DE"/><stop offset="1" stop-color="#E6C384"/></linearGradient>
<linearGradient id="verifier-shell" x2=".3" y2="1"><stop stop-color="#FFFFFF"/><stop offset="1" stop-color="#F8E9CB"/></linearGradient>
<linearGradient id="verifier-screen" x2=".7" y2="1"><stop stop-color="#55412B"/><stop offset="1" stop-color="#172A31"/></linearGradient>
<clipPath id="verifier-clip"><rect width="256" height="256" rx="48"/></clipPath>
</defs>
<g clip-path="url(#verifier-clip)">
<rect width="256" height="256" fill="url(#verifier-bg)"/>
<circle cx="128" cy="95" r="94" fill="#FFFFFF" opacity=".2"/>
<ellipse cx="128" cy="254" rx="94" ry="20" fill="#B98B42" opacity=".16"/>
<path d="M48 256v-23c0-31 32-49 80-49s80 18 80 49v23" fill="#B98B42"/>
<path d="M62 256v-23c0-24 26-40 66-40s66 16 66 40v23" fill="url(#verifier-shell)"/>
<path d="M106 175h44v24c-11 9-33 9-44 0z" fill="#E6C384"/>
<path d="M128 55V34" stroke="#B98B42" stroke-width="8" stroke-linecap="round"/>
<circle cx="128" cy="30" r="10" fill="#B98B42"/>
<circle cx="125" cy="27" r="4" fill="#FFFFFF" opacity=".7"/>
<rect x="43" y="97" width="22" height="44" rx="11" fill="#B98B42"/>
<rect x="191" y="97" width="22" height="44" rx="11" fill="#B98B42"/>
<rect x="54" y="55" width="148" height="128" rx="42" fill="#B98B42" opacity=".18" transform="translate(0 5)"/>
<rect x="54" y="51" width="148" height="128" rx="42" fill="url(#verifier-shell)"/>
<path d="M78 72c12-8 25-10 50-10" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<rect x="69" y="87" width="118" height="66" rx="25" fill="url(#verifier-screen)"/>
<rect x="87" y="105" width="16" height="23" rx="8" fill="#F8E9CB"/>
<rect x="153" y="105" width="16" height="23" rx="8" fill="#F8E9CB"/>
<path d="M119 136q9 6 18 0" fill="none" stroke="#F8E9CB" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="79" cy="137" r="5" fill="#E6C384" opacity=".55"/>
<circle cx="177" cy="137" r="5" fill="#E6C384" opacity=".55"/>
<path d="M102 225h52" stroke="#E6C384" stroke-width="7" stroke-linecap="round"/>
<path d="M114 239h28" stroke="#E6C384" stroke-width="5" stroke-linecap="round" opacity=".65"/>
<circle cx="205" cy="204" r="29" fill="#FFFFFF" opacity=".28"/>
<circle cx="205" cy="203" r="25" fill="#55412B"/>
<g fill="none" stroke="#F8E9CB" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m194 203 8 8 15-17"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+127
View File
@@ -0,0 +1,127 @@
{
"version": 2,
"commonInstructions": [
"你是 YCode 的通用开发角色。根据当前仓库、技术栈和用户目标工作,不预设编程语言、框架、行业、项目目录或外部服务。",
"先阅读适用的项目约定,使用真实文件、运行记录和明确输入作为依据;区分已确认事实、推测、已实施和未验证内容。",
"作为委派成员时,只处理主智能体明确下发的任务,遵守允许修改的文件范围,不扩大目标,不创建嵌套子任务;整体需求、架构取舍、冲突处理与最终验收由主智能体负责。",
"只使用宿主实际提供的工具和权限;角色说明不授予额外能力。需要但没有命令、浏览器、联网或其他工具时,明确报告主智能体继续处理,不虚构操作或结果。",
"专业技能仅在任务确有需要且实际可用时按需读取,不默认加载语言、文档格式、设计系统或算法专项技能。",
"报告保持简洁,包含检查或修改的文件、关键结论与证据、实际验证及结果、遗留限制和需要主智能体处理的事项。"
],
"agents": [
{
"role": "explorer",
"avatar": "./avatars/explorer.png",
"id": "senior-project-manager",
"agentName": "senior-project-manager",
"displayName": { "zh": "探索", "en": "Explorer" },
"profession": { "zh": "代码探索员", "en": "Code Explorer" },
"description": "Read-only codebase exploration: locate entry points, trace calls and state transitions, and identify constraints and affected files before implementation.",
"displayDescription": { "zh": "负责梳理任意代码库的结构、调用链、关键约束与改动影响,基于文件证据说明结论,默认只读。", "en": "Maps code structure, execution paths, constraints and impact with file evidence. Read-only by default." },
"tags": [{ "zh": "代码定位", "en": "Code Mapping" }, { "zh": "调用链", "en": "Call Paths" }, { "zh": "影响范围", "en": "Impact Analysis" }],
"quickPrompts": [
{ "zh": "请梳理当前需求涉及的代码路径、关键约束和改动影响,给出文件依据,先不修改代码。", "en": "Map the code paths, constraints and impact of the current requirement with file references, without editing code." },
{ "zh": "请定位当前功能从入口到执行结果的完整调用链与状态变化。", "en": "Trace the current feature from entry point to result, including state transitions." },
{ "zh": "请检查已有实现、可复用模块和测试入口,列出尚不明确的事项。", "en": "Identify existing implementations, reusable modules, test entry points and unresolved questions." }
],
"instructions": [
"职责是搜集和解释代码事实,为后续实现提供有依据的地图。接手仓库、定位功能或评估影响时调用。",
"默认只读,不编辑代码、配置、文档或测试,不执行会改变仓库状态的操作。",
"优先定向搜索和必要文件读取,追踪真实入口、调用关系、数据与状态流、错误处理及测试入口;避免逐文件漫游或堆砌整段源码。",
"输出关键文件与符号、执行路径、约束和影响范围;未知项说明缺少什么证据。方案取舍交给主智能体,不擅自开始实现。"
]
},
{
"role": "implementer",
"avatar": "./avatars/implementer.png",
"id": "cpp-super-expert",
"agentName": "cpp-super-expert",
"displayName": { "zh": "实现", "en": "Implementer" },
"profession": { "zh": "实现工程师", "en": "Implementation Engineer" },
"description": "Implement a clearly scoped feature or refactor after requirements and interfaces are agreed; modify only assigned files and validate changed behavior.",
"displayDescription": { "zh": "负责按明确需求完成跨语言、跨框架的代码实现与局部重构,遵守文件边界,交付可验证的完整改动。", "en": "Implements scoped features and refactors across languages and frameworks, with explicit file ownership and validation." },
"tags": [{ "zh": "功能实现", "en": "Implementation" }, { "zh": "局部重构", "en": "Scoped Refactoring" }, { "zh": "行为验证", "en": "Validation" }],
"quickPrompts": [
{ "zh": "请按当前需求实现最小完整改动,遵守项目约定,验证相关行为并说明结果。", "en": "Implement the smallest complete change for the current requirement, follow project conventions and validate affected behavior." },
{ "zh": "请在约定的文件范围内完成这个模块,并保持已确认的接口和兼容行为。", "en": "Implement this module within the assigned files while preserving agreed interfaces and compatibility." },
{ "zh": "请对指定代码做有明确目标的局部重构,并验证行为没有意外改变。", "en": "Perform a targeted refactor of the specified code and verify behavior has not changed unexpectedly." }
],
"instructions": [
"职责是交付已明确目标的实现。需求、接口或文件归属存在阻断性歧义时,先将具体问题报告主智能体;可以继续不依赖该决定的工作。",
"只修改获准的文件,保留已有用户改动。跨模块接口、公共契约、依赖升级和范围扩展由主智能体决定。",
"优先复用现有结构和组件;改动应覆盖必要的错误处理与兼容路径,避免无关重构。",
"运行与改动相称的验证;必要时补充能发现真实行为回归的测试,不为低影响改动堆砌重复断言。",
"交付文件清单、行为变化、验证命令和实际结果;未运行的检查与仍待集成的部分明确标注。"
]
},
{
"role": "verifier",
"avatar": "./avatars/verifier.png",
"id": "ui-designer",
"agentName": "ui-designer",
"displayName": { "zh": "验收", "en": "Verifier" },
"profession": { "zh": "测试验收员", "en": "Test and Acceptance Engineer" },
"description": "Validate implemented behavior using relevant tests and reproducible acceptance steps; report failures, actual results and coverage gaps independently from implementation.",
"displayDescription": { "zh": "负责围绕当前改动执行适当测试和实际行为验收,覆盖关键路径与边界条件,清楚报告结果及未覆盖项。", "en": "Checks implemented behavior with focused tests, reproducible acceptance steps and explicit coverage limits." },
"tags": [{ "zh": "测试执行", "en": "Test Execution" }, { "zh": "行为验收", "en": "Acceptance" }, { "zh": "边界回归", "en": "Regression" }],
"quickPrompts": [
{ "zh": "请围绕本次改动执行适当测试与实际行为验收,给出复现步骤、运行结果和未覆盖项。", "en": "Validate this change with appropriate tests and behavior checks; report steps, actual results and untested areas." },
{ "zh": "请检查关键路径、边界条件和失败恢复,整理能够复现的问题。", "en": "Check critical paths, edge cases and failure recovery, and document reproducible issues." },
{ "zh": "请核对现有测试是否覆盖本次行为变化,只补充确有必要的回归验证。", "en": "Check whether existing tests cover the behavior change and add only necessary regression coverage." }
],
"instructions": [
"职责是独立验证实际行为是否满足验收条件,不以实现者的完成声明代替证据。",
"先选择与改动有关的已有测试和实际操作路径,覆盖正常、异常、取消与恢复等适用状态;根据具体任务决定是否需要界面、接口或持久化检查。",
"只有获准时才补充或修改指定测试文件;发现产品缺陷时记录问题并交给实现者或主智能体,不通过放宽断言、跳过失败或改业务代码制造通过。",
"记录真实命令、退出状态、关键输出与复现步骤;构建通过、mock通过和真实端到端验收分别报告。",
"工具不可用、依赖缺失或条件不具备时,报告阻碍和尚未验收的范围;不将计划执行写成已经执行。"
]
},
{
"role": "reviewer",
"avatar": "./avatars/reviewer.png",
"id": "senior-algorithm-expert",
"agentName": "senior-algorithm-engineer",
"displayName": { "zh": "审查", "en": "Reviewer" },
"profession": { "zh": "代码审查员", "en": "Code Reviewer" },
"description": "Independently review a change for concrete correctness bugs, regressions, concurrency risks and missing tests; return evidence-based findings without editing files.",
"displayDescription": { "zh": "负责独立审查代码正确性、回归风险、边界处理和测试缺口,提供可复现的问题与文件依据,默认只读。", "en": "Independently reviews correctness, regressions, edge cases and test gaps with reproducible findings. Read-only by default." },
"tags": [{ "zh": "正确性", "en": "Correctness" }, { "zh": "回归风险", "en": "Regression Risks" }, { "zh": "独立审查", "en": "Independent Review" }],
"quickPrompts": [
{ "zh": "请独立审查当前改动,优先检查正确性、回归、边界和测试缺口,给出问题及文件依据,先不修改代码。", "en": "Independently review the current change for correctness, regressions, edge cases and test gaps, with file evidence and no edits." },
{ "zh": "请沿真实调用路径检查这次修改是否引入状态、并发或资源管理问题。", "en": "Trace actual execution paths for state, concurrency and resource-lifetime problems introduced by this change." },
{ "zh": "请复核这些审查发现,确认触发条件和影响,剔除没有依据的推测。", "en": "Verify these review findings, confirm triggers and impact, and discard unsupported speculation." }
],
"instructions": [
"职责是独立发现有依据的缺陷。默认只读,不编辑产品代码、配置、文档或测试,不代替实现者修复。",
"先理解改动目标与差异,再沿相关调用路径核对兼容性、边界处理、状态一致性、资源生命周期和测试覆盖。涉及权限、数据或性能的改动按实际影响检查。",
"每个发现给出严重程度、文件位置、具体触发条件、可复现路径或证据和实际影响;区分确认缺陷与待验证风险。",
"优先报告正确性和回归问题,避免纯风格偏好、无关重构建议与假设性告警。没有确认问题时如实说明审查范围和剩余验证缺口。",
"修复是否采用及如何集成由主智能体决定,不扩大审查目标或改写已有任务。"
]
},
{
"role": "debugger",
"avatar": "./avatars/debugger.png",
"id": "development-debugger",
"agentName": "development-debugger",
"displayName": { "zh": "诊断", "en": "Debugger" },
"profession": { "zh": "问题诊断员", "en": "Root Cause Investigator" },
"description": "Investigate a concrete error, failed test, hang or unexpected behavior; reproduce it, test hypotheses and establish the root cause before a minimal fix.",
"displayDescription": { "zh": "负责复现异常、分析日志和状态变化并定位根因,区分事实与假设,给出最小修复方案和可执行验证步骤。", "en": "Reproduces failures, tests hypotheses and identifies root causes before proposing a minimal fix and verification." },
"tags": [{ "zh": "问题复现", "en": "Reproduction" }, { "zh": "根因定位", "en": "Root Cause" }, { "zh": "修复验证", "en": "Fix Verification" }],
"quickPrompts": [
{ "zh": "请调查当前问题,先复现并用证据定位根因,再提出最小修复与验证方案。", "en": "Investigate this issue, reproduce it and establish the root cause with evidence before proposing a minimal fix and validation." },
{ "zh": "请分析这段报错、日志或失败记录,区分直接现象、根因和仍需验证的假设。", "en": "Analyze this error, log or failure record; distinguish symptoms, root causes and hypotheses requiring verification." },
{ "zh": "请定位这个偶发异常的触发条件与状态变化,给出能够验证判断的复现方法。", "en": "Identify the triggers and state transitions behind this intermittent failure and provide a reproducible validation method." }
],
"instructions": [
"职责是调查已经出现的具体异常,与代码探索的结构梳理和代码审查的改动检查区分。先定位根因,再考虑修复。",
"收集报错、堆栈、日志、环境和最近改动,建立最短可行复现步骤;每个假设说明支持证据及可证伪的检查方法。",
"跟踪真实数据与状态变化,避免凭现象直接修改代码或只掩盖错误。未复现时明确已排除什么、还缺少什么。",
"默认只做诊断和修复建议;只有主智能体明确授权修复且指定文件范围时才编辑,采用最小改动并验证原问题与相关回归。",
"交付复现条件、根因证据、修复建议或实际改动、验证结果及未解决事项。"
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 models.dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loaded 100 of 434 files, more files were not shown because too many files have changed in this diff. Show more