@xmanrui/dsh-im 4.10.0 → 4.11.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 (56) hide show
  1. package/PROACTIVE_DELIVERY.en.md +15 -1
  2. package/PROACTIVE_DELIVERY.md +15 -1
  3. package/README.en.md +5 -1
  4. package/README.md +6 -2
  5. package/lib/client.js +1619 -769
  6. package/lib/index.js +248 -244
  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 +25 -2
  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 +26 -204
  14. package/plugin-src/client/channels/qq/api.js +4 -0
  15. package/plugin-src/client/channels/qq/index.js +25 -2
  16. package/plugin-src/client/channels/shared/token-api.js +4 -0
  17. package/plugin-src/client/channels/shared/token-channel.js +25 -3
  18. package/plugin-src/client/channels/wecom/api.js +4 -0
  19. package/plugin-src/client/channels/wecom/index.js +25 -2
  20. package/plugin-src/client/channels/weixin/api.js +4 -0
  21. package/plugin-src/client/channels/weixin/index.js +25 -2
  22. package/plugin-src/client/channels/whatsapp/api.js +4 -0
  23. package/plugin-src/client/channels/whatsapp/index.js +24 -2
  24. package/plugin-src/client/delivery-settings.js +73 -5
  25. package/plugin-src/client/i18n.js +20 -0
  26. package/plugin-src/client/model-setting.js +104 -0
  27. package/plugin-src/client/styles.js +50 -0
  28. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  29. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  31. package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
  32. package/plugin-src/host/channels/qq/production.mjs +3 -0
  33. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  34. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
  35. package/plugin-src/host/channels/shared/production.mjs +3 -0
  36. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  37. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  38. package/plugin-src/host/channels/slack/production.mjs +3 -0
  39. package/plugin-src/host/channels/slack/rpc.mjs +9 -0
  40. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  41. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  42. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  43. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  44. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  45. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  46. package/plugin-src/host/delivery-adapter.mjs +141 -3
  47. package/plugin-src/host/delivery-rpc.mjs +12 -0
  48. package/plugin-src/host/delivery-service.mjs +110 -3
  49. package/plugin-src/host/delivery-suggestions.mjs +46 -0
  50. package/plugin-src/host/harness-session-coordinator.mjs +6 -5
  51. package/plugin-src/host/index.mjs +18 -1
  52. package/plugin-src/host/session-sync-coordinator.mjs +217 -0
  53. package/scripts/verify-package.mjs +12 -6
  54. package/src/channels/shared/bot-workspace-store.mjs +285 -38
  55. package/src/channels/shared/harness-client.mjs +124 -43
  56. package/src/channels/shared/model-setting.mjs +110 -0
@@ -23,8 +23,16 @@ import {
23
23
  normalizeContextEnhancementConfig,
24
24
  validateContextEnhancementConfig,
25
25
  } from './context-enhancement.mjs';
26
+ import {
27
+ modelCatalogHas,
28
+ normalizeModelCatalog,
29
+ sameModelSelection,
30
+ validateModelSelection,
31
+ } from './model-setting.mjs';
26
32
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
27
33
 
34
+ const DELIVERY_DOCUMENT_VERSION = 2;
35
+ export const CURRENT_DOCUMENT_VERSION = 3;
28
36
  const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
29
37
 
30
38
  function workspaceSessionStale(message) {
@@ -84,13 +92,21 @@ function jsonRecord(value) {
84
92
  }
85
93
  }
86
94
 
87
- function normalizeDeliveryTarget(value, { targetId } = {}) {
95
+ function conversationKeyOf(value) {
96
+ if (typeof value !== 'string' || !value || value.length > 1_024
97
+ || value.trim() !== value || /[\u0000-\u001f\u007f]/u.test(value)) {
98
+ throw deliveryTargetError('invalid-target', 'Invalid private conversation key');
99
+ }
100
+ return value;
101
+ }
102
+
103
+ function normalizeDeliveryTarget(value, { targetId, allowSessionSync = false } = {}) {
88
104
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
89
105
  throw deliveryTargetError('invalid-target', 'Invalid delivery target');
90
106
  }
91
107
  const allowed = targetId === undefined
92
- ? new Set(['targetId', 'name', 'kind', 'route'])
93
- : new Set(['name', 'kind', 'route']);
108
+ ? new Set(['targetId', 'name', 'kind', 'route', ...(allowSessionSync ? ['sessionSync'] : [])])
109
+ : new Set(['name', 'kind', 'route', ...(allowSessionSync ? ['sessionSync'] : [])]);
94
110
  if (Object.keys(value).some((key) => !allowed.has(key))) {
95
111
  throw deliveryTargetError('invalid-target', 'Invalid delivery target');
96
112
  }
@@ -105,15 +121,37 @@ function normalizeDeliveryTarget(value, { targetId } = {}) {
105
121
  }
106
122
  name = value.name.trim();
107
123
  }
