@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.
- package/PROACTIVE_DELIVERY.en.md +15 -1
- package/PROACTIVE_DELIVERY.md +15 -1
- package/README.en.md +5 -1
- package/README.md +6 -2
- package/lib/client.js +1619 -769
- package/lib/index.js +248 -244
- package/package.json +4 -3
- package/plugin-src/client/channel-card-meta.js +9 -1
- package/plugin-src/client/channels/dingtalk/api.js +4 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -2
- package/plugin-src/client/channels/feishu/api.js +4 -0
- package/plugin-src/client/channels/feishu/group-settings.js +581 -0
- package/plugin-src/client/channels/feishu/index.js +26 -204
- package/plugin-src/client/channels/qq/api.js +4 -0
- package/plugin-src/client/channels/qq/index.js +25 -2
- package/plugin-src/client/channels/shared/token-api.js +4 -0
- package/plugin-src/client/channels/shared/token-channel.js +25 -3
- package/plugin-src/client/channels/wecom/api.js +4 -0
- package/plugin-src/client/channels/wecom/index.js +25 -2
- package/plugin-src/client/channels/weixin/api.js +4 -0
- package/plugin-src/client/channels/weixin/index.js +25 -2
- package/plugin-src/client/channels/whatsapp/api.js +4 -0
- package/plugin-src/client/channels/whatsapp/index.js +24 -2
- package/plugin-src/client/delivery-settings.js +73 -5
- package/plugin-src/client/i18n.js +20 -0
- package/plugin-src/client/model-setting.js +104 -0
- package/plugin-src/client/styles.js +50 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
- package/plugin-src/host/channels/qq/production.mjs +3 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
- package/plugin-src/host/channels/shared/production.mjs +3 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
- package/plugin-src/host/channels/slack/production.mjs +3 -0
- package/plugin-src/host/channels/slack/rpc.mjs +9 -0
- package/plugin-src/host/channels/wecom/production.mjs +3 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +3 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/delivery-adapter.mjs +141 -3
- package/plugin-src/host/delivery-rpc.mjs +12 -0
- package/plugin-src/host/delivery-service.mjs +110 -3
- package/plugin-src/host/delivery-suggestions.mjs +46 -0
- package/plugin-src/host/harness-session-coordinator.mjs +6 -5
- package/plugin-src/host/index.mjs +18 -1
- package/plugin-src/host/session-sync-coordinator.mjs +217 -0
- package/scripts/verify-package.mjs +12 -6
- package/src/channels/shared/bot-workspace-store.mjs +285 -38
- package/src/channels/shared/harness-client.mjs +124 -43
- 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.
|
|
145
|
-
//
|
|
146
|
-
// switch template and one mapped field-input
|
|
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
|
|
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 !==
|
|
152
|
-
throw new Error('checkable inputs must be limited to
|
|
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)) {
|