@modesto_ma/dsh-channels 0.5.0-beta.3 → 0.5.0-beta.5

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 (46) hide show
  1. package/README.md +88 -27
  2. package/docs/attachment-pipeline-v2.md +1 -1
  3. package/docs/feishu-onboarding.md +3 -3
  4. package/lib/client.js +1427 -1338
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +7958 -6710
  7. package/lib/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/access/controller.ts +32 -1
  10. package/src/analyze/contract.ts +121 -0
  11. package/src/analyze/launcher.ts +353 -0
  12. package/src/analyze/server.ts +201 -0
  13. package/src/cards/analyze.ts +116 -0
  14. package/src/cards/index.ts +1 -0
  15. package/src/cards/streaming.ts +58 -0
  16. package/src/client/api.ts +15 -0
  17. package/src/client/number-field.ts +97 -0
  18. package/src/client/panel/access-tab.tsx +61 -20
  19. package/src/client/panel/advanced-tab.tsx +59 -68
  20. package/src/client/panel/connection-tab.tsx +147 -164
  21. package/src/client/panel/credentials-card.tsx +124 -87
  22. package/src/client/panel/fields-tab.tsx +27 -0
  23. package/src/client/panel/index.tsx +126 -55
  24. package/src/client/panel/shared.tsx +369 -252
  25. package/src/client/panel/whitelist.tsx +197 -0
  26. package/src/client/panel/wiki-tab.tsx +48 -0
  27. package/src/client/styles.ts +108 -91
  28. package/src/config.ts +30 -4
  29. package/src/domain.ts +55 -0
  30. package/src/feishu/scopes.ts +13 -7
  31. package/src/feishu/tools.ts +3 -1
  32. package/src/health.ts +12 -0
  33. package/src/host/agent.ts +5 -1
  34. package/src/index.ts +2 -2
  35. package/src/interactions/pending.ts +11 -2
  36. package/src/routing/inbound.ts +57 -14
  37. package/src/service.ts +161 -28
  38. package/src/settings/contract.ts +222 -146
  39. package/src/settings/routes.ts +66 -14
  40. package/src/settings/surface.ts +234 -203
  41. package/src/streamer.ts +156 -7
  42. package/src/workspaces.ts +31 -0
  43. package/src/client/panel/access-card.tsx +0 -303
  44. package/src/client/panel/model-tab.tsx +0 -23
  45. package/src/client/panel/sessions-tab.tsx +0 -21
  46. package/src/client/panel/workspace-tab.tsx +0 -24
@@ -1,80 +1,71 @@
1
1
  /**
2
- * 「高级」tab:流式刷新等低层字段 + 只读部署信息(overlayPath / version /
3
- * 部署级参数)+ 「清除 Web 覆盖」。清除覆盖是整体动作,不走 draft。
2
+ * 「高级」tab:日志分析接口(监听地址字段 + 访问令牌凭据行)、SDK 日志级别
3
+ * 与 Web 覆盖文件(「恢复 profile 配置」删除整个覆盖层,不走 draft)。
4
4
  *
5
5
  * @module dsh-channels/client/panel/advanced-tab
6
6
  */
7
7
 
