@xmanrui/dsh-im 4.10.0 → 4.12.0

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.
Files changed (62) hide show
  1. package/PROACTIVE_DELIVERY.en.md +15 -1
  2. package/PROACTIVE_DELIVERY.md +15 -1
  3. package/README.en.md +7 -2
  4. package/README.md +8 -3
  5. package/lib/client.js +2341 -1315
  6. package/lib/index.js +250 -245
  7. package/package.json +4 -3
  8. package/plugin-src/client/channel-card-meta.js +9 -1
  9. package/plugin-src/client/channels/dingtalk/api.js +4 -0
  10. package/plugin-src/client/channels/dingtalk/index.js +41 -7
  11. package/plugin-src/client/channels/feishu/api.js +4 -0
  12. package/plugin-src/client/channels/feishu/group-settings.js +581 -0
  13. package/plugin-src/client/channels/feishu/index.js +64 -233
  14. package/plugin-src/client/channels/qq/api.js +4 -0
  15. package/plugin-src/client/channels/qq/index.js +41 -7
  16. package/plugin-src/client/channels/shared/collapsible-account.js +80 -0
  17. package/plugin-src/client/channels/shared/token-api.js +4 -0
  18. package/plugin-src/client/channels/shared/token-channel.js +62 -29
  19. package/plugin-src/client/channels/wecom/api.js +4 -0
  20. package/plugin-src/client/channels/wecom/index.js +41 -7
  21. package/plugin-src/client/channels/weixin/api.js +4 -0
  22. package/plugin-src/client/channels/weixin/index.js +41 -7
  23. package/plugin-src/client/channels/whatsapp/api.js +4 -0
  24. package/plugin-src/client/channels/whatsapp/index.js +40 -7
  25. package/plugin-src/client/delivery-settings.js +73 -5
  26. package/plugin-src/client/i18n.js +22 -0
  27. package/plugin-src/client/model-setting.js +104 -0
  28. package/plugin-src/client/styles.js +62 -0
  29. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  30. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  31. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  32. package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
  33. package/plugin-src/host/channels/qq/production.mjs +3 -0
  34. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  35. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
  36. package/plugin-src/host/channels/shared/production.mjs +3 -0
  37. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  38. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  39. package/plugin-src/host/channels/slack/production.mjs +3 -0
  40. package/plugin-src/host/channels/slack/rpc.mjs +9 -0
  41. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  42. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  43. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  44. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  45. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  46. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  47. package/plugin-src/host/delivery-adapter.mjs +141 -3
  48. package/plugin-src/host/delivery-rpc.mjs +12 -0
  49. package/plugin-src/host/delivery-service.mjs +110 -3
  50. package/plugin-src/host/delivery-suggestions.mjs +46 -0
  51. package/plugin-src/host/harness-session-coordinator.mjs +6 -5
  52. package/plugin-src/host/index.mjs +18 -1
  53. package/plugin-src/host/session-sync-coordinator.mjs +217 -0
  54. package/scripts/verify-package.mjs +12 -6
  55. package/src/channels/shared/bot-workspace-store.mjs +285 -38
  56. package/src/channels/shared/harness-client.mjs +124 -43
  57. package/src/channels/shared/i18n-en/wecom.mjs +24 -0
  58. package/src/channels/shared/model-setting.mjs +110 -0
  59. package/src/channels/wecom/wecom-bridge.mjs +275 -2
  60. package/src/channels/wecom/wecom-cards.mjs +101 -0
  61. package/src/channels/wecom/wecom-runtime.mjs +2 -0
  62. package/src/channels/whatsapp/whatsapp-runtime.mjs +78 -26
@@ -5,10 +5,15 @@ import {
5
5
  } from '../../../../src/channels/shared/agent-preset.mjs';
6
6
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
7
7
  import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
8
+ import {
9
+ normalizeModelCatalog,
10
+ normalizeModelSelection,
11
+ } from '../../../../src/channels/shared/model-setting.mjs';
8
12
  import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
9
13
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
10
14
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
11
15
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
16
+ import { validModelPayload } from '../shared/model-setting-rpc.mjs';
12
17
  import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
