@xmanrui/dsh-im 4.23.0 → 4.24.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.
Files changed (41) hide show
  1. package/README.en.md +7 -2
  2. package/README.md +7 -2
  3. package/lib/client.js +498 -347
  4. package/lib/index.js +284 -283
  5. package/package.json +11 -2
  6. package/plugin-src/client/channel-card-meta.js +3 -9
  7. package/plugin-src/client/channels/dingtalk/index.js +9 -8
  8. package/plugin-src/client/channels/feishu/index.js +21 -17
  9. package/plugin-src/client/channels/qq/index.js +9 -8
  10. package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
  11. package/plugin-src/client/channels/shared/token-api.js +3 -0
  12. package/plugin-src/client/channels/shared/token-channel.js +9 -8
  13. package/plugin-src/client/channels/telegram/index.js +3 -0
  14. package/plugin-src/client/channels/telegram/styles.js +12 -0
  15. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -8
  17. package/plugin-src/client/channels/wecom-app/index.js +9 -8
  18. package/plugin-src/client/channels/weixin/index.js +9 -8
  19. package/plugin-src/client/channels/whatsapp/index.js +9 -8
  20. package/plugin-src/client/i18n.js +13 -1
  21. package/plugin-src/client/index.js +10 -2
  22. package/plugin-src/client/styles.js +12 -8
  23. package/plugin-src/client/update-panel.js +18 -8
  24. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  25. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  26. package/plugin-src/host/modern-harness-api.mjs +7 -2
  27. package/plugin-src/host/update-service.mjs +19 -14
  28. package/scripts/verify-package.mjs +11 -5
  29. package/src/channels/email/email-runtime.mjs +9 -2
  30. package/src/channels/email/transports/agent-mail.mjs +14 -3
  31. package/src/channels/feishu/bridge.mjs +122 -46
  32. package/src/channels/feishu/feishu-channel.mjs +35 -0
  33. package/src/channels/feishu/live-cot.mjs +260 -0
  34. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  35. package/src/channels/feishu/step-push-mode.mjs +10 -4
  36. package/src/channels/shared/harness-client.mjs +224 -39
  37. package/src/channels/shared/text-harness-bridge.mjs +60 -14
  38. package/src/channels/shared/workspace-session.mjs +7 -1
  39. package/src/channels/telegram/config-store.mjs +4 -1
  40. package/src/channels/telegram/telegram-controller.mjs +13 -1
  41. package/src/channels/telegram/telegram-runtime.mjs +198 -1