8
- import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
9
- import type { PanelWire } from '../../settings/contract.ts'
10
- import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
11
- import type { BusyAction, FormView } from './shared.tsx'
8
+ import { fillCopy, PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
9
+ import { CredentialRow } from './credentials-card.tsx'
10
+ import { FieldSections, Row, Section } from './shared.tsx'
11
+ import type { AnalyzeTokenView, FormView } from './shared.tsx'
12
12
 
13
- export function AdvancedTab(props: { form: FormView; onClear: () => void }): JSX.Element {
14
- const { form, onClear } = props
15
- return (
16
- <div className="dsh-channels-cfg__tabBody">
17
- <TabHelp text={PANEL_COPY.advancedTabHelp} />
18
- <FieldsCard form={form} groups={TAB_FIELD_GROUPS.advanced} />
19
- <SaveBar form={form} tab="advanced" />
20
- <AdvancedCard panel={form.panel} busy={form.busy} locked={form.locked} onClear={onClear} />
21
- </div>
22
- )
23
- }
24
-
25
- /** Read-only deployment facts; the only write action here drops the overlay file. */
26
- function AdvancedCard(props: {
27
- panel: PanelWire
28
- busy: BusyAction | null
29
- locked: boolean
13
+ export function AdvancedTab(props: {
14
+ form: FormView
15
+ analyzeToken: AnalyzeTokenView
30
16
  onClear: () => void
31
17
  }): JSX.Element {
32
- const { panel, busy, locked, onClear } = props
33
- const { readOnly } = panel.config
18
+ const { form, analyzeToken, onClear } = props
19
+ const { panel, busy, locked } = form
20
+ const tokenState = panel.config.credentials.analyzeToken
34
21
  const overridden = panel.config.overriddenKeys.length
35
22
  return (
36
- <section className="dsh-channels-cfg__card">
37
- <div className="dsh-channels-cfg__cardHead">
38
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.advancedCard}</span>
39
- <span className="dsh-channels-cfg__cardMeta">
40
- {overridden === 0 ? PANEL_COPY.inherited : `${PANEL_COPY.overridden} ${overridden} 项`}
41
- </span>
42
- </div>
43
- <span className="dsh-channels-cfg__help">{PANEL_COPY.advancedHelp}</span>
44
- <div className="dsh-channels-cfg__labelRow">
45
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.readOnlyCard}</span>
46
- </div>
47
- <div className="dsh-channels-cfg__readOnly">
48
- {readOnly.map(field => (
49
- <div className="dsh-channels-cfg__kv" key={field.key}>
50
- <span className="dsh-channels-cfg__kvKey">{field.label}</span>
51
- <span className="dsh-channels-cfg__kvValue">{field.value}</span>
52
- {field.help === undefined ? null : <span className="dsh-channels-cfg__help">{field.help}</span>}
53
- </div>
54
- ))}
55
- <div className="dsh-channels-cfg__kv">
56
- <span className="dsh-channels-cfg__kvKey">{PANEL_COPY.overlayPath}</span>
57
- <span className="dsh-channels-cfg__kvValue dsh-channels-cfg__mono">{panel.status.overlayPath}</span>
58
- <span className="dsh-channels-cfg__help">{PANEL_COPY.overlayPathHelp}</span>
59
- </div>
60
- <div className="dsh-channels-cfg__kv">
61
- <span className="dsh-channels-cfg__kvKey">{PANEL_COPY.version}</span>
62
- <span className="dsh-channels-cfg__kvValue dsh-channels-cfg__mono">{panel.status.version}</span>
63
- </div>
64
- </div>
65
- <div className="dsh-channels-cfg__actions">
66
- <button
67
- type="button"
68
- className="dsh-channels-cfg__btn"
69
- onClick={onClear}
70
- disabled={overridden === 0 || busy !== null || locked}
71
- >
72
- {busy === 'clear' ? PANEL_COPY.clearing : PANEL_COPY.clear}
73
- </button>
74
- <span className="dsh-channels-cfg__help">
75
- {overridden === 0 ? PANEL_COPY.noOverrides : `${PANEL_COPY.overridden} ${overridden} 项`}
76
- </span>
77
- </div>
78
- </section>
23
+ <div className="dsh-channels-cfg__tabBody">
24
+ <FieldSections
25
+ form={form}
26
+ groups={TAB_FIELD_GROUPS.advanced}
27
+ extra={{
28
+ analyze: (
29
+ <CredentialRow
30
+ label={PANEL_COPY.analyzeTokenLabel}
31
+ summary={PANEL_COPY.analyzeTokenSummary}
32
+ state={tokenState}
33
+ value={analyzeToken.value}
34
+ secret
35
+ placeholder={PANEL_COPY.analyzeTokenPlaceholder}
36
+ issue={analyzeToken.issues.find(item => item.key === 'analyzeToken')}
37
+ busy={analyzeToken.busy}
38
+ disabled={analyzeToken.busy !== null || analyzeToken.locked || !tokenState.writable}
39
+ onChange={analyzeToken.onChange}
40
+ onSave={analyzeToken.onSave}
41
+ onClear={analyzeToken.onClear}
42
+ />
43
+ ),
44
+ }}
45
+ />
46
+ <Section title={PANEL_COPY.overlayTitle}>
47
+ <Row
48
+ label={PANEL_COPY.overlayPath}
49
+ summary={<span className="dsh-channels-cfg__mono">{panel.status.overlayPath}</span>}
50
+ details={(
51
+ <>
52
+ {PANEL_COPY.overlayPathHelp}
53
+ {' '}
54
+ {overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })}
55
+ </>
56
+ )}
57
+ control={(
58
+ <button
59
+ type="button"
60
+ className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
61
+ onClick={onClear}
62
+ disabled={overridden === 0 || busy !== null || locked}
63
+ >
64
+ {busy === 'clear' ? PANEL_COPY.clearing : PANEL_COPY.clear}
65
+ </button>
66
+ )}
67
+ />
68
+ </Section>
69
+ </div>
79
70
  )
