@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.
@@ -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 cwd = cwdOverride || this.config.cwd || process.cwd()
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: prompt }],
407
+ content: [{ type: 'text', text: promptWithContext }],
394
408
  source: { kind: 'user' },
395
409
  }))
396
410
  }
397
- const wsDetail = cwdOverride ? `\n- **工作区**:\`${cwdOverride}\`` : ''
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
- if (this._restoringConfig) {
494
- await this._restoringConfig
495
- }
506
+ try {
507
+ // 等待配置恢复完成(防止启动时竞态)
508
+ if (this._restoringConfig) {
509
+ await this._restoringConfig
510
+ }
496
511
 
497
- const sender = String(senderId ?? '').trim()
498
- if (!sender) return 'ignored'
499
-
500
- if (!this.isAllowed(sender)) {
501
- // 自动授权:
502
- // - 单聊:白名单为空时,首个发消息的真实用户自动纳入白名单
503
- // - 群聊:首次 @机器人 的群自动纳入(群维度授权,群内成员均可使用)
504
- // 这是"登录后第一条消息/首次被 @即完成授权"的一步到位体验。
505
- const shouldAutoApprove = Boolean(text?.trim()) && (
506
- this.config.allowFrom.length === 0 || // 白名单为空:单聊/群聊都自动授权
507
- isGroup // 群聊:始终自动授权群
508
- )
509
- if (shouldAutoApprove) {
510
- this.config.allowFrom = Array.from(new Set([...this.config.allowFrom, sender]))
511
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] auto-approved ${isGroup ? 'group' : 'sender'} ${sender} into allowlist`)
512
- try {
513
- await this.onFirstSender?.(sender)
514
- } catch (err) {
515
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to persist first sender: ${err instanceof Error ? err.message : String(err)}`)
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.isAllowed(sender)) {
523
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
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
- const fullText = text?.trim() ?? ''
535
- if (!fullText) {
536
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore empty message from ${sender}`)
537
- return 'ignored'
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
- this.peerId = sender
555
+ this.peerId = sender
541
556
 
542
- if (await routeCommand(this, fullText)) return 'routed'
557
+ if (await routeCommand(this, fullText)) return 'routed'
543
558
 
544
- let agent = this.activeAgent()
545
- if (!agent && this.activeSessionId) {
546
- const sessionId = this.activeSessionId
547
- if (this._restoringSessionMap.has(sessionId)) {
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
- const agentOptions = {}
557
- if (this.config.agentProvider) agentOptions.provider = this.config.agentProvider
558
- if (this.config.agentModel) agentOptions.model = this.config.agentModel
559
- if (!agentOptions.provider || !agentOptions.model) {
560
- try {
561
- const def = this.ctx.get?.('agentDefaultModel')?.currentSelection?.()
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 headers = await this.ctx.sessionPersistence?.list?.()
574
- persisted = Array.isArray(headers) && headers.some((h) => h?.id === sessionId)
575
- } catch { /* 读取失败则按未持久化处理 */ }
576
-
577
- let handle
578
- if (persisted) {
579
- handle = await this.ctx.agents.resume({
580
- resumeSessionId: sessionId,
581
- agentOptions,
582
- })
583
- } else {
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
- handle = await this.ctx.agents.create({
591
- sessionId: sessionId,
592
- meta,
593
- agentOptions,
594
- })
595
- }
596
- const resumedAgent = handle?.agent
597
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] re-attached agent to session ${sessionId} (${persisted ? 'resume' : 'create'})`)
598
- return resumedAgent
599
- } catch (err) {
600
- const reason = err instanceof Error ? err.message : String(err)
601
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to re-attach agent: ${reason}`)
602
- // 清除失效的 activeSessionId,避免用户误以为还在旧会话中
603
- if (this.activeSessionId === sessionId) {
604
- this.activeSessionId = null
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
- await this.sendText(`❌ **恢复会话失败**:${reason}\n\n> 发送 \`/new <提示词>\` 可新建一个会话。`)
607
- return null
608
- }
609
- })().finally(() => {
610
- this._restoringSessionMap.delete(sessionId)
611
- })
618
+ })().finally(() => {
619
+ this._restoringSessionMap.delete(sessionId)
620
+ })
612
621
 
613
- this._restoringSessionMap.set(sessionId, restorePromise)
614
- agent = await restorePromise
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
- // 针对微信/IM客户端用户,注入上下文提示,规范 Agent 仅在需要向用户发送文件附件时输出 [SEND_FILE: <文件绝对路径>]
623
- const promptWithContext = `${fullText}\n\n<!-- [dsh-bridge 提示] 当前用户正通过【${this.platform.name}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
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
- const messageValue = createUserMessage({
626
- content: [{ type: 'text', text: promptWithContext }],
627
- source: { kind: 'user' },
628
- })
629
- agent.followup(messageValue)
630
- await this.sendTyping(1).catch(() => {})
631
- return 'routed'
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 safeTitle = (rawTitle || '新会话').replace(/\|/g, '').replace(/\r?\n/g, ' ')
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
  }