@xmanrui/dsh-im 4.10.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/PROACTIVE_DELIVERY.en.md +15 -1
  2. package/PROACTIVE_DELIVERY.md +15 -1
  3. package/README.en.md +5 -1
  4. package/README.md +6 -2
  5. package/lib/client.js +1619 -769
  6. package/lib/index.js +248 -244
  7. package/package.json +4 -3
  8. package/plugin-src/client/channel-card-meta.js +9 -1
  9. package/plugin-src/client/channels/dingtalk/api.js +4 -0
  10. package/plugin-src/client/channels/dingtalk/index.js +25 -2
  11. package/plugin-src/client/channels/feishu/api.js +4 -0
  12. package/plugin-src/client/channels/feishu/group-settings.js +581 -0
  13. package/plugin-src/client/channels/feishu/index.js +26 -204
  14. package/plugin-src/client/channels/qq/api.js +4 -0
  15. package/plugin-src/client/channels/qq/index.js +25 -2
  16. package/plugin-src/client/channels/shared/token-api.js +4 -0
  17. package/plugin-src/client/channels/shared/token-channel.js +25 -3
  18. package/plugin-src/client/channels/wecom/api.js +4 -0
  19. package/plugin-src/client/channels/wecom/index.js +25 -2
  20. package/plugin-src/client/channels/weixin/api.js +4 -0
  21. package/plugin-src/client/channels/weixin/index.js +25 -2
  22. package/plugin-src/client/channels/whatsapp/api.js +4 -0
  23. package/plugin-src/client/channels/whatsapp/index.js +24 -2
  24. package/plugin-src/client/delivery-settings.js +73 -5
  25. package/plugin-src/client/i18n.js +20 -0
  26. package/plugin-src/client/model-setting.js +104 -0
  27. package/plugin-src/client/styles.js +50 -0
  28. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  29. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  31. package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
  32. package/plugin-src/host/channels/qq/production.mjs +3 -0
  33. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  34. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
  35. package/plugin-src/host/channels/shared/production.mjs +3 -0
  36. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  37. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  38. package/plugin-src/host/channels/slack/production.mjs +3 -0
  39. package/plugin-src/host/channels/slack/rpc.mjs +9 -0
  40. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  41. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  42. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  43. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  44. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  45. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  46. package/plugin-src/host/delivery-adapter.mjs +141 -3
  47. package/plugin-src/host/delivery-rpc.mjs +12 -0
  48. package/plugin-src/host/delivery-service.mjs +110 -3
  49. package/plugin-src/host/delivery-suggestions.mjs +46 -0
  50. package/plugin-src/host/harness-session-coordinator.mjs +6 -5
  51. package/plugin-src/host/index.mjs +18 -1
  52. package/plugin-src/host/session-sync-coordinator.mjs +217 -0
  53. package/scripts/verify-package.mjs +12 -6
  54. package/src/channels/shared/bot-workspace-store.mjs +285 -38
  55. package/src/channels/shared/harness-client.mjs +124 -43
  56. package/src/channels/shared/model-setting.mjs +110 -0
