Repository navigation
Releases: YixuAn-13/we-need-ds
Release list
v5 - Turn-Aware DSH Minimal Simulation (v5.1)
v5.1(最新有效版本)
- 判定轮:DSH 官方单行人格
You are a helpful software engineer assistant.+ 工具裁至 Bash/Edit 两件套(常态生效,不限首轮) - 执行轮(v5.1):全量 MCP/Skill 工具放行 + DSH 人格同步切换(
executionDshPersona: false可退回完全透传) - 纯请求结构判定,零运行时状态,重试幂等
版本沿革
| Tag | 语义 | Commit |
|---|---|---|
| v1 | 双阶段工具解耦 | v1.0.0 (27367a6) |
| v2 | 消耗型武装窗口 | d8b6a4b |
| v3 | 链感知武装窗口 | 69cceab |
| v4 | 判定轮裁剪 + 提示词层引导 | 18ab59c |
| v5 | 轮次感知 DSH 极简模拟,武装窗口退役 | 3adc757 → a2b7b57 |
测试:17 组模拟断言 + 18 组端到端断言全部通过。
v2.4.4 接管态路由 fail-safe + 误杀防护 + 漂移告警
审计三项修复 (v2.4.4)
机制版本不变 (v5.2),本次为安全边界与可观测性加固。
🔴 D2 — 接管态路由 fail-safe
cc-haha 接管态下,若请求携带的 API Key 未在 keyMap 命中,代理不再兜底 defaultUpstream,而是显式返回 502。防止被接管 provider 换了 key 后请求被带新 key 静默错发到原始上游(违背"502 显式失败优于静默错发"设计原则)。无 key 请求与纯 Claude Code 模式兜底语义不变。
⚪ D6 — killDaemonOnPort 误杀防护
boot/restart 清理残留 daemon 前加双闸门:先 /health-check 确认是本插件自家 daemon 才杀;无应答则检查进程映像须为 node.exe,端口被无关程序占用时返回 foreign 不杀(boot 跳过、restart 中止并提示改端口)。
🟡 D1 — doctor 接管漂移告警
账本处于接管态但被接管的 provider 已从池中消失(id 漂移)/ baseUrl 被外部改回直连 / 当前无任何 provider 指向代理时,doctor 打印醒目的接管漂移告警及恢复命令(纯输出、零行为改动)。补上"cc-haha 不跑钩子 → 接管悬空时 We need 静默失效"的盲区。
测试
- test_full 144 → 150 断言(新增 A35–A40:D2 未命中拒绝错发/命中正常/无 key 兜底;D6 空闲端口/自家 daemon 识别杀掉)
- test_stress S4 夹具修正(旧夹具经由 pb 的 key 走 pa 上游,正是 D2 修复的错发路径假绿,改用被接管 provider 自身 key)
- 三套件全绿 (full 150 + consume 18 + simulation 18) + stress 全部通过
明确不修的项 (设计边界内)
- D3 直连副本可误激活:逃生口与陷阱同体,已有标记与对账
- D4 cc-haha 轨无自愈:用户否决系统级注册的直接代价,副本+其余 provider 直连已防死锁
- D5 keyMap 明文:与 providers.json 同等敏感等级,无新增泄露面
- D7 探测经裁剪:裁剪不改协议结构,无害
v2.4.3 — 判定轮注入 thinking + 剥离 system-reminder,修复 "We need" 实测不触发
两根因修复(16 组对照实验闭合因果链)
实测仍无法触发 "We need" 后,做了三轮共 16 组直连官方端点的对照实验,锁定触发条件链:单行人格 + thinking 字段存在 + user 消息 reminder 噪声 < 3 份,缺一不可。旧版只满足第一条。
🔴 缺口 1 — thinking 字段从未注入(第一根因)
thinkingBudget默认0使applyThinkingBudget成 no-op → 判定轮发出"极简但无 thinking"形态(实验 P 组 = ❌)- 默认
0 → 8000(loadConfig与config.json双处),判定轮在 Anthropic 路径注入thinking:{type:"enabled",budget_tokens:8000} - 新增 max_tokens clamp 守卫:宿主小 max_tokens 请求(标题生成等,≤ budget)跳过注入,防
max_tokens > budget_tokens400;OpenAI 路径维持不注入(防中转站 400)
🔴 缺口 2 — system-reminder 噪声未剥
- CLAUDE.md / memory / MCP instructions / currentDate 以
<system-reminder>注入 user content,几十份起步(实验 F/G 组 = ❌) - 判定轮新增
stripSystemReminders:按标签剥所有 user 消息(string 与数组 text 块,含历史轮);整条剥空保留原文(防空 content 400);非 text 块(tool_result/image)原样保留 - Q 组关键洞察:抑制来自标签结构本身,非文本量 → 按标签匹配不按长度
🟡 缺口 3 — 接受为设计边界
历史 tool_use/tool_result 块抑制字面前缀(H 组),但删除会致 tool_result 孤儿 → 400,协议上不可裁。推理质量不崩,"We need" 是可观测量非目标。
🧪 端到端活体实测(发布前对官方端点验证)
构造"脏请求"(12 块人格 + 30 工具 + 4 份 reminder)过塑形管线后直发 api.deepseek.com/anthropic:
- 英文任务 → thinking 块以字面 "We need answer user asks..." 开头,链长 9000+ 字符 ✅
- 中文任务 → 同构深度规划链以 "我们需要回答中文…" 开头(模型随任务语言镜像前缀——历史上这常被误读为"没触发")
input_tokens30k+ → 163,证明裁剪真实发生
其他
- 机制版本 v5.1 → v5.2;测试 135 → 144(新增 E10–E18:注入/clamp 守卫/reminder 剥离全形态/非 DS 透传);三套件全绿(含 stress 25)
- 非 DS 模型底线不变:字节级原样透传
v2.4.2 — 并发/极端场景压力审查 + 生产端到端复验
v2.4.2 — 2026-09-08
Added (concurrency & extreme-scenario stress regression)
- New
test_stress.js— 25 assertions covering process-level concurrency and hostile state mutation that the main suite (single-process, sequential) does not reach. Scenarios: 20-way concurrent forwarding under takeover (no cross-talk, no packet loss/duplication at the upstream), 10-round on/off flapping (every round converges to a consistent state, zero copy leaks), 8 concurrentctlprocesses racing on/off (file-lock + atomic-write keepproviders.jsonvalid with no stray.tmp), an in-flight slow request vs a concurrentoff(neither blocks the other), user hand-editingproviders.jsonmid-takeover (deleting the hooked provider / switchingactiveId) thenoff(no crash, ledger retains the deleted provider's original upstream, surviving providers untouched), daemon killed mid-session → dead-state detection →bootrecovery re-takeover, concurrent takeover switching A→B→A (exactly one copy, single-provider invariant holds), and 30 interleaved health/forward/ctl requests. All green across repeated runs. - Production end-to-end re-verified with a fresh
off → oncycle against the real DeepSeek upstream:offrestored the provider to direct and removed the copy;onre-hooked it and created the escape copy; a real request through:20329returned HTTP 200, the proxy loggeddecision turn DSH-minimal, input tokens were trimmed (full 7-tool payload → DSH minimal set), the "We need" reasoning chain fired, and every tool the model requested fell within the trimmed{Bash, Edit}set — proving the trimming is real and the model acts inside the minimal environment.
Docs
- Engineering report: registered
test_stress.jsin the component table, test-system section, and file tree.
v2.4.1 — 端口边界精确化 + M4 守卫补测
v2.4.1 — 2026-09-07
Fixed (port-boundary precision + guard coverage)
isSelfProxyUrl/isProxiedUrlsubstring false-match fixed. Both usedurl.includes(':<port>'), so a real upstream on port203290was misjudged as "the proxy itself" (:20329is a substring of:203290) → rejected → 502. Same class as the already-fixed M5. Now matched with:${port}(?![0-9])so the port must be followed by a non-digit (slash, colon, or end). Fail-safe direction, but wrong behavior — fixed for correctness. Tests M1a–M1c, M2a–M2c.- M4 malformed-path guard now has end-to-end coverage. The
/^\/(?!\/)/rejection of//hostabsolute-form paths existed inproxy.jssince v2.4.0 but had no test; added a live-daemon test proving//evil.example.com/v1/messages→ 400 while/v1/messages→ 200 passthrough. Tests M4a/M4b. - Test flakiness reduced. Bumped the post-
killPortsettle wait from 300ms to 600ms in Phase L — a killed daemon occasionally hadn't fully released its port before the next spawn, causing a rare spuriousEADDRINUSEfailure (a known trap, not a logic bug).
Tests
- New Phase M (test_full, +8): port-boundary precision for both URL predicates, malformed-path guard end-to-end. 135 assertions green.
v2.4.0 — 深度审计 + 三方重试对照全面修复
v2.4.0 — 2026-09-07
Fixed (deep audit + third-party retry comparison)
- H1 — invalid
--providerno longer destroys an existing takeover.enableInterceptionranrestoreAllProxied/removeAllCopiesbefore validating the requested provider, so a bad--providerid wiped the current hook then bailed. Validation now happens first; every rejection path returns before any mutation. Regression test L9a/L9b. - Retry storm eradicated (classified retry). The proxy sits between aggressive-retry clients (Claude Code CLI retries ~10×) and an upstream that may itself retry; the old blanket "retry on any error" could stack into 30+ attempts. Now: deterministic network errors (
ENOTFOUND/EAI_AGAIN/EHOSTUNREACH/EACCES/EPERM/invalid URL) fail fast with no retry; only transient failures (empty body, reset, 408/409/425/429, 5xx, timeouts) retry. DefaultupstreamRetrieslowered2 → 1.ECONNREFUSEDdeliberately stays retryable (covers local-relay restart windows). Tests L2a–L2d, L5, L6a/L6b. - Client disconnect cancels upstream. A hoisted per-request
ctx.clientGoneflag (set onresclose) is checked before every attempt, after the response arrives, and in the catch — a gone client no longer triggers further retries or leaves a half-forwarded stream. A latentconst ctx = {}shadowing bug inside the request handler (which had silently disabled the whole mechanism) is removed. Test L7. Retry-Afterclamped to 0–60s and the exponential backoff capped at 60s, so an absurd header can't wedge the proxy. Tests L1a–L1d.- Standard error bodies. Proxy-generated 502/400 responses now emit Anthropic (
{type:"error",error:{type:"api_error",message}}) or OpenAI ({error:{message,type,code}}) shapes based on the request path, instead of an ad-hoc{error:{message}}. Tests L3a/L3b, L5b. - M2 —
dssubstring false-match fixed.isDeepSeekProModel/providerHasTargetModelsmatched any normalized name containingds, somodels,adsl, etc. could false-trigger. Nowdsmust be a standalone token (delimited by separators/boundaries). Tests L4a–L4e. - M1 — hook intent consistency.
SessionEndnow callsrecoverOrphans(restore providers to direct but keepenabled) instead ofdisableInterception(which cleared the intent);SessionStartonly auto-hooks when the ledger isenabled, so a deliberateoffis no longer silently re-opened next session. Tests L10a/L10b, L11. - M3 — stale keyMap pruning. Keys no longer present in the provider pool are dropped on takeover, preventing routing to a deleted provider's old upstream.
- M4 — malformed request-path guard. Paths not starting with a single slash are rejected with 400 (blocks
//host-style absolute-form / SSRF-ish probes). - M5 — exact port match in
killDaemonOnPort. Port matching now compares the full trailing segment instead of a substring, so killing:2032no longer matches:20329. - L2 —
env/defaultsentinel restored inresolveTargetBaseUrl(a prior cleanup had dropped it, hanging the env-fallback route). - L3 —
--providermissing-value validation inctl.js(a trailing--providerwith no argument now errors clearly instead of silently taking over the default).
Docs
- README (zh + en):
upstreamRetriesdefault2 → 1with classified-retry rationale; backoff/Retry-Afterclamp documented; hook semantics rewritten (SessionEnd keeps intent, SessionStart respectsoff); DS-detection rule updated to the standalone-ds-token form; version line2.3.x → 2.4.x.
Tests
- New Phase L (test_full, +28): backoff growth/clamp, retry classification, standard error bodies, M2 false-match regression, ENOTFOUND fast-fail, 5xx retry count, client-disconnect no-retry,
/ctl onproviderId end-to-end + H1 regression, session-end intent retention, session-startoffrespect. 127 assertions green.
we-need-ds v2.3.0
v2.3.0 — provider 选择接管 + 关机持久化根因显式告知
- 新增
ctl list:列出全部 provider,标注 🎯含DS模型 / ⭐当前默认 / 🔌代理中,DS 优先排序 on --provider <id|name>:直接接管指定 provider,无需先在 cc-haha 手动设默认- 接管非默认 provider 时自动同步 activeId(sidecar 按 activeId 路由,必须同步)
- 拒绝路径防呆:指定非 DS / 不存在的 provider 在写状态前拒绝,不污染 providers.json
- skills(on / we-need-ds) 改为先列清单再让用户选择接管目标
- README(zh+en) cc-haha 段顶部加醒目警告框:接管改写 providers.json 且关机/重启后持久保留,副本 + 未接管 provider 为兜底
- test_full 新增 Phase K (+10),共 99 断言全绿
v2.2.1 — 统一 daemon 端口释放
restart/boot共用killDaemonOnPort:taskkill 后轮询确认端口释放,根治残留进程占端口导致的假接管- README 澄清接管只看 provider 声明的 models 字段
v2.2.0 — 单服务商接管 + 直连副本逃生口(根治重启死锁)
核心:根治重启死锁 —— 单服务商接管 + 直连副本逃生口
代理不再把 providers.json 里全部 27 个服务商的 baseUrl 批量改写到代理端口(旧版一旦 daemon 死亡即全局瘫痪、连恢复命令都发不出)。现在:
- 只接管当前默认(activeId)且确实提供 DeepSeek Pro 模型的那一个服务商,其余服务商保持各自真实上游,物理层面 100% 直连可用。
- 接管时克隆一份
· 直连副本(baseUrl 指向原始真实上游),作为 cc-haha 下拉框里随时可切的逃生口;daemon 死了切到它即恢复直连,再从容跑 off/boot/on。 - 副本兼任冗余账本:即使 runtime-state.json 丢失/损坏,也能从副本 baseUrl 把本体救回真实上游。
- 两段式安全释放:先还原本体 → 重读验证确已直连 → 才删副本;中途崩溃绝不让用户失去备用出口。
- 旧全量接管态自动迁移:下次 on/boot 先把指向代理的 provider 还原、清旧副本,再按新方案接管;切换默认服务商重跑 on 时旧本体自动还原、副本始终只留一份。
文档
- README(中/英):cc-haha 使用新增「开启前先把当前真正在用的服务商设为默认」,并说明直连副本逃生口与「退出前手动 off」习惯。
- 新增 docs/ENGINEERING_REPORT.md 完整工程报告(架构/实现细节/问题史/边界/测试体系)。
测试
- test_full.js 新增 Phase J(+10 项):单服务商接管、非 active 不动、副本字段语义、账本剪枝、幂等不重复建副本、off 还原+清副本、账本丢失副本救援、非 DS active 不接管。全套 89/89 通过,生产文件零污染。
机制版本 v5.1(轮次感知 DSH 极简模拟)不变;插件 semver 2.2.0。
v2.1.13 — 失败还原可逆(当轮恢复裁剪) + 中途断开透明化
本次更新
修复:失败还原不再是单向的
- 用户点破:v2.1.12 的"失败即还原"是单向的——还原后即使 daemon 恢复,后续消息也一直直连不裁剪,直到手动
on。 - 现在
user-prompt-submit新增分支:账本enabled+ daemon 存活 + 零 provider 被接管 → 自动重接管。恢复后的第一轮立即回到代理+裁剪。 - 防御性检查:端口被非 we-need-ds 进程占用时不会误重接管(daemon 起不来仍走还原直连)。
变更:中途断开透明化
- 首字节后上游断开/错误透传给客户端:客户端收到真实的
aborted(而非伪造的 502),由客户端 harness 自行决定重试——与真实 harness 面对不稳定上游的语义一致。 - 首字节前的重试、三段健康门、空 body 检测、5xx 透传全部不变。
测试
- 三套全绿(test_full 79 + simulation 18 + consume 18),生产文件 md5 前后字节一致;
- 新增 Phase I(钩子重接管语义 4 项)+ 中途断连 e2e(客户端先收到首块、上游断开后收到真实 aborted)。
v2.1.12 — 死锁根治:恢复失败即还原直连 + 死状态告警
本次更新
修复:杜绝"端点指向死代理"死锁
- 用户点破的关键死锁:重启后 provider 残留指向代理端口、daemon 未运行时,连恢复指令本身(经模型端点)都发不出去——鸡生蛋死锁。
- 失败即还原:所有恢复路径(
on/boot/ session-start 钩子 / user-prompt-submit 钩子)一旦拉起 daemon 失败,自动把仍指向代理端口的 provider 还原为真实上游直连——失败永远落在可用状态(直连),绝不留在死状态;端口冲突清除后随时可重试on。 - 新增
detectDeadState(config):统计指向代理端口的 provider 数。 status/doctor死状态告警:检测到 provider 指向代理端口但 daemon 未运行时,打印醒目告警 + 精确恢复命令。
保留(按用户要求)
- Claude Code 全部钩子路径(SessionStart/UserPromptSubmit/SessionEnd)保持前几个版本已验证可用的代码不动,仅失败分支加了还原兜底。
测试
- 三套全绿(test_full 74 + simulation 18 + consume 18),生产文件 md5 前后字节一致;
- 新增隔离 e2e:检测死状态 → recoverOrphans → provider 回到真实上游、死标记清除。
附:本机故障实测结论
- 转发机制本身正常(YJS 经 20329 返回 200 真实推理);
api.b.ai系 provider 是上游自身拒绝连接(直连同样 ECONNREFUSED),与插件无关;本地 ollama/litellm 服务未运行。