@xmanrui/dsh-im 4.23.0 → 4.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.23.0",
3
+ "version": "4.24.0",
4
4
  "description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -110,7 +110,12 @@
110
110
  "cordis.patch.yml",
111
111
  "lib",
112
112
  "plugin-src",
113
- "scripts",
113
+ "scripts/verify-interface-language.mjs",
114
+ "scripts/verify-lan-management.mjs",
115
+ "scripts/verify-model-setting.mjs",
116
+ "scripts/verify-package.mjs",
117
+ "scripts/verify-session-channel-logos.mjs",
118
+ "scripts/verify-session-title-prefix.mjs",
114
119
  "src",
115
120
  "PROACTIVE_DELIVERY.md",
116
121
  "PROACTIVE_DELIVERY.en.md",
@@ -466,7 +466,7 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
466
466
  );
467
467
  }
468
468
 
469
- /** One select for the step-push presentation: off / per-step posts / process card. */
469
+ /** One select for the step-push presentation. */
470
470
  function StepPushEditor({ value = false, mode = "post", disabled = false, onSave, onModeSave }) {
471
471
  const titleId = React.useId();
472
472
  const helpId = `${titleId}-help`;
@@ -496,7 +496,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
496
496
  if (value === true) await onSave?.(false);
497
497
  return;
498
498
  }
499
- const nextMode = next === "streaming_card" ? "streaming_card" : "post";
499
+ const nextMode = ["streaming_card", "live_cot"].includes(next) ? next : "post";
500
500
  // Enabling (or switching presentation) may need both writes; the flag
501
501
  // must land before the mode so the runtime never sees a mode without
502
502
  // step push enabled.
@@ -507,7 +507,9 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
507
507
 
508
508
  const helpText = current === "off"
509
509
  ? "适合日常问答:执行过程中不显示工具调用等中间步骤,只回复最终结果"
510
- : current === "streaming_card"
510
+ : current === "live_cot"
511
+ ? "使用飞书原生思考过程展示推理、工具调用与结果,最终答案单独发送"
512
+ : current === "streaming_card"
511
513
  ? "推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏"
512
514
  : "每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息";
513
515
 
@@ -529,7 +531,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
529
531
  id: helpId,
530
532
  className: "dim-presetTooltip",
531
533
  role: "tooltip",
532
- }, "设置任务执行过程的呈现方式:不显示、实时卡片或逐步消息"))),
534
+ }, "设置任务执行过程的呈现方式:不显示、原生直播、实时卡片或逐步消息"))),
533
535
  saving
534
536
  ? h("span", { className: "dim-feishuGroupControlStatus", role: "status" }, "保存中…")
535
537
  : null),
@@ -541,6 +543,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
541
543
  onChange: change,
542
544
  },
543
545
  h("option", { value: "off" }, "不显示过程(只发送最终答案)"),
546
+ h("option", { value: "live_cot" }, "实时直播(飞书原生思考过程)"),
544
547
  h("option", { value: "streaming_card" }, "实时过程卡(全程一张卡片动态更新)"),
545
548
  h("option", { value: "post" }, "逐步直播(每一步单独发一条消息)")),
546
549
  h("p", { className: "dim-feishuGroupHelp" }, helpText),
@@ -38,6 +38,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
38
38
  setContextEnhancement: 'bot.context-enhancement.set',
39
39
  setAccessPolicy: 'bot.access-policy.set',
40
40
  setAlias: 'bot.alias.set',
41
+ setThinkingTraces: 'bot.thinking-traces.set',
41
42
  });
42
43
 
