@xmanrui/dsh-im 4.24.0 → 4.25.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 (53) hide show
  1. package/README.en.md +11 -3
  2. package/README.md +11 -3
  3. package/THIRD_PARTY_NOTICES.md +2 -0
  4. package/lib/client.js +965 -607
  5. package/lib/index.js +296 -287
  6. package/package.json +8 -2
  7. package/plugin-src/client/channel-card-meta.js +3 -9
  8. package/plugin-src/client/channel-logos.js +11 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +9 -8
  10. package/plugin-src/client/channels/feishu/index.js +14 -13
  11. package/plugin-src/client/channels/matrix/api.js +11 -0
  12. package/plugin-src/client/channels/matrix/index.js +151 -0
  13. package/plugin-src/client/channels/matrix/styles.js +36 -0
  14. package/plugin-src/client/channels/qq/index.js +9 -8
  15. package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
  16. package/plugin-src/client/channels/shared/token-channel.js +9 -8
  17. package/plugin-src/client/channels/wecom/index.js +9 -8
  18. package/plugin-src/client/channels/wecom-app/index.js +9 -8
  19. package/plugin-src/client/channels/weixin/index.js +9 -8
  20. package/plugin-src/client/channels/whatsapp/index.js +9 -8
  21. package/plugin-src/client/i18n.js +21 -0
  22. package/plugin-src/client/index.js +20 -0
  23. package/plugin-src/client/session-channel-logos.js +2 -1
  24. package/plugin-src/client/styles.js +15 -9
  25. package/plugin-src/client/update-panel.js +42 -18
  26. package/plugin-src/host/channels/matrix/index.mjs +31 -0
  27. package/plugin-src/host/channels/matrix/production.mjs +227 -0
  28. package/plugin-src/host/channels/matrix/rpc.mjs +228 -0
  29. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  30. package/plugin-src/host/channels/shared/startup-error.mjs +2 -1
  31. package/plugin-src/host/delivery-adapter.mjs +18 -0
  32. package/plugin-src/host/delivery-suggestions.mjs +13 -0
  33. package/plugin-src/host/index.mjs +3 -0
  34. package/plugin-src/host/update-service.mjs +19 -14
  35. package/scripts/verify-lan-management.mjs +1 -1
  36. package/scripts/verify-package.mjs +5 -1
  37. package/src/channels/feishu/bridge.mjs +91 -42
  38. package/src/channels/matrix/matrix-api.mjs +696 -0
  39. package/src/channels/matrix/matrix-bridge.mjs +20 -0
  40. package/src/channels/matrix/matrix-config-store.mjs +356 -0
  41. package/src/channels/matrix/matrix-controller.mjs +404 -0
  42. package/src/channels/matrix/matrix-crypto-store.mjs +279 -0
  43. package/src/channels/matrix/matrix-crypto.mjs +1014 -0
  44. package/src/channels/matrix/matrix-harness-client.mjs +11 -0
  45. package/src/channels/matrix/matrix-normalize.mjs +357 -0
  46. package/src/channels/matrix/matrix-rich-text.mjs +313 -0
  47. package/src/channels/matrix/matrix-runtime.mjs +900 -0
  48. package/src/channels/shared/command-catalog.mjs +1 -1
  49. package/src/channels/shared/i18n-en/matrix.mjs +75 -0
  50. package/src/channels/shared/i18n-en.mjs +2 -0
  51. package/src/channels/shared/session-channel-labels.mjs +1 -0
  52. package/src/channels/shared/text-harness-bridge.mjs +3 -0
  53. package/src/channels/shared/workspace-session.mjs +7 -1
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
 
5
5
  import { WeixinLogoGlyph } from '../../channel-logos.js';
6
6
  import { QrActionIcon } from '../../credential-binding.js';
7
- import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
7
+ import { AccountSettingsToggle, CollapsibleAccountSection } from '../shared/collapsible-account.js';
8
8
  import { h } from '../../i18n.js';
