@xmanrui/dsh-im 4.9.1 → 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 (72) hide show
  1. package/PROACTIVE_DELIVERY.en.md +23 -1
  2. package/PROACTIVE_DELIVERY.md +23 -1
  3. package/README.en.md +5 -1
  4. package/README.md +6 -2
  5. package/lib/client.js +2077 -673
  6. package/lib/index.js +277 -272
  7. package/package.json +13 -1
  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 +6 -0
  12. package/plugin-src/client/channels/feishu/group-settings.js +581 -0
  13. package/plugin-src/client/channels/feishu/index.js +26 -150
  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/global-settings.js +341 -0
  26. package/plugin-src/client/i18n.js +49 -0
  27. package/plugin-src/client/index.js +47 -6
  28. package/plugin-src/client/model-setting.js +104 -0
  29. package/plugin-src/client/styles.js +99 -0
  30. package/plugin-src/host/channels/dingtalk/production.mjs +15 -0
  31. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  32. package/plugin-src/host/channels/feishu/production.mjs +17 -0
  33. package/plugin-src/host/channels/feishu/rpc.mjs +32 -0
  34. package/plugin-src/host/channels/qq/production.mjs +15 -0
  35. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  36. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
  37. package/plugin-src/host/channels/shared/production.mjs +15 -0
  38. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  39. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  40. package/plugin-src/host/channels/slack/production.mjs +15 -0
  41. package/plugin-src/host/channels/slack/rpc.mjs +9 -0
  42. package/plugin-src/host/channels/wecom/production.mjs +15 -0
  43. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  44. package/plugin-src/host/channels/weixin/production.mjs +15 -0
  45. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  46. package/plugin-src/host/channels/whatsapp/production.mjs +15 -0
  47. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  48. package/plugin-src/host/delivery-adapter.mjs +142 -3
  49. package/plugin-src/host/delivery-rpc.mjs +12 -0
  50. package/plugin-src/host/delivery-service.mjs +125 -3
  51. package/plugin-src/host/delivery-suggestions.mjs +46 -0
  52. package/plugin-src/host/harness-session-coordinator.mjs +32 -10
  53. package/plugin-src/host/inbound-ttl-rpc.mjs +83 -0
  54. package/plugin-src/host/inbound-ttl-runtime.mjs +85 -0
  55. package/plugin-src/host/inbound-ttl-service.mjs +179 -0
  56. package/plugin-src/host/index.mjs +26 -1
  57. package/plugin-src/host/modern-harness-api.mjs +58 -2
  58. package/plugin-src/host/session-sync-coordinator.mjs +217 -0
  59. package/scripts/verify-package.mjs +12 -6
  60. package/src/channels/feishu/bridge.mjs +164 -4
  61. package/src/channels/feishu/feishu-channel.mjs +45 -10
  62. package/src/channels/feishu/feishu-runtime.mjs +9 -0
  63. package/src/channels/feishu/message-utils.mjs +13 -0
  64. package/src/channels/feishu/multi-bot-controller.mjs +16 -0
  65. package/src/channels/feishu/plugin-config-store.mjs +1 -0
  66. package/src/channels/feishu/state-store.mjs +25 -0
  67. package/src/channels/shared/bot-workspace-store.mjs +291 -38
  68. package/src/channels/shared/harness-client.mjs +124 -43
  69. package/src/channels/shared/inbound-file.mjs +117 -2
  70. package/src/channels/shared/inbound-ttl-store.mjs +110 -0
  71. package/src/channels/shared/inbound-ttl.mjs +22 -0
  72. 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);
