@xmanrui/dsh-im 2.1.0 → 2.2.1
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 +25 -8
- package/README.md +25 -8
- package/lib/client.js +142 -9
- package/lib/index.js +195 -175
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/index.js +1 -1
- package/plugin-src/client/i18n.js +5 -1
- 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/channels/shared/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +5 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -2
- package/src/channels/feishu/feishu-cards.mjs +6 -2
- package/src/channels/qq/qq-bridge.mjs +4 -2
- package/src/channels/shared/harness-client.mjs +26 -2
- package/src/channels/shared/i18n-en/feishu.mjs +2 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +8 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +39 -0
- package/src/channels/shared/model-command.mjs +356 -27
- package/src/channels/shared/text-harness-bridge.mjs +4 -2
- 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
- package/src/channels/whatsapp/whatsapp-runtime.mjs +27 -6
|
@@ -5,8 +5,13 @@ 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';
|
|
11
16
|
const MODEL_SELECTION_MISMATCH = 'model-selection-mismatch';
|
|
12
17
|
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
|
@@ -28,6 +33,35 @@ function rpcOptions(signal) {
|
|
|
28
33
|
return signal ? { signal } : {};
|
|
29
34
|
}
|
|
30
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
|
+
|
|
31
65
|
function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
32
66
|
if (!value || typeof value !== 'object'
|
|
33
67
|
|| !Array.isArray(value.groups) || !Array.isArray(value.failures)) {
|
|
@@ -46,10 +80,18 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
|
46
80
|
models: group.models.map((model) => {
|
|
47
81
|
if (!model || typeof model !== 'object'
|
|
48
82
|
|| typeof model.id !== 'string' || !model.id
|
|
49
|
-
|| typeof model.name !== 'string' || !model.name
|
|
83
|
+
|| typeof model.name !== 'string' || !model.name
|
|
84
|
+
|| (model.description !== undefined && typeof model.description !== 'string')) {
|
|
50
85
|
throw new TypeError('Harness returned an invalid model');
|
|
51
86
|
}
|
|
52
|
-
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
|
+
};
|
|
53
95
|
}),
|
|
54
96
|
};
|
|
55
97
|
});
|
|
@@ -65,10 +107,19 @@ function normalizeCatalog(value, { requireCurrent = false } = {}) {
|
|
|
65
107
|
if (value.current !== undefined) {
|
|
66
108
|
if (!value.current || typeof value.current !== 'object'
|
|
67
109
|
|| typeof value.current.provider !== 'string' || !value.current.provider
|
|
68
|
-
|| 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))) {
|
|
69
114
|
throw new TypeError('Harness returned an invalid current model');
|
|
70
115
|
}
|
|
71
|
-
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
|
+
};
|
|
72
123
|
} else if (requireCurrent) {
|
|
73
124
|
throw new TypeError('Harness returned no current model');
|
|
74
125
|
}
|
|
@@ -83,6 +134,24 @@ function sameModel(left, right) {
|
|
|
83
134
|
return left?.provider === right?.provider && left?.model === right?.model;
|
|
84
135
|
}
|
|
85
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
|
+
|
|
86
155
|
function selectionMismatch(expected, actual, source) {
|
|
87
156
|
const error = new Error(`Harness ${source} did not confirm the selected model`);
|
|
88
157
|
error.code = MODEL_SELECTION_MISMATCH;
|
|
@@ -129,12 +198,54 @@ function modelAt(catalog, requestedIndex) {
|
|
|
129
198
|
return null;
|
|
130
199
|
}
|
|
131
200
|
|
|
132
|
-
function
|
|
201
|
+
function positiveNumberRequest(requested) {
|
|
133
202
|
if (!/^\d+$/u.test(requested)) return null;
|
|
134
203
|
const index = Number(requested);
|
|
135
204
|
return { index: Number.isSafeInteger(index) && index > 0 ? index : null };
|
|
136
205
|
}
|
|
137
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
|
+
|
|
138
249
|
function invalidModelNumberMessage(requested) {
|
|
139
250
|
return [
|
|
140
251
|
t('模型序号无效:{input}', { input: safeDisplayText(requested) }),
|
|
@@ -143,6 +254,29 @@ function invalidModelNumberMessage(requested) {
|
|
|
143
254
|
].join('\n');
|
|
144
255
|
}
|
|
145
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
|
+
|
|
146
280
|
function formatCatalog(catalog) {
|
|
147
281
|
const currentId = catalog.current
|
|
148
282
|
? modelId(catalog.current.provider, catalog.current.model)
|
|
@@ -164,20 +298,72 @@ function formatCatalog(catalog) {
|
|
|
164
298
|
lines.push(`- ${safeDisplayText(failure.name) || safeDisplayText(failure.id)}`);
|
|
165
299
|
}
|
|
166
300
|
}
|
|
167
|
-
if (index > 0) lines.push('', t('切换模型:/model <序号>'));
|
|
301
|
+
if (index > 0) lines.push('', t('切换模型:/model <序号> [推理等级ID]'));
|
|
168
302
|
return lines.join('\n');
|
|
169
303
|
}
|
|
170
304
|
|
|
171
|
-
function currentModelMessage(
|
|
305
|
+
function currentModelMessage(catalog) {
|
|
172
306
|
return [
|
|
173
307
|
t('当前模型:'),
|
|
174
|
-
modelId(current.provider, current.model),
|
|
308
|
+
modelId(catalog.current.provider, catalog.current.model),
|
|
309
|
+
t('当前推理等级:{effort}', { effort: currentReasoningEffortText(catalog) }),
|
|
175
310
|
'',
|
|
176
311
|
t('查看全部模型:/models'),
|
|
177
|
-
t('
|
|
312
|
+
t('查看可用推理等级:/reasoninglist'),
|
|
313
|
+
t('切换模型:/model <序号> [推理等级ID]'),
|
|
178
314
|
].join('\n');
|
|
179
315
|
}
|
|
180
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
|
+
|
|
181
367
|
function noSessionMessage() {
|
|
182
368
|
return [
|
|
183
369
|
t('当前聊天还没有会话。'),
|
|
@@ -187,6 +373,14 @@ function noSessionMessage() {
|
|
|
187
373
|
].join('\n');
|
|
188
374
|
}
|
|
189
375
|
|
|
376
|
+
function noReasoningSessionMessage() {
|
|
377
|
+
return [
|
|
378
|
+
t('当前聊天还没有会话。'),
|
|
379
|
+
'',
|
|
380
|
+
t('请先发送一条普通消息创建会话。'),
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
190
384
|
function errorCode(error) {
|
|
191
385
|
return error?.code ?? error?.failure?.code;
|
|
192
386
|
}
|
|
@@ -200,6 +394,9 @@ function modelErrorMessage(error, action) {
|
|
|
200
394
|
return t('当前聊天绑定的会话已不存在,请重试。');
|
|
201
395
|
}
|
|
202
396
|
if (code === 'model-unavailable') {
|
|
397
|
+
if (action === 'reasoning-select') {
|
|
398
|
+
return t('无法切换推理等级。当前模型或推理等级不可用。');
|
|
399
|
+
}
|
|
203
400
|
return t('无法切换到该模型。模型当前不可用,或不支持当前会话中的图片。');
|
|
204
401
|
}
|
|
205
402
|
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
@@ -211,26 +408,32 @@ function modelErrorMessage(error, action) {
|
|
|
211
408
|
if (code === MODEL_SELECTION_MISMATCH) {
|
|
212
409
|
const expected = error?.expected;
|
|
213
410
|
const actual = error?.actual;
|
|
214
|
-
const lines = [
|
|
411
|
+
const lines = [action === 'reasoning-select'
|
|
412
|
+
? t('推理等级切换失败,请稍后重试。')
|
|
413
|
+
: t('模型切换失败,请稍后重试。')];
|
|
215
414
|
if (expected?.provider && expected?.model) {
|
|
216
|
-
lines.push('', `requested: ${safeDisplayText(
|
|
415
|
+
lines.push('', `requested: ${safeDisplayText(selectionText(expected))}`);
|
|
217
416
|
}
|
|
218
417
|
if (actual?.provider && actual?.model) {
|
|
219
418
|
const label = error?.source === 'models.current'
|
|
220
419
|
? t('当前模型:')
|
|
221
420
|
: 'selectModel.selected:';
|
|
222
|
-
lines.push(`${label} ${safeDisplayText(
|
|
421
|
+
lines.push(`${label} ${safeDisplayText(selectionText(actual))}`);
|
|
223
422
|
} else {
|
|
224
423
|
lines.push(`${error?.source ?? 'Harness'}: unconfirmed`);
|
|
225
424
|
}
|
|
226
425
|
return lines.join('\n');
|
|
227
426
|
}
|
|
228
427
|
if (code === 'cancelled' || error?.name === 'AbortError') {
|
|
229
|
-
|
|
428
|
+
if (action === 'list') return t('获取模型列表已取消。');
|
|
429
|
+
if (action === 'reasoning-list') return t('获取推理等级列表已取消。');
|
|
430
|
+
if (action === 'reasoning-select') return t('推理等级切换已取消。');
|
|
431
|
+
return t('模型切换已取消。');
|
|
230
432
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
433
|
+
if (action === 'list') return t('暂时无法获取模型列表,请稍后重试。');
|
|
434
|
+
if (action === 'reasoning-list') return t('暂时无法获取推理等级,请稍后重试。');
|
|
435
|
+
if (action === 'reasoning-select') return t('推理等级切换失败,请稍后重试。');
|
|
436
|
+
return t('模型切换失败,请稍后重试。');
|
|
234
437
|
}
|
|
235
438
|
|
|
236
439
|
async function boundSession(harness, state, key, options) {
|
|
@@ -279,12 +482,12 @@ async function selectAndVerifyModel(session, selection, options) {
|
|
|
279
482
|
throw new TypeError('Harness session does not support model selection');
|
|
280
483
|
}
|
|
281
484
|
const selected = (await session.selectModel(selection, options))?.selected;
|
|
282
|
-
if (!
|
|
485
|
+
if (!confirmsSelection(selected, selection)) {
|
|
283
486
|
throw selectionMismatch(selection, selected, 'selectModel.selected');
|
|
284
487
|
}
|
|
285
488
|
const current = (await sessionCatalog(session, options)).current;
|
|
286
|
-
if (!
|
|
287
|
-
throw selectionMismatch(
|
|
489
|
+
if (!sameSelection(current, selected)) {
|
|
490
|
+
throw selectionMismatch(selected, current, 'models.current');
|
|
288
491
|
}
|
|
289
492
|
return current;
|
|
290
493
|
}
|
|
@@ -293,17 +496,29 @@ function isModelsCommand(command) {
|
|
|
293
496
|
return MODELS_COMMAND.test(command);
|
|
294
497
|
}
|
|
295
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
|
+
|
|
296
507
|
export function isModelCommand(text) {
|
|
297
508
|
if (typeof text !== 'string') return false;
|
|
298
509
|
const command = text.trim();
|
|
299
|
-
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);
|
|
300
515
|
}
|
|
301
516
|
|
|
302
517
|
export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
303
518
|
if (!isModelCommand(text)) return null;
|
|
304
519
|
const command = text.trim();
|
|
305
520
|
if (options.hasImages) {
|
|
306
|
-
return commandResult(t('
|
|
521
|
+
return commandResult(t('模型和推理等级命令仅支持纯文字,请移除图片后重试。'));
|
|
307
522
|
}
|
|
308
523
|
const requestOptions = rpcOptions(options.signal);
|
|
309
524
|
|
|
@@ -320,20 +535,115 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
320
535
|
}
|
|
321
536
|
}
|
|
322
537
|
|
|
323
|
-
|
|
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);
|
|
324
633
|
if (!match) return commandResult(t(MODEL_USAGE));
|
|
325
634
|
const requested = match[1];
|
|
635
|
+
const requestedEffort = match[2];
|
|
326
636
|
if (!requested) {
|
|
327
637
|
try {
|
|
328
638
|
const bound = await boundSession(harness, state, key, requestOptions);
|
|
329
639
|
if (!bound) return commandResult(noSessionMessage());
|
|
330
640
|
const catalog = await sessionCatalog(bound.session, requestOptions);
|
|
331
|
-
return commandResult(currentModelMessage(catalog
|
|
641
|
+
return commandResult(currentModelMessage(catalog));
|
|
332
642
|
} catch (error) {
|
|
333
643
|
return commandResult(modelErrorMessage(error, 'select'));
|
|
334
644
|
}
|
|
335
645
|
}
|
|
336
|
-
const numberRequest =
|
|
646
|
+
const numberRequest = positiveNumberRequest(requested);
|
|
337
647
|
if (numberRequest?.index === null) {
|
|
338
648
|
return commandResult(invalidModelNumberMessage(requested));
|
|
339
649
|
}
|
|
@@ -370,6 +680,18 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
370
680
|
t('请发送 /models 查看可用模型。'),
|
|
371
681
|
].join('\n'));
|
|
372
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
|
+
}
|
|
373
695
|
|
|
374
696
|
let applied;
|
|
375
697
|
if (bound) {
|
|
@@ -400,8 +722,15 @@ export async function runModelCommand(text, harness, state, key, options = {}) {
|
|
|
400
722
|
}
|
|
401
723
|
return commandResult(t(`模型已切换为:
|
|
402
724
|
{model}
|
|
403
|
-
|
|
404
|
-
|
|
725
|
+
推理等级:{effort}
|
|
726
|
+
|
|
727
|
+
后续消息将使用该模型和推理等级。`, {
|
|
728
|
+
model: modelId(applied.provider, applied.model),
|
|
729
|
+
effort: reasoningEffortText(
|
|
730
|
+
targetModel,
|
|
731
|
+
effectiveReasoningEffort(applied, targetModel),
|
|
732
|
+
),
|
|
733
|
+
}));
|
|
405
734
|
});
|
|
406
735
|
} catch (error) {
|
|
407
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>'),
|
|
@@ -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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
|
2
3
|
extractWeixinFiles,
|
|
3
4
|
extractWeixinImages,
|
|
4
5
|
extractWeixinText,
|
|
@@ -59,8 +60,10 @@ const HELP_TEXT = () => [
|
|
|
59
60
|
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
60
61
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
61
62
|
t('/models 按序号列出所有可用模型'),
|
|
62
|
-
t('/
|
|
63
|
-
t('
|
|
63
|
+
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
|
64
|
+
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
|
|
65
|
+
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
|
|
66
|
+
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
|
|
64
67
|
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
65
68
|
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
66
69
|
t('纯数字 ID:/preset id:<ID>'),
|
|
@@ -184,7 +187,7 @@ export class WeixinHarnessBridge {
|
|
|
184
187
|
status = createWeixinBridgeStatus(),
|
|
185
188
|
logger = console,
|
|
186
189
|
replyTimeoutMs = 600_000,
|
|
187
|
-
maxMessageChars =
|
|
190
|
+
maxMessageChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
|
188
191
|
signal,
|
|
189
192
|
}) {
|
|
190
193
|
if (!api || typeof api.sendText !== 'function') throw new TypeError('Weixin API is required');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WeixinApiError } from './weixin-api.mjs';
|
|
1
|
+
import { DEFAULT_WEIXIN_MAX_MESSAGE_CHARS, WeixinApiError } from './weixin-api.mjs';
|
|
2
2
|
import { createWeixinBridgeStatus, WeixinHarnessBridge } from './weixin-bridge.mjs';
|
|
3
3
|
import {
|
|
4
4
|
connectionTestTarget,
|
|
@@ -109,7 +109,7 @@ export class WeixinRuntime {
|
|
|
109
109
|
state,
|
|
110
110
|
logger = console,
|
|
111
111
|
replyTimeoutMs = 600_000,
|
|
112
|
-
maxMessageChars =
|
|
112
|
+
maxMessageChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
|
113
113
|
startRetryDelaysMs,
|
|
114
114
|
}) {
|
|
115
115
|
if (!api || !config || !token || !harness || !state) {
|