43
44
  export function createTokenChannelApi(channel, connectionSummary, {
@@ -69,6 +70,8 @@ export function createTokenChannelApi(channel, connectionSummary, {
69
70
  model: normalizeModelSelection(value.model),
70
71
  agentPreset: normalizeAgentPresetId(value.agentPreset),
71
72
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
73
+ // Absent from the backend means ON; only an explicit false opts out.
74
+ thinkingTraces: value.thinkingTraces !== false,
72
75
  ...(Object.hasOwn(value, 'accessPolicy')
73
76
  ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
74
77
  : {}),
@@ -4,6 +4,7 @@ import {
4
4
  TELEGRAM_ENDPOINTS,
5
5
  telegramClientApi,
6
6
  } from './api.js';
7
+ import { ThinkingTracesSettings } from './thinking-traces.js';
7
8
  import { installTelegramStyles } from './styles.js';
8
9
 
9
10
  const channel = createTokenChannelSettings({
@@ -19,6 +20,8 @@ const channel = createTokenChannelSettings({
19
20
  emptyTitle: '接入 Telegram 机器人',
20
21
  emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
21
22
  platformLabel: 'Telegram',
23
+ AccountSettings: ThinkingTracesSettings,
24
+ accountSettingsEndpoint: 'bot.thinking-traces.set',
22
25
  });
23
26
 
24
27
  export const TelegramSettingsTab = channel.SettingsTab;
@@ -4,6 +4,18 @@ const CSS = String.raw`
4
4
  .dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
5
5
  .dtg-avatar { color: #fff; background: #229ed9; }
6
6
  .dtg-avatar svg { display: block; }
7
+ .dim-accountSettings { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l2, #eceef1); }
8
+ .dim-accountSettingsTitle { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #646a73); letter-spacing: 0.02em; }
9
+ .dim-accountSettingsRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 40px; cursor: pointer; }
10
+ .dim-accountSettingsText { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
11
+ .dim-accountSettingsLabel { font-size: 13px; font-weight: 500; color: var(--dsw-alias-label, #1f2329); }
12
+ .dim-accountSettingsHelp { font-size: 12px; color: var(--dsw-alias-label-tertiary, #8a919f); }
13
+ .dim-accountSettingsSwitch { appearance: none; flex: none; width: 32px; height: 19px; margin: 0; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-interactive-bg-hover, #eef0f3); cursor: pointer; position: relative; }
14
+ .dim-accountSettingsSwitch::before { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: var(--dsw-alias-label-secondary, #646a73); transition: transform 120ms ease, background 120ms ease; }
15
+ .dim-accountSettingsSwitch:checked { border-color: #229ed9; background: #229ed9; }
16
+ .dim-accountSettingsSwitch:checked::before { transform: translateX(13px); background: #fff; }
17
+ .dim-accountSettingsSwitch:disabled { opacity: 0.5; cursor: not-allowed; }
18
+ @media (prefers-reduced-motion: reduce) { .dim-accountSettingsSwitch::before { transition: none; } }
7
19
  `;
8
20
 
9
21
  export function installTelegramStyles() {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 会话行为设置块:思考过程留痕开关(Telegram 专属)。
3
+ *
4
+ * 开启后,机器人把 agent 的推理与工具调用作为独立的 💭/🔧 消息实时发出来,
5
+ * 最终答案单独成条;关闭则回到「占位符被最终答案覆盖」的原有单消息行为。
6
+ */
7
+ import { h } from '../../i18n.js';
8
+
9
+ export function ThinkingTracesSettings({ account, busy, onSave }) {
10
+ const checked = account.thinkingTraces !== false;
11
+ return h('div', { className: 'dim-accountSettings' },
12
+ h('h4', { className: 'dim-accountSettingsTitle' }, '会话行为'),
13
+ h('label', { className: 'dim-accountSettingsRow' },
14
+ h('span', { className: 'dim-accountSettingsText' },
15
+ h('span', { className: 'dim-accountSettingsLabel' }, '思考过程留痕'),
16
+ h('span', { className: 'dim-accountSettingsHelp' },
17
+ '实时显示思考步骤与工具调用,最终答案单独成条')),
18
+ h('input', {
19
+ type: 'checkbox',
20
+ className: 'dim-accountSettingsSwitch',
21
+ checked,
22
+ disabled: Boolean(busy),
23
+ onChange: (event) => { onSave?.({ thinkingTraces: event.target.checked }); },
24
+ })));
25
+ }
@@ -262,11 +262,13 @@ const EN = Object.freeze({
262
262
  '查看分步直推说明': 'View step push help',
263
263
  '开启后逐步推送工具调用与过程说明': 'Push tool calls and process notes step by step',
264
264
  '任务过程展示': 'Task progress display',
265
- '设置任务执行过程的呈现方式:不显示、实时卡片或逐步消息': 'Choose how the execution is presented: hidden, one live card, or step-by-step messages',
265
+ '设置任务执行过程的呈现方式:不显示、原生直播、实时卡片或逐步消息': 'Choose how execution is presented: hidden, native live process, one live card, or step-by-step messages',
266
266
  '不显示过程(只发送最终答案)': 'Hide the process (send the final answer only)',
267
+ '实时直播(飞书原生思考过程)': 'Live process (native Feishu thinking process)',
267
268
  '实时过程卡(全程一张卡片动态更新)': 'Live process card (one card updated throughout)',
268
269
  '逐步直播(每一步单独发一条消息)': 'Step-by-step feed (one message per step)',
269
270
  '适合日常问答:执行过程中不显示工具调用等中间步骤,只回复最终结果': 'For everyday Q&A: tool calls and other interim steps stay hidden, only the final result is replied',
271
+ '使用飞书原生思考过程展示推理、工具调用与结果,最终答案单独发送': 'Use Feishu’s native thinking process for reasoning, tool calls, and results, then send the final answer separately',
270
272
  '推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏': 'Recommended for long tasks: the process and the final answer update live in one card without flooding the chat',
271
273
  '每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息': 'Every step is sent as its own message (including tool calls and notes); long tasks may send many messages in a row',
272
274
  '分步直推设置保存失败,请重试。': 'Could not save the step push setting. Try again.',
@@ -667,6 +669,11 @@ const EN = Object.freeze({
667
669
  '接入 Telegram 机器人': 'Connect a Telegram bot',
668
670
  '先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
669
671
  '填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
672
+ '会话行为': 'Conversation behavior',
673
+ '思考过程留痕': 'Thinking traces',
674
+ '实时显示思考步骤与工具调用,最终答案单独成条': 'Show thinking steps and tool calls as they happen; the final answer arrives as its own message',
675
+ '请提交有效的思考过程留痕设置。': 'Submit a valid thinking-traces setting.',
676
+ '正在使用{name}…': 'Using {name}…',
670
677
  '访问模式': 'Access mode',
671
678
  'Telegram 访问模式': 'Telegram access mode',
672
679
  '查看 Telegram 访问模式说明': 'View Telegram access mode details',
@@ -324,8 +324,16 @@ export function IMSettingsTab({
324
324
  'aria-label': 'dsh-im GitHub',
325
325
  'aria-describedby': githubTooltipId,
326
326
  },
327
- h('span', null, 'GitHub'),
328
- h('span', { className: 'dim-githubArrow', 'aria-hidden': 'true' }, '↗')),
327
+ h('svg', {
328
+ width: 18,
329
+ height: 18,
330
+ viewBox: '0 0 16 16',
331
+ fill: 'currentColor',
332
+ focusable: 'false',
333
+ 'aria-hidden': 'true',
334
+ }, h('path', {
335
+ d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z',
336
+ }))),
329
337
  h('span', {
330
338
  id: githubTooltipId,
331
339
  className: 'dim-githubTooltip',
@@ -93,10 +93,10 @@ const CSS = String.raw`
93
93
  .dim-updateFooter .dim-updateButton:first-child { margin-right: auto; }
94
94
  .dim-updatePrimary, .dim-updatePrimary:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #3370ff); color: #fff; background: var(--dsw-alias-state-business-primary, #3370ff); }
95
95
  .dim-githubAction { position: relative; display: inline-flex; flex: none; }
96
- .dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
96
+ .dim-githubLink { width: 30px; height: 30px; display: grid; place-items: center; flex: none; padding: 0; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
97
+ .dim-githubLink svg { display: block; }
97
98
  .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
98
99
  .dim-githubLink:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
99
- .dim-githubArrow { font-size: 13px; line-height: 1; }
100
100
  .dim-githubTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 20; width: max-content; max-width: min(220px, 80vw); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
101
101
  .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
102
102
  .dim-generalSettingsAction { position: relative; display: inline-flex; flex: none; }
@@ -15,6 +15,7 @@ import {
15
15
  validAgentPresetPayload,
16
16
  } from './agent-preset-rpc.mjs';
17
17
  import { SET_MODEL_ENDPOINT, validModelPayload } from './model-setting-rpc.mjs';
18
+ import { SET_THINKING_TRACES_ENDPOINT, validThinkingTracesPayload } from './thinking-traces-rpc.mjs';
18
19
 
19
20
  export const TOKEN_BOT_ENDPOINTS = Object.freeze({
20
21
  status: 'connection.status',
@@ -27,6 +28,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
27
28
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
28
29
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
29
30
  setAlias: SET_ALIAS_ENDPOINT,
31
+ setThinkingTraces: SET_THINKING_TRACES_ENDPOINT,
30
32
  });
31
33
 
32
34
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -97,6 +99,10 @@ function payloadFailure(endpoint, payload) {
97
99
  return validAliasPayload(payload)
98
100
  ? null : '请输入有效的别名(最多 80 个字符)。';
99
101
  }
102
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setThinkingTraces) {
103
+ return validThinkingTracesPayload(payload)
104
+ ? null : '请提交有效的思考过程留痕设置。';
105
+ }
100
106
  return 'Unknown bot endpoint.';
101
107
  }
102
108
 
@@ -194,6 +200,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
194
200
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
195
201
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
196
202
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
203
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setThinkingTraces) {
204
+ if (typeof controller.setThinkingTraces !== 'function') throw new Error('Thinking traces update is unavailable');
205
+ value = await controller.setThinkingTraces(payload.botId, payload.thinkingTraces);
197
206
  } else {
198
207
  value = await controller.deleteBot(payload.botId);
199
208
  }
@@ -0,0 +1,11 @@
1
+ export const SET_THINKING_TRACES_ENDPOINT = 'bot.thinking-traces.set';
2
+
3
+ export function validThinkingTracesPayload(payload) {
4
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
5
+ || Reflect.ownKeys(payload).length !== 2
6
+ || !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'thinkingTraces')
7
+ || typeof payload.botId !== 'string'
8
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)
9
+ || typeof payload.thinkingTraces !== 'boolean') return false;
10
+ return true;
11
+ }
@@ -382,7 +382,7 @@ class ModernHarnessApi {
382
382
  return;
383
383
  }
384
384
  if (frame.type !== 'chunk' || frame.index !== stream.entries.length) return;
385
- stream.entries.push({
385
+ const entry = {
386
386
  type: 'transient',
387
387
  event: {
388
388
  type: 'assistant/chunk',
@@ -392,7 +392,12 @@ class ModernHarnessApi {
392
392
  time: frame.time,
393
393
  data: { turn: stream.turn, step: stream.step, chunk: frame.chunk },
394
394
  },
395
- });
395
+ };
396
+ stream.entries.push(entry);
397
+ // History only exposes these transient chunks while the attempt is open.
398
+ // Broadcast each one as it arrives so live IM renderers cannot miss a
399
+ // short reasoning phase that starts and commits between two history polls.
400
+ this.#broadcast({ type: 'session/event', sessionId, event: entry.event });
396
401
  }
397
402
 
398
403
  #withAssistantStream(sessionId, entries) {
@@ -142,22 +142,28 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
142
142
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
143
143
  }
144
144
  // Connections still have no channel-enable toggle. Checkable inputs are owned
145
- // only by the shared context editor and the saved-target Session sync row.
145
+ // only by the shared context editor, the saved-target Session sync row, the
146
+ // email settings, and the Telegram thinking-traces block.
146
147
  // The context editor contains one switch template and one mapped field-input
147
- // template; the delivery target adds one ordinary checkbox template.
148
+ // template; the delivery target adds one ordinary checkbox template; the
149
+ // Telegram thinking-traces block adds one ordinary checkbox template.
148
150
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
149
151
  const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
150
152
  const emailSettingsSource = await readFile(resolve(root, 'plugin-src/client/channels/email/index.js'), 'utf8');
153
+ const thinkingTracesSource = await readFile(resolve(root, 'plugin-src/client/channels/telegram/thinking-traces.js'), 'utf8');
151
154
  const otherClientSources = clientSources
152
155
  .replace(contextEditorSource, '')
153
156
  .replace(deliverySettingsSource, '')
154
- .replace(emailSettingsSource, '');
157
+ .replace(emailSettingsSource, '')
158
+ .replace(thinkingTracesSource, '');
155
159
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
156
160
  || (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
157
161
  || /role:\s*["']switch["']/u.test(deliverySettingsSource)
162
+ || (thinkingTracesSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
163
+ || /role:\s*["']switch["']/u.test(thinkingTracesSource)
158
164
  || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
159
- || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
160
- throw new Error('checkable inputs must be limited to context enhancement and Session sync');
165
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 4) {
166
+ throw new Error('checkable inputs must be limited to context enhancement, Session sync, email settings, and the Telegram thinking-traces toggle');
161
167
  }
162
168
  for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
163
169
  if (!host.includes(marker) || !client.includes(marker)) {
@@ -200,8 +200,15 @@ export function normalizeEmail(parsed, { address, state } = {}) {
200
200
  // attachment type under any other key is silently dropped.
201
201
  ...(attachment.contentType ? { mediaType: String(attachment.contentType) } : {}),
202
202
  // The bridge streams files via a loader so large attachments are not
203
- // held in memory until they are actually needed.
204
- load: async () => attachment.content,
203
+ // held in memory until they are actually needed. Transports differ in
204
+ // what `content` is: IMAP hands over a Buffer (already fetched with the
205
+ // body), while the Agent mailbox can only fetch bytes on demand and so
206
+ // exposes a function. Returning that function unchanged made the loader
207
+ // resolve to a function, which the inbound-file layer rejects as
208
+ // `inbound-file-data-invalid` — the download never happened.
209
+ load: async () => (typeof attachment.content === 'function'
210
+ ? attachment.content()
211
+ : attachment.content),
205
212
  })),
206
213
  reactionTarget: null,
207
214
  replyTarget: {
@@ -301,10 +301,21 @@ export class AgentMailTransport {
301
301
  this.#connected = false;
302
302
  }
303
303
 
304
- /** The newest id, used to seed a cursor. */
304
+ /**
305
+ * The newest id, used to seed a cursor.
306
+ *
307
+ * Reads the list summary only. Going through `listMessages` without an
308
+ * allowlist meant "no filter", so seeding a cursor downloaded the body of the
309
+ * newest message — mail the policy may well refuse, fetched before anyone
310
+ * asked for it. The newest id is in the first summary already.
311
+ */
305
312
  async latestUid() {
306
- const listed = await this.listMessages({ afterUid: null, limit: 1 });
307
- return listed.length > 0 ? listed[0].uid : 0;
313
+ const { document } = await this.#call(
314
+ ['message', '+list', '--dir', INBOX, '--limit', '1'],
315
+ { signal: this.#signal },
316
+ );
317
+ const items = Array.isArray(document?.data?.data) ? document.data.data : [];
318
+ return String(items[0]?.message_id ?? '').trim() || 0;
308
319
  }
309
320
 
310
321
  /**
@@ -115,6 +115,7 @@ import {
115
115
  FEISHU_STEP_PUSH_MODES,
116
116
  normalizeFeishuStepPushMode,
117
117
  } from './step-push-mode.mjs';
118
+ import { FeishuLiveCot } from './live-cot.mjs';
118
119
 
119
120
  // Lazily evaluated: t() must run after setImHostLanguage, not at import time.
120
121
  const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
@@ -607,7 +608,7 @@ export class FeishuHarnessBridge {
607
608
  #groupTopicReply = false;
608
609
  /** When true, streaming turns push tool calls and interim notes as discrete messages. */
609
610
  #stepPush = false;
610
- /** Step push presentation: 'post' (discrete messages) or 'streaming_card'. */
611
+ /** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
611
612
  #stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
612
613
  /** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
613
614
  #stepPushSendState = new Map();
@@ -4931,6 +4932,19 @@ export class FeishuHarnessBridge {
4931
4932
  // 流式卡片模式:每轮一张过程卡(原地 patch),过程与最终答案都进卡;
4932
4933
  // post 模式维持逐条直推。先预建卡片状态,纯问答回合也能在收尾时开卡。
4933
4934
  const streamingCard = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
4935
+ const liveCot = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.LIVE_COT
4936
+ && typeof this.#channel?.createCot === 'function'
4937
+ && typeof this.#channel?.writeCotEvents === 'function';
4938
+ const cot = liveCot
4939
+ ? new FeishuLiveCot(this.#channel, chatId, {
4940
+ replyTo: messageId,
4941
+ hidden: false,
4942
+ onFailure: (error) => this.#logger.warn?.(
4943
+ '[dsh-feishu] native live process failed; final answer will continue:',
4944
+ error?.message ?? String(error),
4945
+ ),
4946
+ })
4947
+ : null;
4934
4948
  if (streamingCard) {
4935
4949
  if (this.#stepCards.has(key)) {
4936
4950
  // 上一轮异常退出留下的「运行中」卡片先收尾,避免与本轮混淆。
@@ -4983,7 +4997,9 @@ export class FeishuHarnessBridge {
4983
4997
  messageId,
4984
4998
  );
4985
4999
  };
4986
- const watchdog = streamingCard ? null : this.#startThinkingStatusWatchdog(key, chatId, messageId);
5000
+ const watchdog = streamingCard || liveCot
5001
+ ? null
5002
+ : this.#startThinkingStatusWatchdog(key, chatId, messageId);
4987
5003
  let completed;
4988
5004
  try {
4989
5005
  completed = await askInWorkspaceSession({
@@ -5000,7 +5016,7 @@ export class FeishuHarnessBridge {
5000
5016
  existsOptions: { signal: this.#signal },
5001
5017
  askOptions: {
5002
5018
  ...baseAskOptions,
5003
- progressMode: 'all',
5019
+ progressMode: liveCot ? 'live' : 'all',
5004
5020
  // 提问/审批卡弹出前先定格当前过程卡,答案随之流到交互消息之后
5005
5021
  // 的新卡上(与主流式路径的 rotate 语义一致)。
5006
5022
  ...(streamingCard ? {
@@ -5012,6 +5028,15 @@ export class FeishuHarnessBridge {
5012
5028
  },
5013
5029
  } : {}),
5014
5030
  onUpdate: async (update) => {
5031
+ if (liveCot) {
5032
+ await cot.handle(update);
5033
+ if (update.type === 'assistant-message') {
5034
+ pendingStep = update;
5035
+ } else if (update.type === 'tool') {
5036
+ pendingStep = null;
5037
+ }
5038
+ return;
5039
+ }
5015
5040
  if (update.type === 'assistant-message') {
5016
5041
  if (pendingStep && Number(update.step) > Number(pendingStep.step)) {
5017
5042
  await flushPendingStep();
@@ -5054,6 +5079,7 @@ export class FeishuHarnessBridge {
5054
5079
  },
5055
5080
  });
5056
5081
  } catch (error) {
5082
+ await cot?.finish(error);
5057
5083
  if (streamingCard) {
5058
5084
  this.#stepStopFlags.delete(key);
5059
5085
  await this.#finishStepCard(key, { stopped: true });
@@ -5063,6 +5089,7 @@ export class FeishuHarnessBridge {
5063
5089
  // 回合结束(成功/失败/中断):停看门狗并撤回残留思考中心跳。
5064
5090
  await watchdog?.stop();
5065
5091
  }
5092
+ await cot?.finish();
5066
5093
  markAskComplete();
5067
5094
  const finalStepText = pendingStep ? pendingStep.text : null;
5068
5095
  pendingStep = null;
@@ -5130,7 +5157,7 @@ export class FeishuHarnessBridge {
5130
5157
  }
5131
5158
  textReceipt = createDeliveryReceipt({
5132
5159
  deliveryId: messageId,
5133
- presentation: 'feishu-step-push-post',
5160
+ presentation: liveCot ? 'feishu-live-cot-answer' : 'feishu-step-push-post',
5134
5161
  providerMessageIds,
5135
5162
  });
5136
5163
  this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
@@ -8,6 +8,7 @@ const STREAM_ELEMENT_ID = 'stream_md';
8
8
  const DEFAULT_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
9
9
  const MAX_STREAM_CHARS = 28000;
10
10
  const MAX_FILE_OPERATION_TIMEOUT_MS = 120_000;
11
+ const COT_API = '/open-apis/im/v1/message_cot';
11
12
 
12
13
  const FILE_DELIVERY_ERRORS = new Map([
13
14
  [99991672, ['artifact-permission-required', 'Feishu file delivery requires the im:resource permission.']],
@@ -365,6 +366,40 @@ export class VerifiedFeishuChannel {
365
366
  });
366
367
  }
367
368
 
369
+ /** Open one native Feishu thinking-process message. */
370
+ async createCot(chatId, { replyTo, hidden = false } = {}) {
371
+ const response = assertApiSuccess('Feishu message_cot.create', await this.#client.request({
372
+ method: 'POST',
373
+ url: `${COT_API}?receive_id_type=chat_id`,
374
+ data: {
375
+ receive_id: chatId,
376
+ ...(replyTo ? { origin_message_id: replyTo } : {}),
377
+ cot_hidden: hidden === true,
378
+ enable_badge: false,
379
+ update_feed_rank: false,
380
+ },
381
+ }));
382
+ const cotId = response?.data?.cot_id;
383
+ const messageId = response?.data?.message_id;
384
+ if (!cotId || !messageId) {
385
+ throw new Error('Feishu message_cot.create returned no cot_id/message_id');
386
+ }
387
+ return { cotId, messageId };
388
+ }
389
+
390
+ /** Append ordered AG-UI events to one native thinking process. */
391
+ async writeCotEvents(handle, events) {
392
+ assertApiSuccess('Feishu message_cot.write', await this.#client.request({
393
+ method: 'PUT',
394
+ url: COT_API,
395
+ data: {
396
+ events,
397
+ message_id: handle.messageId,
398
+ cot_id: handle.cotId,
399
+ },
400
+ }));
401
+ }
402
+
368
403
  async #sendArtifact(chatId, file, {
369
404
  replyTo,
370
405
  signal,