@wenbin_wb/dsh-bridge 2.10.8 → 2.10.10

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.
@@ -19,11 +19,11 @@
19
19
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
20
20
  import { randomUUID } from 'node:crypto'
21
21
  import { stat } from 'node:fs/promises'
22
- import { normalize } from 'node:path'
22
+ import { normalize, isAbsolute } from 'node:path'
23
23
  import { resolveFilePath } from './message-split.js'
24
24
  import { splitForIM, textOfAssistantMessage, extractAndStripSendFileDirectives, extractFilePathsFromText, isPathAllowedForSend } from './message-split.js'
25
25
  import { routeCommand } from './commands.js'
26
- import { listSessions, listWorkspaces, validateWorkspacePath, renderSessions, sessionsInDisplayOrder, describeTurnEnd, helpText, fmtTime, fmtSessionId, sessionLabel } from './session-catalog.js'
26
+ import { listSessions, listWorkspaces, validateWorkspacePath, renderSessions, sessionsInDisplayOrder, describeTurnEnd, helpText, fmtTime, fmtSessionId, sessionLabel, sessionHeaderOf } from './session-catalog.js'
27
27
 
28
28
  export { textOfAssistantMessage } from './message-split.js'
29
29
 
@@ -67,6 +67,22 @@ function sleep(ms) {
67
67
  return new Promise((resolve) => setTimeout(resolve, ms))
68
68
  }
69
69
 
70
+ /**
71
+ * 判断某个路径是否为可用的绝对目录。
72
+ * 用于校验从配置恢复出来的 cwd:DSH 只拒绝非绝对路径,不会替调用方创建目录,
73
+ * 不校验就会把会话建在一个不存在的 cwd 上。
74
+ * @param {unknown} path
75
+ * @returns {Promise<boolean>}
76
+ */
77
+ async function isUsableDirectory(path) {
78
+ if (typeof path !== 'string' || path.length === 0 || !isAbsolute(path)) return false
79
+ try {
80
+ return (await stat(path)).isDirectory()
81
+ } catch {
82
+ return false
83
+ }
84
+ }
85
+
70
86
  // ---------------------------------------------------------------------------
71
87
  // 会话桥基类
72
88
  // ---------------------------------------------------------------------------
@@ -183,13 +199,51 @@ export class ConversationBridge {
183
199
  if (sessions.length > 0) this.setActiveSessionById(sessions[0].id)
184
200
  }
185
201
 
