@xmanrui/dsh-im 0.17.0 → 0.18.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 (49) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +964 -538
  4. package/lib/index.js +189 -144
  5. package/package.json +1 -1
  6. package/plugin-src/client/agent-preset.js +110 -0
  7. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +48 -2
  9. package/plugin-src/client/channels/feishu/api.js +5 -0
  10. package/plugin-src/client/channels/feishu/index.js +39 -2
  11. package/plugin-src/client/channels/qq/api.js +5 -0
  12. package/plugin-src/client/channels/qq/index.js +37 -6
  13. package/plugin-src/client/channels/shared/token-api.js +5 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  15. package/plugin-src/client/channels/wecom/api.js +5 -0
  16. package/plugin-src/client/channels/wecom/index.js +37 -6
  17. package/plugin-src/client/channels/weixin/api.js +15 -0
  18. package/plugin-src/client/channels/weixin/index.js +54 -4
  19. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  21. package/plugin-src/client/i18n.js +29 -0
  22. package/plugin-src/client/styles.js +8 -0
  23. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  24. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  25. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  26. package/plugin-src/host/channels/feishu/production.mjs +6 -3
  27. package/plugin-src/host/channels/feishu/rpc.mjs +17 -0
  28. package/plugin-src/host/channels/qq/production.mjs +10 -4
  29. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  30. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  31. package/plugin-src/host/channels/shared/production.mjs +10 -4
  32. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  33. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  34. package/plugin-src/host/channels/slack/production.mjs +10 -4
  35. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  36. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  37. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  38. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  39. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  40. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  41. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  42. package/src/channels/discord/discord-api.mjs +1 -1
  43. package/src/channels/shared/agent-preset.mjs +74 -0
  44. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  45. package/src/channels/shared/harness-client.mjs +6 -4
  46. package/src/channels/shared/image-prompt.mjs +32 -1
  47. package/src/channels/weixin/weixin-api.mjs +1 -1
  48. package/src/channels/weixin/weixin-bridge.mjs +23 -3
  49. package/src/channels/weixin/weixin-controller.mjs +15 -0
@@ -5,6 +5,10 @@ import {
5
5
  SET_WORKSPACE_ENDPOINT,
6
6
  validWorkspacePayload,
7
7
  } from '../shared/workspace-rpc.mjs';
8
+ import {
9
+ SET_AGENT_PRESET_ENDPOINT,
10
+ validAgentPresetPayload,
11
+ } from '../shared/agent-preset-rpc.mjs';
8
12
  import {
9
13
  connectionTestTargetUnavailable,
10
14
  publicConnectionTestResult,
@@ -20,6 +24,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
20
24
  reconnectBot: 'bot.reconnect',
21
25
  deleteBot: 'bot.delete',
22
26
  setWorkspace: SET_WORKSPACE_ENDPOINT,
27
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
23
28
  });
24
29
  export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
25
30
 
@@ -74,6 +79,10 @@ function payloadFailure(endpoint, payload) {
74
79
  return validWorkspacePayload(payload)
75
80
  ? null : '请输入工作区绝对路径。';
76
81
  }
82
+ if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
83
+ return validAgentPresetPayload(payload)
84
+ ? null : '请选择 Agent Preset。';
85
+ }
77
86
  return 'Unknown Weixin endpoint.';
78
87
  }
79
88
 
@@ -198,6 +207,12 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
198
207
  await controller.updateWorkspace(payload.botId, payload.workspace),
199
208
  cachedEncode,
200
209
  );
210
+ } else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
211
+ if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
212
+ value = await publicStatus(
213
+ await controller.updateAgentPreset(payload.botId, payload.agentPreset),
214
+ cachedEncode,
215
+ );
201
216
  } else {
202
217
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
203
218
  }
