@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.24.0",
4
- "description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
3
+ "version": "4.25.0",
4
+ "description": "把十二种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect twelve IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -21,6 +21,7 @@
21
21
  "discord",
22
22
  "whatsapp",
23
23
  "imessage",
24
+ "matrix",
24
25
  "ai-office"
25
26
  ],
26
27
  "author": "xmanrui (https://github.com/xmanrui)",
@@ -80,6 +81,10 @@
80
81
  {
81
82
  "name": "wings1848",
82
83
  "url": "https://github.com/wings1848"
84
+ },
85
+ {
86
+ "name": "Zhe (Phil) Yang",
87
+ "url": "https://github.com/yangzhe1991"
83
88
  }
84
89
  ],
85
90
  "license": "MIT",
@@ -162,6 +167,7 @@
162
167
  "node": ">=22.19"
163
168
  },
164
169
  "dependencies": {
170
+ "@matrix-org/olm": "3.2.15",
165
171
  "@tencent-connect/qqbot-connector": "1.2.0",
166
172
  "@tencent-connect/qqbot-nodejs": "1.0.4",
167
173
  "@wecom/aibot-node-sdk": "1.0.7",
@@ -7,7 +7,7 @@ export const BotSettingsContext = React.createContext(Object.freeze({
7
7
  openBotSettings() {},
8
8
  }));
9
9
 
10
- function SettingsGlyph() {
10
+ export function SettingsGlyph() {
11
11
  return h('svg', {
12
12
  viewBox: '0 0 24 24',
13
13
  width: 16,
@@ -32,14 +32,12 @@ export function BotSettingsButton({
32
32
  channelSettings,
33
33
  }) {
34
34
  const { openBotSettings } = React.useContext(BotSettingsContext);
35
- const tooltipId = React.useId();
36
35
  return h('span', { className: 'dim-botSettingsAction' },
37
36
  h('button', {
38
37
  type: 'button',
39
38
  className: 'dim-botSettingsButton',
40
39
  'data-delivery-channel': channel,
41
40
  'aria-label': '更多机器人设置',
42
- 'aria-describedby': tooltipId,
43
41
  onClick: () => openBotSettings?.({
44
42
  ...(channelSettings && typeof channelSettings === 'object' ? channelSettings : {}),
45
43
  channel,
@@ -48,12 +46,8 @@ export function BotSettingsButton({
48
46
  connected: Boolean(connected),
49
47
  accessPolicy,
50
48
  }),
51
- }, h(SettingsGlyph)),
52
- h('span', {
53
- id: tooltipId,
54
- className: 'dim-botSettingsTooltip',
55
- role: 'tooltip',
56
- }, '更多机器人设置'));
49
+ }, h(SettingsGlyph), h('span', null, '更多设置'),
50
+ h('span', { className: 'dim-moreSettingsChevron', 'aria-hidden': 'true' })));
57
51
  }
58
52
 
59
53
  function messageErrorTime(value) {
@@ -155,6 +155,17 @@ export function IMessageLogoGlyph({ size } = {}) {
155
155
  }));
156
156
  }
157
157
 
158
+ export function MatrixLogoGlyph({ size } = {}) {
159
+ return h('svg', {
160
+ ...dimensions(size), viewBox: '0 0 24 24', focusable: 'false',
161
+ 'aria-hidden': 'true', 'data-im-channel-logo': 'matrix',
162
+ }, h('path', {
163
+ fill: 'currentColor',
164
+ 'fill-rule': 'evenodd',
165
+ d: 'M4 4h16a2.4 2.4 0 0 1 2.4 2.4v8.2A2.4 2.4 0 0 1 20 17h-7.55l-4.2 3.3a.6.6 0 0 1-.98-.47V17H4A2.4 2.4 0 0 1 1.6 14.6V6.4A2.4 2.4 0 0 1 4 4Zm3 4.1a1.45 1.45 0 1 0 0 2.9 1.45 1.45 0 0 0 0-2.9Zm5.6 0a1.45 1.45 0 1 0 0 2.9 1.45 1.45 0 0 0 0-2.9Zm5.6 0a1.45 1.45 0 1 0 0 2.9 1.45 1.45 0 0 0 0-2.9Z',
166
+ }));
167
+ }
168
+
158
169
  export function OfficeLogoGlyph({ size } = {}) {
159
170
  return h('svg', {
160
171
  ...dimensions(size),
@@ -3,7 +3,7 @@ import { BotName } from '../../bot-alias.js';
3
3
  import * as React from 'react';
4
4
 
5
5
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
6
- import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
6
+ import { AccountSettingsToggle, CollapsibleAccountSection } from '../shared/collapsible-account.js';
7
7
  import { h } from '../../i18n.js';
8
8
  import { WorkspaceEditor } from '../../workspace-editor.js';
9
9
  import { ContextEnhancementEditor } from '../../context-enhancement.js';
@@ -251,6 +251,13 @@ export function AccountCard({
251
251
  return h('article', { className: 'ddt-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
252
252
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
253
253
  h(CollapsibleAccountSection, {
254
+ settings: h(BotSettingsButton, {
255
+ channel: 'dingtalk',
256
+ botId: account.botId,
257
+ botName: account.bot.name,
258
+ connected: account.connected,
259
+ accessPolicy: account.accessPolicy,
260
+ }),
254
261
  id: `ddt-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
255
262
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
256
263
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -272,13 +279,7 @@ export function AccountCard({
272
279
  lastCheckedAt: account.health.lastCheckedAt,
273
280
  formatCheckedTime: checkedTime,
274
281
  }),
275
- h(BotSettingsButton, {
276
- channel: 'dingtalk',
277
- botId: account.botId,
278
- botName: account.bot.name,
279
- connected: account.connected,
280
- accessPolicy: account.accessPolicy,
281
- })))
282
+ h(AccountSettingsToggle)))
282
283
  },
283
284
  h(WorkspaceEditor, {
284
285
  workspace: account.workspace,
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
 
5
5
  import { FeishuLogoGlyph } from "../../channel-logos.js";
6
6
  import { CredentialActionIcon, CredentialBindingPanel, 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
  FEISHU_ENDPOINTS,
@@ -600,6 +600,18 @@ export function BotCard({
600
600
  },
601
601
  h("div", { className: "bxf-cardBody dim-botCardBody" },
602
602
  h(CollapsibleAccountSection, {
603
+ settings: h(BotSettingsButton, {
604
+ channel: "feishu",
605
+ botId: connection.botId,
606
+ botName: bot.name,
607
+ connected,
608
+ accessPolicy: connection.accessPolicy,
609
+ channelSettings: {
610
+ groupResponseMode: connection.groupResponseMode,
611
+ groupTopicReply: connection.groupTopicReply,
612
+ groupMessagePermissionGranted: connection.groupMessagePermissionGranted,
613
+ },
614
+ }),
603
615
  id: `bxf-settings-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`,
604
616
  header: h("div", { className: "bxf-connectedTop dim-botCardTop" },
605
617
  h("div", { className: "bxf-botIdentity dim-botIdentity" },
@@ -624,18 +636,7 @@ export function BotCard({
624
636
  formatCheckedTime,
625
637
  healthState: stateForDisplay,
626
638
  }),
627
- h(BotSettingsButton, {
628
- channel: "feishu",
629
- botId: connection.botId,
630
- botName: bot.name,
631
- connected,
632
- accessPolicy: connection.accessPolicy,
633
- channelSettings: {
634
- groupResponseMode: connection.groupResponseMode,
635
- groupTopicReply: connection.groupTopicReply,
636
- groupMessagePermissionGranted: connection.groupMessagePermissionGranted,
637
- },
638
- }))),
639
+ h(AccountSettingsToggle))),
639
640
  },
640
641
  h(WorkspaceEditor, {
641
642
  workspace: connection.workspace,
@@ -0,0 +1,11 @@
1
+ import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
2
+
3
+ export const MATRIX_RPC_CHANNEL = '/matrix';
4
+ export const MATRIX_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
5
+
6
+ const api = createTokenChannelApi('Matrix', ' CS API 长轮询');
7
+
8
+ export const unwrapRpcResult = api.unwrapRpcResult;
9
+ export const normalizeSnapshot = api.normalizeSnapshot;
10
+ export const presentError = api.presentError;
11
+ export { api as matrixClientApi };
@@ -0,0 +1,151 @@
1
+ import * as React from 'react';
2
+
3
+ import { MatrixLogoGlyph } from '../../channel-logos.js';
4
+ import { h } from '../../i18n.js';
5
+ import { createTokenChannelSettings } from '../shared/token-channel.js';
6
+ import { MATRIX_ENDPOINTS, matrixClientApi } from './api.js';
7
+ import { installMatrixStyles } from './styles.js';
8
+
9
+ const MATRIX_USER_ID_PATTERN = /^@[^:@\s]+:[^:@\s]+(?::\d{1,5})?$/u;
10
+
11
+ function validHomeserver(value) {
12
+ if (!value || value.length > 2_048) return false;
13
+ try {
14
+ const url = new URL(value);
15
+ return (url.protocol === 'https:' || url.protocol === 'http:')
16
+ && !url.username && !url.password && !url.hash;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export function MatrixCredentialPanel({ busy, error, onSubmit, onCancel }) {
23
+ const [homeserver, setHomeserver] = React.useState('');
24
+ const [accessToken, setAccessToken] = React.useState('');
25
+ const [userId, setUserId] = React.useState('');
26
+ const [password, setPassword] = React.useState('');
27
+ const headingId = React.useId();
28
+
29
+ const normalizedHomeserver = homeserver.trim();
30
+ const normalizedUserId = userId.trim();
31
+ const tokenMode = Boolean(accessToken.trim());
32
+ const loginMode = Boolean(normalizedUserId) && Boolean(password);
33
+ const canSubmit = validHomeserver(normalizedHomeserver)
34
+ && (tokenMode
35
+ ? (!normalizedUserId && !password && accessToken.trim().length >= 8)
36
+ : (!accessToken.trim() && MATRIX_USER_ID_PATTERN.test(normalizedUserId) && password.length >= 1));
37
+
38
+ const submit = (event) => {
39
+ event.preventDefault();
40
+ if (!canSubmit || busy) return;
41
+ void onSubmit?.(tokenMode
42
+ ? { homeserver: normalizedHomeserver, accessToken: accessToken.trim() }
43
+ : {
44
+ homeserver: normalizedHomeserver,
45
+ userId: normalizedUserId,
46
+ password,
47
+ });
48
+ };
49
+
50
+ return h('section', {
51
+ className: 'ddt-card dim-surfaceCard dim-credentialPanel dmt-setup',
52
+ 'aria-labelledby': headingId,
53
+ },
54
+ h('h3', { id: headingId, className: 'dim-credentialTitle' }, '接入 Matrix 机器人'),
55
+ h('div', { className: 'dmt-guide' },
56
+ h('div', { className: 'dmt-guideCopy' },
57
+ h('strong', null, '连接你的 Matrix homeserver'),
58
+ h('p', null, '填写 homeserver 地址,并提供访问令牌,或者用户 ID 与密码的组合。机器人只访问该 homeserver,不连接第三方服务器。'))),
59
+ h('form', { className: 'dim-credentialForm dim-credentialFormSingle', onSubmit: submit },
60
+ h('div', { className: 'dmt-fields' },
61
+ h('label', { className: 'dim-credentialField dmt-fieldWide' },
62
+ h('span', null, 'Homeserver 地址'),
63
+ h('input', {
64
+ type: 'url',
65
+ value: homeserver,
66
+ onChange: (event) => setHomeserver(event.target.value),
67
+ placeholder: 'https://matrix.example.org',
68
+ maxLength: 2048,
69
+ autoCapitalize: 'none',
70
+ autoCorrect: 'off',
71
+ spellCheck: false,
72
+ disabled: busy,
73
+ required: true,
74
+ })),
75
+ h('label', { className: 'dim-credentialField dmt-fieldWide' },
76
+ h('span', null, '访问令牌(与用户 ID、密码二选一)'),
77
+ h('input', {
78
+ type: 'password',
79
+ value: accessToken,
80
+ onChange: (event) => setAccessToken(event.target.value),
81
+ placeholder: 'syt_…',
82
+ maxLength: 4096,
83
+ autoCapitalize: 'none',
84
+ autoCorrect: 'off',
85
+ spellCheck: false,
86
+ autoComplete: 'new-password',
87
+ disabled: busy,
88
+ })),
89
+ h('label', { className: 'dim-credentialField' },
90
+ h('span', null, '用户 ID(可选)'),
91
+ h('input', {
92
+ type: 'text',
93
+ value: userId,
94
+ onChange: (event) => setUserId(event.target.value),
95
+ placeholder: '@bot:example.org',
96
+ maxLength: 512,
97
+ autoCapitalize: 'none',
98
+ autoCorrect: 'off',
99
+ spellCheck: false,
100
+ disabled: busy,
101
+ })),
102
+ h('label', { className: 'dim-credentialField' },
103
+ h('span', null, '密码(可选)'),
104
+ h('input', {
105
+ type: 'password',
106
+ value: password,
107
+ onChange: (event) => setPassword(event.target.value),
108
+ maxLength: 1024,
109
+ autoComplete: 'new-password',
110
+ disabled: busy,
111
+ })),
112
+ h('p', { className: 'dmt-tokenHint' }, '访问令牌来自 Element 设置 → 帮助 → 编辑设置,或 homeserver 的登录接口;用户 ID 与密码方式会为本安装派生稳定设备会话。')),
113
+ error ? h('p', { className: 'dim-credentialError', role: 'alert' }, error.message ?? String(error)) : null,
114
+ h('div', { className: 'ddt-actions dim-viewActions dim-credentialActions' },
115
+ h('button', {
116
+ type: 'submit',
117
+ className: 'ddt-button',
118
+ 'data-kind': 'primary',
119
+ disabled: busy || !canSubmit,
120
+ }, busy ? '正在验证并连接…' : '验证并连接'),
121
+ h('button', {
122
+ type: 'button',
123
+ className: 'ddt-button',
124
+ onClick: onCancel,
125
+ disabled: busy,
126
+ }, '取消'))));
127
+ }
128
+
129
+ const channel = createTokenChannelSettings({
130
+ channel: 'Matrix',
131
+ endpoints: MATRIX_ENDPOINTS,
132
+ api: matrixClientApi,
133
+ LogoGlyph: MatrixLogoGlyph,
134
+ installStyles: installMatrixStyles,
135
+ pageClass: 'dmt-page',
136
+ avatarClass: 'dmt-avatar',
137
+ connectionLabel: 'CS API 长轮询',
138
+ emptyTitle: '接入 Matrix 机器人',
139
+ emptyDescription: '填写 homeserver 地址与访问令牌,或者用户 ID 与密码,即可让机器人以 CS API 长轮询接收消息并以富文本回复。',
140
+ platformLabel: 'Matrix homeserver',
141
+ CredentialPanel: MatrixCredentialPanel,
142
+ credentialPayload: ({ homeserver, accessToken, userId, password }) => ({ homeserver, accessToken, userId, password }),
143
+ credentialAriaLabel: '使用 homeserver 与凭据接入 Matrix 机器人',
144
+ credentialOpenLabel: '接入机器人',
145
+ credentialCloseLabel: '收起接入',
146
+ credentialNoun: '访问令牌或用户 ID 与密码',
147
+ emptyActionLabel: '开始接入',
148
+ });
149
+
150
+ export const MatrixSettingsTab = channel.SettingsTab;
151
+ export const MatrixAccountCard = channel.AccountCard;
@@ -0,0 +1,36 @@
1
+ export const MATRIX_STYLE_ID = 'xmanrui-dsh-im-matrix-settings';
2
+
3
+ const CSS = String.raw`
4
+ .dmt-page { --ddt-accent: #1a8f6f; --ddt-accent-deep: #12654f; --ddt-accent-wash: #eef8f4; }
5
+ .dmt-avatar { background: linear-gradient(145deg, #fff, #f6faf8); border: 1px solid #e2eae7; }
6
+ .dmt-avatar svg { display: block; }
7
+ .dim-logoMatrix { color: white; background: #0dbd8b; }
8
+ .dim-logoMatrix svg { width: 22px; height: 22px; }
9
+ .dmt-setup { display: grid; gap: 18px; }
10
+ .dmt-guide { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 18px; padding: 16px; border: 1px solid color-mix(in srgb, #1a8f6f 18%, var(--dsw-alias-border-l2, #e5e6eb)); border-radius: 11px; background: color-mix(in srgb, #1a8f6f 4%, var(--dsw-alias-bg-layer-1, #fff)); }
11
+ .dmt-guideCopy { min-width: 0; }
12
+ .dmt-guideCopy strong { display: block; margin-bottom: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; }
13
+ .dmt-guideCopy p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.6; }
14
+ .dmt-guideActions { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
15
+ .dmt-guideActions .ddt-button { white-space: nowrap; }
16
+ .dmt-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
17
+ .dmt-fields .dmt-fieldWide { grid-column: 1 / -1; }
18
+ .dmt-tokenHint { grid-column: 1 / -1; margin: -4px 0 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.55; }
19
+ @container (max-width: 680px) {
20
+ .dmt-guide { grid-template-columns: minmax(0, 1fr); }
21
+ .dmt-guideActions { justify-content: flex-start; }
22
+ .dmt-fields { grid-template-columns: minmax(0, 1fr); }
23
+ }
24
+ `;
25
+
26
+ export function installMatrixStyles() {
27
+ if (typeof document === 'undefined') return () => {};
28
+ const existing = document.querySelector(`style[data-plugin-css="${MATRIX_STYLE_ID}"]`);
29
+ if (existing) return () => {};
30
+ const style = document.createElement('style');
31
+ style.dataset.plugin = '@xmanrui/dsh-im';
32
+ style.dataset.pluginCss = MATRIX_STYLE_ID;
33
+ style.textContent = CSS;
34
+ document.head.appendChild(style);
35
+ return () => style.remove();
36
+ }
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
 
5
5
  import { QqLogoGlyph } from '../../channel-logos.js';
6
6
  import { CredentialActionIcon, CredentialBindingPanel, 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';
@@ -189,6 +189,13 @@ export function AccountCard({
189
189
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
190
190
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
191
191
  h(CollapsibleAccountSection, {
192
+ settings: h(BotSettingsButton, {
193
+ channel: 'qq',
194
+ botId: account.botId,
195
+ botName: account.bot.name,
196
+ connected: account.connected,
197
+ accessPolicy: account.accessPolicy,
198
+ }),
192
199
  id: `qq-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
193
200
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
194
201
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -209,13 +216,7 @@ export function AccountCard({
209
216
  lastCheckedAt: account.health.lastCheckedAt,
210
217
  formatCheckedTime: checkedTime,
211
218
  }),
212
- h(BotSettingsButton, {
213
- channel: 'qq',
214
- botId: account.botId,
215
- botName: account.bot.name,
216
- connected: account.connected,
217
- accessPolicy: account.accessPolicy,
218
- })))
219
+ h(AccountSettingsToggle)))
219
220
  },
220
221
  h(WorkspaceEditor, {
221
222
  workspace: account.workspace,
@@ -3,32 +3,64 @@
3
3
  *
4
4
  * When several accounts are connected, each account's workspace / preset /
5
5
  * context-enhancement editors make the list extremely long. This component
6
- * renders the account header as the toggle itself: collapsed, the card shows
7
- * only the header line; clicking the header expands the settings details.
6
+ * keeps one settings toggle in the existing header tools slot. Clicking the
7
+ * header also expands the settings details; more settings live in the body.
8
8
  *
9
9
  * Design notes:
10
- * - The whole header is the toggle (click + keyboard). Buttons rendered inside
11
- * the header must call stopPropagation so they keep their own behaviour.
12
- * - A chevron sits at the end of the header line as the affordance.
10
+ * - The native toggle owns keyboard interaction. Header controls stop click
11
+ * propagation so editing a name cannot accidentally toggle the card.
13
12
  * - Height animation uses grid-template-rows: 0fr -> 1fr (pure CSS).
14
- * - Full a11y: the header is a button-like region with aria-expanded /
15
- * aria-controls; the details region keeps native semantics.
13
+ * - The toggle exposes aria-expanded / aria-controls; the body keeps native
14
+ * semantics and hides collapsed controls from focus and accessibility.
16
15
  * - Honors prefers-reduced-motion.
17
16
  * - Styles are managed by the IM settings plugin, not individual accounts.
18
17
  */
19
18
  import * as React from 'react';
20
19
  import { h } from '../../i18n.js';
20
+ import { SettingsGlyph } from '../../channel-card-meta.js';
21
+
22
+ const AccountSectionContext = React.createContext(null);
23
+
24
+ /** Renders in the original settings-button slot, beside the status metadata. */
25
+ export function AccountSettingsToggle() {
26
+ const { open, toggle, contentId } = React.useContext(AccountSectionContext);
27
+ return h('button', {
28
+ type: 'button',
29
+ className: 'dim-accountSettingsToggle',
30
+ 'aria-label': open ? '收起该账号的设置' : '展开该账号的设置',
31
+ 'aria-expanded': open ? 'true' : 'false',
32
+ 'aria-controls': contentId,
33
+ onClick: (event) => {
34
+ event.stopPropagation();
35
+ toggle();
36
+ },
37
+ }, h(SettingsGlyph),
38
+ h('svg', {
39
+ className: 'dim-collapsibleChevron',
40
+ viewBox: '0 0 12 16',
41
+ width: 12,
42
+ height: 16,
43
+ fill: 'none',
44
+ stroke: 'currentColor',
45
+ strokeWidth: 1.8,
46
+ strokeLinecap: 'round',
47
+ strokeLinejoin: 'round',
48
+ 'aria-hidden': 'true',
49
+ }, h('path', { d: 'M2 6 6 10 10 6' })));
50
+ }
21
51
 
22
52
  /**
23
53
  * Collapsible account card.
24
54
  *
25
55
  * `header` renders the always-visible header line; clicking it toggles.
26
56
  * Any interactive control inside the header should stop the click event so it
27
- * does not toggle the section (e.g. the bot settings button).
57
+ * does not toggle the section (e.g. the alias editor).
58
+ * `settings` is the original more-settings action, shown above the editors.
28
59
  * `children` is the collapsed details region.
29
60
  */
30
61
  export function CollapsibleAccountSection({
31
62
  header,
63
+ settings,
32
64
  defaultOpen = false,
33
65
  open: controlledOpen,
34
66
  onToggle,
@@ -37,44 +69,35 @@ export function CollapsibleAccountSection({
37
69
  children,
38
70
  }) {
39
71
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
72
+ const generatedId = React.useId();
40
73
  const open = controlledOpen ?? uncontrolledOpen;
41
- const contentId = id ? `${id}-content` : undefined;
74
+ const contentId = `${id ?? generatedId}-content`;
42
75
 
43
76
  const toggle = () => {
44
77
  if (controlledOpen === undefined) setUncontrolledOpen((value) => !value);
45
78
  onToggle?.(!open);
46
79
  };
47
80
 
48
- const onKeyDown = (event) => {
49
- if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
50
- event.preventDefault();
51
- toggle();
52
- }
53
- };
54
-
55
- return h('div', {
81
+ return h(AccountSectionContext.Provider, { value: { open, toggle, contentId } },
82
+ h('div', {
56
83
  className: `dim-collapsibleAccount ${open ? 'is-open' : ''} ${className}`.trim(),
57
84
  'data-open': open ? 'true' : 'false',
58
85
  },
59
86
  h('div', {
60
87
  className: 'dim-collapsibleHead',
61
- role: 'button',
62
- tabIndex: 0,
63
88
  onClick: toggle,
64
- onKeyDown,
65
- 'aria-expanded': open ? 'true' : 'false',
66
- 'aria-controls': contentId,
67
- 'aria-label': open ? '收起该账号的设置' : '展开该账号的设置',
68
89
  },
69
90
  h('div', { className: 'dim-collapsibleHeaderContent' }, header),
70
- h('span', { className: 'dim-collapsibleChevron', 'aria-hidden': 'true' }),
71
91
  ),
72
92
  h('div', {
73
93
  id: contentId,
74
94
  className: 'dim-collapsibleBody',
75
95
  role: 'region',
76
96
  },
77
- h('div', { className: 'dim-collapsibleBodyInner' }, children),
97
+ h('div', { className: 'dim-collapsibleBodyInner' },
98
+ settings ? h('div', { className: 'dim-accountSettingsHeader' },
99
+ h('span', null, '常用配置'), settings) : null,
100
+ children),
78
101
  ),
79
- );
102
+ ));
80
103
  }
@@ -3,7 +3,7 @@ import { BotName } from '../../bot-alias.js';
3
3
  import * as React from 'react';
4
4
 
5
5
  import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
6
- import { CollapsibleAccountSection } from './collapsible-account.js';
6
+ import { AccountSettingsToggle, CollapsibleAccountSection } from './collapsible-account.js';
7
7
  import { h } from '../../i18n.js';
8
8
  import { installDingtalkStyles } from '../dingtalk/styles.js';
9
9
  import { WorkspaceEditor } from '../../workspace-editor.js';
@@ -92,6 +92,13 @@ export function createTokenChannelSettings(definition) {
92
92
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
93
93
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
94
94
  h(CollapsibleAccountSection, {
95
+ settings: h(BotSettingsButton, {
96
+ channel: channel.toLowerCase(),
97
+ botId: account.botId,
98
+ botName: account.bot.name,
99
+ connected: account.connected,
100
+ accessPolicy: account.accessPolicy,
101
+ }),
95
102
  id: `tok-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
96
103
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
97
104
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -113,13 +120,7 @@ export function createTokenChannelSettings(definition) {
113
120
  lastCheckedAt: account.health.lastCheckedAt,
114
121
  formatCheckedTime: checkedTime,
115
122
  }),
116
- h(BotSettingsButton, {
117
- channel: channel.toLowerCase(),
118
- botId: account.botId,
119
- botName: account.bot.name,
120
- connected: account.connected,
121
- accessPolicy: account.accessPolicy,
122
- }))),
123
+ h(AccountSettingsToggle))),
123
124
  },
124
125
  h(WorkspaceEditor, {
125
126
  workspace: account.workspace,
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
 
5
5
  import { WecomLogoGlyph } from '../../channel-logos.js';
6
6
  import { CredentialActionIcon, CredentialBindingPanel, 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';
@@ -188,6 +188,13 @@ export function AccountCard({
188
188
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
189
189
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
190
190
  h(CollapsibleAccountSection, {
191
+ settings: h(BotSettingsButton, {
192
+ channel: 'wecom',
193
+ botId: account.botId,
194
+ botName: account.bot.name,
195
+ connected: account.connected,
196
+ accessPolicy: account.accessPolicy,
197
+ }),
191
198
  id: `wec-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
192
199
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
193
200
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -208,13 +215,7 @@ export function AccountCard({
208
215
  lastCheckedAt: account.health.lastCheckedAt,
209
216
  formatCheckedTime: checkedTime,
210
217
  }),
211
- h(BotSettingsButton, {
212
- channel: 'wecom',
213
- botId: account.botId,
214
- botName: account.bot.name,
215
- connected: account.connected,
216
- accessPolicy: account.accessPolicy,
217
- })))
218
+ h(AccountSettingsToggle)))
218
219
  },
219
220
  h(WorkspaceEditor, {
220
221
  workspace: account.workspace,
@@ -3,7 +3,7 @@ import { BotName } from '../../bot-alias.js';
3
3
  import * as React from 'react';
4
4
 
5
5
  import { WecomLogoGlyph } from '../../channel-logos.js';
6
- import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
6
+ import { AccountSettingsToggle, CollapsibleAccountSection } from '../shared/collapsible-account.js';
7
7
  import { h } from '../../i18n.js';
8
8
  import { WorkspaceEditor } from '../../workspace-editor.js';
9
9
  import { ContextEnhancementEditor } from '../../context-enhancement.js';
@@ -263,6 +263,13 @@ export function AccountCard({
263
263
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
264
264
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
265
265
  h(CollapsibleAccountSection, {
266
+ settings: h(BotSettingsButton, {
267
+ channel: 'wecomApp',
268
+ botId: account.botId,
269
+ botName: account.bot.name,
270
+ connected: account.connected,
271
+ accessPolicy: account.accessPolicy,
272
+ }),
266
273
  id: elementId,
267
274
  header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
268
275
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
@@ -283,13 +290,7 @@ export function AccountCard({
283
290
  lastCheckedAt: account.health.lastCheckedAt,
284
291
  formatCheckedTime: checkedTime,
285
292
  }),
286
- h(BotSettingsButton, {
287
- channel: 'wecomApp',
288
- botId: account.botId,
289
- botName: account.bot.name,
290
- connected: account.connected,
291
- accessPolicy: account.accessPolicy,
292
- })))
293
+ h(AccountSettingsToggle)))
293
294
  },
294
295
  h(CallbackUrlBox, {
295
296
  url: account.bot.callbackUrl,