@zmainer/dsh-wx-bridge 1.1.5 → 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;
@@ -863,6 +867,8 @@ function ensurePushHelper() {
863
867
  * 规则:`defaultPreset:` 一行**无论写的是什么值**都换成当前 `acp.permPreset`;
864
868
  * 再按模板最后的缩进追加提示行(推送命令要给手机侧的 agent 看)。
865
869
  */
870
+ let chatOverlayPath = ''
871
+
866
872
  export function buildChatOverlayText(text, permPreset, hintLines) {
867
873
  const lines = String(text ?? '').split(NL)
868
874
  while (lines.length && !String(lines[lines.length - 1]).trim()) lines.pop()
@@ -888,7 +894,6 @@ function ensureChatOverlay() {
888
894
  try {
889
895
  const f = join(STATE_DIR, 'acp-overlay-chat.gen.yml')
890
896
  writeFileSync(f, out)
891
- // 只在内容变了时记一行,避免每次 spawn 都刷日志
892
897
  if (!chatOverlayPath) log('chat-overlay-written', { file: f, permPreset: ACP_PERM_PRESET })
893
898
  chatOverlayPath = f
894
899
  return f
@@ -993,7 +998,7 @@ function getAcpHost(key) {
993
998
  if (!acpHosts.has(k)) acpHosts.set(k, new AcpHost({
994
999
  bin: resolveAcpBin(), node: NODE_BIN, home: ACP_HOME, cwd: CWD, patch: overlayFor(k),
995
1000
  permPolicy: ACP_PERM, permFallback: 'reject', onPermission: (req) => handlePermissionRequest(req),
996
- promptTimeoutMs: TASK_TIMEOUT_MS, log,
1001
+ promptTimeoutMs: PROMPT_TIMEOUT_MS, log,
997
1002
  }))
998
1003
  // 策略是热切换的:已有宿主每次取用都同步一次(改 /审批 后不必重启桥)
999
1004
  const host = acpHosts.get(k)
@@ -1355,12 +1360,14 @@ async function applyPeerConfig(peer, sessionId) {
1355
1360
 
1356
1361
  /**
1357
1362
  * 一轮任务走 ACP:把用户原话交给 DSH 会话(**不拼接历史**),上下文由 DSH 维护。
1358
- * 失败返回 null,由调用方回落宿主/headless 路径。
1363
+ * 返回 null 才意味着"ACP 不可用",由调用方回落宿主/headless 路径;
1364
+ * **超时不算不可用**(那一轮还在跑)→ 返回 `{pending:true}` 并等 onLate 补发结果。
1359
1365
  */
1360
- async function runViaAcp(text, peer, taskId) {
1366
+ async function runViaAcp(text, peer, taskId, peerKey) {
1361
1367
  if (!ACP_ENABLED) return null
1368
+ let sessionId = ''
1362
1369
  try {
1363
- const sessionId = await ensurePeerSession(peer)
1370
+ sessionId = await ensurePeerSession(peer)
1364
1371
  if (!sessionId) return null
1365
1372
  const rec = state.tasks[taskId]
1366
1373
  if (rec) { rec.session = sessionId; rec.mode = 'acp' }
@@ -1370,17 +1377,47 @@ async function runViaAcp(text, peer, taskId) {
1370
1377
  const rec = state.tasks[taskId]
1371
1378
  if (rec) { rec.lastTool = String(u.title || 'tool'); rec.lastToolAt = Date.now() } // /task 能看到"最近在干什么"
1372
1379
  }
1380
+ }, {
1381
+ timeoutMs: PROMPT_TIMEOUT_MS,
1382
+ keepAliveOnTimeout: true,
1383
+ onLate: (late) => deliverLateAcpResult(peer, peerKey, taskId, late),
1373
1384
  })
1374
1385
  log('task-done-acp', { id: taskId, session: sessionId, outLen: (r.text || '').length, tools: r.toolCalls, stop: r.stopReason })
1375
1386
  const notice = peer.acpNotice || ''
1376
1387
  if (notice) peer.acpNotice = ''
1377
1388
  return { ok: true, text: r.text, sessionId, viaAcp: true, stopReason: r.stopReason, notice }
1378
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
+ }
1379
1400
  log('task-acp-failed', { id: taskId, error: String(e?.message ?? e) })
1380
1401
  return null
1381
1402
  }
1382
1403
  }
1383
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
+
1384
1421
  function killTree(pid) { try { spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }) } catch {} }
1385
1422
  function runDsh(task, cwd, taskId) {
1386
1423
  return new Promise((resolve) => {
@@ -1470,7 +1507,7 @@ const runningCount = () => Object.values(state.tasks).filter((t) => t.status ===
1470
1507
  async function processTask(token, peerKey, from, taskId, text, contextToken) {
1471
1508
  const peer = state.peers[peerKey]
1472
1509
  const t = state.tasks[taskId]
1473
- t.status = 'running'; t.startedAt = new Date().toISOString(); activeTasks = runningCount()
1510
+ t.status = 'running'; t.u = text; t.startedAt = new Date().toISOString(); activeTasks = runningCount()
1474
1511
  await saveState(state)
1475
1512
  const history = (peer.history || []).filter((h) => h.ok !== false).slice(-6)
1476
1513
  const prompt = PROMPT_PREFIX + sanitize(history.length
@@ -1478,16 +1515,28 @@ async function processTask(token, peerKey, from, taskId, text, contextToken) {
1478
1515
  : text)
1479
1516
  let r = null
1480
1517
  // ① 首选 ACP:DSH 原生会话 + 原生上下文(桥不拼接历史)
1481
- 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 = ''
1482
1528
  // ② 其次宿主内执行(历史行为;不产生 turn 时返回空 → 继续回落)
1483
- 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' } }
1484
1530
  // ③ 最后回落:一次性 headless(沿用拼接提示词,仅作兜底)
1485
- if (!r) r = await runDsh(prompt, peer.cwd, taskId)
1531
+ if (!r) { r = await runDsh(prompt, peer.cwd, taskId); viaFallback = 'headless' }
1486
1532
  t.status = r.timedOut ? 'timeout' : (r.ok ? 'done' : 'failed')
1487
1533
  t.endedAt = new Date().toISOString(); t.log = r.log; activeTasks = runningCount()
1488
1534
  peer.history = ((peer.history || []).concat([{ u: text, a: r.ok ? r.text : (r.error || '失败'), ok: !!r.ok, at: t.endedAt }])).slice(-50)
1489
- log('task-done', { id: taskId, ok: r.ok, timeout: !!r.timedOut, outLen: r.text?.length ?? 0 })
1490
- 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 ?? '未知错误')))
1491
1540
  if (audited.hits.length) log('output-audit', { id: taskId, hits: audited.hits })
1492
1541
  await reply(token, from, audited.text, contextToken)
1493
1542
  await saveState(state)
@@ -2214,13 +2263,60 @@ async function main() {
2214
2263
  hostHome: HOST_HOME || '(未指定)', acpHome: ACP_HOME, childHome: CHILD_HOME,
2215
2264
  vault: VAULT_PATH || '(未指定)', contract: CONTRACT_FILE,
2216
2265
  acp: { enabled: ACP_ENABLED, preset: ACP_CFG.preset || '(跟随宿主默认)', permPolicy: ACP_PERM,
2217
- 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 },
2218
2268
  overlays: { presetOverlay: overlayFor(ACP_DEFAULT_PRESET), chatOverlay: ensureChatOverlay() },
2219
2269
  push: { cli: PUSH_CLI, outbox: OUTBOX_DIR },
2220
2270
  }, null, 1))
2221
2271
  process.exit(0)
2222
2272
  }
2223
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
+ }
2224
2320
  acquireLock()
2225
2321
  // 上次进程遗留的运行态任务:本轮一启动就标成 interrupted。
2226
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.5",
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
+ }