202
+ // ---- Agent preset 组装(DSH ≥0.1.5 的 agent plane 挂载)----
203
+ //
204
+ // DSH 0.1.5 起把 agent plane(工具、提示词段、技能目录)整体挪到了 agent preset 后面:
205
+ // 宿主 composition 里 tool-fs / tool-bash / tool-jobs / tool-skill 等行被显式禁用,
206
+ // 改由每个会话在创建时挂载 preset 提供。挂载只发生在 agents.create / agents.resume 的
207
+ // setup 回调里——DSH 自己的会话控制器就是 `resolve()` + `mount(agentCtx, id)`;
208
+ // 只往 meta 里写 agentPreset 不会挂载任何东西,该会话会落在"空 preset 层",
209
+ // 表现为工具集残缺(dsh-agent-presets 会打 "published without joining an agent preset" 警告)。
210
+ //
211
+ // 旧版 DSH(agent plane 仍在宿主层、无 agentPresets 服务)保持原行为,不传 setup。
212
+ // 配置或历史里记着一个本机不存在的 preset 时,回退到 DSH 默认 preset 并告警,
213
+ // 绝不在空层上建会话,也不把 not-found 错误直接甩给用户。
214
+ async _composeAgentPreset(presetId) {
215
+ // 空串/纯空白视为"未配置"(设置页清除该字段后就是空串),交给 DSH 默认预设
216
+ const wanted = typeof presetId === 'string' && presetId.trim().length > 0 ? presetId.trim() : undefined
217
+ const presets = this.ctx.get?.('agentPresets')
218
+ if (!presets?.resolve || !presets?.mount) {
219
+ return { agentPreset: wanted, setup: undefined, fallbackFrom: undefined }
220
+ }
221
+ let resolvedId
222
+ let fallbackFrom
223
+ try {
224
+ resolvedId = (await presets.resolve(wanted)).id
225
+ } catch (err) {
226
+ if (!wanted) throw err
227
+ const reason = err instanceof Error ? err.message : String(err)
228
+ this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] agent preset "${wanted}" unavailable (${reason}); falling back to the default preset`)
229
+ resolvedId = (await presets.resolve(undefined)).id
230
+ fallbackFrom = wanted
231
+ }
232
+ return {
233
+ agentPreset: resolvedId,
234
+ fallbackFrom,
235
+ setup: async (agentCtx) => { await presets.mount(agentCtx, resolvedId) },
236
+ }
237
+ }
238
+
186
239
  async createSession(prompt, cwdOverride) {
187
240
  // 使用 DSH 原生格式 session-${uuid},与 ctx.sessions 持久化系统兼容
188
241
  const sessionId = `session-${randomUUID()}`
189
242
  try {
190
243
  const workspaces = await listWorkspaces(this)
191
244
  const defaultWsPath = workspaces.length > 0 ? workspaces[0].path : process.cwd()
192
- const cwd = cwdOverride || this.config.cwd || defaultWsPath
245
+ let cwd = cwdOverride || this.config.cwd || defaultWsPath
246
+ let cwdNotice = ''
193
247
  // 校验指定目录存在且是目录,防止路径遍历
194
248
  if (cwdOverride) {
195
249
  const validation = await validateWorkspacePath(this, cwdOverride)
@@ -197,11 +251,17 @@ export class ConversationBridge {
197
251
  await this.sendText(validation.error)
198
252
  return
199
253
  }
254
+ } else if (this.config.cwd && !(await isUsableDirectory(this.config.cwd))) {
255
+ // 配置里恢复出来的工作区不可用(换机器、目录被删、写成相对路径等):
256
+ // 回落首个已注册工作区并明确告知——DSH 只拒绝非绝对路径,不会替我们建目录,
257
+ // 静默沿用会让会话建在一个不存在的 cwd 上
258
+ this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] configured cwd "${this.config.cwd}" is not a usable directory; falling back to ${defaultWsPath}`)
259
+ cwdNotice = `\n- ⚠️ 配置的工作区 \`${this.config.cwd}\` 不可用,本次会话改用 \`${defaultWsPath}\``
260
+ cwd = defaultWsPath
200
261
  }
201
- const meta = {
202
- cwd,
203
- agentPreset: this.config.agentPreset || 'routing-suite',
204
- }
262
+ const composition = await this._composeAgentPreset(this.config.agentPreset)
263
+ const meta = { cwd }
264
+ if (composition.agentPreset) meta.agentPreset = composition.agentPreset
205
265
  const agentOptions = {}
206
266
  if (this.config.agentProvider) agentOptions.provider = this.config.agentProvider
207
267
  if (this.config.agentModel) agentOptions.model = this.config.agentModel
@@ -215,7 +275,10 @@ export class ConversationBridge {
215
275
  } catch { /* 默认模型服务不可用则忽略,交由 DSH 自行处理 */ }
216
276
  }
217
277
  // 注意:不预创建 ctx.sessions——agents.create 会自己 prepare+enter session
218
- const handle = await this.ctx.agents.create({ sessionId, meta, agentOptions })
278
+ // 旧版 DSH 没有 setup 概念,仅在确有 preset 组装时才带上该键
279
+ const createOptions = { sessionId, meta, agentOptions }
280
+ if (composition.setup) createOptions.setup = composition.setup
281
+ const handle = await this.ctx.agents.create(createOptions)
219
282
  this.setActiveSession(handle.agent.session)
220
283
 
221
284
  // 同步挂载进 DSH 工作区账本 (workspaceRegistry),使新会话在 Web 侧边栏和 /list 中精准归组
@@ -235,9 +298,13 @@ export class ConversationBridge {
235
298
  source: { kind: 'user' },
236
299
  }))
237
300
  }