@@ -14,6 +14,7 @@ import {
14
14
  createWorkspaceAwareController,
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
+ import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
17
18
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
18
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
19
20
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
@@ -64,7 +65,9 @@ export async function createProductionController(ctx, config = {}, internals = {
64
65
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
65
66
  const configuredBots = configStore.list();
66
67
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
67
- await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
68
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
69
+ defaultAgentPreset: config.agentPreset,
70
+ })));
68
71
  const observedConfigStore = typeof configStore.remove === 'function'
69
72
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
70
73
  : configStore;
@@ -86,7 +89,6 @@ export async function createProductionController(ctx, config = {}, internals = {
86
89
  const harness = new Harness({
87
90
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
88
91
  workspace: defaultWorkspace,
89
- ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
90
92
  autostart: false,
91
93
  dshBin: config.dshBin ?? 'dsh',
92
94
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -100,7 +102,7 @@ export async function createProductionController(ctx, config = {}, internals = {
100
102
  logger,
101
103
  createRuntime: async ({ botId, config: botConfig, authDir }) => {
102
104
  const state = await stateFor(botId);
103
- await workspaces.ensure(botId);
105
+ await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
104
106
  const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
105
107
  return new Runtime({
106
108
  config: botConfig,
@@ -136,7 +138,11 @@ export async function createProductionController(ctx, config = {}, internals = {
136
138
  }
137
139
  },
138
140
  });
139
- const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
141
+ const controller = createWorkspaceAwareController(coreController, {
142
+ workspaces,
143
+ stateFor,
144
+ agentPresetCatalog: () => listAgentPresetCatalog(ctx),
145
+ });
140
146
  const supervisor = createSupervisor({
141
147
  channel: 'whatsapp',
142
148
  controller,
@@ -3,6 +3,7 @@ import QRCode from 'qrcode';
3
3
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
4
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
5
5
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
6
+ import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
6
7
 
7
8
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
8
9
  export const WHATSAPP_ENDPOINTS = Object.freeze({
@@ -13,6 +14,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
13
14
  reconnectBot: 'bot.reconnect',
14
15
  deleteBot: 'bot.delete',
15
16
  setWorkspace: SET_WORKSPACE_ENDPOINT,
17
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
16
18
  });
17
19
  export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
18
20
 
@@ -52,6 +54,10 @@ function payloadFailure(endpoint, payload) {
52
54
  return validWorkspacePayload(payload)
53
55
  ? null : '请输入工作区绝对路径。';
54
56
  }
57
+ if (endpoint === WHATSAPP_ENDPOINTS.setAgentPreset) {
58
+ return validAgentPresetPayload(payload)
59
+ ? null : '请选择 Agent Preset。';
60
+ }
55
61
  return 'Unknown WhatsApp endpoint.';
56
62
  }
57
63
 
@@ -154,6 +160,12 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
154
160
  await controller.updateWorkspace(payload.botId, payload.workspace),
155
161
  cachedEncode,
156
162
  );
163
+ } else if (endpoint === WHATSAPP_ENDPOINTS.setAgentPreset) {
164
+ if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
165
+ value = await publicStatus(
166
+ await controller.updateAgentPreset(payload.botId, payload.agentPreset),
167
+ cachedEncode,
168
+ );
157
169
  } else {
158
170
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
159
171
  }
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.18.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),
@@ -0,0 +1,74 @@
1
+ /** Matches DeepSeek Harness agent-preset directory ids. */
2
+ export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
3
+
4
+ export const EMPTY_AGENT_PRESET_CATALOG = Object.freeze({
5
+ defaultId: '',
6
+ items: Object.freeze([]),
7
+ });
8
+
9
+ export function normalizeAgentPresetId(value) {
10
+ if (value == null) return null;
11
+ if (typeof value !== 'string') return null;
12
+ const id = value.trim();
13
+ return AGENT_PRESET_ID.test(id) ? id : null;
14
+ }
15
+
16
+ export function validateAgentPresetId(value) {
17
+ if (value == null || value === '') return null;
18
+ const id = normalizeAgentPresetId(value);
19
+ if (!id) {
20
+ const error = new Error('Agent Preset 无效。');
21
+ error.code = 'agent-preset-invalid';
22
+ throw error;
23
+ }
24
+ return id;
25
+ }
26
+
27
+ function catalogItem(value) {
28
+ if (typeof value === 'string') {
29
+ const id = normalizeAgentPresetId(value);
30
+ return id ? { id, label: id } : null;
31
+ }
32
+ if (!value || typeof value !== 'object') return null;
33
+ if (value.broken !== undefined) return null;
34
+ const id = normalizeAgentPresetId(value.id);
35
+ if (!id) return null;
36
+ const label = typeof value.name === 'string' && value.name.trim()
37
+ ? value.name.trim().slice(0, 128)
38
+ : typeof value.label === 'string' && value.label.trim()
39
+ ? value.label.trim().slice(0, 128)
40
+ : id;
41
+ return { id, label };
42
+ }
43
+
44
+ export function normalizeAgentPresetCatalog(value) {
45
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
46
+ return { defaultId: '', items: [] };
47
+ }
48
+ const items = [];
49
+ const seen = new Set();
50
+ for (const entry of Array.isArray(value.items) ? value.items : []) {
51
+ const item = catalogItem(entry);
52
+ if (!item || seen.has(item.id)) continue;
53
+ seen.add(item.id);
54
+ items.push(item);
55
+ }
56
+ return {
57
+ defaultId: normalizeAgentPresetId(value.defaultId) ?? '',
58
+ items,
59
+ };
60
+ }
61
+
62
+ export async function listAgentPresetCatalog(ctx) {
63
+ try {
64
+ const service = typeof ctx?.get === 'function' ? ctx.get('agentPresets') : ctx?.agentPresets;
65
+ if (!service || typeof service.list !== 'function') return { defaultId: '', items: [] };
66
+ const listed = await service.list();
67
+ return normalizeAgentPresetCatalog({
68
+ defaultId: typeof service.defaultId === 'string' ? service.defaultId : '',
69
+ items: Array.isArray(listed) ? listed : [],
70
+ });
71
+ } catch {
72
+ return { defaultId: '', items: [] };
73
+ }
74
+ }
@@ -9,6 +9,10 @@ import {
9
9
  } from 'node:fs/promises';
10
10
  import { dirname, isAbsolute, resolve } from 'node:path';
11
11
 
12
+ import {
13
+ normalizeAgentPresetCatalog,
14
+ validateAgentPresetId,
15
+ } from './agent-preset.mjs';
12
16
  import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
13
17
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
14
18
 
@@ -49,7 +53,22 @@ function normalizeDocument(value) {
49
53
  || typeof workspace !== 'string' || !isAbsolute(workspace)) return null;
50
54
  workspaces[botId] = resolve(workspace);
51
55
  }
52
- return { version: 1, workspaces };
56
+ let agentPresets = {};
57
+ if (value.agentPresets !== undefined) {
58
+ if (!value.agentPresets || typeof value.agentPresets !== 'object'
59
+ || Array.isArray(value.agentPresets)) return null;
60
+ for (const [botId, agentPreset] of Object.entries(value.agentPresets)) {
61
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return null;
62
+ try {
63
+ const normalized = validateAgentPresetId(agentPreset);
64
+ if (!normalized) return null;
65
+ agentPresets[botId] = normalized;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+ return { version: 1, workspaces, agentPresets };
53
72
  }
54
73
 
55
74
  export async function validateWorkspacePath(value) {
@@ -79,6 +98,7 @@ export class BotWorkspaceStore {
79
98
  #path;
80
99
  #defaultWorkspace;
81
100
  #workspaces = {};
101
+ #agentPresets = {};
82
102
  #generations = new Map();
83
103
  #nextGeneration = 1;
84
104
  #incarnations = new Map();
@@ -100,9 +120,11 @@ export class BotWorkspaceStore {
100
120
  const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
101
121
  if (!normalized) throw new Error('dsh-im workspace config is invalid');
102
122
  this.#workspaces = normalized.workspaces;
123
+ this.#agentPresets = normalized.agentPresets;
103
124
  } catch (error) {
104
125
  if (error?.code !== 'ENOENT') throw error;
105
126
  this.#workspaces = {};
127
+ this.#agentPresets = {};
106
128
  }
107
129
  this.#generations.clear();
108
130
  this.#nextGeneration = 1;
@@ -130,6 +152,10 @@ export class BotWorkspaceStore {
130
152
  return this.#workspaces[botIdOf(botId)] ?? this.#defaultWorkspace;
131
153
  }
132
154
 
155
+ agentPresetFor(botId) {
156
+ return this.#agentPresets[botIdOf(botId)] ?? null;
157
+ }
158
+
133
159
  generationFor(botId) {
134
160
  return this.#generations.get(botIdOf(botId)) ?? null;
135
161
  }
@@ -148,18 +174,24 @@ export class BotWorkspaceStore {
148
174
  }
149
175
  }
150
176
 
151
- async ensure(botId, { workspace = this.#defaultWorkspace } = {}) {
177
+ async ensure(botId, { workspace = this.#defaultWorkspace, defaultAgentPreset } = {}) {
152
178
  const id = botIdOf(botId);
153
179
  const initialWorkspace = resolve(workspace);
154
180
  return this.#enqueue(id, async () => {
155
181
  if (!this.#workspaces[id]) {
182
+ const agentPreset = validateAgentPresetId(defaultAgentPreset);
183
+ const hadAgentPreset = Object.hasOwn(this.#agentPresets, id);
184
+ const previousAgentPreset = this.#agentPresets[id];
156
185
  this.#workspaces[id] = initialWorkspace;
186
+ if (agentPreset) this.#agentPresets[id] = agentPreset;
157
187
  this.#generations.set(id, this.#freshGeneration());
158
188
  this.#incarnations.set(id, this.#freshIncarnation());
159
189
  try {
160
190
  await this.#persist();
161
191
  } catch (error) {
162
192
  delete this.#workspaces[id];
193
+ if (hadAgentPreset) this.#agentPresets[id] = previousAgentPreset;
194
+ else delete this.#agentPresets[id];
163
195
  this.#generations.delete(id);
164
196
  this.#incarnations.delete(id);
165
197
  throw error;
@@ -207,6 +239,37 @@ export class BotWorkspaceStore {
207
239
  });
208
240
  }
209
241
 
242
+ async setAgentPreset(botId, value, { incarnation } = {}) {
243
+ const id = botIdOf(botId);
244
+ if (!this.has(id)
245
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
246
+ const error = new Error('找不到要修改的机器人。');
247
+ error.code = 'workspace-bot-not-found';
248
+ throw error;
249
+ }
250
+ const agentPreset = validateAgentPresetId(value);
251
+ return this.#enqueue(id, async () => {
252
+ if (!this.has(id)
253
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
254
+ const error = new Error('找不到要修改的机器人。');
255
+ error.code = 'workspace-bot-not-found';
256
+ throw error;
257
+ }
258
+ const previous = this.#agentPresets[id] ?? null;
259
+ if (previous === agentPreset) return agentPreset;
260
+ if (agentPreset) this.#agentPresets[id] = agentPreset;
261
+ else delete this.#agentPresets[id];
262
+ try {
263
+ await this.#persist();
264
+ } catch (error) {
265
+ if (previous) this.#agentPresets[id] = previous;
266
+ else delete this.#agentPresets[id];
267
+ throw error;
268
+ }
269
+ return agentPreset;
270
+ });
271
+ }
272
+
210
273
  async bindWorkspaceSession(botId, value, {
211
274
  conversationKey,
212
275
  sessionId,
@@ -353,7 +416,11 @@ export class BotWorkspaceStore {
353
416
 
354
417
  async reconcile(activeBotIds) {
355
418
  const active = new Set([...activeBotIds].map(botIdOf));
356
- const candidates = new Set([...Object.keys(this.#workspaces), ...this.#dirtyRemovals]);
419
+ const candidates = new Set([
420
+ ...Object.keys(this.#workspaces),
421
+ ...Object.keys(this.#agentPresets),
422
+ ...this.#dirtyRemovals,
423
+ ]);
357
424
  for (const botId of candidates) {
358
425
  if (!active.has(botId)) await this.remove(botId);
359
426
  }
@@ -364,7 +431,11 @@ export class BotWorkspaceStore {
364
431
  return {
365
432
  ...status,
366
433
  bots: status.bots.map((bot) => bot?.botId
367
- ? { ...bot, workspace: this.workspaceFor(bot.botId) }
434
+ ? {
435
+ ...bot,
436
+ workspace: this.workspaceFor(bot.botId),
437
+ agentPreset: this.agentPresetFor(bot.botId),
438
+ }
368
439
  : bot),
369
440
  };
370
441
  }
@@ -392,8 +463,10 @@ export class BotWorkspaceStore {
392
463
 
393
464
  async #retireCurrentIncarnation(id) {
394
465
  const hadWorkspace = Object.hasOwn(this.#workspaces, id);
395
- const needsCleanup = hadWorkspace || this.#dirtyRemovals.has(id);
466
+ const hadPreset = Object.hasOwn(this.#agentPresets, id);
467
+ const needsCleanup = hadWorkspace || hadPreset || this.#dirtyRemovals.has(id);
396
468
  delete this.#workspaces[id];
469
+ delete this.#agentPresets[id];
397
470
  this.#generations.delete(id);
398
471
  this.#incarnations.delete(id);
399
472
  if (!needsCleanup) return {
@@ -425,6 +498,9 @@ export class BotWorkspaceStore {
425
498
 
426
499
  async #persist() {
427
500
  const document = { version: 1, workspaces: this.#workspaces };
501
+ if (Object.keys(this.#agentPresets).length > 0) {
502
+ document.agentPresets = this.#agentPresets;
503
+ }
428
504
  await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
429
505
  const temporary = `${this.#path}.tmp`;
430
506
  await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
@@ -436,7 +512,8 @@ export class BotWorkspaceStore {
436
512
  }
437
513
 
438
514
  async #persistCurrentDocument() {
439
- if (Object.keys(this.#workspaces).length > 0) {
515
+ if (Object.keys(this.#workspaces).length > 0
516
+ || Object.keys(this.#agentPresets).length > 0) {
440
517
  await this.#persist();
441
518
  return;
442
519
  }
@@ -450,10 +527,29 @@ export class BotWorkspaceStore {
450
527
  }
451
528
  }
452
529
 
453
- function decorateResult(workspaces, result) {
530
+ function resolveAgentPresetCatalog(catalog) {
531
+ if (!catalog) return null;
532
+ const value = typeof catalog === 'function' ? catalog() : catalog;
533
+ return value && typeof value.then === 'function'
534
+ ? value.then(normalizeAgentPresetCatalog)
535
+ : normalizeAgentPresetCatalog(value);
536
+ }
537
+
538
+ function decorateResult(workspaces, result, catalog) {
539
+ const decorate = (value) => {
540
+ const decorated = workspaces.decorateStatus(value);
541
+ if (!catalog || !decorated || typeof decorated !== 'object') return decorated;
542
+ const attachCatalog = (agentPresetCatalog) => (
543
+ agentPresetCatalog ? { ...decorated, agentPresetCatalog } : decorated
544
+ );
545
+ const agentPresetCatalog = resolveAgentPresetCatalog(catalog);
546
+ return agentPresetCatalog && typeof agentPresetCatalog.then === 'function'
547
+ ? agentPresetCatalog.then(attachCatalog)
548
+ : attachCatalog(agentPresetCatalog);
549
+ };
454
550
  return result && typeof result.then === 'function'
455
- ? result.then((value) => workspaces.decorateStatus(value))
456
- : workspaces.decorateStatus(result);
551
+ ? result.then(decorate)
552
+ : decorate(result);
457
553
  }
458
554
 
459
555
  function targetStatus(controller) {
@@ -608,9 +704,11 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
608
704
  throw error;
609
705
  }
610
706
  const generation = workspaces.generationFor(botId);
707
+ const agentPreset = workspaces.agentPresetFor(botId);
611
708
  const sessionId = await target.createSession({
612
709
  ...options,
613
710
  workspace: workspaces.workspaceFor(botId),
711
+ ...(agentPreset == null ? {} : { agentPreset }),
614
712
  });
615
713
  sessionGenerations.set(sessionId, generation);
616
714
  return sessionId;
@@ -765,7 +863,7 @@ export function createBotScopedHarness(harness, options) {
765
863
  return createBotWorkspaceScope(harness, options).harness;
766
864
  }
767
865
 
768
- export function createWorkspaceAwareController(controller, { workspaces, stateFor }) {
866
+ export function createWorkspaceAwareController(controller, { workspaces, stateFor, agentPresetCatalog } = {}) {
769
867
  if (!controller || !workspaces || typeof stateFor !== 'function') {
770
868
  throw new TypeError('controller, workspaces, and stateFor are required');
771
869
  }
@@ -778,6 +876,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
778
876
  if (transitions.get(botId) === current) transitions.delete(botId);
779
877
  });
780
878
  };
879
+ const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog);
781
880
  const updateWorkspace = (botId, workspace) => {
782
881
  // Capture at API invocation, before even waiting for an older outer
783
882
  // transition. A queued request still belongs to the incarnation that the
@@ -795,7 +894,34 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
795
894
  clearSessions: () => state.clearSessions(),
796
895
  incarnation,
797
896
  });
798
- return workspaces.decorateStatus(await controller.status());
897
+ return decorate(await controller.status());
898
+ });
899
+ };
900
+ const updateAgentPreset = (botId, agentPreset) => {
901
+ const incarnation = workspaces.incarnationFor(botId);
902
+ const normalizedAgentPreset = validateAgentPresetId(agentPreset);
903
+ return withBotTransition(botId, async () => {
904
+ const snapshot = await controller.status();
905
+ if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
906
+ const error = new Error('找不到要修改的机器人。');
907
+ error.code = 'workspace-bot-not-found';
908
+ throw error;
909
+ }
910
+ const catalog = normalizedAgentPreset && agentPresetCatalog
911
+ ? await resolveAgentPresetCatalog(agentPresetCatalog)
912
+ : null;
913
+ if (normalizedAgentPreset && agentPresetCatalog
914
+ && !catalog?.items.some((item) => item.id === normalizedAgentPreset)) {
915
+ const error = new Error('Agent Preset 不存在或不可用。');
916
+ error.code = 'agent-preset-unavailable';
917
+ throw error;
918
+ }
919
+ await workspaces.setAgentPreset(botId, normalizedAgentPreset, { incarnation });
920
+ return decorateResult(
921
+ workspaces,
922
+ await controller.status(),
923
+ catalog ?? agentPresetCatalog,
924
+ );
799
925
  });
800
926
  };
801
927
  const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
@@ -822,7 +948,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
822
948
  try {
823
949
  const result = await invokeDelete();
824
950
  await workspaces.finishRemoval(removal);
825
- return workspaces.decorateStatus(result);
951
+ return decorate(result);
826
952
  } catch (error) {
827
953
  const after = await targetStatus(controller).catch(() => null);
828
954
  const knownAbsent = Array.isArray(after?.bots)
@@ -836,6 +962,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
836
962
  return new Proxy(controller, {
837
963
  get(target, property) {
838
964
  if (property === 'updateWorkspace') return updateWorkspace;
965
+ if (property === 'updateAgentPreset') return updateAgentPreset;
839
966
  const value = Reflect.get(target, property, target);
840
967
  if (typeof value !== 'function') return value;
841
968
  if (property === 'deleteBot') {
@@ -848,11 +975,11 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
848
975
  return async (...args) => {
849
976
  const before = await target.status();
850
977
  const botId = before?.bots?.[0]?.botId;
851
- if (!botId) return decorateResult(workspaces, value.apply(target, args));
978
+ if (!botId) return decorate(value.apply(target, args));
852
979
  return deleteWithWorkspace(botId, () => value.apply(target, args));
853
980
  };
854
981
  }
855
- return (...args) => decorateResult(workspaces, value.apply(target, args));
982
+ return (...args) => decorate(value.apply(target, args));
856
983
  },
857
984
  });
858
985
  }
@@ -592,11 +592,13 @@ export class HarnessClient {
592
592
  }
593
593
 
594
594
  async createSession(options = {}) {
595
- await this.ensureRunning(options);
596
- const workspaceId = await this.workspaceId(options);
595
+ const { agentPreset: requestedPreset, ...rpcOptions } = options;
596
+ await this.ensureRunning(rpcOptions);
597
+ const workspaceId = await this.workspaceId(rpcOptions);
597
598
  const payload = { workspaceId };
598
- if (this.#agentPreset !== undefined) payload.agentPreset = this.#agentPreset;
599
- const created = await this.rpc('session.create', payload, 30_000, options);
599
+ const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
600
+ if (agentPreset != null) payload.agentPreset = agentPreset;
601
+ const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
600
602
  return created.sessionId;
601
603
  }
602
604
 
@@ -13,6 +13,18 @@ export class ImagePromptError extends Error {
13
13
  }
14
14
  }
15
15
 
16
+ const HOST_ATTACHMENT_USER_MESSAGES = Object.freeze({
17
+ MODEL_DOES_NOT_SUPPORT_IMAGES:
18
+ '当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
19
+ IMAGE_TOO_LARGE: '图片超过宿主允许的大小,请压缩后重试。',
20
+ IMAGE_TOO_MANY_PIXELS: '图片分辨率过高,请压缩后重试。',
21
+ INVALID_IMAGE: '图片内容无效或格式不受支持,请重新发送。',
22
+ INVALID_IMAGE_BASE64: '未能读取图片内容,请重新发送。',
23
+ IMAGE_TYPE_MISMATCH: '图片格式与实际内容不一致,请重新发送。',
24
+ TOO_MANY_IMAGES: '一次发送的图片数量超过宿主限制,请减少后重试。',
25
+ IMAGES_TOO_LARGE: '图片总大小超过宿主限制,请减少图片或压缩后重试。',
26
+ });
27
+
16
28
  function requestSignal(signal, timeoutMs) {
17
29
  const timeout = AbortSignal.timeout(timeoutMs);
18
30
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
@@ -263,6 +275,25 @@ export async function promptContentForMessage(message, {
263
275
  return content;
264
276
  }
265
277
 
278
+ /** Return only allowlisted, user-safe image failure details. */
279
+ export function imagePromptDiagnostic(error) {
280
+ if (error instanceof ImagePromptError) {
281
+ return {
282
+ code: 'image-prompt-error',
283
+ reason: error.code,
284
+ userMessage: error.userMessage,
285
+ };
286
+ }
287
+ if (error?.code !== 'attachment-error' || typeof error?.details?.reason !== 'string') {
288
+ return null;
289
+ }
290
+ const reason = error.details.reason;
291
+ const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
292
+ ? HOST_ATTACHMENT_USER_MESSAGES[reason]
293
+ : null;
294
+ return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
295
+ }
296
+
266
297
  export function imagePromptUserMessage(error) {
267
- return error instanceof ImagePromptError ? error.userMessage : null;
298
+ return imagePromptDiagnostic(error)?.userMessage ?? null;
268
299
  }
@@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
184
184
  function baseInfo() {
185
185
  return {
186
186
  channel_version: WEIXIN_PROTOCOL_VERSION,
187
- bot_agent: 'DeepSeekHarness/0.17.0',
187
+ bot_agent: 'DeepSeekHarness/0.18.0',
188
188
  };
189
189
  }
190
190