80
71
  }
@@ -1,20 +1,19 @@
1
1
  /**
2
- * 「连接」tab:按 Host 派生的生命周期阶段渲染连接引导。
2
+ * 「连接」tab:按 Host 派生的生命周期阶段渲染。
3
3
  *
4
- * - unconfigured:onboarding 三步 + 凭据卡(不渲染可用的「连接」按钮)
5
- * - idle:连接信息卡 + 「连接飞书」
6
- * - error:连接信息卡 + 最近一次连接失败 + 「重试」
7
- * - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
8
- * - connecting:禁用按钮显示「连接中…」
9
- * - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
4
+ * - unconfigured:上手三步 + 应用凭据
5
+ * - idle / error / connecting / connected:状态块(当前阶段的唯一操作 + 运行数字)
6
+ * - 连接后:自检(权限缺口可展开、斜杠指令面板同步结果)
7
+ * - 有需重连才生效的已保存配置:顶部「重启连接」横幅
10
8
  *
11
9
  * @module dsh-channels/client/panel/connection-tab
12
10
  */
13
11
 
14
- import { PANEL_COPY } from '../../settings/contract.ts'
12
+ import { useState } from 'react'
13
+ import { fillCopy, PANEL_COPY } from '../../settings/contract.ts'
15
14
  import type { ChannelStatusWire, PanelWire, ScopeCheckWire, SlashCommandSyncWire } from '../../settings/contract.ts'
16
- import { CredentialsCard } from './credentials-card.tsx'
17
- import { connectionStateLabel, fieldLabel, formatUptime, phaseBadge, TabHelp } from './shared.tsx'
15
+ import { CredentialsSection } from './credentials-card.tsx'
16
+ import { fieldLabel, phaseBadge, phaseSummary, Pill, Row, Section } from './shared.tsx'
18
17
  import type { BusyAction, CredentialsView } from './shared.tsx'
19
18
 
