@wenbin_wb/dsh-bridge 2.8.5 → 2.8.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.
- package/CHANGELOG.md +34 -0
- package/client/client.js +396 -39
- package/client/index.js +399 -40
- package/docs/fix-tunnel-sse-and-session-list.md +134 -0
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +9 -0
- package/lib/cloudflared-manager.mjs +75 -12
- package/lib/index.js +147 -26
- package/lib/platform/conversation-bridge.js +153 -128
- package/lib/tunnel-client.mjs +402 -289
- package/package.json +2 -2
|
@@ -360,7 +360,9 @@ export class ConversationBridge {
|
|
|
360
360
|
// 使用 DSH 原生格式 session-${uuid},与 ctx.sessions 持久化系统兼容
|
|
361
361
|
const sessionId = `session-${randomUUID()}`
|
|
362
362
|
try {
|
|
363
|
-
const
|
|
363
|
+
const workspaces = await listWorkspaces(this)
|
|
364
|
+
const defaultWsPath = workspaces.length > 0 ? workspaces[0].path : process.cwd()
|
|
365
|
+
const cwd = cwdOverride || this.config.cwd || defaultWsPath
|
|
364
366
|
// 校验指定目录存在且是目录,防止路径遍历
|
|
365
367
|
if (cwdOverride) {
|
|
366
368
|
const validation = await validateWorkspacePath(this, cwdOverride)
|
|
@@ -388,13 +390,25 @@ export class ConversationBridge {
|
|
|
388
390
|
// 注意:不预创建 ctx.sessions——agents.create 会自己 prepare+enter session
|
|
389
391
|
const handle = await this.ctx.agents.create({ sessionId, meta, agentOptions })
|
|
390
392
|
this.setActiveSession(handle.agent.session)
|
|
393
|
+
|
|
394
|
+
// 同步挂载进 DSH 工作区账本 (workspaceRegistry),使新会话在 Web 侧边栏和 /list 中精准归组
|
|
395
|
+
try {
|
|
396
|
+
const reg = this.ctx.workspaceRegistry
|
|
397
|
+
const entities = reg?.list ? (await reg.list()) : []
|
|
398
|
+
const normCwd = normalize(cwd).toLowerCase()
|
|
399
|
+
const match = (entities || []).find((ws) => ws?.path && normalize(ws.path).toLowerCase() === normCwd)
|
|
400
|
+
if (match?.attachSession) await match.attachSession(sessionId)
|
|
401
|
+
} catch { /* 账本写入失败不影响会话创建 */ }
|
|
402
|
+
|
|
391
403
|
if (prompt) {
|
|
404
|
+
const platformName = this.platform?.name || 'IM客户端'
|
|
405
|
+
const promptWithContext = `${prompt}\n\n<!-- [dsh-bridge 提示] 当前用户正通过【${platformName}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
|
|
392
406
|
handle.agent.followup(createUserMessage({
|
|
393
|
-
content: [{ type: 'text', text:
|
|
407
|
+
content: [{ type: 'text', text: promptWithContext }],
|
|
394
408
|
source: { kind: 'user' },
|
|
395
409
|
}))
|
|
396
410
|
}
|
|
397
|
-
const wsDetail =
|
|
411
|
+
const wsDetail = cwd ? `\n- **工作区**:\`${cwd}\`` : ''
|
|
398
412
|
const hint = prompt ? '' : '\n\n> 💡 发送任意消息即可直接与 Agent 对话。'
|
|
399
413
|
await this.sendText(`✓ **已创建新会话**\n- **会话 ID**:\`${fmtSessionId(handle.agent.session.id)}\`${wsDetail}${hint}`)
|
|
400
414
|
} catch (error) {
|
|
@@ -489,146 +503,147 @@ export class ConversationBridge {
|
|
|
489
503
|
// 'ignored' 消息被忽略(未授权/群消息/空消息)
|
|
490
504
|
// 'routed' 消息已路由到 agent
|
|
491
505
|
async handleInbound({ senderId, text, isGroup = false }) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
506
|
+
try {
|
|
507
|
+
// 等待配置恢复完成(防止启动时竞态)
|
|
508
|
+
if (this._restoringConfig) {
|
|
509
|
+
await this._restoringConfig
|
|
510
|
+
}
|
|
496
511
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
512
|
+
const sender = String(senderId ?? '').trim()
|
|
513
|
+
if (!sender) return 'ignored'
|
|
514
|
+
|
|
515
|
+
if (!this.isAllowed(sender)) {
|
|
516
|
+
// 自动授权:
|
|
517
|
+
// - 单聊:白名单为空时,首个发消息的真实用户自动纳入白名单
|
|
518
|
+
// - 群聊:首次 @机器人 的群自动纳入(群维度授权,群内成员均可使用)
|
|
519
|
+
// 这是"登录后第一条消息/首次被 @即完成授权"的一步到位体验。
|
|
520
|
+
const shouldAutoApprove = Boolean(text?.trim()) && (
|
|
521
|
+
this.config.allowFrom.length === 0 || // 白名单为空:单聊/群聊都自动授权
|
|
522
|
+
isGroup // 群聊:始终自动授权群
|
|
523
|
+
)
|
|
524
|
+
if (shouldAutoApprove) {
|
|
525
|
+
this.config.allowFrom = Array.from(new Set([...this.config.allowFrom, sender]))
|
|
526
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] auto-approved ${isGroup ? 'group' : 'sender'} ${sender} into allowlist`)
|
|
527
|
+
try {
|
|
528
|
+
await this.onFirstSender?.(sender)
|
|
529
|
+
} catch (err) {
|
|
530
|
+
this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to persist first sender: ${err instanceof Error ? err.message : String(err)}`)
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] media-only first message from ${sender} not auto-approved (waiting for text)`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 如果仍未通过白名单,拒绝处理(防止绕过白名单)
|
|
537
|
+
if (!this.isAllowed(sender)) {
|
|
538
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
|
|
539
|
+
return 'ignored'
|
|
516
540
|
}
|
|
517
|
-
} else {
|
|
518
|
-
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] media-only first message from ${sender} not auto-approved (waiting for text)`)
|
|
519
541
|
}
|
|
520
542
|
|
|
521
|
-
//
|
|
522
|
-
if (!this.
|
|
523
|
-
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore message from
|
|
543
|
+
// 仅在不支持群聊的平台忽略群消息(QQ 等支持群聊的平台放行)
|
|
544
|
+
if (isGroup && !this.platform?.capabilities?.supportsGroup) {
|
|
545
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore group message from ${sender} (no group support)`)
|
|
524
546
|
return 'ignored'
|
|
525
547
|
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// 仅在不支持群聊的平台忽略群消息(QQ 等支持群聊的平台放行)
|
|
529
|
-
if (isGroup && !this.platform?.capabilities?.supportsGroup) {
|
|
530
|
-
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore group message from ${sender} (no group support)`)
|
|
531
|
-
return 'ignored'
|
|
532
|
-
}
|
|
533
548
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
549
|
+
const fullText = text?.trim() ?? ''
|
|
550
|
+
if (!fullText) {
|
|
551
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore empty message from ${sender}`)
|
|
552
|
+
return 'ignored'
|
|
553
|
+
}
|
|
539
554
|
|
|
540
|
-
|
|
555
|
+
this.peerId = sender
|
|
541
556
|
|
|
542
|
-
|
|
557
|
+
if (await routeCommand(this, fullText)) return 'routed'
|
|
543
558
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
try {
|
|
549
|
-
await this._restoringSessionMap.get(sessionId)
|
|
550
|
-
} catch { /* 错误已在原始 Promise 中捕获 */ }
|
|
551
|
-
agent = this.activeAgent()
|
|
552
|
-
} else {
|
|
553
|
-
const restorePromise = (async () => {
|
|
554
|
-
// agent 不在内存(DSH 重启后或切换到持久化会话)→ re-attach。
|
|
559
|
+
let agent = this.activeAgent()
|
|
560
|
+
if (!agent && this.activeSessionId) {
|
|
561
|
+
const sessionId = this.activeSessionId
|
|
562
|
+
if (this._restoringSessionMap.has(sessionId)) {
|
|
555
563
|
try {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (def) {
|
|
563
|
-
if (!agentOptions.provider && def.provider) agentOptions.provider = def.provider
|
|
564
|
-
if (!agentOptions.model && def.model) agentOptions.model = def.model
|
|
565
|
-
}
|
|
566
|
-
} catch { /* ignore */ }
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// 判断该 session 是否已持久化:已持久化 → agents.resume(从持久化加载历史恢复,
|
|
570
|
-
// 避免 agents.create 用空 seed 与已持久化事件冲突);未持久化 → agents.create。
|
|
571
|
-
let persisted = false
|
|
564
|
+
await this._restoringSessionMap.get(sessionId)
|
|
565
|
+
} catch { /* 错误已在原始 Promise 中捕获 */ }
|
|
566
|
+
agent = this.activeAgent()
|
|
567
|
+
} else {
|
|
568
|
+
const restorePromise = (async () => {
|
|
569
|
+
// agent 不在内存(DSH 重启后或切换到持久化会话)→ re-attach。
|
|
572
570
|
try {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// 读取持久化会话的 cwd 做 fallback(新建会话时用)
|
|
585
|
-
let sessionCwd = this.config.cwd || process.cwd()
|
|
586
|
-
const meta = {
|
|
587
|
-
cwd: sessionCwd,
|
|
588
|
-
agentPreset: this.config.agentPreset || 'routing-suite',
|
|
571
|
+
const agentOptions = {}
|
|
572
|
+
if (this.config.agentProvider) agentOptions.provider = this.config.agentProvider
|
|
573
|
+
if (this.config.agentModel) agentOptions.model = this.config.agentModel
|
|
574
|
+
if (!agentOptions.provider || !agentOptions.model) {
|
|
575
|
+
try {
|
|
576
|
+
const def = this.ctx.get?.('agentDefaultModel')?.currentSelection?.()
|
|
577
|
+
if (def) {
|
|
578
|
+
if (!agentOptions.provider && def.provider) agentOptions.provider = def.provider
|
|
579
|
+
if (!agentOptions.model && def.model) agentOptions.model = def.model
|
|
580
|
+
}
|
|
581
|
+
} catch { /* ignore */ }
|
|
589
582
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
583
|
+
|
|
584
|
+
// 判断该 session 是否已持久化:已持久化 → agents.resume(从持久化加载历史恢复,
|
|
585
|
+
// 避免 agents.create 用空 seed 与已持久化事件冲突);未持久化 → agents.create。
|
|
586
|
+
let persisted = false
|
|
587
|
+
try {
|
|
588
|
+
const headers = await this.ctx.sessionPersistence?.list?.()
|
|
589
|
+
persisted = Array.isArray(headers) && headers.some((h) => h?.id === sessionId)
|
|
590
|
+
} catch { /* 读取失败则按未持久化处理 */ }
|
|
591
|
+
|
|
592
|
+
let handle
|
|
593
|
+
if (persisted) {
|
|
594
|
+
handle = await this.ctx.agents.resume({
|
|
595
|
+
resumeSessionId: sessionId,
|
|
596
|
+
agentOptions,
|
|
597
|
+
})
|
|
598
|
+
} else {
|
|
599
|
+
handle = await this.ctx.agents.create({
|
|
600
|
+
sessionId,
|
|
601
|
+
meta: { cwd: this.config.cwd || process.cwd() },
|
|
602
|
+
agentOptions,
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
const resumedAgent = handle?.agent
|
|
606
|
+
this.logger?.info?.(`[dsh-bridge ${this.platform.id}] re-attached agent to session ${sessionId} (${persisted ? 'resume' : 'create'})`)
|
|
607
|
+
return resumedAgent
|
|
608
|
+
} catch (err) {
|
|
609
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
610
|
+
this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to re-attach agent: ${reason}`)
|
|
611
|
+
// 清除失效的 activeSessionId,避免用户误以为还在旧会话中
|
|
612
|
+
if (this.activeSessionId === sessionId) {
|
|
613
|
+
this.activeSessionId = null
|
|
614
|
+
}
|
|
615
|
+
await this.sendText(`❌ **恢复会话失败**:${reason}\n\n> 发送 \`/new <提示词>\` 可新建一个会话。`)
|
|
616
|
+
return null
|
|
605
617
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
})().finally(() => {
|
|
610
|
-
this._restoringSessionMap.delete(sessionId)
|
|
611
|
-
})
|
|
618
|
+
})().finally(() => {
|
|
619
|
+
this._restoringSessionMap.delete(sessionId)
|
|
620
|
+
})
|
|
612
621
|
|
|
613
|
-
|
|
614
|
-
|
|
622
|
+
this._restoringSessionMap.set(sessionId, restorePromise)
|
|
623
|
+
agent = await restorePromise
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!agent) {
|
|
627
|
+
await this.sendText(`> 💤 **当前没有活动会话**\n> 发送 \`/new <提示词>\` 开始新会话,或发送 \`/sessions\` 查看已有会话。`)
|
|
628
|
+
return 'routed'
|
|
615
629
|
}
|
|
616
|
-
}
|
|
617
|
-
if (!agent) {
|
|
618
|
-
await this.sendText(`> 💤 **当前没有活动会话**\n> 发送 \`/new <提示词>\` 开始新会话,或发送 \`/sessions\` 查看已有会话。`)
|
|
619
|
-
return 'routed'
|
|
620
|
-
}
|
|
621
630
|
|
|
622
|
-
|
|
623
|
-
|
|
631
|
+
// 针对微信/IM客户端用户,注入上下文提示,规范 Agent 仅在需要向用户发送文件附件时输出 [SEND_FILE: <文件绝对路径>]
|
|
632
|
+
const promptWithContext = `${fullText}\n\n<!-- [dsh-bridge 提示] 当前用户正通过【${this.platform.name}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
|
|
624
633
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
634
|
+
const messageValue = createUserMessage({
|
|
635
|
+
content: [{ type: 'text', text: promptWithContext }],
|
|
636
|
+
source: { kind: 'user' },
|
|
637
|
+
})
|
|
638
|
+
agent.followup(messageValue)
|
|
639
|
+
await this.sendTyping(1).catch(() => {})
|
|
640
|
+
return 'routed'
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
643
|
+
this.logger?.error?.(`[dsh-bridge ${this.platform.id}] unhandled error in handleInbound: ${errMsg}`)
|
|
644
|
+
await this.sendText(`❌ **执行出错**:${errMsg}`).catch(() => {})
|
|
645
|
+
return 'ignored'
|
|
646
|
+
}
|
|
632
647
|
}
|
|
633
648
|
|
|
634
649
|
// ---- 发送(子类必须实现)----
|
|
@@ -1027,6 +1042,15 @@ function isSubagentSession(cacheRow, liveSession) {
|
|
|
1027
1042
|
return false
|
|
1028
1043
|
}
|
|
1029
1044
|
|
|
1045
|
+
function formatGoalTitle(raw) {
|
|
1046
|
+
if (typeof raw === 'string' && raw) return raw
|
|
1047
|
+
if (raw && typeof raw === 'object') {
|
|
1048
|
+
const obj = raw.objective ?? raw.goal?.objective ?? raw.title
|
|
1049
|
+
if (typeof obj === 'string' && obj) return obj
|
|
1050
|
+
}
|
|
1051
|
+
return ''
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1030
1054
|
// 列出会话:严格对齐 DSH Web 端侧边栏会话树逻辑。
|
|
1031
1055
|
// 1. 过滤已归档会话 (archivedSessionIds)
|
|
1032
1056
|
// 2. 过滤未发起提问的空白会话 (blank: true)
|
|
@@ -1060,7 +1084,7 @@ async function listSessions(node) {
|
|
|
1060
1084
|
let title = s.title || (s.events ? foldTitle(s.events) : '')
|
|
1061
1085
|
if (!title) {
|
|
1062
1086
|
const cache = projCache[s.id]
|
|
1063
|
-
title = cache?.rows?.title?.val || cache?.rows?.goal?.val
|
|
1087
|
+
title = cache?.rows?.title?.val || formatGoalTitle(cache?.rows?.goal?.val)
|
|
1064
1088
|
}
|
|
1065
1089
|
result.push({
|
|
1066
1090
|
id: s.id,
|
|
@@ -1090,7 +1114,7 @@ async function listSessions(node) {
|
|
|
1090
1114
|
continue
|
|
1091
1115
|
}
|
|
1092
1116
|
|
|
1093
|
-
let title = cache?.rows?.title?.val || cache?.rows?.goal?.val
|
|
1117
|
+
let title = cache?.rows?.title?.val || formatGoalTitle(cache?.rows?.goal?.val)
|
|
1094
1118
|
let createdAt = cache?.identity?.createdAt || 0
|
|
1095
1119
|
let cwd = ws.path
|
|
1096
1120
|
|
|
@@ -1453,7 +1477,8 @@ async function renderSessions(node) {
|
|
|
1453
1477
|
const isActive = session.id === node.activeSessionId
|
|
1454
1478
|
const statusTag = isActive ? '`[当前]`' : '-'
|
|
1455
1479
|
const rawTitle = session.title || (session.events ? sessionLabel(session) : '')
|
|
1456
|
-
const
|
|
1480
|
+
const titleText = formatGoalTitle(rawTitle) || (typeof rawTitle === 'string' ? rawTitle : '') || '新会话'
|
|
1481
|
+
const safeTitle = String(titleText).replace(/\|/g, '|').replace(/\r?\n/g, ' ')
|
|
1457
1482
|
const when = session.createdAt ? fmtTime(session.createdAt) : '-'
|
|
1458
1483
|
parts.push(`| **#${idx}** | ${safeTitle} | ${when} | ${statusTag} |`)
|
|
1459
1484
|
}
|