@@ -323,6 +390,12 @@ export class BotWorkspaceStore {
323
390
  return Object.hasOwn(this.#workspaces, id) && !this.#removals.has(id);
324
391
  }
325
392
 
393
+ listBotIds() {
394
+ return Object.keys(this.#workspaces)
395
+ .filter((botId) => !this.#removals.has(botId))
396
+ .sort();
397
+ }
398
+
326
399
  incarnationFor(botId) {
327
400
  return this.#incarnations.get(botIdOf(botId)) ?? null;
328
401
  }
@@ -335,6 +408,11 @@ export class BotWorkspaceStore {
335
408
  return this.#agentPresets[botIdOf(botId)] ?? null;
336
409
  }
337
410
 
411
+ modelFor(botId) {
412
+ const selection = this.#models[botIdOf(botId)];
413
+ return selection ? { ...selection } : null;
414
+ }
415
+
338
416
  contextEnhancementFor(botId) {
339
417
  const id = botIdOf(botId);
340
418
  return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
@@ -353,7 +431,7 @@ export class BotWorkspaceStore {
353
431
  const id = botIdOf(botId);
354
432
  if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
355
433
  return Object.entries(this.#deliveryTargets[id] ?? {})
356
- .map(([targetId, target]) => normalizeDeliveryTarget(target, { targetId }))
434
+ .map(([targetId, target]) => publicDeliveryTarget(target, { targetId }))
357
435
  .sort((left, right) => left.targetId.localeCompare(right.targetId));
358
436
  }
359
437
 
@@ -362,7 +440,29 @@ export class BotWorkspaceStore {
362
440
  const targetKey = targetIdOf(targetId);
363
441
  if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
364
442
  const target = this.#deliveryTargets[id]?.[targetKey];
365
- 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
+ ));
366
466
  }
367
467
 
368
468
  async createDeliveryTarget(botId, value) {
@@ -378,10 +478,11 @@ export class BotWorkspaceStore {
378
478
  ...this.#deliveryTargets,
379
479
  [id]: { ...(this.#deliveryTargets[id] ?? {}), [targetId]: stored },
380
480
  };
381
- 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);
382
483
  this.#deliveryTargets = next;
383
- this.#version = 2;
384
- return normalizeDeliveryTarget(stored, { targetId });
484
+ this.#version = nextVersion;
485
+ return publicDeliveryTarget(stored, { targetId });
385
486
  });
386
487
  }
387
488
 
@@ -395,14 +496,19 @@ export class BotWorkspaceStore {
395
496
  throw deliveryTargetError('unknown-target', 'Unknown target');
396
497
  }
397
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;
398
503
  const next = {
399
504
  ...this.#deliveryTargets,
400
- [id]: { ...this.#deliveryTargets[id], [targetKey]: stored },
505
+ [id]: { ...this.#deliveryTargets[id], [targetKey]: nextStored },
401
506
  };
402
- 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);
403
509
  this.#deliveryTargets = next;
404
- this.#version = 2;
405
- return normalizeDeliveryTarget(stored, { targetId: targetKey });
510
+ this.#version = nextVersion;
511
+ return publicDeliveryTarget(nextStored, { targetId: targetKey });
406
512
  });
407
513
  }
408
514
 
@@ -419,13 +525,40 @@ export class BotWorkspaceStore {
419
525
  const next = { ...this.#deliveryTargets };
420
526
  if (Object.keys(botTargets).length > 0) next[id] = botTargets;
421
527
  else delete next[id];
422
- 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);
423
530
  this.#deliveryTargets = next;
424
- this.#version = 2;
531
+ this.#version = nextVersion;
425
532
  return true;
426
533
  });
427
534
  }
428
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
+
429
562
  generationFor(botId) {
430
563
  return this.#generations.get(botIdOf(botId)) ?? null;
431
564
  }
@@ -471,7 +604,9 @@ export class BotWorkspaceStore {
471
604
  this.#generations.set(id, this.#freshGeneration());
472
605
  this.#incarnations.set(id, this.#freshIncarnation());
473
606
  }
474
- const nextVersion = initializesAccessPolicy ? 2 : this.#version;
607
+ const nextVersion = initializesAccessPolicy
608
+ ? Math.max(this.#version, DELIVERY_DOCUMENT_VERSION)
609
+ : this.#version;
475
610
  try {
476
611
  await this.#persist(
477
612
  this.#contextEnhancement,
@@ -565,6 +700,37 @@ export class BotWorkspaceStore {
565
700
  });
566
701
  }
567
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
+
568
734
  async setContextEnhancement(botId, value, { incarnation } = {}) {
569
735
  const id = botIdOf(botId);
570
736
  const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
@@ -598,11 +764,11 @@ export class BotWorkspaceStore {
598
764
  await this.#persist(
599
765
  this.#contextEnhancement,
600
766
  this.#deliveryTargets,
601
- 2,
767
+ Math.max(this.#version, DELIVERY_DOCUMENT_VERSION),
602
768
  next,
603
769
  );
604
770
  this.#accessPolicies = next;
605
- this.#version = 2;
771
+ this.#version = Math.max(this.#version, DELIVERY_DOCUMENT_VERSION);
606
772
  return policy;
607
773
  });
608
774
  }