@@ -0,0 +1,217 @@
1
+ import {
2
+ AssistantTextAccumulator,
3
+ consumeDshImInputOrigin,
4
+ textFromHarnessContent,
5
+ } from '../../src/channels/shared/harness-client.mjs';
6
+
7
+ const DSH_USER_PREFIX = '[来自 DSH]\n';
8
+ const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
9
+
10
+ function completedTurn(reason) {
11
+ return (typeof reason === 'string' ? reason : reason?.kind) === 'completed';
12
+ }
13
+
14
+ function recipientKey(target) {
15
+ return JSON.stringify([target.channel, target.botId, target.targetId]);
16
+ }
17
+
18
+ function sessionIdOf(session) {
19
+ const value = session?.id ?? session?.sessionId;
20
+ return typeof value === 'string' && value ? value : null;
21
+ }
22
+
23
+ function userInputOrigin(scope, event) {
24
+ const source = event?.data?.source;
25
+ if (source?.kind !== 'user' || typeof source.rpcId !== 'string' || !source.rpcId) {
26
+ return 'other';
27
+ }
28
+ return consumeDshImInputOrigin(scope, source.rpcId) ? 'im' : 'dsh';
29
+ }
30
+
31
+ function validRecipient(target) {
32
+ return target && typeof target === 'object'
33
+ && typeof target.channel === 'string' && target.channel
34
+ && typeof target.botId === 'string' && target.botId
35
+ && typeof target.targetId === 'string' && target.targetId;
36
+ }
37
+
38
+ export function createSessionSyncCoordinator({ deliveryService, logger = console }) {
39
+ if (typeof deliveryService?.listSessionSyncTargets !== 'function'
40
+ || typeof deliveryService?.sendSessionSyncText !== 'function') {
41
+ throw new TypeError('Session sync requires a complete delivery service');
42
+ }
43
+
44
+ const turns = new Map();
45
+ const tails = new Map();
46
+ let closed = false;
47
+
48
+ const logFailure = (phase, target, error) => {
49
+ logger.warn?.(
50
+ `[dsh-im] ignored Session sync ${phase} failure`
51
+ + (target ? ` (${target.channel}/${target.botId}/${target.targetId})` : '')
52
+ + ` [${error?.code ?? error?.name ?? 'unknown-error'}]`,
53
+ );
54
+ };
55
+
56
+ const deliver = async (sessionId, targets, text, phase) => {
57
+ const unique = new Map();
58
+ for (const target of targets) {
59
+ if (validRecipient(target)) unique.set(recipientKey(target), target);
60
+ }
61
+ const entries = [...unique.entries()];
62
+ const results = await Promise.allSettled(entries.map(([, target]) => (
63
+ deliveryService.sendSessionSyncText(
64
+ target.botId,
65
+ target.targetId,
66
+ sessionId,
67
+ text,
68
+ )
69
+ )));
70
+ const successful = new Map();
71
+ results.forEach((result, index) => {
72
+ const [key, target] = entries[index];
73
+ if (result.status === 'fulfilled') successful.set(key, target);
74
+ else logFailure(phase, target, result.reason);
75
+ });
76
+ return successful;
77
+ };
78
+
79
+ const processEvent = async (sessionId, event, origin) => {
80
+ if (closed || !event || typeof event !== 'object') return;
81
+
82
+ if (event.type === 'turn/start') {
83
+ const turn = event.data?.turn;
84
+ if (!Number.isSafeInteger(turn)) return;
85
+ turns.set(sessionId, {
86
+ turn,
87
+ step: null,
88
+ origin: 'unknown',
89
+ recipients: null,
90
+ assistant: new AssistantTextAccumulator(),
91
+ });
92
+ return;
93
+ }
94
+
95
+ const state = turns.get(sessionId);
96
+ if (!state) return;
97
+
98
+ if (event.type === 'step/start') {
99
+ if (event.data?.turn !== undefined && event.data.turn !== state.turn) return;
100
+ state.step = Number.isSafeInteger(event.data?.step) ? event.data.step : null;
101
+ return;
102
+ }
103
+
104
+ if (event.type === 'step/end') {
105
+ if (event.data?.turn !== undefined && event.data.turn !== state.turn) return;
106
+ if (event.data?.step === undefined || event.data.step === state.step) state.step = null;
107
+ return;
108
+ }
109
+
110
+ if (event.type === 'user/message') {
111
+ if (event.surfaceOp !== 'append') return;
112
+ if (state.origin === 'unknown') state.origin = origin;
113
+ if (state.origin !== 'dsh' || origin !== 'dsh') return;
114
+ const text = textFromHarnessContent(event.data?.content);
115
+ if (!text) return;
116
+
117
+ let targets;
118
+ try {
119
+ targets = await deliveryService.listSessionSyncTargets(sessionId);
120
+ } catch (error) {
121
+ logFailure('lookup', null, error);
122
+ targets = [];
123
+ }
124
+ const successful = await deliver(
125
+ sessionId,
126
+ targets,
127
+ `${DSH_USER_PREFIX}${text}`,
128
+ 'user delivery',
129
+ );
130
+ if (state.recipients === null) {
131
+ state.recipients = successful;
132
+ } else {
133
+ for (const key of [...state.recipients.keys()]) {
134
+ if (!successful.has(key)) state.recipients.delete(key);
135
+ }
136
+ }
137
+ return;
138
+ }
139
+
140
+ if (event.type === 'assistant/message') {
141
+ if (state.origin !== 'dsh' || event.surfaceOp !== 'append'
142
+ || event.data?.interrupted === true
143
+ || (event.data?.turn !== undefined && event.data.turn !== state.turn)) return;
144
+ const text = textFromHarnessContent(event.data?.message?.content);
145
+ const step = Number.isSafeInteger(event.data?.step) ? event.data.step : state.step;
146
+ state.assistant.setCanonical(step, text);
147
+ return;
148
+ }
149
+
150
+ if (event.type !== 'turn/end' || event.data?.turn !== state.turn) return;
151
+ turns.delete(sessionId);
152
+ if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
153
+ || !state.recipients?.size || !state.assistant.text) return;
154
+ await deliver(
155
+ sessionId,
156
+ state.recipients.values(),
157
+ `${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
158
+ 'assistant delivery',
159
+ );
160
+ };
161
+
162
+ const enqueue = (sessionId, event, origin = 'other') => {
163
+ if (closed || typeof sessionId !== 'string' || !sessionId) return Promise.resolve();
164
+ const previous = tails.get(sessionId) ?? Promise.resolve();
165
+ const task = previous.then(
166
+ () => processEvent(sessionId, event, origin),
167
+ () => processEvent(sessionId, event, origin),
168
+ );
169
+ const tail = task.catch((error) => logFailure('event handling', null, error)).finally(() => {
170
+ if (tails.get(sessionId) === tail) tails.delete(sessionId);
171
+ });
172
+ tails.set(sessionId, tail);
173
+ return tail;
174
+ };
175
+
176
+ return Object.freeze({
177
+ enqueue,
178
+ async whenIdle() {
179
+ while (tails.size > 0) await Promise.allSettled([...tails.values()]);
180
+ },
181
+ close() {
182
+ closed = true;
183
+ turns.clear();
184
+ },
185
+ });
186
+ }
187
+
188
+ export function installSessionSyncCoordinator(ctx, deliveryService, {
189
+ logger = console,
190
+ inputScope = ctx?.root ?? ctx,
191
+ } = {}) {
192
+ if (typeof ctx?.on !== 'function') {
193
+ throw new TypeError('Session sync requires Host session events');
194
+ }
195
+ const coordinator = createSessionSyncCoordinator({ deliveryService, logger });
196
+ const disposeEvent = ctx.on('session/event', (session, event) => {
197
+ const sessionId = sessionIdOf(session);
198
+ if (!sessionId) return;
199
+ // Origin lookup must be synchronous: the originating ask may clean its id
200
+ // immediately after this event callback returns.
201
+ const origin = event?.type === 'user/message'
202
+ ? userInputOrigin(inputScope, event)
203
+ : 'other';
204
+ void coordinator.enqueue(sessionId, event, origin);
205
+ }, { global: true });
206
+ let disposed = false;
207
+ const close = () => {
208
+ if (disposed) return;
209
+ disposed = true;
210
+ disposeEvent?.();
211
+ coordinator.close();
212
+ };
213
+ if (typeof ctx.effect === 'function') {
214
+ ctx.effect(() => close, 'dsh-im: Session sync coordinator');
215
+ }
216
+ return Object.freeze({ ...coordinator, close });
217
+ }
@@ -141,15 +141,21 @@ if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).leng
141
141
  if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
142
142
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
143
143
  }
144
- // Connections still have no channel-enable toggle. Only the shared context
145
- // editor owns checkable inputs. Its reusable scope component contains one
146
- // switch template and one mapped field-input template; it renders both twice.
144
+ // Connections still have no channel-enable toggle. Checkable inputs are owned
145
+ // only by the shared context editor and the saved-target Session sync row.
146
+ // The context editor contains one switch template and one mapped field-input
147
+ // template; the delivery target adds one ordinary checkbox template.
147
148
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
148
- const otherClientSources = clientSources.replace(contextEditorSource, '');
149
+ const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
150
+ const otherClientSources = clientSources
151
+ .replace(contextEditorSource, '')
152
+ .replace(deliverySettingsSource, '');
149
153
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
154
+ || (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
155
+ || /role:\s*["']switch["']/u.test(deliverySettingsSource)
150
156
  || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
151
- || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 2) {
152
- throw new Error('checkable inputs must be limited to the context-enhancement editor');
157
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
158
+ throw new Error('checkable inputs must be limited to context enhancement and Session sync');
153
159
  }
154
160
  for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
155
161
  if (!host.includes(marker) || !client.includes(marker)) {