@xmanrui/dsh-im 4.24.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.24.0",
3
+ "version": "4.24.1",
4
4
  "description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -80,6 +80,10 @@
80
80
  {
81
81
  "name": "wings1848",
82
82
  "url": "https://github.com/wings1848"
83
+ },
84
+ {
85
+ "name": "Zhe (Phil) Yang",
86
+ "url": "https://github.com/yangzhe1991"
83
87
  }
84
88
  ],
85
89
  "license": "MIT",
@@ -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) {
@@ -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,
@@ -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,
@@ -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.',
@@ -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' : ''}`,