@@ -764,6 +930,7 @@ export class BotWorkspaceStore {
764
930
  const candidates = new Set([
765
931
  ...Object.keys(this.#workspaces),
766
932
  ...Object.keys(this.#agentPresets),
933
+ ...Object.keys(this.#models),
767
934
  ...Object.keys(this.#contextEnhancement),
768
935
  ...Object.keys(this.#deliveryTargets),
769
936
  ...Object.keys(this.#accessPolicies),
@@ -783,6 +950,7 @@ export class BotWorkspaceStore {
783
950
  ...bot,
784
951
  workspace: this.workspaceFor(bot.botId),
785
952
  agentPreset: this.agentPresetFor(bot.botId),
953
+ model: this.modelFor(bot.botId),
786
954
  contextEnhancement: this.contextEnhancementFor(bot.botId),
787
955
  accessPolicy: this.accessPolicyFor(bot.botId),
788
956
  }
@@ -814,13 +982,15 @@ export class BotWorkspaceStore {
814
982
  async #retireCurrentIncarnation(id) {
815
983
  const hadWorkspace = Object.hasOwn(this.#workspaces, id);
816
984
  const hadPreset = Object.hasOwn(this.#agentPresets, id);
985
+ const hadModel = Object.hasOwn(this.#models, id);
817
986
  const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
818
987
  const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
819
988
  const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
820
- const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
989
+ const needsCleanup = hadWorkspace || hadPreset || hadModel || hadContextEnhancement
821
990
  || hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
822
991
  delete this.#workspaces[id];
823
992
  delete this.#agentPresets[id];
993
+ delete this.#models[id];
824
994
  delete this.#contextEnhancement[id];
825
995
  delete this.#deliveryTargets[id];
826
996
  delete this.#accessPolicies[id];
@@ -863,6 +1033,7 @@ export class BotWorkspaceStore {
863
1033
  version,
864
1034
  workspaces: this.#workspaces,
865
1035
  agentPresets: this.#agentPresets,
1036
+ models: this.#models,
866
1037
  contextEnhancement,
867
1038
  deliveryTargets,
868
1039
  accessPolicies,
@@ -873,6 +1044,7 @@ export class BotWorkspaceStore {
873
1044
  async #persistCurrentDocument() {
874
1045
  if (Object.keys(this.#workspaces).length > 0
875
1046
  || Object.keys(this.#agentPresets).length > 0
1047
+ || Object.keys(this.#models).length > 0
876
1048
  || Object.keys(this.#contextEnhancement).length > 0
877
1049
  || Object.keys(this.#deliveryTargets).length > 0
878
1050
  || Object.keys(this.#accessPolicies).length > 0) {
@@ -897,12 +1069,26 @@ function resolveAgentPresetCatalog(catalog) {
897
1069
  : normalizeAgentPresetCatalog(value);
898
1070
  }
899
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
+
900
1080
  function unavailableAgentPreset() {
901
1081
  const error = new Error('Agent Preset 不存在或不可用。');
902
1082
  error.code = 'agent-preset-unavailable';
903
1083
  return error;
904
1084
  }
905
1085
 
1086
+ function unavailableModel() {
1087
+ const error = new Error('模型不存在或不可用。');
1088
+ error.code = 'model-selection-unavailable';
1089
+ return error;
1090
+ }
1091
+
906
1092
  function assertCurrentBotScope(isCurrentScope) {
907
1093
  if (isCurrentScope()) return;
908
1094
  const error = new Error('找不到要修改的机器人。');
@@ -910,17 +1096,23 @@ function assertCurrentBotScope(isCurrentScope) {
910
1096
  throw error;
911
1097
  }
912
1098
 
913
- function decorateResult(workspaces, result, catalog) {
1099
+ function decorateResult(workspaces, result, agentPresetCatalogSource, modelCatalogSource) {
914
1100
  const decorate = (value) => {
915
1101
  const decorated = workspaces.decorateStatus(value);
916
- if (!catalog || !decorated || typeof decorated !== 'object') return decorated;
917
- const attachCatalog = (agentPresetCatalog) => (
918
- agentPresetCatalog ? { ...decorated, agentPresetCatalog } : decorated
919
- );
920
- const agentPresetCatalog = resolveAgentPresetCatalog(catalog);
921
- return agentPresetCatalog && typeof agentPresetCatalog.then === 'function'
922
- ? agentPresetCatalog.then(attachCatalog)
923
- : 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);
924
1116
  };
925
1117
  return result && typeof result.then === 'function'
926
1118
  ? result.then(decorate)
@@ -1152,11 +1344,27 @@ export function createBotWorkspaceScope(
1152
1344
  }
1153
1345
  const generation = workspaces.generationFor(botId);
1154
1346
  const agentPreset = workspaces.agentPresetFor(botId);
1347
+ const model = workspaces.modelFor(botId);
1155
1348
  const sessionId = await target.createSession({
1156
1349
  ...options,
1157
1350
  workspace: workspaces.workspaceFor(botId),
1158
1351
  ...(agentPreset == null ? {} : { agentPreset }),
1159
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
+ }
1160
1368
  sessionGenerations.set(sessionId, generation);
1161
1369
  return sessionId;
1162
1370
  };
@@ -1318,7 +1526,12 @@ export function createBotScopedHarness(harness, options) {
1318
1526
  return createBotWorkspaceScope(harness, options).harness;
1319
1527
  }
1320
1528
 
1321
- export function createWorkspaceAwareController(controller, { workspaces, stateFor, agentPresetCatalog } = {}) {
1529
+ export function createWorkspaceAwareController(controller, {
1530
+ workspaces,
1531
+ stateFor,
1532
+ agentPresetCatalog,
1533
+ modelCatalog,
1534
+ } = {}) {
1322
1535
  if (!controller || !workspaces || typeof stateFor !== 'function') {
1323
1536
  throw new TypeError('controller, workspaces, and stateFor are required');
1324
1537
  }
@@ -1331,7 +1544,12 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1331
1544
  if (transitions.get(botId) === current) transitions.delete(botId);
1332
1545
  });
1333
1546
  };
1334
- const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog);
1547
+ const decorate = (value) => decorateResult(
1548
+ workspaces,
1549
+ value,
1550
+ agentPresetCatalog,
1551
+ modelCatalog,
1552
+ );
1335
1553
  const updateWorkspace = (botId, workspace) => {
1336
1554
  // Capture at API invocation, before even waiting for an older outer
1337
1555
  // transition. A queued request still belongs to the incarnation that the
@@ -1374,6 +1592,32 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1374
1592
  workspaces,
1375
1593
  await controller.status(),
1376
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,
1377
1621
  );
1378
1622
  });
1379
1623
  };
@@ -1387,13 +1631,17 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1387
1631
  error.code = 'workspace-bot-not-found';
1388
1632
  throw error;
1389
1633
  }
1390
- const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
1634
+ const [catalog, models] = await Promise.all([
1635
+ resolveAgentPresetCatalog(agentPresetCatalog),
1636
+ resolveModelCatalog(modelCatalog),
1637
+ ]);
1391
1638
  const decorated = workspaces.decorateStatus(snapshot);
1392
1639
  const updated = {
1393
1640
  ...decorated,
1394
1641
  bots: decorated.bots.map((bot) => bot?.botId === botId
1395
1642
  ? { ...bot, contextEnhancement: config } : bot),
1396
1643
  ...(catalog ? { agentPresetCatalog: catalog } : {}),
1644
+ ...(models ? { modelCatalog: models } : {}),
1397
1645
  };
1398
1646
  // QR/status projection can fail too. Prepare the complete response before
1399
1647
  // commit so a failed save never publishes new running settings.
@@ -1412,13 +1660,17 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1412
1660
  error.code = 'workspace-bot-not-found';
1413
1661
  throw error;
1414
1662
  }
1415
- const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
1663
+ const [catalog, models] = await Promise.all([
1664
+ resolveAgentPresetCatalog(agentPresetCatalog),
1665
+ resolveModelCatalog(modelCatalog),
1666
+ ]);
1416
1667
  const decorated = workspaces.decorateStatus(snapshot);
1417
1668
  const updated = {
1418
1669
  ...decorated,
1419
1670
  bots: decorated.bots.map((bot) => bot?.botId === botId
1420
1671
  ? { ...bot, accessPolicy: policy } : bot),
1421
1672
  ...(catalog ? { agentPresetCatalog: catalog } : {}),
1673
+ ...(models ? { modelCatalog: models } : {}),
1422
1674
  };
1423
1675
  // Prepare the complete channel-specific response before commit. Failed
1424
1676
  // projections and disk writes must leave the live policy unchanged.
@@ -1466,6 +1718,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1466
1718
  get(target, property) {
1467
1719
  if (property === 'updateWorkspace') return updateWorkspace;
1468
1720
  if (property === 'updateAgentPreset') return updateAgentPreset;
1721
+ if (property === 'updateModel') return updateModel;
1469
1722
  if (property === 'updateContextEnhancement') return updateContextEnhancement;
1470
1723
  if (property === 'updateAccessPolicy') return updateAccessPolicy;
1471
1724
  const value = Reflect.get(target, property, target);