Feature: Walk-away Workbench 走开即工作台 (web-terminal v0.7)
状态 / Status: 提案 / Draft(探索完成,已并入两个 Band 的草案并应用评审意见,待评审后进入 PLAN)
目标 / Goal: v0.7 把 web-terminal 从"能远程看的终端"推进成一个走开即工作台 (Walk-away Workbench),分两个 Band:
- Band A — 闭合"走开"循环 (Finish the walk-away loop):主机主动找到你的手机(Web Push / ntfy),让你在锁屏上批准/拒绝一个被 hold 住的工具权限(不开 app),并把移动端输入做得更快(语音、快捷回复),加上"我离开时发生了什么"时间线,和"会话卡住了"主动告警。
- Band B — 在终端之上长出工作台 (Become a workbench above the terminal):每个会话的只读 git diff/review、statusLine 遥测(成本/上下文/PR/额度仪表)、从 UI 建 git worktree(真并行不互踩)、plan-mode/权限模式中继(启动时选模式 + plan 门三选一)。
- 关键结论 / Key finding:9 个特性几乎全部是两条既有侧信道的延伸——(1) loopback hook 侧信道(
POST /hook、held POST /hook/permission,src/server.ts:261-303)与已安装的 PWA(public/sw.js、public/manifest.webmanifest);(2) 安全 git-exec 模式(execFileAsync('git', …),无 shell,src/http/projects.ts)。服务端依旧不解析一个终端字节——新增的只有 git 子进程、带外 JSON、和一个出站的推送签名。B3(建 worktree)是唯一写盘特性,必须挂 Origin/CSRF 守卫;A1 的锁屏审批是本版本最大的正确性改动(要改 /hook/permission 的"无客户端不 hold"门,见 §A1)。
0. 文档导航 (Section Map)
| # |
章节 |
对应需求 |
| 1 |
概览、目标、非目标 (Overview / Goals / Non-Goals) |
必备 (1) |
| 2 |
共享设计原则 (Shared Design Principles) |
必备 (3) |
| 3 |
Band A — A1…A5(各含用户故事 / FR 表 / 设计与数据流 / 安全 / 配置 / 验收) |
必备 (2) |
| 4 |
Band B — B1…B4(同上结构) |
必备 (2) |
| 5 |
合并安全章节 (Consolidated Security) |
必备 (4) |
| 6 |
合并配置 env 表 (Consolidated Config) |
必备 (5) |
| 7 |
优先级与跨特性依赖排程 (Prioritization & Dependency Ordering) |
必备 (6) |
| 8 |
验收标准汇总 (Acceptance-Criteria Summary) |
必备 (7) |
| 9 |
一句话总结 |
— |
1. 概览、目标与非目标 (Overview, Goals & Non-Goals)
1.1 问题陈述 (Problem)
今天的核心用例是 vibe coding:派 Claude Code 一个任务、走开、回来从任意设备看进展。v0.3–v0.6 已经做到"会话不随断连而死""多设备镜像""缩略图墙""项目面板""远程 approve/reject"。但循环仍有两道缺口:
- 走开后没人叫你——会话需要批准工具或跑完了,除非你盯着 tab,否则不知道(A 段补齐"主机主动找你 + 锁屏直接处理")。
- 终端之上没有工作台——回看 Claude 改了什么只能滚字节缓冲;看不到这次烧了多少钱/上下文还剩多少;多 agent 并行会踩同一个工作树(B 段把这些做成结构化的、带外的视图与受控写操作)。
1.2 目标 (Goals)
- 闭合走开循环:NEEDS-INPUT / DONE / STUCK 三类信号主动推到手机;锁屏 Allow/Deny 即可让 Claude 继续,全程不开 app。
- 更快的移动输入:语音口述、快捷回复 chips、可保存的提示词调色板。
- 离开期回放:每会话的人话时间线("跑了 Bash · 改了 3 个文件 · 等批准 · 完成")。
- 终端之上的工作台:只读 diff/review、per-tab 遥测仪表、从 UI 建 worktree、plan-mode 中继。
- 不破坏护城河:自托管、不经云、byte-shuttle、LAN-only(Tailscale 推荐)、config 无硬编码、immutable、TDD ≥80%。
1.3 非目标 (Non-Goals,全 Band 通用)
- 不加登录/鉴权/多用户隔离(属未来 Band C)。A1 引入的"按 pending 决策的能力 token"只是让锁屏 Allow/Deny 安全的最小手段,不是应用级 auth。
- 不在服务端解析终端语义 / ANSI——所有新信号都是带外 JSON 或 git 子进程输出,xterm.js 仍是唯一的终端解释者。
- 不做服务端语音转写 / 文件编辑器 / 行级回评 agent / worktree 看板 / 跨会话成本汇总 / 推送任意自定义消息(各特性 §"暂不做"细列)。
- 不把本服务暴露到公网。Band A1 的 Web Push 需要 HTTPS——通过 Tailscale Serve / TLS 实现(见 §2 与 §A1 的可行性前提),而非裸 LAN-over-HTTP 的公网化。
2. 共享设计原则 (Shared Design Principles)
适用于全部 9 个特性的不可协商前提,逐项落到既有代码:
- SP1 — Byte-shuttle 不变:服务端不新增任何 ANSI/终端语义解析。git 数据流、statusLine 遥测、时间线全部带外(HTTP/JSON 或 git 子进程),与终端 WS 字节流完全隔离——与 v0.3 hook 侧信道同构。
- SP2 — 复用 hook 侧信道:Claude 进程 POST 来的端点(
/hook、/hook/permission,未来 /hook/status、/hook/decision)。loopback-only 的摄入端点用 isLoopback(req.socket.remoteAddress) 拦非本机(src/server.ts:70-78);唯一例外是 /hook/decision(A1),它被远程设备的 SW 调用,故改用 requireAllowedOrigin + 能力 token(见 §A1.5、§5)。
- SP3 — 复用 held-permission 机器:
pendingApprovals / resolvePending / permDecision(src/server.ts:114-122, 61-63)。A1 的锁屏决策与 B4 的 plan 门都是这条通道上的新 resolver / 新 gate 类型,不新建审批状态机。
- SP4 — 复用安全 git-exec 模式(强制):任何
git 调用走 execFileAsync('git', [...])(无 shell)+ timeout + maxBuffer 截断 + mapWithConcurrency 并发上限,照搬 src/http/projects.ts、src/http/worktrees.ts。绝不字符串拼 shell。入口 path 一律 path.isAbsolute() + fs.stat().isDirectory() + hasGitEntry 三连校验。
- SP5 — 只读 vs 状态变更的守卫口径(已确立):只读发现端点(
/projects、/live-sessions、A4 /…/events、B1 /projects/diff)不挂 Origin 守卫;任何写/副作用端点(B3 /projects/worktree、A1 /push/subscribe、/hook/decision)必须挂 requireAllowedOrigin(src/server.ts:215-222,已被 DELETE /live-sessions、POST /open-in-editor 使用)。
- SP6 — PWA 是交付载体:A1 在
public/sw.js 加 push/notificationclick,在 manifest.webmanifest 之上利用既有可安装性。WS 仍随页面协议选 ws:/wss:(M6),CSP 仍 script-src 'self'(src/server.ts:135-138)。
- SP7 — 不信任外部数据 + 防 XSS:git diff/statusLine/hook body/snippet 标签都是未经信任的工具/文件内容。前端一律
textContent/安全 DOM 构建(el()),禁止 innerHTML 渲染外部内容(makeLauncher 的 innerHTML 仅限可信品牌 SVG,不可作先例)。未信任 JSON 用 unknown + 逐字段收窄(仿 parseHookEvent),never throw。
- SP8 — config 无硬编码 + env 命名约定(L4):服务端
cfg 读的 env 一律无前缀(如 PORT、VAPID_PRIVATE_KEY、WORKTREE_ROOT);spawn 注入 / hook 脚本读的 env 一律 WEBTERM_ 前缀(如 WEBTERM_SESSION、WEBTERM_STATUSLINE_URL、WEBTERM_NTFY_TOKEN)。新参数走 src/config.ts 的 parse* 帮手 + src/types.ts 的 Config。秘密在启动校验/缺失优雅降级,never 硬编码、never 入日志。
- SP9 — 类型契约集中(协调点):所有新共享类型加在
src/types.ts(单一 owner 任务,见 §7),模块内不本地重声明。
- SP10 — 共享 UI 组件(M4):A4 时间线、A5 卡住徽标、B2 仪表、既有 working/waiting/idle 状态点都渲染进
tabs.ts/preview-grid.ts/projects.ts。统一抽出一个 per-session 状态/仪表组件与一个 refreshTab owner,复用既有状态色语义,避免三个任务抢同一片 DOM、重复实现陈旧/着色逻辑。
3. Band A — 闭合"走开"循环
触发点(A 段共用):服务端已把 hook 折成粗 ClaudeStatus(parseHookEvent,src/http/hook.ts:24-57),并经 manager.handleHookEvent 广播(src/session/manager.ts:196-212)。Band A 挂在同样两个转移上:
- NEEDS-INPUT(高优) =
PermissionRequest 或 Notification(notification_type==='permission_prompt') → waiting;PermissionRequest 会被服务端 hold 在 pendingApprovals 等决策。
- DONE(低优) =
Stop/SessionEnd → idle。
加上 STUCK(A5 新增,非 hook 触发) = 输出静默超阈值。三类信号都经 A1 的统一通知服务出站。
A1. 移动推送 + 锁屏审批 (Mobile Push + Lock-Screen Triage)
A1.1 用户故事 (User Stories)
- US-A1a(核心):装了 PWA 后,Claude 需要批准工具时,我手机锁屏收到带 Allow / Deny 的推送;点一下就解决并让 Claude 继续——我不开 app。
- US-A1b:Claude 跑完(Stop/SessionEnd)我收到低优通知("会话完成"),知道该回来看。
- US-A1c:不想依赖浏览器 PWA 时,hook 脚本可额外 ping 一个 ntfy / Pushover topic,任何装了那个 app 的设备都会响——与 PWA 是否安装/打开无关(也是裸 LAN-over-HTTP 部署下的可用回退,见 A1.3 可行性前提)。
A1.2 功能需求 (Functional Requirements)
| ID |
需求 |
优先级 |
| A1-FR1 |
Web Push 订阅:POST /push/subscribe 存浏览器 PushSubscription,DELETE /push/subscribe 删;持久化到 config 目录下的 JSON(重启存活)。写端点,挂 requireAllowedOrigin |
P0 |
| A1-FR2 |
服务端在 §3 两个转移上用 VAPID 签名发 Web Push:NEEDS-INPUT(高优、requireInteraction)与 DONE(低优)。依赖 web-push npm 包(见 L1/§5 供应链) |
P0 |
| A1-FR3 |
GET /push/vapid-key 暴露公钥给 SW 订阅;私钥+subject 来自 env。VAPID env 未设 → 整个 push 优雅禁用(端点 503、UI 隐藏开关、不崩) |
P0 |
| A1-FR4 |
改 /hook/permission 的 hold 门(最关键,见 A1.4 C1):当前"session.clients.size === 0 → 立即 {} 不 hold"会让"零 tab 走开"时根本不 hold、不 waiting、不推送。改为:有 ≥1 个挂着的客户端 或 push 已启用且有 ≥1 个活跃订阅 → hold;hold 时铸一个按 pending 决策的能力 token |
P0 |
| A1-FR5 |
SW push 渲染通知:NEEDS-INPUT 带 Allow / Deny 动作按钮;payload 携带 sessionId、工具名、短 detail、和那次 pending 决策的能力 token |
P0 |
| A1-FR6 |
SW notificationclick(Allow/Deny) → POST /hook/decision {sessionId, decision, token},校验 token + Origin 后 resolvePending(...permDecision(...))(与 WS approve/reject 同效)。点正文(无动作)= 打开 app 并聚焦该会话 |
P0 |
| A1-FR7 |
同一 tag(按 session) 使通知替换不堆叠;决策被另一设备/超时(cfg.permTimeoutMs)解决后,其它设备的通知被更新/清除(无失效的 Allow/Deny) |
P1 |
| A1-FR8 |
ntfy/Pushover 桥(秘密走 env,见 H3):scripts/setup-hooks.mjs 在 WEBTERM_NTFY_URL/WEBTERM_NTFY_TOPIC(或 WEBTERM_PUSHOVER_*)存在时,给 hook 命令追加一条 fire-and-forget curl,priority 映射(NEEDS-INPUT=high、DONE=low)。token 以 spawn 注入的 $WEBTERM_NTFY_TOKEN 引用,绝不写字面量进 settings.json |
P1 |
| A1-FR9 |
全局/按会话静音开关(客户端 localStorage 偏好)+ 服务端全局 DND env 默认 |
P2 |
A1.3 UX 与可行性前提 (UX & Feasibility)
- 可行性前提(C2,置顶):Service Worker +
pushManager.subscribe 要求安全上下文。手机访问 http://192.168.x.x:3000 不是安全上下文(只有 localhost 是),而项目文档默认是裸 LAN-over-HTTP(CLAUDE.md / TECH_DOC §7)。因此 Band A1 的 Web Push 需要 HTTPS——推荐 Tailscale Serve / TLS。前端必须 !isSecureContext 检测并给出明确文案("启用推送需经 HTTPS/Tailscale 访问"),并在不安全上下文下隐藏 🔔 开关、引导改用 A1-FR8 的 ntfy/Pushover 桥(curl 直连外部服务,不受安全上下文限制,是裸 LAN 部署的回退)。iOS 还额外要求 PWA 已加到主屏。CSP connect-src 'self' ws: wss: 在 TLS 同源下满足 push 端点可达。
- 设置:工具栏 🔔 开关 → 浏览器权限提示 →
pushManager.subscribe(vapidKey) → POST /push/subscribe;开关反映 granted/denied/unsupported/insecure-context。
- NEEDS-INPUT 通知:标题
⏳ Claude needs you — <repo/session>,正文 = 工具名 + detail 首行,动作 [Allow] [Deny],requireInteraction:true。Allow/Deny 不开 app 即解决;下次打开有 toast 确认结果。
- DONE 通知:
✅ Session done — <repo/session>,无动作、低优、点开聚焦该 tab。
- 移动优先:核心价值就是锁屏处理,happy path 零前台交互;不可用时降级到 ntfy/Pushover。
A1.4 数据流 (Design / Data Flow)
- 新共享类型(加
src/types.ts):PushSubscriptionRecord(endpoint/keys/createdAt);pendingApprovals 条目扩 token + expiresAt;NotifyClass = 'needs-input'|'done'|'stuck'。
- 新文件:
src/push/push-service.ts(签名/发送/通知服务)、src/push/subscription-store.ts(持久化/剪除)、public/push.ts(订阅流 + 🔔 + 决策 fetch)。
A1.5 安全考量 (Security)
/hook/decision 是新敏感面:它被远程设备 SW 调用(非 loopback,isLoopback 不适用),故必须 (1) 过 requireAllowedOrigin(CSWSH/CSRF),(2) 校验能力 token。token 绑定到"该 session + 当前那次 pending 请求",resolve/超时即失效(M1)——泄漏的长期订阅 token 不能解决任意未来审批。失配/陈旧 → 403。对 /push/subscribe 与 /hook/decision 限流。
- VAPID 私钥 + subject 是秘密 → 仅 env、启动校验、绝不入日志;缺失 → 禁用而非崩。
- 订阅库含 endpoint+keys,按敏感处理(文件权限 600,绝不经任何 GET 暴露);返回 404/410 的订阅剪除;
PUSH_MAX_SUBS 封顶防 DoS。
- ntfy token(H3):以 spawn 注入 env 引用,不写进 settings.json,也不进 curl 字面 argv(避免
ps/备份文件泄密)。
- payload 最小化:只带 session id + 工具名 + 短 detail,无命令输出、无秘密(守 byte-shuttle 边界)。
- 供应链(L1):新增
web-push 运行时依赖——纳入安全清单与锁版本。
A1.6 配置 (Config)
| Env |
默认 |
说明 |
VAPID_PUBLIC_KEY |
未设→push 关 |
公钥(暴露给客户端) |
VAPID_PRIVATE_KEY |
—(秘密) |
私钥;启用 push 必需 |
VAPID_SUBJECT |
mailto:admin@localhost |
VAPID sub |
PUSH_STORE_PATH |
<homeDir>/.web-terminal/push-subs.json |
订阅持久化文件(600) |
PUSH_MAX_SUBS |
50 |
订阅数上限(DoS) |
NOTIFY_DONE |
1 |
是否发低优 DONE 推送 |
NOTIFY_DND |
0 |
全局免打扰默认(A1-FR9) |
DECISION_TOKEN_TTL_MS |
=PERM_TIMEOUT_MS |
能力 token 寿命(随 pending 决策) |
(spawn/hook env)WEBTERM_NTFY_URL/WEBTERM_NTFY_TOPIC/WEBTERM_NTFY_TOKEN/WEBTERM_PUSHOVER_TOKEN/WEBTERM_PUSHOVER_USER |
未设→桥关 |
ntfy/Pushover 桥(token 走 env,不入 settings.json) |
A1.7 暂不做 (Out of Scope)
- 任意/自定义用户推送、营销、定时通知。
- 多用户订阅归属 / 按用户路由(无 auth — Band C)。
- 原生 iOS/Android app 或 FCM/APNs 直连(仅 Web Push + ntfy/Pushover)。
- 富媒体/图片通知。
A1.8 验收标准 (Acceptance)
- AC-A1.1:装 PWA 并开 🔔 后,
POST /push/subscribe 204;GET /push/vapid-key 返回配置的公钥。
- AC-A1.2(C1 核心):在零浏览器 tab 挂着的情况下,Claude 触发工具批准 → 手机锁屏出现带 Allow/Deny 的通知;点 Allow 解决 held 请求并让 Claude 继续,不开 app;点 Deny 则拒绝。
- AC-A1.3:一台设备解决(或
cfg.permTimeoutMs 超时)后,其它设备的通知被替换/清除(无失效 Allow/Deny)。
- AC-A1.4:
Stop/SessionEnd 在 NOTIFY_DONE=1 时到达低优 DONE 通知。
- AC-A1.5(C2):在不安全上下文(http LAN)下 UI 隐藏 🔔 并提示需 HTTPS/Tailscale,不崩;HTTPS 下订阅成功。
- AC-A1.6:VAPID env 未设 → push 端点 503、UI 隐藏开关、无崩。
- AC-A1.7(H3):
WEBTERM_NTFY_* 设置后 setup-hooks.mjs 写出会 ping ntfy 的命令且正确 priority;settings.json 与进程 argv 中不含任何 token 字面量;web-terminal 外该 curl no-op。
- AC-A1.8(M1):
/hook/decision 拒绝缺/外来 Origin(403)与坏/陈旧 token(403);resolve 后旧 token → 403。
A2. 语音口述 (Push-to-Talk Mic on Mobile Input Bar)
A2.1 用户故事
- US-A2:移动端按住 🎤 说话,松手后转写文本被打进 Claude(可选自动 Enter),无需软键盘。
A2.2 功能需求
| ID |
需求 |
优先级 |
| A2-FR1 |
按住录音(Web Speech SpeechRecognition)、松手停止;最终转写经既有 raw-bytes 路径发送 |
P0 |
| A2-FR2 |
实时 interim 转写显示在小 overlay/chip,发送前可见 |
P1 |
| A2-FR3 |
"发送 vs 仅插入"设置(是否补 \r);默认仅插入、用户手动点 ⏎ |
P1 |
| A2-FR4 |
不支持的浏览器(如非 Chromium)隐藏 🎤,绝不抛错 |
P0 |
| A2-FR5 |
识别语言可选(默认 navigator.language) |
P2 |
A2.3 UX 与数据流
- 🎤 置于键栏(
public/keybar.ts),touchstart/touchend 按住语义并 preventDefault 保持软键盘不弹(仿 keybar.ts:104-108);按住时脉冲红点 + interim chip;桌面点击切换回退(仿 :111-114)。
- 松手 →
onSend(transcript [+ '\r']) = TerminalSession.send(public/terminal-session.ts:316-319)→ {type:'input'} → pty.write(byte-shuttle 不变)。纯前端,零服务端改动。
A2.4 安全考量
- 麦克风权限浏览器把关;转写作 raw
input.data 逐字透传(协议禁止过滤输入内容),无新注入面。
- 隐私警示(L2):Chrome Web Speech 会把音频流给 Google 服务——文档明示;这是客户端选择,自托管服务端永不接触音频(设 AC 验证)。
A2.5 配置:无 env(前端)。客户端偏好 dictation.autoSend/dictation.lang(localStorage)。
A2.6 暂不做:服务端 STT(Whisper 等)、语音命令(如"approve")、唤醒词/常听、录音存储。
A2.7 验收标准
- AC-A2.1:支持的移动浏览器上按住 🎤 说话显示 interim,松手把最终转写打进当前终端。
- AC-A2.2:使用麦克风时软键盘不弹(touch
preventDefault)。
- AC-A2.3:无
SpeechRecognition 的浏览器隐藏 🎤 且不报错。
- AC-A2.4:自动发送开 → 以
\r 结尾;关 → 等手动 ⏎。
- AC-A2.5(L2):抓包/服务端日志确认无任何音频抵达服务端。
A3. 快捷回复 chips + 提示词调色板 (Quick-Reply Chips + Saved Prompt Palette)
A3.1 用户故事
- US-A3a:移动端在键栏旁有 tap-to-send chips(
yes、continue、1/2/3、Esc),一点即发。
- US-A3b:可保存自定义 snippet("run the tests"、"write a commit"、"/clear")到调色板,一点即发,跨会话持久。
A3.2 功能需求
| ID |
需求 |
优先级 |
| A3-FR1 |
内置快捷 chips(yes\r/continue\r/1\r/2\r/3\r/Esc),各经既有 onSend 发送 |
P0 |
| A3-FR2 |
用户调色板:增/改/删命名 snippet,localStorage 持久;每条含文本 + 是否补 \r |
P0 |
| A3-FR3 |
一点发送任意 snippet(同键栏 ws.send 机制) |
P0 |
| A3-FR4 |
重排/收藏;首次运行播种合理默认集 |
P1 |
| A3-FR5 |
长按编辑(移动)/ 右键(桌面) |
P2 |
| A3-FR6 |
调色板 JSON 导入/导出(手动跨设备搬运) |
P2 |
A3.3 UX 与数据流
- chips 横向滚动行,置于键栏内/上,视觉与既有键栏一致;
+ 开小型调色板编辑器(复用既有玻璃浮层样式)。点 chip 即发(无确认),编辑显式(长按/+)。
- 点 chip/snippet →
onSend(text [+ '\r']) = TerminalSession.send(terminal-session.ts:316-319)→ {type:'input'} → pty.write。调色板存 localStorage,无服务端。
A3.4 安全考量
- snippet 是用户文本,按"和打字同信任级"作 raw input;标签以
textContent 渲染,绝不 innerHTML 用户串(SP7)。localStorage 按 origin 隔离、无服务端存储、无新端点。
A3.5 配置:无 env(客户端,调色板/默认在 localStorage)。
A3.6 暂不做:服务端/团队共享 snippet 库(无 auth — Band C)、参数化模板、经服务端同步调色板(导入/导出是手动桥)。
A3.7 验收标准
- AC-A3.1:内置 chips 出现并发正确字节(含
\r)。
- AC-A3.2:可增/改/删/重排 snippet,刷新后持久。
- AC-A3.3:点 snippet 经与键栏同路径发送(终端确实收到)。
- AC-A3.4:含特殊字符的标签作惰性文本渲染(无 DOM/脚本注入)。
A4. 活动时间线 ("What Happened While I Was Gone")
A4.1 用户故事
- US-A4:回来时在缩略图/详情旁看到人话、带时间戳的流:"10:02 ran Bash · 10:03 edited 3 files · 10:05 waiting for approval (Write) · 10:09 done",几秒看懂离开期发生了什么。
A4.2 功能需求
| ID |
需求 |
优先级 |
| A4-FR1 |
把离散 hook 事件(PreToolUse、PostToolUse、Notification、Stop、SessionEnd、UserPromptSubmit)存入每会话、带时间戳、有界的内存事件环。注意:当前安装器 FF_EVENTS 不含 PostToolUse(H1,见 §A4.5),必须补注册 |
P0 |
| A4-FR2 |
GET /live-sessions/:id/events 返回该会话最近时间线(按数封顶) |
P0 |
| A4-FR3 |
每事件含:时间戳、事件类、工具名(有则)、服务端派生的人话短标签(PreToolUse(Bash)→"ran Bash") |
P0 |
| A4-FR4 |
前端在详情/缩略图旁渲染流(newest-first),开着时自动刷新 |
P0 |
| A4-FR5 |
有界环(每会话 cfg.timelineMax),旧的淘汰;与字节环互补不替代 |
P0 |
| A4-FR6 |
hook body 提供文件路径时(Edit/Write 的 tool_input)捕获"改了哪些文件",否则回退工具名 |
P1 |
| A4-FR7 |
折叠快速重复事件("ran Bash ×4") |
P2 |
A4.3 UX
- 详情页/缩略图卡片的 "Timeline" 面板:
HH:MM · 图标 · 标签 竖列,按类着色(working=中性、waiting=琥珀、done=绿,复用既有状态语义,经 §SP10 的共享组件)。时间线补充实时缩略图(RingBuffer.tail(),src/server.ts:197-209):缩略图是"现在屏幕长啥样",时间线是"这段时间发生了啥"。
A4.4 数据流与对 hook 摄入的扩展(H2,非纯复用)
- 被编辑面(明确列出,非"reuse"):
src/http/hook.ts(parseHookEvent 或新 appender)+ /hook 路由(src/server.ts:266-272 当前只把 {status,detail} 传给 manager)+ scripts/setup-hooks.mjs(补 PostToolUse)。
- 新文件:
src/session/timeline.ts(有界环 + 标签派生 + sanitize,纯、可单测)、public/timeline.ts(面板)。
- 新类型:
TimelineEvent(src/types.ts);Session 加有界 timeline("在不可变 meta 上挂可变 runtime handle"模式,同 buffer/lastOutputAt)。
A4.5 安全考量
- 与
/live-sessions 同威胁模型:只读发现、不挂 Origin 守卫(守卫给状态变更路由);暴露工具名/文件路径给 LAN 设备——与"已对 LAN 交出 shell"一致(Tailscale-only,TECH_DOC §7)。
- 未信任 hook body:whitelist
hook_event_name、截断字符串长度、sanitize 控制字符(复用 sanitizeForLog,src/server.ts:80-86)后再存路径/工具名。有界环防事件洪水撑爆内存(DoS)。
A4.6 配置
| Env |
默认 |
说明 |
TIMELINE_MAX |
200 |
每会话事件环上限 |
TIMELINE_ENABLED |
1 |
捕获/服务时间线总开关 |
A4.7 暂不做:跨重启持久化(仅内存,同会话)、Claude 推理的 token 级全转录(~/.claude 已有深度回放)、服务端 ANSI 推断活动(守 byte-shuttle)、时间线搜索/分析。
A4.8 验收标准
- AC-A4.1:跑一个 Claude 任务产出离散、带时间戳、人话的条目(工具用、waiting、done)。
- AC-A4.2:
GET /live-sessions/:id/events 返回最近事件,受 TIMELINE_MAX 界。
- AC-A4.3:面板在缩略图/详情旁渲染并随新事件更新。
- AC-A4.4(SP7):hook body 的工具名/路径经 sanitize(无控制字符)并截断后才显示。
- AC-A4.5(H1):
setup-hooks.mjs 安装后 FF_EVENTS 含 PostToolUse,"完成 Bash"类事件确实到达。
- AC-A4.6:
TIMELINE_ENABLED=0 不存事件、端点返回空。
A5. 卡住/静默主动告警 (Stuck / Idle Proactive Alert)
A5.1 用户故事
- US-A5:会话既没"完成"也没 hold 审批、只是静默(卡在未挂 hook 的提示,或 wedged)时,若无输出超 N 分钟而它看着在忙,我收到"会话可能卡住 — 12 分钟前最后活动"告警。
A5.2 功能需求
| ID |
需求 |
优先级 |
| A5-FR1 |
检测静默:复用 lastOutputAt;now - lastOutputAt > STUCK_TTL 且非 idle/已退出 → stuck-候选 |
P0 |
| A5-FR2 |
首次越阈经 A1(push/ntfy)发通知;每个 stuck 回合只发一次(输出恢复后重新武装) |
P0 |
| A5-FR3 |
检测扫描跑在既有 reaper 间隔(cfg.reapIntervalMs),不新增常驻 timer |
P0 |
| A5-FR4 |
UI 出 "stuck" 徽标(缩略图墙/tab 状态,经 §SP10 共享组件),无 push 也可见 |
P1 |
| A5-FR5 |
阈值可配;0 禁用 |
P0 |
| A5-FR6 |
可选抑制"从未为 vibe-coding 启动且零客户端"的会话——但默认无脑告警(你就是走开了) |
P2 |
A5.3 设计与数据流(对 reaper/manager 的真实改动,H4)
- 复用:
lastOutputAt(src/types.ts:163,已是 orphan-reclaim 的 liveness 代理);reaper 节拍(不新 timer,KISS)。
- 新改动(重新界定 Owns):
manager.ts 加 sweepStuck() + manager↔notifyService 接线;Session 加 stuckNotified flag。依赖 A1 先落地 notifyService(M5)。
A5.4 安全考量
- 无新端点,服务端内部;继承 A1 通知安全(token、payload 最小化)。阈值经 env、校验为非负整数(复用
parseNonNegativeInt,src/config.ts:42-55),0=禁用。每回合至多一次告警(A5-FR2),防告警风暴。
A5.5 配置
| Env |
默认 |
说明 |
STUCK_TTL |
600(秒,10 分钟) |
静默窗口;0 禁用 |
STUCK_ALERT |
1 |
卡住告警总开关 |
A5.6 暂不做:经进程组/子进程探测的真挂死检测(node-pty 跨平台不可靠,同 orphan-reclaim 限制,ARCHITECTURE §3.5/M3)、自动补救(发输入/杀会话)、每命令"预期时长"自适应阈值。
A5.7 验收标准
- AC-A5.1:无输出达
STUCK_TTL(且非 idle/退出)→ 经 A1 通道恰发一次 stuck 通知。
- AC-A5.2:输出恢复后 flag 重新武装,后续静默可再告警。
- AC-A5.3(H4):扫描不加新 timer(跑在既有 reaper pass 内),且确实覆盖 attached 会话(不止 detached)。
- AC-A5.4:
STUCK_TTL=0 或 STUCK_ALERT=0 禁用、无通知。
- AC-A5.5:缩略图墙/tab 状态出 "stuck" 徽标。
4. Band B — 在终端之上长出工作台
B1. 每会话只读 git diff / review (Read-only Git Diff / Review)
B1.1 用户故事
- US-B1.1(核心):手机点项目/会话的 Diff → 看到按文件分组的 unified diff,每文件带
+增/-删 统计、绿/红高亮、可折叠/跳转——只读,不怕误触改坏。
- US-B1.2:可在 未暂存(working)/已暂存(staged) 间切换。
- US-B1.3:窄屏不横向滚到吐——可切 unified↔split,默认窄屏 unified + 软换行。
- US-B1.4(仅记录):行级回评给 agent——Band B 不做。
B1.2 功能需求
| ID |
需求 |
优先级 |
| FR-B1.1 |
GET /projects/diff?path=<abs>&staged=0|1 只读端点:跑 git diff [--staged] -- 取 patch,返回结构化 diff |
P0 |
| FR-B1.2 |
复用安全 git-exec:execFile(无 shell)+ timeout + maxBuffer 截断 + 入口 path 校验(绝对+目录+git 仓库) |
P0 |
| FR-B1.3 |
纯函数 parseUnifiedDiff(patch) / parseNumstat(out) → DiffFile[](可单测、零 DOM) |
P0 |
| FR-B1.4 |
前端 diff 查看器:按文件分组、+/- 徽标、行级绿/红、文件侧栏跳转/折叠 |
P0 |
| FR-B1.5 |
working/staged 切换;用 git status --porcelain/numstat 标注 untracked(避免 --no-index 的反直觉双操作数与预期非零退出,L3) |
P1 |
| FR-B1.6 |
入口:项目详情页 + 每会话工具栏各加 Diff/Review 按钮(会话用其 spawn cwd 作 path) |
P1 |
| FR-B1.7 |
unified↔split 切换;窄屏默认 unified+软换行;空 diff 友好态 |
P1 |
| FR-B1.8 |
大 diff 防爆:服务端按文件数/总字节封顶,超限返回 truncated 标记 |
P2 |
| FR-B1.9 |
已提交对比:?base=<rev> 跑 git diff <base>(base 走 git rev-parse --verify 白名单,见安全) |
P2 |
B1.3 设计与数据流
- 新类型(
src/types.ts):DiffLine{kind,text}、DiffFile{oldPath,newPath,status,added,removed,binary,hunks}、DiffResult{files,staged,truncated}。
- 新文件:
src/http/diff.ts、public/diff.ts。
B1.4 安全考量
- 只读:仅
git diff(无写参数),不挂 Origin 守卫(同 /projects/detail 口径)。
- 无 shell 注入:
execFile 数组参数;-- 终结选项防 path/base 被当 flag。
- path 越界:绝对 + isDirectory + hasGitEntry 三连;
base(FR-B1.9)必须 git rev-parse --verify 白名单化,否则保持在 P0 之外。
- XSS(关键):diff 是任意文件内容——全程
textContent/el(),零 innerHTML;CSP 兜底。
- DoS:timeout + maxBuffer + 文件/字节封顶(FR-B1.8)。
B1.5 配置
| Env |
默认 |
说明 |
DIFF_TIMEOUT_MS |
2000 |
单次 git diff 超时 |
DIFF_MAX_BYTES |
2097152 (2MB) |
patch 截断上限 |
DIFF_MAX_FILES |
300 |
返回文件数上限,超出标 truncated |
B1.6 暂不做:行级回评(仅记录)、编辑/暂存/丢弃/git add -p/commit(破坏只读+byte-shuttle,明确不做)、语法着色(不引第三方高亮库)。
B1.7 验收标准
- AC-B1.1:有未提交改动的 repo 调端点返回正确
DiffFile[],+/- 与 git diff --numstat 一致;staged=1 返回已暂存集。
- AC-B1.2:非 git/不存在 path → 404;超大 diff →
truncated:true 且不 OOM。
- AC-B1.3:手机查看器逐文件跳转/折叠、working↔staged 切换可用,无横向滚到吐。
- AC-B1.4(SP7):含
<script>/ANSI/反引号的内容原样作文本显示,不执行、不破坏布局。
- AC-B1.5:
parseUnifiedDiff/parseNumstat 纯函数单测 ≥80%(含重命名/新增/删除/二进制/空 diff/untracked)。
B2. statusLine 遥测 → per-tab 成本/上下文/PR 仪表
⚠ 研究优先(H5,阻断式前提):B2 的字段假设(context_window.used_percentage、cost.total_cost_usd、rate(5h/7d)、pr)必须先对照真实 statusLine stdin schema 核实,再让 §7 的 T-types 冻结 StatusTelemetry。研究未确认 → 该任务返回 [!] BLOCKED,不得猜测字段名。
B2.1 用户故事
- US-B2.1(核心):tab 上一眼看到上下文进度条、累计 $ 成本、模型 chip、未关闭 PR 徽标,不用进会话翻。
- US-B2.2:缩略图墙/项目卡片也显示,挑"快烧爆上下文/快没额度"的优先处理。
- US-B2.3:看到 5h/7d 速率额度仪表,避免撞墙。
B2.2 功能需求
| ID |
需求 |
优先级 |
| FR-B2.1 |
随 setup-hooks 安装 statusLine 脚本:读 stdin statusLine JSON → POST 到 $WEBTERM_STATUSLINE_URL(带 X-Webterm-Session),并回一行精简文本给 Claude 状态行 |
P0 |
| FR-B2.2 |
POST /hook/status(loopback-only,复用 isLoopback):parseStatusLine 解析→存该 session 最新遥测→广播给挂着的客户端 |
P0 |
| FR-B2.3 |
按 sessionId 存最新遥测(类比 claudeStatus);attach 时随状态一起补发(晚加入设备立即看到) |
P0 |
| FR-B2.4 |
新 ServerMessage 类型 telemetry;同步更新 serialize、客户端 handle switch、任何 exhaustiveness 检查(M3);TerminalSession 暴露 getter + onTelemetry 回调 |
P0 |
| FR-B2.5 |
每 tab 渲染:上下文进度条 + $ 成本 + 模型 chip + PR 徽标(带 review_state 颜色),经 §SP10 共享仪表组件 |
P0 |
| FR-B2.6 |
缩略图/项目卡片/manage 页复用同组小仪表 |
P1 |
| FR-B2.7 |
5h/7d rate-limit 双 meter;lines +/−、effort 作 tooltip |
P1 |
| FR-B2.8 |
解析对缺字段宽容(缺哪个不渲染哪个),陈旧遥测(>TTL)置灰 |
P1 |
| FR-B2.9 |
PR 徽标点击 = 复制/打开 PR URL(同源 UI 内展示,外跳需用户显式点击) |
P2 |
B2.3 设计与数据流
- spawn env 注入(B2 唯一的 spawn 改动):在
src/session/session.ts:94-98 现有 WEBTERM_SESSION/WEBTERM_HOOK_URL 旁加 WEBTERM_STATUSLINE_URL: http://127.0.0.1:${cfg.port}/hook/status;web-terminal 外该变量未设 → curl no-op。
- 新类型(
src/types.ts):StatusTelemetry{contextUsedPct?,costUsd?,linesAdded?,linesRemoved?,model?,effort?,pr?{number,url,reviewState?},rate?{fiveHourPct?,sevenDayPct?},at};ServerMessage |= {type:'telemetry',telemetry};Session.telemetry: StatusTelemetry|null;LiveSessionInfo/ProjectSessionRef 可选带最新遥测。
- 新文件:
src/http/statusline.ts、scripts/statusline.mjs。
B2.4 安全考量
- loopback-only:
/hook/status 复用 isLoopback——外部 LAN 设备无法伪造遥测。
- 未信任 JSON:
parseStatusLine 用 unknown + 逐字段收窄,缺/脏字段安全降级,never throw。
- XSS:model/PR URL 等走
textContent;PR URL 不自动 window.open,仅用户显式点跳。
- 体量:
express.json({limit:'64kb'})(同 /hook),只存"最新一条",无累积。
B2.5 配置
| Env |
默认 |
说明 |
STATUSLINE_TTL_MS |
30000 |
多久未更新视为陈旧(前端置灰) |
(spawn env)WEBTERM_STATUSLINE_URL |
服务端按 cfg.port 注入 |
脚本 POST 目标,非用户配置 |
B2.6 暂不做:遥测历史/趋势图、跨会话成本汇总(只存最新一条)、服务端解析 statusLine 的 ANSI/格式(仍 byte-shuttle,只摄入结构化 JSON)、把遥测做成 push 告警(属 A1 范畴)。
B2.7 验收标准
- AC-B2.1:
npm run setup-hooks 后 ~/.claude/settings.json 含 statusLine 段且幂等、--remove 干净移除。
- AC-B2.2:跑会话后 tab 出现上下文进度条 + $ 成本 + 模型 chip。
- AC-B2.3(M3):晚加入设备 attach 时立即收到当前遥测(与既有
status 回放一起补发)。
- AC-B2.4:缺字段 JSON → 只渲染已有项不报错;陈旧(>TTL)置灰。
- AC-B2.5:非本机 POST
/hook/status → 403。
- AC-B2.6:
parseStatusLine 纯函数单测 ≥80%(满/缺/脏/空);ServerMessage 序列化/反序列化往返测试覆盖 telemetry。
B3. 从 UI 建 git worktree ⚠ 状态变更 (唯一写盘特性)
B3.1 用户故事
- US-B3.1(核心):项目详情页点 + New worktree,填新分支名、选 agent,系统
git worktree add -b <branch> 建独立目录并在那里开会话——多 agent 真并行不互踩。
- US-B3.2:新 worktree 立刻出现在列表,新 tab 标注分支名。
- US-B3.3(仅记录):prune/promote/merge worktree——Band B 不做。
B3.2 功能需求
| ID |
需求 |
优先级 |
| FR-B3.1 |
POST /projects/worktree(Origin/CSRF 守卫):{path, branch, base?};执行 git worktree add -b <branch> <computedDir> [<base>] |
P0 |
| FR-B3.2 |
严格校验:path 绝对+isDirectory+isGit;branch 过 git ref 规则子集(拒控制字符/../前导 -/空白/~^:?*[/@{);computedDir 必须落在受控 base 内 |
P0 |
| FR-B3.3 |
安全 exec:execFile('git', [...]) 无 shell + timeout + 数量上限;失败返回结构化 error(不泄完整 stderr) |
P0 |
| FR-B3.4 |
新目录 = <WORKTREE_ROOT 或 <repo>-worktrees>/<sanitized-branch>(可配 base) |
P0 |
| FR-B3.5 |
成功返回新 path+branch;前端调既有 openProject(newPath, label, cmd) 在该目录开会话 |
P0 |
| FR-B3.6 |
详情页表单:分支名 + base 选择 + agent 选择(claude/codex);建好刷新 worktree 列表 |
P1 |
| FR-B3.7 |
防重:分支/目录已存在 → 明确报错并提示进入已有 worktree |
P1 |
| FR-B3.8 |
移动端友好表单(大点击区、键栏不挡) |
P1 |
| FR-B3.9 |
从现有分支建(不带 -b) |
P2 |
B3.3 设计与数据流
- 新类型/函数:
CreateWorktreeResult{ok,path?,branch?,status?,error?}(src/types.ts);validateBranchName/computeWorktreeDir(纯,单测)/createWorktree(扩展既有 src/http/worktrees.ts,与 listWorktrees 同文件)。
B3.4 安全考量(本特性最重)
- CSRF/Origin:写端点必须
requireAllowedOrigin(否则恶意页可无预检 POST 建分支/写盘)——同 DELETE /live-sessions、/open-in-editor。
- 命令注入:
execFile 数组参数,绝不 shell 拼接;git 选项前置、用户值在 -- 后。
- 分支名注入/越界:
validateBranchName 拒 ../越界 //控制字符/前导 -(防当 flag)/空白/~^:?*[/@{,遵 git ref 规则子集。
- 路径越界(关键,M2):
computeWorktreeDir 用 path.resolve 后对两端先 fs.realpath 再做前缀 startsWith 包含校验(防 symlink 化的 WORKTREE_ROOT 或 repo 绕过字符串前缀),sanitize 分支为目录段(/→-),杜绝写到任意位置。base(FR-B3.9)若接受须 git rev-parse --verify。
- DoS/资源:timeout + 单次只建一个 + 数量上限;error 文案不回传完整 git stderr。
- 审计:
sanitizeForLog(src/server.ts:80-86)记录 who/what(branch/path 截断、去控制字符)。
B3.5 配置
| Env |
默认 |
说明 |
WORKTREE_ENABLED |
1 |
总开关(保守部署可关此写能力) |
WORKTREE_ROOT |
<repo>-worktrees(同级) |
新 worktree 落地根;强制所有 worktree 落此前缀内 |
WORKTREE_TIMEOUT_MS |
10000 |
git worktree add 超时 |
B3.6 暂不做:prune/promote/merge/remove worktree(仅记录)、worktree 看板、远程分支跟踪/--track/PR 自动开。
B3.7 验收标准
- AC-B3.1:填新分支名 → 在
WORKTREE_ROOT 下建出目录,git worktree list 可见,新 tab 在该目录跑起 agent。
- AC-B3.2:缺/伪造 Origin → 403;非 git path → 4xx;非法分支名(
../x、-rf、控制字符)→ 拒绝且不执行 git。
- AC-B3.3:分支/目录已存在 → 明确错误,无半成品残留。
- AC-B3.4(M2):symlink 化的
WORKTREE_ROOT/repo 不能绕过包含校验(realpath 后仍被拒)。
- AC-B3.5:两个 worktree 各开一个 agent 互不踩工作树(手动并行正确性)。
- AC-B3.6:
validateBranchName/computeWorktreeDir 纯函数单测 ≥80%(含越界/注入/symlink)+ 端点集成测试(真起 server、临时 repo)。
B4. plan-mode / 权限模式中继 (Permission-mode Relay)
⚠ 研究优先(H5,阻断式前提):--permission-mode 取值(default/acceptEdits/plan/bypassPermissions)、ExitPlanMode 是否经 /hook/permission 到达、以及 permDecision 的 decision JSON 能否携带"更新后的权限模式",必须先对照 Claude Code 官方文档/源码确认——当前 permDecision(src/server.ts:61-63)只发 {hookSpecificOutput:{hookEventName:'PermissionRequest',decision:{behavior}}},无证据能携带 mode。研究未确认 → 任务返回 [!] BLOCKED,不得猜测形状。"auto" 暂映射 bypassPermissions,PRD 标注待核。
B4.1 用户故事
- US-B4.1(核心):开会话时选权限模式(default/acceptEdits/plan/auto),作
--permission-mode 传给 CLI。
- US-B4.2:plan 模式出计划要退出 plan(ExitPlanMode)时,手机弹三选一门:approve+auto(接受编辑直接干)/ approve+review(逐项确认)/ keep planning(继续规划),复用既有 held-permission 通道。
B4.2 功能需求
| ID |
需求 |
优先级 |
| FR-B4.1 |
启动会话可选 permission mode;映射 CLI --permission-mode <mode> |
P0 |
| FR-B4.2 |
openProject/launcher 的 cmd 从 'claude\r' 升级为 claude --permission-mode <mode>\r(仅当选了非默认) |
P0 |
| FR-B4.3 |
plan-gate 识别:/hook/permission 收到 tool_name==='ExitPlanMode'(待研究确认)时标记 plan gate,pending 带类型标志 |
P0 |
| FR-B4.4 |
协议扩展:ServerMessage.status 增 gate?:'tool'|'plan';ClientMessage approve 携带决策(approve+auto / approve+review / keep planning) |
P0 |
| FR-B4.5 |
审批栏(updateApprovalBar)plan gate 时渲染三按钮;普通 tool gate 仍两按钮 |
P0 |
| FR-B4.6 |
决策→Claude:permDecision 扩展为写回带目标 permission mode 的 decision JSON(形状待研究确认;plan 批准后切 acceptEdits/default;keep planning=deny/留 plan) |
P0 |
| FR-B4.7 |
模式选择持久化(localStorage);后台 tab 命中 plan gate 触发既有通知(依赖 A1,M5) |
P1 |
| FR-B4.8 |
"auto"(绕过权限)需 UI 二次确认 + 视觉警示(高危),可 env 全局禁用 |
P1 |
| FR-B4.9 |
statusLine(B2)若上报 permission mode,则 tab chip 显示当前模式 |
P2 |
B4.3 设计与数据流
- 新类型/协议(
src/types.ts):type PermissionMode = 'default'|'acceptEdits'|'plan'|'auto';ClientMessage approve |= {type:'approve', mode?:PermissionMode};ServerMessage.status 增 gate?:'tool'|'plan';permDecision(behavior, mode?)(形状待核)。
B4.4 安全考量
- loopback-only:plan gate 仍走
/hook/permission(isLoopback),决策只能由本机 Claude 取走。
- 多设备一致性:保持"最后一个观看者离开才释放 hold"(
src/server.ts:472-474),plan gate 同理。
- "auto"/bypassPermissions 高危:等于无人值守全自动改盘——UI 二次确认 + 显著警示 +
ALLOW_AUTO_MODE 全局可禁用。
- 未信任决策输入:ws 的
mode 在 parseClientMessage 边界白名单校验(仅四枚举),非法降级为最保守(default 或 deny)。
- 超时回退不变:plan gate 超时回退到 Claude 自身交互提示(
cfg.permTimeoutMs),不卡死。
B4.5 配置
| Env |
默认 |
说明 |
DEFAULT_PERMISSION_MODE |
default |
未选时启动会话用的模式 |
ALLOW_AUTO_MODE |
0 |
是否允许 "auto"(bypassPermissions);默认禁高危全自动 |
(复用)PERM_TIMEOUT_MS |
300000 |
plan/tool gate hold 超时,沿用既有(src/config.ts:32)。held-curl --max-time 必须 > 此值(L5) |
B4.6 暂不做:自定义 per-tool 细粒度 allow 规则集、会话运行中动态切模式(除 plan gate 结果切换外)、非 Claude agent 的权限模式(hook/plan 目前 Claude 专属,标注未来中立化)。
B4.7 验收标准
- AC-B4.1:选 plan 模式开会话 → 实际以
claude --permission-mode plan 启动。
- AC-B4.2:plan 出计划触发门 → 手机审批栏出现三按钮;approve+auto 后以 acceptEdits 继续;keep planning 则留 plan。
- AC-B4.3:普通 tool gate 仍两按钮(无回归);多设备下关一台不取消他人门;超时回退到 Claude 自身提示。
- AC-B4.4:
ALLOW_AUTO_MODE=0 时 UI 不提供/禁用 auto;非法 mode ws 帧被边界拒绝并安全降级。
- AC-B4.5:
permDecision(mode)、cmd 构造、parseClientMessage 对 approve.mode 的校验均有单测;/hook/permission plan-gate 路径有集成测试。
- AC-B4.6(H5):研究 spike 已确认
--permission-mode 值集、ExitPlanMode 投递路径、decision-JSON 携带 mode 的形状(或记录无法携带、改走替代方案)后方可实现。
5. 合并安全章节 (Consolidated Security)
整体守 byte-shuttle/LAN 哲学(带外 JSON、git 子进程、前端-only A2/A3、无服务端 ANSI 解析)。逐风险归并:
| 风险类 |
涉及特性 |
缓解(落到代码) |
| CSWSH/CSRF(状态变更) |
A1(/push/subscribe,/hook/decision)、B3(/projects/worktree) |
全挂 requireAllowedOrigin(src/server.ts:215-222)。/hook/decision 是唯一远程非-loopback 的敏感写端点,额外要能力 token |
| loopback 伪造 |
A1 hold 门、A4 hook 摄入、B2 /hook/status、B4 plan gate |
摄入端点全过 isLoopback(:70-78);statusLine/hook/permission 进程恒在本机 |
| 能力 token(最小授权,非 app auth) |
A1 /hook/decision、A5/B4 经 A1 |
token 绑定到 session+当前 pending 决策、resolve/超时即失效(M1);失配/陈旧 → 403;限流 |
| 安全上下文不可用 |
A1 Web Push |
!isSecureContext 检测 + UI 文案;Band A1 需 HTTPS(Tailscale Serve/TLS);裸 LAN 回退到 ntfy/Pushover 桥(C2) |
| 秘密泄露 |
A1 VAPID(env、不入日志、缺失降级)、A1 ntfy token(spawn env 引用,不入 settings.json/argv,H3) |
env-only + 启动校验 + 600 文件权限;订阅库绝不经 GET 暴露 |
| 命令注入 |
B1 diff、B3 worktree |
execFile('git', [...]) 无 shell + -- 终结选项;用户值作独立 argv |
| 路径越界 |
B1、B3 |
绝对+isDirectory+hasGitEntry 三连;B3 额外 realpath 两端 + 前缀包含(M2)+ 分支 sanitize 为目录段 |
| 分支名/ref 注入 |
B3、B1(base) |
validateBranchName(git ref 规则子集);base 走 git rev-parse --verify 白名单 |
| XSS(未信任工具/文件内容) |
B1 diff、B2 遥测、A3 标签、A4 时间线 |
一律 textContent/el(),禁 innerHTML 渲染外部内容;CSP script-src 'self' 兜底(SP7) |
| 未信任 JSON |
A4 hook body、B2 statusLine、B4 mode |
unknown + 逐字段收窄、never throw;whitelist 事件名、截断长度、sanitize 控制字符 |
| DoS / 资源 |
A1(PUSH_MAX_SUBS)、A4(TIMELINE_MAX 环)、A5(每回合一告警)、B1(timeout/maxBuffer/文件封顶)、B2(只存最新一条)、B3(timeout/数量上限) |
见各特性配置 |
| 告警风暴 |
A5、A1 |
tag 去重 + 每 stuck 回合至多一次(A5-FR2) |
| 供应链 |
A1 web-push |
新运行时依赖,锁版本、纳入安全清单(L1) |
| 隐私(音频外流) |
A2 |
文档明示 Chrome Web Speech 流向 Google;服务端永不接触音频(AC-A2.5,L2) |
| timeout 配置一致性 |
A1/B4 hold |
held-curl --max-time(setup-hooks.mjs:36 现为 350)必须 > cfg.permTimeoutMs(现 300);纳入 config 校验,避免裸 350 字面量(L5) |
6. 合并配置 env 表 (Consolidated Config)
命名约定(SP8 / L4):服务端 cfg 读 = 无前缀;spawn 注入 / hook 脚本读 = WEBTERM_ 前缀。全部经 src/config.ts 的 parse* 帮手解析;秘密缺失优雅降级,never 硬编码。
服务端 cfg env(无前缀)
| Env |
默认 |
特性 |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT |
未设→push 关 / —(秘密)/ mailto:admin@localhost |
A1 |
PUSH_STORE_PATH |
<homeDir>/.web-terminal/push-subs.json |
A1 |
PUSH_MAX_SUBS |
50 |
A1 |
NOTIFY_DONE |
1 |
A1 |
NOTIFY_DND |
0 |
A1 |
DECISION_TOKEN_TTL_MS |
=PERM_TIMEOUT_MS |
A1 |
TIMELINE_MAX |
200 |
A4 |
TIMELINE_ENABLED |
1 |
A4 |
STUCK_TTL |
600(秒) |
A5 |
STUCK_ALERT |
1 |
A5 |
DIFF_TIMEOUT_MS |
2000 |
B1 |
DIFF_MAX_BYTES |
2097152 |
B1 |
DIFF_MAX_FILES |
300 |
B1 |
STATUSLINE_TTL_MS |
30000 |
B2 |
WORKTREE_ENABLED |
1 |
B3 |
WORKTREE_ROOT |
<repo>-worktrees |
B3 |
WORKTREE_TIMEOUT_MS |
10000 |
B3 |
DEFAULT_PERMISSION_MODE |
default |
B4 |
ALLOW_AUTO_MODE |
0 |
B4 |
(复用,校验 --max-time > 之)PERM_TIMEOUT_MS |
300000 |
A1/B4 (L5) |
spawn 注入 / hook 脚本 env(WEBTERM_ 前缀)
| Env |
来源 |
特性 |
WEBTERM_STATUSLINE_URL |
spawn 按 cfg.port 注入(session.ts) |
B2 |
WEBTERM_NTFY_URL / WEBTERM_NTFY_TOPIC / WEBTERM_NTFY_TOKEN |
spawn/hook env(token 不入 settings.json,H3) |
A1 |
WEBTERM_PUSHOVER_TOKEN / WEBTERM_PUSHOVER_USER |
spawn/hook env |
A1 |
7. 优先级与跨特性依赖排程 (Prioritization & Dependency Ordering)
C3 修正(最重要的排程约束):原 Band A、Band B 两张任务表都各自宣称独占 src/types.ts、src/server.ts、src/session/manager.ts、public/tabs.ts、public/terminal-session.ts、scripts/setup-hooks.mjs——违反 Owns: disjoint 不变式。本节合并为一个跨 Band 波次计划,每个共享文件只有一个 owner 任务;其余为 disjoint 新文件,可并行。沿用 v0.6 做法(docs/FEATURE_PROJECT_MANAGER.md §7)派给 module-builder,建议 isolation: worktree,TDD ≥80%。
7.1 优先级与"为什么这个顺序"
- R0 研究 spike(阻断,H5):核实 statusLine schema(B2)、
--permission-mode 值集 + ExitPlanMode 投递 + decision-JSON 能否带 mode(B4)。未确认则 B2/B4 的类型不得在 T-types 冻结,相关任务返回 [!] BLOCKED。
- A1 通知服务先行(M5):A5、B4-FR7 都消费 A1 的
notifyService/push 通道——provider 必须先落地,排程不得把消费者排在 provider 前。
- B1(只读 diff):纯复用、低风险、高价值,可与 A 段并行早做。
- A2/A3(前端-only):独立、低风险,任意时段可落。
- A4(时间线):依赖对 hook 摄入的扩展(H2)。
- B3(worktree):唯一写盘、安全最重,但与通知链独立,可并行推进。
- B2(遥测):受 R0 阻断;类型确认后并入。
- B4(plan 中继):受 R0 阻断 + 依赖 A1(背景 tab plan gate 通知,M5)。
7.2 共享文件 → 单一 owner(C3 核心)
| 共享文件 |
唯一 owner 任务 |
合并了哪些特性的改动 |
src/types.ts |
T-types |
全部新共享类型(A1 token/订阅、A4 TimelineEvent、B1 DiffResult、B2 StatusTelemetry+ServerMessage、B3 CreateWorktreeResult、B4 PermissionMode+协议扩展、各 Config/Session 字段) |
src/config.ts |
T-config |
全部新 env 解析 + L5 的 --max-time>permTimeoutMs 校验 |
src/session/manager.ts |
T-manager |
A4 时间线 append、B2 telemetry 存+广播、A5 sweepStuck()(H4)、B4 gate flag、manager↔notifyService 接线(M5) |
src/session/session.ts |
T-spawn-env |
B2 WEBTERM_STATUSLINE_URL + A1 WEBTERM_NTFY_* 注入 |
src/http/hook.ts + /hook 路由 |
T-hook-intake |
A4 扩展摄入(保留时间戳/tool_input,H2)、B4 gate 识别协调 |
src/server.ts |
T-server-wire |
A1 /push/*+/hook/decision+改 hold 门(C1)、A4 /…/events、A5 reaper 加 sweep 回调、B1 /projects/diff、B2 /hook/status、B3 /projects/worktree、B4 扩 /hook/permission gate + approve mode |
scripts/setup-hooks.mjs |
T-hooks-installer |
A4 补 PostToolUse(H1)、A1 ntfy/Pushover 桥(env token,H3)、B2 statusLine 段、L5 --max-time 取自/校验于 config |
public/tabs.ts |
T-tabs |
B2 仪表、A5 stuck 徽标、B4 三按钮审批+mode 选择+cmd、A2/A3/A4 面板挂载点、§SP10 共享状态/仪表组件 + 单一 refreshTab owner(M4) |
public/terminal-session.ts |
T-termsession |
B2 telemetry 捕获+onTelemetry、B4 gate 捕获 |
public/projects.ts |
T-projects-ui |
B1 diff 入口、B3 New-worktree 表单 |
public/sw.js |
T-sw |
A1 push/notificationclick(保留不拦 /term//hook) |
7.3 Disjoint 新文件任务(可并行)
| 任务 |
Owns(新文件) |
特性 |
| N-push |
src/push/push-service.ts + src/push/subscription-store.ts (+ test) |
A1 |
| N-timeline |
src/session/timeline.ts(有界环/标签/sanitize,纯)(+ test) |
A4 |
| N-diff-be |
src/http/diff.ts (+ test) |
B1 |
| N-statusline-be |
src/http/statusline.ts (+ test) |
B2 |
| N-worktree |
src/http/worktrees.ts(加 createWorktree/validateBranchName/computeWorktreeDir,与 listWorktrees 同文件)(+ test) |
B3 |
| N-statusline-script |
scripts/statusline.mjs |
B2 |
| N-push-ui |
public/push.ts(订阅流 + 🔔 + 决策 fetch) |
A1 |
| N-voice |
public/voice.ts |
A2 |
| N-quickreply |
public/quick-reply.ts |
A3 |
| N-timeline-ui |
public/timeline.ts |
A4 |
| N-diff-ui |
public/diff.ts |
B1 |
7.4 波次 (Waves)
- W-1(阻断研究):
R0(B2/B4 schema 确认)。先于 T-types 对 B2/B4 字段的冻结。
- W0(协调点):
T-types → 然后 T-config、T-spawn-env。
- W1(并行新文件 + 纯核心):
N-push、N-timeline、N-diff-be、N-statusline-be(待 R0)、N-worktree、N-statusline-script、T-hook-intake、以及前端 N-voice、N-quickreply、N-diff-ui、N-timeline-ui、N-push-ui、T-sw。
- W2(后端汇聚,单 owner,串行避免 clobber):
T-manager(依赖 N-push/N-timeline/统计核心)→ T-server-wire(依赖全部 be 任务 + T-manager)→ T-hooks-installer。
- W3(前端汇聚,单 owner):
T-termsession → T-tabs(含 SP10 共享组件)、T-projects-ui。
- W4(验证,报告-only):集成/E2E(真起 server、临时 repo、HTTPS 上下文模拟 push 注册),按 §8 汇总验收;缺陷回路给对应 owner(不跨 lane 改文件)。
7.5 显式依赖边
A5 → A1(notifyService);B4-FR7 → A1(背景通知);B2/B4 → R0(schema);T-server-wire → {N-push, N-diff-be, N-statusline-be, N-worktree, T-hook-intake, T-manager};T-tabs/T-termsession/T-projects-ui → {T-server-wire + 各 N-*-ui};T-config/T-manager/...→ T-types。
8. 验收标准汇总 (Acceptance-Criteria Summary)
| 特性 |
关键验收(含本次评审修正点) |
| A1 |
零 tab 走开时锁屏 Allow/Deny 即解决 held 请求(C1, AC-A1.2);不安全上下文隐藏 🔔 并提示需 HTTPS(C2, AC-A1.5);VAPID 未设 503/隐藏不崩;ntfy token 不入 settings.json/argv(H3, AC-A1.7);陈旧/外来 Origin/坏 token → 403(M1, AC-A1.8);DONE 低优通知 |
| A2 |
按住 🎤 说话→打进终端;软键盘不弹;无 API 隐藏不崩;无音频抵达服务端(L2) |
| A3 |
内置 chips + 可持久调色板,同键栏路径发送;标签惰性文本(无注入) |
| A4 |
离散人话时间线;/…/events 受 TIMELINE_MAX 界;安装器含 PostToolUse(H1);hook body sanitize 后显示(SP7);TIMELINE_ENABLED=0 空 |
| A5 |
静默达 STUCK_TTL 经 A1 恰发一次;恢复后重新武装;扫描覆盖 attached 会话且无新 timer(H4);0 禁用 |
| B1 |
结构化 diff + numstat 一致;working/staged;非 git→404;超大→truncated 不 OOM;<script> 作文本(SP7);纯解析单测≥80%;untracked 经 porcelain 标注(L3) |
| B2 |
tab 仪表;晚加入设备 attach 立即收到遥测(M3, AC-B2.3);缺字段宽容/陈旧置灰;非本机→403;telemetry 序列化往返测试;R0 已确认字段(H5) |
| B3 |
在 WORKTREE_ROOT 建出并开 agent;缺/伪造 Origin→403;非法分支名拒绝不执行;realpath 后仍拒 symlink 绕过(M2);并行不互踩;纯函数+集成测试 |
| B4 |
plan 模式启动;plan 门三按钮且 approve+auto 切 acceptEdits;tool gate 仍两按钮无回归;多设备一致;ALLOW_AUTO_MODE=0 禁 auto;非法 mode 边界拒绝;R0 已确认 schema(H5) |
| 跨特性 |
共享文件单 owner 无 clobber(C3);env 命名约定(L4);held-curl --max-time>permTimeoutMs 经 config 校验(L5);共享状态/仪表组件无 DOM 争用(M4) |
全局门槛:所有纯核心(diff/statusLine/timeline 解析、branch/dir 校验、push payload、label 派生)单测 ≥80%;端点经集成测试(真起 server);A1 push 在模拟 TLS/安全上下文下注册成功。
9. 一句话总结 (One-line Summary)
v0.7 走开即工作台:A 段靠"hook 触发 → 出站推送 → 锁屏决策回 held 通道"闭合走开循环(核心修正:hold 门要在零客户端但有推送订阅时也 hold,且 push 需 HTTPS);B 段靠"只读 git-exec(diff)+ 受控写 git-exec(worktree)+ loopback JSON 摄入(statusLine)+ held-permission 新 gate(plan)"在终端之上长出工作台。服务端仍不解析一个终端字节;9 个特性合并为一个单-owner-per-shared-file 的波次计划,B2/B4 受一个阻断式 schema 研究 spike 把关。