@xmanrui/dsh-im 2.0.1 → 2.2.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 +23 -6
- package/README.md +23 -6
- package/assets/screenshot-menu-card.png +0 -0
- package/lib/client.js +140 -7
- package/lib/index.js +240 -192
- package/package.json +1 -1
- package/plugin-src/client/i18n.js +4 -0
- package/plugin-src/client/index.js +72 -11
- package/plugin-src/client/loopback-recovery.js +75 -0
- package/plugin-src/client/styles.js +10 -0
- package/plugin-src/host/build.mjs +3 -0
- package/plugin-src/host/channels/shared/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +5 -2
- package/plugin-src/host/lark-sdk-handshake-patch.mjs +181 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -2
- package/src/channels/feishu/bridge.mjs +1411 -164
- package/src/channels/feishu/feishu-cards.mjs +667 -56
- package/src/channels/feishu/feishu-runtime.mjs +129 -43
- package/src/channels/qq/qq-bridge.mjs +4 -2
- package/src/channels/shared/control-command.mjs +2 -2
- package/src/channels/shared/harness-client.mjs +31 -2
- package/src/channels/shared/i18n-en/feishu.mjs +133 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +8 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +43 -0
- package/src/channels/shared/model-command.mjs +413 -31
- package/src/channels/shared/text-harness-bridge.mjs +4 -2
- package/src/channels/shared/workspace-command.mjs +4 -4
- package/src/channels/wecom/wecom-bridge.mjs +4 -2
- package/src/channels/weixin/weixin-api.mjs +2 -1
- package/src/channels/weixin/weixin-bridge.mjs +6 -3
- package/src/channels/weixin/weixin-runtime.mjs +2 -2
|
@@ -5,9 +5,15 @@ import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
|
5
5
|
|
|
6
6
|
const MODEL_COMMAND = /^\/model(?=$|\s)/i;
|
|
7
7
|
const MODELS_COMMAND = /^\/models(?=$|\s)/i;
|
|
8
|
-
const
|
|
8
|
+
const REASONING_COMMAND = /^\/reasoning(?=$|\s)/i;
|
|
9
|
+
const REASONINGS_COMMAND = /^\/reasonings(?=$|\s)/i;
|
|
10
|
+
const REASONING_LIST_COMMAND = /^\/reasoninglist(?=$|\s)/i;
|
|
11
|
+
const MODEL_USAGE = '用法:/model <序号或 provider/model> [推理等级ID]';
|
|
9
12
|
const MODELS_USAGE = '用法:/models(不带参数)';
|
|
13
|
+
const REASONING_USAGE = '用法:/reasoning [序号、等级ID或 --default]';
|
|
14
|
+
const REASONING_LIST_USAGE = '用法:/reasoninglist 或 /reasonings(不带参数)';
|
|
10
15
|
const SESSION_BINDING_CHANGED = 'session-binding-changed';
|
|
16
|
+
const MODEL_SELECTION_MISMATCH = 'model-selection-mismatch';
|
|
11
17
|
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
|
12
18
|
|
|
13
19
|
function commandResult(message) {
|
|
@@ -27,6 +33,35 @@ function rpcOptions(signal) {
|
|
|
27
33
|
return signal ? { signal } : {};
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
function normalizeReasoning(value) {
|
|
37
|
+
if (value === undefined) return undefined;
|
|
38
|
+
if (!value || typeof value !== 'object'
|
|
39
|
+
|| !Array.isArray(value.efforts) || value.efforts.length === 0) {
|
|
40
|
+
throw new TypeError('Harness returned invalid model reasoning metadata');
|
|
41
|
+
}
|
|
42
|
+
const efforts = value.efforts.map((effort) => {
|
|
43
|
+
if (!effort || typeof effort !== 'object'
|
|
44
|
+
|| typeof effort.id !== 'string' || !effort.id
|
|
45
|
+
|| typeof effort.name !== 'string' || !effort.name
|
|
46
|
+
|| (effort.description !== undefined && typeof effort.description !== 'string')) {
|
|
47
|
+
throw new TypeError('Harness returned an invalid reasoning effort');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
id: effort.id,
|
|
51
|
+
name: effort.name,
|
|
52
|
+
...(effort.description === undefined ? {} : { description: effort.description }),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
if (value.defaultEffort !== undefined
|
|
56
|
+
&& (typeof value.defaultEffort !== 'string' || !value.defaultEffort)) {
|
|
57
|
+
throw new TypeError('Harness returned an invalid default reasoning effort');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
efforts,
|
|
61
|
+
...(value.defaultEffort === undefined ? {} : { defaultEffort: value.defaultEffort }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
31
66
|
if (!value || typeof value !== 'object'
|
|
32
67
|
|| !Array.isArray(value.groups) || !Array.isArray(value.failures)) {
|
|
@@ -45,10 +80,18 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
|
45
80
|
models: group.models.map((model) => {
|
|
46
81
|
if (!model || typeof model !== 'object'
|
|
47
82
|
|| typeof model.id !== 'string' || !model.id
|
|
48
|
-
|| typeof model.name !== 'string' || !model.name
|
|
83
|
+
|| typeof model.name !== 'string' || !model.name
|
|
84
|
+
|| (model.description !== undefined && typeof model.description !== 'string')) {
|
|
49
85
|
throw new TypeError('Harness returned an invalid model');
|
|
50
86
|
}
|
|
51
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
id: model.id,
|
|
89
|
+
name: model.name,
|
|
90
|
+
...(model.description === undefined ? {} : { description: model.description }),
|
|
91
|
+
...(model.reasoning === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { reasoning: normalizeReasoning(model.reasoning) }),
|
|
94
|
+
};
|
|
52
95
|
}),
|
|
53
96
|
};
|
|
54
97
|
});
|
|
@@ -64,10 +107,19 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
|
64
107
|
if (value.current !== undefined) {
|
|
65
108
|
if (!value.current || typeof value.current !== 'object'
|
|
66
109
|
|| typeof value.current.provider !== 'string' || !value.current.provider
|
|
67
|
-
|| typeof value.current.model !== 'string' || !value.current.model
|
|
110
|
+
|| typeof value.current.model !== 'string' || !value.current.model
|
|
111
|
+
|| (value.current.reasoningEffort !== undefined
|
|
112
|
+
&& (typeof value.current.reasoningEffort !== 'string'
|
|
113
|
+
|| !value.current.reasoningEffort))) {
|
|
68
114
|
throw new TypeError('Harness returned an invalid current model');
|
|
69
115
|
}
|
|
70
|
-
current = {
|
|
116
|
+
current = {
|
|
117
|
+
provider: value.current.provider,
|
|
118
|
+
model: value.current.model,
|
|
119
|
+
...(value.current.reasoningEffort === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { reasoningEffort: value.current.reasoningEffort }),
|
|
122
|
+
};
|
|
71
123
|
} else if (requireCurrent) {
|
|
72
124
|
throw new TypeError('Harness returned no current model');
|
|
73
125
|
}
|
|
@@ -78,6 +130,50 @@ function modelId(provider, model) {
|
|
|
78
130
|
return `${provider}/${model}`;
|
|
79
131
|
}
|
|
80
132
|
|
|
133
|
+
function sameModel(left, right) {
|
|
134
|
+
return left?.provider === right?.provider && left?.model === right?.model;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sameSelection(left, right) {
|
|
138
|
+
return sameModel(left, right) && left?.reasoningEffort === right?.reasoningEffort;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function confirmsSelection(actual, requested) {
|
|
142
|
+
return sameModel(actual, requested)
|
|
143
|
+
&& (requested.reasoningEffort === undefined
|
|
144
|
+
|| actual?.reasoningEffort === requested.reasoningEffort);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function selectionText(selection) {
|
|
148
|
+
if (!selection?.provider || !selection?.model) return '';
|
|
149
|
+
const id = modelId(selection.provider, selection.model);
|
|
150
|
+
return selection.reasoningEffort === undefined
|
|
151
|
+
? id
|
|
152
|
+
: `${id} · reasoningEffort=${safeDisplayText(selection.reasoningEffort)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function selectionMismatch(expected, actual, source) {
|
|
156
|
+
const error = new Error(`Harness ${source} did not confirm the selected model`);
|
|
157
|
+
error.code = MODEL_SELECTION_MISMATCH;
|
|
158
|
+
error.expected = expected;
|
|
159
|
+
error.actual = actual;
|
|
160
|
+
error.source = source;
|
|
161
|
+
return error;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sessionBindingChanged() {
|
|
165
|
+
const error = new Error('Conversation binding changed during model selection');
|
|
166
|
+
error.code = SESSION_BINDING_CHANGED;
|
|
167
|
+
return error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function assertSessionBinding(state, key, expectedSessionId) {
|
|
171
|
+
const currentSessionId = typeof state?.sessionFor === 'function'
|
|
172
|
+
? state.sessionFor(key)
|
|
173
|
+
: null;
|
|
174
|
+
if (currentSessionId !== expectedSessionId) throw sessionBindingChanged();
|
|
175
|
+
}
|
|
176
|
+
|
|
81
177
|
function matchingModel(catalog, requested) {
|
|
82
178
|
for (const group of catalog.groups) {
|
|
83
179
|
for (const model of group.models) {
|
|
@@ -102,12 +198,54 @@ function modelAt(catalog, requestedIndex) {
|
|
|
102
198
|
return null;
|
|
103
199
|
}
|
|
104
200
|
|
|
105
|
-
function
|
|
201
|
+
function positiveNumberRequest(requested) {
|
|
106
202
|
if (!/^\d+$/u.test(requested)) return null;
|
|
107
203
|
const index = Number(requested);
|
|
108
204
|
return { index: Number.isSafeInteger(index) && index > 0 ? index : null };
|
|
109
205
|
}
|
|
110
206
|
|
|
207
|
+
function modelForSelection(catalog, selection) {
|
|
208
|
+
if (!selection) return null;
|
|
209
|
+
const group = catalog.groups.find(({ id }) => id === selection.provider);
|
|
210
|
+
return group?.models.find(({ id }) => id === selection.model) ?? null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reasoningEffortAt(model, requestedIndex) {
|
|
214
|
+
return model?.reasoning?.efforts?.[requestedIndex - 1] ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reasoningEffortById(model, requestedId) {
|
|
218
|
+
return model?.reasoning?.efforts?.find(({ id }) => id === requestedId) ?? null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function effectiveReasoningEffort(current, model) {
|
|
222
|
+
return current?.reasoningEffort ?? model?.reasoning?.defaultEffort;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function reasoningEffortText(model, effortId) {
|
|
226
|
+
if (effortId === undefined) return t('Default(由模型或 Provider 决定)');
|
|
227
|
+
const effort = reasoningEffortById(model, effortId);
|
|
228
|
+
if (!effort) return safeDisplayText(effortId);
|
|
229
|
+
const name = safeDisplayText(effort.name);
|
|
230
|
+
const id = safeDisplayText(effort.id);
|
|
231
|
+
return name === id ? id : `${name} (${id})`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function currentReasoningEffortText(catalog) {
|
|
235
|
+
const model = modelForSelection(catalog, catalog.current);
|
|
236
|
+
return reasoningEffortText(
|
|
237
|
+
model,
|
|
238
|
+
effectiveReasoningEffort(catalog.current, model),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function reasoningMarker(effortId, currentId, defaultId) {
|
|
243
|
+
if (effortId === currentId && effortId === defaultId) return t('(当前、默认)');
|
|
244
|
+
if (effortId === currentId) return t('(当前)');
|
|
245
|
+
if (effortId === defaultId) return t('(默认)');
|
|
246
|
+
return '';
|
|
247
|
+
}
|
|
248
|
+
|
|
111
249
|
function invalidModelNumberMessage(requested) {
|
|
112
250
|
return [
|
|
113
251
|
t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
|
|
@@ -116,6 +254,29 @@ function invalidModelNumberMessage(requested) {
|
|
|
116
254
|
].join('\n');
|
|
117
255
|
}
|
|
118
256
|
|
|
257
|
+
function invalidReasoningNumberMessage(requested) {
|
|
258
|
+
return [
|
|
259
|
+
t('推理等级序号无效:{input}', { input: safeDisplayText(requested) }),
|
|
260
|
+
'',
|
|
261
|
+
t('请发送 /reasoninglist 查看并输入有效的正整数序号。'),
|
|
262
|
+
].join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function unsupportedReasoningMessage(selection, requested, model) {
|
|
266
|
+
const lines = [
|
|
267
|
+
t('模型不支持推理等级:{effort}', { effort: safeDisplayText(requested) }),
|
|
268
|
+
'',
|
|
269
|
+
safeDisplayText(selectionText(selection)),
|
|
270
|
+
];
|
|
271
|
+
const ids = model?.reasoning?.efforts?.map(({ id }) => safeDisplayText(id)) ?? [];
|
|
272
|
+
if (ids.length > 0) {
|
|
273
|
+
lines.push(t('可用推理等级:{efforts}', { efforts: ids.join(', ') }));
|
|
274
|
+
} else {
|
|
275
|
+
lines.push(t('该模型不提供可切换的推理等级。'));
|
|
276
|
+
}
|
|
277
|
+
return lines.join('\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
119
280
|
function formatCatalog(catalog) {
|
|
120
281
|
const currentId = catalog.current
|
|
121
282
|
? modelId(catalog.current.provider, catalog.current.model)
|
|
@@ -137,20 +298,72 @@ function formatCatalog(catalog) {
|
|
|
137
298
|
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
|
|
138
299
|
}
|
|
139
300
|
}
|
|
140
|
-
if (index > 0) lines.push('', t('切换模型:/model <序号>'));
|
|
301
|
+
if (index > 0) lines.push('', t('切换模型:/model <序号> [推理等级ID]'));
|
|
141
302
|
return lines.join('\n');
|
|
142
303
|
}
|
|
143
304
|
|
|
144
|
-
function currentModelMessage(
|
|
305
|
+
function currentModelMessage(catalog) {
|
|
145
306
|
return [
|
|
146
307
|
t('当前模型:'),
|
|
147
|
-
modelId(current.provider, current.model),
|
|
308
|
+
modelId(catalog.current.provider, catalog.current.model),
|
|
309
|
+
t('当前推理等级:{effort}', { effort: currentReasoningEffortText(catalog) }),
|
|
148
310
|
'',
|
|
149
311
|
t('查看全部模型:/models'),
|
|
150
|
-
t('
|
|
312
|
+
t('查看可用推理等级:/reasoninglist'),
|
|
313
|
+
t('切换模型:/model <序号> [推理等级ID]'),
|
|
151
314
|
].join('\n');
|
|
152
315
|
}
|
|
153
316
|
|
|
317
|
+
function currentReasoningMessage(catalog) {
|
|
318
|
+
return [
|
|
319
|
+
t('当前模型:'),
|
|
320
|
+
modelId(catalog.current.provider, catalog.current.model),
|
|
321
|
+
t('当前推理等级:{effort}', { effort: currentReasoningEffortText(catalog) }),
|
|
322
|
+
'',
|
|
323
|
+
t('查看可用推理等级:/reasoninglist'),
|
|
324
|
+
t('切换推理等级:/reasoning <序号或等级ID>'),
|
|
325
|
+
t('恢复默认等级:/reasoning --default'),
|
|
326
|
+
].join('\n');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatReasoningCatalog(catalog) {
|
|
330
|
+
const current = catalog.current;
|
|
331
|
+
const model = modelForSelection(catalog, current);
|
|
332
|
+
const currentEffort = effectiveReasoningEffort(current, model);
|
|
333
|
+
const lines = [
|
|
334
|
+
t('当前模型:'),
|
|
335
|
+
modelId(current.provider, current.model),
|
|
336
|
+
t('当前推理等级:{effort}', { effort: reasoningEffortText(model, currentEffort) }),
|
|
337
|
+
'',
|
|
338
|
+
t('可用推理等级:'),
|
|
339
|
+
];
|
|
340
|
+
if (!model?.reasoning) {
|
|
341
|
+
lines.push(
|
|
342
|
+
t('该模型不提供可切换的推理等级。'),
|
|
343
|
+
'',
|
|
344
|
+
t('恢复默认等级:/reasoning --default'),
|
|
345
|
+
);
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
for (const [index, effort] of model.reasoning.efforts.entries()) {
|
|
349
|
+
const label = reasoningEffortText(model, effort.id);
|
|
350
|
+
const marker = reasoningMarker(
|
|
351
|
+
effort.id,
|
|
352
|
+
currentEffort,
|
|
353
|
+
model.reasoning.defaultEffort,
|
|
354
|
+
);
|
|
355
|
+
lines.push(`${index + 1}. ${label}${marker}`);
|
|
356
|
+
const description = safeDisplayText(effort.description);
|
|
357
|
+
if (description) lines.push(` ${description}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push(
|
|
360
|
+
'',
|
|
361
|
+
t('切换推理等级:/reasoning <序号或等级ID>'),
|
|
362
|
+
t('恢复默认等级:/reasoning --default'),
|
|
363
|
+
);
|
|
364
|
+
return lines.join('\n');
|
|
365
|
+
}
|
|
366
|
+
|
|
154
367
|
function noSessionMessage() {
|
|
155
368
|
return [
|
|
156
369
|
t('当前聊天还没有会话。'),
|
|
@@ -160,6 +373,14 @@ function noSessionMessage() {
|
|
|
160
373
|
].join('\n');
|
|
161
374
|
}
|
|
162
375
|
|
|
376
|
+
function noReasoningSessionMessage() {
|
|
377
|
+
return [
|
|
378
|
+
t('当前聊天还没有会话。'),
|
|
379
|
+
'',
|
|
380
|
+
t('请先发送一条普通消息创建会话。'),
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
163
384
|
function errorCode(error) {
|
|
164
385
|
return error?.code ?? error?.failure?.code;
|
|
165
386
|
}
|
|
@@ -173,6 +394,9 @@ function modelErrorMessage(error, action) {
|
|
|
173
394
|
return t('当前聊天绑定的会话已不存在,请重试。');
|
|
174
395
|
}
|
|
175
396
|
if (code === 'model-unavailable') {
|
|
397
|
+
if (action === 'reasoning-select') {
|
|
398
|
+
return t('无法切换推理等级。当前模型或推理等级不可用。');
|
|
399
|
+
}
|
|
176
400
|
return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
|
|
177
401
|
}
|
|
178
402
|
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
@@ -181,12 +405,35 @@ function modelErrorMessage(error, action) {
|
|
|
181
405
|
if (code === SESSION_BINDING_CHANGED) {
|
|
182
406
|
return t('当前聊天绑定的会话已发生变化,请重试。');
|
|
183
407
|
}
|
|
408
|
+
if (code === MODEL_SELECTION_MISMATCH) {
|
|
409
|
+
const expected = error?.expected;
|
|
410
|
+
const actual = error?.actual;
|
|
411
|
+
const lines = [action === 'reasoning-select'
|
|
412
|
+
? t('推理等级切换失败,请稍后重试。')
|
|
413
|
+
: t('模型切换失败,请稍后重试。')];
|
|
414
|
+
if (expected?.provider && expected?.model) {
|
|
415
|
+
lines.push('', `requested: ${safeDisplayText(selectionText(expected))}`);
|
|
416
|
+
}
|
|
417
|
+
if (actual?.provider && actual?.model) {
|
|
418
|
+
const label = error?.source === 'models.current'
|
|
419
|
+
? t('当前模型:')
|
|
420
|
+
: 'selectModel.selected:';
|
|
421
|
+
lines.push(`${label} ${safeDisplayText(selectionText(actual))}`);
|
|
422
|
+
} else {
|
|
423
|
+
lines.push(`${error?.source ?? 'Harness'}: unconfirmed`);
|
|
424
|
+
}
|
|
425
|
+
return lines.join('\n');
|
|
426
|
+
}
|
|
184
427
|
if (code === 'cancelled' || error?.name === 'AbortError') {
|
|
185
|
-
|
|
428
|
+
if (action === 'list') return t('获取模型列表已取消。');
|
|
429
|
+
if (action === 'reasoning-list') return t('获取推理等级列表已取消。');
|
|
430
|
+
if (action === 'reasoning-select') return t('推理等级切换已取消。');
|
|
431
|
+
return t('模型切换已取消。');
|
|
186
432
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
433
|
+
if (action === 'list') return t('暂时无法获取模型列表,请稍后重试。');
|
|
434
|
+
if (action === 'reasoning-list') return t('暂时无法获取推理等级,请稍后重试。');
|
|
435
|
+
if (action === 'reasoning-select') return t('推理等级切换失败,请稍后重试。');
|
|
436
|
+
return t('模型切换失败,请稍后重试。');
|
|
190
437
|
}
|
|
191
438
|
|
|
192
439
|
async function boundSession(harness, state, key, options) {
|
|
@@ -230,21 +477,48 @@ async function sessionCatalog(session, options) {
|
|
|
230
477
|
return normalizeCatalog(await session.models(options), { requireCurrent: true });
|
|
231
478
|
}
|
|
232
479
|
|
|
480
|
+
async function selectAndVerifyModel(session, selection, options) {
|
|
481
|
+
if (typeof session?.selectModel !== 'function') {
|
|
482
|
+
throw new TypeError('Harness session does not support model selection');
|
|
483
|
+
}
|
|
484
|
+
const selected = (await session.selectModel(selection, options))?.selected;
|
|
485
|
+
if (!confirmsSelection(selected, selection)) {
|
|
486
|
+
throw selectionMismatch(selection, selected, 'selectModel.selected');
|
|
487
|
+
}
|
|
488
|
+
const current = (await sessionCatalog(session, options)).current;
|
|
489
|
+
if (!sameSelection(current, selected)) {
|
|
490
|
+
throw selectionMismatch(selected, current, 'models.current');
|
|
491
|
+
}
|
|
492
|
+
return current;
|
|
493
|
+
}
|
|
494
|
+
|
|
233
495
|
function isModelsCommand(command) {
|
|
234
496
|
return MODELS_COMMAND.test(command);
|
|
235
497
|
}
|
|
236
498
|
|
|
499
|
+
function isReasoningListCommand(command) {
|
|
500
|
+
return REASONING_LIST_COMMAND.test(command) || REASONINGS_COMMAND.test(command);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isReasoningCommand(command) {
|
|
504
|
+
return REASONING_COMMAND.test(command);
|
|
505
|
+
}
|
|
506
|
+
|
|
237
507
|
export function isModelCommand(text) {
|
|
238
508
|
if (typeof text !== 'string') return false;
|
|
239
509
|
const command = text.trim();
|
|
240
|
-
return MODELS_COMMAND.test(command)
|
|
510
|
+
return MODELS_COMMAND.test(command)
|
|
511
|
+
|| MODEL_COMMAND.test(command)
|
|
512
|
+
|| REASONING_LIST_COMMAND.test(command)
|
|
513
|
+
|| REASONINGS_COMMAND.test(command)
|
|
514
|
+
|| REASONING_COMMAND.test(command);
|
|
241
515
|
}
|
|
242
516
|
|
|
243
517
|
export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
244
518
|
if (!isModelCommand(text)) return null;
|
|
245
519
|
const command = text.trim();
|
|
246
520
|
if (options.hasImages) {
|
|
247
|
-
return commandResult(t('
|
|
521
|
+
return commandResult(t('模型和推理等级命令仅支持纯文字,请移除图片后重试。'));
|
|
248
522
|
}
|
|
249
523
|
const requestOptions = rpcOptions(options.signal);
|
|
250
524
|
|
|
@@ -261,20 +535,115 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
261
535
|
}
|
|
262
536
|
}
|
|
263
537
|
|
|
264
|
-
|
|
538
|
+
if (isReasoningListCommand(command)) {
|
|
539
|
+
if (!/^\/(?:reasoninglist|reasonings)[ \t]*$/iu.test(command)) {
|
|
540
|
+
return commandResult(t(REASONING_LIST_USAGE));
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const bound = await boundSession(harness, state, key, requestOptions);
|
|
544
|
+
if (!bound) return commandResult(noReasoningSessionMessage());
|
|
545
|
+
return commandResult(formatReasoningCatalog(
|
|
546
|
+
await sessionCatalog(bound.session, requestOptions),
|
|
547
|
+
));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return commandResult(modelErrorMessage(error, 'reasoning-list'));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (isReasoningCommand(command)) {
|
|
554
|
+
const match = /^\/reasoning(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
|
|
555
|
+
if (!match) return commandResult(t(REASONING_USAGE));
|
|
556
|
+
const requested = match[1];
|
|
557
|
+
if (!requested) {
|
|
558
|
+
try {
|
|
559
|
+
const bound = await boundSession(harness, state, key, requestOptions);
|
|
560
|
+
if (!bound) return commandResult(noReasoningSessionMessage());
|
|
561
|
+
return commandResult(currentReasoningMessage(
|
|
562
|
+
await sessionCatalog(bound.session, requestOptions),
|
|
563
|
+
));
|
|
564
|
+
} catch (error) {
|
|
565
|
+
return commandResult(modelErrorMessage(error, 'reasoning-list'));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (options.pendingInteraction) {
|
|
569
|
+
return commandResult([
|
|
570
|
+
t('当前任务正在等待你的回答或审批。'),
|
|
571
|
+
'',
|
|
572
|
+
t('请先处理当前请求,或者发送 /stop 停止任务。'),
|
|
573
|
+
].join('\n'));
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
return await withSessionBindingLock(state, key, async () => {
|
|
577
|
+
const bound = await boundSession(harness, state, key, requestOptions);
|
|
578
|
+
if (!bound) return commandResult(noReasoningSessionMessage());
|
|
579
|
+
if (await sessionIsBusy(bound.session, options.control, requestOptions)) {
|
|
580
|
+
return commandResult(t('当前任务正在运行,请等待完成或先发送 /stop。'));
|
|
581
|
+
}
|
|
582
|
+
const catalog = await sessionCatalog(bound.session, requestOptions);
|
|
583
|
+
const current = catalog.current;
|
|
584
|
+
const model = modelForSelection(catalog, current);
|
|
585
|
+
|
|
586
|
+
let effort;
|
|
587
|
+
if (requested.toLowerCase() === '--default') {
|
|
588
|
+
effort = undefined;
|
|
589
|
+
} else {
|
|
590
|
+
if (!model?.reasoning) {
|
|
591
|
+
return commandResult(unsupportedReasoningMessage(current, requested, model));
|
|
592
|
+
}
|
|
593
|
+
effort = reasoningEffortById(model, requested);
|
|
594
|
+
if (!effort) {
|
|
595
|
+
const numberRequest = positiveNumberRequest(requested);
|
|
596
|
+
if (numberRequest?.index === null) {
|
|
597
|
+
return commandResult(invalidReasoningNumberMessage(requested));
|
|
598
|
+
}
|
|
599
|
+
if (!numberRequest) {
|
|
600
|
+
return commandResult(unsupportedReasoningMessage(current, requested, model));
|
|
601
|
+
}
|
|
602
|
+
effort = reasoningEffortAt(model, numberRequest.index);
|
|
603
|
+
if (!effort) return commandResult(invalidReasoningNumberMessage(requested));
|
|
604
|
+
}
|
|
605
|
+
effort = effort.id;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const selection = {
|
|
609
|
+
provider: current.provider,
|
|
610
|
+
model: current.model,
|
|
611
|
+
...(effort === undefined ? {} : { reasoningEffort: effort }),
|
|
612
|
+
};
|
|
613
|
+
const applied = await selectAndVerifyModel(bound.session, selection, requestOptions);
|
|
614
|
+
assertSessionBinding(state, key, bound.sessionId);
|
|
615
|
+
return commandResult(t(`推理等级已切换为:
|
|
616
|
+
{effort}
|
|
617
|
+
|
|
618
|
+
当前模型:{model}
|
|
619
|
+
后续消息将使用该推理等级。`, {
|
|
620
|
+
model: modelId(applied.provider, applied.model),
|
|
621
|
+
effort: reasoningEffortText(
|
|
622
|
+
model,
|
|
623
|
+
effectiveReasoningEffort(applied, model),
|
|
624
|
+
),
|
|
625
|
+
}));
|
|
626
|
+
});
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return commandResult(modelErrorMessage(error, 'reasoning-select'));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const match = /^\/model(?:[ \t]+([^\s]+)(?:[ \t]+([^\s]+))?)?[ \t]*$/iu.exec(command);
|
|
265
633
|
if (!match) return commandResult(t(MODEL_USAGE));
|
|
266
634
|
const requested = match[1];
|
|
635
|
+
const requestedEffort = match[2];
|
|
267
636
|
if (!requested) {
|
|
268
637
|
try {
|
|
269
638
|
const bound = await boundSession(harness, state, key, requestOptions);
|
|
270
639
|
if (!bound) return commandResult(noSessionMessage());
|
|
271
640
|
const catalog = await sessionCatalog(bound.session, requestOptions);
|
|
272
|
-
return commandResult(currentModelMessage(catalog
|
|
641
|
+
return commandResult(currentModelMessage(catalog));
|
|
273
642
|
} catch (error) {
|
|
274
643
|
return commandResult(modelErrorMessage(error, 'select'));
|
|
275
644
|
}
|
|
276
645
|
}
|
|
277
|
-
const numberRequest =
|
|
646
|
+
const numberRequest = positiveNumberRequest(requested);
|
|
278
647
|
if (numberRequest?.index === null) {
|
|
279
648
|
return commandResult(invalidModelNumberMessage(requested));
|
|
280
649
|
}
|
|
@@ -311,12 +680,23 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
311
680
|
t('请发送 /models 查看可用模型。'),
|
|
312
681
|
].join('\n'));
|
|
313
682
|
}
|
|
683
|
+
const targetModel = modelForSelection(catalog, selection);
|
|
684
|
+
if (requestedEffort !== undefined) {
|
|
685
|
+
const effort = reasoningEffortById(targetModel, requestedEffort);
|
|
686
|
+
if (!effort) {
|
|
687
|
+
return commandResult(unsupportedReasoningMessage(
|
|
688
|
+
selection,
|
|
689
|
+
requestedEffort,
|
|
690
|
+
targetModel,
|
|
691
|
+
));
|
|
692
|
+
}
|
|
693
|
+
selection.reasoningEffort = effort.id;
|
|
694
|
+
}
|
|
314
695
|
|
|
696
|
+
let applied;
|
|
315
697
|
if (bound) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
await bound.session.selectModel(selection, requestOptions);
|
|
698
|
+
applied = await selectAndVerifyModel(bound.session, selection, requestOptions);
|
|
699
|
+
assertSessionBinding(state, key, bound.sessionId);
|
|
320
700
|
} else {
|
|
321
701
|
if (typeof harness?.createSession !== 'function'
|
|
322
702
|
|| typeof harness?.workspaceSession !== 'function'
|
|
@@ -329,15 +709,10 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
329
709
|
throw new TypeError('Harness returned an invalid session id');
|
|
330
710
|
}
|
|
331
711
|
const session = harness.workspaceSession(sessionId);
|
|
332
|
-
|
|
333
|
-
throw new TypeError('Harness session does not support model selection');
|
|
334
|
-
}
|
|
335
|
-
await session.selectModel(selection, requestOptions);
|
|
712
|
+
applied = await selectAndVerifyModel(session, selection, requestOptions);
|
|
336
713
|
const currentSessionId = state.sessionFor(key);
|
|
337
714
|
if (typeof currentSessionId === 'string' && currentSessionId) {
|
|
338
|
-
|
|
339
|
-
changed.code = SESSION_BINDING_CHANGED;
|
|
340
|
-
throw changed;
|
|
715
|
+
throw sessionBindingChanged();
|
|
341
716
|
}
|
|
342
717
|
if (await state.setSession(key, sessionId) === false) {
|
|
343
718
|
const stale = new Error('Workspace changed while binding the new session');
|
|
@@ -347,8 +722,15 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
347
722
|
}
|
|
348
723
|
return commandResult(t(`模型已切换为:
|
|
349
724
|
{model}
|
|
725
|
+
推理等级:{effort}
|
|
350
726
|
|
|
351
|
-
|
|
727
|
+
后续消息将使用该模型和推理等级。`, {
|
|
728
|
+
model: modelId(applied.provider, applied.model),
|
|
729
|
+
effort: reasoningEffortText(
|
|
730
|
+
targetModel,
|
|
731
|
+
effectiveReasoningEffort(applied, targetModel),
|
|
732
|
+
),
|
|
733
|
+
}));
|
|
352
734
|
});
|
|
353
735
|
} catch (error) {
|
|
354
736
|
return commandResult(modelErrorMessage(error, 'select'));
|
|
@@ -401,8 +401,10 @@ export class TextHarnessBridge {
|
|
|
401
401
|
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
402
402
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
403
403
|
t('/models 按序号列出所有可用模型'),
|
|
404
|
-
t('/
|
|
405
|
-
t('
|
|
404
|
+
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
|
405
|
+
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
|
|
406
|
+
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
|
|
407
|
+
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
|
|
406
408
|
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
407
409
|
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
408
410
|
t('纯数字 ID:/preset id:<ID>'),
|
|
@@ -92,8 +92,8 @@ async function selectedWorkspacePath(value) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export async function workspacePathSnapshot(harness) {
|
|
96
|
-
const listed = await harness.listWorkspaces();
|
|
95
|
+
export async function workspacePathSnapshot(harness, options = {}) {
|
|
96
|
+
const listed = await harness.listWorkspaces(options);
|
|
97
97
|
const currentValue = typeof harness?.currentWorkspace === 'function'
|
|
98
98
|
? harness.currentWorkspace()
|
|
99
99
|
: null;
|
|
@@ -155,7 +155,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
export async function resolveSessionListWorkspace(selector, harness) {
|
|
158
|
+
export async function resolveSessionListWorkspace(selector, harness, options = {}) {
|
|
159
159
|
if (!selector) {
|
|
160
160
|
if (typeof harness?.currentWorkspace !== 'function') {
|
|
161
161
|
return { error: t('当前机器人没有可用的工作区。') };
|
|
@@ -169,7 +169,7 @@ export async function resolveSessionListWorkspace(selector, harness) {
|
|
|
169
169
|
if (typeof harness?.listWorkspaces !== 'function') {
|
|
170
170
|
return { error: t('当前机器人暂不支持按序号选择工作区。') };
|
|
171
171
|
}
|
|
172
|
-
const { paths } = await workspacePathSnapshot(harness);
|
|
172
|
+
const { paths } = await workspacePathSnapshot(harness, options);
|
|
173
173
|
const position = Number(selector);
|
|
174
174
|
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
175
175
|
return { error: t('工作区序号不存在,请先执行 /workspacelist。') };
|
|
@@ -53,8 +53,10 @@ function helpText() {
|
|
|
53
53
|
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
54
54
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
55
55
|
t('/models 按序号列出所有可用模型'),
|
|
56
|
-
t('/
|
|
57
|
-
t('
|
|
56
|
+
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
|
57
|
+
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
|
|
58
|
+
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
|
|
59
|
+
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
|
|
58
60
|
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
59
61
|
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
60
62
|
t('纯数字 ID:/preset id:<ID>'),
|
|
@@ -12,6 +12,7 @@ export const WEIXIN_QR_BASE_URL = 'https://ilinkai.weixin.qq.com/';
|
|
|
12
12
|
export const WEIXIN_PROTOCOL_VERSION = '2.4.6';
|
|
13
13
|
export const DEFAULT_BOT_TYPE = '3';
|
|
14
14
|
export const WEIXIN_CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
|
|
15
|
+
export const DEFAULT_WEIXIN_MAX_MESSAGE_CHARS = 1_800;
|
|
15
16
|
|
|
16
17
|
const WEIXIN_CDN_HOST = 'novac2c.cdn.weixin.qq.com';
|
|
17
18
|
|
|
@@ -744,7 +745,7 @@ export function weixinMessageId(message) {
|
|
|
744
745
|
return nonEmptyString(message?.client_id);
|
|
745
746
|
}
|
|
746
747
|
|
|
747
|
-
export function splitWeixinText(text, maxChars =
|
|
748
|
+
export function splitWeixinText(text, maxChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS) {
|
|
748
749
|
if (text.length <= maxChars) return [text];
|
|
749
750
|
const chunks = [];
|
|
750
751
|
let remaining = text;
|