古法编程模式

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/clientdsh 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.bakpreset.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_offerAI 写完骨架后注册题目 → 交互提问 接受挑战/跳过;接受→solving,跳过→清除并正常实现。files 必填(骨架必须先写入真实项目文件);scope 声明大请求的拆分范围;max_hints(1-20,缺省 3)设定本题提示预算;difficulty(0-1)出题自评
gufa_hintsolving 期用户每次求助/提问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,name gufa-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.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', 零值),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_offeroffers+1(仅首次注册;提问无应答后的重入不重复计)
    接受accepts+1、difficultySum+自评难度、difficultyCount+1、hintsBudget+maxHints
    跳过skips+1
    gufa_submitattempts+1
    gufa_hinthintsUsed+1
    gufa_give_upgiveUps+1、modes.takeover+1
    fail+放弃fails+1、giveUpsAfterFail+1、modes.takeover+1、难度修正入账
    passpasses+1、modes[收尾]+1、scoreSum+score、weightedScoreSum+score×最终难度、scoredDifficultySum+最终难度、难度修正入账
    gufa_exitexits+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.tszod schema 与解码(持久化值校验)
host/settings-store.tssettings 命名空间进程内单例 + 读-改-写
host/challenge-store.ts按会话键控的挑战状态读写(含进程内缓存)
host/authority.ts活体 agent + 直接人类轮次校验
host/transitions.ts纯迁移函数(可测,含 consumeHint/reviseDifficulty/recordScore)
host/stats.ts统计纯记录函数 + schemastery schema + StatsRecorder(settings 读-改-写)
host/ask.tsuserQuestions 交互提问封装
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 层自动补默认)。