@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.
Files changed (31) hide show
  1. package/README.en.md +23 -6
  2. package/README.md +23 -6
  3. package/assets/screenshot-menu-card.png +0 -0
  4. package/lib/client.js +140 -7
  5. package/lib/index.js +240 -192
  6. package/package.json +1 -1
  7. package/plugin-src/client/i18n.js +4 -0
  8. package/plugin-src/client/index.js +72 -11
  9. package/plugin-src/client/loopback-recovery.js +75 -0
  10. package/plugin-src/client/styles.js +10 -0
  11. package/plugin-src/host/build.mjs +3 -0
  12. package/plugin-src/host/channels/shared/rpc.mjs +11 -0
  13. package/plugin-src/host/channels/weixin/production.mjs +5 -2
  14. package/plugin-src/host/lark-sdk-handshake-patch.mjs +181 -0
  15. package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -2
  16. package/src/channels/feishu/bridge.mjs +1411 -164
  17. package/src/channels/feishu/feishu-cards.mjs +667 -56
  18. package/src/channels/feishu/feishu-runtime.mjs +129 -43
  19. package/src/channels/qq/qq-bridge.mjs +4 -2
  20. package/src/channels/shared/control-command.mjs +2 -2
  21. package/src/channels/shared/harness-client.mjs +31 -2
  22. package/src/channels/shared/i18n-en/feishu.mjs +133 -0
  23. package/src/channels/shared/i18n-en/shared-a.mjs +8 -0
  24. package/src/channels/shared/i18n-en/shared-b.mjs +43 -0
  25. package/src/channels/shared/model-command.mjs +413 -31
  26. package/src/channels/shared/text-harness-bridge.mjs +4 -2
  27. package/src/channels/shared/workspace-command.mjs +4 -4
  28. package/src/channels/wecom/wecom-bridge.mjs +4 -2
  29. package/src/channels/weixin/weixin-api.mjs +2 -1
  30. package/src/channels/weixin/weixin-bridge.mjs +6 -3
  31. 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 MODEL_USAGE = '用法:/model <序号> 或 /model <provider>/<model>';
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 { id: model.id, name: model.name };
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 = { provider: value.current.provider, model: value.current.model };
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 modelNumberRequest(requested) {
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(current) {
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('切换模型:/model <序号>'),
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
- return action === 'list' ? t('获取模型列表已取消。') : t('模型切换已取消。');
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
- return action === 'list'
188
- ? t('暂时无法获取模型列表,请稍后重试。')
189
- : t('模型切换失败,请稍后重试。');
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) || MODEL_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
- const match = /^\/model(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
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.current));
641
+ return commandResult(currentModelMessage(catalog));
273
642
  } catch (error) {
274
643
  return commandResult(modelErrorMessage(error, 'select'));
275
644
  }
276
645
  }
277
- const numberRequest = modelNumberRequest(requested);
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
- if (typeof bound.session.selectModel !== 'function') {
317
- throw new TypeError('Harness session does not support model selection');
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
- if (!session || typeof session.selectModel !== 'function') {
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
- const changed = new Error('Conversation binding changed during model selection');
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
- 后续消息将使用该模型。`, { model: modelId(selection.provider, selection.model) }));
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('/model [序号或完整模型ID] 查看或切换当前会话模型'),
405
- t('示例:先发 /models,再发 /model 2'),
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('/model [序号或完整模型ID] 查看或切换当前会话模型'),
57
- t('示例:先发 /models,再发 /model 2'),
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 = 4_000) {
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;