@xmanrui/dsh-im 3.1.1 → 4.0.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/README.en.md +16 -1
- package/README.md +16 -1
- package/lib/client.js +1653 -333
- package/lib/index.js +235 -226
- package/package.json +5 -1
- package/plugin-src/client/channels/dingtalk/api.js +3 -0
- package/plugin-src/client/channels/dingtalk/index.js +18 -5
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +18 -5
- package/plugin-src/client/channels/qq/api.js +3 -0
- package/plugin-src/client/channels/qq/index.js +13 -0
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +13 -1
- package/plugin-src/client/channels/wecom/api.js +3 -0
- package/plugin-src/client/channels/wecom/index.js +13 -0
- package/plugin-src/client/channels/weixin/api.js +3 -0
- package/plugin-src/client/channels/weixin/index.js +19 -5
- package/plugin-src/client/channels/whatsapp/api.js +3 -0
- package/plugin-src/client/channels/whatsapp/index.js +13 -0
- package/plugin-src/client/context-enhancement.js +275 -0
- package/plugin-src/client/i18n.js +54 -3
- package/plugin-src/client/styles.js +78 -0
- package/plugin-src/client/update-panel.js +108 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
- package/plugin-src/host/channels/qq/index.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +1 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +1 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/index.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +1 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/harness-connection.mjs +9 -4
- package/plugin-src/host/index.mjs +43 -29
- package/plugin-src/host/modern-harness-api.mjs +577 -0
- package/plugin-src/host/update-runtime.mjs +3 -2
- package/plugin-src/host/update-service.mjs +2 -0
- package/scripts/verify-package.mjs +15 -2
- package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
- package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
- package/src/channels/discord/discord-runtime.mjs +18 -4
- package/src/channels/feishu/bridge.mjs +18 -3
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +20 -4
- package/src/channels/qq/qq-runtime.mjs +4 -0
- package/src/channels/shared/bot-workspace-store.mjs +103 -6
- package/src/channels/shared/context-enhancement.mjs +135 -0
- package/src/channels/shared/harness-client.mjs +18 -0
- package/src/channels/shared/text-harness-bridge.mjs +19 -4
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +24 -2
- package/src/channels/wecom/wecom-bridge.mjs +18 -3
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +19 -4
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { hasActiveHarnessInteractionOwner } from '../../src/channels/shared/harness-client.mjs';
|
|
4
|
+
|
|
5
|
+
const modernApis = new WeakMap();
|
|
6
|
+
|
|
7
|
+
function failureOf(error) {
|
|
8
|
+
const failure = error?.failure;
|
|
9
|
+
if (failure && typeof failure === 'object'
|
|
10
|
+
&& typeof failure.code === 'string'
|
|
11
|
+
&& typeof failure.message === 'string') {
|
|
12
|
+
return {
|
|
13
|
+
code: failure.code,
|
|
14
|
+
message: failure.message,
|
|
15
|
+
details: failure.details && typeof failure.details === 'object' ? failure.details : {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (error?.name === 'AbortError' || error?.name === 'RemoteInvocationCancelled') {
|
|
19
|
+
return {
|
|
20
|
+
code: 'cancelled',
|
|
21
|
+
message: error instanceof Error ? error.message : 'Harness request was cancelled',
|
|
22
|
+
details: {},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
code: 'internal',
|
|
27
|
+
message: error instanceof Error ? error.message : String(error),
|
|
28
|
+
details: {},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rpcResult(request, operation) {
|
|
33
|
+
return Promise.resolve().then(operation).then(
|
|
34
|
+
(value) => ({ rpcId: request.rpcId, result: { ok: true, value } }),
|
|
35
|
+
(error) => ({ rpcId: request.rpcId, result: { ok: false, error: failureOf(error) } }),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function remoteRequest(namespace, method, args, signal) {
|
|
40
|
+
return {
|
|
41
|
+
namespace,
|
|
42
|
+
method,
|
|
43
|
+
args,
|
|
44
|
+
...(signal === undefined ? {} : { signal }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function questionError(message, code) {
|
|
49
|
+
const error = new Error(message);
|
|
50
|
+
error.name = 'UserQuestionError';
|
|
51
|
+
error.code = code;
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchesQuestions(value, pending) {
|
|
56
|
+
if (!value || typeof value !== 'object'
|
|
57
|
+
|| value.sessionId !== pending.sessionId
|
|
58
|
+
|| !value.answer || typeof value.answer !== 'object'
|
|
59
|
+
|| !Array.isArray(value.answer.answers)
|
|
60
|
+
|| value.answer.answers.length !== pending.questions.length) return false;
|
|
61
|
+
return value.answer.answers.every((answer, index) => {
|
|
62
|
+
const question = pending.questions[index];
|
|
63
|
+
if (!answer || typeof answer !== 'object'
|
|
64
|
+
|| answer.id !== question.id
|
|
65
|
+
|| !Array.isArray(answer.selected)
|
|
66
|
+
|| answer.selected.some((label) => typeof label !== 'string')
|
|
67
|
+
|| new Set(answer.selected).size !== answer.selected.length
|
|
68
|
+
|| (answer.custom !== undefined && typeof answer.custom !== 'string')) return false;
|
|
69
|
+
const custom = answer.custom?.trim();
|
|
70
|
+
if (custom !== undefined && !custom) return false;
|
|
71
|
+
if (question.multiSelect !== true
|
|
72
|
+
&& ((custom !== undefined && answer.selected.length > 0) || answer.selected.length > 1)) return false;
|
|
73
|
+
const labels = new Set(question.options?.map((option) => option.label) ?? []);
|
|
74
|
+
return answer.selected.every((label) => labels.has(label));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function expandChunkRecord(record) {
|
|
79
|
+
if (record?.type === 'event' && record.event && typeof record.event === 'object') {
|
|
80
|
+
return [{ event: record.event }];
|
|
81
|
+
}
|
|
82
|
+
const packed = record?.type === 'chunks' ? record.event : null;
|
|
83
|
+
if (!packed || typeof packed !== 'object') {
|
|
84
|
+
throw new Error('Harness returned an invalid session history record');
|
|
85
|
+
}
|
|
86
|
+
const { data } = packed;
|
|
87
|
+
const kind = packed.type;
|
|
88
|
+
const members = kind === 'chunkrow/tool-call-chunks' ? data?.args : data?.texts;
|
|
89
|
+
if (!data || !Array.isArray(members) || members.length === 0
|
|
90
|
+
|| members.some((member) => typeof member !== 'string')
|
|
91
|
+
|| !Array.isArray(data.dt) || data.dt.length !== members.length - 1) {
|
|
92
|
+
throw new Error('Harness returned an invalid packed session chunk');
|
|
93
|
+
}
|
|
94
|
+
let time = packed.time;
|
|
95
|
+
return members.map((member, index) => {
|
|
96
|
+
if (index > 0) time += data.dt[index - 1];
|
|
97
|
+
let chunk;
|
|
98
|
+
if (kind === 'chunkrow/text-chunks') {
|
|
99
|
+
chunk = { type: 'text-delta', index: data.index, text: member };
|
|
100
|
+
} else if (kind === 'chunkrow/reasoning-chunks') {
|
|
101
|
+
chunk = { type: 'reasoning-delta', index: data.index, text: member };
|
|
102
|
+
} else if (kind === 'chunkrow/tool-call-chunks') {
|
|
103
|
+
chunk = {
|
|
104
|
+
type: 'tool-call-delta',
|
|
105
|
+
index: data.index,
|
|
106
|
+
id: data.id,
|
|
107
|
+
...(Object.hasOwn(data, 'name') ? { name: data.name } : {}),
|
|
108
|
+
argumentsDelta: member,
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
throw new Error(`Harness returned an unsupported packed session chunk: ${String(kind)}`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
event: {
|
|
115
|
+
type: 'assistant/chunk',
|
|
116
|
+
seq: packed.seq + index,
|
|
117
|
+
time,
|
|
118
|
+
data: { turn: data.turn, step: data.step, chunk },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function historyEntries(records) {
|
|
125
|
+
if (!Array.isArray(records)) throw new Error('Harness returned invalid session history records');
|
|
126
|
+
return records.flatMap(expandChunkRecord);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class MuxSubscription {
|
|
130
|
+
#frames = [];
|
|
131
|
+
#waiting = null;
|
|
132
|
+
#closed = false;
|
|
133
|
+
#signal;
|
|
134
|
+
#onAbort;
|
|
135
|
+
#dispose;
|
|
136
|
+
|
|
137
|
+
constructor(signal, dispose) {
|
|
138
|
+
this.#signal = signal;
|
|
139
|
+
this.#dispose = dispose;
|
|
140
|
+
this.#onAbort = () => this.close();
|
|
141
|
+
signal?.addEventListener('abort', this.#onAbort, { once: true });
|
|
142
|
+
if (signal?.aborted) this.close();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
push(frame) {
|
|
146
|
+
if (this.#closed) return;
|
|
147
|
+
if (this.#waiting) {
|
|
148
|
+
const resolve = this.#waiting;
|
|
149
|
+
this.#waiting = null;
|
|
150
|
+
resolve({ value: frame, done: false });
|
|
151
|
+
} else {
|
|
152
|
+
this.#frames.push(frame);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
next() {
|
|
157
|
+
if (this.#frames.length > 0) {
|
|
158
|
+
return Promise.resolve({ value: this.#frames.shift(), done: false });
|
|
159
|
+
}
|
|
160
|
+
if (this.#closed) return Promise.resolve({ value: undefined, done: true });
|
|
161
|
+
return new Promise((resolve) => { this.#waiting = resolve; });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return() {
|
|
165
|
+
this.close();
|
|
166
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
close() {
|
|
170
|
+
if (this.#closed) return;
|
|
171
|
+
this.#closed = true;
|
|
172
|
+
this.#signal?.removeEventListener('abort', this.#onAbort);
|
|
173
|
+
this.#dispose?.();
|
|
174
|
+
this.#dispose = null;
|
|
175
|
+
if (this.#waiting) {
|
|
176
|
+
const resolve = this.#waiting;
|
|
177
|
+
this.#waiting = null;
|
|
178
|
+
resolve({ value: undefined, done: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
[Symbol.asyncIterator]() {
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class ModernHarnessApi {
|
|
188
|
+
#gateway;
|
|
189
|
+
#scope;
|
|
190
|
+
#mux = new Set();
|
|
191
|
+
#pendingQuestions = new Map();
|
|
192
|
+
#pendingApprovals = new Map();
|
|
193
|
+
#sessionCursors = new Map();
|
|
194
|
+
#disposers = [];
|
|
195
|
+
#disposed = false;
|
|
196
|
+
|
|
197
|
+
constructor(ctx, gateway, scope) {
|
|
198
|
+
this.#gateway = gateway;
|
|
199
|
+
this.#scope = scope;
|
|
200
|
+
|
|
201
|
+
this.host = Object.freeze({
|
|
202
|
+
describe: (request) => rpcResult(request, () => ({ ready: true, transport: 'typert' })),
|
|
203
|
+
});
|
|
204
|
+
this.workspace = Object.freeze({
|
|
205
|
+
list: (request, signal) => rpcResult(request, () => this.#workspaceList(signal)),
|
|
206
|
+
create: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
207
|
+
'workspace', 'create', { request: request.payload }, signal,
|
|
208
|
+
)),
|
|
209
|
+
});
|
|
210
|
+
this.sessions = Object.freeze({
|
|
211
|
+
list: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
212
|
+
'session', 'list', { _request: request.payload }, signal,
|
|
213
|
+
)),
|
|
214
|
+
create: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
215
|
+
'session', 'create', { request: request.payload }, signal,
|
|
216
|
+
)),
|
|
217
|
+
history: (request, signal) => rpcResult(request, () => this.#history(request.payload, signal)),
|
|
218
|
+
prompt: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
219
|
+
'session',
|
|
220
|
+
'prompt',
|
|
221
|
+
{ request: { requestId: request.rpcId, ...request.payload } },
|
|
222
|
+
signal,
|
|
223
|
+
)),
|
|
224
|
+
cancel: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
225
|
+
'session', 'cancel', { request: { sessionId: request.payload.sessionId } }, signal,
|
|
226
|
+
)),
|
|
227
|
+
models: (request, signal) => rpcResult(request, () => this.#sessionModels(
|
|
228
|
+
request.payload.sessionId, signal,
|
|
229
|
+
)),
|
|
230
|
+
selectModel: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
231
|
+
'session', 'selectModel', { request: request.payload }, signal,
|
|
232
|
+
)),
|
|
233
|
+
});
|
|
234
|
+
this.llm = Object.freeze({
|
|
235
|
+
models: (request, signal) => rpcResult(request, async () => {
|
|
236
|
+
const catalog = await this.#modelCatalog(signal);
|
|
237
|
+
return { groups: catalog.groups, failures: catalog.failures };
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
this.events = Object.freeze({
|
|
241
|
+
mux: (_request, signal) => this.#openMux(signal),
|
|
242
|
+
});
|
|
243
|
+
this.respond = (message) => Promise.resolve(this.#respond(message));
|
|
244
|
+
|
|
245
|
+
if (typeof ctx?.on === 'function') {
|
|
246
|
+
this.#disposers.push(ctx.on('session/event', (session, event) => {
|
|
247
|
+
const sessionId = session?.id;
|
|
248
|
+
if (typeof sessionId !== 'string' || !event || typeof event !== 'object') return;
|
|
249
|
+
if (Number.isSafeInteger(event.seq)) this.#rememberCursor(sessionId, event.seq);
|
|
250
|
+
this.#broadcast({ type: 'session/event', sessionId, event });
|
|
251
|
+
}, { global: true }));
|
|
252
|
+
this.#disposers.push(ctx.on(
|
|
253
|
+
'approval/request',
|
|
254
|
+
(request, next) => this.#requestApproval(request, next),
|
|
255
|
+
{ global: true, prepend: true },
|
|
256
|
+
));
|
|
257
|
+
this.#disposers.push(ctx.on(
|
|
258
|
+
'user-questions/request',
|
|
259
|
+
(request, next) => this.#requestQuestion(request, next),
|
|
260
|
+
{ global: true, prepend: true },
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async #invoke(namespace, method, args, signal) {
|
|
266
|
+
return this.#gateway.invoke(remoteRequest(namespace, method, args, signal));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async #streamFirst(namespace, method, args, signal) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const streamSignal = signal
|
|
272
|
+
? AbortSignal.any([signal, controller.signal])
|
|
273
|
+
: controller.signal;
|
|
274
|
+
let iterator;
|
|
275
|
+
try {
|
|
276
|
+
const source = await this.#gateway.stream(remoteRequest(
|
|
277
|
+
namespace, method, args, streamSignal,
|
|
278
|
+
));
|
|
279
|
+
iterator = source[Symbol.asyncIterator]();
|
|
280
|
+
const first = await iterator.next();
|
|
281
|
+
if (first.done) throw new Error(`Harness ${namespace}.${method} stream ended before its baseline`);
|
|
282
|
+
return first.value;
|
|
283
|
+
} finally {
|
|
284
|
+
controller.abort(new DOMException('Baseline received', 'AbortError'));
|
|
285
|
+
await Promise.resolve(iterator?.return?.()).catch(() => undefined);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async #workspaceList(signal) {
|
|
290
|
+
const frame = await this.#streamFirst('workspace', 'follow', {}, signal);
|
|
291
|
+
if (frame?.type !== 'baseline' || !frame.value || typeof frame.value !== 'object') {
|
|
292
|
+
throw new Error('Harness workspace.follow returned no baseline');
|
|
293
|
+
}
|
|
294
|
+
return frame.value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async #sessionSnapshot(sessionId, maxMessages, signal) {
|
|
298
|
+
const frame = await this.#streamFirst('session', 'follow', {
|
|
299
|
+
request: {
|
|
300
|
+
address: { kind: 'session', sessionId },
|
|
301
|
+
maxMessages,
|
|
302
|
+
},
|
|
303
|
+
}, signal);
|
|
304
|
+
if (frame?.type !== 'snapshot' || !Number.isSafeInteger(frame.cursor)) {
|
|
305
|
+
throw new Error('Harness session.follow returned no snapshot');
|
|
306
|
+
}
|
|
307
|
+
this.#rememberCursor(sessionId, frame.cursor);
|
|
308
|
+
return frame;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async #history(payload, signal) {
|
|
312
|
+
const { sessionId, maxMessages = 50, beforeSeq } = payload;
|
|
313
|
+
let cursor = this.#sessionCursors.get(sessionId);
|
|
314
|
+
if (cursor === undefined) {
|
|
315
|
+
const snapshot = await this.#sessionSnapshot(sessionId, maxMessages, signal);
|
|
316
|
+
cursor = this.#sessionCursors.get(sessionId) ?? snapshot.cursor;
|
|
317
|
+
if (beforeSeq === undefined && cursor === snapshot.cursor) {
|
|
318
|
+
return {
|
|
319
|
+
events: historyEntries(snapshot.records),
|
|
320
|
+
hasMore: snapshot.hasMore === true,
|
|
321
|
+
...(snapshot.projections === undefined ? {} : { projections: snapshot.projections }),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const page = await this.#invoke('session', 'page', {
|
|
326
|
+
request: {
|
|
327
|
+
address: { kind: 'session', sessionId },
|
|
328
|
+
throughSeq: cursor,
|
|
329
|
+
maxMessages,
|
|
330
|
+
...(beforeSeq === undefined ? {} : { beforeSeq }),
|
|
331
|
+
},
|
|
332
|
+
}, signal);
|
|
333
|
+
return { events: historyEntries(page.records), hasMore: page.hasMore === true };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#rememberCursor(sessionId, cursor) {
|
|
337
|
+
const previous = this.#sessionCursors.get(sessionId);
|
|
338
|
+
if (previous === undefined || cursor > previous) this.#sessionCursors.set(sessionId, cursor);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#modelCatalog(signal) {
|
|
342
|
+
return this.#invoke('session', 'modelCatalog', {}, signal);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async #sessionModels(sessionId, signal) {
|
|
346
|
+
const [catalog, list] = await Promise.all([
|
|
347
|
+
this.#modelCatalog(signal),
|
|
348
|
+
this.#invoke('session', 'list', { _request: {} }, signal),
|
|
349
|
+
]);
|
|
350
|
+
const summary = list?.items?.find((item) => item?.sessionId === sessionId);
|
|
351
|
+
if (!summary) {
|
|
352
|
+
const error = new Error(`session "${sessionId}" not found`);
|
|
353
|
+
error.failure = {
|
|
354
|
+
code: 'session-not-found',
|
|
355
|
+
message: error.message,
|
|
356
|
+
details: { sessionId },
|
|
357
|
+
};
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
const selection = summary.projections?.values?.modelSelection;
|
|
361
|
+
const current = selection?.next ?? selection?.lastUsed ?? catalog.default;
|
|
362
|
+
return {
|
|
363
|
+
current,
|
|
364
|
+
routable: catalog.routableProviders.includes(current.provider),
|
|
365
|
+
groups: catalog.groups,
|
|
366
|
+
failures: catalog.failures,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#openMux(signal) {
|
|
371
|
+
let subscription;
|
|
372
|
+
subscription = new MuxSubscription(signal, () => this.#mux.delete(subscription));
|
|
373
|
+
this.#mux.add(subscription);
|
|
374
|
+
for (const pending of this.#pendingQuestions.values()) subscription.push(this.#questionFrame(pending));
|
|
375
|
+
for (const pending of this.#pendingApprovals.values()) subscription.push(this.#approvalFrame(pending));
|
|
376
|
+
return subscription;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#broadcast(payload, rpcId = randomUUID()) {
|
|
380
|
+
const frame = { rpcId, payload };
|
|
381
|
+
for (const subscription of this.#mux) subscription.push(frame);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#claimableAgent(agent) {
|
|
385
|
+
const sessionId = agent?.session?.id ?? agent?.id;
|
|
386
|
+
if (typeof sessionId !== 'string' || !agent?.session || !Array.isArray(agent.session.events)) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return hasActiveHarnessInteractionOwner(
|
|
390
|
+
this.#scope,
|
|
391
|
+
sessionId,
|
|
392
|
+
agent.session.events,
|
|
393
|
+
) ? { sessionId, session: agent.session } : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#questionFrame(pending) {
|
|
397
|
+
return {
|
|
398
|
+
rpcId: pending.rpcId,
|
|
399
|
+
payload: {
|
|
400
|
+
type: 'question/requested',
|
|
401
|
+
sessionId: pending.sessionId,
|
|
402
|
+
questions: pending.questions,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#requestQuestion(request, next) {
|
|
408
|
+
const owner = this.#claimableAgent(request?.agent);
|
|
409
|
+
if (!owner) return next();
|
|
410
|
+
if (request.signal?.aborted) {
|
|
411
|
+
return Promise.reject(questionError(
|
|
412
|
+
'ask_user_question was aborted before the user answered', 'ASK_ABORTED',
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
const pending = {
|
|
417
|
+
rpcId: randomUUID(),
|
|
418
|
+
sessionId: owner.sessionId,
|
|
419
|
+
questions: request.questions,
|
|
420
|
+
signal: request.signal,
|
|
421
|
+
settle: (outcome, value) => {
|
|
422
|
+
if (!this.#pendingQuestions.delete(pending.rpcId)) return;
|
|
423
|
+
request.signal?.removeEventListener('abort', onAbort);
|
|
424
|
+
this.#broadcast({
|
|
425
|
+
type: 'question/resolved',
|
|
426
|
+
sessionId: pending.sessionId,
|
|
427
|
+
questionRpcId: pending.rpcId,
|
|
428
|
+
outcome,
|
|
429
|
+
});
|
|
430
|
+
if (outcome === 'answered') resolve(value);
|
|
431
|
+
else reject(value);
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
const onAbort = () => pending.settle('cancelled', questionError(
|
|
435
|
+
'ask_user_question was aborted before the user answered', 'ASK_ABORTED',
|
|
436
|
+
));
|
|
437
|
+
this.#pendingQuestions.set(pending.rpcId, pending);
|
|
438
|
+
request.signal?.addEventListener('abort', onAbort, { once: true });
|
|
439
|
+
this.#broadcast(this.#questionFrame(pending).payload, pending.rpcId);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#approvalFrame(pending) {
|
|
444
|
+
return {
|
|
445
|
+
rpcId: pending.rpcId,
|
|
446
|
+
payload: {
|
|
447
|
+
type: 'approval/requested',
|
|
448
|
+
sessionId: pending.sessionId,
|
|
449
|
+
approvalId: pending.approvalId,
|
|
450
|
+
toolName: pending.toolName,
|
|
451
|
+
...(pending.callId === undefined ? {} : { callId: pending.callId }),
|
|
452
|
+
...(pending.reason === undefined ? {} : { reason: pending.reason }),
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#requestApproval(request, next) {
|
|
458
|
+
const owner = this.#claimableAgent(request?.agent);
|
|
459
|
+
if (!owner) return next();
|
|
460
|
+
if (request.signal?.aborted) return Promise.resolve('cancelled');
|
|
461
|
+
const claimed = new Set([...this.#pendingApprovals.values()].map((entry) => entry.approvalId));
|
|
462
|
+
const decided = new Set();
|
|
463
|
+
let approvalId;
|
|
464
|
+
for (let index = owner.session.events.length - 1; index >= 0; index -= 1) {
|
|
465
|
+
const event = owner.session.events[index];
|
|
466
|
+
if (event.type === 'approval/decided') {
|
|
467
|
+
decided.add(event.data?.id);
|
|
468
|
+
} else if (event.type === 'approval/asked') {
|
|
469
|
+
const id = event.data?.id;
|
|
470
|
+
if (!id || decided.has(id) || claimed.has(id)) continue;
|
|
471
|
+
if ((request.callId ?? null) !== (event.data?.callId ?? null)) continue;
|
|
472
|
+
approvalId = id;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (approvalId === undefined) return next();
|
|
477
|
+
return new Promise((resolve) => {
|
|
478
|
+
const pending = {
|
|
479
|
+
rpcId: randomUUID(),
|
|
480
|
+
sessionId: owner.sessionId,
|
|
481
|
+
approvalId,
|
|
482
|
+
toolName: request.toolName,
|
|
483
|
+
callId: request.callId,
|
|
484
|
+
reason: request.reason,
|
|
485
|
+
settle: (outcome) => {
|
|
486
|
+
if (!this.#pendingApprovals.delete(pending.rpcId)) return;
|
|
487
|
+
request.signal?.removeEventListener('abort', onAbort);
|
|
488
|
+
this.#broadcast({
|
|
489
|
+
type: 'approval/resolved',
|
|
490
|
+
sessionId: pending.sessionId,
|
|
491
|
+
approvalId: pending.approvalId,
|
|
492
|
+
outcome,
|
|
493
|
+
});
|
|
494
|
+
resolve(outcome);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
const onAbort = () => pending.settle('cancelled');
|
|
498
|
+
this.#pendingApprovals.set(pending.rpcId, pending);
|
|
499
|
+
request.signal?.addEventListener('abort', onAbort, { once: true });
|
|
500
|
+
this.#broadcast(this.#approvalFrame(pending).payload, pending.rpcId);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#respond(message) {
|
|
505
|
+
const approval = this.#pendingApprovals.get(message?.rpcId);
|
|
506
|
+
if (approval) {
|
|
507
|
+
const value = message?.result?.value;
|
|
508
|
+
if (message?.result?.ok !== true
|
|
509
|
+
|| !value || typeof value !== 'object'
|
|
510
|
+
|| value.sessionId !== approval.sessionId
|
|
511
|
+
|| value.approvalId !== approval.approvalId
|
|
512
|
+
|| (value.outcome !== 'allowed-once' && value.outcome !== 'rejected')) {
|
|
513
|
+
return { accepted: false, reason: 'bad-response' };
|
|
514
|
+
}
|
|
515
|
+
approval.settle(value.outcome);
|
|
516
|
+
return { accepted: true };
|
|
517
|
+
}
|
|
518
|
+
const question = this.#pendingQuestions.get(message?.rpcId);
|
|
519
|
+
if (!question) return { accepted: false, reason: 'not-pending' };
|
|
520
|
+
if (message?.result?.ok !== true) {
|
|
521
|
+
if (message?.result?.error?.code !== 'cancelled') {
|
|
522
|
+
return { accepted: false, reason: 'bad-response' };
|
|
523
|
+
}
|
|
524
|
+
question.settle('cancelled', questionError(
|
|
525
|
+
'the user cancelled ask_user_question', 'ASK_CANCELLED',
|
|
526
|
+
));
|
|
527
|
+
return { accepted: true };
|
|
528
|
+
}
|
|
529
|
+
if (!matchesQuestions(message.result.value, question)) {
|
|
530
|
+
return { accepted: false, reason: 'bad-response' };
|
|
531
|
+
}
|
|
532
|
+
const answer = {
|
|
533
|
+
answers: message.result.value.answer.answers.map((item) => ({
|
|
534
|
+
id: item.id,
|
|
535
|
+
selected: [...item.selected],
|
|
536
|
+
...(item.custom === undefined ? {} : { custom: item.custom }),
|
|
537
|
+
})),
|
|
538
|
+
};
|
|
539
|
+
question.settle('answered', answer);
|
|
540
|
+
return { accepted: true };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
dispose() {
|
|
544
|
+
if (this.#disposed) return;
|
|
545
|
+
this.#disposed = true;
|
|
546
|
+
for (const subscription of [...this.#mux]) subscription.close();
|
|
547
|
+
for (const pending of [...this.#pendingApprovals.values()]) pending.settle('cancelled');
|
|
548
|
+
for (const pending of [...this.#pendingQuestions.values()]) pending.settle(
|
|
549
|
+
'cancelled',
|
|
550
|
+
questionError('dsh-im interaction adapter was disposed', 'ASK_ABORTED'),
|
|
551
|
+
);
|
|
552
|
+
for (const dispose of this.#disposers.splice(0).reverse()) dispose?.();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Create one modern compatibility API per Cordis Host root. */
|
|
557
|
+
export function modernHarnessApi(ctx) {
|
|
558
|
+
const scope = ctx?.root ?? ctx;
|
|
559
|
+
if (!scope || !['object', 'function'].includes(typeof scope)) {
|
|
560
|
+
throw new TypeError('dsh-im requires a Cordis Host context');
|
|
561
|
+
}
|
|
562
|
+
const cached = modernApis.get(scope);
|
|
563
|
+
if (cached) return cached;
|
|
564
|
+
const gateway = ctx?.typertGateway;
|
|
565
|
+
if (!gateway || typeof gateway.invoke !== 'function' || typeof gateway.stream !== 'function') {
|
|
566
|
+
throw new TypeError('dsh-im requires the modern Host Typert gateway');
|
|
567
|
+
}
|
|
568
|
+
const api = new ModernHarnessApi(ctx, gateway, scope);
|
|
569
|
+
modernApis.set(scope, api);
|
|
570
|
+
if (typeof ctx.effect === 'function') {
|
|
571
|
+
ctx.effect(() => () => {
|
|
572
|
+
if (modernApis.get(scope) === api) modernApis.delete(scope);
|
|
573
|
+
api.dispose();
|
|
574
|
+
}, 'dsh-im: modern Harness compatibility API');
|
|
575
|
+
}
|
|
576
|
+
return api;
|
|
577
|
+
}
|
|
@@ -280,6 +280,8 @@ export function createUpdateRuntime(options = {}) {
|
|
|
280
280
|
|
|
281
281
|
const profile = await packageAt(runtime.profileDir);
|
|
282
282
|
const installed = await packageAt(join(runtime.profileDir, 'node_modules', PACKAGE_NAME));
|
|
283
|
+
result.sourceInstall = !registrySpec(profile.manifest.dependencies?.[PACKAGE_NAME])
|
|
284
|
+
|| !inside(join(runtime.profileDir, 'node_modules'), installed.directory);
|
|
283
285
|
result.installedVersion = typeof installed.manifest.version === 'string' ? installed.manifest.version : null;
|
|
284
286
|
result.packageValid = await validPackage(installed);
|
|
285
287
|
const loaded = await loadedPackage;
|
|
@@ -298,8 +300,7 @@ export function createUpdateRuntime(options = {}) {
|
|
|
298
300
|
if (boundProfile !== undefined && boundProfile !== identity) result.blockedReason = 'installation-changed';
|
|
299
301
|
else if (!sameLoadedPackage && boundProfile === undefined) result.blockedReason = 'installation-changed';
|
|
300
302
|
else if (!result.packageValid) result.blockedReason = 'invalid-installation';
|
|
301
|
-
else if (
|
|
302
|
-
|| !inside(join(runtime.profileDir, 'node_modules'), installed.directory)) result.blockedReason = 'source-install';
|
|
303
|
+
else if (result.sourceInstall) result.blockedReason = 'source-install';
|
|
303
304
|
else if (runtime.blockedReason) result.blockedReason = runtime.blockedReason;
|
|
304
305
|
else if (!sameLoadedPackage) result.blockedReason = 'pending-restart';
|
|
305
306
|
else if (preflight) await checkRegistry(runtime);
|
|
@@ -201,6 +201,8 @@ export function createUpdateService({
|
|
|
201
201
|
latestVersion: checked?.release.version ?? null,
|
|
202
202
|
profileName: environment.profileName ?? null,
|
|
203
203
|
environmentKind: environment.environmentKind ?? 'cli',
|
|
204
|
+
// Preserve source protection even when restart/recovery takes status priority.
|
|
205
|
+
sourceInstall: environment.sourceInstall === true || environment.blockedReason === 'source-install',
|
|
204
206
|
canInstall,
|
|
205
207
|
blockedReason,
|
|
206
208
|
checkedAt: checked?.checkedAt ?? null,
|
|
@@ -29,6 +29,7 @@ const required = [
|
|
|
29
29
|
'plugin-src/client/channels/slack/index.js',
|
|
30
30
|
'plugin-src/client/i18n.js',
|
|
31
31
|
'plugin-src/client/update-panel.js',
|
|
32
|
+
'plugin-src/client/context-enhancement.js',
|
|
32
33
|
'plugin-src/host/update-service.mjs',
|
|
33
34
|
'plugin-src/host/update-runtime.mjs',
|
|
34
35
|
'plugin-src/host/update-rpc.mjs',
|
|
@@ -51,6 +52,7 @@ const required = [
|
|
|
51
52
|
'src/channels/discord/discord-runtime.mjs',
|
|
52
53
|
'src/channels/whatsapp/whatsapp-runtime.mjs',
|
|
53
54
|
'src/channels/whatsapp/whatsapp-web-session.mjs',
|
|
55
|
+
'src/channels/shared/context-enhancement.mjs',
|
|
54
56
|
];
|
|
55
57
|
await Promise.all(required.map((path) => access(resolve(root, path))));
|
|
56
58
|
|
|
@@ -135,8 +137,19 @@ if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).leng
|
|
|
135
137
|
if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
|
|
136
138
|
throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
|
|
137
139
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
// Connections still have no channel-enable toggle. Only the shared context
|
|
141
|
+
// editor owns checkable inputs: two scope switches and one mapped field input.
|
|
142
|
+
const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
|
|
143
|
+
const otherClientSources = clientSources.replace(contextEditorSource, '');
|
|
144
|
+
if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
|
|
145
|
+
|| (client.match(/role:\s*["']switch["']/g) ?? []).length !== 2
|
|
146
|
+
|| (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
|
|
147
|
+
throw new Error('checkable inputs must be limited to the context-enhancement editor');
|
|
148
|
+
}
|
|
149
|
+
for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
|
|
150
|
+
if (!host.includes(marker) || !client.includes(marker)) {
|
|
151
|
+
throw new Error(`context-enhancement marker missing from Host or Client bundle: ${marker}`);
|
|
152
|
+
}
|
|
140
153
|
}
|
|
141
154
|
if (!client.includes('container-type: inline-size')
|
|
142
155
|
|| !client.includes('@container (max-width: 680px)')) {
|