@xmanrui/dsh-im 0.17.1 → 0.19.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/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +1297 -600
  4. package/lib/index.js +190 -145
  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 +28 -5
  10. package/plugin-src/client/channels/feishu/index.js +330 -41
  11. package/plugin-src/client/channels/feishu/styles.js +46 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +37 -6
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  16. package/plugin-src/client/channels/wecom/api.js +5 -0
  17. package/plugin-src/client/channels/wecom/index.js +37 -6
  18. package/plugin-src/client/channels/weixin/api.js +5 -0
  19. package/plugin-src/client/channels/weixin/index.js +50 -4
  20. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  22. package/plugin-src/client/i18n.js +59 -0
  23. package/plugin-src/client/styles.js +8 -0
  24. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  26. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  27. package/plugin-src/host/channels/feishu/production.mjs +8 -3
  28. package/plugin-src/host/channels/feishu/rpc.mjs +78 -7
  29. package/plugin-src/host/channels/qq/production.mjs +10 -4
  30. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  31. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/shared/production.mjs +10 -4
  33. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  34. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  35. package/plugin-src/host/channels/slack/production.mjs +10 -4
  36. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  37. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  38. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  39. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  40. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  41. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  42. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  43. package/src/channels/discord/discord-api.mjs +1 -1
  44. package/src/channels/feishu/bridge.mjs +29 -4
  45. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  46. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  47. package/src/channels/feishu/group-response-mode.mjs +15 -0
  48. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  49. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  50. package/src/channels/feishu/repair-manager.mjs +17 -3
  51. package/src/channels/shared/agent-preset.mjs +74 -0
  52. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  53. package/src/channels/shared/harness-client.mjs +6 -4
  54. package/src/channels/telegram/telegram-api.mjs +31 -0
  55. package/src/channels/telegram/telegram-runtime.mjs +25 -1
  56. package/src/channels/weixin/weixin-api.mjs +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+
