@modesto_ma/dsh-channels 0.1.0 → 0.3.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.
- package/README.md +89 -55
- package/docs/access-control-design.md +5 -5
- package/docs/feasibility-and-design.md +54 -26
- package/docs/feishu-onboarding.md +26 -20
- package/docs/persistence.md +4 -4
- package/docs/refactor-plan.md +6 -5
- package/docs/workspace-isolation-design.md +78 -0
- package/lib/client.js +1715 -882
- package/lib/client.js.map +1 -1
- package/lib/index.js +1556 -680
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +23 -0
- package/src/cards/primitives.ts +20 -0
- package/src/cards/streaming.ts +310 -291
- package/src/cleanup.ts +256 -119
- package/src/client/api.ts +20 -0
- package/src/client/catalog.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/client/panel/access-card.tsx +294 -0
- package/src/client/panel/access-tab.tsx +46 -0
- package/src/client/panel/advanced-tab.tsx +80 -0
- package/src/client/panel/cleanup-card.tsx +106 -0
- package/src/client/panel/connection-tab.tsx +214 -0
- package/src/client/panel/credentials-card.tsx +104 -0
- package/src/client/panel/index.tsx +862 -0
- package/src/client/panel/model-tab.tsx +23 -0
- package/src/client/panel/sessions-tab.tsx +21 -0
- package/src/client/panel/shared.tsx +488 -0
- package/src/client/panel/workspace-tab.tsx +40 -0
- package/src/client/styles.ts +24 -6
- package/src/config.ts +44 -12
- package/src/domain.ts +34 -3
- package/src/feishu/agent-context.ts +9 -5
- package/src/index.ts +32 -14
- package/src/routing/inbound.ts +88 -43
- package/src/service.ts +259 -65
- package/src/session/isolation.ts +277 -0
- package/src/session/manager.ts +63 -20
- package/src/session/repair.ts +2 -3
- package/src/session/routes.ts +2 -0
- package/src/session-commands.ts +41 -14
- package/src/settings/contract.ts +223 -47
- package/src/settings/directory-picker.ts +121 -0
- package/src/settings/routes.ts +49 -2
- package/src/settings/surface.ts +171 -25
- package/src/streamer.ts +70 -30
- package/src/workspaces.ts +50 -28
- package/src/client/panel.tsx +0 -1475
package/src/client/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { PANEL_COPY } from '../settings/contract.ts'
|
|
12
|
-
import { FeishuChannelsSection } from './panel.tsx'
|
|
12
|
+
import { FeishuChannelsSection } from './panel/index.tsx'
|
|
13
13
|
import { installStyles } from './styles.ts'
|
|
14
14
|
|
|
15
15
|
// 平台 client runtime 的最小结构声明:只声明本 bundle 用到的两个能力。
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 访问与授权卡:fail-closed 警告、群/用户白名单增删、待处理申请处理。
|
|
3
|
+
*
|
|
4
|
+
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
5
|
+
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
6
|
+
* 管理员与准入模式由同 tab 上方「准入与授权」字段表单编辑,这里不再重复。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-channels/client/panel/access-card
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
13
|
+
import type {
|
|
14
|
+
AccessGroupWire,
|
|
15
|
+
AccessRequestWire,
|
|
16
|
+
AccessUserWire,
|
|
17
|
+
PanelWire,
|
|
18
|
+
} from '../../settings/contract.ts'
|
|
19
|
+
import { accessModeLabel, EmptyState } from './shared.tsx'
|
|
20
|
+
import type { BusyAction } from './shared.tsx'
|
|
21
|
+
|
|
22
|
+
/** Local `YYYY-MM-DD HH:mm` rendering for authorization timestamps. */
|
|
23
|
+
function formatAccessDate(ms: number): string {
|
|
24
|
+
if (!Number.isFinite(ms) || ms <= 0) return PANEL_COPY.unknownValue
|
|
25
|
+
const date = new Date(ms)
|
|
26
|
+
const pad = (value: number): string => String(value).padStart(2, '0')
|
|
27
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Which authorization entry produced one grant. */
|
|
31
|
+
function accessSourceLabel(source: AccessGroupWire['source']): string {
|
|
32
|
+
switch (source) {
|
|
33
|
+
case 'command': return PANEL_COPY.accessSourceCommand
|
|
34
|
+
case 'card': return PANEL_COPY.accessSourceCard
|
|
35
|
+
default: return PANEL_COPY.accessSourcePanel
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AccessCardProps {
|
|
40
|
+
panel: PanelWire
|
|
41
|
+
busy: BusyAction | null
|
|
42
|
+
locked: boolean
|
|
43
|
+
notice: string | null
|
|
44
|
+
error: string | null
|
|
45
|
+
onAddGroup: (chatId: string, name: string) => Promise<boolean>
|
|
46
|
+
onRemoveGroup: (chatId: string) => void
|
|
47
|
+
onAddUser: (openId: string, name: string) => Promise<boolean>
|
|
48
|
+
onRemoveUser: (openId: string) => void
|
|
49
|
+
onResolveRequest: (token: string, decision: 'allow' | 'deny') => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function AccessCard(props: AccessCardProps): JSX.Element {
|
|
53
|
+
const { panel, busy, locked, notice, error, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props
|
|
54
|
+
const { access } = panel
|
|
55
|
+
const [groupChatId, setGroupChatId] = useState('')
|
|
56
|
+
const [groupName, setGroupName] = useState('')
|
|
57
|
+
const [userOpenId, setUserOpenId] = useState('')
|
|
58
|
+
const [userName, setUserName] = useState('')
|
|
59
|
+
const groups: readonly AccessGroupWire[] = access.groups
|
|
60
|
+
const users: readonly AccessUserWire[] = access.users
|
|
61
|
+
const requests: readonly AccessRequestWire[] = access.requests
|
|
62
|
+
const working = busy === 'access'
|
|
63
|
+
const disabled = busy !== null || locked || !access.loaded
|
|
64
|
+
|
|
65
|
+
const submitGroup = async (): Promise<void> => {
|
|
66
|
+
if (groupChatId.trim() === '') return
|
|
67
|
+
if (await onAddGroup(groupChatId, groupName)) {
|
|
68
|
+
setGroupChatId('')
|
|
69
|
+
setGroupName('')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const submitUser = async (): Promise<void> => {
|
|
74
|
+
if (userOpenId.trim() === '') return
|
|
75
|
+
if (await onAddUser(userOpenId, userName)) {
|
|
76
|
+
setUserOpenId('')
|
|
77
|
+
setUserName('')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<section className="dsh-channels-cfg__card">
|
|
83
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
84
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.accessCard}</span>
|
|
85
|
+
<span className="dsh-channels-cfg__cardMeta">{accessModeLabel(panel, access.mode)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.accessCardHelp}</span>
|
|
88
|
+
|
|
89
|
+
{access.mode === 'open'
|
|
90
|
+
? <div className="dsh-channels-cfg__banner">{PANEL_COPY.accessModeOpenWarning}</div>
|
|
91
|
+
: null}
|
|
92
|
+
{access.loaded && access.mode === 'guarded' && !access.admin.configured
|
|
93
|
+
? (
|
|
94
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
|
|
95
|
+
<strong>{PANEL_COPY.accessAdminMissingWarning}</strong>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
: null}
|
|
99
|
+
|
|
100
|
+
{!access.loaded
|
|
101
|
+
? <EmptyState text={PANEL_COPY.accessNotLoaded} />
|
|
102
|
+
: (
|
|
103
|
+
<>
|
|
104
|
+
<div className="dsh-channels-cfg__accessSection">
|
|
105
|
+
<div className="dsh-channels-cfg__labelRow">
|
|
106
|
+
<span className="dsh-channels-cfg__label">{PANEL_COPY.accessGroupsTitle}</span>
|
|
107
|
+
<span className="dsh-channels-cfg__cardMeta">{groups.length} 个</span>
|
|
108
|
+
</div>
|
|
109
|
+
{groups.length === 0
|
|
110
|
+
? <span className="dsh-channels-cfg__help">{PANEL_COPY.accessGroupsEmpty}</span>
|
|
111
|
+
: (
|
|
112
|
+
<ul className="dsh-channels-cfg__accessList">
|
|
113
|
+
{groups.map(group => (
|
|
114
|
+
<li className="dsh-channels-cfg__accessRow" key={group.chatId}>
|
|
115
|
+
<span className="dsh-channels-cfg__accessMain">
|
|
116
|
+
<span>{group.name ?? group.chatId}</span>
|
|
117
|
+
<span className="dsh-channels-cfg__accessMeta">
|
|
118
|
+
<span className="dsh-channels-cfg__mono">{group.chatId}</span>
|
|
119
|
+
{` · ${accessSourceLabel(group.source)} · ${formatAccessDate(group.addedAt)}`}
|
|
120
|
+
</span>
|
|
121
|
+
</span>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
className="dsh-channels-cfg__btn"
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
onClick={() => onRemoveGroup(group.chatId)}
|
|
127
|
+
>
|
|
128
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove}
|
|
129
|
+
</button>
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
)}
|
|
134
|
+
<div className="dsh-channels-cfg__accessAdd">
|
|
135
|
+
<input
|
|
136
|
+
type="text"
|
|
137
|
+
className="dsh-channels-cfg__input dsh-channels-cfg__accessInput"
|
|
138
|
+
value={groupChatId}
|
|
139
|
+
placeholder={PANEL_COPY.accessGroupChatIdPlaceholder}
|
|
140
|
+
aria-label={PANEL_COPY.accessGroupChatIdPlaceholder}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
onChange={event => setGroupChatId(event.target.value)}
|
|
143
|
+
/>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
className="dsh-channels-cfg__input dsh-channels-cfg__accessInput"
|
|
147
|
+
value={groupName}
|
|
148
|
+
placeholder={PANEL_COPY.accessGroupNamePlaceholder}
|
|
149
|
+
aria-label={PANEL_COPY.accessGroupNamePlaceholder}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
onChange={event => setGroupName(event.target.value)}
|
|
152
|
+
/>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
156
|
+
disabled={disabled || groupChatId.trim() === ''}
|
|
157
|
+
onClick={() => { void submitGroup() }}
|
|
158
|
+
>
|
|
159
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="dsh-channels-cfg__accessSection">
|
|
165
|
+
<div className="dsh-channels-cfg__labelRow">
|
|
166
|
+
<span className="dsh-channels-cfg__label">{PANEL_COPY.accessUsersTitle}</span>
|
|
167
|
+
<span className="dsh-channels-cfg__cardMeta">{users.length} 个</span>
|
|
168
|
+
</div>
|
|
169
|
+
{users.length === 0
|
|
170
|
+
? <span className="dsh-channels-cfg__help">{PANEL_COPY.accessUsersEmpty}</span>
|
|
171
|
+
: (
|
|
172
|
+
<ul className="dsh-channels-cfg__accessList">
|
|
173
|
+
{users.map(user => (
|
|
174
|
+
<li className="dsh-channels-cfg__accessRow" key={user.openId}>
|
|
175
|
+
<span className="dsh-channels-cfg__accessMain">
|
|
176
|
+
<span>{user.name ?? user.openId}</span>
|
|
177
|
+
<span className="dsh-channels-cfg__accessMeta">
|
|
178
|
+
<span className="dsh-channels-cfg__mono">{user.openId}</span>
|
|
179
|
+
{` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`}
|
|
180
|
+
</span>
|
|
181
|
+
</span>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
className="dsh-channels-cfg__btn"
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
onClick={() => onRemoveUser(user.openId)}
|
|
187
|
+
>
|
|
188
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove}
|
|
189
|
+
</button>
|
|
190
|
+
</li>
|
|
191
|
+
))}
|
|
192
|
+
</ul>
|
|
193
|
+
)}
|
|
194
|
+
<div className="dsh-channels-cfg__accessAdd">
|
|
195
|
+
<input
|
|
196
|
+
type="text"
|
|
197
|
+
className="dsh-channels-cfg__input dsh-channels-cfg__accessInput"
|
|
198
|
+
value={userOpenId}
|
|
199
|
+
placeholder={PANEL_COPY.accessUserOpenIdPlaceholder}
|
|
200
|
+
aria-label={PANEL_COPY.accessUserOpenIdPlaceholder}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
onChange={event => setUserOpenId(event.target.value)}
|
|
203
|
+
/>
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
className="dsh-channels-cfg__input dsh-channels-cfg__accessInput"
|
|
207
|
+
value={userName}
|
|
208
|
+
placeholder={PANEL_COPY.accessUserNamePlaceholder}
|
|
209
|
+
aria-label={PANEL_COPY.accessUserNamePlaceholder}
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
onChange={event => setUserName(event.target.value)}
|
|
212
|
+
/>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
216
|
+
disabled={disabled || userOpenId.trim() === ''}
|
|
217
|
+
onClick={() => { void submitUser() }}
|
|
218
|
+
>
|
|
219
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddUser}
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="dsh-channels-cfg__accessSection">
|
|
225
|
+
<div className="dsh-channels-cfg__labelRow">
|
|
226
|
+
<span className="dsh-channels-cfg__label">{PANEL_COPY.accessRequestsTitle}</span>
|
|
227
|
+
<span className="dsh-channels-cfg__cardMeta">{requests.length} 条</span>
|
|
228
|
+
</div>
|
|
229
|
+
{requests.length === 0
|
|
230
|
+
? <span className="dsh-channels-cfg__help">{PANEL_COPY.accessRequestsEmpty}</span>
|
|
231
|
+
: (
|
|
232
|
+
<ul className="dsh-channels-cfg__accessList">
|
|
233
|
+
{requests.map(request => (
|
|
234
|
+
<li className="dsh-channels-cfg__accessRow" key={request.token}>
|
|
235
|
+
<span className="dsh-channels-cfg__accessMain">
|
|
236
|
+
<span>
|
|
237
|
+
{request.kind === 'group' ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser}
|
|
238
|
+
{':'}
|
|
239
|
+
{request.name ?? request.targetId}
|
|
240
|
+
</span>
|
|
241
|
+
<span className="dsh-channels-cfg__accessMeta">
|
|
242
|
+
<span className="dsh-channels-cfg__mono">{request.targetId}</span>
|
|
243
|
+
{` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`}
|
|
244
|
+
{request.kind === 'user' && request.requester !== undefined
|
|
245
|
+
? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}`
|
|
246
|
+
: ''}
|
|
247
|
+
{request.kind === 'user' && request.groupName !== undefined
|
|
248
|
+
? ` · ${request.groupName}`
|
|
249
|
+
: request.kind === 'user' && request.chatId !== undefined
|
|
250
|
+
? ` · ${request.chatId}`
|
|
251
|
+
: ''}
|
|
252
|
+
</span>
|
|
253
|
+
</span>
|
|
254
|
+
<span className="dsh-channels-cfg__accessActions">
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
onClick={() => onResolveRequest(request.token, 'allow')}
|
|
260
|
+
>
|
|
261
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow}
|
|
262
|
+
</button>
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
className="dsh-channels-cfg__btn"
|
|
266
|
+
disabled={disabled}
|
|
267
|
+
onClick={() => onResolveRequest(request.token, 'deny')}
|
|
268
|
+
>
|
|
269
|
+
{working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny}
|
|
270
|
+
</button>
|
|
271
|
+
</span>
|
|
272
|
+
</li>
|
|
273
|
+
))}
|
|
274
|
+
</ul>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{notice === null
|
|
281
|
+
? null
|
|
282
|
+
: (
|
|
283
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok" role="status" aria-live="polite">
|
|
284
|
+
<span>{notice}</span>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
{error === null
|
|
288
|
+
? null
|
|
289
|
+
: <span className="dsh-channels-cfg__fieldError" role="alert">{error}</span>}
|
|
290
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.accessHints}</span>
|
|
291
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.accessChatIdHint}</span>
|
|
292
|
+
</section>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「访问授权」tab:准入与授权字段表单(管理员 open_id / 准入模式 /
|
|
3
|
+
* 申请有效期)+ 白名单与待处理申请卡。表单保存走全局 draft,白名单
|
|
4
|
+
* 增删是独立即时 mutation。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-channels/client/panel/access-tab
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
|
|
10
|
+
import { AccessCard } from './access-card.tsx'
|
|
11
|
+
import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
|
|
12
|
+
import type { FormView } from './shared.tsx'
|
|
13
|
+
|
|
14
|
+
export interface AccessTabProps {
|
|
15
|
+
form: FormView
|
|
16
|
+
accessNotice: string | null
|
|
17
|
+
accessError: string | null
|
|
18
|
+
onAddGroup: (chatId: string, name: string) => Promise<boolean>
|
|
19
|
+
onRemoveGroup: (chatId: string) => void
|
|
20
|
+
onAddUser: (openId: string, name: string) => Promise<boolean>
|
|
21
|
+
onRemoveUser: (openId: string) => void
|
|
22
|
+
onResolveRequest: (token: string, decision: 'allow' | 'deny') => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function AccessTab(props: AccessTabProps): JSX.Element {
|
|
26
|
+
const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props
|
|
27
|
+
return (
|
|
28
|
+
<div className="dsh-channels-cfg__tabBody">
|
|
29
|
+
<TabHelp text={PANEL_COPY.accessTabHelp} />
|
|
30
|
+
<FieldsCard form={form} groups={TAB_FIELD_GROUPS.access} />
|
|
31
|
+
<SaveBar form={form} tab="access" />
|
|
32
|
+
<AccessCard
|
|
33
|
+
panel={form.panel}
|
|
34
|
+
busy={form.busy}
|
|
35
|
+
locked={form.locked}
|
|
36
|
+
notice={accessNotice}
|
|
37
|
+
error={accessError}
|
|
38
|
+
onAddGroup={onAddGroup}
|
|
39
|
+
onRemoveGroup={onRemoveGroup}
|
|
40
|
+
onAddUser={onAddUser}
|
|
41
|
+
onRemoveUser={onRemoveUser}
|
|
42
|
+
onResolveRequest={onResolveRequest}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「高级」tab:流式刷新等低层字段 + 只读部署信息(overlayPath / version /
|
|
3
|
+
* 部署级参数)+ 「清除 Web 覆盖」。清除覆盖是整体动作,不走 draft。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-channels/client/panel/advanced-tab
|
|
6
|
+
*/
|
|
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'
|
|
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
|
|
30
|
+
onClear: () => void
|
|
31
|
+
}): JSX.Element {
|
|
32
|
+
const { panel, busy, locked, onClear } = props
|
|
33
|
+
const { readOnly } = panel.config
|
|
34
|
+
const overridden = panel.config.overriddenKeys.length
|
|
35
|
+
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>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 目录清理卡:展示清理预览(保留期 / 自动清理 / 候选数量与空间),
|
|
3
|
+
* 执行前二次确认,结果与部分失败逐条展示。`cleanup.error` 非空时
|
|
4
|
+
* (例如未连接、无法扫描工作区)按钮天然禁用并显示原因。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-channels/client/panel/cleanup-card
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
10
|
+
import type { CleanupPreviewWire, CleanupRunWire, PanelWire } from '../../settings/contract.ts'
|
|
11
|
+
import { formatBytes, retentionLabel } from './shared.tsx'
|
|
12
|
+
import type { BusyAction } from './shared.tsx'
|
|
13
|
+
|
|
14
|
+
export function CleanupCard(props: {
|
|
15
|
+
panel: PanelWire
|
|
16
|
+
cleanup: CleanupPreviewWire
|
|
17
|
+
busy: BusyAction | null
|
|
18
|
+
locked: boolean
|
|
19
|
+
result: CleanupRunWire | null
|
|
20
|
+
error: string | null
|
|
21
|
+
onRun: () => void
|
|
22
|
+
}): JSX.Element {
|
|
23
|
+
const { panel, cleanup, busy, locked, result, error, onRun } = props
|
|
24
|
+
const autoText = cleanup.retention === 'off'
|
|
25
|
+
? PANEL_COPY.cleanupAutoInactive
|
|
26
|
+
: cleanup.auto ? PANEL_COPY.cleanupAutoOn : PANEL_COPY.cleanupAutoOff
|
|
27
|
+
const blocked = cleanup.error !== undefined
|
|
28
|
+
? `${PANEL_COPY.cleanupPreviewFailed}:${cleanup.error}`
|
|
29
|
+
: cleanup.retention === 'off'
|
|
30
|
+
? PANEL_COPY.cleanupDisabledOff
|
|
31
|
+
: cleanup.candidates === 0
|
|
32
|
+
? PANEL_COPY.cleanupDisabledEmpty
|
|
33
|
+
: null
|
|
34
|
+
const facts = [
|
|
35
|
+
{ label: PANEL_COPY.cleanupRetention, value: retentionLabel(panel, cleanup) },
|
|
36
|
+
{ label: PANEL_COPY.cleanupAutoLabel, value: autoText },
|
|
37
|
+
{ label: PANEL_COPY.cleanupCandidates, value: `${cleanup.candidates} 个` },
|
|
38
|
+
{ label: PANEL_COPY.cleanupWorkspaces, value: `${cleanup.workspaces} 个` },
|
|
39
|
+
{ label: PANEL_COPY.cleanupCandidateBytes, value: formatBytes(cleanup.candidateBytes) },
|
|
40
|
+
]
|
|
41
|
+
const running = busy === 'cleanup'
|
|
42
|
+
const failed = result !== null && (!result.ok || error !== null)
|
|
43
|
+
return (
|
|
44
|
+
<section className="dsh-channels-cfg__card">
|
|
45
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
46
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.cleanupCard}</span>
|
|
47
|
+
<span className="dsh-channels-cfg__cardMeta">{retentionLabel(panel, cleanup)}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.cleanupCardHelp}</span>
|
|
50
|
+
<div className="dsh-channels-cfg__facts">
|
|
51
|
+
{facts.map(fact => (
|
|
52
|
+
<div className="dsh-channels-cfg__fact" key={fact.label}>
|
|
53
|
+
<span className="dsh-channels-cfg__factLabel">{fact.label}</span>
|
|
54
|
+
<span className="dsh-channels-cfg__factValue">{fact.value}</span>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
{blocked === null ? null : <span className="dsh-channels-cfg__help">{blocked}</span>}
|
|
59
|
+
{result === null
|
|
60
|
+
? null
|
|
61
|
+
: (
|
|
62
|
+
<div className={`dsh-channels-cfg__notice dsh-channels-cfg__notice--${failed ? 'error' : 'ok'}`}>
|
|
63
|
+
{result.ok
|
|
64
|
+
? (
|
|
65
|
+
<span>
|
|
66
|
+
{PANEL_COPY.cleanupDone}:{PANEL_COPY.cleanupDeleted} {result.deletedWorkspaces} 个,
|
|
67
|
+
{PANEL_COPY.cleanupDeletedDirs} {result.deletedSessionDirs} 个,
|
|
68
|
+
{PANEL_COPY.cleanupRetired} {result.retiredRoutes} 条,
|
|
69
|
+
{PANEL_COPY.cleanupFreed} {formatBytes(result.freedBytes)}。
|
|
70
|
+
</span>
|
|
71
|
+
)
|
|
72
|
+
: (
|
|
73
|
+
<span>
|
|
74
|
+
{PANEL_COPY.cleanupFailed}
|
|
75
|
+
{result.error === undefined ? '' : `:${result.error}`}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
{result.failures.length === 0
|
|
79
|
+
? null
|
|
80
|
+
: (
|
|
81
|
+
<>
|
|
82
|
+
<span>{PANEL_COPY.cleanupFailures}</span>
|
|
83
|
+
<ul className="dsh-channels-cfg__issues">
|
|
84
|
+
{result.failures.map((failure, index) => (
|
|
85
|
+
<li key={`${index}-${failure}`} className="dsh-channels-cfg__mono">{failure}</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
{error === null ? null : <span className="dsh-channels-cfg__fieldError">{error}</span>}
|
|
93
|
+
<div className="dsh-channels-cfg__actions">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
|
|
97
|
+
onClick={onRun}
|
|
98
|
+
disabled={running || busy !== null || locked || blocked !== null}
|
|
99
|
+
>
|
|
100
|
+
{running ? PANEL_COPY.cleanupRunning : PANEL_COPY.cleanupNow}
|
|
101
|
+
</button>
|
|
102
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.cleanupConfirmHint}</span>
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
)
|
|
106
|
+
}
|