@@ -45,6 +45,8 @@ const EN = Object.freeze({
45
45
  'IM机器人设置': 'IM bot settings',
46
46
  'IM 面板加载失败': 'Could not load the IM panel',
47
47
  '更多机器人设置': 'More bot settings',
48
+ '更多设置': 'More settings',
49
+ '常用配置': 'Common settings',
48
50
  '机器人设置': 'Bot settings',
49
51
  '机器人设置页签': 'Bot settings tabs',
50
52
  '投递设置': 'Delivery settings',
@@ -130,6 +132,8 @@ const EN = Object.freeze({
130
132
  '已安装版本': 'Installed version',
131
133
  '目标 profile': 'Target profile',
132
134
  '目标版本': 'Target version',
135
+ '最新版本': 'Latest version',
136
+ '上次更新目标': 'Previous update target',
133
137
  '无法确认': 'Unknown',
134
138
  '发现新版本': 'Update available',
135
139
  '已是最新版本': 'Up to date',
@@ -143,6 +147,7 @@ const EN = Object.freeze({
143
147
  '更新已生效': 'Updated version is active',
144
148
  '更新失败': 'Update failed',
145
149
  '更新请求失败': 'Update request failed',
150
+ '暂时无法安装更新': 'Update installation is unavailable',
146
151
  '仅更新 DSH-IM。安装完成后需手动重启后台;本功能不会自动重启或主动刷新页面。': 'Only DSH-IM will be updated. Restart the backend manually after installation. This feature does not automatically restart it or initiate a page refresh.',
147
152
  '当前是源码或链接安装,只能检查版本;请手动更新源码,或迁移到 npm 安装。': 'This is a source or linked installation. You can check versions, but must update the source manually or migrate to an npm installation.',
148
153
  '无法确认当前 profile,请在对应的 Harness 环境中手动更新。': 'The current profile could not be identified. Update manually in the corresponding Harness environment.',
@@ -262,11 +267,13 @@ const EN = Object.freeze({
262
267
  '查看分步直推说明': 'View step push help',
263
268
  '开启后逐步推送工具调用与过程说明': 'Push tool calls and process notes step by step',
264
269
  '任务过程展示': 'Task progress display',
265
- '设置任务执行过程的呈现方式:不显示、实时卡片或逐步消息': 'Choose how the execution is presented: hidden, one live card, or step-by-step messages',
270
+ '设置任务执行过程的呈现方式:不显示、原生直播、实时卡片或逐步消息': 'Choose how execution is presented: hidden, native live process, one live card, or step-by-step messages',
266
271
  '不显示过程(只发送最终答案)': 'Hide the process (send the final answer only)',
272
+ '实时直播(飞书原生思考过程)': 'Live process (native Feishu thinking process)',
267
273
  '实时过程卡(全程一张卡片动态更新)': 'Live process card (one card updated throughout)',
268
274
  '逐步直播(每一步单独发一条消息)': 'Step-by-step feed (one message per step)',
269
275
  '适合日常问答:执行过程中不显示工具调用等中间步骤,只回复最终结果': 'For everyday Q&A: tool calls and other interim steps stay hidden, only the final result is replied',
276
+ '使用飞书原生思考过程展示推理、工具调用与结果,最终答案单独发送': 'Use Feishu’s native thinking process for reasoning, tool calls, and results, then send the final answer separately',
270
277
  '推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏': 'Recommended for long tasks: the process and the final answer update live in one card without flooding the chat',
271
278
  '每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息': 'Every step is sent as its own message (including tool calls and notes); long tasks may send many messages in a row',
272
279
  '分步直推设置保存失败,请重试。': 'Could not save the step push setting. Try again.',
@@ -667,6 +674,11 @@ const EN = Object.freeze({
667
674
  '接入 Telegram 机器人': 'Connect a Telegram bot',
668
675
  '先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
669
676
  '填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
677
+ '会话行为': 'Conversation behavior',
678
+ '思考过程留痕': 'Thinking traces',
679
+ '实时显示思考步骤与工具调用,最终答案单独成条': 'Show thinking steps and tool calls as they happen; the final answer arrives as its own message',
680
+ '请提交有效的思考过程留痕设置。': 'Submit a valid thinking-traces setting.',
681
+ '正在使用{name}…': 'Using {name}…',
670
682
  '访问模式': 'Access mode',
671
683
  'Telegram 访问模式': 'Telegram access mode',
672
684
  '查看 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; }
@@ -417,10 +417,15 @@ const CSS = String.raw`
417
417
  .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px 8px; }
418
418
  .dim-collapsibleAccount { min-width: 0; display: flex; flex-direction: column; }
419
419
  .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; -webkit-user-select: none; }
420
- .dim-collapsibleHead:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 8px; }
421
420
  .dim-collapsibleHeaderContent { min-width: 0; flex: 1 1 auto; display: flex; align-items: center; }
422
- .dim-collapsibleChevron { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 9px; height: 9px; border-right: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); border-bottom: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); transform: rotate(-45deg); transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
423
- .dim-collapsibleAccount.is-open .dim-collapsibleChevron { transform: rotate(45deg); }
421
+ /* Keep the original header footprint; center both icons with equal side space. */
422
+ .dim-accountSettingsToggle { flex: none; width: 45px; height: 32px; display: inline-flex; gap: 5px; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 8px; color: var(--dsw-alias-state-business-primary, #3370ff); background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 8%, var(--dsw-alias-bg-layer-1, #fff)); cursor: pointer; }
423
+ .dim-accountSettingsToggle > svg { flex: none; display: block; }
424
+ .dim-accountSettingsToggle:hover { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 14%, var(--dsw-alias-bg-layer-1, #fff)); }
425
+ .dim-accountSettingsToggle:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
426
+ .dim-collapsibleChevron { width: 12px; height: 16px; transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
427
+ .dim-accountSettingsToggle[aria-expanded="true"] .dim-collapsibleChevron { transform: rotate(180deg); }
428
+ .dim-accountSettingsHeader { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #e5e6eb); color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
424
429
  /* Animate height without measuring content; hide collapsed controls from focus and accessibility. */
425
430
  .dim-collapsibleBody { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .22s cubic-bezier(.4, 0, .2, 1); }
426
431
  .dim-collapsibleAccount.is-open > .dim-collapsibleBody { grid-template-rows: 1fr; }
@@ -445,11 +450,10 @@ const CSS = String.raw`
445
450
  .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
446
451
  .dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
447
452
  .dim-botSettingsAction { position: relative; flex: none; display: inline-flex; }
448
- .dim-botSettingsButton { width: 32px; height: 32px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; cursor: pointer; transition: color .15s ease, background .15s ease; }
453
+ .dim-botSettingsButton { min-height: 32px; display: inline-flex; align-items: center; gap: 6px; padding: 0 6px; border: 0; border-radius: 8px; color: var(--dsw-alias-state-business-primary, #3370ff); background: transparent; font: inherit; font-size: 12px; white-space: nowrap; cursor: pointer; transition: color .15s ease, background .15s ease; }
449
454
  .dim-botSettingsButton:hover { color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
450
455
  .dim-botSettingsButton:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
451
- .dim-botSettingsTooltip { position: absolute; top: calc(100% + 6px); right: 0; z-index: 30; width: max-content; 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; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
452
- .dim-botSettingsAction:hover .dim-botSettingsTooltip, .dim-botSettingsAction:focus-within .dim-botSettingsTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
456
+ .dim-moreSettingsChevron { width: 6px; height: 6px; margin-right: 2px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); }
453
457
  .dim-deliveryPage { min-width: 0; display: grid; }
454
458
  .dim-deliveryHeader { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
455
459
  .dim-botSettingsTabsBar { min-width: 0; margin-top: 10px; border-bottom: 1px solid var(--dsw-alias-border-l2, #dfe1e5); }
@@ -72,14 +72,17 @@ function summary(snapshot, action, error) {
72
72
  if (snapshot?.job?.state === 'restart-required' || snapshot?.blockedReason === 'pending-restart') {
73
73
  return '已安装,待手动重启';
74
74
  }
75
- if (snapshot?.job?.state === 'completed') return '更新已生效';
76
- if (snapshot?.job?.state === 'failed') return '更新失败';
77
- if (snapshot?.job?.state === 'interrupted') return '上次更新已中断,请检查安装状态后重试。';
78
75
  if (error) return '更新请求失败';
76
+ if (!snapshot?.job?.recoverable) {
77
+ if (snapshot?.job?.state === 'failed') return '更新失败';
78
+ if (snapshot?.job?.state === 'interrupted') return '上次更新已中断,请检查安装状态后重试。';
79
+ }
80
+ if (snapshot?.blockedReason === 'no-update') return '当前版本无需更新';
81
+ if (snapshot?.blockedReason) return '暂时无法安装更新';
79
82
  if (snapshot?.canInstall) return '发现新版本';
80
83
  if (snapshot?.checkedAt && snapshot.latestVersion === snapshot.runningVersion) return '已是最新版本';
81
- if (snapshot?.blockedReason === 'no-update') return '当前版本无需更新';
82
84
  if (snapshot?.checkedAt) return '已获取 npm 最新版本';
85
+ if (snapshot?.job?.state === 'completed') return '更新已生效';
83
86
  return '检查 npm 最新版本,不会自动安装。';
84
87
  }
85
88
 
@@ -102,7 +105,7 @@ function manualUpdateCommand(snapshot) {
102
105
  const targets = [
103
106
  snapshot.latestVersion,
104
107
  pendingRestart ? snapshot.installedVersion : null,
105
- snapshot.job?.state !== 'completed' ? snapshot.job?.targetVersion : null,
108
+ snapshot.job?.state !== 'completed' && !snapshot.job?.recoverable ? snapshot.job?.targetVersion : null,
106
109
  ].filter(validVersion);
107
110
  // Keep a known target pinned, and never suggest downgrading an installed/running version.
108
111
  const version = targets.length
@@ -394,9 +397,12 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
394
397
  const blocked = BLOCKED_REASONS[snapshot?.blockedReason] ?? ERROR_MESSAGES[snapshot?.blockedReason];
395
398
  const canConfirm = snapshot?.canInstall && snapshot.checkId && !activeJob && !restartRequired;
396
399
  const versionsDiffer = snapshot && clientVersion && snapshot.runningVersion !== clientVersion;
397
- const failedJob = ['failed', 'interrupted'].includes(snapshot?.job?.state);
400
+ const failedJob = ['failed', 'interrupted'].includes(snapshot?.job?.state) && !snapshot.job.recoverable;
398
401
  const jobMessage = snapshot?.job?.message;
399
- const targetVersion = snapshot?.job?.targetVersion;
402
+ const targetVersion = activeJob || restartRequired ? snapshot?.job?.targetVersion : null;
403
+ const historicalTargetVersion = !activeJob && !restartRequired ? snapshot?.job?.targetVersion : null;
404
+ const latestVersion = !activeJob && !restartRequired && !error && snapshot?.checkedAt
405
+ ? snapshot.latestVersion : null;
400
406
  const manualCommand = manualUpdateCommand(snapshot);
401
407
  const buttonLabel = action === 'checking' ? '检查中…'
402
408
  : action === 'starting' || activeJob ? '正在更新…'
@@ -413,7 +419,7 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
413
419
  onClick: () => {
414
420
  setOpen(true);
415
421
  if (restartRequired) void refreshStatus();
416
- else if (!snapshot?.canInstall && !snapshot?.job) void check();
422
+ else if (!snapshot?.canInstall && !activeJob && !uncertainInstall) void check();
417
423
  },
418
424
  }, buttonLabel, buttonLabel === '更新至' ? ` v${snapshot.latestVersion}` : null),
419
425
  open ? h(UpdateDialog, { onClose: () => setOpen(false) },
@@ -425,6 +431,10 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
425
431
  h('dt', null, '已安装版本'), h('dd', null, `v${snapshot.installedVersion}`)) : null,
426
432
  targetVersion ? h(React.Fragment, null,
427
433
  h('dt', null, '目标版本'), h('dd', null, `v${targetVersion}`)) : null,
434
+ latestVersion ? h(React.Fragment, null,
435
+ h('dt', null, '最新版本'), h('dd', null, `v${latestVersion}`)) : null,
436
+ historicalTargetVersion ? h(React.Fragment, null,
437
+ h('dt', null, '上次更新目标'), h('dd', null, `v${historicalTargetVersion}`)) : null,
428
438
  h('dt', null, '目标 profile'), h('dd', null, snapshot?.profileName ?? '无法确认')),
429
439
  h('div', {
430
440
  className: `dim-updateStatus${error || failedJob ? ' dim-updateStatusError' : ''}`,
@@ -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) {
@@ -113,7 +113,7 @@ function processAlive(pid) {
113
113
  function publicJob(job) {
114
114
  if (!job) return null;
115
115
  const { id, state, targetVersion, message } = job;
116
- return { id, state, targetVersion, message };
116
+ return { id, state, targetVersion, message, recoverable: job.recoverable === true };
117
117
  }
118
118
 
119
119
  /** One update job per profile. Persist intent before starting pnpm, and never apply a restart here. */
@@ -158,27 +158,32 @@ export function createUpdateService({
158
158
  if (!JOB_STATES.has(job.state) || typeof job.id !== 'string' || !semver.valid(job.targetVersion)) {
159
159
  throw updateError('state-unavailable');
160
160
  }
161
- if (ACTIVE_STATES.has(job.state) && job.id !== activeJob?.id) {
161
+ // Recovery is derived from the current installation, never a persisted flag.
162
+ job = { ...job, recoverable: false };
163
+ const historical = job.id !== activeJob?.id;
164
+ if (ACTIVE_STATES.has(job.state) && historical) {
162
165
  if (lock === NO_LOCK || lock?.id !== job.id || !processAlive(lock?.pid)) {
163
166
  job = { ...job, state: 'interrupted', message: 'recovery-required' };
164
167
  }
165
168
  }
166
- if (job.id !== activeJob?.id && !ACTIVE_STATES.has(job.state)) {
167
- if (lock !== NO_LOCK) return { ...job, state: 'interrupted', message: 'recovery-required' };
168
- // A later Host can retry after a verified manual repair. Never infer this
169
- // from version equality while an old process lock is still present.
170
- if (['failed', 'interrupted'].includes(job.state) && environment.packageValid === true
171
- && environment.installedVersion === runningVersion && !environment.blockedReason) {
172
- return job.targetVersion === runningVersion
173
- ? { ...job, state: 'completed', message: 'recovered' }
174
- : { ...job, recoverable: true };
175
- }
169
+ if (historical && !ACTIVE_STATES.has(job.state) && lock !== NO_LOCK) {
170
+ return { ...job, state: 'interrupted', message: 'recovery-required' };
176
171
  }
177
172
  if (job.state === 'restart-required' || job.state === 'completed') {
178
173
  if (environment.installedVersion !== job.targetVersion || environment.packageValid !== true) {
179
- return { ...job, state: 'interrupted', message: 'installation-changed' };
174
+ job = { ...job, state: 'interrupted', message: 'installation-changed' };
175
+ } else {
176
+ job = { ...job, state: runningVersion === job.targetVersion ? 'completed' : 'restart-required' };
180
177
  }
181
- return { ...job, state: runningVersion === job.targetVersion ? 'completed' : 'restart-required' };
178
+ }
179
+ // Reconcile after deriving the job state so an external upgrade or rollback
180
+ // can also recover. Keep history read-only and do not claim its target ran.
181
+ if (historical && lock === NO_LOCK && ['failed', 'interrupted'].includes(job.state)
182
+ && environment.packageValid === true && environment.eligible === true
183
+ && environment.installedVersion === runningVersion && !environment.blockedReason) {
184
+ return job.targetVersion === runningVersion
185
+ ? { ...job, state: 'completed', message: 'recovered' }
186
+ : { ...job, recoverable: true };
182
187
  }
183
188
  return job;
184
189
  }
@@ -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
  /**