3
+ import { h } from './i18n.js';
4
+
5
+ export const SET_AGENT_PRESET_ENDPOINT = 'bot.preset.set';
6
+
7
+ const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
8
+
9
+ export const EMPTY_AGENT_PRESET_CATALOG = Object.freeze({
10
+ defaultId: '',
11
+ items: Object.freeze([]),
12
+ });
13
+
14
+ export const AgentPresetCatalogContext = React.createContext(EMPTY_AGENT_PRESET_CATALOG);
15
+
16
+ export function normalizeAgentPresetId(value) {
17
+ if (typeof value !== 'string') return '';
18
+ const id = value.trim();
19
+ return PRESET_ID.test(id) ? id : '';
20
+ }
21
+
22
+ export function normalizeAgentPresetCatalog(value) {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
24
+ return { defaultId: '', items: [] };
25
+ }
26
+ const items = [];
27
+ const seen = new Set();
28
+ for (const entry of Array.isArray(value.items) ? value.items : []) {
29
+ const id = typeof entry === 'string'
30
+ ? normalizeAgentPresetId(entry)
31
+ : normalizeAgentPresetId(entry?.id);
32
+ if (!id || seen.has(id)) continue;
33
+ seen.add(id);
34
+ const label = typeof entry?.label === 'string' && entry.label.trim()
35
+ ? entry.label.trim().slice(0, 128)
36
+ : typeof entry?.name === 'string' && entry.name.trim()
37
+ ? entry.name.trim().slice(0, 128)
38
+ : id;
39
+ items.push({ id, label });
40
+ }
41
+ return {
42
+ defaultId: normalizeAgentPresetId(value.defaultId),
43
+ items,
44
+ };
45
+ }
46
+
47
+ export function AgentPresetEditor({ agentPreset = '', disabled = false, onSave }) {
48
+ const catalog = React.useContext(AgentPresetCatalogContext) ?? EMPTY_AGENT_PRESET_CATALOG;
49
+ const current = normalizeAgentPresetId(agentPreset);
50
+ const [saving, setSaving] = React.useState(false);
51
+ const [error, setError] = React.useState(null);
52
+
53
+ const items = [];
54
+ const seen = new Set();
55
+ for (const item of Array.isArray(catalog.items) ? catalog.items : []) {
56
+ if (!item?.id || seen.has(item.id)) continue;
57
+ seen.add(item.id);
58
+ items.push(item);
59
+ }
60
+ const currentUnavailable = Boolean(current && !seen.has(current));
61
+ if (currentUnavailable) items.push({ id: current, label: current, unavailable: true });
62
+
63
+ const inheritLabel = '跟随 Host 默认';
64
+
65
+ const change = async (event) => {
66
+ const next = event.target.value;
67
+ if (next === current || saving || disabled) return;
68
+ setSaving(true);
69
+ setError(null);
70
+ try {
71
+ await onSave?.(next || null);
72
+ } catch (cause) {
73
+ setError(cause?.message ?? 'Agent Preset 修改失败,请重试。');
74
+ } finally {
75
+ setSaving(false);
76
+ }
77
+ };
78
+
79
+ return h('div', { className: 'dim-preset' },
80
+ h('div', { className: 'dim-presetHeader' },
81
+ h('span', null, 'Agent Preset'),
82
+ saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
83
+ React.createElement('select', {
84
+ className: 'dim-presetSelect',
85
+ value: current,
86
+ disabled: disabled || saving,
87
+ 'aria-label': 'Agent Preset',
88
+ onChange: (event) => { void change(event); },
89
+ },
90
+ h('option', { value: '' }, inheritLabel),
91
+ ...items.map((item) => h(
92
+ 'option',
93
+ { key: item.id, value: item.id },
94
+ item.unavailable
95
+ ? [item.id, '(已不可用)']
96
+ : item.label && item.label !== item.id ? `${item.label}(${item.id})` : item.id,
97
+ )),
98
+ ),
99
+ h(
100
+ 'small',
101
+ { className: 'dim-presetHelp' },
102
+ '只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。',
103
+ ),
104
+ error || currentUnavailable ? h(
105
+ 'p',
106
+ { className: 'dim-presetError', role: error ? 'alert' : 'status' },
107
+ error ?? '当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。',
108
+ ) : null,
109
+ );
110
+ }
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
2
4
 
3
5
  export const DINGTALK_ENDPOINTS = Object.freeze({
@@ -9,6 +11,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
9
11
  reconnectBot: 'bot.reconnect',
10
12
  deleteBot: 'bot.delete',
11
13
  setWorkspace: 'bot.workspace.set',
14
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
12
15
  });
13
16
 
14
17
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@@ -157,6 +160,7 @@ function normalizeBot(value) {
157
160
  connected,
158
161
  configured: value.configured !== false,
159
162
  workspace: optionalString(value.workspace, 4_096) ?? '',
163
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
160
164
  bot: {
161
165
  name: optionalString(bot.name, 100) ?? '钉钉机器人',
162
166
  clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
@@ -200,6 +204,7 @@ export function normalizeSnapshot(value) {
200
204
  },
201
205
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
202
206
  testMessage: normalizeTestMessage(source.testMessage),
207
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
203
208
  };
204
209
  }
205
210
 
@@ -3,6 +3,11 @@ import * as React from 'react';
3
3
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
4
4
  import { h } from '../../i18n.js';
5
5
  import { WorkspaceEditor } from '../../workspace-editor.js';
6
+ import {
7
+ AgentPresetCatalogContext,
8
+ AgentPresetEditor,
9
+ EMPTY_AGENT_PRESET_CATALOG,
10
+ } from '../../agent-preset.js';
6
11
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
7
12
  import {
8
13
  DINGTALK_ENDPOINTS,
@@ -213,6 +218,7 @@ export function AccountCard({
213
218
  removing,
214
219
  onReconnect,
215
220
  onWorkspaceSave,
221
+ onAgentPresetSave,
216
222
  onRequestRemove,
217
223
  onConfirmRemove,
218
224
  onCancelRemove,
@@ -241,6 +247,11 @@ export function AccountCard({
241
247
  disabled: Boolean(busy),
242
248
  onSave: onWorkspaceSave,
243
249
  }),
250
+ h(AgentPresetEditor, {
251
+ agentPreset: account.agentPreset,
252
+ disabled: Boolean(busy),
253
+ onSave: onAgentPresetSave,
254
+ }),
244
255
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
245
256
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
246
257
  feedback ? h('div', {
@@ -272,6 +283,7 @@ function AccountList(props) {
272
283
  removing: props.removeTarget === account.botId,
273
284
  onReconnect: () => props.onReconnect(account),
274
285
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
286
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
275
287
  onRequestRemove: () => props.onRequestRemove(account),
276
288
  onConfirmRemove: () => props.onConfirmRemove(account),
277
289
  onCancelRemove: props.onCancelRemove,
@@ -283,6 +295,7 @@ const EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 });
283
295
  export function DingtalkSettingsTab({ rpcCall }) {
284
296
  const [model, setModel] = React.useState({
285
297
  phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
298
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
286
299
  });
287
300
  const [provision, setProvision] = React.useState(null);
288
301
  const [busy, setBusy] = React.useState(false);
@@ -389,6 +402,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
389
402
  totals: snapshot.totals,
390
403
  revision: snapshot.revision,
391
404
  error: null,
405
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
392
406
  });
393
407
  discardStaleFeedback(snapshot);
394
408
  if (restoreProvisioning && snapshot.provisioning) {
@@ -504,6 +518,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
504
518
  totals: snapshot.totals,
505
519
  revision: snapshot.revision,
506
520
  error: null,
521
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
507
522
  });
508
523
  discardStaleFeedback(snapshot);
509
524
  }
@@ -638,6 +653,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
638
653
  totals: snapshot.totals,
639
654
  revision: snapshot.revision,
640
655
  error: null,
656
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
641
657
  });
