@zmainer/dsh-wx-bridge 1.1.6 → 1.1.7

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.
@@ -128,6 +128,11 @@ export class AcpHost {
128
128
  const p = this.pending.get(f.id)
129
129
  if (!p) return
130
130
  this.pending.delete(f.id)
131
+ if (p.late) {
132
+ // 迟到帧:调用方早已按超时返回,但这一轮其实跑完了 → 交回给它补发结果
133
+ try { p.late(f.error ? null : f.result, f.error ? new Error(String(f.error.message || 'rpc error')) : null) } catch (e) { this.log('acp-late-handler-failed', { error: String(e?.message ?? e) }) }
134
+ return
135
+ }
131
136
  if (f.error) p.reject(Object.assign(new Error(String(f.error.message || 'rpc error')), { rpc: f.error }))
132
137
  else p.resolve(f.result)
133
138
  return
@@ -181,10 +186,23 @@ export class AcpHost {
181
186
  try { this.child.stdin.write(JSON.stringify(frame) + NL) } catch (e) { this.log('acp-write-failed', { error: String(e?.message ?? e) }) }
182
187
  }
183
188
 
184
- _request(method, params, timeoutMs) {
189
+ _request(method, params, timeoutMs, opts) {
190
+ const o = opts || {}
185
191
  return new Promise((resolve, reject) => {
186
192
  const id = this.nextId++
187
- const t = setTimeout(() => { this.pending.delete(id); reject(new Error('ACP 超时:' + method)) }, Number(timeoutMs || this.promptTimeoutMs))
193
+ const t = setTimeout(() => {
194
+ const err = Object.assign(new Error('ACP 超时:' + method), { code: 'ACP_TIMEOUT', method })
195
+ // 超时 ≠ 失败:`session/prompt` 的一方仍在跑,服务端最后一定会回这一帧。
196
+ // keepAliveOnTimeout 时**保留 pending 条目**,让它落地时走 onLate(避免"以为失败了"→ 兜底重跑一遍)。
197
+ if (o.keepAliveOnTimeout) {
198
+ const p = this.pending.get(id)
199
+ if (p) { p.late = o.onLate || null; p.timedOutAt = Date.now() }
200
+ this.log('acp-request-timeout-kept-alive', { id, method, timeoutMs: Number(timeoutMs || this.promptTimeoutMs) })
201
+ } else {
202
+ this.pending.delete(id)
203
+ }
204
+ reject(err)
205
+ }, Number(timeoutMs || this.promptTimeoutMs))
188
206
  this.pending.set(id, { resolve: (v) => { clearTimeout(t); resolve(v) }, reject: (e) => { clearTimeout(t); reject(e) } })
189
207
  this._raw({ jsonrpc: '2.0', id, method, params })
190
208
  })
@@ -230,10 +248,16 @@ export class AcpHost {
230
248
 
231
249
  /**
232
250
  * 发一轮提示词,收集助手正文(思考块与工具生命周期走 onUpdate,便于回传进度)。
251
+ *
252
+ * @param {object} [opts]
253
+ * @param {number} [opts.timeoutMs] 本轮等待上限(默认 promptTimeoutMs)
254
+ * @param {boolean} [opts.keepAliveOnTimeout] 超时后**不要丢弃**这一轮:迟到帧交给 onLate
255
+ * @param {(r:{text:string,stopReason:string|null,toolCalls:number,error:Error|null})=>void} [opts.onLate]
233
256
  * @returns {Promise<{text:string, stopReason:string, toolCalls:number}>}
234
257
  */
235
- async ask(sessionId, text, onUpdate) {
258
+ async ask(sessionId, text, onUpdate, opts) {
236
259
  await this.ensure()
260
+ const o = opts || {}
237
261
  let out = ''
238
262
  let tools = 0
239
263
  const sink = (u) => {
@@ -241,12 +265,24 @@ export class AcpHost {
241
265
  else if (u.sessionUpdate === 'tool_call') tools++
242
266
  if (onUpdate) { try { onUpdate(u) } catch {} }
243
267
  }
268
+ const dropSink = () => { if (this.inflight.get(sessionId) === sink) this.inflight.delete(sessionId) }
244
269
  this.inflight.set(sessionId, sink)
270
+ const late = o.onLate
271
+ ? (result, error) => {
272
+ dropSink()
273
+ try { o.onLate({ text: out.trim(), stopReason: result?.stopReason ?? null, toolCalls: tools, error: error || null }) }
274
+ catch (e) { this.log('acp-late-callback-failed', { error: String(e?.message ?? e) }) }
275
+ }
276
+ : null
245
277
  try {
246
- const r = await this._request('session/prompt', { sessionId, prompt: [{ type: 'text', text: String(text) }] })
278
+ const r = await this._request('session/prompt', { sessionId, prompt: [{ type: 'text', text: String(text) }] },
279
+ o.timeoutMs, { keepAliveOnTimeout: !!o.keepAliveOnTimeout, onLate: late })
280
+ dropSink()
247
281
  return { text: out.trim(), stopReason: r?.stopReason ?? null, toolCalls: tools }
248
- } finally {
249
- this.inflight.delete(sessionId)
282
+ } catch (e) {
283
+ // 超时且已保留:**留着 sink**——这一轮还在跑,正文要靠后续 session/update 累积
284
+ if (!(o.keepAliveOnTimeout && isAcpTimeout(e))) dropSink()
285
+ throw e
250
286
  }
251
287
  }
252
288
 
@@ -272,3 +308,13 @@ export class AcpHost {
272
308
  this.ready = null
273
309
  }
274
310
  }
311
+
312
+ /**
313
+ * 这个错误是不是"本轮超时"(而不是"通道不可用")?
314
+ * 超时意味着 **ACP 那一轮还在跑**,调用方不该把它当失败去走兜底(会重复执行同一件事)。
315
+ */
316
+ export function isAcpTimeout(err) {
317
+ if (!err) return false
318
+ if (err.code === 'ACP_TIMEOUT') return true
319
+ return /ACP 超时|ACP_TIMEOUT/.test(String(err.message || ''))
320
+ }
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, renameSync, statSync, readdirSync, copyFileSync, rmSync } from 'node:fs'
15
15
  import { spawn, execFileSync } from 'node:child_process'
16
- import { AcpHost } from './acp.mjs'
16
+ import { AcpHost, isAcpTimeout } from './acp.mjs'
17
17
  import { ensureAcpPresetSupport, acpPresetSupported, acpPackageFile, acpPackageCandidates } from './acp-preset-shim.mjs'
18
18
  import { parseApprovalReply, pickOptionByDecision, formatApprovalCard, decisionAck, DECISION_ALLOW, DECISION_REJECT } from './approval-reply.mjs'
19
19
  import { parsePushEntry, pickRecipient } from './outbox.mjs'
@@ -183,7 +183,7 @@ function ensureDshBin() {
183
183
 
184
184
  const API_BASE = process.env.ILINK_BASE || 'https://ilinkai.weixin.qq.com'
185
185
  const PROTOCOL_VERSION = '2.4.6'
186
- const BRIDGE_VERSION = '0.5.0'
186
+ const BRIDGE_VERSION = '0.5.1'
187
187
  const BOOT_ID = process.pid
188
188
  const MESSAGE_TYPE_BOT = 2
189
189
  const ITEM_TYPE_TEXT = 1
@@ -236,6 +236,10 @@ const PERM_MODES = ['ask', 'allow', 'reject']
236
236
  function normPermMode(v, d = 'ask') { const s = String(v ?? '').toLowerCase(); return PERM_MODES.includes(s) ? s : d }
237
237
  let ACP_PERM = normPermMode(ACP_CFG.permPolicy, 'ask')
238
238
  const APPROVAL_TIMEOUT_MS = Number(process.env.WXBRIDGE_APPROVAL_TIMEOUT_MS || ACP_CFG.approvalTimeoutMs || 120000)
239
+ // 一轮 ACP 提示词的等待上限(与"任务超时"分开):
240
+ // 任务超时(TASK_TIMEOUT_MS,5 分钟)用在**兜底路径**上——那里超时是真杀子进程;
241
+ // 而 ACP 超时只意味着"我们不再等",那一轮**还在跑**,所以给足 15 分钟,且超时不回落(见 runViaAcp)。
242
+ const PROMPT_TIMEOUT_MS = Number(process.env.BRIDGE_PROMPT_TIMEOUT_MS || ACP_CFG.promptTimeoutMs || 900000)
239
243
  // 手机通道的权限预设(ACP 叠层里 permission.defaultPreset)。默认 workspace-write:
240
244
  // 越界(工作区之外)的写与提权会变成审批卡推到微信,回「批准 / 拒绝」即可 —— 手机可应答后,
241
245
  // 不必再用"最宽权限"绕开审批。要回到旧行为:config.json 的 acp.permPreset 设 danger-full-access;
@@ -822,7 +826,7 @@ function presetKey(peer) {
822
826
  if (peer.acpMode === 'chat') return 'chat'
823
827
  return ACP_DEFAULT_PRESET
824
828
  }
825
- function overlayFor(key) {
829
+ function overlayFor(key) {
826
830
  if (key === 'chat') return ensureChatOverlay()
827
831
  if (!overlayCache.has(key)) overlayCache.set(key, ensurePresetOverlay(key) || '')
828
832
  return overlayCache.get(key) || ACP_PATCH
@@ -881,7 +885,7 @@ export function buildChatOverlayText(text, permPreset, hintLines) {
881
885
  * 必须重启桥才会重写,否则老生成文件会把新预设**静默顶掉**(2026-09-24 实测踩过)。
882
886
  * 这里**每次都重写**(一次几毫秒):宁可多写一次,也不要让"磁盘上的旧生成物"成为隐形真源。
883
887
  */
884
- function ensureChatOverlay() {
888
+ function ensureChatOverlay() {
885
889
  const src = join(HERE, 'acp-overlay-chat.yml')
886
890
  let text = ''
887
891
  try { text = readFileSync(src, 'utf8') } catch { return src }
@@ -994,7 +998,7 @@ function getAcpHost(key) {
994
998
  if (!acpHosts.has(k)) acpHosts.set(k, new AcpHost({
995
999
  bin: resolveAcpBin(), node: NODE_BIN, home: ACP_HOME, cwd: CWD, patch: overlayFor(k),
996
1000
  permPolicy: ACP_PERM, permFallback: 'reject', onPermission: (req) => handlePermissionRequest(req),
997
- promptTimeoutMs: TASK_TIMEOUT_MS, log,
1001
+ promptTimeoutMs: PROMPT_TIMEOUT_MS, log,
998
1002
  }))
999
1003
  // 策略是热切换的:已有宿主每次取用都同步一次(改 /审批 后不必重启桥)
1000
1004
  const host = acpHosts.get(k)
@@ -1356,12 +1360,14 @@ async function applyPeerConfig(peer, sessionId) {
1356
1360
 
1357
1361
  /**
1358
1362
  * 一轮任务走 ACP:把用户原话交给 DSH 会话(**不拼接历史**),上下文由 DSH 维护。
1359
- * 失败返回 null,由调用方回落宿主/headless 路径。
1363
+ * 返回 null 才意味着"ACP 不可用",由调用方回落宿主/headless 路径;
1364
+ * **超时不算不可用**(那一轮还在跑)→ 返回 `{pending:true}` 并等 onLate 补发结果。
1360
1365
  */
1361
- async function runViaAcp(text, peer, taskId) {
1366
+ async function runViaAcp(text, peer, taskId, peerKey) {
1362
1367
  if (!ACP_ENABLED) return null
1368
+ let sessionId = ''
1363
1369
  try {
1364
- const sessionId = await ensurePeerSession(peer)
1370
+ sessionId = await ensurePeerSession(peer)
1365
1371
  if (!sessionId) return null
1366
1372
  const rec = state.tasks[taskId]
1367
1373
  if (rec) { rec.session = sessionId; rec.mode = 'acp' }
@@ -1371,17 +1377,47 @@ async function runViaAcp(text, peer, taskId) {
1371
1377
  const rec = state.tasks[taskId]
1372
1378
  if (rec) { rec.lastTool = String(u.title || 'tool'); rec.lastToolAt = Date.now() } // /task 能看到"最近在干什么"
1373
1379
  }
1380
+ }, {
1381
+ timeoutMs: PROMPT_TIMEOUT_MS,
1382
+ keepAliveOnTimeout: true,
1383
+ onLate: (late) => deliverLateAcpResult(peer, peerKey, taskId, late),
1374
1384
  })
1375
1385
  log('task-done-acp', { id: taskId, session: sessionId, outLen: (r.text || '').length, tools: r.toolCalls, stop: r.stopReason })
1376
1386
  const notice = peer.acpNotice || ''
1377
1387
  if (notice) peer.acpNotice = ''
1378
1388
  return { ok: true, text: r.text, sessionId, viaAcp: true, stopReason: r.stopReason, notice }
1379
1389
  } catch (e) {
1390
+ if (isAcpTimeout(e)) {
1391
+ // **超时 ≠ 失败**:这一轮还在 ACP 侧跑,服务端最后一定会回帧(onLate 负责补发)。
1392
+ // 这里绝不能返回 null —— 那会让 processTask 去走兜底路径,把同一件事**再干一遍**(而且是拼接提示词的旧形态)。
1393
+ log('task-acp-still-running', { id: taskId, session: sessionId, waitedMs: PROMPT_TIMEOUT_MS, error: String(e?.message ?? e) })
1394
+ return { ok: false, pending: true, sessionId, error: '这一轮超过 ' + Math.round(PROMPT_TIMEOUT_MS / 60000) + ' 分钟仍在跑' }
1395
+ }
1396
+ // 真·通道不可用:回落之前先取消这一轮,避免"兜底路径与 ACP 同时跑"重复执行
1397
+ if (sessionId) {
1398
+ try { await getAcpHost(peerMode(peer)).cancel(sessionId); log('acp-fallback-cancel', { id: taskId, session: sessionId }) } catch {}
1399
+ }
1380
1400
  log('task-acp-failed', { id: taskId, error: String(e?.message ?? e) })
1381
1401
  return null
1382
1402
  }
1383
1403
  }
1384
1404
 
1405
+ /** 迟到的那一轮跑完了:把结果补发给用户(手机侧不必重发原话)。 */
1406
+ function deliverLateAcpResult(peer, peerKey, taskId, late) {
1407
+ const t = state.tasks[taskId] || {}
1408
+ const audited = auditOutput('⏳(这一轮跑完了)' + NL + (late.error ? '执行失败:' + late.error.message : late.text))
1409
+ if (audited.hits.length) log('output-audit', { id: taskId, hits: audited.hits })
1410
+ t.status = late.error ? 'failed' : 'done'
1411
+ t.endedAt = new Date().toISOString()
1412
+ t.lateDelivered = true
1413
+ peer.history = ((peer.history || []).concat([{ u: t.u || '', a: late.text || String(late.error?.message || '失败'), ok: !late.error, at: t.endedAt, late: true }])).slice(-50)
1414
+ log('task-late-done', { id: taskId, ok: !late.error, outLen: (late.text || '').length, tools: late.toolCalls })
1415
+ saveState(state).catch(() => {})
1416
+ const key = peerKey || Object.keys(state.peers).find((k) => state.peers[k] === peer) || ''
1417
+ if (!key) return
1418
+ reply(wechatToken, key, audited.text, peer.lastContextToken).catch((e) => log('late-reply-failed', { error: String(e?.message ?? e) }))
1419
+ }
1420
+
1385
1421
  function killTree(pid) { try { spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }) } catch {} }
1386
1422
  function runDsh(task, cwd, taskId) {
1387
1423
  return new Promise((resolve) => {
@@ -1471,7 +1507,7 @@ const runningCount = () => Object.values(state.tasks).filter((t) => t.status ===
1471
1507
  async function processTask(token, peerKey, from, taskId, text, contextToken) {
1472
1508
  const peer = state.peers[peerKey]
1473
1509
  const t = state.tasks[taskId]
1474
- t.status = 'running'; t.startedAt = new Date().toISOString(); activeTasks = runningCount()
1510
+ t.status = 'running'; t.u = text; t.startedAt = new Date().toISOString(); activeTasks = runningCount()
1475
1511
  await saveState(state)
1476
1512
  const history = (peer.history || []).filter((h) => h.ok !== false).slice(-6)
1477
1513
  const prompt = PROMPT_PREFIX + sanitize(history.length
@@ -1479,16 +1515,28 @@ async function processTask(token, peerKey, from, taskId, text, contextToken) {
1479
1515
  : text)
1480
1516
  let r = null
1481
1517
  // ① 首选 ACP:DSH 原生会话 + 原生上下文(桥不拼接历史)
1482
- r = await runViaAcp(text, peer, taskId)
1518
+ r = await runViaAcp(text, peer, taskId, peerKey)
1519
+ // ①-b 超时 ≠ 失败:这一轮还在 ACP 侧跑,结果由 onLate 补发;**不走兜底**(否则同一件事被重跑一遍)
1520
+ if (r && r.pending) {
1521
+ t.pendingSince = new Date().toISOString(); activeTasks = runningCount()
1522
+ await saveState(state)
1523
+ return reply(token, from, '⏳ 这一轮已跑 ' + Math.round(PROMPT_TIMEOUT_MS / 60000) + ' 分钟仍在继续(会话 '
1524
+ + String(r.sessionId || '').slice(0, 12) + '…)。' + NL
1525
+ + '我**不会重复执行**它;跑完直接把结果发到这里(/task 看进度,/cancel 可中止)。', contextToken)
1526
+ }
1527
+ let viaFallback = ''
1483
1528
  // ② 其次宿主内执行(历史行为;不产生 turn 时返回空 → 继续回落)
1484
- if (!r) { const viaHost = await runViaHost(text, peer, taskId); if (viaHost && String(viaHost.text || '').trim()) r = viaHost }
1529
+ if (!r) { const viaHost = await runViaHost(text, peer, taskId); if (viaHost && String(viaHost.text || '').trim()) { r = viaHost; viaFallback = 'host' } }
1485
1530
  // ③ 最后回落:一次性 headless(沿用拼接提示词,仅作兜底)
1486
- if (!r) r = await runDsh(prompt, peer.cwd, taskId)
1531
+ if (!r) { r = await runDsh(prompt, peer.cwd, taskId); viaFallback = 'headless' }
1487
1532
  t.status = r.timedOut ? 'timeout' : (r.ok ? 'done' : 'failed')
1488
1533
  t.endedAt = new Date().toISOString(); t.log = r.log; activeTasks = runningCount()
1489
1534
  peer.history = ((peer.history || []).concat([{ u: text, a: r.ok ? r.text : (r.error || '失败'), ok: !!r.ok, at: t.endedAt }])).slice(-50)
1490
- log('task-done', { id: taskId, ok: r.ok, timeout: !!r.timedOut, outLen: r.text?.length ?? 0 })
1491
- const audited = auditOutput((r.notice ? r.notice + NL : '') + (r.ok ? r.text : '执行失败:' + (r.error ?? '未知错误')))
1535
+ log('task-done', { id: taskId, ok: r.ok, timeout: !!r.timedOut, viaFallback: viaFallback || 'acp', outLen: r.text?.length ?? 0 })
1536
+ const fallbackNotice = viaFallback
1537
+ ? '⚠️ 本轮 ACP 路径不可用,走了兜底(' + viaFallback + ');兜底路径会把历史拼接进提示词(不是上下文回退)。' + NL
1538
+ : ''
1539
+ const audited = auditOutput((r.notice ? r.notice + NL : '') + fallbackNotice + (r.ok ? r.text : '执行失败:' + (r.error ?? '未知错误')))
1492
1540
  if (audited.hits.length) log('output-audit', { id: taskId, hits: audited.hits })
1493
1541
  await reply(token, from, audited.text, contextToken)
1494
1542
  await saveState(state)
@@ -2215,13 +2263,60 @@ async function main() {
2215
2263
  hostHome: HOST_HOME || '(未指定)', acpHome: ACP_HOME, childHome: CHILD_HOME,
2216
2264
  vault: VAULT_PATH || '(未指定)', contract: CONTRACT_FILE,
2217
2265
  acp: { enabled: ACP_ENABLED, preset: ACP_CFG.preset || '(跟随宿主默认)', permPolicy: ACP_PERM,
2218
- permPreset: ACP_PERM_PRESET, approvalTimeoutMs: APPROVAL_TIMEOUT_MS, patchAcp: ACP_CFG.patchAcp !== false },
2266
+ permPreset: ACP_PERM_PRESET, approvalTimeoutMs: APPROVAL_TIMEOUT_MS, promptTimeoutMs: PROMPT_TIMEOUT_MS,
2267
+ taskTimeoutMs: TASK_TIMEOUT_MS, patchAcp: ACP_CFG.patchAcp !== false },
2219
2268
  overlays: { presetOverlay: overlayFor(ACP_DEFAULT_PRESET), chatOverlay: ensureChatOverlay() },
2220
2269
  push: { cli: PUSH_CLI, outbox: OUTBOX_DIR },
2221
2270
  }, null, 1))
2222
2271
  process.exit(0)
2223
2272
  }
2224
2273
 
2274
+ if (args.includes('--selftest-acp-timeout')) {
2275
+ // 离线自测:**超时 ≠ 失败** 这条规则的三段行为(不连模型、不起子进程)
2276
+ const rows = []
2277
+ const mk = (timeoutMs) => {
2278
+ const h = new AcpHost({ promptTimeoutMs: timeoutMs, permPolicy: 'ask', log: () => {} })
2279
+ h.ready = Promise.resolve({}) // 跳过真实握手
2280
+ h.child = { stdin: { write: () => {} }, exitCode: null }
2281
+ return h
2282
+ }
2283
+ const frame = (id, text, stop) => JSON.stringify({ jsonrpc: '2.0', id, result: { stopReason: stop, _text: text } }) + NL
2284
+ // ① 分类:超时 vs 其它
2285
+ const timeoutErr = Object.assign(new Error('ACP 超时:session/prompt'), { code: 'ACP_TIMEOUT' })
2286
+ rows.push({ name: 'isAcpTimeout(tagged)', ok: isAcpTimeout(timeoutErr) === true })
2287
+ rows.push({ name: 'isAcpTimeout(plain message)', ok: isAcpTimeout(new Error('ACP 超时:session/prompt')) === true })
2288
+ rows.push({ name: 'isAcpTimeout(其它错误)', ok: isAcpTimeout(new Error('ACP 进程已退出(code=1)')) === false })
2289
+ // ② keepAlive:超时后迟到帧交给 onLate(正文靠 session/update 累积)
2290
+ {
2291
+ const h = mk(120)
2292
+ let late = null
2293
+ const id = h.nextId
2294
+ let code = ''
2295
+ try { await h.ask('s-late', 'hi', null, { timeoutMs: 120, keepAliveOnTimeout: true, onLate: (r) => { late = r } }) } catch (e) { code = e.code || '' }
2296
+ const keptAfterTimeout = h.pending.has(id)
2297
+ h._onData(Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'session/update', params: { sessionId: 's-late', update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: '迟到正文' } } } }) + NL))
2298
+ h._onData(Buffer.from(frame(id, '', 'end_turn')))
2299
+ rows.push({ name: '超时错误带 code=ACP_TIMEOUT', ok: code === 'ACP_TIMEOUT' })
2300
+ rows.push({ name: '超时后 pending 条目被保留', ok: keptAfterTimeout === true })
2301
+ rows.push({ name: '迟到帧触发 onLate 且正文完整', ok: !!late && late.text === '迟到正文' && late.stopReason === 'end_turn' })
2302
+ rows.push({ name: '迟到后 pending 清空 + sink 摘除', ok: !h.pending.has(id) && !h.inflight.has('s-late') })
2303
+ }
2304
+ // ③ 对照:不 keepAlive 时超时即丢弃,迟到帧不会触发任何回调(旧行为)
2305
+ {
2306
+ const h = mk(120)
2307
+ let late = null
2308
+ const id = h.nextId
2309
+ let code = ''
2310
+ try { await h.ask('s-drop', 'hi', null, { timeoutMs: 120, onLate: (r) => { late = r } }) } catch (e) { code = e.code || '' }
2311
+ h._onData(Buffer.from(frame(id, '', 'end_turn')))
2312
+ rows.push({ name: '未开 keepAlive:超时即丢弃且无回调', ok: code === 'ACP_TIMEOUT' && late === null && !h.pending.has(id) && !h.inflight.has('s-drop') })
2313
+ }
2314
+ console.log('[selftest-acp-timeout] ' + JSON.stringify({
2315
+ rows, failed: rows.filter((r) => !r.ok).map((r) => r.name),
2316
+ promptTimeoutMs: PROMPT_TIMEOUT_MS, taskTimeoutMs: TASK_TIMEOUT_MS,
2317
+ }, null, 1))
2318
+ process.exit(rows.every((r) => r.ok) ? 0 : 1)
2319
+ }
2225
2320
  acquireLock()
2226
2321
  // 上次进程遗留的运行态任务:本轮一启动就标成 interrupted。
2227
2322
  // 否则 runningCount() 永远虚高,`while (runningCount() >= MAX_CONCURRENT_TASKS)` 会把新任务永久挡住。
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "@zmainer/dsh-wx-bridge",
3
- "version": "1.1.6",
4
- "type": "module",
5
- "main": "./lib/index.js",
6
- "dsh": {
7
- "bundle": {
8
- "patch": "./cordis.patch.yml"
9
- },
10
- "client": {
11
- "platform": "web"
12
- }
13
- },
14
- "exports": {
15
- ".": "./lib/index.js",
16
- "./client": "./lib/client.js",
17
- "./package.json": "./package.json"
18
- },
19
- "scripts": {
20
- "build": "node scripts/build-client.mjs"
21
- },
22
- "files": [
23
- "lib",
24
- "src",
25
- "scripts",
26
- "cordis.patch.yml",
27
- "README.md",
28
- "package.json"
29
- ],
30
- "description": "DSH plugin: connect WeChat (official iLink/ClawBot) to DeepSeek Harness — settings page with live bridge status, QR device pairing, in-WeChat tool approvals, proactive push, and the bundled bridge + keeper kernel.",
31
- "license": "MIT",
32
- "keywords": [
33
- "deepseek-harness",
34
- "dsh-plugin",
35
- "wechat",
36
- "weixin",
37
- "ilink",
38
- "clawbot"
39
- ],
40
- "repository": {
41
- "type": "git",
42
- "url": "git+https://github.com/zhy5/dsh-wx-bridge.git"
43
- },
44
- "homepage": "https://github.com/zhy5/dsh-wx-bridge#readme",
45
- "bugs": {
46
- "url": "https://github.com/zhy5/dsh-wx-bridge/issues"
47
- },
48
- "author": "zmainer",
49
- "engines": {
50
- "node": ">=22.19.0"
51
- }
52
- }
1
+ {
2
+ "name": "@zmainer/dsh-wx-bridge",
3
+ "version": "1.1.7",
4
+ "type": "module",
5
+ "main": "./lib/index.js",
6
+ "dsh": {
7
+ "bundle": {
8
+ "patch": "./cordis.patch.yml"
9
+ },
10
+ "client": {
11
+ "platform": "web"
12
+ }
13
+ },
14
+ "exports": {
15
+ ".": "./lib/index.js",
16
+ "./client": "./lib/client.js",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "scripts": {
20
+ "build": "node scripts/build-client.mjs"
21
+ },
22
+ "files": [
23
+ "lib",
24
+ "src",
25
+ "scripts",
26
+ "cordis.patch.yml",
27
+ "README.md",
28
+ "package.json"
29
+ ],
30
+ "description": "DSH plugin: connect WeChat (official iLink/ClawBot) to DeepSeek Harness — settings page with live bridge status, QR device pairing, in-WeChat tool approvals, proactive push, and the bundled bridge + keeper kernel.",
31
+ "license": "MIT",
32
+ "keywords": [
33
+ "deepseek-harness",
34
+ "dsh-plugin",
35
+ "wechat",
36
+ "weixin",
37
+ "ilink",
38
+ "clawbot"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/zhy5/dsh-wx-bridge.git"
43
+ },
44
+ "homepage": "https://github.com/zhy5/dsh-wx-bridge#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/zhy5/dsh-wx-bridge/issues"
47
+ },
48
+ "author": "zmainer",
49
+ "engines": {
50
+ "node": ">=22.19.0"
51
+ }
52
+ }