# 古法编程模式 · 设计文档 > 项目代号 `dsh-human-coding`:DeepSeek Harness 的双面插件包(Host + Client) > 与随包的「古法编程」Agent 预设。本文件记录完整设计,供实现与后续迭代对照。 ## 1. 目标 趣味模式,让人重新体验「没有 AI 代写」的编程过程: - 用户照常提出需求;AI 选择把需求变成一道**编程挑战**: AI 只实现代码骨架(真实项目文件 + TODO 标记)与测试; - 用户补全实现,期间 AI **只答疑、给渐进提示,拒绝代写**; - 用户提交后 AI 验收(测试 + 评审),不通过可继续或放弃; - 通过后由用户选择收尾方式:保留自己的写法 / AI 略做修改 / AI 彻底重写; - 放弃或 AI 接手后,AI 恢复正常实现能力; - 每道挑战的接受/跳过/判定/分数/难度沉淀为**跨会话的用户表现统计**, 在「设置 → 插件」卡片中展示长期趋势与表现分。 ## 2. 交付物与安装平面 | 部分 | 位置 | 说明 | |---|---|---| | 插件包 `dsh-human-coding` | 本仓库根目录(Host: `src/host`,Client: `src/client`) | `dsh plugin add .` 装入 Profile | | Agent 预设「古法编程」追加片段 | `preset/`(`agent.cordis.yml` 只含 persona 行与挂包行;`preset.yml` 为显示元数据) | 由安装脚本外科手术式并入用户预设 | | 安装脚本 | `scripts/install.sh`(bash)与 `scripts/install.ps1`(PowerShell) | 构建 + `dsh plugin add` + 片段合并 | 按用户要求:**不直接修改 `.dsh` 目录**。安装流程见 `README.md`:先运行安装 脚本(构建 → `dsh plugin add` → 检测预设:已存在则只合并;不存在则自动定位 已安装的 standard 预设并以其为基础创建 → 在 Profile 的 `cordis.patch.yml` 写入常驻宿主行 `tool-human-coding-host`(statsOnly,只注册统计命名空间与 会话投影,让统计卡片不依赖会话)),自动创建失败时再手动以 standard 为基础创建 `human-coding` 预设并重跑脚本;脚本只替换 persona 行的 prefix/suffix(DSH 0.1.5-rc.1 起 dsh-persona 由单个 `text` 拆为这两个键)、 追加 tool-human-coding 行,其余内容逐字节保留,修改前备份为 `agent.cordis.yml.bak`,`preset.yml` 仅在缺失时创建——**不会破坏用户的其他 设置项**。`preset/` 只保留相对 standard 要添加/替换的内容,不是完整组合。 插件更新后**重跑安装脚本**即可同步 persona 片段与依赖。 > 平面说明:统计命名空间的消费者在设置 UI(Agent 平面之外),因此由 > 宿主行注册;古法工具与提示段只在「古法编程」预设内注册。投影与命名 > 空间注册均为进程级幂等(模块守卫/单例),两行并存不冲突。 ## 3. 状态机 ``` gufa_offer ──[提问: 接受挑战 / 跳过]──┐ ┌────────┐ 跳过→清除状态→AI 正常实现 │ │ (null) │◄──────────────────────────────────────┐ │ └───┬────┘ (无活动挑战 = 普通模式) │ │ │ 接受挑战 │ │ ▼ │ │ ┌─────────┐ 「提交」→ gufa_submit → reviewing │ │ │ solving │◄───────────────────────────┐ │ │ └───┬─────┘ fail+继续(不扣预算) │ │ │ │ gufa_hint(每次求助 hintsUsed+1) │ │ │ │「放弃」→ gufa_give_up │ │ │ ▼ │ │ │ ┌──────────┐◄─ fail+放弃 ───────────────┘ │ │ │ solution │◄─ pass+略做修改 / 彻底重写 ──────────────┤ │ │(AI 接手) │ │ │ └────┬─────┘ │ │ │ 完成 → gufa_exit / 状态清空 → 回到 (null) ──────┘ │ pass+采用你的写法 → 状态清空 → 总结讲解 → 回到 (null) ``` - 无活动挑战时投影为 `null`,插件对 Agent 行为零影响(普通模式); - `offering` / `reviewing` 是提问挂起态:交互式提问无应答时停留原地, 模型转文字询问后**以相同注册/判定重调工具**(可重入设计); - 统计写入与状态迁移同点发生(见 §8),score/difficulty 在判定落定时 即写状态,收尾选择后一次性入账,重调幂等。 ## 4. 模型工具(Host 注册,8 个) | 工具 | 触发 | 行为 | |---|---|---| | `gufa_offer` | AI 写完骨架后 | 注册题目 → 交互提问 接受挑战/跳过;接受→solving,跳过→清除并正常实现。**files 必填**(骨架必须先写入真实项目文件);scope 声明大请求的拆分范围;max_hints(1-20,缺省 3)设定本题提示预算;difficulty(0-1)出题自评 | | `gufa_hint` | solving 期用户每次求助/提问 | hintsUsed+1(每次求助扣一次预算);按已用/预算比例要求递进具体度;超支后禁止再给提示,引导提交/放弃 | | `gufa_status` | 任意 | 只读:阶段/题目/提交次数/提示消耗与预算/难度/分数 | | `gufa_submit` | 用户说「提交/验收」或点状态条按钮 | attempts+1 → reviewing,指示模型跑测试+评审 | | `gufa_decide` | 评审后 | verdict=fail → 提问 继续/放弃(继续不扣预算,反馈提示不低于已达具体度);verdict=pass → **必给 score(0-1 完成度分数)**、可选 difficulty 修正,再提问 采用你的写法/AI 略做修改/AI 彻底重写 | | `gufa_give_up` | 用户说「放弃」或点按钮 | solving → solution(takeover),AI 正常实现 | | `gufa_cancel` | 题目本身有缺陷(骨架写错、需求不再成立等) | **作废当前挑战:不留任何记录**——清除状态并回滚本题全部计数(提议/接受/难度/提示预算/提交/提示消耗),不写逐题 history;仅对 offering/solving/reviewing 有效,solution 阶段请用 gufa_exit | | `gufa_exit` | 用户要中止 | 任意阶段清除状态(文件保留) | - 输出统一为**指示文本**(string schema),状态机效果在 `execute` 内落定; - 交互提问复用平台 `userQuestions.ask()`(与内置 ask_user_question 同一卡片 UI)。 ## 5. 行为约束(提示段 + 权限) - **提示段**(`systemPrompt.section`,name `gufa-mode`,order 150): 常驻行为契约——出题门槛(真实多行编码、files 必填、大请求只拆一块)、 solving 期绝不代写/绝不透露完整解法、每次求助必须走 gufa_hint 并按比例 递进具体度、预算耗尽即停、各阶段的收尾语义。当前挑战状态由工具结果实时提供。 - **权限**(`authority.ts`,仿内置 goal 工具):状态变更要求调用 agent 为 活体实例、处于其活动驱动、且当前轮次存在直接人类消息;子代理只读。 - **软约束性质**:solving 期「不代写」以提示段为准(趣味模式的自觉); 子代理是不受控盲区。硬拦截(`tools.guard` 拦 write/edit)列为路线图。 ## 6. 状态存储:settings 命名空间(按会话键控) 挑战状态**不落工作区文件、也不写会话日志**,统一持久化在 settings 命名 空间的 `challenges` 节(`Record`): - **为什么不用会话日志**:`dsh-session-persistence` 读取日志时只接受内置 事件类型,仓库外事件必须带 `ignorable` 信封标记,而 `Session.append` API 无法设置该标记——写自定义会话事件会让会话拒绝打开(本插件早期版本 的 `gufa/change`/`gufa/meta`/`gufa/state` 即踩此坑,历史日志由 `scripts/repair-gufa-log.mjs` 修复); - **读写**:Host 侧 `ChallengeStore` 做读-改-写并带进程内缓存(同轮次连续 读写一致,settings 异步落盘不阻塞工具);`null` 清除即删除键,不留残留; - **读取校验**:持久化值经 zod `gufaStateSchema` 解码,非法数据视为无挑战; - **Client**:状态条经平台 `settingsScope` 订阅同一命名空间,取 `challenges[sessionId]`(dock Slot 自带 `sessionId` prop),跨刷新/重启 仍在;统计卡片复用同一 scope 绑定; - 数据跨 DSH 重启持久化;旧会话日志中被标记 ignorable 的 gufa 事件为 惰性残留,不影响读取。 ## 7. Client:状态条与统计卡片 - **状态条**:Slot `conversation.input.dock`(id `gufa`,order 15); 内容:`古法` 徽标 + 题目 + 阶段标签 + 提交次数/提示消耗(X/Y); solving 期常驻「提交」「放弃」按钮:`inputActions.setDraft(...) + submit()`, 等价于用户输入这两个词,由 Agent 走正常 gufa 工具流程。 - **统计卡片**:Slot `settings.plugin.item`(key = `dsh-human-coding`,即 settings 命名空间);折叠卡片顶部一条总量摘要(共 N 次挑战 · 通过率), 分组展示:挑战概览(提议/接受/跳过/接受率)、挑战结果(通过/未通过/ 通过率/中途放弃/失败后放弃/主动退出)、收尾分布(保留/微调/重写/接手 四格分别计数)、答题过程(提交总数/平均提交/提示消耗/提示消耗率)、 评分(平均完成度/表现分/平均难度/已评分题数);底部「清零统计」 按钮(confirm 后同时 `set('stats', 零值)` 与 `set('history', [])`, Host 只读时禁用)。 - **逐题记录表格**(评分组下方):标题/完成度/难度/表现分(贡献)四列, 贡献值 = 该题在总表现分中的占比(performanceContributions), **所有贡献之和 === 总表现分**(表尾合计行展示这层关系);每页固定 5 项翻页;默认按贡献降序,点击表头按标题(locale 序)/完成度/难度/ 贡献切换排序、再点同列切升/降序;标题超长省略号 + 原生 title 提示, 贡献列按「贡献 ÷ 最大贡献」着色(最高最绿、0 贡献中性)。 - 数值呈现:表现分与难度均**固定两位小数**(0.40、0.35 样式; 0.35 的浮点误差由 `toFixed(2)` 正确舍入);表现分带公式 tooltip。 - 配色为统一的红→黄→绿连续色阶(score-color.ts):按数值计算 HSL 色相(0° 红 → 60° 黄 → 120° 绿,饱和度 88%、亮度 52%),以**内联 样式**直接作用于数字,不依赖 CSS 类、主题变量或注入时机。分数越高 越绿;难度越高越红;提示消耗率超 100% 为红色。计数保持中性。 - 数据通道:卡片经平台 `settingsScope.bind({namespace})` 读写,不自定义 RPC; useSyncExternalStore 使用缓存引用快照,命名空间未注册时展示 unavailable 文案而非假 loading。 ## 8. 跨会话统计持久化 - **存储**:settings 服务命名空间 `dsh-human-coding`,节面 `{ stats: GufaStats }` (schemastery schema,全字段默认值);Host 侧 `StatsRecorder` 做读-改-写, 每次记录先 `scope.get()` 最新值再应用纯函数并 `update` 持久化。命名空间 是进程全局的,因此 **scope 进程内模块级单例共享**:首个会话实例注册、 其余实例复用——多会话并发时统计不丢失、不重复注册;写入失败仅告警, 不影响挑战流程; - **记录点**(与状态迁移同点): | 事件 | 入账 | |---|---| | gufa_offer | offers+1(仅首次注册;提问无应答后的重入不重复计) | | 接受 | accepts+1、difficultySum+自评难度、difficultyCount+1、hintsBudget+maxHints | | 跳过 | skips+1 | | gufa_submit | attempts+1 | | gufa_hint | hintsUsed+1 | | gufa_cancel | **回滚本题全部足迹**:offers-1;已接受则 accepts-1、difficultySum-难度、difficultyCount-1、hintsBudget-maxHints;attempts-本题提交数、hintsUsed-本题消耗数(全部钳制 ≥0,不写 history) | | gufa_give_up | giveUps+1、modes.takeover+1 | | fail+放弃 | fails+1、giveUpsAfterFail+1、modes.takeover+1、难度修正入账 | | pass | passes+1、modes[收尾]+1、scoreSum+score、weightedScoreSum+score×最终难度、**scoredDifficultySum+最终难度**、难度修正入账 | | gufa_exit | exits+1(仅 offering/solving/reviewing;solution 阶段已是收尾,不计) | - **推导指标**(Client 即时计算,**结果类指标一律由逐题记录 history 计算,不再依赖聚合存储**;offers/skips/accepts/exits 等事件计数保留 聚合):接受率 = accepts/offers;通过率 = passes/(passes+fails);平均 提交 = attempts/已结束题数;提示消耗率 = hintsUsed/hintsBudget(>100% 告警色);平均完成度 = scoreSum/scoreCount;表现分 = 每题表现分 (完成度 × 难度 × 100,**放弃/未通过按 0 分计**,难度取最终值)从高 到低取前 100 题,按 0.95 几何衰减加权(第 i 高分权重 0.95^(i-1)), 除以**固定**的 100 题全满分归一化因子(100 × Σ_{i=0..99} 0.95^i, 不足 100 题的位次按 0 分计)再 × 100,满分 100 分制。 ## 9. 工程结构 ``` dsh-human-coding/ ├─ package.json # 双出口 + dsh.client 声明(platform: web) ├─ tsconfig.json # strict / bundler 解析 / noEmit(类型检查) ├─ tsdown.config.ts # Host 半边:Node ESM + dts ├─ tsdown.client.config.ts # Client 半边:browser CJS(react 外部化) ├─ scripts/wrap-client.mjs # 把 CJS 包成 __ModuleLoader__ 惰性工厂格式 ├─ src/ │ ├─ shared/state.ts # 纯类型/常量(Host/Client/测试共享,含 GufaStats) │ ├─ host/ # 见 §10 │ └─ client/ # index.tsx + GufaDock.tsx + StatsCard.tsx ├─ preset/ # 「古法编程」预设的追加内容片段 ├─ scripts/ # install.sh / install.ps1(合并式安装)+ wrap-client.mjs(客户端打包) ├─ test/ # vitest 单元测试 └─ docs/ + README.md # 设计与安装文档 ``` 构建:`pnpm install && pnpm build`(typecheck → tsdown host → tsdown client → wrap)。Client bundle 与平台同款(`window.__ModuleLoader__.load({id, factory})`), `require('react')` 由平台种子词提供。 ## 10. Host 模块划分 | 模块 | 职责 | |---|---| | `shared/state.ts` | 状态类型、阶段/选项常量、统计类型与零值(零依赖) | | `host/domain.ts` | zod schema 与解码(持久化值校验) | | `host/settings-store.ts` | settings 命名空间进程内单例 + 读-改-写 | | `host/challenge-store.ts` | 按会话键控的挑战状态读写(含进程内缓存) | | `host/authority.ts` | 活体 agent + 直接人类轮次校验 | | `host/transitions.ts` | 纯迁移函数(可测,含 consumeHint/reviseDifficulty/recordScore) | | `host/stats.ts` | 统计纯记录函数 + schemastery schema + StatsRecorder(settings 读-改-写) | | `host/ask.ts` | userQuestions 交互提问封装 | | `host/prompt.ts` | 模式提示段文本 | | `host/tools/*.ts` | 八个工具 + 输出/卡片共享部件 | | `host/index.ts` | 插件入口(name/inject/Config/apply) | ## 11. 已知限制与路线图 - solving 期「不代写」是软约束(提示段 + 权限),子代理不受控; 硬守卫(`tools.guard` 拦截对题目文件的 write/edit)为 v2; - 状态条按钮以「注入用户消息」实现(平台 `InputActions` 契约), 若未来该接口变化需跟进; - 统计按 Profile 全局聚合(所有启用该模式的会话共用一份,scope 进程内 单例共享);并发 update 仍以最后写入者收敛(单用户场景影响可忽略); 尚无按题/按日明细与时间趋势; - 完成度分数的最终总分公式:表现分 = 单题表现分(完成度×难度×100) 前 100 高按 0.95 几何衰减加权、固定按 100 题归一化(不足 100 题的 位次按 0 分计)(百分制); - 尚无 locale 集成(状态条中文硬编码;统计卡片已有中英词典); - 挑战状态存 settings(按会话键控),会话日志不再写入自定义事件; 状态结构演进时更新 zod schema 默认值即可(settings 层自动补默认)。