642
658
  discardStaleFeedback(snapshot);
643
659
  }
@@ -700,6 +716,33 @@ export function DingtalkSettingsTab({ rpcCall }) {
700
716
  totals: snapshot.totals,
701
717
  revision: snapshot.revision,
702
718
  error: null,
719
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
720
+ });
721
+ discardStaleFeedback(snapshot);
722
+ }
723
+ } finally {
724
+ const shouldRefresh = workspaceFence.endMutation();
725
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
726
+ if (mountedRef.current) setBotBusy(account.botId, null);
727
+ }
728
+ }, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
729
+
730
+ const saveAgentPreset = React.useCallback(async (account, agentPreset) => {
731
+ const snapshotVersion = workspaceFence.beginMutation();
732
+ setBotBusy(account.botId, 'preset');
733
+ try {
734
+ const snapshot = normalizeSnapshot(await invoke(
735
+ DINGTALK_ENDPOINTS.setAgentPreset,
736
+ { botId: account.botId, agentPreset },
737
+ ));
738
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
739
+ setModel({
740
+ phase: 'ready',
741
+ bots: snapshot.bots,
742
+ totals: snapshot.totals,
743
+ revision: snapshot.revision,
744
+ error: null,
745
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
703
746
  });
704
747
  discardStaleFeedback(snapshot);
705
748
  }
@@ -762,7 +805,9 @@ export function DingtalkSettingsTab({ rpcCall }) {
762
805
  })
763
806
  : null;
764
807
 
765
- return h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
808
+ return h(AgentPresetCatalogContext.Provider, {
809
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
810
+ }, h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
766
811
  h(Heading, {
767
812
  totals: model.totals,
768
813
  adding: Boolean(provision),
@@ -798,11 +843,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
798
843
  removeTarget,
799
844
  onReconnect: (account) => void reconnect(account),
800
845
  onWorkspaceSave: saveWorkspace,
846
+ onAgentPresetSave: saveAgentPreset,
801
847
  onRequestRemove: (account) => setRemoveTarget(account.botId),
802
848
  onConfirmRemove: (account) => void remove(account),
803
849
  onCancelRemove: () => setRemoveTarget(null),
804
850
  })
805
- : null));
851
+ : null)));
806
852
  }
