@zmainer/dsh-wx-bridge 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -228,7 +228,7 @@ echo "来自管道的长文本" | node "<dataDir>/wxpush.mjs" --stdin
228
228
  | --- | --- |
229
229
  | DSH / harness | 按官方插件协议安装(`dsh.bundle` 清单 + `dsh plugin add`);内核只用 `dsh --profile acp` 与 `--profile headless` 两个内置 profile,**不绑定某个 harness 小版本**。预设功能依赖已装 `@deepseek-ai/dsh-acp` 支持 `Config.preset`——不支持/被覆盖时自动退回宿主组合(见「预设补丁」) |
230
230
  | Node | **`>=22.19.0`**(`package.json` 的 `engines`):会话文件是**多帧 zstd**,逐帧解压用到 `node:zlib` 的 `zstdDecompressSync`。桌面端自带运行时(本机实测 v26)与系统 Node(本机 v22.19)都验证过 |
231
- | 操作系统 | **Windows 已实测**(进程树终止用 `taskkill /T /F`;运行时入口探测含 `%LOCALAPPDATA%\Programs\*` 扫描、PID 复用判断)。macOS / Linux **未实测**:内核是纯 Node,但上述几处是 Windows 实现,跨平台请先跑 `node lib/kernel/bridge.mjs --selftest-runtime` |
231
+ | 操作系统 | **Windows 已实测**(进程树终止用 `taskkill /T /F`;运行时入口探测含 `%LOCALAPPDATA%\Programs\*` 扫描、PID 复用判断)。macOS / Linux **未实测**:内核是纯 Node,但上述几处是 Windows 实现,跨平台请先跑 `node lib/kernel/bridge.mjs --selftest-runtime`。**已知限制(1.1.8 起明示)**:任务超时/取消的进程树终止走 Windows `taskkill /T /F`,macOS / Linux 下**不会真正终止子进程**(需另行接入 `process.kill`) |
232
232
  | 依赖 | **零运行时依赖**(只用 Node 内建模块);不依赖任何 `@deepseek-ai/*` 包(宿主能力通过 `ctx.inject` 取用,缺失即降级) |
233
233
  | 自测 | **`--print-config`(只读:打印实际配置链)** / `--selftest-runtime`(运行时入口)/ `--selftest-approval`(审批卡与回复解析)/ `--selftest-acp-approval`(审批 ↔ ACP `session/request_permission` 的线上契约)/ **`--selftest-overlays`(叠层模板与生成物:预设一致 + 推送提示 + 缩进)** / `--selftest-outbox`(推送信箱)/ `--selftest-files` / `--selftest-inbound` / `--selftest-presets` |
234
234
 