9
9
  import {
10
10
  WEIXIN_ENDPOINTS,
@@ -230,6 +230,13 @@ export function AccountCard({
230
230
  return h('article', { className: 'dxw-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
231
231
  h('div', { className: 'dxw-cardBody dim-botCardBody' },
232
232
  h(CollapsibleAccountSection, {
233
+ settings: h(BotSettingsButton, {
234
+ channel: 'weixin',
235
+ botId: account.botId,
236
+ botName: account.bot.name,
237
+ connected: account.connected,
238
+ accessPolicy: account.accessPolicy,
239
+ }),
233
240
  id: `dxw-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
234
241
  header: h('div', { className: 'dxw-accountTop dim-botCardTop' },
235
242
  h('div', { className: 'dxw-accountIdentity dim-botIdentity' },
@@ -249,13 +256,7 @@ export function AccountCard({
249
256
  lastCheckedAt: account.health.lastCheckedAt,
250
257
  formatCheckedTime: checkedTime,
251
258
  }),
252
- h(BotSettingsButton, {
253
- channel: 'weixin',
254
- botId: account.botId,
255
- botName: account.bot.name,
256
- connected: account.connected,
257
- accessPolicy: account.accessPolicy,
258
- })))
259
+ h(AccountSettingsToggle)))
259
260
  },
260
261
  h(WorkspaceEditor, {
261
262
  workspace: account.workspace,
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
 
5
5
  import { WhatsappLogoGlyph } from '../../channel-logos.js';
6
6
  import { QrActionIcon } from '../../credential-binding.js';
7
- import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
7
+ import { AccountSettingsToggle, CollapsibleAccountSection } from '../shared/collapsible-account.js';
8
8
  import { h } from '../../i18n.js';
9
9
  import { WorkspaceEditor } from '../../workspace-editor.js';
10
10
  import { ContextEnhancementEditor } from '../../context-enhancement.js';
@@ -208,6 +208,13 @@ export function WhatsappAccountCard({
208
208
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
209
209
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
210
210
  h(CollapsibleAccountSection, {
211
+ settings: h(BotSettingsButton, {
212
+ channel: 'whatsapp',
213
+ botId: account.botId,
214
+ botName: account.bot.name,
215
+ connected: account.connected,
216
+ accessPolicy: account.accessPolicy,
217
+ }),
211
218
  id: `wsp-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
212
219
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
213
220
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -231,13 +238,7 @@ export function WhatsappAccountCard({
231
238
  lastCheckedAt: account.health.lastCheckedAt,
232
239
  formatCheckedTime: checkedTime,
233
240
  }),
234
- h(BotSettingsButton, {
235
- channel: 'whatsapp',
236
- botId: account.botId,
237
- botName: account.bot.name,
238
- connected: account.connected,
239
- accessPolicy: account.accessPolicy,
240
- })))
241
+ h(AccountSettingsToggle)))
241
242
  },
242
243
  h(WorkspaceEditor, {
243
244
  workspace: account.workspace,
@@ -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.',
@@ -666,6 +671,22 @@ const EN = Object.freeze({
666
671
  'Gateway 长连接': 'Gateway persistent connection',
667
672
  ' Socket Mode 长连接': ' Socket Mode persistent connection',
668
673
  'Socket Mode 长连接': 'Socket Mode persistent connection',
674
+ ' CS API 长轮询': ' CS API long polling',
675
+ 'CS API 长轮询': 'CS API long polling',
676
+ '接入 Matrix 机器人': 'Connect a Matrix bot',
677
+ '连接你的 Matrix homeserver': 'Connect your Matrix homeserver',
678
+ '填写 homeserver 地址,并提供访问令牌,或者用户 ID 与密码的组合。机器人只访问该 homeserver,不连接第三方服务器。':
679
+ 'Enter the homeserver URL plus an access token, or a user id with a password. The bot only talks to that homeserver and never to third-party servers.',
680
+ 'Homeserver 地址': 'Homeserver URL',
681
+ '访问令牌(与用户 ID、密码二选一)': 'Access token (or user id + password)',
682
+ '用户 ID(可选)': 'User id (optional)',
683
+ '密码(可选)': 'Password (optional)',
684
+ '访问令牌来自 Element 设置 → 帮助 → 编辑设置,或 homeserver 的登录接口;用户 ID 与密码方式会为本安装派生稳定设备会话。':
685
+ 'The access token comes from Element settings → Help → Advanced settings, or from the homeserver login API; the user id and password flow derives a stable device session for this installation.',
686
+ '填写 homeserver 地址与访问令牌,或者用户 ID 与密码,即可让机器人以 CS API 长轮询接收消息并以富文本回复。':
687
+ 'Enter the homeserver URL with an access token, or a user id with a password, and the bot receives messages over the CS API long poll and replies with rich text.',
688
+ '使用 homeserver 与凭据接入 Matrix 机器人': 'Connect a Matrix bot with the homeserver and credentials',
689
+ '访问令牌或用户 ID 与密码': 'access token or user id with password',
669
690
  '接入 Telegram 机器人': 'Connect a Telegram bot',
670
691
  '先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
671
692
  '填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
@@ -15,6 +15,7 @@ import {
15
15
  WhatsappLogoGlyph,
16
16
  IMessageLogoGlyph,
17
17
  EmailLogoGlyph,
18
+ MatrixLogoGlyph,
18
19
  } from './channel-logos.js';
19
20
  import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
20
21
  import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
@@ -54,6 +55,9 @@ import { IMessageSettingsTab } from './channels/imessage/index.js';
54
55
  import { EmailSettingsTab, useEmailChannelEnabled } from './channels/email/index.js';
55
56
  import { installIMessageStyles } from './channels/imessage/styles.js';
56
57
  import { installEmailStyles } from './channels/email/styles.js';
58
+ import { MATRIX_RPC_CHANNEL } from './channels/matrix/api.js';
59
+ import { MatrixSettingsTab } from './channels/matrix/index.js';
60
+ import { installMatrixStyles } from './channels/matrix/styles.js';
57
61
  import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
58
62
  import {
59
63
  HOST_LANGUAGE_RPC_CHANNEL,
@@ -107,6 +111,7 @@ const CHANNELS = Object.freeze([
107
111
  { id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
108
112
  { id: 'imessage', label: 'iMessage', note: '(实验功能)' },
109
113
  { id: 'email', label: '邮箱', note: '(实验功能)' },
114
+ { id: 'matrix', label: 'Matrix', note: '(实验功能)' },
110
115
  { id: 'office', label: 'AI Office', note: '(实验功能)' },
111
116
  ]);
112
117
 
@@ -167,6 +172,11 @@ function EmailLogo() {
167
172
  h(EmailLogoGlyph));
168
173
  }
169
174
 
175
+ function MatrixLogo() {
176
+ return h('span', { className: 'dim-logo dim-logoMatrix', 'aria-hidden': 'true' },
177
+ h(MatrixLogoGlyph));
178
+ }
179
+
170
180
  function OfficeLogo() {
171
181
  return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
172
182
  h(OfficeLogoGlyph));
@@ -185,6 +195,7 @@ function ChannelLogo({ channel }) {
185
195
  if (channel === 'whatsapp') return h(WhatsappLogo);
186
196
  if (channel === 'imessage') return h(IMessageLogo);
187
197
  if (channel === 'email') return h(EmailLogo);
198
+ if (channel === 'matrix') return h(MatrixLogo);
188
199
  return h(OfficeLogo);
189
200
  }
190
201
 
@@ -210,6 +221,7 @@ export function IMSettingsTab({
210
221
  feishuRpcCall,
211
222
  imessageRpcCall,
212
223
  emailRpcCall,
224
+ matrixRpcCall,
213
225
  qqRpcCall,
214
226
  slackRpcCall,
215
227
  telegramRpcCall,
@@ -275,6 +287,7 @@ export function IMSettingsTab({
275
287
  globalSettingsRpcCall,
276
288
  imessageRpcCall,
277
289
  emailRpcCall,
290
+ matrixRpcCall,
278
291
  }, {
279
292
  location: browserLocation,
280
293
  onRecovery: reportLoopbackRecovery,
@@ -287,6 +300,7 @@ export function IMSettingsTab({
287
300
  globalSettingsRpcCall,
288
301
  imessageRpcCall,
289
302
  emailRpcCall,
303
+ matrixRpcCall,
290
304
  officeRpcCall,
291
305
  qqRpcCall,
292
306
  reportLoopbackRecovery,
@@ -427,6 +441,8 @@ export function IMSettingsTab({
427
441
  ? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
428
442
  : active.id === 'email'
429
443
  ? h(EmailSettingsTab, { rpcCall: rpcCalls.emailRpcCall })
444
+ : active.id === 'matrix'
445
+ ? h(MatrixSettingsTab, { rpcCall: rpcCalls.matrixRpcCall })
430
446
  : h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
431
447
  ),
432
448
  ));
@@ -466,6 +482,7 @@ export function apply(ctx) {
466
482
  installWhatsappStyles(),
467
483
  installIMessageStyles(),
468
484
  installEmailStyles(),
485
+ installMatrixStyles(),
469
486
  installOfficeStyles(),
470
487
  installImStyles(),
471
488
  ];
@@ -496,6 +513,8 @@ export function apply(ctx) {
496
513
  callManagementRpc(ctx.connection, IMESSAGE_RPC_CHANNEL, endpoint, payload, signal);
497
514
  const emailRpcCall = (endpoint, payload, signal) =>
498
515
  callManagementRpc(ctx.connection, EMAIL_RPC_CHANNEL, endpoint, payload, signal);
516
+ const matrixRpcCall = (endpoint, payload, signal) =>
517
+ callManagementRpc(ctx.connection, MATRIX_RPC_CHANNEL, endpoint, payload, signal);
499
518
  const slackRpcCall = (endpoint, payload, signal) =>
500
519
  callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
501
520
  const officeRpcCall = (endpoint, payload, signal) =>
@@ -526,6 +545,7 @@ export function apply(ctx) {
526
545
  imessageRpcCall,
527
546
  officeRpcCall,
528
547
  emailRpcCall,
548
+ matrixRpcCall,
529
549
  updateRpcCall,
530
550
  deliveryRpcCall,
531
551
  globalSettingsRpcCall,
@@ -2,7 +2,7 @@ import { parseSessionChannelTitle } from '../../src/channels/shared/session-chan
2
2
  import {
3
3
  WeixinLogoGlyph, FeishuLogoGlyph, DingtalkLogoGlyph, WecomLogoGlyph, QqLogoGlyph,
4
4
  SlackLogoGlyph, TelegramLogoGlyph, DiscordLogoGlyph, WhatsappLogoGlyph, OfficeLogoGlyph,
5
- IMessageLogoGlyph,
5
+ IMessageLogoGlyph, MatrixLogoGlyph,
6
6
  } from './channel-logos.js';
7
7
 
8
8
  const GLYPHS = Object.freeze({
@@ -16,6 +16,7 @@ const GLYPHS = Object.freeze({
16
16
  discord: [DiscordLogoGlyph, '#5865f2'],
17
17
  whatsapp: [WhatsappLogoGlyph, '#25d366'],
18
18
  imessage: [IMessageLogoGlyph, '#3478f6'],
19
+ matrix: [MatrixLogoGlyph, '#1a8f6f'],
19
20
  office: [OfficeLogoGlyph, '#f97316'],
20
21
  });
21
22
  const CHANNEL_ATTR = 'data-dsh-im-session-channel';
@@ -60,7 +60,9 @@ const CSS = String.raw`
60
60
  .dim-updateButton:hover:not(:disabled) { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
61
61
  .dim-updateButton:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
62
62
  .dim-updateButton:disabled { opacity: .55; cursor: default; }
63
- .dim-updateTrigger { white-space: nowrap; }
63
+ .dim-updateAction { position: relative; display: inline-flex; flex: none; }
64
+ .dim-updateTrigger { width: 30px; height: 30px; flex: none; padding: 0; }
65
+ .dim-updateTrigger svg { display: block; }
64
66
  .dim-updateBackdrop { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px; background: rgb(15 17 21 / 42%); }
65
67
  .dim-updateBackdrop, .dim-updateBackdrop * { box-sizing: border-box; }
66
68
  .dim-updateDialog { width: min(480px, 100%); max-height: calc(100vh - 48px); overflow-y: auto; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 16px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 20px 70px rgb(0 0 0 / 20%); text-align: left; }
@@ -97,8 +99,8 @@ const CSS = String.raw`
97
99
  .dim-githubLink svg { display: block; }
98
100
  .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
99
101
  .dim-githubLink:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
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
- .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
102
+ .dim-updateTooltip, .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; }
103
+ .dim-updateAction:hover .dim-updateTooltip, .dim-updateTrigger:focus-visible + .dim-updateTooltip, .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
102
104
  .dim-generalSettingsAction { position: relative; display: inline-flex; flex: none; }
103
105
  .dim-generalSettingsButton { 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); cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
104
106
  .dim-generalSettingsButton svg { display: block; }
@@ -417,10 +419,15 @@ const CSS = String.raw`
417
419
  .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px 8px; }
418
420
  .dim-collapsibleAccount { min-width: 0; display: flex; flex-direction: column; }
419
421
  .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
422
  .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); }
423
+ /* Keep the original header footprint; center both icons with equal side space. */
424
+ .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; }
425
+ .dim-accountSettingsToggle > svg { flex: none; display: block; }
426
+ .dim-accountSettingsToggle:hover { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 14%, var(--dsw-alias-bg-layer-1, #fff)); }
427
+ .dim-accountSettingsToggle:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
428
+ .dim-collapsibleChevron { width: 12px; height: 16px; transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
429
+ .dim-accountSettingsToggle[aria-expanded="true"] .dim-collapsibleChevron { transform: rotate(180deg); }
430
+ .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
431
  /* Animate height without measuring content; hide collapsed controls from focus and accessibility. */
425
432
  .dim-collapsibleBody { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .22s cubic-bezier(.4, 0, .2, 1); }
426
433
  .dim-collapsibleAccount.is-open > .dim-collapsibleBody { grid-template-rows: 1fr; }
@@ -445,11 +452,10 @@ const CSS = String.raw`
445
452
  .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
446
453
  .dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
447
454
  .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; }
455
+ .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
456
  .dim-botSettingsButton:hover { color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
450
457
  .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); }
458
+ .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
459
  .dim-deliveryPage { min-width: 0; display: grid; }
454
460
  .dim-deliveryHeader { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
455
461
  .dim-botSettingsTabsBar { min-width: 0; margin-top: 10px; border-bottom: 1px solid var(--dsw-alias-border-l2, #dfe1e5); }
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
3
3
  import validSemver from 'semver/functions/valid.js';
4
4
  import compareVersionsDescending from 'semver/functions/rcompare.js';
5
5
 
6
- import { h } from './i18n.js';
6
+ import { h, localizeText } from './i18n.js';
7
7
  import { createPollScheduler } from './lifecycle.js';
8
8
 
9
9
  export const UPDATE_RPC_CHANNEL = '/dsh-im';
@@ -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
@@ -237,6 +240,7 @@ function UpdateDialog({ children, onClose }) {
237
240
  }
238
241
 
239
242
  export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
243
+ const tooltipId = React.useId();
240
244
  const [snapshot, setSnapshot] = React.useState(null);
241
245
  const [action, setAction] = React.useState('status');
242
246
  const [error, setError] = React.useState(null);
@@ -394,28 +398,44 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
394
398
  const blocked = BLOCKED_REASONS[snapshot?.blockedReason] ?? ERROR_MESSAGES[snapshot?.blockedReason];
395
399
  const canConfirm = snapshot?.canInstall && snapshot.checkId && !activeJob && !restartRequired;
396
400
  const versionsDiffer = snapshot && clientVersion && snapshot.runningVersion !== clientVersion;
397
- const failedJob = ['failed', 'interrupted'].includes(snapshot?.job?.state);
401
+ const failedJob = ['failed', 'interrupted'].includes(snapshot?.job?.state) && !snapshot.job.recoverable;
398
402
  const jobMessage = snapshot?.job?.message;
399
- const targetVersion = snapshot?.job?.targetVersion;
403
+ const targetVersion = activeJob || restartRequired ? snapshot?.job?.targetVersion : null;
404
+ const historicalTargetVersion = !activeJob && !restartRequired ? snapshot?.job?.targetVersion : null;
405
+ const latestVersion = !activeJob && !restartRequired && !error && snapshot?.checkedAt
406
+ ? snapshot.latestVersion : null;
400
407
  const manualCommand = manualUpdateCommand(snapshot);
401
408
  const buttonLabel = action === 'checking' ? '检查中…'
402
409
  : action === 'starting' || activeJob ? '正在更新…'
403
410
  : restartRequired ? '待手动重启'
404
411
  : snapshot?.canInstall ? '更新至'
405
412
  : '检查更新';
413
+ const buttonDescription = localizeText(buttonLabel) + (buttonLabel === '更新至' ? ` v${snapshot.latestVersion}` : '');
406
414
 
407
415
  return h(React.Fragment, null,
408
- h('button', {
409
- type: 'button',
410
- className: 'dim-updateButton dim-updateTrigger',
411
- disabled: busyAction,
412
- 'aria-haspopup': 'dialog',
413
- onClick: () => {
414
- setOpen(true);
415
- if (restartRequired) void refreshStatus();
416
- else if (!snapshot?.canInstall && !snapshot?.job) void check();
416
+ h('span', { className: 'dim-updateAction' },
417
+ h('button', {
418
+ type: 'button',
419
+ className: 'dim-updateButton dim-updateTrigger',
420
+ disabled: busyAction,
421
+ 'aria-label': buttonDescription,
422
+ 'aria-describedby': tooltipId,
423
+ 'aria-haspopup': 'dialog',
424
+ onClick: () => {
425
+ setOpen(true);
426
+ if (restartRequired) void refreshStatus();
427
+ else if (!snapshot?.canInstall && !activeJob && !uncertainInstall) void check();
428
+ },
429
+ }, h('svg', {
430
+ width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
431
+ stroke: 'currentColor', strokeWidth: 2.3, strokeLinecap: 'round', strokeLinejoin: 'round',
432
+ focusable: 'false', 'aria-hidden': 'true',
417
433
  },
418
- }, buttonLabel, buttonLabel === '更新至' ? ` v${snapshot.latestVersion}` : null),
434
+ h('path', { d: 'M3 12a9 9 0 0 1 15.36-6.36L21 8' }),
435
+ h('path', { d: 'M21 3v5h-5' }),
436
+ h('path', { d: 'M21 12a9 9 0 0 1-15.36 6.36L3 16' }),
437
+ h('path', { d: 'M8 16H3v5' }))),
438
+ h('span', { id: tooltipId, className: 'dim-updateTooltip', role: 'tooltip' }, buttonDescription)),
419
439
  open ? h(UpdateDialog, { onClose: () => setOpen(false) },
420
440
  h('div', { className: 'dim-updateBody' },
421
441
  h('dl', { className: 'dim-updateVersions' },
@@ -425,6 +445,10 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
425
445
  h('dt', null, '已安装版本'), h('dd', null, `v${snapshot.installedVersion}`)) : null,
426
446
  targetVersion ? h(React.Fragment, null,
427
447
  h('dt', null, '目标版本'), h('dd', null, `v${targetVersion}`)) : null,
448
+ latestVersion ? h(React.Fragment, null,
449
+ h('dt', null, '最新版本'), h('dd', null, `v${latestVersion}`)) : null,
450
+ historicalTargetVersion ? h(React.Fragment, null,
451
+ h('dt', null, '上次更新目标'), h('dd', null, `v${historicalTargetVersion}`)) : null,
428
452
  h('dt', null, '目标 profile'), h('dd', null, snapshot?.profileName ?? '无法确认')),
429
453
  h('div', {
430
454
  className: `dim-updateStatus${error || failedJob ? ' dim-updateStatusError' : ''}`,
@@ -0,0 +1,31 @@
1
+ import { createProductionController } from './production.mjs';
2
+ import { createMatrixRpcHandler, installMatrixRpc, MATRIX_RPC_CHANNEL } from './rpc.mjs';
3
+ import { installProductionChannel } from '../shared/startup.mjs';
4
+
5
+ export const name = 'dsh-im-matrix-host';
6
+ export const inject = ['connection', 'credentials', 'typertGateway'];
7
+
8
+ export async function apply(ctx, config = {}) {
9
+ if (config?.controller) return installMatrixRpc(ctx, config.controller, config.rpcAuthority);
10
+ return installProductionChannel(ctx, config, {
11
+ channel: 'matrix',
12
+ rpcChannel: MATRIX_RPC_CHANNEL,
13
+ createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
14
+ createHandler: controller => createMatrixRpcHandler(controller),
15
+ });
16
+ }
17
+
18
+ export function createMatrixHostPlugin(config) {
19
+ return Object.freeze({ name, inject, apply: (ctx) => apply(ctx, config) });
20
+ }
21
+
22
+ export { createProductionController } from './production.mjs';
23
+ export {
24
+ MATRIX_ENDPOINTS,
25
+ MATRIX_RPC_CHANNEL,
26
+ MATRIX_RPC_ENDPOINTS,
27
+ createMatrixRpcHandler,
28
+ installMatrixRpc,
29
+ } from './rpc.mjs';
30
+ export { MatrixController } from '../../../../src/channels/matrix/matrix-controller.mjs';
31
+ export { MatrixRuntime } from '../../../../src/channels/matrix/matrix-runtime.mjs';