238
- const wsDetail = cwd ? `\n- **工作区**:\`${cwd}\`` : ''
301
+ const wsDetail = cwd ? `\n- **工作区**:\`${cwd}\`${cwdNotice}` : ''
302
+ // 配置的 preset 在本机不存在时,回退必须让用户看得见(否则又会变成"配了不生效")
303
+ const presetNotice = composition.fallbackFrom
304
+ ? `\n- ⚠️ 配置的 Agent 预设 \`${composition.fallbackFrom}\` 在本机不存在,本次会话使用默认预设 \`${composition.agentPreset}\``
305
+ : ''
239
306
  const hint = prompt ? '' : '\n\n> 💡 发送任意消息即可直接与 Agent 对话。'
240
- await this.sendText(`✓ **已创建新会话**\n- **会话 ID**:\`${fmtSessionId(handle.agent.session.id)}\`${wsDetail}${hint}`)
307
+ await this.sendText(`✓ **已创建新会话**\n- **会话 ID**:\`${fmtSessionId(handle.agent.session.id)}\`${wsDetail}${presetNotice}${hint}`)
241
308
  } catch (error) {
242
309
  await this.sendText(`❌ **创建会话失败**:${error instanceof Error ? error.message : String(error)}`)
243
310
  }
@@ -444,23 +511,39 @@ export class ConversationBridge {
444
511
 
445
512
  // 判断该 session 是否已持久化:已持久化 → agents.resume(从持久化加载历史恢复,
446
513
  // 避免 agents.create 用空 seed 与已持久化事件冲突);未持久化 → agents.create。
514
+ // 注意:list() 在新版 DSH 返回 `{ header, revision, sizeBytes }` 快照,必须经
515
+ // sessionHeaderOf 取 header;旧写法直接读 entry.id 会恒为 undefined,把已存在
516
+ // 的会话误判成"未持久化",随后 create 抛 `session "..." already exists`(issue #39)。
447
517
  let persisted = false
518
+ let storedPreset
448
519
  try {
449
- const headers = await this.ctx.sessionPersistence?.list?.()
450
- persisted = Array.isArray(headers) && headers.some((h) => h?.id === sessionId)
520
+ const entries = await this.ctx.sessionPersistence?.list?.()
521
+ const matched = Array.isArray(entries)
522
+ ? sessionHeaderOf(entries.find((entry) => sessionHeaderOf(entry)?.id === sessionId))
523
+ : undefined
524
+ persisted = Boolean(matched)
525
+ storedPreset = matched?.agentPreset
451
526
  } catch { /* 读取失败则按未持久化处理 */ }
452
527
 
528
+ // 恢复会话按其自身记录的 preset 组装(与 DSH 会话控制器一致);未记录则用桥配置,
529
+ // 都为空时交给 DSH 默认 preset。记录值失效时 _composeAgentPreset 会回退默认并告警。
530
+ const composition = await this._composeAgentPreset(persisted ? (storedPreset ?? this.config.agentPreset) : this.config.agentPreset)
531
+
453
532
  let handle
454
533
  if (persisted) {
455
534
  handle = await this.ctx.agents.resume({
456
535
  resumeSessionId: sessionId,
457
536
  agentOptions,
537
+ setup: composition.setup,
458
538
  })
459
539
  } else {
540
+ const meta = { cwd: this.config.cwd || process.cwd() }
541
+ if (composition.agentPreset) meta.agentPreset = composition.agentPreset
460
542
  handle = await this.ctx.agents.create({
461
543
  sessionId,
462
- meta: { cwd: this.config.cwd || process.cwd() },
544
+ meta,
463
545
  agentOptions,
546
+ setup: composition.setup,
464
547
  })
465
548
  }
466
549
  const resumedAgent = handle?.agent
@@ -818,4 +901,5 @@ export const conversationBridgeHelpers = {
818
901
  listSessions,
819
902
  renderSessions,
820
903
  listWorkspaces,
904
+ sessionHeaderOf,
821
905
  }
@@ -5,6 +5,26 @@ import { normalize, resolve } from 'node:path'
5
5
  import { isSafeWorkspacePath } from '../security/path-validator.js'
6
6
  import { getArchivedSessionIds, getSessionProjCache, getRegisteredWorkspaces } from './dsh-storage.js'
7
7
 
8
+ /**
9
+ * 取出 `sessionPersistence.list()` 条目里的会话头(SessionHeader)。
10
+ *
11
+ * DSH 0.1.5 起该方法返回的是**快照** `{ header, revision, sizeBytes }`
12
+ * (见 `dsh-session-persistence-jsonl` 的 `list()`,以及 `dsh-workspace` 的
13
+ * `(await ctx.sessionPersistence.list()).map((snapshot) => snapshot.header)`);
14
+ * 更早的版本直接返回 header 数组。两种形状都要兼容:只按 `entry.id` 取值的旧写法
15
+ * 在新版上恒为 undefined,会把"已持久化的会话"误判成"未持久化",进而对已存在的
16
+ * 会话调用 `agents.create`,抛出 `session "..." already exists`(issue #39)。
17
+ *
18
+ * @param {object} entry list() 返回的单个条目
19
+ * @returns {object|undefined} 会话头,形状不可识别时返回 undefined
20
+ */
21
+ export function sessionHeaderOf(entry) {
22
+ if (!entry || typeof entry !== 'object') return undefined
23
+ const header = entry.header
24
+ if (header && typeof header === 'object' && typeof header.id === 'string') return header
25
+ return typeof entry.id === 'string' ? entry : undefined
26
+ }
27
+
8
28
  export function sessionLabel(session) {
9
29
  for (const event of session.events ?? []) {
10
30
  if (event.type === 'user/message') {
@@ -198,9 +218,11 @@ export async function listSessions(node) {
198
218
  }
199
219
  if (node.ctx.sessionPersistence?.list) {
200
220
  try {
201
- const headers = await node.ctx.sessionPersistence.list()
202
- if (Array.isArray(headers)) {
203
- const coldHeaders = headers.filter((h) => h && h.id && !accounted.has(h.id) && !archived.has(h.id) && !h.archived)
221
+ const entries = await node.ctx.sessionPersistence.list()
222
+ if (Array.isArray(entries)) {
223
+ // 兼容 DSH 0.1.5 的 { header, revision, sizeBytes } 快照与更早的扁平 header 数组
224
+ const coldHeaders = entries.map(sessionHeaderOf)
225
+ .filter((h) => h && !accounted.has(h.id) && !archived.has(h.id) && !h.archived)
204
226
  for (const h of coldHeaders) {
205
227
  accounted.add(h.id)
206
228
  let title
@@ -0,0 +1,52 @@
1
+ // 会话级配置(工作区 / Agent 预设 / 模型路由)的读写归一
2
+ //
3
+ // 这四个字段决定远程会话"建在哪里、挂什么预设、用哪个模型":
4
+ // - cwd 会话工作区
5
+ // - agentPreset DSH agent preset(决定该会话挂载的工具/提示词/技能目录)
6
+ // - agentProvider 模型提供方
7
+ // - agentModel 模型
8
+ //
9
+ // 它们同时出现在三处:cordis 配置 → 设置页写入(config.json)→ 启动恢复。
10
+ // 三处必须共用同一套字段名与空值语义,否则又会出现"配了不生效 / 重启就丢"。
11
+ // 空串表示"未设置":会话创建时回落到 DSH 默认值。
12
+
13
+ /** 会话级配置的字段名(顺序即 UI 展示顺序)。 */
14
+ export const SESSION_CONFIG_FIELDS = ['agentPreset', 'cwd', 'agentProvider', 'agentModel']
15
+
16
+ /**
17
+ * 读取会话级配置(供 UI 回显 / 持久化 patch 使用)。
18
+ * @param {object} config 会话桥的 config
19
+ * @returns {{agentPreset: string, cwd: string, agentProvider: string, agentModel: string}}
20
+ */
21
+ export function readSessionConfig(config) {
22
+ const out = {}
23
+ for (const field of SESSION_CONFIG_FIELDS) {
24
+ out[field] = typeof config?.[field] === 'string' ? config[field] : ''
25
+ }
26
+ return out
27
+ }
28
+
29
+ /**
30
+ * 应用一组会话级配置(就地修改 config)。
31
+ *
32
+ * 只接受字符串入参:未出现在 patch 里的字段保持原值,非字符串(undefined/null/数字)
33
+ * 一律忽略;字符串按 trim 后写入,空串表示显式清除该设置、回落 DSH 默认值。
34
+ *
35
+ * @param {object} config 会话桥的 config
36
+ * @param {object} patch 来自设置页的 patch
37
+ * @returns {string[]} 实际被改写的字段名
38
+ */
39
+ export function applySessionConfig(config, patch) {
40
+ if (!config || !patch) return []
41
+ const changed = []
42
+ for (const field of SESSION_CONFIG_FIELDS) {
43
+ if (!Object.hasOwn(patch, field)) continue
44
+ const raw = patch[field]
45
+ if (typeof raw !== 'string') continue
46
+ const next = raw.trim()
47
+ if (config[field] === next) continue
48
+ config[field] = next
49
+ changed.push(field)
50
+ }
51
+ return changed
52
+ }
package/lib/qq/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // 通过回调 `onPersist` 保存在 $DSH_HOME/dsh-bridge/config.json。
10
10
 
11
11
  import { Platform } from '../platform/base.js'
12
+ import { applySessionConfig, readSessionConfig } from '../platform/session-config.js'
12
13
  import { QqGateway } from './gateway.js'
13
14
  import { QqConversationNode } from './node.js'
14
15
 
@@ -60,6 +61,11 @@ export class QqService extends Platform {
60
61
  maxMessageChars: config.maxMessageChars,
61
62
  sendChunkDelayMs: config.sendChunkDelayMs,
62
63
  activeSessionId: config.activeSessionId,
64
+ // 会话级配置:工作区 / Agent 预设 / 模型路由(构造期来自 cordis 配置,运行期由设置页写入)
65
+ cwd: config.cwd,
66
+ agentPreset: config.agentPreset,
67
+ agentProvider: config.agentProvider,
68
+ agentModel: config.agentModel,
63
69
  }, logger, {
64
70
  onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
65
71
  onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
@@ -131,6 +137,7 @@ export class QqService extends Platform {
131
137
  // 密钥不回传到浏览器;设置页留空时保持已保存密钥
132
138
  clientSecret: '',
133
139
  accountId: this.gateway.accountId,
140
+ ...readSessionConfig(this.node.config),
134
141
  },
135
142
  }
136
143
  }
@@ -144,6 +151,7 @@ export class QqService extends Platform {
144
151
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
145
152
  appId: this.gateway.config.appId,
146
153
  clientSecret: '',
154
+ ...readSessionConfig(this.node.config),
147
155
  }
148
156
  }
149
157
 
@@ -266,7 +274,7 @@ export class QqService extends Platform {
266
274
  }
267
275
 
268
276
  /** 更新运行时配置并持久化。 */
269
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, appId, clientSecret } = {}) {
277
+ async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, appId, clientSecret, agentPreset, cwd, agentProvider, agentModel } = {}) {
270
278
  if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
271
279
  if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
272
280
  if (maxMessageChars != null) {
@@ -275,6 +283,7 @@ export class QqService extends Platform {
275
283
  }
276
284
  if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
277
285
  if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
286
+ applySessionConfig(this.node.config, { agentPreset, cwd, agentProvider, agentModel })
278
287
  if (appId !== undefined || clientSecret !== undefined) {
279
288
  this.gateway.setCredentials({
280
289
  appId: appId !== undefined ? appId : this.gateway.config.appId,
@@ -288,6 +297,7 @@ export class QqService extends Platform {
288
297
  maxMessageChars: this.node.config.maxMessageChars,
289
298
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
290
299
  groupAutoApprove: this.node.config.groupAutoApprove === true,
300
+ ...readSessionConfig(this.node.config),
291
301
  }
292
302
  if (appId !== undefined || clientSecret !== undefined) {
293
303
  patch.appId = this.gateway.config.appId
@@ -4,6 +4,7 @@
4
4
 
5
5
  import QRCode from 'qrcode'
6
6
  import { Platform } from '../platform/base.js'
7
+ import { applySessionConfig, readSessionConfig } from '../platform/session-config.js'
7
8
  import { TelegramGateway } from './gateway.js'
8
9
  import { TelegramConversationNode } from './node.js'
9
10
 
@@ -37,6 +38,11 @@ export class TelegramService extends Platform {
37
38
  maxMessageChars: config.maxMessageChars || 4096,
38
39
  sendChunkDelayMs: config.sendChunkDelayMs,
39
40
  activeSessionId: config.activeSessionId,
41
+ // 会话级配置:工作区 / Agent 预设 / 模型路由(构造期来自 cordis 配置,运行期由设置页写入)
42
+ cwd: config.cwd,
43
+ agentPreset: config.agentPreset,
44
+ agentProvider: config.agentProvider,
45
+ agentModel: config.agentModel,
40
46
  }, logger, {
41
47
  onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
42
48
  onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
@@ -159,6 +165,7 @@ export class TelegramService extends Platform {
159
165
  sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
160
166
  botToken: this.gateway.config.botToken ? '******' : '',
161
167
  proxy: this.gateway.config.proxy || '',
168
+ ...readSessionConfig(this.node?.config),
162
169
  },
163
170
  botInfo: this.gateway.botInfo,
164
171
  botLink,
@@ -173,7 +180,7 @@ export class TelegramService extends Platform {
173
180
  return { success: true, allowFrom: list }
174
181
  }
175
182
 
176
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, botToken, proxy } = {}) {
183
+ async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, botToken, proxy, agentPreset, cwd, agentProvider, agentModel } = {}) {
177
184
  if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
178
185
  if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
179
186
  if (maxMessageChars != null) {
@@ -182,6 +189,7 @@ export class TelegramService extends Platform {
182
189
  }
183
190
  if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
184
191
  if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
192
+ applySessionConfig(this.node.config, { agentPreset, cwd, agentProvider, agentModel })
185
193
 
186
194
  if (botToken !== undefined || proxy !== undefined) {
187
195
  const patch = {}
@@ -196,6 +204,7 @@ export class TelegramService extends Platform {
196
204
  maxMessageChars: this.node.config.maxMessageChars,
197
205
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
198
206
  groupAutoApprove: this.node.config.groupAutoApprove === true,
207
+ ...readSessionConfig(this.node.config),
199
208
  }
200
209
  if (botToken !== undefined) patch.botToken = this.gateway.config.botToken
201
210
  if (proxy !== undefined) patch.proxy = this.gateway.config.proxy
@@ -10,6 +10,7 @@
10
10
  // `onPersist` 保存在 $DSH_HOME/dsh-bridge/config.json。本服务不直接碰文件系统。
11
11
 
12
12
  import { Platform } from '../platform/base.js'
13
+ import { applySessionConfig, readSessionConfig } from '../platform/session-config.js'
13
14
  import { WechatGateway, gatewayConstants } from './gateway.js'
14
15
  import { WechatConversationNode } from './node.js'
15
16
 
@@ -44,6 +45,11 @@ export class WechatService extends Platform {
44
45
  maxMessageChars: config.maxMessageChars,
45
46
  sendChunkDelayMs: config.sendChunkDelayMs,
46
47
  activeSessionId: config.activeSessionId, // v0.2.1:恢复活动会话
48
+ // 会话级配置:工作区 / Agent 预设 / 模型路由(构造期来自 cordis 配置,运行期由设置页写入)
49
+ cwd: config.cwd,
50
+ agentPreset: config.agentPreset,
51
+ agentProvider: config.agentProvider,
52
+ agentModel: config.agentModel,
47
53
  }, logger, {
48
54
  onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
49
55
  onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }), // v0.2.1:持久化活动会话
@@ -111,6 +117,7 @@ export class WechatService extends Platform {
111
117
  approvalTimeoutSec: this.node.config.approvalTimeoutSec,
112
118
  maxMessageChars: this.node.config.maxMessageChars,
113
119
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
120
+ ...readSessionConfig(this.node.config),
114
121
  },
115
122
  }
116
123
  }
@@ -123,6 +130,7 @@ export class WechatService extends Platform {
123
130
  maxMessageChars: this.node.config.maxMessageChars,
124
131
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
125
132
  groupAutoApprove: this.node.config.groupAutoApprove === true,
133
+ ...readSessionConfig(this.node.config),
126
134
  }
127
135
  }
128
136
 
@@ -224,8 +232,8 @@ export class WechatService extends Platform {
224
232
  await this.persist({ allowFrom: clean })
225
233
  }
226
234
 
227
- /** 更新运行时配置(心跳间隔/审批超时/每气泡字数/分块延迟)并持久化。 */
228
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove } = {}) {
235
+ /** 更新运行时配置(心跳间隔/审批超时/每气泡字数/分块延迟/会话级配置)并持久化。 */
236
+ async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, agentPreset, cwd, agentProvider, agentModel } = {}) {
229
237
  if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
230
238
  if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
231
239
  if (maxMessageChars != null) {
@@ -233,12 +241,14 @@ export class WechatService extends Platform {
233
241
  this.node.config.maxMessageChars = (val >= 200) ? val : gatewayConstants.MAX_MESSAGE_CHARS
234
242
  }
235
243
  if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
244
+ applySessionConfig(this.node.config, { agentPreset, cwd, agentProvider, agentModel })
236
245
  await this.persist({
237
246
  digestIntervalSec: this.node.config.digestIntervalSec,
238
247
  approvalTimeoutSec: this.node.config.approvalTimeoutSec,
239
248
  maxMessageChars: this.node.config.maxMessageChars,
240
249
  sendChunkDelayMs: this.node.config.sendChunkDelayMs,
241
250
  groupAutoApprove: this.node.config.groupAutoApprove === true,
251
+ ...readSessionConfig(this.node.config),
242
252
  })
243
253
  }
244
254
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.10.8",
3
+ "version": "2.10.10",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
- "releaseNotes": "【v2.10.8】\n• 新增:登录 Session 持久化(#36)——宿主 dsh web 重启后已登录设备免重输访问密码,手机远程访问不再每天重登;改密码/切换模式仍同步吊销全部旧会话(安全语义保持)\n• 新增:设置页检测 DSH 宿主更新并支持一键升级(npm 全局安装形态)——发现新版黄色提示 + 一键升级按钮;非 npm 安装(源码/打包)仅提醒并给出官方升级方式\n• 修复:iOS Safari 键盘弹起时聊天输入框上下跳动——键盘弹起时固定输入框滚动容器高度,避免随键盘反复重排抖动\n• 新增:移动端输入框可折叠——聊天头部工具栏新增折叠按钮,收起输入框后消息阅读区最大化;底部保留「点击输入消息」细条一键唤回;折叠偏好跨会话记忆",
5
+ "releaseNotes": "【v2.10.10】\n• 新功能:设置页每个 IM 平台卡片内新增「高级设置」——可直接配置该平台远程会话的 Agent 预设(下拉列出本机可用预设,留空用 DSH 默认)、工作区目录、模型提供方/模型,以及摘要间隔、审批超时、单条消息上限、分块发送间隔;保存即生效并随宿主重启保留(此前只能手改 config.json)\n• 修复:DSH 0.1.5 系列上远程 IM 会话(微信/QQ/飞书/Telegram)工具集残缺(#40)——DSH 把工具/提示词/技能挪到 agent preset 后,桥只记录了预设名却从未真正挂载,会话落在「空预设层」因而缺工具(如无法读文件);现在新建与恢复会话都会真实挂载预设(未配置时用 DSH 默认),配置的预设若在本机不存在会回退默认并在会话里明确提示,不再静默降级\n• 修复:宿主重启后给已有会话发消息报「session ... already exists」、随后卡在「当前没有活动会话」(#39)——恢复会话时误判了 DSH 的持久化返回结构,把已存在的历史会话当成新会话创建;现按真实结构识别并恢复\n• 修复:远程会话的 cwd、agentPreset、agentProvider、agentModel 以及 QQ/飞书的摘要间隔、审批超时、分块延时保存后重启即丢(#40)——重启恢复与配置导入时会一并读回;配置的工作区目录不可用时回落到已注册工作区并提示\n• 修复:`/rename` 改名后重启即失效——此前调用的持久化接口在 DSH 中并不存在、被静默跳过;现改用 DSH 原生会话标题服务,改名真正落盘",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "exports": {