@@ -267,6 +267,12 @@ ode_modules\@zmainer\dsh-wx-bridge` → 改名成 `...dsh-wx-bridge.bak` 更稳
267
267
 
268
268
  ## 最近变更
269
269
 
270
+ - **1.1.8**:修**发送回执判定**(用户反馈「日志说发了、手机没收到」)—— 判据原本只查 `ret`,
271
+ 而 iLink 在「HTTP 200 但没投递」时回的是 `errcode`(实测 `{"errcode":-14,"errmsg":"session timeout"}`)⇒ **假成功**:
272
+ 日志记 `sent`、推送件被归档进 `outbox/sent/`。现在 `errcode`/`ret` 任一非 0 一律抛错,回复路径如实记 `send-failed`,
273
+ 成功回执以 `message_id` 为正标志(缺失只告警);拉取侧(`getupdates`)遇业务码记 `getupdates-rejected`。
274
+ 新增离线自测 `--selftest-receipt`(6 例);§兼容性 明示 `killTree` 的平台差异。
275
+
270
276
  - **1.1.7**:**超时 ≠ 失败**(用户反馈「为什么第二预设的提示词又是拼接我的提问发给 agent」)——
271
277
  原先 `session/prompt` 超过 5 分钟就按失败处理:`runViaAcp` 的 catch 一律 `return null` → 桥走兜底(宿主→headless),
272
278
  而**兜底路径的提示词天生是拼接的**(契约+历史+本轮),于是同一轮任务被**重跑一遍**、用户在会话列表里看到的就是那条拼接提示词。
@@ -513,10 +513,21 @@ async function ilink(endpoint, { token, body, method = 'POST', timeoutMs = 15000
513
513
  }
514
514
  const getUpdates = (token, syncBuf, timeoutMs) => ilink('ilink/bot/getupdates', { token, body: { get_updates_buf: syncBuf, base_info: baseInfo() }, timeoutMs })
515
515
  .catch((e) => { if (e?.name === 'AbortError') return { ret: 0, msgs: [], get_updates_buf: syncBuf }; throw e })
516
+ /** 发送回执判定:业务码(errcode / ret)非 0 一律抛错。
517
+ * iLink 在"HTTP 200 但没投递"时回的正是 errcode——2026-09-27 实测:会话窗口失效 → {"errcode":-14,"errmsg":"session timeout"},
518
+ * 而旧判据只看 ret(此时该字段不存在)⇒ 被当成成功。成功回执带 message_id,缺了只记一条告警(不判失败)。 */
519
+ function assertSendReceipt(r, len) {
520
+ const code = r?.errcode ?? r?.ret
521
+ if (code !== undefined && code !== null && Number(code) !== 0) {
522
+ throw new Error('sendmessage 被拒: errcode=' + code + (r?.errmsg ? ' ' + r.errmsg : ''))
523
+ }
524
+ if (r?.message_id == null) log('send-no-receipt', { len, keys: Object.keys(r ?? {}).join(',') })
525
+ return r
526
+ }
516
527
  const sendText = (token, to, text, contextToken) => ilink('ilink/bot/sendmessage', {
517
528
  token,
518
529
  body: { msg: { from_user_id: '', to_user_id: to, client_id: 'dsh-bridge-' + randomUUID(), message_type: MESSAGE_TYPE_BOT, message_state: 2, item_list: [{ type: ITEM_TYPE_TEXT, text_item: { text } }], ...(contextToken ? { context_token: contextToken } : {}) }, base_info: baseInfo() },
519
- })
530
+ }).then((r) => assertSendReceipt(r, String(text).length))
520
531
 
521
532
  function inboundText(msg) { for (const it of msg?.item_list ?? []) if (it?.type === ITEM_TYPE_TEXT && typeof it.text_item?.text === 'string') return it.text_item.text; return '' }
522
533
  /** 入站消息里出现过的条目类型(排障 + 「只认文字」提示用)。 */
@@ -911,8 +922,7 @@ function moveOutboxFile(file, dir, name, reason) {
911
922
  /** 严格发送:任一分段失败都如实抛错(与 reply() 的"尽力而为"不同,推送要能判成败)。 */
912
923
  async function pushSend(token, to, text, ctx) {
913
924
  for (let i = 0; i < text.length; i += MAX_CHUNK) {
914
- const r = await sendText(token, to, text.slice(i, i + MAX_CHUNK), ctx)
915
- if (r && r.ret !== undefined && Number(r.ret) !== 0) throw new Error('ret=' + r.ret)
925
+ await sendText(token, to, text.slice(i, i + MAX_CHUNK), ctx)
916
926
  }
917
927
  return true
918
928
  }
@@ -1457,7 +1467,7 @@ async function reply(token, to, text, ctx) {
1457
1467
  const body = String(text ?? '').trim() || '(空回复)'
1458
1468
  for (let i = 0; i < body.length; i += MAX_CHUNK) {
1459
1469
  const c = body.slice(i, i + MAX_CHUNK)
1460
- try { const r = await sendText(token, to, c, ctx); log('sent', { len: c.length, ret: r?.ret }) } catch (e) { log('send-failed', { error: String(e?.message ?? e) }) }
1470
+ try { const r = await sendText(token, to, c, ctx); log('sent', { len: c.length, message_id: r?.message_id ?? null }) } catch (e) { log('send-failed', { len: c.length, error: String(e?.message ?? e) }) }
1461
1471
  }
1462
1472
  }
1463
1473
 
@@ -2271,6 +2281,25 @@ async function main() {
2271
2281
  process.exit(0)
2272
2282
  }
2273
2283
 
2284
+ if (args.includes('--selftest-receipt')) {
2285
+ // 离线自测:发送回执的业务码判定(纯函数,不连网络、不发消息)
2286
+ const cases = [
2287
+ ['成功(带 message_id)', { message_id: 12345 }, false],
2288
+ ['成功(ret=0 且带 message_id)', { ret: 0, message_id: 1 }, false],
2289
+ ['被拒:errcode=-14 会话窗口失效(2026-09-27 实测形态)', { errcode: -14, errmsg: 'session timeout' }, true],
2290
+ ['被拒:ret 非 0', { ret: 5 }, true],
2291
+ ['被拒:errcode 字符串非 0', { errcode: '-3' }, true],
2292
+ ['缺 message_id(只告警,不判失败)', { ret: 0 }, false],
2293
+ ]
2294
+ const rows = cases.map(([name, r, wantFail]) => {
2295
+ let failed = false
2296
+ let thrown = ''
2297
+ try { assertSendReceipt(r, 10) } catch (e) { failed = true; thrown = String(e?.message ?? e) }
2298
+ return { name, ok: failed === wantFail, failed, thrown }
2299
+ })
2300
+ console.log('[selftest-receipt] ' + JSON.stringify({ rows }, null, 1))
2301
+ process.exit(rows.every((r) => r.ok) ? 0 : 1)
2302
+ }
2274
2303
  if (args.includes('--selftest-acp-timeout')) {
2275
2304
  // 离线自测:**超时 ≠ 失败** 这条规则的三段行为(不连模型、不起子进程)
2276
2305
  const rows = []
@@ -2397,6 +2426,8 @@ async function main() {
2397
2426
  // ③ 外部改了 state(扫码登记新主人/面板操作)
2398
2427
  reloadStateIfChanged()
2399
2428
  const res = await getUpdates(wechatToken, state.syncBuf, timeoutMs)
2429
+ const pollCode = res?.errcode ?? res?.ret
2430
+ if (pollCode !== undefined && pollCode !== null && Number(pollCode) !== 0) log('getupdates-rejected', { errcode: pollCode, errmsg: res?.errmsg ?? '' })
2400
2431
  pollCount++
2401
2432
  if (Number.isFinite(res?.longpolling_timeout_ms) && res.longpolling_timeout_ms > 0) timeoutMs = Math.max(5000, Math.min(res.longpolling_timeout_ms, 120000))
2402
2433
  if (typeof res?.get_updates_buf === 'string' && res.get_updates_buf) state.syncBuf = res.get_updates_buf
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmainer/dsh-wx-bridge",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "dsh": {