@wenbin_wb/dsh-bridge 1.2.3 → 1.2.5
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/client/client.js +59 -40
- package/client/index.js +65 -25
- package/lib/wechat/gateway.js +34 -1
- package/lib/wechat/node.js +81 -14
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -77,7 +77,7 @@ var s = {
|
|
|
77
77
|
muted: { color: "var(--dsw-alias-label-tertiary,#8b93a1)", fontSize: 12, lineHeight: 1.5 },
|
|
78
78
|
label: { color: "var(--dsw-alias-label-primary,currentColor)", fontSize: 13, fontWeight: 500 },
|
|
79
79
|
code: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 12, wordBreak: "break-all", color: "var(--dsw-alias-label-primary,currentColor)" },
|
|
80
|
-
btnPri: { font: "inherit", cursor: "pointer", border: "none", background: "var(--dsw-alias-
|
|
80
|
+
btnPri: { font: "inherit", cursor: "pointer", border: "none", background: "var(--dsw-alias-brand-primary,#4f6ef7)", color: "var(--dsw-alias-label-primary-foreground,#fff)", height: 32, padding: "0 14px", borderRadius: 999, fontSize: 13, fontWeight: 500, display: "inline-flex", alignItems: "center", gap: 4 },
|
|
81
81
|
btnGhost: { font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,transparent)", color: "var(--dsw-alias-label-primary,currentColor)", height: 32, padding: "0 14px", borderRadius: 999, fontSize: 13, display: "inline-flex", alignItems: "center", gap: 4, textDecoration: "none" },
|
|
82
82
|
btnLink: { font: "inherit", cursor: "pointer", border: "none", background: "none", color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, padding: 0, display: "inline-flex", alignItems: "center", gap: 3, textDecoration: "none" },
|
|
83
83
|
qr: { width: 200, height: 200, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", margin: "8px 0", display: "block" },
|
|
@@ -99,11 +99,28 @@ function QrBlock({ url, qr, onReset }) {
|
|
|
99
99
|
const [copied, setCopied] = React.useState(false);
|
|
100
100
|
const [showQr, setShowQr] = React.useState(true);
|
|
101
101
|
const copy = React.useCallback(() => {
|
|
102
|
-
navigator.clipboard?.writeText
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
if (navigator.clipboard?.writeText) {
|
|
103
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
104
|
+
setCopied(true);
|
|
105
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
106
|
+
}).catch(() => fallbackCopy());
|
|
107
|
+
} else {
|
|
108
|
+
fallbackCopy();
|
|
109
|
+
}
|
|
110
|
+
function fallbackCopy() {
|
|
111
|
+
const textarea = document.createElement("textarea");
|
|
112
|
+
textarea.value = url;
|
|
113
|
+
textarea.style.position = "fixed";
|
|
114
|
+
textarea.style.opacity = "0";
|
|
115
|
+
document.body.appendChild(textarea);
|
|
116
|
+
textarea.select();
|
|
117
|
+
const success = document.execCommand("copy");
|
|
118
|
+
document.body.removeChild(textarea);
|
|
119
|
+
if (success) {
|
|
120
|
+
setCopied(true);
|
|
121
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
107
124
|
}, [url]);
|
|
108
125
|
const toggleQr = React.useCallback(() => setShowQr((v) => !v), []);
|
|
109
126
|
return React.createElement(
|
|
@@ -599,48 +616,50 @@ function VersionBanner({ rpcCall }) {
|
|
|
599
616
|
"div",
|
|
600
617
|
{
|
|
601
618
|
style: {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
background: "linear-gradient(135deg, var(--dsw-alias-brand-primary,#4f6ef7) 0%, #6366f1 100%)",
|
|
606
|
-
borderRadius: 10,
|
|
619
|
+
...s.card,
|
|
620
|
+
background: "var(--dsw-alias-state-info-bg,#eff6ff)",
|
|
621
|
+
border: "1px solid var(--dsw-alias-state-info-border,#bfdbfe)",
|
|
607
622
|
padding: "12px 16px",
|
|
608
|
-
marginBottom: 8
|
|
609
|
-
color: "#fff"
|
|
623
|
+
marginBottom: 8
|
|
610
624
|
}
|
|
611
625
|
},
|
|
612
|
-
React.createElement("span", { style: { fontSize: 20 } }, "\u{1F389}"),
|
|
613
626
|
React.createElement(
|
|
614
627
|
"div",
|
|
615
|
-
{ style: { flex:
|
|
628
|
+
{ style: { display: "flex", alignItems: "center", gap: 12 } },
|
|
629
|
+
React.createElement("span", { style: { fontSize: 20 } }, "\u{1F389}"),
|
|
616
630
|
React.createElement(
|
|
617
631
|
"div",
|
|
618
|
-
{ style: {
|
|
619
|
-
|
|
632
|
+
{ style: { flex: 1 } },
|
|
633
|
+
React.createElement("div", {
|
|
634
|
+
style: {
|
|
635
|
+
fontSize: 13,
|
|
636
|
+
fontWeight: 600,
|
|
637
|
+
color: "var(--dsw-alias-state-info-primary,#1e40af)",
|
|
638
|
+
marginBottom: 4
|
|
639
|
+
}
|
|
640
|
+
}, `\u53D1\u73B0\u65B0\u7248\u672C v${info.latest}\uFF08\u5F53\u524D v${info.current}\uFF09`),
|
|
641
|
+
React.createElement("code", {
|
|
642
|
+
style: {
|
|
643
|
+
...s.code,
|
|
644
|
+
fontSize: 11,
|
|
645
|
+
color: "var(--dsw-alias-label-secondary,#6b7280)",
|
|
646
|
+
display: "block"
|
|
647
|
+
}
|
|
648
|
+
}, "dsh plugin --profile web update @wenbin_wb/dsh-bridge --latest")
|
|
620
649
|
),
|
|
621
|
-
React.createElement("
|
|
622
|
-
style: {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
borderRadius: 999,
|
|
635
|
-
fontSize: 12,
|
|
636
|
-
display: "inline-flex",
|
|
637
|
-
alignItems: "center",
|
|
638
|
-
opacity: loading ? 0.5 : 1,
|
|
639
|
-
flexShrink: 0
|
|
640
|
-
},
|
|
641
|
-
onClick: check,
|
|
642
|
-
disabled: loading
|
|
643
|
-
}, loading ? "\u2026" : "\u5237\u65B0")
|
|
650
|
+
React.createElement("button", {
|
|
651
|
+
style: {
|
|
652
|
+
...s.btnGhost,
|
|
653
|
+
height: 30,
|
|
654
|
+
padding: "0 12px",
|
|
655
|
+
fontSize: 12,
|
|
656
|
+
opacity: loading ? 0.5 : 1,
|
|
657
|
+
flexShrink: 0
|
|
658
|
+
},
|
|
659
|
+
onClick: check,
|
|
660
|
+
disabled: loading
|
|
661
|
+
}, loading ? "\u2026" : "\u5237\u65B0")
|
|
662
|
+
)
|
|
644
663
|
),
|
|
645
664
|
links
|
|
646
665
|
);
|
package/client/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const s = {
|
|
|
31
31
|
muted: { color: 'var(--dsw-alias-label-tertiary,#8b93a1)', fontSize: 12, lineHeight: 1.5 },
|
|
32
32
|
label: { color: 'var(--dsw-alias-label-primary,currentColor)', fontSize: 13, fontWeight: 500 },
|
|
33
33
|
code: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 12, wordBreak: 'break-all', color: 'var(--dsw-alias-label-primary,currentColor)' },
|
|
34
|
-
btnPri: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'var(--dsw-alias-
|
|
34
|
+
btnPri: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'var(--dsw-alias-brand-primary,#4f6ef7)', color: 'var(--dsw-alias-label-primary-foreground,#fff)', height: 32, padding: '0 14px', borderRadius: 999, fontSize: 13, fontWeight: 500, display: 'inline-flex', alignItems: 'center', gap: 4 },
|
|
35
35
|
btnGhost: { font: 'inherit', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,transparent)', color: 'var(--dsw-alias-label-primary,currentColor)', height: 32, padding: '0 14px', borderRadius: 999, fontSize: 13, display: 'inline-flex', alignItems: 'center', gap: 4, textDecoration: 'none' },
|
|
36
36
|
btnLink: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'none', color: 'var(--dsw-alias-brand-primary,#4f6ef7)', fontSize: 12, padding: 0, display: 'inline-flex', alignItems: 'center', gap: 3, textDecoration: 'none' },
|
|
37
37
|
qr: { width: 200, height: 200, borderRadius: 10, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', margin: '8px 0', display: 'block' },
|
|
@@ -58,10 +58,33 @@ function QrBlock({ url, qr, onReset }) {
|
|
|
58
58
|
const [showQr, setShowQr] = React.useState(true);
|
|
59
59
|
|
|
60
60
|
const copy = React.useCallback(() => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// 优先使用现代 Clipboard API
|
|
62
|
+
if (navigator.clipboard?.writeText) {
|
|
63
|
+
navigator.clipboard.writeText(url)
|
|
64
|
+
.then(() => {
|
|
65
|
+
setCopied(true);
|
|
66
|
+
setTimeout(() => setCopied(false), 2000);
|
|
67
|
+
})
|
|
68
|
+
.catch(() => fallbackCopy());
|
|
69
|
+
} else {
|
|
70
|
+
fallbackCopy();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 降级方案:使用 document.execCommand (HTTP 环境下 Clipboard API 不可用)
|
|
74
|
+
function fallbackCopy() {
|
|
75
|
+
const textarea = document.createElement('textarea');
|
|
76
|
+
textarea.value = url;
|
|
77
|
+
textarea.style.position = 'fixed';
|
|
78
|
+
textarea.style.opacity = '0';
|
|
79
|
+
document.body.appendChild(textarea);
|
|
80
|
+
textarea.select();
|
|
81
|
+
const success = document.execCommand('copy');
|
|
82
|
+
document.body.removeChild(textarea);
|
|
83
|
+
if (success) {
|
|
84
|
+
setCopied(true);
|
|
85
|
+
setTimeout(() => setCopied(false), 2000);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
65
88
|
}, [url]);
|
|
66
89
|
|
|
67
90
|
const toggleQr = React.useCallback(() => setShowQr(v => !v), []);
|
|
@@ -504,34 +527,51 @@ function VersionBanner({ rpcCall }) { const [info, setInfo] = React.useState(nu
|
|
|
504
527
|
}, '🐛 反馈 Bug'),
|
|
505
528
|
);
|
|
506
529
|
|
|
507
|
-
//
|
|
530
|
+
// 有新版本:信息卡片
|
|
508
531
|
if (hasUpdate) {
|
|
509
532
|
return React.createElement('div', { style: { marginBottom: 16 } },
|
|
510
533
|
React.createElement('div', {
|
|
511
534
|
style: {
|
|
512
|
-
|
|
513
|
-
background: '
|
|
514
|
-
|
|
535
|
+
...s.card,
|
|
536
|
+
background: 'var(--dsw-alias-state-info-bg,#eff6ff)',
|
|
537
|
+
border: '1px solid var(--dsw-alias-state-info-border,#bfdbfe)',
|
|
538
|
+
padding: '12px 16px',
|
|
539
|
+
marginBottom: 8,
|
|
515
540
|
},
|
|
516
541
|
},
|
|
517
|
-
React.createElement('
|
|
518
|
-
|
|
519
|
-
React.createElement('div', { style: {
|
|
520
|
-
|
|
542
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12 } },
|
|
543
|
+
React.createElement('span', { style: { fontSize: 20 } }, '🎉'),
|
|
544
|
+
React.createElement('div', { style: { flex: 1 } },
|
|
545
|
+
React.createElement('div', {
|
|
546
|
+
style: {
|
|
547
|
+
fontSize: 13,
|
|
548
|
+
fontWeight: 600,
|
|
549
|
+
color: 'var(--dsw-alias-state-info-primary,#1e40af)',
|
|
550
|
+
marginBottom: 4,
|
|
551
|
+
},
|
|
552
|
+
}, `发现新版本 v${info.latest}(当前 v${info.current})`),
|
|
553
|
+
React.createElement('code', {
|
|
554
|
+
style: {
|
|
555
|
+
...s.code,
|
|
556
|
+
fontSize: 11,
|
|
557
|
+
color: 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
558
|
+
display: 'block',
|
|
559
|
+
},
|
|
560
|
+
}, 'dsh plugin --profile web update @wenbin_wb/dsh-bridge --latest'),
|
|
521
561
|
),
|
|
522
|
-
React.createElement('
|
|
523
|
-
style: {
|
|
524
|
-
|
|
562
|
+
React.createElement('button', {
|
|
563
|
+
style: {
|
|
564
|
+
...s.btnGhost,
|
|
565
|
+
height: 30,
|
|
566
|
+
padding: '0 12px',
|
|
567
|
+
fontSize: 12,
|
|
568
|
+
opacity: loading ? 0.5 : 1,
|
|
569
|
+
flexShrink: 0,
|
|
570
|
+
},
|
|
571
|
+
onClick: check,
|
|
572
|
+
disabled: loading,
|
|
573
|
+
}, loading ? '…' : '刷新'),
|
|
525
574
|
),
|
|
526
|
-
React.createElement('button', {
|
|
527
|
-
style: {
|
|
528
|
-
font: 'inherit', cursor: 'pointer', border: '1px solid rgba(255,255,255,0.4)',
|
|
529
|
-
background: 'rgba(255,255,255,0.15)', color: '#fff', height: 30, padding: '0 12px',
|
|
530
|
-
borderRadius: 999, fontSize: 12, display: 'inline-flex', alignItems: 'center',
|
|
531
|
-
opacity: loading ? 0.5 : 1, flexShrink: 0,
|
|
532
|
-
},
|
|
533
|
-
onClick: check, disabled: loading,
|
|
534
|
-
}, loading ? '…' : '刷新'),
|
|
535
575
|
),
|
|
536
576
|
links,
|
|
537
577
|
);
|
package/lib/wechat/gateway.js
CHANGED
|
@@ -310,7 +310,10 @@ export class WechatGateway extends Service {
|
|
|
310
310
|
rateLimitCircuitOpenMs: config.rateLimitCircuitOpenMs ?? 30_000,
|
|
311
311
|
rateLimitCircuitWindowMs: config.rateLimitCircuitWindowMs ?? 30_000,
|
|
312
312
|
rateLimitCircuitThreshold: config.rateLimitCircuitThreshold ?? 1,
|
|
313
|
-
allowCdnHosts:
|
|
313
|
+
allowCdnHosts: [
|
|
314
|
+
...DEFAULT_CDN_ALLOWLIST,
|
|
315
|
+
...(config.allowCdnHosts ?? [])
|
|
316
|
+
].filter((v, i, a) => a.indexOf(v) === i), // 去重
|
|
314
317
|
}
|
|
315
318
|
this.syncBuf = ''
|
|
316
319
|
this.pollTask = null
|
|
@@ -322,6 +325,32 @@ export class WechatGateway extends Service {
|
|
|
322
325
|
this.rateLimitHits = []
|
|
323
326
|
this.rateLimitUntil = 0
|
|
324
327
|
this._disposed = false
|
|
328
|
+
// 内存泄漏防护:每 5 分钟清理过期缓存
|
|
329
|
+
this.cleanupInterval = setInterval(() => this._cleanupMaps(), 300_000)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_cleanupMaps() {
|
|
333
|
+
const now = Date.now()
|
|
334
|
+
// 清理 contextTokens:超过 1 小时未使用的删除
|
|
335
|
+
const contextTokenTtl = 3600_000
|
|
336
|
+
// 清理 typingTickets:超过 30 秒的删除
|
|
337
|
+
const typingTicketTtl = 30_000
|
|
338
|
+
|
|
339
|
+
// contextTokens 没有时间戳,保守策略:如果 Map 过大才清理(超过 100 个)
|
|
340
|
+
if (this.contextTokens.size > 100) {
|
|
341
|
+
this.logger?.warn(`contextTokens Map 过大 (${this.contextTokens.size}),清理旧数据`)
|
|
342
|
+
// 保留最近 50 个,删除其余
|
|
343
|
+
const entries = Array.from(this.contextTokens.entries())
|
|
344
|
+
this.contextTokens.clear()
|
|
345
|
+
entries.slice(-50).forEach(([k, v]) => this.contextTokens.set(k, v))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 清理 typingTickets
|
|
349
|
+
for (const [peerId, ticket] of this.typingTickets) {
|
|
350
|
+
if (now - ticket.at > typingTicketTtl) {
|
|
351
|
+
this.typingTickets.delete(peerId)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
325
354
|
}
|
|
326
355
|
|
|
327
356
|
// ---- 状态访问器 ----------------------------------------------------------
|
|
@@ -337,6 +366,10 @@ export class WechatGateway extends Service {
|
|
|
337
366
|
dispose() {
|
|
338
367
|
this._disposed = true
|
|
339
368
|
this.stopPollingLocal = true
|
|
369
|
+
if (this.cleanupInterval) {
|
|
370
|
+
clearInterval(this.cleanupInterval)
|
|
371
|
+
this.cleanupInterval = null
|
|
372
|
+
}
|
|
340
373
|
void this.stop()
|
|
341
374
|
}
|
|
342
375
|
|
package/lib/wechat/node.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
15
15
|
import { randomUUID } from 'node:crypto'
|
|
16
16
|
import { statSync } from 'node:fs'
|
|
17
|
+
import { resolve, normalize } from 'node:path'
|
|
17
18
|
import { gatewayConstants } from './gateway.js'
|
|
18
19
|
|
|
19
20
|
const MAX_MESSAGE_CHARS = gatewayConstants.MAX_MESSAGE_CHARS
|
|
@@ -117,6 +118,13 @@ function packBlocks(blocks, max) {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function splitForWechat(content, max = MAX_MESSAGE_CHARS) {
|
|
121
|
+
// 安全检查:防止畸形输入导致无限循环或崩溃
|
|
122
|
+
if (typeof content !== 'string' || content.length === 0) return []
|
|
123
|
+
if (content.length > 1_000_000) {
|
|
124
|
+
// 超过 1MB 文本,截断并警告
|
|
125
|
+
content = content.slice(0, 1_000_000) + '\n\n[已截断:内容过长]'
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
const normalized = normalizeMarkdownBlocks(content)
|
|
121
129
|
if (!normalized) return []
|
|
122
130
|
if (normalized.length <= max) return [normalized]
|
|
@@ -252,7 +260,9 @@ export class WechatConversationNode {
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
isAllowed(senderId) {
|
|
255
|
-
|
|
263
|
+
if (!Array.isArray(this.config.allowFrom)) return false
|
|
264
|
+
if (this.config.allowFrom.length === 0) return false
|
|
265
|
+
return this.config.allowFrom.includes(senderId)
|
|
256
266
|
}
|
|
257
267
|
|
|
258
268
|
setActiveSession(session) {
|
|
@@ -279,12 +289,28 @@ export class WechatConversationNode {
|
|
|
279
289
|
const sessionId = `session-${randomUUID()}`
|
|
280
290
|
try {
|
|
281
291
|
const cwd = cwdOverride || this.config.cwd || process.cwd()
|
|
282
|
-
//
|
|
292
|
+
// 校验指定目录存在且是目录,防止路径遍历
|
|
283
293
|
if (cwdOverride) {
|
|
294
|
+
// 规范化路径并检查是否在工作区白名单内(必须完全匹配)
|
|
295
|
+
const resolvedCwd = normalize(resolve(cwdOverride))
|
|
296
|
+
const workspaces = await this.ctx.workspaceRegistry.list()
|
|
297
|
+
const allowedPaths = workspaces.map(w => normalize(resolve(w.path)))
|
|
298
|
+
|
|
299
|
+
if (!allowedPaths.includes(resolvedCwd)) {
|
|
300
|
+
const wsDisplay = allowedPaths.slice(0, 5).join('\n ')
|
|
301
|
+
const more = allowedPaths.length > 5 ? `\n ... 等 ${allowedPaths.length} 个工作区` : ''
|
|
302
|
+
await sendTextToPeer(this,
|
|
303
|
+
`${MARK.err} 路径不在已注册工作区内: ${resolvedCwd}\n\n` +
|
|
304
|
+
`可用工作区:\n ${wsDisplay}${more}\n\n` +
|
|
305
|
+
`提示:使用 /workspaces 查看完整列表`
|
|
306
|
+
)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
284
310
|
let ok = false
|
|
285
|
-
try { ok = statSync(
|
|
311
|
+
try { ok = statSync(resolvedCwd).isDirectory() } catch { ok = false }
|
|
286
312
|
if (!ok) {
|
|
287
|
-
await sendTextToPeer(this, `${MARK.err} 工作区目录不存在: ${
|
|
313
|
+
await sendTextToPeer(this, `${MARK.err} 工作区目录不存在: ${resolvedCwd}`)
|
|
288
314
|
return
|
|
289
315
|
}
|
|
290
316
|
}
|
|
@@ -338,6 +364,16 @@ export class WechatConversationNode {
|
|
|
338
364
|
}
|
|
339
365
|
}
|
|
340
366
|
|
|
367
|
+
// 取消并拒绝审批(用于 dispose 清理)
|
|
368
|
+
cancelApproval(number) {
|
|
369
|
+
const entry = this.pending.get(number)
|
|
370
|
+
if (entry) {
|
|
371
|
+
clearTimeout(entry.timer)
|
|
372
|
+
this.pending.delete(number)
|
|
373
|
+
entry.resolve('rejected') // 触发 Promise,防止泄漏
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
341
377
|
resolveApproval(text) {
|
|
342
378
|
const entries = [...this.pending.entries()]
|
|
343
379
|
if (entries.length === 0) return false
|
|
@@ -346,12 +382,16 @@ export class WechatConversationNode {
|
|
|
346
382
|
else if (text === '/no') outcome = 'rejected'
|
|
347
383
|
if (outcome) {
|
|
348
384
|
const [number, entry] = entries[entries.length - 1]
|
|
385
|
+
// 双重检查:确保此 number 仍在 pending 中(防止超时竞态)
|
|
386
|
+
if (!this.pending.has(number)) return false
|
|
349
387
|
this.clearApproval(number)
|
|
350
388
|
entry.resolve(outcome)
|
|
351
389
|
return true
|
|
352
390
|
}
|
|
353
391
|
if ((text === '1' || text === '2') && entries.length === 1) {
|
|
354
392
|
const [number, entry] = entries[0]
|
|
393
|
+
// 双重检查:确保此 number 仍在 pending 中(防止超时竞态)
|
|
394
|
+
if (!this.pending.has(number)) return false
|
|
355
395
|
this.clearApproval(number)
|
|
356
396
|
entry.resolve(text === '1' ? 'allowed-once' : 'rejected')
|
|
357
397
|
return true
|
|
@@ -364,7 +404,7 @@ export class WechatConversationNode {
|
|
|
364
404
|
try { disposer() } catch { /* 忽略 */ }
|
|
365
405
|
}
|
|
366
406
|
this.disposers = []
|
|
367
|
-
for (const number of [...this.pending.keys()]) this.
|
|
407
|
+
for (const number of [...this.pending.keys()]) this.cancelApproval(number)
|
|
368
408
|
}
|
|
369
409
|
|
|
370
410
|
// ---- 入站 ----
|
|
@@ -386,11 +426,18 @@ export class WechatConversationNode {
|
|
|
386
426
|
if (text0.trim()) {
|
|
387
427
|
this.config.allowFrom = [sender]
|
|
388
428
|
this.logger?.info?.(`[dsh-bridge wechat] auto-approved first sender ${sender} into allowlist (scan onboarding)`)
|
|
389
|
-
try {
|
|
429
|
+
try {
|
|
430
|
+
await this.onFirstSender?.(sender)
|
|
431
|
+
} catch (err) {
|
|
432
|
+
this.logger?.warn?.(`[dsh-bridge wechat] failed to persist first sender: ${err instanceof Error ? err.message : String(err)}`)
|
|
433
|
+
}
|
|
390
434
|
} else {
|
|
391
435
|
this.logger?.info?.(`[dsh-bridge wechat] media-only first message from ${sender} not auto-approved (waiting for text)`)
|
|
392
436
|
}
|
|
393
|
-
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 如果仍未通过白名单,拒绝处理(防止绕过白名单)
|
|
440
|
+
if (!this.isAllowed(sender)) {
|
|
394
441
|
this.logger?.info?.(`[dsh-bridge wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
|
|
395
442
|
return
|
|
396
443
|
}
|
|
@@ -495,6 +542,8 @@ export class WechatConversationNode {
|
|
|
495
542
|
} catch (err) {
|
|
496
543
|
const reason = err instanceof Error ? err.message : String(err)
|
|
497
544
|
this.logger?.warn?.(`[dsh-bridge wechat] failed to re-attach agent: ${reason}`)
|
|
545
|
+
// 清除失效的 activeSessionId,避免用户误以为还在旧会话中
|
|
546
|
+
this.activeSessionId = null
|
|
498
547
|
await sendTextToPeer(this, `${MARK.err} 恢复会话失败: ${reason}。发送 /new <提示词> 新建一个会话。`)
|
|
499
548
|
}
|
|
500
549
|
}
|
|
@@ -747,8 +796,10 @@ export class WechatConversationNode {
|
|
|
747
796
|
|
|
748
797
|
void sendTextToPeer(this, prompt)
|
|
749
798
|
|
|
799
|
+
let timeoutFired = false
|
|
750
800
|
const outcome = await new Promise((resolve) => {
|
|
751
801
|
const timer = setTimeout(() => {
|
|
802
|
+
timeoutFired = true
|
|
752
803
|
this.clearApproval(number)
|
|
753
804
|
resolve('rejected')
|
|
754
805
|
}, timeoutSec * 1000)
|
|
@@ -756,8 +807,11 @@ export class WechatConversationNode {
|
|
|
756
807
|
this.registerApproval(number, { number, request: req, resolve, timer })
|
|
757
808
|
})
|
|
758
809
|
|
|
759
|
-
|
|
760
|
-
|
|
810
|
+
// 仅在非超时路径发送确认消息(超时时 resolve 已经发生在 timer 回调)
|
|
811
|
+
if (!timeoutFired) {
|
|
812
|
+
const label = outcome === 'allowed-once' ? `${MARK.ok} 已同意` : outcome === 'rejected' ? `${MARK.err} 已拒绝` : `[${outcome}]`
|
|
813
|
+
void sendTextToPeer(this, `${label}(#${number})`)
|
|
814
|
+
}
|
|
761
815
|
return outcome
|
|
762
816
|
}
|
|
763
817
|
const disposer = this.ctx.on('approval/request', listener)
|
|
@@ -867,9 +921,9 @@ async function listSessions(node) {
|
|
|
867
921
|
}
|
|
868
922
|
|
|
869
923
|
// 列出可用工作区:使用 DSH 官方 workspaceRegistry。返回 [{title, path}]。
|
|
870
|
-
function listWorkspaces(node) {
|
|
924
|
+
async function listWorkspaces(node) {
|
|
871
925
|
try {
|
|
872
|
-
const list = node.ctx.workspaceRegistry?.list?.() ?? []
|
|
926
|
+
const list = await node.ctx.workspaceRegistry?.list?.() ?? []
|
|
873
927
|
const out = []
|
|
874
928
|
for (const ws of list) {
|
|
875
929
|
if (ws && ws.path) out.push({ title: ws.title ?? ws.path, path: ws.path })
|
|
@@ -909,7 +963,7 @@ async function routeCommand(node, text) {
|
|
|
909
963
|
return true
|
|
910
964
|
}
|
|
911
965
|
case 'workspaces': {
|
|
912
|
-
const workspaces = listWorkspaces(node)
|
|
966
|
+
const workspaces = await listWorkspaces(node)
|
|
913
967
|
if (workspaces.length === 0) {
|
|
914
968
|
await sendTextToPeer(node, `${MARK.list} 没有可用的工作区。使用 /new <提示词> @<路径> 指定一个目录。`)
|
|
915
969
|
return true
|
|
@@ -930,14 +984,27 @@ async function routeCommand(node, text) {
|
|
|
930
984
|
if (atMatch) {
|
|
931
985
|
prompt = args.slice(0, atMatch.index).trim()
|
|
932
986
|
const sel = atMatch[1]
|
|
933
|
-
const workspaces = listWorkspaces(node)
|
|
987
|
+
const workspaces = await listWorkspaces(node)
|
|
934
988
|
if (/^\d+$/.test(sel)) {
|
|
935
989
|
const idx = Number(sel)
|
|
936
990
|
const ws = workspaces[idx - 1]
|
|
937
991
|
if (ws) cwd = ws.path
|
|
938
992
|
else { await sendTextToPeer(node, `${MARK.err} 无效工作区编号 ${sel}。用 /workspaces 查看。`); return true }
|
|
939
993
|
} else {
|
|
940
|
-
|
|
994
|
+
// 直接指定路径时,规范化并校验(必须完全匹配已注册工作区)
|
|
995
|
+
const normalized = normalize(resolve(sel))
|
|
996
|
+
const allowedPaths = workspaces.map(w => normalize(resolve(w.path)))
|
|
997
|
+
if (!allowedPaths.includes(normalized)) {
|
|
998
|
+
const wsDisplay = allowedPaths.slice(0, 5).join('\n ')
|
|
999
|
+
const more = allowedPaths.length > 5 ? `\n ... 等 ${allowedPaths.length} 个工作区` : ''
|
|
1000
|
+
await sendTextToPeer(node,
|
|
1001
|
+
`${MARK.err} 路径不在已注册工作区内: ${normalized}\n\n` +
|
|
1002
|
+
`可用工作区:\n ${wsDisplay}${more}\n\n` +
|
|
1003
|
+
`提示:使用 /workspaces 查看完整列表`
|
|
1004
|
+
)
|
|
1005
|
+
return true
|
|
1006
|
+
}
|
|
1007
|
+
cwd = normalized
|
|
941
1008
|
}
|
|
942
1009
|
}
|
|
943
1010
|
await node.createSession(prompt, cwd)
|
package/package.json
CHANGED