古法编程模式
August 31, 2026 · View on GitHub
项目代号
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 行的 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 注册,7 个)
| 工具 | 触发 | 行为 |
|---|---|---|
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_exit | 用户要中止 | 任意阶段清除状态(文件保留) |
- 输出统一为指示文本(string schema),状态机效果在
execute内落定; - 交互提问复用平台
userQuestions.ask()(与内置 ask_user_question 同一卡片 UI)。
5. 行为约束(提示段 + 权限)
- 提示段(
systemPrompt.section,namegufa-mode,order 150): 常驻行为契约——出题门槛(真实多行编码、files 必填、大请求只拆一块)、 solving 期绝不代写/绝不透露完整解法、每次求助必须走 gufa_hint 并按比例 递进具体度、预算耗尽即停、各阶段的收尾语义。当前挑战状态由工具结果实时提供。 - 权限(
authority.ts,仿内置 goal 工具):状态变更要求调用 agent 为 活体实例、处于其活动驱动、且当前轮次存在直接人类消息;子代理只读。 - 软约束性质:solving 期「不代写」以提示段为准(趣味模式的自觉);
子代理是不受控盲区。硬拦截(
tools.guard拦 write/edit)列为路线图。
6. 状态存储:settings 命名空间(按会话键控)
挑战状态不落工作区文件、也不写会话日志,统一持久化在 settings 命名
空间的 challenges 节(Record<sessionId, GufaState | null>):
- 为什么不用会话日志:
dsh-session-persistence读取日志时只接受内置 事件类型,仓库外事件必须带ignorable信封标记,而Session.appendAPI 无法设置该标记——写自定义会话事件会让会话拒绝打开(本插件早期版本 的gufa/change/gufa/meta/gufa/state即踩此坑,历史日志由scripts/repair-gufa-log.mjs修复); - 读写:Host 侧
ChallengeStore做读-改-写并带进程内缓存(同轮次连续 读写一致,settings 异步落盘不阻塞工具);null清除即删除键,不留残留; - 读取校验:持久化值经 zod
gufaStateSchema解码,非法数据视为无挑战; - Client:状态条经平台
settingsScope订阅同一命名空间,取challenges[sessionId](dock Slot 自带sessionIdprop),跨刷新/重启 仍在;统计卡片复用同一 scope 绑定; - 数据跨 DSH 重启持久化;旧会话日志中被标记 ignorable 的 gufa 事件为 惰性残留,不影响读取。
7. Client:状态条与统计卡片
- 状态条:Slot
conversation.input.dock(idgufa,order 15); 内容:古法徽标 + 题目 + 阶段标签 + 提交次数/提示消耗(X/Y); solving 期常驻「提交」「放弃」按钮:inputActions.setDraft(...) + submit(), 等价于用户输入这两个词,由 Agent 走正常 gufa 工具流程。 - 统计卡片:Slot
settings.plugin.item(key =dsh-human-coding,即 settings 命名空间);折叠卡片顶部一条总量摘要(共 N 次挑战 · 通过率), 分组展示:挑战概览(提议/接受/跳过/接受率)、挑战结果(通过/未通过/ 通过率/中途放弃/失败后放弃/主动退出)、收尾分布(保留/微调/重写/接手 四格分别计数)、答题过程(提交总数/平均提交/提示消耗/提示消耗率)、 评分(平均完成度/表现分/平均难度/已评分题数);底部「清零统计」 按钮(confirm 后set('stats', 零值),Host 只读时禁用)。 - 数值呈现:表现分与难度均固定两位小数(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_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 层自动补默认)。