20
19
  export interface ConnectionTabProps {
@@ -27,24 +26,30 @@ export interface ConnectionTabProps {
27
26
 
28
27
  export function ConnectionTab(props: ConnectionTabProps): JSX.Element {
29
28
  const { panel, status, busy, credentials, onReconnect } = props
29
+ const checks = status.scopeCheck !== undefined || status.slashCommandSync !== undefined
30
30
  return (
31
31
  <div className="dsh-channels-cfg__tabBody">
32
- <TabHelp text={PANEL_COPY.connectionTabHelp} />
33
32
  {panel.config.restartPendingKeys.length === 0
34
33
  ? null
35
34
  : <RestartBanner panel={panel} busy={busy} locked={status.busy} onReconnect={onReconnect} />}
36
35
  {status.phase === 'unconfigured'
37
- ? <OnboardingCard status={status} />
38
- : <ConnectionInfoCard status={status} busy={busy} onReconnect={onReconnect} />}
39
- {status.scopeCheck === undefined ? null : <ScopeCheckCard check={status.scopeCheck} />}
40
- {status.slashCommandSync === undefined ? null : <SlashCommandSyncCard sync={status.slashCommandSync} />}
41
- <CredentialsCard panel={panel} credentials={credentials} />
36
+ ? <OnboardingSection status={status} />
37
+ : <StatusBlock status={status} busy={busy} onReconnect={onReconnect} />}
38
+ {checks
39
+ ? (
40
+ <Section title={PANEL_COPY.checksTitle}>
41
+ {status.scopeCheck === undefined ? null : <ScopeCheckRow check={status.scopeCheck} />}
42
+ {status.slashCommandSync === undefined ? null : <SlashCommandSyncRow sync={status.slashCommandSync} />}
43
+ </Section>
44
+ )
45
+ : null}
46
+ <CredentialsSection panel={panel} credentials={credentials} />
42
47
  </div>
43
48
  )
44
49
  }
45
50
 
46
51
  /** First-run guide: create the app, fill the credentials, save and wait. */
47
- function OnboardingCard(props: { status: ChannelStatusWire }): JSX.Element {
52
+ function OnboardingSection(props: { status: ChannelStatusWire }): JSX.Element {
48
53
  const { status } = props
49
54
  const steps = [
50
55
  { title: PANEL_COPY.onboardingStep1Title, text: PANEL_COPY.onboardingStep1Text },
@@ -52,21 +57,17 @@ function OnboardingCard(props: { status: ChannelStatusWire }): JSX.Element {
52
57
  { title: PANEL_COPY.onboardingStep3Title, text: PANEL_COPY.onboardingStep3Text },
53
58
  ]
54
59
  return (
55
- <section className="dsh-channels-cfg__card">
56
- <div className="dsh-channels-cfg__cardHead">
57
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.onboardingTitle}</span>
58
- <span className="dsh-channels-cfg__badge dsh-channels-cfg__badge--muted">
59
- {PANEL_COPY.phaseUnconfigured}
60
- </span>
60
+ <div className="dsh-channels-cfg__hero">
61
+ <div className="dsh-channels-cfg__heroTop">
62
+ <strong>{PANEL_COPY.onboardingTitle}</strong>
61
63
  </div>
62
- <span className="dsh-channels-cfg__help">{PANEL_COPY.onboardingIntro}</span>
63
64
  <ol className="dsh-channels-cfg__steps">
64
65
  {steps.map((step, index) => (
65
66
  <li className="dsh-channels-cfg__step" key={step.title}>
66
67
  <span className="dsh-channels-cfg__stepIndex">{index + 1}</span>
67
68
  <span className="dsh-channels-cfg__stepBody">
68
69
  <span className="dsh-channels-cfg__stepTitle">{step.title}</span>
69
- <span className="dsh-channels-cfg__help">{step.text}</span>
70
+ <span className="dsh-channels-cfg__rowSummary">{step.text}</span>
70
71
  </span>
71
72
  </li>
72
73
  ))}
@@ -74,19 +75,16 @@ function OnboardingCard(props: { status: ChannelStatusWire }): JSX.Element {
74
75
  {status.missingConfig.length === 0
75
76
  ? null
76
77
  : (
77
- <div className="dsh-channels-cfg__banner">
78
- <strong>{PANEL_COPY.missingConfig}:</strong>
79
- <span>{status.missingConfig.join('、')}</span>
80
- </div>
78
+ <span className="dsh-channels-cfg__rowSummary">
79
+ {PANEL_COPY.missingConfig}:{status.missingConfig.join('、')}
80
+ </span>
81
81
  )}
82
- <span className="dsh-channels-cfg__help">{PANEL_COPY.statusHintUnconfigured}</span>
83
- <span className="dsh-channels-cfg__help">{PANEL_COPY.onboardingDocsHint}</span>
84
- </section>
82
+ </div>
85
83
  )
86
84
  }
87
85
 
88
- /** Runtime facts plus the one lifecycle action available in the current phase. */
89
- function ConnectionInfoCard(props: {
86
+ /** Phase, the one lifecycle action available in it, and the runtime counters. */
87
+ function StatusBlock(props: {
90
88
  status: ChannelStatusWire
91
89
  busy: BusyAction | null
92
90
  onReconnect: () => void
@@ -96,163 +94,149 @@ function ConnectionInfoCard(props: {
96
94
  // `status.busy` covers a connection attempt started outside this panel; the
97
95
  // local busy flag covers the panel's own action. Either way the button waits.
98
96
  const actionBusy = busy !== null || status.busy
99
- const connection = connectionStateLabel(status.connection?.state)
100
- const facts = [
101
- { label: PANEL_COPY.statusCard, value: badge.text },
102
- { label: PANEL_COPY.connectionStateLabel, value: connection.text },
103
- { label: PANEL_COPY.botOpenId, value: status.botOpenIdTail ?? PANEL_COPY.unknownValue },
104
- { label: PANEL_COPY.uptime, value: formatUptime(status.uptimeMs) },
97
+ const stats = [
105
98
  { label: PANEL_COPY.routeCount, value: String(status.routeCount) },
106
99
  { label: PANEL_COPY.liveAgentCount, value: String(status.liveAgentCount) },
107
100
  { label: PANEL_COPY.inflightTurnCount, value: String(status.inflightTurnCount) },
101
+ { label: PANEL_COPY.botOpenId, value: status.botOpenIdTail ?? PANEL_COPY.unknownValue, mono: true },
108
102
  ]
103
+ let action: JSX.Element | null = null
104
+ if (status.phase === 'connecting') {
105
+ action = <button type="button" className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary" disabled>{PANEL_COPY.connecting}</button>
106
+ } else if (status.phase === 'idle' || status.phase === 'error') {
107
+ action = (
108
+ <button type="button" className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary" onClick={onReconnect} disabled={actionBusy}>
109
+ {busy === 'reconnect' ? PANEL_COPY.connecting : status.phase === 'idle' ? PANEL_COPY.connect : PANEL_COPY.retry}
110
+ </button>
111
+ )
112
+ } else if (status.phase === 'connected') {
113
+ action = (
114
+ <button type="button" className="dsh-channels-cfg__btn" onClick={onReconnect} disabled={actionBusy}>
115
+ {busy === 'reconnect' ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect}
116
+ </button>
117
+ )
118
+ }
109
119
  return (
110
- <section className="dsh-channels-cfg__card">
111
- <div className="dsh-channels-cfg__cardHead">
112
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.connectionInfo}</span>
113
- <span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${status.configured ? 'ok' : 'bad'}`}>
114
- {status.configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge}
115
- </span>
120
+ <div className="dsh-channels-cfg__hero">
121
+ <div className="dsh-channels-cfg__heroTop">
122
+ <Pill tone={badge.tone} dot>{badge.text}</Pill>
123
+ <strong>{phaseSummary(status.phase)}</strong>
124
+ <span className="dsh-channels-cfg__spacer" />
125
+ {action}
116
126
  </div>
117
127
  {status.lastStartError === undefined
118
128
  ? null
119
129
  : (
120
130
  <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
121
- <strong>{PANEL_COPY.lastError}</strong>
122
- <span className="dsh-channels-cfg__mono">{status.lastStartError}</span>
131
+ {PANEL_COPY.lastError}:<span className="dsh-channels-cfg__mono">{status.lastStartError}</span>
123
132
  </div>
124
133
  )}
125
- <div className="dsh-channels-cfg__facts">
126
- {facts.map(fact => (
127
- <div className="dsh-channels-cfg__fact" key={fact.label}>
128
- <span className="dsh-channels-cfg__factLabel">{fact.label}</span>
129
- <span className="dsh-channels-cfg__factValue">{fact.value}</span>
130
- </div>
131
- ))}
132
- </div>
134
+ {status.phase === 'connected'
135
+ ? (
136
+ <div className="dsh-channels-cfg__stats">
137
+ {stats.map(stat => (
138
+ <div className="dsh-channels-cfg__stat" key={stat.label}>
139
+ <span>{stat.label}</span>
140
+ <strong className={stat.mono === true ? 'dsh-channels-cfg__mono' : undefined}>{stat.value}</strong>
141
+ </div>
142
+ ))}
143
+ </div>
144
+ )
145
+ : null}
133
146
  {status.reasons.length === 0
134
147
  ? null
135
148
  : (
136
- <span className={`dsh-channels-cfg__help${status.level === 'unhealthy' ? ' dsh-channels-cfg__reasonBad' : ''}`}>
149
+ <span className={`dsh-channels-cfg__rowSummary${status.level === 'unhealthy' ? ' dsh-channels-cfg__textBad' : ''}`}>
137
150
  {PANEL_COPY.healthReasons}:{status.reasons.join(';')}
138
151
  </span>
139
152
  )}
140
- <div className="dsh-channels-cfg__actions">
141
- {status.phase === 'connecting'
142
- ? (
143
- <button type="button" className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary" disabled>
144
- {PANEL_COPY.connecting}
145
- </button>
146
- )
147
- : null}
148
- {status.phase === 'idle'
149
- ? (
150
- <button
151
- type="button"
152
- className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
153
- onClick={onReconnect}
154
- disabled={actionBusy}
155
- >
156
- {busy === 'reconnect' ? PANEL_COPY.connecting : PANEL_COPY.connect}
157
- </button>
158
- )
159
- : null}
160
- {status.phase === 'error'
161
- ? (
162
- <button
163
- type="button"
164
- className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
165
- onClick={onReconnect}
166
- disabled={actionBusy}
167
- >
168
- {busy === 'reconnect' ? PANEL_COPY.connecting : PANEL_COPY.retry}
169
- </button>
170
- )
171
- : null}
172
- {status.phase === 'connected'
173
- ? (
174
- <>
175
- <button
176
- type="button"
177
- className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
178
- onClick={onReconnect}
179
- disabled={actionBusy}
180
- >
181
- {busy === 'reconnect' ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect}
182
- </button>
183
- <span className="dsh-channels-cfg__help">{PANEL_COPY.restartWarning}</span>
184
- </>
185
- )
186
- : null}
187
- </div>
188
- </section>
153
+ </div>
154
+ )
155
+ }
156
+
157
+ /** Status icon in front of one check row. */
158
+ function CheckIcon(props: { tone: 'ok' | 'warn' | 'bad' }): JSX.Element {
159
+ return (
160
+ <span className={`dsh-channels-cfg__checkIcon dsh-channels-cfg__checkIcon--${props.tone}`} aria-hidden="true">
161
+ {props.tone === 'ok' ? '✓' : '!'}
162
+ </span>
189
163
  )
190
164
  }
191
165
 
192
- /** Post-connect scope self-check: missing scopes in red, each with a console deep link. */
193
- function ScopeCheckCard(props: { check: ScopeCheckWire }): JSX.Element {
166
+ /** Post-connect scope self-check: the gap count, expandable to each scope with its console link. */
167
+ function ScopeCheckRow(props: { check: ScopeCheckWire }): JSX.Element {
194
168
  const { check } = props
169
+ const [open, setOpen] = useState(false)
170
+ if (check.state === 'ok') {
171
+ return <Row leading={<CheckIcon tone="ok" />} label={PANEL_COPY.scopeCheckOk} />
172
+ }
173
+ if (check.state === 'error') {
174
+ return (
175
+ <Row
176
+ leading={<CheckIcon tone="bad" />}
177
+ label={PANEL_COPY.scopeCheckError}
178
+ summary={<span className="dsh-channels-cfg__mono">{check.error}</span>}
179
+ />
180
+ )
181
+ }
182
+ const required = check.missing.filter(item => item.level === 'required').length
195
183
  return (
196
- <section className="dsh-channels-cfg__card">
197
- <div className="dsh-channels-cfg__cardHead">
198
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.scopeCheckTitle}</span>
199
- </div>
200
- {check.state === 'ok'
201
- ? <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok">{PANEL_COPY.scopeCheckOk}</div>
202
- : null}
203
- {check.state === 'error'
204
- ? (
205
- <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
206
- <strong>{PANEL_COPY.scopeCheckError}</strong>
207
- <span className="dsh-channels-cfg__mono">{check.error}</span>
208
- </div>
209
- )
210
- : null}
211
- {check.state === 'missing'
184
+ <>
185
+ <Row
186
+ leading={<CheckIcon tone={required > 0 ? 'bad' : 'warn'} />}
187
+ label={required > 0
188
+ ? fillCopy(PANEL_COPY.scopeCheckMissingRequired, { n: required })
189
+ : fillCopy(PANEL_COPY.scopeCheckMissing, { n: check.missing.length })}
190
+ summary={required > 0 ? PANEL_COPY.scopeCheckPublish : PANEL_COPY.scopeCheckMissingOptional}
191
+ control={(
192
+ <div className="dsh-channels-cfg__inputRow">
193
+ <button type="button" className="dsh-channels-cfg__more" aria-expanded={open} onClick={() => setOpen(!open)}>
194
+ {open ? PANEL_COPY.collapse : PANEL_COPY.expand}
195
+ </button>
196
+ <a className="dsh-channels-cfg__btn" href={check.consoleUrl} target="_blank" rel="noreferrer noopener">
197
+ {PANEL_COPY.scopeCheckOpenAll}
198
+ </a>
199
+ </div>
200
+ )}
201
+ />
202
+ {open
212
203
  ? (
213
- <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
214
- <strong>{PANEL_COPY.scopeCheckMissing}</strong>
215
- <ul className="dsh-channels-cfg__scopeList">
216
- {check.missing.map(item => (
217
- <li key={item.scopes.join(',')}>
218
- <span>[{item.level === 'required' ? PANEL_COPY.scopeCheckRequired : PANEL_COPY.scopeCheckOptional}] </span>
219
- <span className="dsh-channels-cfg__mono">{item.scopes.join(' / ')}</span>
220
- <span>:{item.purpose} </span>
221
- <a href={item.consoleUrl} target="_blank" rel="noreferrer noopener">{PANEL_COPY.scopeCheckOpenOne}</a>
222
- </li>
223
- ))}
224
- </ul>
225
- <a href={check.consoleUrl} target="_blank" rel="noreferrer noopener">{PANEL_COPY.scopeCheckOpenAll}</a>
226
- </div>
204
+ <ul className="dsh-channels-cfg__scopeList">
205
+ {check.missing.map(item => (
206
+ <li key={item.scopes.join(',')}>
207
+ <code className="dsh-channels-cfg__mono">{item.scopes.join(' / ')}</code>
208
+ {item.level === 'required' ? <Pill tone="bad">{PANEL_COPY.scopeCheckRequired}</Pill> : null}
209
+ <span>{item.purpose}</span>
210
+ <a className="dsh-channels-cfg__link" href={item.consoleUrl} target="_blank" rel="noreferrer noopener">
211
+ {PANEL_COPY.scopeCheckOpenOne}
212
+ </a>
213
+ </li>
214
+ ))}
215
+ </ul>
227
216
  )
228
217
  : null}
229
- </section>
218
+ </>
230
219
  )
231
220
  }
232
221
 
233
- /** Post-connect native slash-command panel sync: counts on success, the Feishu reason on failure. */
234
- function SlashCommandSyncCard(props: { sync: SlashCommandSyncWire }): JSX.Element {
222
+ /** Post-connect native slash-command panel sync: the command count, or the Feishu reason. */
223
+ function SlashCommandSyncRow(props: { sync: SlashCommandSyncWire }): JSX.Element {
235
224
  const { sync } = props
236
- return (
237
- <section className="dsh-channels-cfg__card">
238
- <div className="dsh-channels-cfg__cardHead">
239
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.slashCommandSyncTitle}</span>
240
- </div>
241
- {sync.state === 'ok'
242
- ? (
243
- <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok">
244
- <span>{PANEL_COPY.slashCommandSyncOk} </span>
245
- <span>{PANEL_COPY.slashCommandSyncTotal} {sync.total} · {PANEL_COPY.slashCommandSyncCreated} {sync.created} · {PANEL_COPY.slashCommandSyncUpdated} {sync.updated} · {PANEL_COPY.slashCommandSyncRemoved} {sync.removed}</span>
246
- </div>
247
- )
248
- : (
249
- <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
250
- <strong>{PANEL_COPY.slashCommandSyncError}</strong>
251
- <span className="dsh-channels-cfg__mono">{sync.error}</span>
252
- </div>
253
- )}
254
- </section>
255
- )
225
+ return sync.state === 'ok'
226
+ ? (
227
+ <Row
228
+ leading={<CheckIcon tone="ok" />}
229
+ label={PANEL_COPY.slashCommandSyncTitle}
230
+ summary={fillCopy(PANEL_COPY.slashCommandSyncOk, { total: sync.total })}
231
+ />
232
+ )
233
+ : (
234
+ <Row
235
+ leading={<CheckIcon tone="warn" />}
236
+ label={PANEL_COPY.slashCommandSyncTitle}
237
+ summary={<>{PANEL_COPY.slashCommandSyncError}:<span className="dsh-channels-cfg__mono">{sync.error}</span></>}
238
+ />
239
+ )
256
240
  }
257
241
 
258
242
  /** Config saved but only applied on the next connect: offer the one-click restart. */
@@ -268,16 +252,15 @@ function RestartBanner(props: {
268
252
  .join('、')
269
253
  return (
270
254
  <div className="dsh-channels-cfg__banner">
271
- <span>{PANEL_COPY.restartBanner}{keys}</span>
255
+ <span className="dsh-channels-cfg__bannerText">{PANEL_COPY.restartBanner}{keys}</span>
272
256
  <button
273
257
  type="button"
274
- className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
258
+ className="dsh-channels-cfg__btn"
275
259
  onClick={onReconnect}
276
260
  disabled={busy !== null || locked}
277
261
  >
278
262
  {busy === 'reconnect' ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect}
279
263
  </button>
280
- <span className="dsh-channels-cfg__help">{PANEL_COPY.restartWarning}</span>
281
264
  </div>
282
265
  )
283
266
  }