@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
@@ -14,6 +14,7 @@ const CALLBACK_PROBE_SUCCESS_NOTICE = '✅ 修复完成:已实测收到 card.a
14
14
  const CALLBACK_PROBE_TIMEOUT_NOTICE = '⚠️ 修复验证超时:未收到测试卡按钮的 card.action.trigger,不能确认按钮已修复。请不要重复授权;先检查飞书开放平台的卡片回调配置,确认后再发送 /repair。';
15
15
  const CALLBACK_PROBE_SEND_FAILURE_NOTICE = '⚠️ 修复验证失败:无法发送专用测试卡,不能确认 card.action.trigger 已恢复。请不要重复授权;先检查机器人消息权限和连接状态。';
16
16
  const CALLBACK_PROBE_ABORT_NOTICE = '⚠️ 修复验证中断:Runtime 已停止,未完成 card.action.trigger 实测,不能确认修复成功。请不要重复授权;先等待机器人恢复连接。';
17
+ const REUSABLE_WS_STATES = new Set(['connected', 'connecting', 'reconnecting']);
17
18
 
18
19
  function nonEmptyString(value) {
19
20
  return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -24,6 +25,15 @@ function strictCardOperatorOpenId(event) {
24
25
  ?? nonEmptyString(event?.operator?.operator_id?.open_id);
25
26
  }
26
27
 
28
+ function websocketState(wsClient, fallback) {
29
+ try {
30
+ const state = wsClient?.getConnectionStatus?.()?.state;
31
+ return typeof state === 'string' && state ? state : fallback;
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+
27
37
  function probeError(code, message) {
28
38
  const error = new Error(message);
29
39
  error.code = code;
@@ -104,6 +114,7 @@ export class FeishuRuntime {
104
114
  #bridge = null;
105
115
  #wsClient = null;
106
116
  #starting = null;
117
+ #stopping = null;
107
118
  #abortController = null;
108
119
  #pendingCardActionProbes = new Map();
109
120
  #status;
@@ -170,25 +181,59 @@ export class FeishuRuntime {
170
181
  }
171
182
 
172
183
  async start() {
173
- if (this.#wsClient && this.#status.ready) return this.status;
174
- if (this.#starting) return this.#starting;
184
+ while (true) {
185
+ while (this.#stopping) await this.#stopping;
186
+ if (this.#starting) return this.#starting;
187
+
188
+ const wsClient = this.#wsClient;
189
+ if (wsClient) {
190
+ const state = websocketState(wsClient, this.#status.feishuLongConnectionState);
191
+ if (REUSABLE_WS_STATES.has(state)) return this.status;
192
+
193
+ await this.stop({ preserveError: state === 'failed' });
194
+ continue;
195
+ }
196
+
197
+ // A partial/failed attempt may have created resources before its
198
+ // WSClient became observable. Drain them before assigning a new attempt.
199
+ if (this.#client || this.#bridge || this.#abortController) {
200
+ await this.stop({
201
+ preserveError: this.#status.feishuLongConnectionState === 'failed',
202
+ });
203
+ continue;
204
+ }
205
+
206
+ break;
207
+ }
175
208
 
176
- this.#starting = this.#start().finally(() => {
177
- this.#starting = null;
209
+ let starting;
210
+ starting = this.#start().finally(() => {
211
+ if (this.#starting === starting) this.#starting = null;
178
212
  });
179
- return this.#starting;
213
+ this.#starting = starting;
214
+ return starting;
180
215
  }
181
216
 
182
217
  async #start() {
183
218
  const abortController = new AbortController();
184
219
  this.#abortController = abortController;
185
220
  const { signal } = abortController;
221
+ const abortError = () => (
222
+ signal.reason ?? new DOMException('Feishu runtime stopped', 'AbortError')
223
+ );
224
+ const isCurrentStart = () => (
225
+ !signal.aborted && this.#abortController === abortController
226
+ );
227
+ const assertCurrentStart = () => {
228
+ if (!isCurrentStart()) throw abortError();
229
+ };
186
230
  this.#status.startedAt = new Date().toISOString();
187
231
  this.#status.feishuLongConnectionState = 'connecting';
188
232
  this.#status.lastError = null;
189
233
 
190
234
  try {
191
235
  await this.#harness.ensureRunning({ signal });
236
+ assertCurrentStart();
192
237
  this.#status.harnessReachable = true;
193
238
 
194
239
  const sdkDomain = this.#domain === 'lark'
@@ -204,13 +249,14 @@ export class FeishuRuntime {
204
249
  this.#requestTimeoutMs,
205
250
  );
206
251
  if (httpInstance) larkConfig.httpInstance = httpInstance;
207
- this.#client = new this.#lark.Client(larkConfig);
252
+ const client = new this.#lark.Client(larkConfig);
253
+ this.#client = client;
208
254
  const channel = new VerifiedFeishuChannel({
209
- client: this.#client,
255
+ client,
210
256
  initialText: t('已连接 DeepSeek Harness,正在思考…'),
211
257
  });
212
- this.#bridge = new FeishuHarnessBridge({
213
- client: this.#client,
258
+ const bridge = new FeishuHarnessBridge({
259
+ client,
214
260
  channel,
215
261
  harness: this.#harness,
216
262
  state: this.#state,
@@ -226,22 +272,22 @@ export class FeishuRuntime {
226
272
  signal,
227
273
  logger: this.#logger,
228
274
  });
275
+ this.#bridge = bridge;
229
276
 
230
277
  const dispatcher = new this.#lark.EventDispatcher({}).register({
231
278
  'im.message.receive_v1': (event) => {
232
- this.#bridge.accept(event);
233
- return {};
279
+ if (isCurrentStart()) void bridge.accept(event);
234
280
  },
235
- 'im.message.reaction.created_v1': () => ({}),
236
- 'im.message.reaction.deleted_v1': () => ({}),
281
+ 'im.message.reaction.created_v1': () => undefined,
282
+ 'im.message.reaction.deleted_v1': () => undefined,
237
283
  // Interactive-card button callbacks (only delivered when the app
238
284
  // subscribes card.action.trigger; the number-reply fallback covers
239
285
  // apps that do not).
240
286
  'card.action.trigger': (event) => {
287
+ if (!isCurrentStart()) return;
241
288
  this.#status.cardActionsReceived += 1;
242
289
  this.#status.lastCardActionAt = new Date().toISOString();
243
- if (!this.#consumeCardActionProbe(event)) this.#bridge.onCardAction(event);
244
- return {};
290
+ if (!this.#consumeCardActionProbe(event)) void bridge.onCardAction(event);
245
291
  },
246
292
  });
247
293
 
@@ -249,37 +295,49 @@ export class FeishuRuntime {
249
295
  let settleError;
250
296
  const ready = new Promise((resolve, reject) => {
251
297
  let settled = false;
252
- const timer = setTimeout(() => {
298
+ const onAbort = () => {
299
+ settleError(abortError());
300
+ };
301
+ const settle = (callback, value) => {
253
302
  if (settled) return;
254
303
  settled = true;
255
- reject(new Error(`Feishu WebSocket handshake timed out after ${this.#connectTimeoutMs}ms`));
304
+ clearTimeout(timer);
305
+ signal.removeEventListener('abort', onAbort);
306
+ callback(value);
307
+ };
308
+ const timer = setTimeout(() => {
309
+ settle(
310
+ reject,
311
+ new Error(`Feishu WebSocket handshake timed out after ${this.#connectTimeoutMs}ms`),
312
+ );
256
313
  }, this.#connectTimeoutMs);
257
314
  settleReady = () => {
258
- if (settled) return;
259
- settled = true;
260
- clearTimeout(timer);
261
- resolve();
315
+ settle(resolve);
262
316
  };
263
317
  settleError = (error) => {
264
- if (settled) return;
265
- settled = true;
266
- clearTimeout(timer);
267
- reject(error);
318
+ settle(reject, error);
268
319
  };
320
+ signal.addEventListener('abort', onAbort, { once: true });
321
+ if (signal.aborted) onAbort();
269
322
  });
323
+ // The SDK constructor can throw before Promise.all attaches below.
324
+ // Keep the abort-driven rejection observed in that path as well.
325
+ void ready.catch(() => undefined);
270
326
 
271
- this.#wsClient = new this.#lark.WSClient({
327
+ const wsClient = new this.#lark.WSClient({
272
328
  ...larkConfig,
273
329
  ...(this.#wsAgent ? { agent: this.#wsAgent } : {}),
274
330
  loggerLevel: this.#lark.LoggerLevel.info,
275
- handshakeTimeoutMs: 15000,
331
+ handshakeTimeoutMs: this.#connectTimeoutMs,
276
332
  onReady: () => {
333
+ if (!isCurrentStart()) return;
277
334
  this.#status.feishuLongConnectionState = 'connected';
278
335
  this.#status.ready = true;
279
336
  this.#status.lastError = null;
280
337
  settleReady();
281
338
  },
282
339
  onError: (error) => {
340
+ if (!isCurrentStart()) return;
283
341
  this.#status.feishuLongConnectionState = 'failed';
284
342
  this.#status.ready = false;
285
343
  this.#status.lastError = error?.message ?? String(error);
@@ -287,25 +345,35 @@ export class FeishuRuntime {
287
345
  settleError(error);
288
346
  },
289
347
  onReconnecting: () => {
348
+ if (!isCurrentStart()) return;
290
349
  this.#status.feishuLongConnectionState = 'reconnecting';
291
350
  this.#status.ready = false;
292
351
  },
293
352
  onReconnected: () => {
353
+ if (!isCurrentStart()) return;
294
354
  this.#status.feishuLongConnectionState = 'connected';
295
355
  this.#status.ready = true;
296
356
  this.#status.lastError = null;
297
357
  },
298
358
  });
299
- await this.#wsClient.start({ eventDispatcher: dispatcher }).catch((error) => {
300
- settleError(error);
301
- });
302
- await ready;
359
+ this.#wsClient = wsClient;
360
+ const wsStarted = Promise.resolve()
361
+ .then(() => wsClient.start({ eventDispatcher: dispatcher }))
362
+ .catch((error) => {
363
+ settleError(error);
364
+ throw error;
365
+ });
366
+ await Promise.all([wsStarted, ready]);
367
+ assertCurrentStart();
303
368
  return this.status;
304
369
  } catch (error) {
370
+ // stop() owns the terminal idle state for an explicitly aborted start.
371
+ // In particular, do not let the rejected handshake waiter overwrite it.
372
+ if (signal.aborted) throw error;
305
373
  this.#status.ready = false;
306
374
  this.#status.feishuLongConnectionState = 'failed';
307
375
  this.#status.lastError = error?.message ?? String(error);
308
- await this.stop({ preserveError: true });
376
+ await this.#cleanup({ preserveError: true, abortController });
309
377
  throw error;
310
378
  }
311
379
  }
@@ -492,10 +560,30 @@ export class FeishuRuntime {
492
560
  });
493
561
  }
494
562
 
495
- async stop({ preserveError = false } = {}) {
496
- const error = preserveError ? this.#status.lastError : null;
563
+ stop(options = {}) {
564
+ if (this.#stopping) return this.#stopping;
565
+
566
+ let stopping;
567
+ stopping = this.#stop(options).finally(() => {
568
+ if (this.#stopping === stopping) this.#stopping = null;
569
+ });
570
+ this.#stopping = stopping;
571
+ return stopping;
572
+ }
573
+
574
+ async #stop({ preserveError = false } = {}) {
497
575
  const abortController = this.#abortController;
498
- this.#abortController = null;
576
+ if (this.#abortController === abortController) this.#abortController = null;
577
+ abortController?.abort(new DOMException('Feishu runtime stopped', 'AbortError'));
578
+
579
+ const starting = this.#starting;
580
+ if (starting) await starting.catch(() => undefined);
581
+ return this.#cleanup({ preserveError, abortController });
582
+ }
583
+
584
+ async #cleanup({ preserveError = false, abortController } = {}) {
585
+ const error = preserveError ? this.#status.lastError : null;
586
+ if (this.#abortController === abortController) this.#abortController = null;
499
587
  abortController?.abort(new DOMException('Feishu runtime stopped', 'AbortError'));
500
588
  for (const probe of this.#pendingCardActionProbes.values()) {
501
589
  clearTimeout(probe.timeout);
@@ -508,14 +596,12 @@ export class FeishuRuntime {
508
596
  }
509
597
  this.#pendingCardActionProbes.clear();
510
598
  this.#status.ready = false;
511
- if (this.#wsClient) {
512
- this.#wsClient.close({ force: true });
513
- this.#wsClient = null;
514
- }
515
- if (this.#bridge) {
516
- await this.#bridge.waitForIdle();
517
- this.#bridge = null;
518
- }
599
+ const wsClient = this.#wsClient;
600
+ this.#wsClient = null;
601
+ wsClient?.close({ force: true });
602
+ const bridge = this.#bridge;
603
+ this.#bridge = null;
604
+ if (bridge) await bridge.waitForIdle();
519
605
  this.#client = null;
520
606
  this.#status.feishuLongConnectionState = preserveError ? 'failed' : 'idle';
521
607
  this.#status.lastError = error;
@@ -70,8 +70,10 @@ function helpText() {
70
70
  t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
71
71
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
72
72
  t('/models 按序号列出所有可用模型'),
73
- t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
74
- t('示例:先发 /models,再发 /model 2'),
73
+ t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
74
+ t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
75
+ t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
76
+ t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
75
77
  t('/presetlist 按序号列出可用 Agent Preset'),
76
78
  t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
77
79
  t('纯数字 ID:/preset id:<ID>'),
@@ -70,7 +70,7 @@ export async function runControlCommand(text, harness, state, key, {
70
70
 
71
71
  const session = boundSession(harness, state, key);
72
72
  if (!session) {
73
- return commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
73
+ return commandResult(t('当前聊天没有绑定会话,无法补充指令。请先绑定会话。'));
74
74
  }
75
75
  if (typeof session.steerActiveTurn !== 'function') {
76
76
  throw new TypeError('Harness session does not support steering active turns');
@@ -82,5 +82,5 @@ export async function runControlCommand(text, harness, state, key, {
82
82
  );
83
83
  return steered
84
84
  ? commandResult(t('已提交补充指令,Agent 会在下一步读取。'))
85
- : commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
85
+ : commandResult(t('任务已结束,没有正在运行的任务,无法补充指令。请直接发送消息开始新任务。'));
86
86
  }
@@ -104,7 +104,26 @@ function validModelSelection(value) {
104
104
  && Boolean(value.provider)
105
105
  && typeof value.model === 'string'
106
106
  && Boolean(value.model)
107
- && (value.reasoningEffort === undefined || typeof value.reasoningEffort === 'string');
107
+ && (value.reasoningEffort === undefined
108
+ || (typeof value.reasoningEffort === 'string' && Boolean(value.reasoningEffort)));
109
+ }
110
+
111
+ function validModelReasoning(value) {
112
+ return value !== null
113
+ && typeof value === 'object'
114
+ && Array.isArray(value.efforts)
115
+ && value.efforts.length > 0
116
+ && value.efforts.every((effort) => (
117
+ effort !== null
118
+ && typeof effort === 'object'
119
+ && typeof effort.id === 'string'
120
+ && Boolean(effort.id)
121
+ && typeof effort.name === 'string'
122
+ && Boolean(effort.name)
123
+ && (effort.description === undefined || typeof effort.description === 'string')
124
+ ))
125
+ && (value.defaultEffort === undefined
126
+ || (typeof value.defaultEffort === 'string' && Boolean(value.defaultEffort)));
108
127
  }
109
128
 
110
129
  function validateModelCatalog(value, method, { session = false } = {}) {
@@ -123,7 +142,9 @@ function validateModelCatalog(value, method, { session = false } = {}) {
123
142
  for (const model of group.models) {
124
143
  if (!model || typeof model !== 'object'
125
144
  || typeof model.id !== 'string' || !model.id
126
- || typeof model.name !== 'string' || !model.name) {
145
+ || typeof model.name !== 'string' || !model.name
146
+ || (model.description !== undefined && typeof model.description !== 'string')
147
+ || (model.reasoning !== undefined && !validModelReasoning(model.reasoning))) {
127
148
  throw new Error(`Harness returned an invalid response for ${method}`);
128
149
  }
129
150
  }
@@ -223,6 +244,11 @@ function workspaceSessions(workspace, archivedSessionIds, sessionList) {
223
244
  origin: summary?.origin === 'subagent' ? 'subagent' : null,
224
245
  summaryAvailable: summary !== undefined,
225
246
  };
247
+ const lastSeq = summary?.projections?.asOfSeq;
248
+ // This is the projection's durable lower bound, not necessarily the live
249
+ // log tail for a cold session. Harness uses -1 as the legitimate bound
250
+ // for a session with no projected events yet.
251
+ if (Number.isSafeInteger(lastSeq) && lastSeq >= -1) session.lastSeq = lastSeq;
226
252
  const time = sessionTimeMs(summary);
227
253
  if (time !== null) session.time = time;
228
254
  return session;
@@ -723,6 +749,9 @@ export class HarnessClient {
723
749
  sessionId,
724
750
  provider: selection.provider,
725
751
  model: selection.model,
752
+ ...(selection.reasoningEffort === undefined
753
+ ? {}
754
+ : { reasoningEffort: selection.reasoningEffort }),
726
755
  }, 30_000, signal ? { ...options, signal } : options);
727
756
  };
728
757
  const value = this.#sessionMaintenanceExecutor
@@ -131,8 +131,129 @@ export default {
131
131
  '已取消关注「{title}」。': 'Unwatched "{title}".',
132
132
  '取消失败:{message}': 'Could not unwatch: {message}',
133
133
  '飞书交互问题发送失败。': 'Failed to send the Feishu interaction question.',
134
+ '当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。':
135
+ 'The current task is still running. Stop it or wait for it to finish before starting a new session.',
136
+ '请输入补充指令后再提交。': 'Enter an instruction before submitting.',
137
+ '操作过于频繁,请稍后再试。': 'Too many card actions are pending. Please try again shortly.',
138
+ '卡片操作失败,请稍后重试。': 'The card action failed. Please try again later.',
139
+ '请先选择至少一个会话。': 'Select at least one session first.',
140
+ '已批量关注 {count} 个会话。': 'Now watching {count} sessions.',
141
+ '已批量关注 {count} 个会话,另有 {failed} 个未成功。':
142
+ 'Now watching {count} sessions; {failed} could not be processed.',
143
+ '已关注(或已达关注上限)。': 'Sessions are already watched, or the watch limit has been reached.',
144
+ '已取消关注 {count} 个会话。': 'Stopped watching {count} sessions.',
145
+ '已取消关注 {count} 个会话,另有 {failed} 个未成功。':
146
+ 'Stopped watching {count} sessions; {failed} could not be processed.',
147
+ '所选会话均未处理成功,请稍后重试。':
148
+ 'None of the selected sessions could be processed. Please try again later.',
149
+ '所选会话已在关注列表中。': 'The selected sessions are already being watched.',
150
+ '所选会话已不在关注列表中。': 'The selected sessions are no longer in the watch list.',
151
+ '未取消任何关注。': 'No watches were removed.',
152
+ '当前没有绑定的会话,请先从会话列表选择。':
153
+ 'No session is currently bound. Select one from the session list first.',
154
+ '已就绪,直接发消息即可继续当前会话。':
155
+ 'Ready. Send a message to continue the current session.',
156
+ '修复需在私聊中验证接入者身份,请直接发送 /repair 开始。':
157
+ 'Repair must verify the owner in a direct chat. Send /repair there to begin.',
158
+ '暂时无法获取预设列表,请稍后重试。':
159
+ 'Could not load the preset list. Please try again later.',
160
+ '暂时无法获取系统状态,请稍后重试。':
161
+ 'Could not load system status. Please try again later.',
162
+ '连接正常': 'Connected',
163
+ '预设:{preset}': 'Preset: {preset}',
164
+ '未知': 'Unknown',
165
+ '/stop 执行完成。': '/stop completed.',
166
+ '停止任务失败,请稍后重试。': 'Could not stop the task. Please try again later.',
167
+ '已提交补充指令。': 'Instruction submitted.',
168
+ '预设重置失败,请稍后重试。': 'Could not reset the preset. Please try again later.',
169
+ '预设切换失败,请稍后重试。': 'Could not switch the preset. Please try again later.',
134
170
 
135
171
  // feishu/feishu-cards.mjs — interactive cards
172
+ '🤖 助手中心': '🤖 Assistant center',
173
+ '**设置**': '**Settings**',
174
+ '切换会话': 'Switch session',
175
+ '选择会话(当前未绑定)': 'Select a session (none currently bound)',
176
+ '跟随默认': 'Follow default',
177
+ '切换预设': 'Switch preset',
178
+ '切换模型': 'Switch model',
179
+ '当前工作区暂无可用会话。': 'There are no available sessions in the current workspace.',
180
+ '🤖 切换预设': '🤖 Switch preset',
181
+ '🧠 切换模型': '🧠 Switch model',
182
+ '🆕 新会话': '🆕 New session',
183
+ '📋 会话/关注': '📋 Sessions / watches',
184
+ '🗂 工作区列表': '🗂 Workspace list',
185
+ '**任务控制**': '**Task controls**',
186
+ '⏹ 停止': '⏹ Stop',
187
+ '📐 压缩': '📐 Compact',
188
+ '**补充指令**': '**Steer task**',
189
+ '选择补充指令': 'Choose an instruction',
190
+ '继续': 'Continue',
191
+ '加速运行': 'Move faster',
192
+ '总结当前进展': 'Summarize current progress',
193
+ '更简洁些': 'Be more concise',
194
+ '更详细些': 'Be more detailed',
195
+ '✏️ 更多 / 自定义…': '✏️ More / custom…',
196
+ '🗄 归档:{state}': '🗄 Archived: {state}',
197
+ '已显示': 'shown',
198
+ '已隐藏': 'hidden',
199
+ '切换归档显示': 'Toggle archived sessions',
200
+ '📊 状态': '📊 Status',
201
+ '📖 帮助': '📖 Help',
202
+ '**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5**🔧修复 · **6**帮助':
203
+ '**Number fallback**\n**1** Workspace list · **2** New session · **3** Session list · **4** Status\n**5** 🔧 Repair · **6** Help',
204
+ '跟随 Host 默认{default}': 'Follow Host default{default}',
205
+ '**当前**:{value}': '**Current**: {value}',
206
+ '**Host 默认**:{value}': '**Host default**: {value}',
207
+ '未设置': 'Not set',
208
+ '选择预设': 'Select a preset',
209
+ '🔄 跟随默认': '🔄 Follow default',
210
+ '当前没有可选择的预设。': 'There are no presets to choose from.',
211
+ '🤖 预设列表': '🤖 Preset list',
212
+ '**当前模型**:{model}': '**Current model**: {model}',
213
+ '选择模型': 'Select a model',
214
+ '🧠 模型列表': '🧠 Model list',
215
+ '{icon} 飞书机器人{state}': '{icon} Feishu bot {state}',
216
+ '已连接': 'connected',
217
+ '未连接': 'disconnected',
218
+ '📂 工作区:`{workspace}`': '📂 Workspace: `{workspace}`',
219
+ '🤖 预设:{preset}': '🤖 Preset: {preset}',
220
+ '🧠 模型:{model}': '🧠 Model: {model}',
221
+ '💬 会话:{count} 个': '💬 Sessions: {count}',
222
+ '📊 系统状态': '📊 System status',
223
+ '📋 会话 / 工作区': '📋 Sessions / workspace',
224
+ '/sessionlist 列出工作区会话': '/sessionlist List workspace sessions',
225
+ '/session ID 绑定已有会话': '/session ID Bind an existing session',
226
+ '/workspacelist 列出工作区': '/workspacelist List workspaces',
227
+ '/workspace 路径 切换工作区': '/workspace PATH Switch workspace',
228
+ '/new 开启全新会话': '/new Start a new session',
229
+ '📊 状态 / 压缩': '📊 Status / compact',
230
+ '/status 连接状态': '/status Connection status',
231
+ '/compact 压缩当前会话上下文': '/compact Compact the current session context',
232
+ '/archived on/off 会话列表显示/隐藏归档': '/archived on/off Show/hide archived sessions',
233
+ '👁 关注': '👁 Watches',
234
+ '/watch ID 关注会话(完成后推送)': '/watch ID Watch a session (push on completion)',
235
+ '/watchlist 关注列表': '/watchlist List watched sessions',
236
+ '/unwatch ID 取消关注': '/unwatch ID Stop watching a session',
237
+ '🤖 预设 / 模型': '🤖 Presets / models',
238
+ '/models 列出模型': '/models List models',
239
+ '🎮 任务控制': '🎮 Task controls',
240
+ '/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
241
+ '**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
242
+ '**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
243
+ '**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/repair` — 修复卡片按钮':
244
+ '**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/repair` — repair card buttons',
245
+ '**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**修复 · **6**帮助':
246
+ '**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Repair · **6** Help',
247
+ '从下方下拉选择补充指令;最后一项可自定义输入。':
248
+ 'Choose an instruction below; the last option lets you enter a custom one.',
249
+ '当前没有绑定会话,请先绑定会话再补充指令。':
250
+ 'No session is bound. Bind one before steering the task.',
251
+ '➕ 补充指令': '➕ Steer task',
252
+ '输入补充指令后点「提交」,发送给当前运行的任务。':
253
+ 'Enter an instruction and press Submit to send it to the running task.',
254
+ '输入你的补充指令': 'Enter your instruction',
255
+ '提交': 'Submit',
256
+ '➕ 自定义指令': '➕ Custom instruction',
136
257
  '🤖 助手菜单': '🤖 Assistant menu',
137
258
  '**点击按钮或直接回复数字**': '**Tap a button or reply with a number**',
138
259
  '1 · 会话列表': '1 · Sessions',
@@ -182,6 +303,18 @@ export default {
182
303
  '任务完成会自动推送,回复数字或点按钮取消关注:':
183
304
  'Completion is pushed automatically. Reply with a number or tap a button to unwatch:',
184
305
  '👁 关注列表': '👁 Watch list',
306
+ '⭐ 取消关注': '⭐ Unwatch',
307
+ '☆ 关注': '☆ Watch',
308
+ '🔍 关注列表': '🔍 Watch list',
309
+ '勾选要关注的会话': 'Select sessions to watch',
310
+ '勾选要取消关注的会话': 'Select sessions to unwatch',
311
+ '当前没有关注的会话。任务完成会自动推送结果。':
312
+ 'No sessions are being watched. Results are pushed automatically when tasks complete.',
313
+ '当前关注 **{count}** 个会话:': 'Currently watching **{count}** sessions:',
314
+ '**➕ 添加关注**(多选下拉勾选)': '**➕ Add watch** (select from the multi-select dropdown)',
315
+ '**➖ 取消关注**(多选下拉勾选)': '**➖ Remove watch** (select from the multi-select dropdown)',
316
+ '📋 会话列表': '📋 Session list',
317
+ '🔙 返回菜单': '🔙 Back to menu',
185
318
  '已完成': 'Completed',
186
319
  '已停止': 'Stopped',
187
320
  '已中止': 'Aborted',
@@ -50,9 +50,17 @@ export default {
50
50
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
51
51
  '/session <Session ID or workspace index> Bind this chat to the specified session',
52
52
  '/models 按序号列出所有可用模型': '/models List all available models by index',
53
+ '/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级':
54
+ '/reasoninglist or /reasonings List reasoning efforts for the current model by index',
55
+ '/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级':
56
+ '/reasoning [index, effort ID, or --default] Show or switch the current reasoning effort',
53
57
  '/model [序号或完整模型ID] 查看或切换当前会话模型':
54
58
  '/model [index or full model ID] Show or switch the model of the current session',
59
+ '/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型':
60
+ '/model [index or full model ID] [reasoning effort ID] Show or switch the current Session model',
55
61
  '示例:先发 /models,再发 /model 2': 'Example: send /models first, then /model 2',
62
+ '示例:先发 /models,再发 /model 2 [推理等级ID]':
63
+ 'Example: send /models first, then /model 2 [reasoning effort ID]',
56
64
  '/presetlist 按序号列出可用 Agent Preset': '/presetlist List available Agent Presets by index',
57
65
  '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset':
58
66
  '/preset [index or full ID] Show or set the Agent Preset of this bot',