@xmanrui/dsh-im 0.17.1 → 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 (46) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +931 -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 +5 -0
  18. package/plugin-src/client/channels/weixin/index.js +50 -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 +9 -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/weixin/weixin-api.mjs +1 -1
@@ -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.1)',
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
 
@@ -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.1',
187
+ bot_agent: 'DeepSeekHarness/0.18.0',
188
188
  };
189
189
  }
190
190