124
+ let sessionSync;
125
+ if (value.sessionSync !== undefined) {
126
+ if (!allowSessionSync || !value.sessionSync || typeof value.sessionSync !== 'object'
127
+ || Array.isArray(value.sessionSync)
128
+ || Object.keys(value.sessionSync).length !== 1
129
+ || !Object.hasOwn(value.sessionSync, 'conversationKey')) {
130
+ throw deliveryTargetError('invalid-target', 'Invalid delivery target session sync');
131
+ }
132
+ sessionSync = { conversationKey: conversationKeyOf(value.sessionSync.conversationKey) };
133
+ }
108
134
  return {
109
135
  targetId: id,
110
136
  ...(name === undefined ? {} : { name }),
111
137
  kind: value.kind,
112
138
  route: jsonRecord(value.route),
139
+ ...(sessionSync === undefined ? {} : { sessionSync }),
113
140
  };
114
141
  }
115
142
 
116
- function normalizeDeliveryTargets(value) {
143
+ function publicDeliveryTarget(value, { targetId } = {}) {
144
+ const normalized = normalizeDeliveryTarget(value, { targetId, allowSessionSync: true });
145
+ const { sessionSync: _sessionSync, ...target } = normalized;
146
+ return target;
147
+ }
148
+
149
+ function sameDeliveryRoute(left, right) {
150
+ return left?.kind === right?.kind
151
+ && JSON.stringify(left?.route) === JSON.stringify(right?.route);
152
+ }
153
+
154
+ function normalizeDeliveryTargets(value, { version } = {}) {
117
155
  const deliveryTargets = Object.create(null);
118
156
  if (value === undefined) return deliveryTargets;
119
157
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
@@ -123,7 +161,10 @@ function normalizeDeliveryTargets(value) {
123
161
  if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return null;
124
162
  const normalizedTargets = Object.create(null);
125
163
  for (const [targetId, target] of Object.entries(targets)) {
126
- const normalized = normalizeDeliveryTarget(target, { targetId });
164
+ const normalized = normalizeDeliveryTarget(target, {
165
+ targetId,
166
+ allowSessionSync: version >= CURRENT_DOCUMENT_VERSION,
167
+ });
127
168
  const { targetId: _targetId, ...stored } = normalized;
128
169
  normalizedTargets[targetId] = stored;
129
170
  }
@@ -156,7 +197,8 @@ function normalizeAccessPolicies(value, workspaces) {
156
197
  }
157
198
 
158
199
  function normalizeDocument(value) {
159
- if (!value || ![1, 2].includes(value.version) || !value.workspaces
200
+ if (!value || ![1, DELIVERY_DOCUMENT_VERSION, CURRENT_DOCUMENT_VERSION].includes(value.version)
201
+ || !value.workspaces
160
202
  || typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return null;
161
203
  const workspaces = {};
162
204
  for (const [botId, workspace] of Object.entries(value.workspaces)) {
@@ -179,6 +221,22 @@ function normalizeDocument(value) {
179
221
  }
180
222
  }
181
223
  }
224
+ const models = {};
225
+ if (value.models !== undefined) {
226
+ if (!value.models || typeof value.models !== 'object' || Array.isArray(value.models)) {
227
+ return null;
228
+ }
229
+ for (const [botId, model] of Object.entries(value.models)) {
230
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return null;
231
+ try {
232
+ const normalized = validateModelSelection(model);
233
+ if (!normalized) return null;
234
+ models[botId] = normalized;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+ }
182
240
  const contextEnhancement = Object.create(null);
183
241
  // Enhancement damage is isolated from the existing workspace/preset document.
184
242
  if (value.contextEnhancement && typeof value.contextEnhancement === 'object'
@@ -190,16 +248,20 @@ function normalizeDocument(value) {
190
248
  }
191
249
  }
192
250
  if (value.version === 1 && value.deliveryTargets !== undefined) return null;
193
- const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets);
251
+ const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets, { version: value.version });
194
252
  if (!deliveryTargets) return null;
195
253
  const accessPolicies = normalizeAccessPolicies(value.accessPolicies, workspaces);
196
- const version = value.accessPolicies === undefined ? value.version : 2;
254
+ const version = Math.max(
255
+ value.version,
256
+ value.accessPolicies === undefined ? 1 : DELIVERY_DOCUMENT_VERSION,
257
+ );
197
258
  return {
198
259
  // A v1 file cannot be emitted with this optional v2 section. If one is
199
260
  // recovered from an interrupted/manual edit, retain it on the next write.
200
261
  version,
201
262
  workspaces,
202
263
  agentPresets,
264
+ models,
203
265
  contextEnhancement,
204
266
  deliveryTargets,
205
267
  accessPolicies,
@@ -210,19 +272,21 @@ function storedDocument({
210
272
  version,
211
273
  workspaces,
212
274
  agentPresets,
275
+ models,
213
276
  contextEnhancement,
214
277
  deliveryTargets,
215
278
  accessPolicies,
216
279
  }) {
217
280
  const document = { version, workspaces };
218
281
  if (Object.keys(agentPresets).length > 0) document.agentPresets = agentPresets;
282
+ if (Object.keys(models).length > 0) document.models = models;
219
283
  if (Object.keys(contextEnhancement).length > 0) {
220
284
  document.contextEnhancement = contextEnhancement;
221
285
  }
222
- if (version >= 2 && Object.keys(deliveryTargets).length > 0) {
286
+ if (version >= DELIVERY_DOCUMENT_VERSION && Object.keys(deliveryTargets).length > 0) {
223
287
  document.deliveryTargets = deliveryTargets;
224
288
  }
225
- if (version >= 2 && Object.keys(accessPolicies).length > 0) {
289
+ if (version >= DELIVERY_DOCUMENT_VERSION && Object.keys(accessPolicies).length > 0) {
226
290
  document.accessPolicies = accessPolicies;
227
291
  }
228
292
  return document;
@@ -267,6 +331,7 @@ export class BotWorkspaceStore {
267
331
  #version = 1;
268
332
  #workspaces = {};
269
333
  #agentPresets = {};
334
+ #models = {};
270
335
  #contextEnhancement = {};
271
336
  #deliveryTargets = Object.create(null);
272
337
  #accessPolicies = Object.create(null);
@@ -293,6 +358,7 @@ export class BotWorkspaceStore {
293
358
  this.#version = normalized.version;
294
359
  this.#workspaces = normalized.workspaces;
295
360
  this.#agentPresets = normalized.agentPresets;
361
+ this.#models = normalized.models;
296
362
  this.#contextEnhancement = normalized.contextEnhancement;
297
363
  this.#deliveryTargets = normalized.deliveryTargets;
298
364
  this.#accessPolicies = normalized.accessPolicies;
@@ -301,6 +367,7 @@ export class BotWorkspaceStore {
301
367
  this.#version = 1;
302
368
  this.#workspaces = {};
303
369
  this.#agentPresets = {};
370
+ this.#models = {};
304
371
  this.#contextEnhancement = {};
305
372
  this.#deliveryTargets = Object.create(null);
306
373
  this.#accessPolicies = Object.create(null);
@@ -341,6 +408,11 @@ export class BotWorkspaceStore {
341
408
  return this.#agentPresets[botIdOf(botId)] ?? null;
342
409
  }
343
410
 
411
+ modelFor(botId) {
412
+ const selection = this.#models[botIdOf(botId)];
413
+ return selection ? { ...selection } : null;
414
+ }
415
+
344
416
  contextEnhancementFor(botId) {
345
417
  const id = botIdOf(botId);
346
418
  return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
@@ -359,7 +431,7 @@ export class BotWorkspaceStore {
359
431
  const id = botIdOf(botId);
360
432
  if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
361
433
  return Object.entries(this.#deliveryTargets[id] ?? {})
362
- .map(([targetId, target]) => normalizeDeliveryTarget(target, { targetId }))
434
+ .map(([targetId, target]) => publicDeliveryTarget(target, { targetId }))
363
435
  .sort((left, right) => left.targetId.localeCompare(right.targetId));
364
436
  }
365
437
 
@@ -368,7 +440,29 @@ export class BotWorkspaceStore {
368
440
  const targetKey = targetIdOf(targetId);
369
441
  if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
370
442
  const target = this.#deliveryTargets[id]?.[targetKey];
371
- return target ? normalizeDeliveryTarget(target, { targetId: targetKey }) : null;
443
+ return target ? publicDeliveryTarget(target, { targetId: targetKey }) : null;
444
+ }
445
+
446
+ listSessionSyncTargets() {
447
+ const targets = [];
448
+ for (const [botId, botTargets] of Object.entries(this.#deliveryTargets)) {
449
+ if (!this.has(botId)) continue;
450
+ for (const [targetId, target] of Object.entries(botTargets)) {
451
+ const normalized = normalizeDeliveryTarget(target, {
452
+ targetId,
453
+ allowSessionSync: true,
454
+ });
455
+ if (!normalized.sessionSync) continue;
456
+ targets.push({
457
+ botId,
458
+ targetId,
459
+ conversationKey: normalized.sessionSync.conversationKey,
460
+ });
461
+ }
462
+ }
463
+ return targets.sort((left, right) => (
464
+ left.botId.localeCompare(right.botId) || left.targetId.localeCompare(right.targetId)
465
+ ));
372
466
  }
373
467
 
374
468
  async createDeliveryTarget(botId, value) {
@@ -384,10 +478,11 @@ export class BotWorkspaceStore {
384
478
  ...this.#deliveryTargets,
385
479
  [id]: { ...(this.#deliveryTargets[id] ?? {}), [targetId]: stored },
386
480
  };
387
- await this.#persist(this.#contextEnhancement, next, 2);
481
+ const nextVersion = Math.max(this.#version, DELIVERY_DOCUMENT_VERSION);
482
+ await this.#persist(this.#contextEnhancement, next, nextVersion);
388
483
  this.#deliveryTargets = next;
389
- this.#version = 2;
390
- return normalizeDeliveryTarget(stored, { targetId });
484
+ this.#version = nextVersion;
485
+ return publicDeliveryTarget(stored, { targetId });
391
486
  });
392
487
  }
393
488
 
@@ -401,14 +496,19 @@ export class BotWorkspaceStore {
401
496
  throw deliveryTargetError('unknown-target', 'Unknown target');
402
497
  }
403
498
  const { targetId: _targetId, ...stored } = replacement;
499
+ const previous = this.#deliveryTargets[id][targetKey];
500
+ const nextStored = previous.sessionSync && sameDeliveryRoute(previous, stored)
501
+ ? { ...stored, sessionSync: previous.sessionSync }
502
+ : stored;
404
503
  const next = {
405
504
  ...this.#deliveryTargets,
406
- [id]: { ...this.#deliveryTargets[id], [targetKey]: stored },
505
+ [id]: { ...this.#deliveryTargets[id], [targetKey]: nextStored },
407
506
  };
408
- await this.#persist(this.#contextEnhancement, next, 2);
507
+ const nextVersion = Math.max(this.#version, DELIVERY_DOCUMENT_VERSION);
508
+ await this.#persist(this.#contextEnhancement, next, nextVersion);
409
509
  this.#deliveryTargets = next;
410
- this.#version = 2;
411
- return normalizeDeliveryTarget(stored, { targetId: targetKey });
510
+ this.#version = nextVersion;
511
+ return publicDeliveryTarget(nextStored, { targetId: targetKey });
412
512
  });
413
513
  }
414
514
 
@@ -425,13 +525,40 @@ export class BotWorkspaceStore {
425
525
  const next = { ...this.#deliveryTargets };
426
526
  if (Object.keys(botTargets).length > 0) next[id] = botTargets;
427
527
  else delete next[id];
428
- await this.#persist(this.#contextEnhancement, next, 2);
528
+ const nextVersion = Math.max(this.#version, DELIVERY_DOCUMENT_VERSION);
529
+ await this.#persist(this.#contextEnhancement, next, nextVersion);
429
530
  this.#deliveryTargets = next;
430
- this.#version = 2;
531
+ this.#version = nextVersion;
431
532
  return true;
432
533
  });
433
534
  }
434
535
 
536
+ async setDeliveryTargetSessionSync(botId, targetId, conversationKeyOrNull) {
537
+ const id = botIdOf(botId);
538
+ const targetKey = targetIdOf(targetId);
539
+ const conversationKey = conversationKeyOrNull === null
540
+ ? null
541
+ : conversationKeyOf(conversationKeyOrNull);
542
+ return this.#enqueue(id, async () => {
543
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
544
+ const previous = this.#deliveryTargets[id]?.[targetKey];
545
+ if (!previous) throw deliveryTargetError('unknown-target', 'Unknown target');
546
+ const previousKey = previous.sessionSync?.conversationKey ?? null;
547
+ if (previousKey === conversationKey) return conversationKey !== null;
548
+ const nextStored = { ...previous };
549
+ if (conversationKey === null) delete nextStored.sessionSync;
550
+ else nextStored.sessionSync = { conversationKey };
551
+ const next = {
552
+ ...this.#deliveryTargets,
553
+ [id]: { ...this.#deliveryTargets[id], [targetKey]: nextStored },
554
+ };
555
+ await this.#persist(this.#contextEnhancement, next, CURRENT_DOCUMENT_VERSION);
556
+ this.#deliveryTargets = next;
557
+ this.#version = CURRENT_DOCUMENT_VERSION;
558
+ return conversationKey !== null;
559
+ });
560
+ }
561
+
435
562
  generationFor(botId) {
436
563
  return this.#generations.get(botIdOf(botId)) ?? null;
437
564
  }
@@ -477,7 +604,9 @@ export class BotWorkspaceStore {
477
604
  this.#generations.set(id, this.#freshGeneration());
478
605
  this.#incarnations.set(id, this.#freshIncarnation());
479
606
  }
480
- const nextVersion = initializesAccessPolicy ? 2 : this.#version;
607
+ const nextVersion = initializesAccessPolicy
608
+ ? Math.max(this.#version, DELIVERY_DOCUMENT_VERSION)
609
+ : this.#version;
481
610
  try {
482
611
  await this.#persist(
483
612
  this.#contextEnhancement,
@@ -571,6 +700,37 @@ export class BotWorkspaceStore {
571
700
  });
572
701
  }
573
702
 
703
+ async setModel(botId, value, { incarnation } = {}) {
704
+ const id = botIdOf(botId);
705
+ if (!this.has(id)
706
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
707
+ const error = new Error('找不到要修改的机器人。');
708
+ error.code = 'workspace-bot-not-found';
709
+ throw error;
710
+ }
711
+ const model = validateModelSelection(value);
712
+ return this.#enqueue(id, async () => {
713
+ if (!this.has(id)
714
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
715
+ const error = new Error('找不到要修改的机器人。');
716
+ error.code = 'workspace-bot-not-found';
717
+ throw error;
718
+ }
719
+ const previous = this.#models[id] ?? null;
720
+ if (sameModelSelection(previous, model)) return model ? { ...model } : null;
721
+ if (model) this.#models[id] = model;
722
+ else delete this.#models[id];
723
+ try {
724
+ await this.#persist();
725
+ } catch (error) {
726
+ if (previous) this.#models[id] = previous;
727
+ else delete this.#models[id];
728
+ throw error;
729
+ }
730
+ return model ? { ...model } : null;
731
+ });
732
+ }
733
+
574
734
  async setContextEnhancement(botId, value, { incarnation } = {}) {
575
735
  const id = botIdOf(botId);
576
736
  const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
@@ -604,11 +764,11 @@ export class BotWorkspaceStore {
604
764
  await this.#persist(
605
765
  this.#contextEnhancement,
606
766
  this.#deliveryTargets,
607
- 2,
767
+ Math.max(this.#version, DELIVERY_DOCUMENT_VERSION),
608
768
  next,
609
769
  );
610
770
  this.#accessPolicies = next;
611
- this.#version = 2;
771
+ this.#version = Math.max(this.#version, DELIVERY_DOCUMENT_VERSION);
612
772
  return policy;
613
773
  });
614
774
  }
@@ -770,6 +930,7 @@ export class BotWorkspaceStore {
770
930
  const candidates = new Set([
771
931
  ...Object.keys(this.#workspaces),
772
932
  ...Object.keys(this.#agentPresets),
933
+ ...Object.keys(this.#models),
773
934
  ...Object.keys(this.#contextEnhancement),
774
935
  ...Object.keys(this.#deliveryTargets),
775
936
  ...Object.keys(this.#accessPolicies),
@@ -789,6 +950,7 @@ export class BotWorkspaceStore {
789
950
  ...bot,
790
951
  workspace: this.workspaceFor(bot.botId),
791
952
  agentPreset: this.agentPresetFor(bot.botId),
953
+ model: this.modelFor(bot.botId),
792
954
  contextEnhancement: this.contextEnhancementFor(bot.botId),
793
955
  accessPolicy: this.accessPolicyFor(bot.botId),
794
956
  }
@@ -820,13 +982,15 @@ export class BotWorkspaceStore {
820
982
  async #retireCurrentIncarnation(id) {
821
983
  const hadWorkspace = Object.hasOwn(this.#workspaces, id);
822
984
  const hadPreset = Object.hasOwn(this.#agentPresets, id);
985
+ const hadModel = Object.hasOwn(this.#models, id);
823
986
  const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
824
987
  const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
825
988
  const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
826
- const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
989
+ const needsCleanup = hadWorkspace || hadPreset || hadModel || hadContextEnhancement
827
990
  || hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
828
991
  delete this.#workspaces[id];
829
992
  delete this.#agentPresets[id];
993
+ delete this.#models[id];
830
994
  delete this.#contextEnhancement[id];
831
995
  delete this.#deliveryTargets[id];
832
996
  delete this.#accessPolicies[id];
@@ -869,6 +1033,7 @@ export class BotWorkspaceStore {
869
1033
  version,
870
1034
  workspaces: this.#workspaces,
871
1035
  agentPresets: this.#agentPresets,
1036
+ models: this.#models,
872
1037
  contextEnhancement,
873
1038
  deliveryTargets,
874
1039
  accessPolicies,
@@ -879,6 +1044,7 @@ export class BotWorkspaceStore {
879
1044
  async #persistCurrentDocument() {
880
1045
  if (Object.keys(this.#workspaces).length > 0
881
1046
  || Object.keys(this.#agentPresets).length > 0
1047
+ || Object.keys(this.#models).length > 0
882
1048
  || Object.keys(this.#contextEnhancement).length > 0
883
1049
  || Object.keys(this.#deliveryTargets).length > 0
884
1050
  || Object.keys(this.#accessPolicies).length > 0) {
@@ -903,12 +1069,26 @@ function resolveAgentPresetCatalog(catalog) {
903
1069
  : normalizeAgentPresetCatalog(value);
904
1070
  }
905
1071
 
1072
+ function resolveModelCatalog(catalog) {
1073
+ if (!catalog) return null;
1074
+ const value = typeof catalog === 'function' ? catalog() : catalog;
1075
+ return value && typeof value.then === 'function'
1076
+ ? value.then(normalizeModelCatalog)
1077
+ : normalizeModelCatalog(value);
1078
+ }
1079
+
906
1080
  function unavailableAgentPreset() {
907
1081
  const error = new Error('Agent Preset 不存在或不可用。');
908
1082
  error.code = 'agent-preset-unavailable';
909
1083
  return error;
910
1084
  }
911
1085
 
1086
+ function unavailableModel() {
1087
+ const error = new Error('模型不存在或不可用。');
1088
+ error.code = 'model-selection-unavailable';
1089
+ return error;
1090
+ }
1091
+
912
1092
  function assertCurrentBotScope(isCurrentScope) {
913
1093
  if (isCurrentScope()) return;
914
1094
  const error = new Error('找不到要修改的机器人。');
@@ -916,17 +1096,23 @@ function assertCurrentBotScope(isCurrentScope) {
916
1096
  throw error;
917
1097
  }
918
1098
 
919
- function decorateResult(workspaces, result, catalog) {
1099
+ function decorateResult(workspaces, result, agentPresetCatalogSource, modelCatalogSource) {
920
1100
  const decorate = (value) => {
921
1101
  const decorated = workspaces.decorateStatus(value);
922
- if (!catalog || !decorated || typeof decorated !== 'object') return decorated;
923
- const attachCatalog = (agentPresetCatalog) => (
924
- agentPresetCatalog ? { ...decorated, agentPresetCatalog } : decorated
925
- );
926
- const agentPresetCatalog = resolveAgentPresetCatalog(catalog);
927
- return agentPresetCatalog && typeof agentPresetCatalog.then === 'function'
928
- ? agentPresetCatalog.then(attachCatalog)
929
- : attachCatalog(agentPresetCatalog);
1102
+ if ((!agentPresetCatalogSource && !modelCatalogSource)
1103
+ || !decorated || typeof decorated !== 'object') return decorated;
1104
+ const attachCatalogs = ([agentPresetCatalog, modelCatalog]) => ({
1105
+ ...decorated,
1106
+ ...(agentPresetCatalog ? { agentPresetCatalog } : {}),
1107
+ ...(modelCatalog ? { modelCatalog } : {}),
1108
+ });
1109
+ const catalogs = [
1110
+ resolveAgentPresetCatalog(agentPresetCatalogSource),
1111
+ resolveModelCatalog(modelCatalogSource),
1112
+ ];
1113
+ return catalogs.some((catalog) => catalog && typeof catalog.then === 'function')
1114
+ ? Promise.all(catalogs).then(attachCatalogs)
1115
+ : attachCatalogs(catalogs);
930
1116
  };
931
1117
  return result && typeof result.then === 'function'
932
1118
  ? result.then(decorate)
@@ -1158,11 +1344,27 @@ export function createBotWorkspaceScope(
1158
1344
  }
1159
1345
  const generation = workspaces.generationFor(botId);
1160
1346
  const agentPreset = workspaces.agentPresetFor(botId);
1347
+ const model = workspaces.modelFor(botId);
1161
1348
  const sessionId = await target.createSession({
1162
1349
  ...options,
1163
1350
  workspace: workspaces.workspaceFor(botId),
1164
1351
  ...(agentPreset == null ? {} : { agentPreset }),
1165
1352
  });
1353
+ if (model) {
1354
+ if (typeof target.selectSessionModel !== 'function') {
1355
+ throw new TypeError('Harness does not support model selection');
1356
+ }
1357
+ const selected = await target.selectSessionModel(
1358
+ sessionId,
1359
+ model,
1360
+ options.signal ? { signal: options.signal } : {},
1361
+ );
1362
+ if (!sameModelSelection(selected?.selected, model)) {
1363
+ const error = new Error('Harness did not confirm the selected model');
1364
+ error.code = 'model-selection-mismatch';
1365
+ throw error;
1366
+ }
1367
+ }
1166
1368
  sessionGenerations.set(sessionId, generation);
1167
1369
  return sessionId;
1168
1370
  };
@@ -1324,7 +1526,12 @@ export function createBotScopedHarness(harness, options) {
1324
1526
  return createBotWorkspaceScope(harness, options).harness;
1325
1527
  }
1326
1528
 
1327
- export function createWorkspaceAwareController(controller, { workspaces, stateFor, agentPresetCatalog } = {}) {
1529
+ export function createWorkspaceAwareController(controller, {
1530
+ workspaces,
1531
+ stateFor,
1532
+ agentPresetCatalog,
1533
+ modelCatalog,
1534
+ } = {}) {
1328
1535
  if (!controller || !workspaces || typeof stateFor !== 'function') {
1329
1536
  throw new TypeError('controller, workspaces, and stateFor are required');
1330
1537
  }
@@ -1337,7 +1544,12 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1337
1544
  if (transitions.get(botId) === current) transitions.delete(botId);
1338
1545
  });
1339
1546
  };
1340
- const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog);
1547
+ const decorate = (value) => decorateResult(
1548
+ workspaces,
1549
+ value,
1550
+ agentPresetCatalog,
1551
+ modelCatalog,
1552
+ );
1341
1553
  const updateWorkspace = (botId, workspace) => {
1342
1554
  // Capture at API invocation, before even waiting for an older outer
1343
1555
  // transition. A queued request still belongs to the incarnation that the
@@ -1380,6 +1592,32 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1380
1592
  workspaces,
1381
1593
  await controller.status(),
1382
1594
  catalog ?? agentPresetCatalog,
1595
+ modelCatalog,
1596
+ );
1597
+ });
1598
+ };
1599
+ const updateModel = (botId, value) => {
1600
+ const incarnation = workspaces.incarnationFor(botId);
1601
+ const model = validateModelSelection(value);
1602
+ return withBotTransition(botId, async () => {
1603
+ const snapshot = await controller.status();
1604
+ if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
1605
+ const error = new Error('找不到要修改的机器人。');
1606
+ error.code = 'workspace-bot-not-found';
1607
+ throw error;
1608
+ }
1609
+ const catalog = model && modelCatalog
1610
+ ? await resolveModelCatalog(modelCatalog)
1611
+ : null;
1612
+ if (model && (!modelCatalog || !modelCatalogHas(catalog, model))) {
1613
+ throw unavailableModel();
1614
+ }
1615
+ await workspaces.setModel(botId, model, { incarnation });
1616
+ return decorateResult(
1617
+ workspaces,
1618
+ await controller.status(),
1619
+ agentPresetCatalog,
1620
+ catalog ?? modelCatalog,
1383
1621
  );
1384
1622
  });
1385
1623
  };
@@ -1393,13 +1631,17 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1393
1631
  error.code = 'workspace-bot-not-found';
1394
1632
  throw error;
1395
1633
  }
1396
- const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
1634
+ const [catalog, models] = await Promise.all([
1635
+ resolveAgentPresetCatalog(agentPresetCatalog),
1636
+ resolveModelCatalog(modelCatalog),
1637
+ ]);
1397
1638
  const decorated = workspaces.decorateStatus(snapshot);
1398
1639
  const updated = {
1399
1640
  ...decorated,
1400
1641
  bots: decorated.bots.map((bot) => bot?.botId === botId
1401
1642
  ? { ...bot, contextEnhancement: config } : bot),
1402
1643
  ...(catalog ? { agentPresetCatalog: catalog } : {}),
1644
+ ...(models ? { modelCatalog: models } : {}),
1403
1645
  };
1404
1646
  // QR/status projection can fail too. Prepare the complete response before
1405
1647
  // commit so a failed save never publishes new running settings.
@@ -1418,13 +1660,17 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1418
1660
  error.code = 'workspace-bot-not-found';
1419
1661
  throw error;
1420
1662
  }
1421
- const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
1663
+ const [catalog, models] = await Promise.all([
1664
+ resolveAgentPresetCatalog(agentPresetCatalog),
1665
+ resolveModelCatalog(modelCatalog),
1666
+ ]);
1422
1667
  const decorated = workspaces.decorateStatus(snapshot);
1423
1668
  const updated = {
1424
1669
  ...decorated,
1425
1670
  bots: decorated.bots.map((bot) => bot?.botId === botId
1426
1671
  ? { ...bot, accessPolicy: policy } : bot),
1427
1672
  ...(catalog ? { agentPresetCatalog: catalog } : {}),
1673
+ ...(models ? { modelCatalog: models } : {}),
1428
1674
  };
1429
1675
  // Prepare the complete channel-specific response before commit. Failed
1430
1676
  // projections and disk writes must leave the live policy unchanged.
@@ -1472,6 +1718,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1472
1718
  get(target, property) {
1473
1719
  if (property === 'updateWorkspace') return updateWorkspace;
1474
1720
  if (property === 'updateAgentPreset') return updateAgentPreset;
1721
+ if (property === 'updateModel') return updateModel;
1475
1722
  if (property === 'updateContextEnhancement') return updateContextEnhancement;
1476
1723
  if (property === 'updateAccessPolicy') return updateAccessPolicy;
1477
1724
  const value = Reflect.get(target, property, target);