13
18
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
14
19
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
@@ -283,6 +288,7 @@ function publicBotEntry(entry) {
283
288
  state: connectionState(source, registration, connected),
284
289
  connected,
285
290
  configured: source.configured === true,
291
+ model: normalizeModelSelection(source.model),
286
292
  agentPreset: normalizeAgentPresetId(source.agentPreset),
287
293
  contextEnhancement: normalizeContextEnhancementConfig(source.contextEnhancement),
288
294
  accessPolicy: normalizeAccessPolicy(source.accessPolicy),
@@ -333,6 +339,7 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
333
339
  health: publicHealth(source, connected),
334
340
  bots,
335
341
  agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
342
+ modelCatalog: normalizeModelCatalog(source.modelCatalog),
336
343
  totals: {
337
344
  configured: bots.length || (source.configured === true ? 1 : 0),
338
345
  connected: bots.length ? bots.filter((bot) => bot.connected).length : (connected ? 1 : 0),
@@ -422,6 +429,9 @@ function validPayload(endpoint, payload) {
422
429
  return validWorkspacePayload(payload)
423
430
  ? null : '请输入工作区绝对路径。';
424
431
  }
432
+ if (endpoint === FEISHU_ENDPOINTS.setModel) {
433
+ return validModelPayload(payload) ? null : '请选择有效模型。';
434
+ }
425
435
  if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
426
436
  return validAgentPresetPayload(payload)
427
437
  ? null : '请选择 Agent Preset。';
@@ -692,6 +702,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
692
702
  await controller.updateWorkspace(payload.botId, payload.workspace),
693
703
  { encodeQr: cachedEncodeQr },
694
704
  );
705
+ } else if (endpoint === FEISHU_ENDPOINTS.setModel) {
706
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
707
+ value = await toPublicFeishuStatus(
708
+ await controller.updateModel(payload.botId, payload.model),
709
+ { encodeQr: cachedEncodeQr },
710
+ );
695
711
  } else if (endpoint === FEISHU_ENDPOINTS.setContextEnhancement) {
696
712
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
697
713
  value = await controller.updateContextEnhancement(
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
18
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
18
19
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
19
20
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
21
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
@@ -102,6 +103,7 @@ export async function createProductionController(ctx, config = {}, internals = {
102
103
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
103
104
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
104
105
  });
106
+ const modelCatalog = () => listModelCatalog(harness);
105
107
  const coreController = new Controller({
106
108
  qrAuth,
107
109
  credentials: ctx.credentials,
@@ -153,6 +155,7 @@ export async function createProductionController(ctx, config = {}, internals = {
153
155
  workspaces,
154
156
  stateFor,
155
157
  agentPresetCatalog,
158
+ modelCatalog,
156
159
  });
157
160
  const supervisor = createSupervisor({
158
161
  controller,
@@ -8,6 +8,7 @@ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/
8
8
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
9
9
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
10
10
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
11
+ import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
11
12
 
12
13
  export const QQ_RPC_CHANNEL = '/qq';
13
14
  export const QQ_ENDPOINTS = Object.freeze({
@@ -19,6 +20,7 @@ export const QQ_ENDPOINTS = Object.freeze({
19
20
  reconnectBot: 'bot.reconnect',
20
21
  deleteBot: 'bot.delete',
21
22
  setWorkspace: SET_WORKSPACE_ENDPOINT,
23
+ setModel: SET_MODEL_ENDPOINT,
22
24
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
23
25
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
26
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
@@ -76,6 +78,9 @@ function payloadFailure(endpoint, payload) {
76
78
  return validWorkspacePayload(payload)
77
79
  ? null : '请输入工作区绝对路径。';
78
80
  }
81
+ if (endpoint === QQ_ENDPOINTS.setModel) {
82
+ return validModelPayload(payload) ? null : '请选择有效模型。';
83
+ }
79
84
  if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
80
85
  return validAgentPresetPayload(payload)
81
86
  ? null : '请选择 Agent Preset。';
@@ -183,6 +188,12 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
183
188
  await controller.updateWorkspace(payload.botId, payload.workspace),
184
189
  cachedEncode,
185
190
  );
191
+ } else if (endpoint === QQ_ENDPOINTS.setModel) {
192
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
193
+ value = await publicStatus(
194
+ await controller.updateModel(payload.botId, payload.model),
195
+ cachedEncode,
196
+ );
186
197
  } else if (endpoint === QQ_ENDPOINTS.setContextEnhancement) {
187
198
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
188
199
  value = await controller.updateContextEnhancement(
@@ -0,0 +1,19 @@
1
+ import { normalizeModelSelection } from '../../../../src/channels/shared/model-setting.mjs';
2
+
3
+ export const SET_MODEL_ENDPOINT = 'bot.model.set';
4
+
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+
9
+ export function validModelPayload(payload) {
10
+ if (!isRecord(payload)
11
+ || !Object.keys(payload).every((key) => ['botId', 'model'].includes(key))
12
+ || typeof payload.botId !== 'string'
13
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
14
+ if (payload.model === null) return true;
15
+ return isRecord(payload.model)
16
+ && Object.keys(payload.model).length === 2
17
+ && Object.keys(payload.model).every((key) => ['provider', 'model'].includes(key))
18
+ && normalizeModelSelection(payload.model) !== null;
19
+ }
@@ -17,6 +17,7 @@ import {
17
17
  observeBotWorkspaceRemovals,
18
18
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
19
19
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
20
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
20
21
  import {
21
22
  createDeliveryAdapter,
22
23
  supportsDeliveryChannel,
@@ -111,6 +112,7 @@ export async function createTokenProductionController(ctx, config, internals, de
111
112
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
112
113
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
113
114
  });
115
+ const modelCatalog = () => listModelCatalog(harness);
114
116
  const coreController = new ResolvedController({
115
117
  credentials: ctx.credentials,
116
118
  configStore: observedConfigStore,
@@ -161,6 +163,7 @@ export async function createTokenProductionController(ctx, config, internals, de
161
163
  workspaces,
162
164
  stateFor,
163
165
  agentPresetCatalog,
166
+ modelCatalog,
164
167
  });
165
168
  const supervisor = createSupervisor({
166
169
  channel,
@@ -11,6 +11,7 @@ import {
11
11
  SET_AGENT_PRESET_ENDPOINT,
12
12
  validAgentPresetPayload,
13
13
  } from './agent-preset-rpc.mjs';
14
+ import { SET_MODEL_ENDPOINT, validModelPayload } from './model-setting-rpc.mjs';
14
15
 
15
16
  export const TOKEN_BOT_ENDPOINTS = Object.freeze({
16
17
  status: 'connection.status',
@@ -18,6 +19,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
18
19
  reconnectBot: 'bot.reconnect',
19
20
  deleteBot: 'bot.delete',
20
21
  setWorkspace: SET_WORKSPACE_ENDPOINT,
22
+ setModel: SET_MODEL_ENDPOINT,
21
23
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
24
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
23
25
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
@@ -71,6 +73,10 @@ function payloadFailure(endpoint, payload) {
71
73
  return validWorkspacePayload(payload)
72
74
  ? null : '请输入工作区绝对路径。';
73
75
  }
76
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setModel) {
77
+ return validModelPayload(payload)
78
+ ? null : '请选择有效模型。';
79
+ }
74
80
  if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
75
81
  return validAgentPresetPayload(payload)
76
82
  ? null : '请选择 Agent Preset。';
@@ -164,6 +170,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
164
170
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
165
171
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
166
172
  value = await controller.updateWorkspace(payload.botId, payload.workspace);
173
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setModel) {
174
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
175
+ value = await controller.updateModel(payload.botId, payload.model);
167
176
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
168
177
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
169
178
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
@@ -22,6 +22,8 @@ export function publicWorkspaceError(error) {
22
22
  'workspace-bot-not-found',
23
23
  'agent-preset-invalid',
24
24
  'agent-preset-unavailable',
25
+ 'model-selection-invalid',
26
+ 'model-selection-unavailable',
25
27
  'context-enhancement-invalid',
26
28
  ].includes(error?.code)) return null;
27
29
  return { code: error.code, message: error.message };
@@ -13,6 +13,7 @@ import {
13
13
  observeBotWorkspaceRemovals,
14
14
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
15
15
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
16
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
16
17
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
17
18
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
18
19
  import { pluginPaths } from '../shared/production.mjs';
@@ -90,6 +91,7 @@ export async function createProductionController(ctx, config = {}, internals = {
90
91
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
91
92
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
92
93
  });
94
+ const modelCatalog = () => listModelCatalog(harness);
93
95
  const coreController = new ResolvedController({
94
96
  credentials: ctx.credentials,
95
97
  configStore: observedConfigStore,
@@ -142,6 +144,7 @@ export async function createProductionController(ctx, config = {}, internals = {
142
144
  workspaces,
143
145
  stateFor,
144
146
  agentPresetCatalog,
147
+ modelCatalog,
145
148
  });
146
149
  const supervisor = createSupervisor({
147
150
  channel: 'slack',
@@ -11,6 +11,7 @@ import {
11
11
  SET_AGENT_PRESET_ENDPOINT,
12
12
  validAgentPresetPayload,
13
13
  } from '../shared/agent-preset-rpc.mjs';
14
+ import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
14
15
 
15
16
  export const SLACK_RPC_CHANNEL = '/slack';
16
17
  export const SLACK_ENDPOINTS = Object.freeze({
@@ -19,6 +20,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
19
20
  reconnectBot: 'bot.reconnect',
20
21
  deleteBot: 'bot.delete',
21
22
  setWorkspace: SET_WORKSPACE_ENDPOINT,
23
+ setModel: SET_MODEL_ENDPOINT,
22
24
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
23
25
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
26
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
@@ -75,6 +77,9 @@ function payloadFailure(endpoint, payload) {
75
77
  return validWorkspacePayload(payload)
76
78
  ? null : '请输入工作区绝对路径。';
77
79
  }
80
+ if (endpoint === SLACK_ENDPOINTS.setModel) {
81
+ return validModelPayload(payload) ? null : '请选择有效模型。';
82
+ }
78
83
  if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
79
84
  return validAgentPresetPayload(payload)
80
85
  ? null : '请选择 Agent Preset。';
@@ -166,6 +171,10 @@ export function createSlackRpcHandler(controller) {
166
171
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
167
172
  value = await controller.updateWorkspace(payload.botId, payload.workspace);
168
173
  }
174
+ else if (endpoint === SLACK_ENDPOINTS.setModel) {
175
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
176
+ value = await controller.updateModel(payload.botId, payload.model);
177
+ }
169
178
  else if (endpoint === SLACK_ENDPOINTS.setContextEnhancement) {
170
179
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
171
180
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
18
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
18
19
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
19
20
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
21
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
@@ -105,6 +106,7 @@ export async function createProductionController(ctx, config = {}, internals = {
105
106
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
106
107
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
107
108
  });
109
+ const modelCatalog = () => listModelCatalog(harness);
108
110
  const coreController = new Controller({
109
111
  qrAuth,
110
112
  credentials: ctx.credentials,
@@ -157,6 +159,7 @@ export async function createProductionController(ctx, config = {}, internals = {
157
159
  workspaces,
158
160
  stateFor,
159
161
  agentPresetCatalog,
162
+ modelCatalog,
160
163
  });
161
164
  const supervisor = createSupervisor({
162
165
  controller,
@@ -4,6 +4,7 @@ import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/
4
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
5
5
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
6
6
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
7
+ import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
7
8
  import {
8
9
  connectionTestTargetUnavailable,
9
10
  publicConnectionTestResult,
@@ -19,6 +20,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
19
20
  reconnectBot: 'bot.reconnect',
20
21
  deleteBot: 'bot.delete',
21
22
  setWorkspace: SET_WORKSPACE_ENDPOINT,
23
+ setModel: SET_MODEL_ENDPOINT,
22
24
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
23
25
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
24
26
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
@@ -76,6 +78,9 @@ function payloadFailure(endpoint, payload) {
76
78
  return validWorkspacePayload(payload)
77
79
  ? null : '请输入工作区绝对路径。';
78
80
  }
81
+ if (endpoint === WECOM_ENDPOINTS.setModel) {
82
+ return validModelPayload(payload) ? null : '请选择有效模型。';
83
+ }
79
84
  if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
80
85
  return validAgentPresetPayload(payload)
81
86
  ? null : '请选择 Agent Preset。';
@@ -184,6 +189,12 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
184
189
  await controller.updateWorkspace(payload.botId, payload.workspace),
185
190
  cachedEncode,
186
191
  );
192
+ } else if (endpoint === WECOM_ENDPOINTS.setModel) {
193
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
194
+ value = await publicStatus(
195
+ await controller.updateModel(payload.botId, payload.model),
196
+ cachedEncode,
197
+ );
187
198
  } else if (endpoint === WECOM_ENDPOINTS.setContextEnhancement) {
188
199
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
189
200
  value = await controller.updateContextEnhancement(
@@ -18,6 +18,7 @@ import {
18
18
  observeBotWorkspaceRemovals,
19
19
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
20
20
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
21
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
21
22
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
22
23
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
23
24
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
@@ -108,6 +109,7 @@ export async function createProductionController(ctx, config = {}, internals = {
108
109
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
109
110
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
110
111
  });
112
+ const modelCatalog = () => listModelCatalog(harness);
111
113
  const coreController = new Controller({
112
114
  api,
113
115
  credentials: ctx.credentials,
@@ -160,6 +162,7 @@ export async function createProductionController(ctx, config = {}, internals = {
160
162
  workspaces,
161
163
  stateFor,
162
164
  agentPresetCatalog,
165
+ modelCatalog,
163
166
  });
164
167
  const supervisor = createSupervisor({
165
168
  controller,
@@ -11,6 +11,7 @@ import {
11
11
  SET_AGENT_PRESET_ENDPOINT,
12
12
  validAgentPresetPayload,
13
13
  } from '../shared/agent-preset-rpc.mjs';
14
+ import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
14
15
  import {
15
16
  connectionTestTargetUnavailable,
16
17
  publicConnectionTestResult,
@@ -26,6 +27,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
26
27
  reconnectBot: 'bot.reconnect',
27
28
  deleteBot: 'bot.delete',
28
29
  setWorkspace: SET_WORKSPACE_ENDPOINT,
30
+ setModel: SET_MODEL_ENDPOINT,
29
31
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
30
32
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
31
33
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
@@ -83,6 +85,9 @@ function payloadFailure(endpoint, payload) {
83
85
  return validWorkspacePayload(payload)
84
86
  ? null : '请输入工作区绝对路径。';
85
87
  }
88
+ if (endpoint === WEIXIN_ENDPOINTS.setModel) {
89
+ return validModelPayload(payload) ? null : '请选择有效模型。';
90
+ }
86
91
  if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
87
92
  return validAgentPresetPayload(payload)
88
93
  ? null : '请选择 Agent Preset。';
@@ -219,6 +224,12 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
219
224
  await controller.updateWorkspace(payload.botId, payload.workspace),
220
225
  cachedEncode,
221
226
  );
227
+ } else if (endpoint === WEIXIN_ENDPOINTS.setModel) {
228
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
229
+ value = await publicStatus(
230
+ await controller.updateModel(payload.botId, payload.model),
231
+ cachedEncode,
232
+ );
222
233
  } else if (endpoint === WEIXIN_ENDPOINTS.setContextEnhancement) {
223
234
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
224
235
  value = await controller.updateContextEnhancement(
@@ -18,6 +18,7 @@ import {
18
18
  observeBotWorkspaceRemovals,
19
19
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
20
20
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
21
+ import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
21
22
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
22
23
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
23
24
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
@@ -111,6 +112,7 @@ export async function createProductionController(ctx, config = {}, internals = {
111
112
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
112
113
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
113
114
  });
115
+ const modelCatalog = () => listModelCatalog(harness);
114
116
  const coreController = new Controller({
115
117
  configStore: observedConfigStore,
116
118
  authPath: paths.authPath,
@@ -167,6 +169,7 @@ export async function createProductionController(ctx, config = {}, internals = {
167
169
  workspaces,
168
170
  stateFor,
169
171
  agentPresetCatalog,
172
+ modelCatalog,
170
173
  });
171
174
  const supervisor = createSupervisor({
172
175
  channel: 'whatsapp',
@@ -6,6 +6,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
6
6
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
7
7
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
8
8
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
9
+ import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
9
10
 
10
11
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
11
12
  export const WHATSAPP_ENDPOINTS = Object.freeze({
@@ -17,6 +18,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
17
18
  deleteBot: 'bot.delete',
18
19
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
21
+ setModel: SET_MODEL_ENDPOINT,
20
22
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
21
23
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
22
24
  });
@@ -62,6 +64,9 @@ function payloadFailure(endpoint, payload) {
62
64
  return validWorkspacePayload(payload)
63
65
  ? null : '请输入工作区绝对路径。';
64
66
  }
67
+ if (endpoint === WHATSAPP_ENDPOINTS.setModel) {
68
+ return validModelPayload(payload) ? null : '请选择有效模型。';
69
+ }
65
70
  if (endpoint === WHATSAPP_ENDPOINTS.setAgentPreset) {
66
71
  return validAgentPresetPayload(payload)
67
72
  ? null : '请选择 Agent Preset。';
@@ -172,6 +177,12 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
172
177
  await controller.updateWorkspace(payload.botId, payload.workspace),
173
178
  cachedEncode,
174
179
  );
180
+ } else if (endpoint === WHATSAPP_ENDPOINTS.setModel) {
181
+ if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
182
+ value = await publicStatus(
183
+ await controller.updateModel(payload.botId, payload.model),
184
+ cachedEncode,
185
+ );
175
186
  } else if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
176
187
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
177
188
  value = await controller.updateContextEnhancement(
@@ -1,4 +1,8 @@
1
- import { deliverySuggestionsFromSessions } from './delivery-suggestions.mjs';
1
+ import {
2
+ deliverySuggestionsFromSessions,
3
+ privateConversationKeyMatchesTarget,
4
+ resolvePrivateConversationKey,
5
+ } from './delivery-suggestions.mjs';
2
6
 
3
7
  const CHANNELS = new Set([
4
8
  'weixin',
@@ -22,6 +26,12 @@ function invalidTarget(message) {
22
26
  return error;
23
27
  }
24
28
 
29
+ function sessionSyncUnavailable(message = 'Session sync is unavailable for this target') {
30
+ const error = new Error(message);
31
+ error.code = 'session-sync-unavailable';
32
+ return error;
33
+ }
34
+
25
35
  function isRecord(value) {
26
36
  return value !== null && typeof value === 'object' && !Array.isArray(value);
27
37
  }
@@ -129,6 +139,49 @@ export function normalizeDeliveryTarget(channel, value, { targetIdRequired = tru
129
139
  return normalized;
130
140
  }
131
141
 
142
+ function targetWithoutSessionSync(target) {
143
+ return {
144
+ targetId: target.targetId,
145
+ ...(target.name === undefined ? {} : { name: target.name }),
146
+ kind: target.kind,
147
+ route: target.route,
148
+ };
149
+ }
150
+
151
+ function sessionSyncConfigs(workspaces, botId) {
152
+ if (typeof workspaces.listSessionSyncTargets !== 'function') return [];
153
+ const targets = workspaces.listSessionSyncTargets();
154
+ return botId === undefined ? targets : targets.filter((target) => target.botId === botId);
155
+ }
156
+
157
+ function stateSessions(state) {
158
+ if (!state || typeof state.snapshot !== 'function') return null;
159
+ const sessions = state.snapshot()?.sessions;
160
+ return sessions && typeof sessions === 'object' && !Array.isArray(sessions) ? sessions : null;
161
+ }
162
+
163
+ function sessionSyncView(channel, target, sessions, configured) {
164
+ if (configured) {
165
+ const valid = privateConversationKeyMatchesTarget(
166
+ channel,
167
+ configured.conversationKey,
168
+ target,
169
+ );
170
+ if (!valid || !sessions) return { enabled: true, state: 'unavailable' };
171
+ return {
172
+ enabled: true,
173
+ state: typeof sessions[configured.conversationKey] === 'string'
174
+ && Boolean(sessions[configured.conversationKey])
175
+ ? 'active'
176
+ : 'waiting',
177
+ };
178
+ }
179
+ const conversationKey = sessions
180
+ ? resolvePrivateConversationKey(channel, sessions, target)
181
+ : null;
182
+ return { enabled: false, state: conversationKey ? 'off' : 'unavailable' };
183
+ }
184
+
132
185
  /** Bind one channel's existing workspace store and unwrapped controller to DeliveryService. */
133
186
  export function createDeliveryAdapter({ channel, workspaces, coreController, stateFor }) {
134
187
  if (!supportsDeliveryChannel(channel)) throw new TypeError(`Unsupported delivery channel: ${channel}`);
@@ -145,7 +198,22 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
145
198
  channel,
146
199
  ownsBot: (botId) => workspaces.has(botId),
147
200
  listBots: () => workspaces.listBotIds(),
148
- listTargets: (botId) => workspaces.listDeliveryTargets(botId),
201
+ async listTargets(botId) {
202
+ const targets = workspaces.listDeliveryTargets(botId);
203
+ const configured = new Map(
204
+ sessionSyncConfigs(workspaces, botId).map((target) => [target.targetId, target]),
205
+ );
206
+ let sessions = null;
207
+ try {
208
+ sessions = stateSessions(await stateFor(botId));
209
+ } catch {
210
+ // Target CRUD remains usable when its persisted conversation state cannot be read.
211
+ }
212
+ return targets.map((target) => ({
213
+ ...target,
214
+ sessionSync: sessionSyncView(channel, target, sessions, configured.get(target.targetId)),
215
+ }));
216
+ },
149
217
  async listSuggestions(botId) {
150
218
  const state = await stateFor(botId);
151
219
  if (!state || typeof state.snapshot !== 'function') {
@@ -169,12 +237,82 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
169
237
  ),
170
238
  deleteTarget: (botId, targetId) => workspaces.deleteDeliveryTarget(botId, targetId),
171
239
  async sendText(botId, target, text, options = {}) {
172
- const normalized = normalizeDeliveryTarget(channel, target);
240
+ const normalized = normalizeDeliveryTarget(channel, targetWithoutSessionSync(target));
173
241
  if (typeof coreController.sendProactiveText !== 'function') {
174
242
  throw new TypeError('delivery controller cannot send proactive text');
175
243
  }
176
244
  await coreController.sendProactiveText(botId, normalized, text, options);
177
245
  return { sent: true };
178
246
  },
247
+ async setSessionSync(botId, targetId, enabled) {
248
+ if (typeof workspaces.setDeliveryTargetSessionSync !== 'function') {
249
+ throw sessionSyncUnavailable();
250
+ }
251
+ const target = workspaces.deliveryTargetFor(botId, targetId);
252
+ if (!target) {
253
+ const error = new Error('Unknown target');
254
+ error.code = 'unknown-target';
255
+ throw error;
256
+ }
257
+ if (!enabled) {
258
+ await workspaces.setDeliveryTargetSessionSync(botId, targetId, null);
259
+ let sessions = null;
260
+ try {
261
+ sessions = stateSessions(await stateFor(botId));
262
+ } catch {
263
+ // The setting is already safely disabled; only its availability is unknown.
264
+ }
265
+ return sessionSyncView(channel, target, sessions, null);
266
+ }
267
+ const sessions = stateSessions(await stateFor(botId));
268
+ const conversationKey = resolvePrivateConversationKey(channel, sessions, target);
269
+ if (!conversationKey) throw sessionSyncUnavailable();
270
+ await workspaces.setDeliveryTargetSessionSync(botId, targetId, conversationKey);
271
+ return { enabled: true, state: 'active' };
272
+ },
273
+ async listSessionSyncTargets(sessionId) {
274
+ if (typeof sessionId !== 'string' || !sessionId) {
275
+ throw new TypeError('sessionId is required');
276
+ }
277
+ const matches = [];
278
+ for (const configured of sessionSyncConfigs(workspaces)) {
279
+ const target = workspaces.deliveryTargetFor(configured.botId, configured.targetId);
280
+ if (!target || !privateConversationKeyMatchesTarget(
281
+ channel,
282
+ configured.conversationKey,
283
+ target,
284
+ )) continue;
285
+ let sessions;
286
+ try {
287
+ sessions = stateSessions(await stateFor(configured.botId));
288
+ } catch {
289
+ continue;
290
+ }
291
+ if (sessions?.[configured.conversationKey] === sessionId) {
292
+ matches.push({ botId: configured.botId, targetId: configured.targetId });
293
+ }
294
+ }
295
+ return matches;
296
+ },
297
+ async sendSessionSyncText(botId, targetId, sessionId, text, options = {}) {
298
+ if (typeof text !== 'string' || !text.trim()) throw new TypeError('text is required');
299
+ const configured = sessionSyncConfigs(workspaces, botId)
300
+ .find((target) => target.targetId === targetId);
301
+ const target = workspaces.deliveryTargetFor(botId, targetId);
302
+ if (!configured || !target || !privateConversationKeyMatchesTarget(
303
+ channel,
304
+ configured.conversationKey,
305
+ target,
306
+ )) throw sessionSyncUnavailable('Session sync target is no longer enabled');
307
+ const sessions = stateSessions(await stateFor(botId));
308
+ if (sessions?.[configured.conversationKey] !== sessionId) {
309
+ throw sessionSyncUnavailable('Session sync target is no longer bound to this Session');
310
+ }
311
+ if (typeof coreController.sendProactiveText !== 'function') {
312
+ throw new TypeError('delivery controller cannot send proactive text');
313
+ }
314
+ await coreController.sendProactiveText(botId, target, text, options);
315
+ return { sent: true };
316
+ },
179
317
  });
180
318
  }