807
853
 
808
854
  export function apply(ctx) {
@@ -6,12 +6,15 @@
6
6
  * must never be returned by any endpoint on this channel.
7
7
  */
8
8
 
9
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
10
+
9
11
  export const FEISHU_RPC_CHANNEL = "/feishu";
10
12
 
11
13
  export const FEISHU_ENDPOINTS = Object.freeze({
12
14
  status: "connection.status",
13
15
  beginProvisioning: "provision.begin",
14
16
  beginCallbackRepair: "bot.callback-repair.begin",
17
+ beginGroupMessagePermission: "bot.group-message-permission.begin",
15
18
  pollProvisioning: "provision.poll",
16
19
  cancelProvisioning: "provision.cancel",
17
20
  bindCredentials: "bot.bind-credentials",
@@ -19,6 +22,8 @@ export const FEISHU_ENDPOINTS = Object.freeze({
19
22
  disconnectBot: "bot.disconnect",
20
23
  deleteBot: "bot.delete",
21
24
  setWorkspace: "bot.workspace.set",
25
+ setAgentPreset: "bot.preset.set",
26
+ setGroupResponseMode: "bot.group-response-mode.set",
22
27
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
23
28
  testConnection: "connection.test",
24
29
  disconnect: "connection.disconnect",
@@ -27,6 +32,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
27
32
  export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
28
33
  PROVISION: "provision",
29
34
  CALLBACK_REPAIR: "callback_repair",
35
+ GROUP_MESSAGE_PERMISSION: "group_message_permission",
30
36
  });
31
37
 
32
38
  const CONNECTION_STATES = new Set([
@@ -67,6 +73,10 @@ function optionalTimestamp(value) {
67
73
  return undefined;
68
74
  }
69
75
 
76
+ export function normalizeGroupResponseMode(value) {
77
+ return value === "all" ? "all" : "mention";
78
+ }
79
+
70
80
  function clamp(value, min, max, fallback) {
71
81
  return typeof value === "number" && Number.isFinite(value)
72
82
  ? Math.min(max, Math.max(min, value))
@@ -74,9 +84,18 @@ function clamp(value, min, max, fallback) {
74
84
  }
75
85
 
76
86
  function normalizeRegistrationOperation(value) {
77
- return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
78
- ? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
79
- : FEISHU_REGISTRATION_OPERATIONS.PROVISION;
87
+ if (value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR) {
88
+ return FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
89
+ }
90
+ if (value === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION) {
91
+ return FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
92
+ }
93
+ return FEISHU_REGISTRATION_OPERATIONS.PROVISION;
94
+ }
95
+
96
+ function isTargetedAppUpdate(operation) {
97
+ return operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
98
+ || operation === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
80
99
  }
81
100
 
82
101
  export function unwrapRpcResult(result) {
@@ -109,8 +128,8 @@ export function normalizeProvisioning(value, now = Date.now()) {
109
128
  const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
110
129
  const operation = normalizeRegistrationOperation(source.operation);
111
130
  const botId = optionalString(source.botId);
112
- if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
113
- throw new Error("飞书服务返回的修复信息缺少 botId");
131
+ if (isTargetedAppUpdate(operation) && !botId) {
132
+ throw new Error("飞书服务返回的应用更新信息缺少 botId");
114
133
  }
115
134
  return {
116
135
  attemptId,
@@ -182,6 +201,9 @@ export function normalizeBotConnection(value, fallbackBotId) {
182
201
  connected,
183
202
  configured: value.configured !== false,
184
203
  workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
204
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
205
+ groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
206
+ groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
185
207
  bot: normalizeBot(value.bot),
186
208
  health: normalizeHealth(value.health, connected),
187
209
  error: normalizeError(value.error),
@@ -236,6 +258,7 @@ export function normalizeBotsSnapshot(value) {
236
258
  ? normalizeProvisioning(value.provisioning)
237
259
  : undefined,
238
260
  error: normalizeError(value.error),
261
+ agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
239
262
  };
240
263
  }
241
264