@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/lib/client.js
CHANGED
|
@@ -20,10 +20,15 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
const PANEL_ROUTE = "/dsh-channels/panel";
|
|
21
21
|
const CONFIG_ROUTE = "/dsh-channels/config";
|
|
22
22
|
const CREDENTIALS_ROUTE = "/dsh-channels/credentials";
|
|
23
|
+
/**
|
|
24
|
+
* Connection lifecycle action (POST). While the channel runs it restarts the
|
|
25
|
+
* long connection; while it is stopped it opens one — so the panel has exactly
|
|
26
|
+
* one "connect / reconnect" button. Credentials are resolved again either way.
|
|
27
|
+
*/
|
|
23
28
|
const RECONNECT_ROUTE = "/dsh-channels/reconnect";
|
|
24
29
|
/** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
|
|
25
30
|
const CATALOG_ROUTE = "/dsh-channels/catalog";
|
|
26
|
-
/**
|
|
31
|
+
/** 手动执行一次过期目录清理(POST,无请求体)。 */
|
|
27
32
|
const CLEANUP_ROUTE = "/dsh-channels/cleanup";
|
|
28
33
|
/** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
|
|
29
34
|
const ACCESS_GROUPS_ROUTE = "/dsh-channels/access/groups";
|
|
@@ -31,25 +36,115 @@ window.__ModuleLoader__.load({
|
|
|
31
36
|
const ACCESS_USERS_ROUTE = "/dsh-channels/access/users";
|
|
32
37
|
/** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
|
|
33
38
|
const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
|
|
39
|
+
/**
|
|
40
|
+
* 打开宿主机的系统目录选择框(POST,无请求体),返回绝对路径。
|
|
41
|
+
*
|
|
42
|
+
* 只有宿主挂载了 `directoryPicker` 的 native 后端(桌面/本地 GUI 会话)才
|
|
43
|
+
* 会注册实际选择逻辑;远程/无头会话的 browse 后端不可用,面板不下发
|
|
44
|
+
* [`EditableFieldWire.browse`],保留手写输入。
|
|
45
|
+
*/
|
|
46
|
+
const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
|
|
47
|
+
/**
|
|
48
|
+
* Top-level panel tabs, in render order. The panel shows the connection state
|
|
49
|
+
* header above every tab, so switching tabs never hides whether the bridge is
|
|
50
|
+
* connected.
|
|
51
|
+
*/
|
|
52
|
+
const PANEL_TABS = [
|
|
53
|
+
{
|
|
54
|
+
id: "connection",
|
|
55
|
+
label: "连接"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "sessions",
|
|
59
|
+
label: "会话与行为"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "model",
|
|
63
|
+
label: "模型"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "workspace",
|
|
67
|
+
label: "工作区"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "access",
|
|
71
|
+
label: "访问授权"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "advanced",
|
|
75
|
+
label: "高级"
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
/** Which editable field groups each tab renders (the tabs own one or two). */
|
|
79
|
+
const TAB_FIELD_GROUPS = {
|
|
80
|
+
connection: [],
|
|
81
|
+
sessions: ["behavior", "content"],
|
|
82
|
+
model: ["model"],
|
|
83
|
+
workspace: ["workspace"],
|
|
84
|
+
access: ["access"],
|
|
85
|
+
advanced: ["streaming"]
|
|
86
|
+
};
|
|
87
|
+
/** Section heading for one field group inside its tab. */
|
|
88
|
+
const FIELD_GROUP_LABELS = {
|
|
89
|
+
workspace: "工作区与文件",
|
|
90
|
+
behavior: "行为",
|
|
91
|
+
content: "内容与附件",
|
|
92
|
+
model: "模型与推理",
|
|
93
|
+
streaming: "流式卡片与日志",
|
|
94
|
+
access: "准入与授权"
|
|
95
|
+
};
|
|
34
96
|
/** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
|
|
35
97
|
const PANEL_COPY = {
|
|
36
98
|
title: "飞书渠道",
|
|
37
|
-
description: "
|
|
99
|
+
description: "飞书长连接、应用凭据与渠道配置。凭据只写入 DSH 凭据库且不回显;标注「需重连」的项在连接建立时读取。",
|
|
38
100
|
statusCard: "连接状态",
|
|
101
|
+
phaseUnconfigured: "待配置",
|
|
102
|
+
phaseIdle: "未连接",
|
|
103
|
+
phaseConnecting: "连接中",
|
|
104
|
+
phaseConnected: "已连接",
|
|
105
|
+
phaseError: "连接失败",
|
|
106
|
+
configuredBadge: "凭据已配置",
|
|
107
|
+
unconfiguredBadge: "凭据未配置",
|
|
108
|
+
statusSummaryConnected: "飞书长连接已建立,机器人可以收发消息。",
|
|
109
|
+
statusSummaryUnconfigured: "尚未配置飞书应用凭据:插件已加载,但不会连接飞书。填写凭据并保存后会自动连接。",
|
|
110
|
+
statusSummaryIdle: "飞书应用凭据已就绪,但当前没有建立长连接。",
|
|
111
|
+
statusSummaryConnecting: "正在连接飞书长连接…",
|
|
112
|
+
statusSummaryError: "最近一次连接飞书失败,请检查凭据与网络后重试。",
|
|
113
|
+
lastError: "最近一次连接失败",
|
|
114
|
+
missingConfig: "缺少配置",
|
|
115
|
+
statusHintUnconfigured: "配置完成后插件会自动连接;也可以稍后在本页手动连接。",
|
|
116
|
+
connectionInfo: "连接信息",
|
|
117
|
+
connectionStateLabel: "飞书长连接",
|
|
118
|
+
botOpenId: "Bot open_id",
|
|
119
|
+
uptime: "运行时长",
|
|
120
|
+
routeCount: "话题路由",
|
|
121
|
+
liveAgentCount: "活动会话",
|
|
122
|
+
inflightTurnCount: "在途回复",
|
|
123
|
+
healthReasons: "健康判定",
|
|
124
|
+
unknownValue: "—",
|
|
125
|
+
connect: "连接飞书",
|
|
126
|
+
connecting: "连接中…",
|
|
127
|
+
onboardingTitle: "开始使用飞书渠道",
|
|
128
|
+
onboardingIntro: "只需三步:在飞书开放平台创建自建应用,把 App ID / App Secret 填入本页,保存后自动连接。",
|
|
129
|
+
onboardingStep1Title: "创建飞书自建应用",
|
|
130
|
+
onboardingStep1Text: "在飞书开放平台创建企业自建应用,开启机器人能力;事件订阅选择「长连接」,订阅 im.message.receive_v1(接收消息)。",
|
|
131
|
+
onboardingStep2Title: "填写应用凭据",
|
|
132
|
+
onboardingStep2Text: "把应用的 App ID(cli_ 开头)与 App Secret 填入下方「飞书应用凭据」,值会写入 DSH 凭据库。",
|
|
133
|
+
onboardingStep3Title: "保存并等待连接",
|
|
134
|
+
onboardingStep3Text: "两项凭据都保存后插件会自动连接;状态变为「已连接」即可在飞书里私聊机器人。",
|
|
135
|
+
onboardingDocsHint: "开放平台地址:open.feishu.cn/app;应用需要开通 im:message、im:message:send_as_bot 等消息权限。",
|
|
39
136
|
credentialsCard: "飞书应用凭据",
|
|
40
|
-
|
|
41
|
-
modelCardHelp: "模型服务商、模型 id、推理档位与新会话的 Agent 预设。下拉选项来自宿主机实时目录;保存后对新会话生效。",
|
|
42
|
-
configCard: "常用配置",
|
|
43
|
-
advancedCard: "只读与高级信息",
|
|
137
|
+
credentialsCardHelp: "凭据值经 DSH 凭据服务写入本机凭据库;面板只显示「已配置 / 未配置」,永不回显已存值。两项都配置后才会连接飞书。",
|
|
44
138
|
appIdLabel: "飞书 App ID",
|
|
45
139
|
appSecretLabel: "飞书 App Secret",
|
|
46
|
-
credentialsHelp: "值通过 DSH 凭据服务写入本机凭据库;面板只显示是否已配置,永不回显已存值。留空表示保持当前值。",
|
|
47
140
|
credentialsRef: "凭据引用",
|
|
48
141
|
saveCredentials: "保存凭据",
|
|
49
142
|
savingCredentials: "保存中…",
|
|
50
143
|
credentialSaved: "凭据已保存",
|
|
51
|
-
credentialSavedReconnected: "
|
|
144
|
+
credentialSavedReconnected: "凭据已保存,飞书已连接",
|
|
52
145
|
credentialSavedPending: "凭据已保存,重启飞书连接后生效",
|
|
146
|
+
credentialSavedUnconfigured: "凭据已保存;还缺少另一项凭据,补齐后会自动连接",
|
|
147
|
+
credentialAutoConnectFailed: "凭据已保存,但自动连接失败",
|
|
53
148
|
credentialRemoved: "已移除存储的凭据",
|
|
54
149
|
credentialRemovedPending: "已移除存储的凭据,重启飞书连接后生效",
|
|
55
150
|
removeCredential: "移除已存值",
|
|
@@ -59,40 +154,62 @@ window.__ModuleLoader__.load({
|
|
|
59
154
|
credentialReadOnly: "当前凭据存储不可写",
|
|
60
155
|
appIdPlaceholder: "输入 App ID(cli_…)",
|
|
61
156
|
appSecretPlaceholder: "输入 App Secret",
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
157
|
+
credentialsPendingHint: "填写后点「保存凭据」;两项齐全时插件会自动连接飞书。",
|
|
158
|
+
connectionTabHelp: "连接状态与飞书应用凭据。凭据不完整时不会连接飞书。",
|
|
159
|
+
sessionsTabHelp: "私聊、群聊触发方式与入站内容;「需重连」项在下次连接时读取。",
|
|
160
|
+
modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
|
|
161
|
+
workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。",
|
|
162
|
+
accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
|
|
163
|
+
advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
|
|
164
|
+
save: "保存更改",
|
|
66
165
|
saving: "保存中…",
|
|
166
|
+
saveHintDirty: "只保存改动过的字段;标注「需重连」的项在下次连接时生效。",
|
|
167
|
+
discardTab: "放弃本页更改",
|
|
168
|
+
saveHintClean: "没有未保存的更改。",
|
|
169
|
+
dirtyCount: "未保存项",
|
|
170
|
+
browseDirectory: "选择目录…",
|
|
171
|
+
browsingDirectory: "选择中…",
|
|
172
|
+
directoryPicked: "已选择目录",
|
|
173
|
+
directoryPickFailed: "无法打开系统目录选择器",
|
|
174
|
+
saved: "已保存",
|
|
175
|
+
cleared: "已清除 Web 覆盖,恢复为 profile 配置",
|
|
67
176
|
clear: "清除 Web 覆盖",
|
|
68
177
|
clearing: "清除中…",
|
|
178
|
+
noOverrides: "当前没有 Web 覆盖",
|
|
179
|
+
overridden: "Web 覆盖",
|
|
180
|
+
inherited: "继承 profile",
|
|
181
|
+
restartTag: "需重连",
|
|
182
|
+
restartBanner: "以下配置需重启飞书连接后生效:",
|
|
183
|
+
restartWarning: "重启会断开当前飞书长连接并中断进行中的回复,请确认空闲时操作。",
|
|
69
184
|
reconnect: "重启飞书连接",
|
|
70
185
|
reconnecting: "重连中…",
|
|
71
186
|
refresh: "刷新状态",
|
|
72
187
|
retry: "重试",
|
|
188
|
+
pollFailed: "状态刷新失败",
|
|
189
|
+
connectDone: "已连接飞书",
|
|
190
|
+
reconnectDone: "已重启飞书连接",
|
|
191
|
+
saveInvalid: "请先修正标红的配置项",
|
|
192
|
+
enabled: "已启用",
|
|
193
|
+
disabled: "已关闭",
|
|
194
|
+
credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
|
|
195
|
+
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
73
196
|
loading: "加载中…",
|
|
74
197
|
loadFailed: "设置面板加载失败",
|
|
75
|
-
saved: "已保存",
|
|
76
|
-
savedModel: "模型配置已保存",
|
|
77
|
-
cleared: "已清除 Web 覆盖,恢复为 profile 配置",
|
|
78
198
|
catalogFailed: "模型目录加载失败,已退回手填。",
|
|
79
199
|
catalogLoading: "正在读取宿主机选项…",
|
|
80
200
|
catalogUnavailable: "宿主机未提供可选项,请手填。",
|
|
81
|
-
noOverrides: "当前没有 Web 覆盖",
|
|
82
|
-
overridden: "Web 覆盖",
|
|
83
|
-
inherited: "继承 profile",
|
|
84
|
-
restartTag: "需重连",
|
|
85
|
-
restartBanner: "以下配置需重启飞书连接后生效:",
|
|
86
|
-
restartWarning: "重启会断开当前飞书长连接并中断进行中的回复,请确认空闲时操作。",
|
|
87
|
-
lastError: "最近一次启动失败",
|
|
88
|
-
notRunning: "未运行",
|
|
89
201
|
version: "插件版本",
|
|
90
202
|
overlayPath: "覆盖文件",
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
overlayPathHelp: "可以直接编辑该 JSON 文件;面板保存会整体重写它,「清除 Web 覆盖」会删除它并回落到 profile 配置。",
|
|
204
|
+
advancedCard: "只读与高级信息",
|
|
205
|
+
advancedHelp: "这些值来自 profile 配置,不能在面板里编辑。",
|
|
206
|
+
readOnlyCard: "部署级参数",
|
|
207
|
+
cleanupCard: "目录清理",
|
|
208
|
+
cleanupCardHelp: "删除超过保留期且不再活跃的飞书话题目录(以及旧版本按话题建立的工作区目录);用户工作区本身、DSH 会话日志都不会删除。删除不可恢复:相关旧话题会失效并提示已过期。",
|
|
93
209
|
cleanupRetention: "保留期",
|
|
94
210
|
cleanupAutoLabel: "自动清理",
|
|
95
|
-
cleanupCandidates: "
|
|
211
|
+
cleanupCandidates: "可清理目录",
|
|
212
|
+
cleanupWorkspaces: "插件工作区",
|
|
96
213
|
cleanupCandidateBytes: "预计释放空间",
|
|
97
214
|
cleanupRetentionOff: "关闭(不删除)",
|
|
98
215
|
cleanupAutoOn: "已开启(启动时 + 每天一次)",
|
|
@@ -100,24 +217,22 @@ window.__ModuleLoader__.load({
|
|
|
100
217
|
cleanupAutoInactive: "未执行(保留期为关闭)",
|
|
101
218
|
cleanupNow: "立即清理",
|
|
102
219
|
cleanupRunning: "清理中…",
|
|
103
|
-
cleanupConfirm: "
|
|
104
|
-
cleanupConfirmHint: "
|
|
105
|
-
cleanupDisabledOff: "
|
|
106
|
-
cleanupDisabledEmpty: "
|
|
220
|
+
cleanupConfirm: "确定清理过期目录吗?该操作不可恢复,会删除这些目录内的全部文件(包括 attachments/ 附件)。",
|
|
221
|
+
cleanupConfirmHint: "删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。",
|
|
222
|
+
cleanupDisabledOff: "保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。",
|
|
223
|
+
cleanupDisabledEmpty: "没有超过保留期的过期目录。",
|
|
107
224
|
cleanupPreviewFailed: "无法读取清理预览",
|
|
108
225
|
cleanupDone: "清理完成",
|
|
109
226
|
cleanupFailed: "清理失败",
|
|
110
227
|
cleanupDeleted: "已删除工作区",
|
|
228
|
+
cleanupDeletedDirs: "话题目录",
|
|
111
229
|
cleanupRetired: "已清理话题路由",
|
|
112
230
|
cleanupFreed: "释放空间",
|
|
113
|
-
cleanupFailures: "
|
|
231
|
+
cleanupFailures: "以下目录未能删除:",
|
|
114
232
|
accessCard: "访问与授权",
|
|
115
|
-
accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 +
|
|
233
|
+
accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 open_id。",
|
|
116
234
|
accessModeOpenWarning: "当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。",
|
|
117
|
-
accessAdminMissingWarning: "当前准入=guarded
|
|
118
|
-
accessAdminLabel: "管理员",
|
|
119
|
-
accessAdminMissing: "未配置",
|
|
120
|
-
accessAdminHint: "修改管理员请到「常用配置 → 行为 → 管理员 open_id」;私聊机器人发送 /userid 可查看自己的 open_id(ou_ 开头,不是 user_id/邮箱)。",
|
|
235
|
+
accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 open_id。",
|
|
121
236
|
accessGroupsTitle: "已授权群",
|
|
122
237
|
accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
|
|
123
238
|
accessUsersTitle: "已授权用户",
|
|
@@ -275,6 +390,24 @@ window.__ModuleLoader__.load({
|
|
|
275
390
|
}
|
|
276
391
|
return JSON.parse(text);
|
|
277
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Ask the Host to open its native directory chooser (the panel's「选择目录…」
|
|
395
|
+
* button). The request stays open while the OS dialog does; a cancel comes back
|
|
396
|
+
* as `{ ok: true, canceled: true }`, not as an error.
|
|
397
|
+
*/
|
|
398
|
+
async function pickDirectory() {
|
|
399
|
+
const response = await request(PICK_DIRECTORY_ROUTE, { method: "POST" });
|
|
400
|
+
const text = await response.text();
|
|
401
|
+
if (!response.ok) try {
|
|
402
|
+
return JSON.parse(text);
|
|
403
|
+
} catch {
|
|
404
|
+
return {
|
|
405
|
+
ok: false,
|
|
406
|
+
error: errorFromBody(response.status, text)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return JSON.parse(text);
|
|
410
|
+
}
|
|
278
411
|
/** Store provided credential values (the request/response never carry stored values). */
|
|
279
412
|
async function saveCredentials(values) {
|
|
280
413
|
const response = await request(CREDENTIALS_ROUTE, {
|
|
@@ -367,7 +500,7 @@ window.__ModuleLoader__.load({
|
|
|
367
500
|
* 浏览器半区的设置面板选择器目录折叠:把 Host 下发的字段描述与实时目录
|
|
368
501
|
* (`/dsh-channels/catalog`)合成通用表单需要的 select 描述。
|
|
369
502
|
*
|
|
370
|
-
* 拆成独立模块(不含 JSX/React)以便单测;`panel.tsx` 只负责串起状态与渲染。
|
|
503
|
+
* 拆成独立模块(不含 JSX/React)以便单测;`panel/index.tsx` 只负责串起状态与渲染。
|
|
371
504
|
*
|
|
372
505
|
* @module dsh-channels/client/catalog
|
|
373
506
|
*/
|
|
@@ -455,31 +588,16 @@ window.__ModuleLoader__.load({
|
|
|
455
588
|
});
|
|
456
589
|
}
|
|
457
590
|
//#endregion
|
|
458
|
-
//#region src/client/panel.tsx
|
|
591
|
+
//#region src/client/panel/shared.tsx
|
|
459
592
|
/**
|
|
460
|
-
*
|
|
593
|
+
* 设置面板共享层:页面状态类型、Host 字段渲染原语(FieldRow / FieldsCard /
|
|
594
|
+
* SaveBar)、生命周期徽标与纯格式化工具。
|
|
461
595
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* 校验与持久化方。组件不 import 任何 DSH 客户端包,只依赖 react 与平台
|
|
465
|
-
* 的 `settings.section` 槽位。
|
|
596
|
+
* 这一层不发请求、不持有页面状态,只把 Host 下发的 wire 数据渲染成控件;
|
|
597
|
+
* 页面外壳在 `index.tsx`,各 tab 只组合这些原语。
|
|
466
598
|
*
|
|
467
|
-
* @module dsh-channels/client/panel
|
|
599
|
+
* @module dsh-channels/client/panel/shared
|
|
468
600
|
*/
|
|
469
|
-
const GROUP_ORDER = [
|
|
470
|
-
"workspace",
|
|
471
|
-
"behavior",
|
|
472
|
-
"content",
|
|
473
|
-
"model",
|
|
474
|
-
"streaming"
|
|
475
|
-
];
|
|
476
|
-
const GROUP_LABELS = {
|
|
477
|
-
workspace: "工作区与文件",
|
|
478
|
-
behavior: "行为",
|
|
479
|
-
content: "内容与附件",
|
|
480
|
-
model: "模型",
|
|
481
|
-
streaming: "流式卡片"
|
|
482
|
-
};
|
|
483
601
|
function messageOf(error) {
|
|
484
602
|
return error instanceof Error ? error.message : String(error);
|
|
485
603
|
}
|
|
@@ -502,12 +620,9 @@ window.__ModuleLoader__.load({
|
|
|
502
620
|
const digits = unit === 0 || value >= 10 ? 0 : 1;
|
|
503
621
|
return `${value.toFixed(digits)} ${units[unit] ?? "B"}`;
|
|
504
622
|
}
|
|
505
|
-
/**
|
|
506
|
-
function retentionLabel(panel, cleanup) {
|
|
507
|
-
return panel.config.fields.find((item) => item.key === "workspaceRetention")?.options?.find((option) => option.value === cleanup.retention)?.label ?? (cleanup.retention === "off" ? PANEL_COPY.cleanupRetentionOff : cleanup.retention);
|
|
508
|
-
}
|
|
623
|
+
/** Compact duration for the connection card. */
|
|
509
624
|
function formatUptime(ms) {
|
|
510
|
-
if (ms === void 0 || !Number.isFinite(ms)) return
|
|
625
|
+
if (ms === void 0 || !Number.isFinite(ms)) return PANEL_COPY.unknownValue;
|
|
511
626
|
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
512
627
|
const days = Math.floor(totalSeconds / 86400);
|
|
513
628
|
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
@@ -517,81 +632,18 @@ window.__ModuleLoader__.load({
|
|
|
517
632
|
if (minutes > 0) return `${minutes} 分 ${totalSeconds % 60} 秒`;
|
|
518
633
|
return `${totalSeconds} 秒`;
|
|
519
634
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
text: "已连接",
|
|
524
|
-
tone: "ok"
|
|
525
|
-
};
|
|
526
|
-
case "connecting": return {
|
|
527
|
-
text: "连接中",
|
|
528
|
-
tone: "warn"
|
|
529
|
-
};
|
|
530
|
-
case "reconnecting": return {
|
|
531
|
-
text: "重连中",
|
|
532
|
-
tone: "warn"
|
|
533
|
-
};
|
|
534
|
-
case "failed": return {
|
|
535
|
-
text: "连接失败",
|
|
536
|
-
tone: "bad"
|
|
537
|
-
};
|
|
538
|
-
case "idle": return {
|
|
539
|
-
text: "未连接",
|
|
540
|
-
tone: "muted"
|
|
541
|
-
};
|
|
542
|
-
default: return {
|
|
543
|
-
text: state ?? "未知",
|
|
544
|
-
tone: "muted"
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function statusBadge(status) {
|
|
549
|
-
if (status.busy) return {
|
|
550
|
-
text: "重连中",
|
|
551
|
-
tone: "warn"
|
|
552
|
-
};
|
|
553
|
-
if (!status.running) return {
|
|
554
|
-
text: "未运行",
|
|
555
|
-
tone: "bad"
|
|
556
|
-
};
|
|
557
|
-
if (status.level === "healthy") return {
|
|
558
|
-
text: "运行中 · 健康",
|
|
559
|
-
tone: "ok"
|
|
560
|
-
};
|
|
561
|
-
if (status.level === "degraded") return {
|
|
562
|
-
text: "运行中 · 降级",
|
|
563
|
-
tone: "warn"
|
|
564
|
-
};
|
|
565
|
-
return {
|
|
566
|
-
text: "运行中 · 异常",
|
|
567
|
-
tone: "bad"
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
/** Local `YYYY-MM-DD HH:mm` rendering for authorization timestamps. */
|
|
571
|
-
function formatAccessDate(ms) {
|
|
572
|
-
if (!Number.isFinite(ms) || ms <= 0) return "—";
|
|
573
|
-
const date = new Date(ms);
|
|
574
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
575
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
635
|
+
/** Effective value for one key (falling back to the profile base). */
|
|
636
|
+
function effectiveValue(panel, key) {
|
|
637
|
+
return panel.config.effective[key] ?? panel.config.base[key];
|
|
576
638
|
}
|
|
577
|
-
/**
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
case "command": return PANEL_COPY.accessSourceCommand;
|
|
581
|
-
case "card": return PANEL_COPY.accessSourceCard;
|
|
582
|
-
default: return PANEL_COPY.accessSourcePanel;
|
|
583
|
-
}
|
|
639
|
+
/** Host label for one editable key; the raw key is the technical fallback. */
|
|
640
|
+
function fieldLabel(fields, key) {
|
|
641
|
+
return fields.find((field) => field.key === key)?.label ?? key;
|
|
584
642
|
}
|
|
585
643
|
/** Effective access-mode label, reusing the Host field options for consistency. */
|
|
586
644
|
function accessModeLabel(panel, mode) {
|
|
587
645
|
return panel.config.fields.find((item) => item.key === "accessMode")?.options?.find((option) => option.value === mode)?.label ?? mode;
|
|
588
646
|
}
|
|
589
|
-
function fieldLabel(fields, key) {
|
|
590
|
-
return fields.find((field) => field.key === key)?.label ?? key;
|
|
591
|
-
}
|
|
592
|
-
function effectiveValue(panel, key) {
|
|
593
|
-
return panel.config.effective[key] ?? panel.config.base[key];
|
|
594
|
-
}
|
|
595
647
|
/** Numeric draft kept as the raw input string; parsed on save. */
|
|
596
648
|
function numericDraft(field, raw) {
|
|
597
649
|
if (typeof raw === "number") return raw;
|
|
@@ -599,12 +651,16 @@ window.__ModuleLoader__.load({
|
|
|
599
651
|
const parsed = Number(raw);
|
|
600
652
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
601
653
|
}
|
|
602
|
-
|
|
654
|
+
/**
|
|
655
|
+
* Turn the dirty draft into the wire payload, keeping the Host-descriptor
|
|
656
|
+
* bounds check client-side. Field messages stay technical (the Host re-validates
|
|
657
|
+
* every value on save).
|
|
658
|
+
*/
|
|
659
|
+
function validateDraft(panel, draft) {
|
|
603
660
|
const values = {};
|
|
604
661
|
const issues = [];
|
|
605
662
|
for (const field of panel.config.fields) {
|
|
606
663
|
if (!(field.key in draft)) continue;
|
|
607
|
-
if (only !== void 0 && !only.has(field.key)) continue;
|
|
608
664
|
const raw = draft[field.key];
|
|
609
665
|
if (field.kind === "number") {
|
|
610
666
|
const parsed = numericDraft(field, raw);
|
|
@@ -643,9 +699,81 @@ window.__ModuleLoader__.load({
|
|
|
643
699
|
issues
|
|
644
700
|
};
|
|
645
701
|
}
|
|
702
|
+
/** Header badge for the Host-derived lifecycle phase. */
|
|
703
|
+
function phaseBadge(phase) {
|
|
704
|
+
switch (phase) {
|
|
705
|
+
case "connected": return {
|
|
706
|
+
text: PANEL_COPY.phaseConnected,
|
|
707
|
+
tone: "ok"
|
|
708
|
+
};
|
|
709
|
+
case "connecting": return {
|
|
710
|
+
text: PANEL_COPY.phaseConnecting,
|
|
711
|
+
tone: "warn"
|
|
712
|
+
};
|
|
713
|
+
case "error": return {
|
|
714
|
+
text: PANEL_COPY.phaseError,
|
|
715
|
+
tone: "bad"
|
|
716
|
+
};
|
|
717
|
+
case "idle": return {
|
|
718
|
+
text: PANEL_COPY.phaseIdle,
|
|
719
|
+
tone: "muted"
|
|
720
|
+
};
|
|
721
|
+
default: return {
|
|
722
|
+
text: PANEL_COPY.phaseUnconfigured,
|
|
723
|
+
tone: "muted"
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** One-line explanation shown under the page title for the current phase. */
|
|
728
|
+
function phaseSummary(phase) {
|
|
729
|
+
switch (phase) {
|
|
730
|
+
case "connected": return PANEL_COPY.statusSummaryConnected;
|
|
731
|
+
case "connecting": return PANEL_COPY.statusSummaryConnecting;
|
|
732
|
+
case "error": return PANEL_COPY.statusSummaryError;
|
|
733
|
+
case "idle": return PANEL_COPY.statusSummaryIdle;
|
|
734
|
+
default: return PANEL_COPY.statusSummaryUnconfigured;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Text input paired with the host-native「选择目录…」button. The button owns
|
|
739
|
+
* its own busy state: the OS dialog can stay open for a long time, so the
|
|
740
|
+
* rest of the form stays usable while the operator browses.
|
|
741
|
+
*/
|
|
742
|
+
function DirectoryTextInput(props) {
|
|
743
|
+
const { field, value, inputClass, disabled, onChange, onPick } = props;
|
|
744
|
+
const [picking, setPicking] = (0, react.useState)(false);
|
|
745
|
+
const pick = async () => {
|
|
746
|
+
setPicking(true);
|
|
747
|
+
try {
|
|
748
|
+
await onPick();
|
|
749
|
+
} finally {
|
|
750
|
+
setPicking(false);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
754
|
+
className: "dsh-channels-cfg__inputRow",
|
|
755
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
756
|
+
type: "text",
|
|
757
|
+
className: inputClass,
|
|
758
|
+
value: String(value ?? ""),
|
|
759
|
+
placeholder: field.placeholder,
|
|
760
|
+
"aria-label": field.label,
|
|
761
|
+
disabled: disabled || picking,
|
|
762
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
763
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
764
|
+
type: "button",
|
|
765
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--browse",
|
|
766
|
+
disabled: disabled || picking,
|
|
767
|
+
onClick: () => {
|
|
768
|
+
pick();
|
|
769
|
+
},
|
|
770
|
+
children: picking ? PANEL_COPY.browsingDirectory : PANEL_COPY.browseDirectory
|
|
771
|
+
})]
|
|
772
|
+
});
|
|
773
|
+
}
|
|
646
774
|
/** One generic form control driven by the Host field descriptor. */
|
|
647
775
|
function FieldRow(props) {
|
|
648
|
-
const { field, value, issues, onChange } = props;
|
|
776
|
+
const { field, value, issues, disabled = false, onChange, onPickDirectory } = props;
|
|
649
777
|
const issue = issues.find((item) => item.key === field.key);
|
|
650
778
|
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
651
779
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -667,13 +795,16 @@ window.__ModuleLoader__.load({
|
|
|
667
795
|
type: "checkbox",
|
|
668
796
|
className: "dsh-channels-cfg__checkbox",
|
|
669
797
|
checked: value === true,
|
|
798
|
+
"aria-label": field.label,
|
|
799
|
+
disabled,
|
|
670
800
|
onChange: (event) => onChange(field.key, event.target.checked)
|
|
671
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === true ?
|
|
801
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === true ? PANEL_COPY.enabled : PANEL_COPY.disabled })]
|
|
672
802
|
}) : null,
|
|
673
803
|
field.kind === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
674
804
|
className: inputClass,
|
|
675
805
|
value: String(value ?? ""),
|
|
676
|
-
disabled: field.disabled === true,
|
|
806
|
+
disabled: disabled || field.disabled === true,
|
|
807
|
+
"aria-label": field.label,
|
|
677
808
|
onChange: (event) => onChange(field.key, event.target.value),
|
|
678
809
|
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
679
810
|
value: option.value,
|
|
@@ -687,13 +818,24 @@ window.__ModuleLoader__.load({
|
|
|
687
818
|
min: field.min,
|
|
688
819
|
max: field.max,
|
|
689
820
|
step: field.step,
|
|
821
|
+
"aria-label": field.label,
|
|
822
|
+
disabled,
|
|
690
823
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
691
824
|
}) : null,
|
|
692
|
-
field.kind === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
825
|
+
field.kind === "text" ? field.browse === "directory" && onPickDirectory !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectoryTextInput, {
|
|
826
|
+
field,
|
|
827
|
+
value,
|
|
828
|
+
inputClass,
|
|
829
|
+
disabled,
|
|
830
|
+
onChange,
|
|
831
|
+
onPick: onPickDirectory
|
|
832
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
693
833
|
type: "text",
|
|
694
834
|
className: inputClass,
|
|
695
835
|
value: String(value ?? ""),
|
|
696
836
|
placeholder: field.placeholder,
|
|
837
|
+
"aria-label": field.label,
|
|
838
|
+
disabled,
|
|
697
839
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
698
840
|
}) : null,
|
|
699
841
|
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -707,143 +849,657 @@ window.__ModuleLoader__.load({
|
|
|
707
849
|
]
|
|
708
850
|
});
|
|
709
851
|
}
|
|
710
|
-
/**
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
{
|
|
725
|
-
label: "运行时长",
|
|
726
|
-
value: formatUptime(status.uptimeMs)
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
label: "话题路由",
|
|
730
|
-
value: String(status.routeCount)
|
|
731
|
-
},
|
|
732
|
-
{
|
|
733
|
-
label: "活动会话",
|
|
734
|
-
value: String(status.liveAgentCount)
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
label: "在途回复",
|
|
738
|
-
value: String(status.inflightTurnCount)
|
|
739
|
-
}
|
|
740
|
-
];
|
|
741
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
852
|
+
/**
|
|
853
|
+
* One tab's editable fields, partitioned by the Host field groups that the tab
|
|
854
|
+
* owns (`TAB_FIELD_GROUPS`). Empty groups render nothing, so a Host that moves
|
|
855
|
+
* every key out of a group does not leave a stray heading behind.
|
|
856
|
+
*/
|
|
857
|
+
function FieldsCard(props) {
|
|
858
|
+
const { form, groups } = props;
|
|
859
|
+
const sections = groups.map((group) => ({
|
|
860
|
+
group,
|
|
861
|
+
label: FIELD_GROUP_LABELS[group],
|
|
862
|
+
fields: form.fields.filter((field) => field.group === group)
|
|
863
|
+
})).filter((section) => section.fields.length > 0);
|
|
864
|
+
if (sections.length === 0) return null;
|
|
865
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
742
866
|
className: "dsh-channels-cfg__card",
|
|
867
|
+
children: sections.map((section) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
868
|
+
className: "dsh-channels-cfg__group",
|
|
869
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
870
|
+
className: "dsh-channels-cfg__labelRow",
|
|
871
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
872
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
873
|
+
children: section.label
|
|
874
|
+
})
|
|
875
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
876
|
+
className: "dsh-channels-cfg__grid",
|
|
877
|
+
children: section.fields.map((field) => {
|
|
878
|
+
const onPick = form.onPickDirectory;
|
|
879
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
|
|
880
|
+
field,
|
|
881
|
+
value: form.draft[field.key] ?? effectiveValue(form.panel, field.key),
|
|
882
|
+
issues: form.issues,
|
|
883
|
+
disabled: form.locked,
|
|
884
|
+
onChange: form.onChange,
|
|
885
|
+
onPickDirectory: onPick === void 0 ? void 0 : () => onPick(field.key)
|
|
886
|
+
}, field.key);
|
|
887
|
+
})
|
|
888
|
+
})]
|
|
889
|
+
}, section.group))
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
/** The unified per-tab action bar: save every dirty field or drop the draft. */
|
|
893
|
+
function SaveBar(props) {
|
|
894
|
+
const { form, tab } = props;
|
|
895
|
+
const groups = new Set(TAB_FIELD_GROUPS[tab]);
|
|
896
|
+
const pageDirty = form.fields.filter((field) => groups.has(field.group) && field.key in form.draft).length;
|
|
897
|
+
const dirty = form.dirtyCount > 0;
|
|
898
|
+
const disabled = !dirty || form.busy !== null || form.locked;
|
|
899
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
900
|
+
className: "dsh-channels-cfg__footer",
|
|
743
901
|
children: [
|
|
744
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}),
|
|
751
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
752
|
-
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${badge.tone}`,
|
|
753
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__dot" }), badge.text]
|
|
754
|
-
}),
|
|
755
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
756
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
757
|
-
children: "每 5 秒自动刷新"
|
|
758
|
-
})
|
|
759
|
-
]
|
|
760
|
-
}),
|
|
761
|
-
status.lastStartError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
762
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
763
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.lastError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
764
|
-
className: "dsh-channels-cfg__mono",
|
|
765
|
-
children: status.lastStartError
|
|
766
|
-
})]
|
|
767
|
-
}),
|
|
768
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
769
|
-
className: "dsh-channels-cfg__facts",
|
|
770
|
-
children: facts.map((fact) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
771
|
-
className: "dsh-channels-cfg__fact",
|
|
772
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
773
|
-
className: "dsh-channels-cfg__factLabel",
|
|
774
|
-
children: fact.label
|
|
775
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
776
|
-
className: "dsh-channels-cfg__factValue",
|
|
777
|
-
children: fact.value
|
|
778
|
-
})]
|
|
779
|
-
}, fact.label))
|
|
780
|
-
}),
|
|
781
|
-
status.reasons.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
782
|
-
className: `dsh-channels-cfg__help${status.level === "unhealthy" ? " dsh-channels-cfg__reasonBad" : ""}`,
|
|
783
|
-
children: ["健康判定:", status.reasons.join(";")]
|
|
902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
903
|
+
type: "button",
|
|
904
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
905
|
+
onClick: form.onSave,
|
|
906
|
+
disabled,
|
|
907
|
+
children: form.busy === "save" ? PANEL_COPY.saving : PANEL_COPY.save
|
|
784
908
|
}),
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
910
|
+
type: "button",
|
|
911
|
+
className: "dsh-channels-cfg__btn",
|
|
912
|
+
onClick: () => form.onDiscardTab(tab),
|
|
913
|
+
disabled: pageDirty === 0 || form.busy !== null || form.locked,
|
|
914
|
+
children: PANEL_COPY.discardTab
|
|
788
915
|
}),
|
|
789
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
790
|
-
|
|
916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
917
|
+
pageDirty > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
918
|
+
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--dirty",
|
|
791
919
|
children: [
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
disabled: busy !== null,
|
|
804
|
-
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
805
|
-
}),
|
|
806
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
807
|
-
className: "dsh-channels-cfg__help",
|
|
808
|
-
children: PANEL_COPY.restartWarning
|
|
809
|
-
})
|
|
920
|
+
PANEL_COPY.dirtyCount,
|
|
921
|
+
" ",
|
|
922
|
+
pageDirty
|
|
923
|
+
]
|
|
924
|
+
}) : dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
925
|
+
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--dirty",
|
|
926
|
+
children: [
|
|
927
|
+
PANEL_COPY.dirtyCount,
|
|
928
|
+
" ",
|
|
929
|
+
form.dirtyCount,
|
|
930
|
+
"(其他标签页)"
|
|
810
931
|
]
|
|
932
|
+
}) : null,
|
|
933
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
934
|
+
className: "dsh-channels-cfg__help",
|
|
935
|
+
children: dirty ? PANEL_COPY.saveHintDirty : PANEL_COPY.saveHintClean
|
|
811
936
|
})
|
|
812
937
|
]
|
|
813
938
|
});
|
|
814
939
|
}
|
|
815
|
-
/**
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
940
|
+
/** Muted help paragraph introducing one tab. */
|
|
941
|
+
function TabHelp(props) {
|
|
942
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
943
|
+
className: "dsh-channels-cfg__tabHelp",
|
|
944
|
+
children: props.text
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
/** Transient page-level banner (save / credentials / reconnect / catalog). */
|
|
948
|
+
function NoticeBar(props) {
|
|
949
|
+
const { notice } = props;
|
|
950
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
951
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${notice.kind === "ok" ? "ok" : "error"}`,
|
|
952
|
+
role: "status",
|
|
953
|
+
"aria-live": "polite",
|
|
954
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice.text })
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
/** Muted empty/blocked state (e.g. access lists before the channel connects). */
|
|
958
|
+
function EmptyState(props) {
|
|
959
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
960
|
+
className: "dsh-channels-cfg__emptyState",
|
|
961
|
+
children: props.text
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** Human label for the official SDK's WebSocket lifecycle state. */
|
|
965
|
+
function connectionStateLabel(state) {
|
|
966
|
+
switch (state) {
|
|
967
|
+
case "connected": return {
|
|
968
|
+
text: PANEL_COPY.phaseConnected,
|
|
969
|
+
tone: "ok"
|
|
970
|
+
};
|
|
971
|
+
case "connecting": return {
|
|
972
|
+
text: PANEL_COPY.phaseConnecting,
|
|
973
|
+
tone: "warn"
|
|
974
|
+
};
|
|
975
|
+
case "reconnecting": return {
|
|
976
|
+
text: PANEL_COPY.phaseConnecting,
|
|
977
|
+
tone: "warn"
|
|
978
|
+
};
|
|
979
|
+
case "failed": return {
|
|
980
|
+
text: PANEL_COPY.phaseError,
|
|
981
|
+
tone: "bad"
|
|
982
|
+
};
|
|
983
|
+
case "idle": return {
|
|
984
|
+
text: PANEL_COPY.phaseIdle,
|
|
985
|
+
tone: "muted"
|
|
986
|
+
};
|
|
987
|
+
default: return {
|
|
988
|
+
text: state ?? PANEL_COPY.unknownValue,
|
|
989
|
+
tone: "muted"
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/** Retention label, reusing the Host field options so the copy stays on the Host. */
|
|
994
|
+
function retentionLabel(panel, cleanup) {
|
|
995
|
+
return panel.config.fields.find((item) => item.key === "workspaceRetention")?.options?.find((option) => option.value === cleanup.retention)?.label ?? (cleanup.retention === "off" ? PANEL_COPY.cleanupRetentionOff : cleanup.retention);
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/client/panel/access-card.tsx
|
|
999
|
+
/**
|
|
1000
|
+
* 访问与授权卡:fail-closed 警告、群/用户白名单增删、待处理申请处理。
|
|
1001
|
+
*
|
|
1002
|
+
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
1003
|
+
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
1004
|
+
* 管理员与准入模式由同 tab 上方「准入与授权」字段表单编辑,这里不再重复。
|
|
1005
|
+
*
|
|
1006
|
+
* @module dsh-channels/client/panel/access-card
|
|
1007
|
+
*/
|
|
1008
|
+
/** Local `YYYY-MM-DD HH:mm` rendering for authorization timestamps. */
|
|
1009
|
+
function formatAccessDate(ms) {
|
|
1010
|
+
if (!Number.isFinite(ms) || ms <= 0) return PANEL_COPY.unknownValue;
|
|
1011
|
+
const date = new Date(ms);
|
|
1012
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1013
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1014
|
+
}
|
|
1015
|
+
/** Which authorization entry produced one grant. */
|
|
1016
|
+
function accessSourceLabel(source) {
|
|
1017
|
+
switch (source) {
|
|
1018
|
+
case "command": return PANEL_COPY.accessSourceCommand;
|
|
1019
|
+
case "card": return PANEL_COPY.accessSourceCard;
|
|
1020
|
+
default: return PANEL_COPY.accessSourcePanel;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function AccessCard(props) {
|
|
1024
|
+
const { panel, busy, locked, notice, error, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1025
|
+
const { access } = panel;
|
|
1026
|
+
const [groupChatId, setGroupChatId] = (0, react.useState)("");
|
|
1027
|
+
const [groupName, setGroupName] = (0, react.useState)("");
|
|
1028
|
+
const [userOpenId, setUserOpenId] = (0, react.useState)("");
|
|
1029
|
+
const [userName, setUserName] = (0, react.useState)("");
|
|
1030
|
+
const groups = access.groups;
|
|
1031
|
+
const users = access.users;
|
|
1032
|
+
const requests = access.requests;
|
|
1033
|
+
const working = busy === "access";
|
|
1034
|
+
const disabled = busy !== null || locked || !access.loaded;
|
|
1035
|
+
const submitGroup = async () => {
|
|
1036
|
+
if (groupChatId.trim() === "") return;
|
|
1037
|
+
if (await onAddGroup(groupChatId, groupName)) {
|
|
1038
|
+
setGroupChatId("");
|
|
1039
|
+
setGroupName("");
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
const submitUser = async () => {
|
|
1043
|
+
if (userOpenId.trim() === "") return;
|
|
1044
|
+
if (await onAddUser(userOpenId, userName)) {
|
|
1045
|
+
setUserOpenId("");
|
|
1046
|
+
setUserName("");
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1050
|
+
className: "dsh-channels-cfg__card",
|
|
1051
|
+
children: [
|
|
1052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1053
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1054
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1055
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1056
|
+
children: PANEL_COPY.accessCard
|
|
1057
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1058
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1059
|
+
children: accessModeLabel(panel, access.mode)
|
|
1060
|
+
})]
|
|
1061
|
+
}),
|
|
1062
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1063
|
+
className: "dsh-channels-cfg__help",
|
|
1064
|
+
children: PANEL_COPY.accessCardHelp
|
|
1065
|
+
}),
|
|
1066
|
+
access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1067
|
+
className: "dsh-channels-cfg__banner",
|
|
1068
|
+
children: PANEL_COPY.accessModeOpenWarning
|
|
1069
|
+
}) : null,
|
|
1070
|
+
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1071
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1072
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning })
|
|
1073
|
+
}) : null,
|
|
1074
|
+
!access.loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, { text: PANEL_COPY.accessNotLoaded }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1075
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1076
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1077
|
+
children: [
|
|
1078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1079
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1080
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1081
|
+
className: "dsh-channels-cfg__label",
|
|
1082
|
+
children: PANEL_COPY.accessGroupsTitle
|
|
1083
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1084
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1085
|
+
children: [groups.length, " 个"]
|
|
1086
|
+
})]
|
|
1087
|
+
}),
|
|
1088
|
+
groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1089
|
+
className: "dsh-channels-cfg__help",
|
|
1090
|
+
children: PANEL_COPY.accessGroupsEmpty
|
|
1091
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1092
|
+
className: "dsh-channels-cfg__accessList",
|
|
1093
|
+
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1094
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1095
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1096
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1097
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: group.name ?? group.chatId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1098
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1099
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1100
|
+
className: "dsh-channels-cfg__mono",
|
|
1101
|
+
children: group.chatId
|
|
1102
|
+
}), ` · ${accessSourceLabel(group.source)} · ${formatAccessDate(group.addedAt)}`]
|
|
1103
|
+
})]
|
|
1104
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1105
|
+
type: "button",
|
|
1106
|
+
className: "dsh-channels-cfg__btn",
|
|
1107
|
+
disabled,
|
|
1108
|
+
onClick: () => onRemoveGroup(group.chatId),
|
|
1109
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1110
|
+
})]
|
|
1111
|
+
}, group.chatId))
|
|
1112
|
+
}),
|
|
1113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1114
|
+
className: "dsh-channels-cfg__accessAdd",
|
|
1115
|
+
children: [
|
|
1116
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1117
|
+
type: "text",
|
|
1118
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1119
|
+
value: groupChatId,
|
|
1120
|
+
placeholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1121
|
+
"aria-label": PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1122
|
+
disabled,
|
|
1123
|
+
onChange: (event) => setGroupChatId(event.target.value)
|
|
1124
|
+
}),
|
|
1125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1126
|
+
type: "text",
|
|
1127
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1128
|
+
value: groupName,
|
|
1129
|
+
placeholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1130
|
+
"aria-label": PANEL_COPY.accessGroupNamePlaceholder,
|
|
1131
|
+
disabled,
|
|
1132
|
+
onChange: (event) => setGroupName(event.target.value)
|
|
1133
|
+
}),
|
|
1134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1135
|
+
type: "button",
|
|
1136
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1137
|
+
disabled: disabled || groupChatId.trim() === "",
|
|
1138
|
+
onClick: () => {
|
|
1139
|
+
submitGroup();
|
|
1140
|
+
},
|
|
1141
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup
|
|
1142
|
+
})
|
|
1143
|
+
]
|
|
1144
|
+
})
|
|
1145
|
+
]
|
|
1146
|
+
}),
|
|
1147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1148
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1149
|
+
children: [
|
|
1150
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1151
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1152
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1153
|
+
className: "dsh-channels-cfg__label",
|
|
1154
|
+
children: PANEL_COPY.accessUsersTitle
|
|
1155
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1156
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1157
|
+
children: [users.length, " 个"]
|
|
1158
|
+
})]
|
|
1159
|
+
}),
|
|
1160
|
+
users.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1161
|
+
className: "dsh-channels-cfg__help",
|
|
1162
|
+
children: PANEL_COPY.accessUsersEmpty
|
|
1163
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1164
|
+
className: "dsh-channels-cfg__accessList",
|
|
1165
|
+
children: users.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1166
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1167
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1168
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1169
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.openId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1170
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1172
|
+
className: "dsh-channels-cfg__mono",
|
|
1173
|
+
children: user.openId
|
|
1174
|
+
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1175
|
+
})]
|
|
1176
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1177
|
+
type: "button",
|
|
1178
|
+
className: "dsh-channels-cfg__btn",
|
|
1179
|
+
disabled,
|
|
1180
|
+
onClick: () => onRemoveUser(user.openId),
|
|
1181
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1182
|
+
})]
|
|
1183
|
+
}, user.openId))
|
|
1184
|
+
}),
|
|
1185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1186
|
+
className: "dsh-channels-cfg__accessAdd",
|
|
1187
|
+
children: [
|
|
1188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1189
|
+
type: "text",
|
|
1190
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1191
|
+
value: userOpenId,
|
|
1192
|
+
placeholder: PANEL_COPY.accessUserOpenIdPlaceholder,
|
|
1193
|
+
"aria-label": PANEL_COPY.accessUserOpenIdPlaceholder,
|
|
1194
|
+
disabled,
|
|
1195
|
+
onChange: (event) => setUserOpenId(event.target.value)
|
|
1196
|
+
}),
|
|
1197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1198
|
+
type: "text",
|
|
1199
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1200
|
+
value: userName,
|
|
1201
|
+
placeholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
1202
|
+
"aria-label": PANEL_COPY.accessUserNamePlaceholder,
|
|
1203
|
+
disabled,
|
|
1204
|
+
onChange: (event) => setUserName(event.target.value)
|
|
1205
|
+
}),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1207
|
+
type: "button",
|
|
1208
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1209
|
+
disabled: disabled || userOpenId.trim() === "",
|
|
1210
|
+
onClick: () => {
|
|
1211
|
+
submitUser();
|
|
1212
|
+
},
|
|
1213
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddUser
|
|
1214
|
+
})
|
|
1215
|
+
]
|
|
1216
|
+
})
|
|
1217
|
+
]
|
|
1218
|
+
}),
|
|
1219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1220
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1221
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1222
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1223
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1224
|
+
className: "dsh-channels-cfg__label",
|
|
1225
|
+
children: PANEL_COPY.accessRequestsTitle
|
|
1226
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1227
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1228
|
+
children: [requests.length, " 条"]
|
|
1229
|
+
})]
|
|
1230
|
+
}), requests.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1231
|
+
className: "dsh-channels-cfg__help",
|
|
1232
|
+
children: PANEL_COPY.accessRequestsEmpty
|
|
1233
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1234
|
+
className: "dsh-channels-cfg__accessList",
|
|
1235
|
+
children: requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1236
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1237
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1238
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1240
|
+
request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser,
|
|
1241
|
+
":",
|
|
1242
|
+
request.name ?? request.targetId
|
|
1243
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1244
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1245
|
+
children: [
|
|
1246
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1247
|
+
className: "dsh-channels-cfg__mono",
|
|
1248
|
+
children: request.targetId
|
|
1249
|
+
}),
|
|
1250
|
+
` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`,
|
|
1251
|
+
request.kind === "user" && request.requester !== void 0 ? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}` : "",
|
|
1252
|
+
request.kind === "user" && request.groupName !== void 0 ? ` · ${request.groupName}` : request.kind === "user" && request.chatId !== void 0 ? ` · ${request.chatId}` : ""
|
|
1253
|
+
]
|
|
1254
|
+
})]
|
|
1255
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1256
|
+
className: "dsh-channels-cfg__accessActions",
|
|
1257
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1258
|
+
type: "button",
|
|
1259
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1260
|
+
disabled,
|
|
1261
|
+
onClick: () => onResolveRequest(request.token, "allow"),
|
|
1262
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1263
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1264
|
+
type: "button",
|
|
1265
|
+
className: "dsh-channels-cfg__btn",
|
|
1266
|
+
disabled,
|
|
1267
|
+
onClick: () => onResolveRequest(request.token, "deny"),
|
|
1268
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1269
|
+
})]
|
|
1270
|
+
})]
|
|
1271
|
+
}, request.token))
|
|
1272
|
+
})]
|
|
1273
|
+
})
|
|
1274
|
+
] }),
|
|
1275
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1276
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
1277
|
+
role: "status",
|
|
1278
|
+
"aria-live": "polite",
|
|
1279
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice })
|
|
1280
|
+
}),
|
|
1281
|
+
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1282
|
+
className: "dsh-channels-cfg__fieldError",
|
|
1283
|
+
role: "alert",
|
|
1284
|
+
children: error
|
|
1285
|
+
}),
|
|
1286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1287
|
+
className: "dsh-channels-cfg__help",
|
|
1288
|
+
children: PANEL_COPY.accessHints
|
|
1289
|
+
}),
|
|
1290
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1291
|
+
className: "dsh-channels-cfg__help",
|
|
1292
|
+
children: PANEL_COPY.accessChatIdHint
|
|
1293
|
+
})
|
|
1294
|
+
]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/client/panel/access-tab.tsx
|
|
1299
|
+
/**
|
|
1300
|
+
* 「访问授权」tab:准入与授权字段表单(管理员 open_id / 准入模式 /
|
|
1301
|
+
* 申请有效期)+ 白名单与待处理申请卡。表单保存走全局 draft,白名单
|
|
1302
|
+
* 增删是独立即时 mutation。
|
|
1303
|
+
*
|
|
1304
|
+
* @module dsh-channels/client/panel/access-tab
|
|
1305
|
+
*/
|
|
1306
|
+
function AccessTab(props) {
|
|
1307
|
+
const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1308
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1309
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1310
|
+
children: [
|
|
1311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.accessTabHelp }),
|
|
1312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
1313
|
+
form,
|
|
1314
|
+
groups: TAB_FIELD_GROUPS.access
|
|
1315
|
+
}),
|
|
1316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
1317
|
+
form,
|
|
1318
|
+
tab: "access"
|
|
1319
|
+
}),
|
|
1320
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessCard, {
|
|
1321
|
+
panel: form.panel,
|
|
1322
|
+
busy: form.busy,
|
|
1323
|
+
locked: form.locked,
|
|
1324
|
+
notice: accessNotice,
|
|
1325
|
+
error: accessError,
|
|
1326
|
+
onAddGroup,
|
|
1327
|
+
onRemoveGroup,
|
|
1328
|
+
onAddUser,
|
|
1329
|
+
onRemoveUser,
|
|
1330
|
+
onResolveRequest
|
|
1331
|
+
})
|
|
1332
|
+
]
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/client/panel/advanced-tab.tsx
|
|
1337
|
+
/**
|
|
1338
|
+
* 「高级」tab:流式刷新等低层字段 + 只读部署信息(overlayPath / version /
|
|
1339
|
+
* 部署级参数)+ 「清除 Web 覆盖」。清除覆盖是整体动作,不走 draft。
|
|
1340
|
+
*
|
|
1341
|
+
* @module dsh-channels/client/panel/advanced-tab
|
|
1342
|
+
*/
|
|
1343
|
+
function AdvancedTab(props) {
|
|
1344
|
+
const { form, onClear } = props;
|
|
1345
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1346
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1347
|
+
children: [
|
|
1348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.advancedTabHelp }),
|
|
1349
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
1350
|
+
form,
|
|
1351
|
+
groups: TAB_FIELD_GROUPS.advanced
|
|
1352
|
+
}),
|
|
1353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
1354
|
+
form,
|
|
1355
|
+
tab: "advanced"
|
|
1356
|
+
}),
|
|
1357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedCard, {
|
|
1358
|
+
panel: form.panel,
|
|
1359
|
+
busy: form.busy,
|
|
1360
|
+
locked: form.locked,
|
|
1361
|
+
onClear
|
|
1362
|
+
})
|
|
1363
|
+
]
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
/** Read-only deployment facts; the only write action here drops the overlay file. */
|
|
1367
|
+
function AdvancedCard(props) {
|
|
1368
|
+
const { panel, busy, locked, onClear } = props;
|
|
1369
|
+
const { readOnly } = panel.config;
|
|
1370
|
+
const overridden = panel.config.overriddenKeys.length;
|
|
1371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1372
|
+
className: "dsh-channels-cfg__card",
|
|
1373
|
+
children: [
|
|
1374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1375
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1376
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1377
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1378
|
+
children: PANEL_COPY.advancedCard
|
|
1379
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1380
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1381
|
+
children: overridden === 0 ? PANEL_COPY.inherited : `${PANEL_COPY.overridden} ${overridden} 项`
|
|
1382
|
+
})]
|
|
1383
|
+
}),
|
|
1384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1385
|
+
className: "dsh-channels-cfg__help",
|
|
1386
|
+
children: PANEL_COPY.advancedHelp
|
|
1387
|
+
}),
|
|
1388
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1389
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1390
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1391
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1392
|
+
children: PANEL_COPY.readOnlyCard
|
|
1393
|
+
})
|
|
1394
|
+
}),
|
|
1395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1396
|
+
className: "dsh-channels-cfg__readOnly",
|
|
1397
|
+
children: [
|
|
1398
|
+
readOnly.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1399
|
+
className: "dsh-channels-cfg__kv",
|
|
1400
|
+
children: [
|
|
1401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1402
|
+
className: "dsh-channels-cfg__kvKey",
|
|
1403
|
+
children: field.label
|
|
1404
|
+
}),
|
|
1405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1406
|
+
className: "dsh-channels-cfg__kvValue",
|
|
1407
|
+
children: field.value
|
|
1408
|
+
}),
|
|
1409
|
+
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1410
|
+
className: "dsh-channels-cfg__help",
|
|
1411
|
+
children: field.help
|
|
1412
|
+
})
|
|
1413
|
+
]
|
|
1414
|
+
}, field.key)),
|
|
1415
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1416
|
+
className: "dsh-channels-cfg__kv",
|
|
1417
|
+
children: [
|
|
1418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1419
|
+
className: "dsh-channels-cfg__kvKey",
|
|
1420
|
+
children: PANEL_COPY.overlayPath
|
|
1421
|
+
}),
|
|
1422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1423
|
+
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1424
|
+
children: panel.status.overlayPath
|
|
1425
|
+
}),
|
|
1426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1427
|
+
className: "dsh-channels-cfg__help",
|
|
1428
|
+
children: PANEL_COPY.overlayPathHelp
|
|
1429
|
+
})
|
|
1430
|
+
]
|
|
1431
|
+
}),
|
|
1432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1433
|
+
className: "dsh-channels-cfg__kv",
|
|
1434
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1435
|
+
className: "dsh-channels-cfg__kvKey",
|
|
1436
|
+
children: PANEL_COPY.version
|
|
1437
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1438
|
+
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1439
|
+
children: panel.status.version
|
|
1440
|
+
})]
|
|
1441
|
+
})
|
|
1442
|
+
]
|
|
1443
|
+
}),
|
|
1444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1445
|
+
className: "dsh-channels-cfg__actions",
|
|
1446
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1447
|
+
type: "button",
|
|
1448
|
+
className: "dsh-channels-cfg__btn",
|
|
1449
|
+
onClick: onClear,
|
|
1450
|
+
disabled: overridden === 0 || busy !== null || locked,
|
|
1451
|
+
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
1452
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1453
|
+
className: "dsh-channels-cfg__help",
|
|
1454
|
+
children: overridden === 0 ? PANEL_COPY.noOverrides : `${PANEL_COPY.overridden} ${overridden} 项`
|
|
1455
|
+
})]
|
|
1456
|
+
})
|
|
1457
|
+
]
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/client/panel/credentials-card.tsx
|
|
1462
|
+
/**
|
|
1463
|
+
* 飞书应用凭据编辑器:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示
|
|
1464
|
+
* 「已配置 / 未配置」,两个方向都不回显已存值。保存动作由页面外壳持有
|
|
1465
|
+
* (凭据保存独立于配置 draft)。
|
|
1466
|
+
*
|
|
1467
|
+
* @module dsh-channels/client/panel/credentials-card
|
|
1468
|
+
*/
|
|
1469
|
+
function CredentialsCard(props) {
|
|
1470
|
+
const { panel, credentials } = props;
|
|
1471
|
+
const { appId, appSecret, issues, busy, locked, onAppIdChange, onAppSecretChange, onSave, onClear } = credentials;
|
|
1472
|
+
const appIdState = panel.config.credentials.appId;
|
|
1473
|
+
const appSecretState = panel.config.credentials.appSecret;
|
|
1474
|
+
const configured = appIdState.configured && appSecretState.configured;
|
|
1475
|
+
const writable = appIdState.writable || appSecretState.writable;
|
|
1476
|
+
const disabled = busy !== null || locked || !writable;
|
|
1477
|
+
const pending = appId.trim() !== "" || appSecret.trim() !== "";
|
|
1478
|
+
const row = (field, label, state, value, type, placeholder, onChange) => {
|
|
1479
|
+
const issue = issues.find((item) => item.key === field);
|
|
1480
|
+
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
1481
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1482
|
+
className: "dsh-channels-cfg__field",
|
|
1483
|
+
children: [
|
|
1484
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1485
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1486
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1487
|
+
className: "dsh-channels-cfg__label",
|
|
1488
|
+
children: label
|
|
1489
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1490
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${state.configured ? "ok" : "bad"}`,
|
|
1491
|
+
children: state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing
|
|
1492
|
+
})]
|
|
1493
|
+
}),
|
|
1494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1495
|
+
className: "dsh-channels-cfg__cred",
|
|
1496
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1497
|
+
type,
|
|
1498
|
+
className: inputClass,
|
|
1499
|
+
value,
|
|
1500
|
+
placeholder,
|
|
846
1501
|
autoComplete: "new-password",
|
|
1502
|
+
"aria-label": label,
|
|
847
1503
|
disabled,
|
|
848
1504
|
onChange: (event) => onChange(event.target.value)
|
|
849
1505
|
}), state.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -863,7 +1519,7 @@ window.__ModuleLoader__.load({
|
|
|
863
1519
|
className: "dsh-channels-cfg__mono",
|
|
864
1520
|
children: state.ref
|
|
865
1521
|
}),
|
|
866
|
-
state.source === void 0 ? "" :
|
|
1522
|
+
state.source === void 0 ? "" : ` · ${state.source}`
|
|
867
1523
|
]
|
|
868
1524
|
}),
|
|
869
1525
|
issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -882,10 +1538,14 @@ window.__ModuleLoader__.load({
|
|
|
882
1538
|
className: "dsh-channels-cfg__cardTitle",
|
|
883
1539
|
children: PANEL_COPY.credentialsCard
|
|
884
1540
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
885
|
-
className:
|
|
886
|
-
children:
|
|
1541
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${configured ? "ok" : "muted"}`,
|
|
1542
|
+
children: configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge
|
|
887
1543
|
})]
|
|
888
1544
|
}),
|
|
1545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1546
|
+
className: "dsh-channels-cfg__help",
|
|
1547
|
+
children: PANEL_COPY.credentialsCardHelp
|
|
1548
|
+
}),
|
|
889
1549
|
row("appId", PANEL_COPY.appIdLabel, appIdState, appId, "text", PANEL_COPY.appIdPlaceholder, onAppIdChange),
|
|
890
1550
|
row("appSecret", PANEL_COPY.appSecretLabel, appSecretState, appSecret, "password", PANEL_COPY.appSecretPlaceholder, onAppSecretChange),
|
|
891
1551
|
writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -905,101 +1565,154 @@ window.__ModuleLoader__.load({
|
|
|
905
1565
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
906
1566
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
907
1567
|
className: "dsh-channels-cfg__help",
|
|
908
|
-
children: PANEL_COPY.
|
|
1568
|
+
children: PANEL_COPY.credentialsPendingHint
|
|
909
1569
|
})
|
|
910
1570
|
]
|
|
911
1571
|
})
|
|
912
1572
|
]
|
|
913
1573
|
});
|
|
914
1574
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region src/client/panel/connection-tab.tsx
|
|
1577
|
+
/**
|
|
1578
|
+
* 「连接」tab:按 Host 派生的生命周期阶段渲染连接引导。
|
|
1579
|
+
*
|
|
1580
|
+
* - unconfigured:onboarding 三步 + 凭据卡(不渲染可用的「连接」按钮)
|
|
1581
|
+
* - idle:连接信息卡 + 「连接飞书」
|
|
1582
|
+
* - error:连接信息卡 + 最近一次连接失败 + 「重试」
|
|
1583
|
+
* - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
|
|
1584
|
+
* - connecting:禁用按钮显示「连接中…」
|
|
1585
|
+
*
|
|
1586
|
+
* @module dsh-channels/client/panel/connection-tab
|
|
1587
|
+
*/
|
|
1588
|
+
function ConnectionTab(props) {
|
|
1589
|
+
const { panel, status, busy, credentials, onReconnect } = props;
|
|
1590
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1591
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1592
|
+
children: [
|
|
1593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.connectionTabHelp }),
|
|
1594
|
+
panel.config.restartPendingKeys.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartBanner, {
|
|
1595
|
+
panel,
|
|
1596
|
+
busy,
|
|
1597
|
+
locked: status.busy,
|
|
1598
|
+
onReconnect
|
|
1599
|
+
}),
|
|
1600
|
+
status.phase === "unconfigured" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OnboardingCard, { status }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConnectionInfoCard, {
|
|
1601
|
+
status,
|
|
1602
|
+
busy,
|
|
1603
|
+
onReconnect
|
|
1604
|
+
}),
|
|
1605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsCard, {
|
|
1606
|
+
panel,
|
|
1607
|
+
credentials
|
|
1608
|
+
})
|
|
1609
|
+
]
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
/** First-run guide: create the app, fill the credentials, save and wait. */
|
|
1613
|
+
function OnboardingCard(props) {
|
|
1614
|
+
const { status } = props;
|
|
1615
|
+
const steps = [
|
|
1616
|
+
{
|
|
1617
|
+
title: PANEL_COPY.onboardingStep1Title,
|
|
1618
|
+
text: PANEL_COPY.onboardingStep1Text
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
title: PANEL_COPY.onboardingStep2Title,
|
|
1622
|
+
text: PANEL_COPY.onboardingStep2Text
|
|
1623
|
+
},
|
|
1624
|
+
{
|
|
1625
|
+
title: PANEL_COPY.onboardingStep3Title,
|
|
1626
|
+
text: PANEL_COPY.onboardingStep3Text
|
|
1627
|
+
}
|
|
1628
|
+
];
|
|
919
1629
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
920
1630
|
className: "dsh-channels-cfg__card",
|
|
921
|
-
children: [
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}),
|
|
941
|
-
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
942
|
-
className: "dsh-channels-cfg__help",
|
|
943
|
-
children: field.help
|
|
944
|
-
})
|
|
945
|
-
]
|
|
946
|
-
}, field.key)),
|
|
947
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
948
|
-
className: "dsh-channels-cfg__kv",
|
|
949
|
-
children: [
|
|
950
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
951
|
-
className: "dsh-channels-cfg__kvKey",
|
|
952
|
-
children: PANEL_COPY.overlayPath
|
|
953
|
-
}),
|
|
954
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
955
|
-
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
956
|
-
children: panel.status.overlayPath
|
|
957
|
-
}),
|
|
958
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
959
|
-
className: "dsh-channels-cfg__help",
|
|
960
|
-
children: "可以直接编辑该 JSON 文件;面板保存会整体重写它,「清除 Web 覆盖」会删除它。"
|
|
961
|
-
})
|
|
962
|
-
]
|
|
963
|
-
}),
|
|
964
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
965
|
-
className: "dsh-channels-cfg__kv",
|
|
1631
|
+
children: [
|
|
1632
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1633
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1634
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1635
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1636
|
+
children: PANEL_COPY.onboardingTitle
|
|
1637
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1638
|
+
className: "dsh-channels-cfg__badge dsh-channels-cfg__badge--muted",
|
|
1639
|
+
children: PANEL_COPY.phaseUnconfigured
|
|
1640
|
+
})]
|
|
1641
|
+
}),
|
|
1642
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1643
|
+
className: "dsh-channels-cfg__help",
|
|
1644
|
+
children: PANEL_COPY.onboardingIntro
|
|
1645
|
+
}),
|
|
1646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
1647
|
+
className: "dsh-channels-cfg__steps",
|
|
1648
|
+
children: steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1649
|
+
className: "dsh-channels-cfg__step",
|
|
966
1650
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
967
|
-
className: "dsh-channels-
|
|
968
|
-
children:
|
|
969
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
970
|
-
className: "dsh-channels-
|
|
971
|
-
children:
|
|
1651
|
+
className: "dsh-channels-cfg__stepIndex",
|
|
1652
|
+
children: index + 1
|
|
1653
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1654
|
+
className: "dsh-channels-cfg__stepBody",
|
|
1655
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1656
|
+
className: "dsh-channels-cfg__stepTitle",
|
|
1657
|
+
children: step.title
|
|
1658
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1659
|
+
className: "dsh-channels-cfg__help",
|
|
1660
|
+
children: step.text
|
|
1661
|
+
})]
|
|
972
1662
|
})]
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
|
|
1663
|
+
}, step.title))
|
|
1664
|
+
}),
|
|
1665
|
+
status.missingConfig.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1666
|
+
className: "dsh-channels-cfg__banner",
|
|
1667
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [PANEL_COPY.missingConfig, ":"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: status.missingConfig.join("、") })]
|
|
1668
|
+
}),
|
|
1669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1670
|
+
className: "dsh-channels-cfg__help",
|
|
1671
|
+
children: PANEL_COPY.statusHintUnconfigured
|
|
1672
|
+
}),
|
|
1673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1674
|
+
className: "dsh-channels-cfg__help",
|
|
1675
|
+
children: PANEL_COPY.onboardingDocsHint
|
|
1676
|
+
})
|
|
1677
|
+
]
|
|
976
1678
|
});
|
|
977
1679
|
}
|
|
978
|
-
/**
|
|
979
|
-
function
|
|
980
|
-
const {
|
|
981
|
-
const
|
|
982
|
-
const
|
|
1680
|
+
/** Runtime facts plus the one lifecycle action available in the current phase. */
|
|
1681
|
+
function ConnectionInfoCard(props) {
|
|
1682
|
+
const { status, busy, onReconnect } = props;
|
|
1683
|
+
const badge = phaseBadge(status.phase);
|
|
1684
|
+
const actionBusy = busy !== null || status.busy;
|
|
1685
|
+
const connection = connectionStateLabel(status.connection?.state);
|
|
983
1686
|
const facts = [
|
|
984
1687
|
{
|
|
985
|
-
label: PANEL_COPY.
|
|
986
|
-
value:
|
|
1688
|
+
label: PANEL_COPY.statusCard,
|
|
1689
|
+
value: badge.text
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
label: PANEL_COPY.connectionStateLabel,
|
|
1693
|
+
value: connection.text
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
label: PANEL_COPY.botOpenId,
|
|
1697
|
+
value: status.botOpenIdTail ?? PANEL_COPY.unknownValue
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
label: PANEL_COPY.uptime,
|
|
1701
|
+
value: formatUptime(status.uptimeMs)
|
|
987
1702
|
},
|
|
988
1703
|
{
|
|
989
|
-
label: PANEL_COPY.
|
|
990
|
-
value:
|
|
1704
|
+
label: PANEL_COPY.routeCount,
|
|
1705
|
+
value: String(status.routeCount)
|
|
991
1706
|
},
|
|
992
1707
|
{
|
|
993
|
-
label: PANEL_COPY.
|
|
994
|
-
value:
|
|
1708
|
+
label: PANEL_COPY.liveAgentCount,
|
|
1709
|
+
value: String(status.liveAgentCount)
|
|
995
1710
|
},
|
|
996
1711
|
{
|
|
997
|
-
label: PANEL_COPY.
|
|
998
|
-
value:
|
|
1712
|
+
label: PANEL_COPY.inflightTurnCount,
|
|
1713
|
+
value: String(status.inflightTurnCount)
|
|
999
1714
|
}
|
|
1000
1715
|
];
|
|
1001
|
-
const running = busy === "cleanup";
|
|
1002
|
-
const failed = result !== null && (!result.ok || error !== null);
|
|
1003
1716
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1004
1717
|
className: "dsh-channels-cfg__card",
|
|
1005
1718
|
children: [
|
|
@@ -1007,15 +1720,18 @@ window.__ModuleLoader__.load({
|
|
|
1007
1720
|
className: "dsh-channels-cfg__cardHead",
|
|
1008
1721
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1009
1722
|
className: "dsh-channels-cfg__cardTitle",
|
|
1010
|
-
children: PANEL_COPY.
|
|
1723
|
+
children: PANEL_COPY.connectionInfo
|
|
1011
1724
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1012
|
-
className:
|
|
1013
|
-
children:
|
|
1725
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${status.configured ? "ok" : "bad"}`,
|
|
1726
|
+
children: status.configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge
|
|
1014
1727
|
})]
|
|
1015
1728
|
}),
|
|
1016
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1017
|
-
className: "dsh-channels-
|
|
1018
|
-
children: PANEL_COPY.
|
|
1729
|
+
status.lastStartError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1730
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1731
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.lastError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1732
|
+
className: "dsh-channels-cfg__mono",
|
|
1733
|
+
children: status.lastStartError
|
|
1734
|
+
})]
|
|
1019
1735
|
}),
|
|
1020
1736
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1021
1737
|
className: "dsh-channels-cfg__facts",
|
|
@@ -1030,82 +1746,166 @@ window.__ModuleLoader__.load({
|
|
|
1030
1746
|
})]
|
|
1031
1747
|
}, fact.label))
|
|
1032
1748
|
}),
|
|
1033
|
-
|
|
1034
|
-
className:
|
|
1035
|
-
children:
|
|
1036
|
-
|
|
1037
|
-
result === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1038
|
-
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${failed ? "error" : "ok"}`,
|
|
1039
|
-
children: [result.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1040
|
-
PANEL_COPY.cleanupDone,
|
|
1749
|
+
status.reasons.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1750
|
+
className: `dsh-channels-cfg__help${status.level === "unhealthy" ? " dsh-channels-cfg__reasonBad" : ""}`,
|
|
1751
|
+
children: [
|
|
1752
|
+
PANEL_COPY.healthReasons,
|
|
1041
1753
|
":",
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
result.deletedWorkspaces,
|
|
1045
|
-
" 个,",
|
|
1046
|
-
PANEL_COPY.cleanupRetired,
|
|
1047
|
-
" ",
|
|
1048
|
-
result.retiredRoutes,
|
|
1049
|
-
" 条,",
|
|
1050
|
-
PANEL_COPY.cleanupFreed,
|
|
1051
|
-
" ",
|
|
1052
|
-
formatBytes(result.freedBytes),
|
|
1053
|
-
"。"
|
|
1054
|
-
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.cleanupFailed, result.error === void 0 ? "" : `:${result.error}`] }), result.failures.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: PANEL_COPY.cleanupFailures }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1055
|
-
className: "dsh-channels-cfg__issues",
|
|
1056
|
-
children: result.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1057
|
-
className: "dsh-channels-cfg__mono",
|
|
1058
|
-
children: failure
|
|
1059
|
-
}, `${index}-${failure}`))
|
|
1060
|
-
})] })]
|
|
1061
|
-
}),
|
|
1062
|
-
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1063
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1064
|
-
children: error
|
|
1754
|
+
status.reasons.join(";")
|
|
1755
|
+
]
|
|
1065
1756
|
}),
|
|
1066
1757
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1067
1758
|
className: "dsh-channels-cfg__actions",
|
|
1068
|
-
children: [
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1759
|
+
children: [
|
|
1760
|
+
status.phase === "connecting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1761
|
+
type: "button",
|
|
1762
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1763
|
+
disabled: true,
|
|
1764
|
+
children: PANEL_COPY.connecting
|
|
1765
|
+
}) : null,
|
|
1766
|
+
status.phase === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1767
|
+
type: "button",
|
|
1768
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1769
|
+
onClick: onReconnect,
|
|
1770
|
+
disabled: actionBusy,
|
|
1771
|
+
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.connect
|
|
1772
|
+
}) : null,
|
|
1773
|
+
status.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1774
|
+
type: "button",
|
|
1775
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1776
|
+
onClick: onReconnect,
|
|
1777
|
+
disabled: actionBusy,
|
|
1778
|
+
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.retry
|
|
1779
|
+
}) : null,
|
|
1780
|
+
status.phase === "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1781
|
+
type: "button",
|
|
1782
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1783
|
+
onClick: onReconnect,
|
|
1784
|
+
disabled: actionBusy,
|
|
1785
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
1786
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1787
|
+
className: "dsh-channels-cfg__help",
|
|
1788
|
+
children: PANEL_COPY.restartWarning
|
|
1789
|
+
})] }) : null
|
|
1790
|
+
]
|
|
1078
1791
|
})
|
|
1079
1792
|
]
|
|
1080
1793
|
});
|
|
1081
1794
|
}
|
|
1082
|
-
/**
|
|
1083
|
-
function
|
|
1084
|
-
const { panel, busy,
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1795
|
+
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
1796
|
+
function RestartBanner(props) {
|
|
1797
|
+
const { panel, busy, locked, onReconnect } = props;
|
|
1798
|
+
const keys = panel.config.restartPendingKeys.map((key) => fieldLabel(panel.config.fields, key)).join("、");
|
|
1799
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1800
|
+
className: "dsh-channels-cfg__banner",
|
|
1801
|
+
children: [
|
|
1802
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.restartBanner, keys] }),
|
|
1803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1804
|
+
type: "button",
|
|
1805
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1806
|
+
onClick: onReconnect,
|
|
1807
|
+
disabled: busy !== null || locked,
|
|
1808
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
1809
|
+
}),
|
|
1810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1811
|
+
className: "dsh-channels-cfg__help",
|
|
1812
|
+
children: PANEL_COPY.restartWarning
|
|
1813
|
+
})
|
|
1814
|
+
]
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region src/client/panel/model-tab.tsx
|
|
1819
|
+
/**
|
|
1820
|
+
* 「模型」tab:模型服务商 / 模型 id / 推理档位 / Agent 预设四个选择器。
|
|
1821
|
+
* 选项来自 Host 实时目录(页面外壳负责 provider→model→effort 级联刷新),
|
|
1822
|
+
* 目录读取中时在表单上方提示。
|
|
1823
|
+
*
|
|
1824
|
+
* @module dsh-channels/client/panel/model-tab
|
|
1825
|
+
*/
|
|
1826
|
+
function ModelTab(props) {
|
|
1827
|
+
const { form, catalogBusy } = props;
|
|
1828
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1829
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1830
|
+
children: [
|
|
1831
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.modelTabHelp }),
|
|
1832
|
+
catalogBusy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1833
|
+
className: "dsh-channels-cfg__help",
|
|
1834
|
+
children: PANEL_COPY.catalogLoading
|
|
1835
|
+
}) : null,
|
|
1836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
1837
|
+
form,
|
|
1838
|
+
groups: TAB_FIELD_GROUPS.model
|
|
1839
|
+
}),
|
|
1840
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
1841
|
+
form,
|
|
1842
|
+
tab: "model"
|
|
1843
|
+
})
|
|
1844
|
+
]
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1848
|
+
//#region src/client/panel/sessions-tab.tsx
|
|
1849
|
+
/**
|
|
1850
|
+
* 「会话与行为」tab:行为 + 内容与附件两组字段(私聊/群聊触发方式、
|
|
1851
|
+
* 首次引导、权限切换、附件接收)。表单字段完全由 Host 描述驱动。
|
|
1852
|
+
*
|
|
1853
|
+
* @module dsh-channels/client/panel/sessions-tab
|
|
1854
|
+
*/
|
|
1855
|
+
function SessionsTab(props) {
|
|
1856
|
+
const { form } = props;
|
|
1857
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1858
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1859
|
+
children: [
|
|
1860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.sessionsTabHelp }),
|
|
1861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
1862
|
+
form,
|
|
1863
|
+
groups: TAB_FIELD_GROUPS.sessions
|
|
1864
|
+
}),
|
|
1865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
1866
|
+
form,
|
|
1867
|
+
tab: "sessions"
|
|
1868
|
+
})
|
|
1869
|
+
]
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/client/panel/cleanup-card.tsx
|
|
1874
|
+
/**
|
|
1875
|
+
* 目录清理卡:展示清理预览(保留期 / 自动清理 / 候选数量与空间),
|
|
1876
|
+
* 执行前二次确认,结果与部分失败逐条展示。`cleanup.error` 非空时
|
|
1877
|
+
* (例如未连接、无法扫描工作区)按钮天然禁用并显示原因。
|
|
1878
|
+
*
|
|
1879
|
+
* @module dsh-channels/client/panel/cleanup-card
|
|
1880
|
+
*/
|
|
1881
|
+
function CleanupCard(props) {
|
|
1882
|
+
const { panel, cleanup, busy, locked, result, error, onRun } = props;
|
|
1883
|
+
const autoText = cleanup.retention === "off" ? PANEL_COPY.cleanupAutoInactive : cleanup.auto ? PANEL_COPY.cleanupAutoOn : PANEL_COPY.cleanupAutoOff;
|
|
1884
|
+
const blocked = cleanup.error !== void 0 ? `${PANEL_COPY.cleanupPreviewFailed}:${cleanup.error}` : cleanup.retention === "off" ? PANEL_COPY.cleanupDisabledOff : cleanup.candidates === 0 ? PANEL_COPY.cleanupDisabledEmpty : null;
|
|
1885
|
+
const facts = [
|
|
1886
|
+
{
|
|
1887
|
+
label: PANEL_COPY.cleanupRetention,
|
|
1888
|
+
value: retentionLabel(panel, cleanup)
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
label: PANEL_COPY.cleanupAutoLabel,
|
|
1892
|
+
value: autoText
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
label: PANEL_COPY.cleanupCandidates,
|
|
1896
|
+
value: `${cleanup.candidates} 个`
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
label: PANEL_COPY.cleanupWorkspaces,
|
|
1900
|
+
value: `${cleanup.workspaces} 个`
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
label: PANEL_COPY.cleanupCandidateBytes,
|
|
1904
|
+
value: formatBytes(cleanup.candidateBytes)
|
|
1107
1905
|
}
|
|
1108
|
-
|
|
1906
|
+
];
|
|
1907
|
+
const running = busy === "cleanup";
|
|
1908
|
+
const failed = result !== null && (!result.ok || error !== null);
|
|
1109
1909
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1110
1910
|
className: "dsh-channels-cfg__card",
|
|
1111
1911
|
children: [
|
|
@@ -1113,258 +1913,133 @@ window.__ModuleLoader__.load({
|
|
|
1113
1913
|
className: "dsh-channels-cfg__cardHead",
|
|
1114
1914
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1115
1915
|
className: "dsh-channels-cfg__cardTitle",
|
|
1116
|
-
children: PANEL_COPY.
|
|
1916
|
+
children: PANEL_COPY.cleanupCard
|
|
1117
1917
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1118
1918
|
className: "dsh-channels-cfg__cardMeta",
|
|
1119
|
-
children:
|
|
1919
|
+
children: retentionLabel(panel, cleanup)
|
|
1120
1920
|
})]
|
|
1121
1921
|
}),
|
|
1122
1922
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1123
1923
|
className: "dsh-channels-cfg__help",
|
|
1124
|
-
children: PANEL_COPY.
|
|
1125
|
-
}),
|
|
1126
|
-
access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1127
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1128
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning })
|
|
1129
|
-
}) : null,
|
|
1130
|
-
access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1131
|
-
className: "dsh-channels-cfg__banner",
|
|
1132
|
-
children: PANEL_COPY.accessModeOpenWarning
|
|
1133
|
-
}) : null,
|
|
1134
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1135
|
-
className: "dsh-channels-cfg__kv",
|
|
1136
|
-
children: [
|
|
1137
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1138
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1139
|
-
children: PANEL_COPY.accessAdminLabel
|
|
1140
|
-
}),
|
|
1141
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1142
|
-
className: `dsh-channels-cfg__kvValue dsh-channels-cfg__mono${access.admin.configured ? "" : " dsh-channels-cfg__reasonBad"}`,
|
|
1143
|
-
children: access.admin.openId ?? PANEL_COPY.accessAdminMissing
|
|
1144
|
-
}),
|
|
1145
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1146
|
-
className: "dsh-channels-cfg__help",
|
|
1147
|
-
children: PANEL_COPY.accessAdminHint
|
|
1148
|
-
})
|
|
1149
|
-
]
|
|
1150
|
-
}),
|
|
1151
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1152
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1153
|
-
children: [
|
|
1154
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1155
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1156
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1157
|
-
className: "dsh-channels-cfg__label",
|
|
1158
|
-
children: PANEL_COPY.accessGroupsTitle
|
|
1159
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1160
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1161
|
-
children: [groups.length, " 个"]
|
|
1162
|
-
})]
|
|
1163
|
-
}),
|
|
1164
|
-
groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1165
|
-
className: "dsh-channels-cfg__help",
|
|
1166
|
-
children: PANEL_COPY.accessGroupsEmpty
|
|
1167
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1168
|
-
className: "dsh-channels-cfg__accessList",
|
|
1169
|
-
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1170
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1171
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1172
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1173
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: group.name ?? group.chatId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1174
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1175
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1176
|
-
className: "dsh-channels-cfg__mono",
|
|
1177
|
-
children: group.chatId
|
|
1178
|
-
}), ` · ${accessSourceLabel(group.source)} · ${formatAccessDate(group.addedAt)}`]
|
|
1179
|
-
})]
|
|
1180
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1181
|
-
type: "button",
|
|
1182
|
-
className: "dsh-channels-cfg__btn",
|
|
1183
|
-
disabled,
|
|
1184
|
-
onClick: () => onRemoveGroup(group.chatId),
|
|
1185
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1186
|
-
})]
|
|
1187
|
-
}, group.chatId))
|
|
1188
|
-
}),
|
|
1189
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1190
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1191
|
-
children: [
|
|
1192
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1193
|
-
type: "text",
|
|
1194
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1195
|
-
value: groupChatId,
|
|
1196
|
-
placeholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1197
|
-
disabled,
|
|
1198
|
-
onChange: (event) => setGroupChatId(event.target.value)
|
|
1199
|
-
}),
|
|
1200
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1201
|
-
type: "text",
|
|
1202
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1203
|
-
value: groupName,
|
|
1204
|
-
placeholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1205
|
-
disabled,
|
|
1206
|
-
onChange: (event) => setGroupName(event.target.value)
|
|
1207
|
-
}),
|
|
1208
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1209
|
-
type: "button",
|
|
1210
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1211
|
-
disabled: disabled || groupChatId.trim() === "",
|
|
1212
|
-
onClick: () => {
|
|
1213
|
-
submitGroup();
|
|
1214
|
-
},
|
|
1215
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup
|
|
1216
|
-
})
|
|
1217
|
-
]
|
|
1218
|
-
})
|
|
1219
|
-
]
|
|
1220
|
-
}),
|
|
1221
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1222
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1223
|
-
children: [
|
|
1224
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1225
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1226
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1227
|
-
className: "dsh-channels-cfg__label",
|
|
1228
|
-
children: PANEL_COPY.accessUsersTitle
|
|
1229
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1230
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1231
|
-
children: [users.length, " 个"]
|
|
1232
|
-
})]
|
|
1233
|
-
}),
|
|
1234
|
-
users.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1235
|
-
className: "dsh-channels-cfg__help",
|
|
1236
|
-
children: PANEL_COPY.accessUsersEmpty
|
|
1237
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1238
|
-
className: "dsh-channels-cfg__accessList",
|
|
1239
|
-
children: users.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1240
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1241
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1242
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1243
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.openId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1244
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1245
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
|
-
className: "dsh-channels-cfg__mono",
|
|
1247
|
-
children: user.openId
|
|
1248
|
-
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1249
|
-
})]
|
|
1250
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1251
|
-
type: "button",
|
|
1252
|
-
className: "dsh-channels-cfg__btn",
|
|
1253
|
-
disabled,
|
|
1254
|
-
onClick: () => onRemoveUser(user.openId),
|
|
1255
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1256
|
-
})]
|
|
1257
|
-
}, user.openId))
|
|
1258
|
-
}),
|
|
1259
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1260
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1261
|
-
children: [
|
|
1262
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1263
|
-
type: "text",
|
|
1264
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1265
|
-
value: userOpenId,
|
|
1266
|
-
placeholder: PANEL_COPY.accessUserOpenIdPlaceholder,
|
|
1267
|
-
disabled,
|
|
1268
|
-
onChange: (event) => setUserOpenId(event.target.value)
|
|
1269
|
-
}),
|
|
1270
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1271
|
-
type: "text",
|
|
1272
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1273
|
-
value: userName,
|
|
1274
|
-
placeholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
1275
|
-
disabled,
|
|
1276
|
-
onChange: (event) => setUserName(event.target.value)
|
|
1277
|
-
}),
|
|
1278
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1279
|
-
type: "button",
|
|
1280
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1281
|
-
disabled: disabled || userOpenId.trim() === "",
|
|
1282
|
-
onClick: () => {
|
|
1283
|
-
submitUser();
|
|
1284
|
-
},
|
|
1285
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddUser
|
|
1286
|
-
})
|
|
1287
|
-
]
|
|
1288
|
-
})
|
|
1289
|
-
]
|
|
1924
|
+
children: PANEL_COPY.cleanupCardHelp
|
|
1290
1925
|
}),
|
|
1291
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1292
|
-
className: "dsh-channels-
|
|
1293
|
-
children:
|
|
1294
|
-
className: "dsh-channels-
|
|
1926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1927
|
+
className: "dsh-channels-cfg__facts",
|
|
1928
|
+
children: facts.map((fact) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1929
|
+
className: "dsh-channels-cfg__fact",
|
|
1295
1930
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1296
|
-
className: "dsh-channels-
|
|
1297
|
-
children:
|
|
1298
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1299
|
-
className: "dsh-channels-
|
|
1300
|
-
children:
|
|
1931
|
+
className: "dsh-channels-cfg__factLabel",
|
|
1932
|
+
children: fact.label
|
|
1933
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
+
className: "dsh-channels-cfg__factValue",
|
|
1935
|
+
children: fact.value
|
|
1301
1936
|
})]
|
|
1302
|
-
}
|
|
1303
|
-
className: "dsh-channels-cfg__help",
|
|
1304
|
-
children: PANEL_COPY.accessRequestsEmpty
|
|
1305
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1306
|
-
className: "dsh-channels-cfg__accessList",
|
|
1307
|
-
children: requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1308
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1309
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1310
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1311
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1312
|
-
request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser,
|
|
1313
|
-
":",
|
|
1314
|
-
request.name ?? request.targetId
|
|
1315
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1316
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1317
|
-
children: [
|
|
1318
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1319
|
-
className: "dsh-channels-cfg__mono",
|
|
1320
|
-
children: request.targetId
|
|
1321
|
-
}),
|
|
1322
|
-
` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`,
|
|
1323
|
-
request.kind === "user" && request.requester !== void 0 ? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}` : "",
|
|
1324
|
-
request.kind === "user" && request.groupName !== void 0 ? ` · 群 ${request.groupName}` : request.kind === "user" && request.chatId !== void 0 ? ` · 群 ${request.chatId}` : ""
|
|
1325
|
-
]
|
|
1326
|
-
})]
|
|
1327
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1328
|
-
className: "dsh-channels-cfg__accessActions",
|
|
1329
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1330
|
-
type: "button",
|
|
1331
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1332
|
-
disabled,
|
|
1333
|
-
onClick: () => onResolveRequest(request.token, "allow"),
|
|
1334
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1335
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1336
|
-
type: "button",
|
|
1337
|
-
className: "dsh-channels-cfg__btn",
|
|
1338
|
-
disabled,
|
|
1339
|
-
onClick: () => onResolveRequest(request.token, "deny"),
|
|
1340
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1341
|
-
})]
|
|
1342
|
-
})]
|
|
1343
|
-
}, request.token))
|
|
1344
|
-
})]
|
|
1937
|
+
}, fact.label))
|
|
1345
1938
|
}),
|
|
1346
|
-
|
|
1347
|
-
className: "dsh-channels-
|
|
1348
|
-
children:
|
|
1939
|
+
blocked === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1940
|
+
className: "dsh-channels-cfg__help",
|
|
1941
|
+
children: blocked
|
|
1942
|
+
}),
|
|
1943
|
+
result === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1944
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${failed ? "error" : "ok"}`,
|
|
1945
|
+
children: [result.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1946
|
+
PANEL_COPY.cleanupDone,
|
|
1947
|
+
":",
|
|
1948
|
+
PANEL_COPY.cleanupDeleted,
|
|
1949
|
+
" ",
|
|
1950
|
+
result.deletedWorkspaces,
|
|
1951
|
+
" 个,",
|
|
1952
|
+
PANEL_COPY.cleanupDeletedDirs,
|
|
1953
|
+
" ",
|
|
1954
|
+
result.deletedSessionDirs,
|
|
1955
|
+
" 个,",
|
|
1956
|
+
PANEL_COPY.cleanupRetired,
|
|
1957
|
+
" ",
|
|
1958
|
+
result.retiredRoutes,
|
|
1959
|
+
" 条,",
|
|
1960
|
+
PANEL_COPY.cleanupFreed,
|
|
1961
|
+
" ",
|
|
1962
|
+
formatBytes(result.freedBytes),
|
|
1963
|
+
"。"
|
|
1964
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.cleanupFailed, result.error === void 0 ? "" : `:${result.error}`] }), result.failures.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: PANEL_COPY.cleanupFailures }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1965
|
+
className: "dsh-channels-cfg__issues",
|
|
1966
|
+
children: result.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1967
|
+
className: "dsh-channels-cfg__mono",
|
|
1968
|
+
children: failure
|
|
1969
|
+
}, `${index}-${failure}`))
|
|
1970
|
+
})] })]
|
|
1349
1971
|
}),
|
|
1350
1972
|
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1351
1973
|
className: "dsh-channels-cfg__fieldError",
|
|
1352
1974
|
children: error
|
|
1353
1975
|
}),
|
|
1354
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1355
|
-
className: "dsh-channels-
|
|
1356
|
-
children:
|
|
1976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1977
|
+
className: "dsh-channels-cfg__actions",
|
|
1978
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1979
|
+
type: "button",
|
|
1980
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1981
|
+
onClick: onRun,
|
|
1982
|
+
disabled: running || busy !== null || locked || blocked !== null,
|
|
1983
|
+
children: running ? PANEL_COPY.cleanupRunning : PANEL_COPY.cleanupNow
|
|
1984
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1985
|
+
className: "dsh-channels-cfg__help",
|
|
1986
|
+
children: PANEL_COPY.cleanupConfirmHint
|
|
1987
|
+
})]
|
|
1988
|
+
})
|
|
1989
|
+
]
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/client/panel/workspace-tab.tsx
|
|
1994
|
+
/**
|
|
1995
|
+
* 「工作区」tab:工作区根目录 / 过期保留策略 / 自动清理字段 + 目录清理卡。
|
|
1996
|
+
* 表单保存栏在字段卡正下方,清理卡(独立动作,不走 draft)在其后。
|
|
1997
|
+
*
|
|
1998
|
+
* @module dsh-channels/client/panel/workspace-tab
|
|
1999
|
+
*/
|
|
2000
|
+
function WorkspaceTab(props) {
|
|
2001
|
+
const { form, cleanup, cleanupResult, cleanupError, onRunCleanup } = props;
|
|
2002
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2003
|
+
className: "dsh-channels-cfg__tabBody",
|
|
2004
|
+
children: [
|
|
2005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.workspaceTabHelp }),
|
|
2006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
2007
|
+
form,
|
|
2008
|
+
groups: TAB_FIELD_GROUPS.workspace
|
|
1357
2009
|
}),
|
|
1358
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1359
|
-
|
|
1360
|
-
|
|
2010
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
2011
|
+
form,
|
|
2012
|
+
tab: "workspace"
|
|
2013
|
+
}),
|
|
2014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CleanupCard, {
|
|
2015
|
+
panel: form.panel,
|
|
2016
|
+
cleanup,
|
|
2017
|
+
busy: form.busy,
|
|
2018
|
+
locked: form.locked,
|
|
2019
|
+
result: cleanupResult,
|
|
2020
|
+
error: cleanupError,
|
|
2021
|
+
onRun: onRunCleanup
|
|
1361
2022
|
})
|
|
1362
2023
|
]
|
|
1363
2024
|
});
|
|
1364
2025
|
}
|
|
1365
|
-
|
|
2026
|
+
//#endregion
|
|
2027
|
+
//#region src/client/panel/index.tsx
|
|
2028
|
+
/**
|
|
2029
|
+
* 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft 与顶部状态头 /
|
|
2030
|
+
* tab 导航的外壳。
|
|
2031
|
+
*
|
|
2032
|
+
* 所有 Host 交互(panel / status / catalog / config / credentials / cleanup /
|
|
2033
|
+
* access / pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
2034
|
+
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `settings.section`
|
|
2035
|
+
* 槽位。
|
|
2036
|
+
*
|
|
2037
|
+
* @module dsh-channels/client/panel
|
|
2038
|
+
*/
|
|
2039
|
+
/** Page shell: one panel payload, one status poll, one global config draft. */
|
|
1366
2040
|
function FeishuChannelsSection(_props) {
|
|
1367
2041
|
const [load, setLoad] = (0, react.useState)({ phase: "loading" });
|
|
2042
|
+
const [activeTab, setActiveTab] = (0, react.useState)("connection");
|
|
1368
2043
|
const [draft, setDraft] = (0, react.useState)({});
|
|
1369
2044
|
const [notice, setNotice] = (0, react.useState)(null);
|
|
1370
2045
|
const [busy, setBusy] = (0, react.useState)(null);
|
|
@@ -1379,8 +2054,14 @@ window.__ModuleLoader__.load({
|
|
|
1379
2054
|
const [accessNotice, setAccessNotice] = (0, react.useState)(null);
|
|
1380
2055
|
const [accessError, setAccessError] = (0, react.useState)(null);
|
|
1381
2056
|
const catalogTicket = (0, react.useRef)(0);
|
|
2057
|
+
/** Latest ready panel, read from the poll loop without restarting it. */
|
|
2058
|
+
const panelRef = (0, react.useRef)(null);
|
|
1382
2059
|
const panel = load.phase === "ready" ? load.panel : null;
|
|
1383
|
-
|
|
2060
|
+
/** Host-side lifecycle busy: connecting/restarting blocks every panel write. */
|
|
2061
|
+
const hostBusy = (status ?? panel?.status)?.busy === true;
|
|
2062
|
+
(0, react.useEffect)(() => {
|
|
2063
|
+
panelRef.current = panel;
|
|
2064
|
+
}, [panel]);
|
|
1384
2065
|
const loadPanel = (0, react.useCallback)(async () => {
|
|
1385
2066
|
setLoad({ phase: "loading" });
|
|
1386
2067
|
setNotice(null);
|
|
@@ -1421,10 +2102,21 @@ window.__ModuleLoader__.load({
|
|
|
1421
2102
|
let cancelled = false;
|
|
1422
2103
|
const tick = async () => {
|
|
1423
2104
|
try {
|
|
2105
|
+
const previousPhase = panelRef.current?.status.phase;
|
|
1424
2106
|
const next = await fetchStatus();
|
|
1425
2107
|
if (cancelled) return;
|
|
1426
2108
|
setStatus(next);
|
|
1427
2109
|
setPollError(null);
|
|
2110
|
+
if (previousPhase !== void 0 && previousPhase !== next.phase) try {
|
|
2111
|
+
const refreshed = await fetchPanel();
|
|
2112
|
+
if (!cancelled) {
|
|
2113
|
+
setLoad({
|
|
2114
|
+
phase: "ready",
|
|
2115
|
+
panel: refreshed
|
|
2116
|
+
});
|
|
2117
|
+
setStatus(refreshed.status);
|
|
2118
|
+
}
|
|
2119
|
+
} catch {}
|
|
1428
2120
|
} catch (error) {
|
|
1429
2121
|
if (!cancelled) setPollError(messageOf(error));
|
|
1430
2122
|
}
|
|
@@ -1471,7 +2163,7 @@ window.__ModuleLoader__.load({
|
|
|
1471
2163
|
const currentValues = (0, react.useMemo)(() => {
|
|
1472
2164
|
if (panel === null) return {};
|
|
1473
2165
|
const valueOf = (key) => {
|
|
1474
|
-
const raw = draft[key] ??
|
|
2166
|
+
const raw = draft[key] ?? panel.config.effective[key] ?? panel.config.base[key];
|
|
1475
2167
|
return typeof raw === "string" && raw !== "" ? raw : void 0;
|
|
1476
2168
|
};
|
|
1477
2169
|
return {
|
|
@@ -1488,13 +2180,14 @@ window.__ModuleLoader__.load({
|
|
|
1488
2180
|
]);
|
|
1489
2181
|
/** Whether one selector currently renders as a dropdown for this form. */
|
|
1490
2182
|
const isSelectField = (0, react.useCallback)((key) => renderedFields.some((field) => field.key === key && field.kind === "select"), [renderedFields]);
|
|
2183
|
+
/** Draft one field, cascading provider → model → effort like the Host catalog. */
|
|
1491
2184
|
const updateDraft = (0, react.useCallback)((key, value) => {
|
|
1492
2185
|
if (panel === null) return;
|
|
1493
|
-
const providerNow = String(draft.provider ??
|
|
2186
|
+
const providerNow = String(draft.provider ?? panel.config.effective.provider ?? panel.config.base.provider ?? "");
|
|
1494
2187
|
setNotice(null);
|
|
1495
2188
|
if (key === "provider") {
|
|
1496
2189
|
const nextProvider = String(value ?? "");
|
|
1497
|
-
const keepModel = nextProvider === String(
|
|
2190
|
+
const keepModel = nextProvider === String(panel.config.effective.provider ?? panel.config.base.provider ?? "") ? String(panel.config.effective.model ?? panel.config.base.model ?? "") : "";
|
|
1498
2191
|
setDraft((previous) => ({
|
|
1499
2192
|
...previous,
|
|
1500
2193
|
provider: value,
|
|
@@ -1534,18 +2227,34 @@ window.__ModuleLoader__.load({
|
|
|
1534
2227
|
}
|
|
1535
2228
|
}, []);
|
|
1536
2229
|
/**
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
2230
|
+
* Manual refresh: reload the whole panel payload so access lists, the cleanup
|
|
2231
|
+
* preview and `restartPendingKeys` catch up with the Host. The dirty draft is
|
|
2232
|
+
* deliberately preserved — refreshing never discards user input.
|
|
1540
2233
|
*/
|
|
1541
|
-
const
|
|
2234
|
+
const onRefresh = (0, react.useCallback)(async () => {
|
|
2235
|
+
try {
|
|
2236
|
+
const next = await fetchPanel();
|
|
2237
|
+
setLoad({
|
|
2238
|
+
phase: "ready",
|
|
2239
|
+
panel: next
|
|
2240
|
+
});
|
|
2241
|
+
setStatus(next.status);
|
|
2242
|
+
setPollError(null);
|
|
2243
|
+
setAccessNotice(null);
|
|
2244
|
+
setAccessError(null);
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
setPollError(messageOf(error));
|
|
2247
|
+
}
|
|
2248
|
+
}, []);
|
|
2249
|
+
/** Submit every dirty field at once; the Host persists and re-validates. */
|
|
2250
|
+
const onSave = (0, react.useCallback)(async () => {
|
|
1542
2251
|
if (panel === null) return;
|
|
1543
|
-
const
|
|
1544
|
-
const { values, issues } = validateDraft(panel, draft, only);
|
|
2252
|
+
const { values, issues } = validateDraft(panel, draft);
|
|
1545
2253
|
if (issues.length > 0) {
|
|
2254
|
+
const detail = issues.map((issue) => `${fieldLabel(panel.config.fields, issue.key)} ${issue.message}`).join(";");
|
|
1546
2255
|
setNotice({
|
|
1547
2256
|
kind: "error",
|
|
1548
|
-
text:
|
|
2257
|
+
text: `${PANEL_COPY.saveInvalid}:${detail}`,
|
|
1549
2258
|
issues
|
|
1550
2259
|
});
|
|
1551
2260
|
return;
|
|
@@ -1570,17 +2279,15 @@ window.__ModuleLoader__.load({
|
|
|
1570
2279
|
setStatus(result.panel.status);
|
|
1571
2280
|
setCleanupResult(null);
|
|
1572
2281
|
setCleanupError(null);
|
|
1573
|
-
const cleared = result.clearedKeys.length === 0 ? "" : `;已清除覆盖:${result.clearedKeys.map((key) => fieldLabel(result.panel.config.fields, key)).join("、")}`;
|
|
1574
|
-
setNotice({
|
|
1575
|
-
kind: "ok",
|
|
1576
|
-
text: `${scope === "model" ? PANEL_COPY.savedModel : PANEL_COPY.saved}${cleared}`
|
|
1577
|
-
});
|
|
1578
2282
|
setDraft((previous) => {
|
|
1579
|
-
if (scope !== "model") return {};
|
|
1580
2283
|
const next = { ...previous };
|
|
1581
|
-
for (const key of
|
|
2284
|
+
for (const key of Object.keys(values)) delete next[key];
|
|
1582
2285
|
return next;
|
|
1583
2286
|
});
|
|
2287
|
+
setNotice({
|
|
2288
|
+
kind: "ok",
|
|
2289
|
+
text: PANEL_COPY.saved
|
|
2290
|
+
});
|
|
1584
2291
|
} catch (error) {
|
|
1585
2292
|
setNotice({
|
|
1586
2293
|
kind: "error",
|
|
@@ -1590,6 +2297,56 @@ window.__ModuleLoader__.load({
|
|
|
1590
2297
|
setBusy(null);
|
|
1591
2298
|
}
|
|
1592
2299
|
}, [draft, panel]);
|
|
2300
|
+
/** Drop the draft keys owned by one tab (nothing is written). */
|
|
2301
|
+
const discardTab = (0, react.useCallback)((tab) => {
|
|
2302
|
+
if (panel === null) return;
|
|
2303
|
+
const groups = new Set(TAB_FIELD_GROUPS[tab]);
|
|
2304
|
+
const keys = new Set(panel.config.fields.filter((field) => groups.has(field.group)).map((field) => field.key));
|
|
2305
|
+
setDraft((previous) => {
|
|
2306
|
+
const next = { ...previous };
|
|
2307
|
+
for (const key of keys) delete next[key];
|
|
2308
|
+
return next;
|
|
2309
|
+
});
|
|
2310
|
+
setNotice(null);
|
|
2311
|
+
if (tab === "model") {
|
|
2312
|
+
catalogTicket.current += 1;
|
|
2313
|
+
setCatalog(catalogFromFields(panel.config.fields));
|
|
2314
|
+
setCatalogBusy(false);
|
|
2315
|
+
}
|
|
2316
|
+
}, [panel]);
|
|
2317
|
+
/**
|
|
2318
|
+
* Open the host's native directory chooser (workspace root「选择目录…」) and
|
|
2319
|
+
* stage the picked absolute path as a draft value. Nothing is persisted until
|
|
2320
|
+
*「保存更改」; a canceled dialog is a no-op, not an error.
|
|
2321
|
+
*/
|
|
2322
|
+
const onPickDirectory = (0, react.useCallback)(async (key) => {
|
|
2323
|
+
setNotice(null);
|
|
2324
|
+
try {
|
|
2325
|
+
const result = await pickDirectory();
|
|
2326
|
+
if (!result.ok) {
|
|
2327
|
+
setNotice({
|
|
2328
|
+
kind: "error",
|
|
2329
|
+
text: result.error
|
|
2330
|
+
});
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
if ("canceled" in result) return;
|
|
2334
|
+
setDraft((previous) => ({
|
|
2335
|
+
...previous,
|
|
2336
|
+
[key]: result.path
|
|
2337
|
+
}));
|
|
2338
|
+
setNotice({
|
|
2339
|
+
kind: "ok",
|
|
2340
|
+
text: `${PANEL_COPY.directoryPicked}:${result.path}`
|
|
2341
|
+
});
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
setNotice({
|
|
2344
|
+
kind: "error",
|
|
2345
|
+
text: messageOf(error)
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
}, []);
|
|
2349
|
+
/** Drop every Web override and return the refreshed panel. */
|
|
1593
2350
|
const onClear = (0, react.useCallback)(async () => {
|
|
1594
2351
|
setBusy("clear");
|
|
1595
2352
|
setNotice(null);
|
|
@@ -1623,8 +2380,15 @@ window.__ModuleLoader__.load({
|
|
|
1623
2380
|
setBusy(null);
|
|
1624
2381
|
}
|
|
1625
2382
|
}, []);
|
|
2383
|
+
const running = (status ?? panel?.status)?.running === true;
|
|
2384
|
+
/**
|
|
2385
|
+
* The single connect/reconnect action: open a stopped channel or restart a
|
|
2386
|
+
* running one. Only a restart interrupts live replies, so only it confirms.
|
|
2387
|
+
*/
|
|
1626
2388
|
const onReconnect = (0, react.useCallback)(async () => {
|
|
1627
|
-
if (
|
|
2389
|
+
if (running) {
|
|
2390
|
+
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.restartWarning) : true)) return;
|
|
2391
|
+
}
|
|
1628
2392
|
setBusy("reconnect");
|
|
1629
2393
|
setNotice(null);
|
|
1630
2394
|
try {
|
|
@@ -1634,6 +2398,7 @@ window.__ModuleLoader__.load({
|
|
|
1634
2398
|
kind: "error",
|
|
1635
2399
|
text: result.error
|
|
1636
2400
|
});
|
|
2401
|
+
if (result.unconfigured === true) setActiveTab("connection");
|
|
1637
2402
|
await refreshStatus();
|
|
1638
2403
|
return;
|
|
1639
2404
|
}
|
|
@@ -1644,7 +2409,7 @@ window.__ModuleLoader__.load({
|
|
|
1644
2409
|
setStatus(result.panel.status);
|
|
1645
2410
|
setNotice({
|
|
1646
2411
|
kind: "ok",
|
|
1647
|
-
text:
|
|
2412
|
+
text: running ? PANEL_COPY.reconnectDone : PANEL_COPY.connectDone
|
|
1648
2413
|
});
|
|
1649
2414
|
} catch (error) {
|
|
1650
2415
|
setNotice({
|
|
@@ -1655,7 +2420,7 @@ window.__ModuleLoader__.load({
|
|
|
1655
2420
|
} finally {
|
|
1656
2421
|
setBusy(null);
|
|
1657
2422
|
}
|
|
1658
|
-
}, [refreshStatus]);
|
|
2423
|
+
}, [refreshStatus, running]);
|
|
1659
2424
|
/**
|
|
1660
2425
|
* Delete stale workspaces after an explicit confirmation. The Host refreshes
|
|
1661
2426
|
* the panel in the same response, so the preview counts update immediately;
|
|
@@ -1664,7 +2429,7 @@ window.__ModuleLoader__.load({
|
|
|
1664
2429
|
const onRunCleanup = (0, react.useCallback)(async () => {
|
|
1665
2430
|
if (panel === null) return;
|
|
1666
2431
|
const { cleanup } = panel;
|
|
1667
|
-
if (!(typeof window.confirm === "function" ? window.confirm(`${PANEL_COPY.cleanupConfirm}\n
|
|
2432
|
+
if (!(typeof window.confirm === "function" ? window.confirm(`${PANEL_COPY.cleanupConfirm}\n${PANEL_COPY.cleanupCandidates}:${cleanup.candidates} · ${PANEL_COPY.cleanupCandidateBytes}:${formatBytes(cleanup.candidateBytes)}`) : true)) return;
|
|
1668
2433
|
setBusy("cleanup");
|
|
1669
2434
|
setNotice(null);
|
|
1670
2435
|
setCleanupResult(null);
|
|
@@ -1791,6 +2556,7 @@ window.__ModuleLoader__.load({
|
|
|
1791
2556
|
setBusy(null);
|
|
1792
2557
|
}
|
|
1793
2558
|
}, [applyAccess]);
|
|
2559
|
+
/** Save provided credential values; the Host reconnects automatically when ready. */
|
|
1794
2560
|
const onSaveCredentials = (0, react.useCallback)(async () => {
|
|
1795
2561
|
const values = {};
|
|
1796
2562
|
if (appIdInput.trim() !== "") values.appId = appIdInput;
|
|
@@ -1815,10 +2581,10 @@ window.__ModuleLoader__.load({
|
|
|
1815
2581
|
setStatus(result.panel.status);
|
|
1816
2582
|
setAppIdInput("");
|
|
1817
2583
|
setAppSecretInput("");
|
|
1818
|
-
const base = result.reconnect === "pending" ? PANEL_COPY.credentialSavedPending : result.reconnect === "auto" ? PANEL_COPY.credentialSavedReconnected : PANEL_COPY.credentialSaved;
|
|
2584
|
+
const base = result.reconnect === "pending" ? PANEL_COPY.credentialSavedPending : result.reconnect === "auto" ? PANEL_COPY.credentialSavedReconnected : result.reconnect === "unconfigured" ? PANEL_COPY.credentialSavedUnconfigured : result.reconnect === "failed" ? PANEL_COPY.credentialAutoConnectFailed : PANEL_COPY.credentialSaved;
|
|
1819
2585
|
setNotice({
|
|
1820
|
-
kind: result.
|
|
1821
|
-
text: result.autoReconnectError === void 0 ? base : `${base}
|
|
2586
|
+
kind: result.reconnect === "failed" || result.autoReconnectError !== void 0 ? "error" : "ok",
|
|
2587
|
+
text: result.autoReconnectError === void 0 ? base : `${base}:${result.autoReconnectError}`
|
|
1822
2588
|
});
|
|
1823
2589
|
} catch (error) {
|
|
1824
2590
|
setNotice({
|
|
@@ -1830,8 +2596,7 @@ window.__ModuleLoader__.load({
|
|
|
1830
2596
|
}
|
|
1831
2597
|
}, [appIdInput, appSecretInput]);
|
|
1832
2598
|
const onClearCredential = (0, react.useCallback)(async (field) => {
|
|
1833
|
-
|
|
1834
|
-
if (!(typeof window.confirm === "function" ? window.confirm(`确定移除已存储的 ${label}?`) : true)) return;
|
|
2599
|
+
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.credentialRemoveConfirm) : true)) return;
|
|
1835
2600
|
setBusy("credentials-clear");
|
|
1836
2601
|
setNotice(null);
|
|
1837
2602
|
try {
|
|
@@ -1862,229 +2627,279 @@ window.__ModuleLoader__.load({
|
|
|
1862
2627
|
setBusy(null);
|
|
1863
2628
|
}
|
|
1864
2629
|
}, []);
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2630
|
+
const issues = (0, react.useMemo)(() => notice?.kind === "error" ? notice.issues ?? [] : [], [notice]);
|
|
2631
|
+
const dirtyCount = Object.keys(draft).length;
|
|
2632
|
+
/** Which tabs currently own a dirty key (the dot is per tab, not global). */
|
|
2633
|
+
const dirtyTabs = (0, react.useMemo)(() => {
|
|
2634
|
+
const tabs = /* @__PURE__ */ new Set();
|
|
2635
|
+
if (panel === null || dirtyCount === 0) return tabs;
|
|
2636
|
+
const groupOf = new Map(panel.config.fields.map((field) => [field.key, field.group]));
|
|
2637
|
+
for (const key of Object.keys(draft)) {
|
|
2638
|
+
const group = groupOf.get(key);
|
|
2639
|
+
if (group === void 0) continue;
|
|
2640
|
+
for (const tab of PANEL_TABS) if (TAB_FIELD_GROUPS[tab.id].includes(group)) tabs.add(tab.id);
|
|
2641
|
+
}
|
|
2642
|
+
return tabs;
|
|
2643
|
+
}, [
|
|
2644
|
+
dirtyCount,
|
|
2645
|
+
draft,
|
|
2646
|
+
panel
|
|
2647
|
+
]);
|
|
2648
|
+
const form = (0, react.useMemo)(() => {
|
|
2649
|
+
if (panel === null) return null;
|
|
2650
|
+
return {
|
|
2651
|
+
panel,
|
|
2652
|
+
fields: renderedFields,
|
|
2653
|
+
draft,
|
|
2654
|
+
issues,
|
|
2655
|
+
busy,
|
|
2656
|
+
locked: hostBusy,
|
|
2657
|
+
dirtyCount,
|
|
2658
|
+
onChange: updateDraft,
|
|
2659
|
+
onSave: () => {
|
|
2660
|
+
onSave();
|
|
2661
|
+
},
|
|
2662
|
+
onDiscardTab: discardTab,
|
|
2663
|
+
onPickDirectory
|
|
2664
|
+
};
|
|
2665
|
+
}, [
|
|
2666
|
+
busy,
|
|
2667
|
+
discardTab,
|
|
2668
|
+
dirtyCount,
|
|
2669
|
+
draft,
|
|
2670
|
+
hostBusy,
|
|
2671
|
+
issues,
|
|
2672
|
+
onPickDirectory,
|
|
2673
|
+
onSave,
|
|
2674
|
+
panel,
|
|
2675
|
+
renderedFields,
|
|
2676
|
+
updateDraft
|
|
2677
|
+
]);
|
|
2678
|
+
const credentials = (0, react.useMemo)(() => ({
|
|
2679
|
+
appId: appIdInput,
|
|
2680
|
+
appSecret: appSecretInput,
|
|
2681
|
+
issues,
|
|
2682
|
+
busy,
|
|
2683
|
+
locked: hostBusy,
|
|
2684
|
+
onAppIdChange: setAppIdInput,
|
|
2685
|
+
onAppSecretChange: setAppSecretInput,
|
|
2686
|
+
onSave: () => {
|
|
2687
|
+
onSaveCredentials();
|
|
2688
|
+
},
|
|
2689
|
+
onClear: (field) => {
|
|
2690
|
+
onClearCredential(field);
|
|
2691
|
+
}
|
|
2692
|
+
}), [
|
|
2693
|
+
appIdInput,
|
|
2694
|
+
appSecretInput,
|
|
2695
|
+
busy,
|
|
2696
|
+
hostBusy,
|
|
2697
|
+
issues,
|
|
2698
|
+
onClearCredential,
|
|
2699
|
+
onSaveCredentials
|
|
2700
|
+
]);
|
|
1878
2701
|
if (load.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1879
|
-
className: "dsh-channels-
|
|
1880
|
-
children:
|
|
2702
|
+
className: "dsh-channels-cfg",
|
|
2703
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2704
|
+
className: "dsh-channels-cfg__loading",
|
|
2705
|
+
children: PANEL_COPY.loading
|
|
2706
|
+
})
|
|
1881
2707
|
});
|
|
1882
|
-
if (load.phase === "error") return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1883
|
-
className: "dsh-channels-
|
|
1884
|
-
children:
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2708
|
+
if (load.phase === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2709
|
+
className: "dsh-channels-cfg",
|
|
2710
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2711
|
+
className: "dsh-channels-cfg__error",
|
|
2712
|
+
children: [
|
|
2713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.loadFailed }),
|
|
2714
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: load.message }),
|
|
2715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2716
|
+
type: "button",
|
|
2717
|
+
className: "dsh-channels-cfg__btn",
|
|
2718
|
+
onClick: () => {
|
|
2719
|
+
loadPanel();
|
|
2720
|
+
},
|
|
2721
|
+
children: PANEL_COPY.retry
|
|
2722
|
+
}) })
|
|
2723
|
+
]
|
|
2724
|
+
})
|
|
1896
2725
|
});
|
|
2726
|
+
if (form === null) return null;
|
|
2727
|
+
const current = status ?? load.panel.status;
|
|
1897
2728
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1898
2729
|
className: "dsh-channels-cfg",
|
|
1899
2730
|
children: [
|
|
1900
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1901
|
-
|
|
1902
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1903
|
-
className: "dsh-channels-cfg__title",
|
|
1904
|
-
children: PANEL_COPY.title
|
|
1905
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1906
|
-
className: "dsh-channels-cfg__desc",
|
|
1907
|
-
children: PANEL_COPY.description
|
|
1908
|
-
})]
|
|
1909
|
-
}),
|
|
1910
|
-
currentStatus === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusCard, {
|
|
1911
|
-
status: currentStatus,
|
|
2731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelHeader, {
|
|
2732
|
+
status: current,
|
|
1912
2733
|
pollError,
|
|
1913
2734
|
busy,
|
|
2735
|
+
locked: hostBusy,
|
|
1914
2736
|
onRefresh: () => {
|
|
1915
|
-
|
|
1916
|
-
},
|
|
1917
|
-
onReconnect: () => {
|
|
1918
|
-
onReconnect();
|
|
2737
|
+
onRefresh();
|
|
1919
2738
|
}
|
|
1920
2739
|
}),
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
panel === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsCard, {
|
|
1926
|
-
panel,
|
|
1927
|
-
appId: appIdInput,
|
|
1928
|
-
appSecret: appSecretInput,
|
|
1929
|
-
issues,
|
|
1930
|
-
busy,
|
|
1931
|
-
onAppIdChange: setAppIdInput,
|
|
1932
|
-
onAppSecretChange: setAppSecretInput,
|
|
1933
|
-
onSave: () => {
|
|
1934
|
-
onSaveCredentials();
|
|
1935
|
-
},
|
|
1936
|
-
onClear: (field) => {
|
|
1937
|
-
onClearCredential(field);
|
|
1938
|
-
}
|
|
2740
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabNav, {
|
|
2741
|
+
activeTab,
|
|
2742
|
+
dirtyTabs,
|
|
2743
|
+
onChange: setActiveTab
|
|
1939
2744
|
}),
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2745
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, { notice }),
|
|
2746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2747
|
+
role: "tabpanel",
|
|
2748
|
+
id: `dsh-channels-tabpanel-${activeTab}`,
|
|
2749
|
+
"aria-labelledby": `dsh-channels-tab-${activeTab}`,
|
|
2750
|
+
children: [
|
|
2751
|
+
activeTab === "connection" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConnectionTab, {
|
|
2752
|
+
panel: form.panel,
|
|
2753
|
+
status: current,
|
|
2754
|
+
busy,
|
|
2755
|
+
credentials,
|
|
2756
|
+
onReconnect: () => {
|
|
1947
2757
|
onReconnect();
|
|
2758
|
+
}
|
|
2759
|
+
}) : null,
|
|
2760
|
+
activeTab === "sessions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionsTab, { form }) : null,
|
|
2761
|
+
activeTab === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelTab, {
|
|
2762
|
+
form,
|
|
2763
|
+
catalogBusy
|
|
2764
|
+
}) : null,
|
|
2765
|
+
activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, {
|
|
2766
|
+
form,
|
|
2767
|
+
cleanup: form.panel.cleanup,
|
|
2768
|
+
cleanupResult,
|
|
2769
|
+
cleanupError,
|
|
2770
|
+
onRunCleanup: () => {
|
|
2771
|
+
onRunCleanup();
|
|
2772
|
+
}
|
|
2773
|
+
}) : null,
|
|
2774
|
+
activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
|
|
2775
|
+
form,
|
|
2776
|
+
accessNotice,
|
|
2777
|
+
accessError,
|
|
2778
|
+
onAddGroup: onAddAccessGroup,
|
|
2779
|
+
onRemoveGroup: (chatId) => {
|
|
2780
|
+
onRemoveAccessGroup(chatId);
|
|
1948
2781
|
},
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
}), configEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2012
|
-
className: "dsh-channels-cfg__cardHead",
|
|
2013
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2014
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
2015
|
-
children: entry.label
|
|
2016
|
-
})
|
|
2017
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2018
|
-
className: "dsh-channels-cfg__grid",
|
|
2019
|
-
children: entry.fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
|
|
2020
|
-
field,
|
|
2021
|
-
value: draft[field.key] ?? effectiveValue(panel, field.key),
|
|
2022
|
-
issues,
|
|
2023
|
-
onChange: updateDraft
|
|
2024
|
-
}, field.key))
|
|
2025
|
-
})] }, entry.group))]
|
|
2026
|
-
}),
|
|
2027
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2028
|
-
className: "dsh-channels-cfg__footer",
|
|
2029
|
-
children: [
|
|
2030
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2031
|
-
type: "button",
|
|
2032
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2033
|
-
onClick: () => {
|
|
2034
|
-
onSave();
|
|
2035
|
-
},
|
|
2036
|
-
disabled: !dirty || busy !== null,
|
|
2037
|
-
children: busy === "save" ? PANEL_COPY.saving : PANEL_COPY.saveAll
|
|
2038
|
-
}),
|
|
2039
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2040
|
-
type: "button",
|
|
2041
|
-
className: "dsh-channels-cfg__btn",
|
|
2042
|
-
onClick: () => {
|
|
2043
|
-
onClear();
|
|
2044
|
-
},
|
|
2045
|
-
disabled: panel.config.overriddenKeys.length === 0 || busy !== null,
|
|
2046
|
-
title: panel.config.overriddenKeys.length === 0 ? PANEL_COPY.noOverrides : void 0,
|
|
2047
|
-
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
2048
|
-
}),
|
|
2049
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
2050
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2051
|
-
className: "dsh-channels-cfg__help",
|
|
2052
|
-
children: dirty ? "有未保存的修改" : "只保存改动过的字段"
|
|
2053
|
-
})
|
|
2054
|
-
]
|
|
2055
|
-
}),
|
|
2056
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CleanupCard, {
|
|
2057
|
-
panel,
|
|
2058
|
-
cleanup: panel.cleanup,
|
|
2059
|
-
busy,
|
|
2060
|
-
result: cleanupResult,
|
|
2061
|
-
error: cleanupError,
|
|
2062
|
-
onRun: () => {
|
|
2063
|
-
onRunCleanup();
|
|
2064
|
-
}
|
|
2065
|
-
}),
|
|
2066
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessCard, {
|
|
2067
|
-
panel,
|
|
2068
|
-
busy,
|
|
2069
|
-
notice: accessNotice,
|
|
2070
|
-
error: accessError,
|
|
2071
|
-
onAddGroup: onAddAccessGroup,
|
|
2072
|
-
onRemoveGroup: (chatId) => {
|
|
2073
|
-
onRemoveAccessGroup(chatId);
|
|
2074
|
-
},
|
|
2075
|
-
onAddUser: onAddAccessUser,
|
|
2076
|
-
onRemoveUser: (openId) => {
|
|
2077
|
-
onRemoveAccessUser(openId);
|
|
2078
|
-
},
|
|
2079
|
-
onResolveRequest: (token, decision) => {
|
|
2080
|
-
onResolveAccessRequest(token, decision);
|
|
2081
|
-
}
|
|
2082
|
-
}),
|
|
2083
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedCard, { panel })
|
|
2084
|
-
] })
|
|
2782
|
+
onAddUser: onAddAccessUser,
|
|
2783
|
+
onRemoveUser: (openId) => {
|
|
2784
|
+
onRemoveAccessUser(openId);
|
|
2785
|
+
},
|
|
2786
|
+
onResolveRequest: (token, decision) => {
|
|
2787
|
+
onResolveAccessRequest(token, decision);
|
|
2788
|
+
}
|
|
2789
|
+
}) : null,
|
|
2790
|
+
activeTab === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedTab, {
|
|
2791
|
+
form,
|
|
2792
|
+
onClear: () => {
|
|
2793
|
+
onClear();
|
|
2794
|
+
}
|
|
2795
|
+
}) : null
|
|
2796
|
+
]
|
|
2797
|
+
})
|
|
2798
|
+
]
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
/** Page header: title + phase badge + refresh, visible on every tab. */
|
|
2802
|
+
function PanelHeader(props) {
|
|
2803
|
+
const { status, pollError, busy, locked, onRefresh } = props;
|
|
2804
|
+
const badge = phaseBadge(status.phase);
|
|
2805
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2806
|
+
className: "dsh-channels-cfg__panelHead",
|
|
2807
|
+
title: PANEL_COPY.description,
|
|
2808
|
+
children: [
|
|
2809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2810
|
+
className: "dsh-channels-cfg__headRow",
|
|
2811
|
+
children: [
|
|
2812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2813
|
+
className: "dsh-channels-cfg__title",
|
|
2814
|
+
children: PANEL_COPY.title
|
|
2815
|
+
}),
|
|
2816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2817
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${badge.tone}`,
|
|
2818
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__dot" }), badge.text]
|
|
2819
|
+
}),
|
|
2820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
2821
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2822
|
+
type: "button",
|
|
2823
|
+
className: "dsh-channels-cfg__btn",
|
|
2824
|
+
onClick: onRefresh,
|
|
2825
|
+
disabled: busy !== null || locked,
|
|
2826
|
+
children: PANEL_COPY.refresh
|
|
2827
|
+
})
|
|
2828
|
+
]
|
|
2829
|
+
}),
|
|
2830
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2831
|
+
className: "dsh-channels-cfg__summary",
|
|
2832
|
+
children: phaseSummary(status.phase)
|
|
2833
|
+
}),
|
|
2834
|
+
pollError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2835
|
+
className: "dsh-channels-cfg__fieldError",
|
|
2836
|
+
role: "status",
|
|
2837
|
+
"aria-live": "polite",
|
|
2838
|
+
children: [
|
|
2839
|
+
PANEL_COPY.pollFailed,
|
|
2840
|
+
":",
|
|
2841
|
+
pollError
|
|
2842
|
+
]
|
|
2843
|
+
})
|
|
2085
2844
|
]
|
|
2086
2845
|
});
|
|
2087
2846
|
}
|
|
2847
|
+
/** Top-level tab navigation; the dirty dot keeps unsaved work visible per tab. */
|
|
2848
|
+
function TabNav(props) {
|
|
2849
|
+
const { activeTab, dirtyTabs, onChange } = props;
|
|
2850
|
+
const refs = (0, react.useRef)([]);
|
|
2851
|
+
/** Roving-focus keyboard navigation between tabs (WAI-ARIA tabs pattern). */
|
|
2852
|
+
const onKeyDown = (index, event) => {
|
|
2853
|
+
const last = PANEL_TABS.length - 1;
|
|
2854
|
+
let next;
|
|
2855
|
+
switch (event.key) {
|
|
2856
|
+
case "ArrowRight":
|
|
2857
|
+
next = index === last ? 0 : index + 1;
|
|
2858
|
+
break;
|
|
2859
|
+
case "ArrowLeft":
|
|
2860
|
+
next = index === 0 ? last : index - 1;
|
|
2861
|
+
break;
|
|
2862
|
+
case "Home":
|
|
2863
|
+
next = 0;
|
|
2864
|
+
break;
|
|
2865
|
+
case "End":
|
|
2866
|
+
next = last;
|
|
2867
|
+
break;
|
|
2868
|
+
default: return;
|
|
2869
|
+
}
|
|
2870
|
+
event.preventDefault();
|
|
2871
|
+
onChange(PANEL_TABS[next].id);
|
|
2872
|
+
refs.current[next]?.focus();
|
|
2873
|
+
};
|
|
2874
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
2875
|
+
className: "dsh-channels-cfg__tabs",
|
|
2876
|
+
role: "tablist",
|
|
2877
|
+
"aria-label": PANEL_COPY.title,
|
|
2878
|
+
children: PANEL_TABS.map((tab, index) => {
|
|
2879
|
+
const selected = tab.id === activeTab;
|
|
2880
|
+
const dirty = dirtyTabs.has(tab.id);
|
|
2881
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2882
|
+
ref: (node) => {
|
|
2883
|
+
refs.current[index] = node;
|
|
2884
|
+
},
|
|
2885
|
+
type: "button",
|
|
2886
|
+
role: "tab",
|
|
2887
|
+
id: `dsh-channels-tab-${tab.id}`,
|
|
2888
|
+
"aria-selected": selected,
|
|
2889
|
+
"aria-controls": selected ? `dsh-channels-tabpanel-${tab.id}` : void 0,
|
|
2890
|
+
tabIndex: selected ? 0 : -1,
|
|
2891
|
+
className: "dsh-channels-cfg__tab",
|
|
2892
|
+
onClick: () => onChange(tab.id),
|
|
2893
|
+
onKeyDown: (event) => onKeyDown(index, event),
|
|
2894
|
+
children: [tab.label, dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2895
|
+
className: "dsh-channels-cfg__tabDot",
|
|
2896
|
+
title: PANEL_COPY.dirtyCount,
|
|
2897
|
+
"aria-label": PANEL_COPY.dirtyCount
|
|
2898
|
+
}) : null]
|
|
2899
|
+
}, tab.id);
|
|
2900
|
+
})
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2088
2903
|
//#endregion
|
|
2089
2904
|
//#region src/client/styles.ts
|
|
2090
2905
|
/**
|
|
@@ -2097,14 +2912,23 @@ window.__ModuleLoader__.load({
|
|
|
2097
2912
|
const STYLE_ATTRIBUTE = "data-dsh-channels-css";
|
|
2098
2913
|
const CSS = `
|
|
2099
2914
|
.dsh-channels-cfg{container-type:inline-size;display:flex;flex-direction:column;gap:12px;width:100%;max-width:720px;color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:20px}
|
|
2100
|
-
.dsh-channels-
|
|
2915
|
+
.dsh-channels-cfg__panelHead{position:sticky;top:0;z-index:2;display:flex;flex-direction:column;gap:4px;padding:10px 0 8px;background:var(--dsw-alias-bg-base,#fff);border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12))}
|
|
2916
|
+
.dsh-channels-cfg__headRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2101
2917
|
.dsh-channels-cfg__title{margin:0;font-size:16px;font-weight:500;line-height:24px}
|
|
2102
|
-
.dsh-channels-
|
|
2918
|
+
.dsh-channels-cfg__summary{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
2919
|
+
.dsh-channels-cfg__tabs{display:flex;align-items:center;gap:2px;flex-wrap:wrap;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12))}
|
|
2920
|
+
.dsh-channels-cfg__tab{display:inline-flex;align-items:center;gap:5px;min-height:32px;padding:0 10px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary,#5c6169);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
2921
|
+
.dsh-channels-cfg__tab:hover{color:var(--dsw-alias-label-primary,#1f2328)}
|
|
2922
|
+
.dsh-channels-cfg__tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:-2px}
|
|
2923
|
+
.dsh-channels-cfg__tab[aria-selected="true"]{color:var(--dsw-alias-state-business-primary,#0a67d3);border-bottom-color:currentColor;font-weight:500}
|
|
2924
|
+
.dsh-channels-cfg__tabDot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-warning-primary,#a15c00);flex:none}
|
|
2925
|
+
.dsh-channels-cfg__tabBody{display:flex;flex-direction:column;gap:12px}
|
|
2926
|
+
.dsh-channels-cfg__tabHelp{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
2103
2927
|
.dsh-channels-cfg__card{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:12px}
|
|
2104
2928
|
.dsh-channels-cfg__cardHead{display:flex;align-items:center;gap:8px;min-height:20px;flex-wrap:wrap}
|
|
2105
2929
|
.dsh-channels-cfg__cardTitle{font-size:13px;font-weight:500}
|
|
2106
2930
|
.dsh-channels-cfg__cardMeta{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
2107
|
-
.dsh-channels-
|
|
2931
|
+
.dsh-channels-cfg__group{display:flex;flex-direction:column;gap:8px}
|
|
2108
2932
|
.dsh-channels-cfg__badge{display:inline-flex;align-items:center;gap:5px;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));font-size:12px;line-height:18px;white-space:nowrap}
|
|
2109
2933
|
.dsh-channels-cfg__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
|
|
2110
2934
|
.dsh-channels-cfg__badge--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);border-color:currentColor}
|
|
@@ -2129,29 +2953,37 @@ window.__ModuleLoader__.load({
|
|
|
2129
2953
|
.dsh-channels-cfg__labelRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
2130
2954
|
.dsh-channels-cfg__label{font-size:12px;color:var(--dsw-alias-label-secondary,#5c6169)}
|
|
2131
2955
|
.dsh-channels-cfg__tag{display:inline-flex;align-items:center;padding:0 5px;border-radius:4px;border:1px dashed currentColor;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a8f98);white-space:nowrap}
|
|
2132
|
-
.dsh-channels-cfg__tag--override{color:var(--dsw-alias-state-business-primary,#0a67d3);border-style:solid}
|
|
2133
2956
|
.dsh-channels-cfg__tag--restart{color:var(--dsw-alias-state-warning-primary,#a15c00)}
|
|
2957
|
+
.dsh-channels-cfg__tag--dirty{color:var(--dsw-alias-state-warning-primary,#a15c00);border-style:solid}
|
|
2134
2958
|
.dsh-channels-cfg__help{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px}
|
|
2135
2959
|
.dsh-channels-cfg__reasonBad{color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2136
2960
|
.dsh-channels-cfg__input{box-sizing:border-box;width:100%;min-height:28px;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px;background:var(--dsw-alias-bg-layer-1,transparent);color:inherit;font:inherit;font-size:13px;outline:none}
|
|
2961
|
+
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
2962
|
+
.dsh-channels-cfg__inputRow .dsh-channels-cfg__input{flex:1 1 auto;width:auto;min-width:0}
|
|
2963
|
+
.dsh-channels-cfg__btn--browse{flex:none}
|
|
2137
2964
|
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
2138
2965
|
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2139
2966
|
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
2140
2967
|
.dsh-channels-cfg__check{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;font-size:13px}
|
|
2141
2968
|
.dsh-channels-cfg__checkbox{width:15px;height:15px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0a67d3);cursor:pointer}
|
|
2142
2969
|
.dsh-channels-cfg__fieldError{color:var(--dsw-alias-state-danger-primary,#c62828);font-size:11px;line-height:16px}
|
|
2143
|
-
.dsh-channels-cfg__notice{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:18px}
|
|
2970
|
+
.dsh-channels-cfg__notice{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
2144
2971
|
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
2145
2972
|
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
2146
2973
|
.dsh-channels-cfg__banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:8px;border:1px dashed var(--dsw-alias-state-warning-primary,#a15c00);color:var(--dsw-alias-state-warning-primary,#a15c00);font-size:12px;line-height:18px}
|
|
2147
2974
|
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
2148
2975
|
.dsh-channels-cfg__spacer{flex:1}
|
|
2976
|
+
.dsh-channels-cfg__steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
2977
|
+
.dsh-channels-cfg__step{display:flex;align-items:flex-start;gap:10px}
|
|
2978
|
+
.dsh-channels-cfg__stepIndex{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;border-radius:50%;border:1px solid currentColor;color:var(--dsw-alias-state-business-primary,#0a67d3);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}
|
|
2979
|
+
.dsh-channels-cfg__stepBody{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
2980
|
+
.dsh-channels-cfg__stepTitle{font-size:13px;font-weight:500}
|
|
2981
|
+
.dsh-channels-cfg__emptyState{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px dashed var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
2149
2982
|
.dsh-channels-cfg__readOnly{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 20px}
|
|
2150
2983
|
.dsh-channels-cfg__kv{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
2151
2984
|
.dsh-channels-cfg__kvKey{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
2152
2985
|
.dsh-channels-cfg__kvValue{overflow-wrap:anywhere}
|
|
2153
2986
|
.dsh-channels-cfg__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
2154
|
-
.dsh-channels-cfg__creds{display:flex;flex-direction:column;gap:6px}
|
|
2155
2987
|
.dsh-channels-cfg__cred{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2156
2988
|
.dsh-channels-cfg__cred .dsh-channels-cfg__input{flex:1 1 240px;min-width:0;width:auto}
|
|
2157
2989
|
.dsh-channels-cfg__accessSection{display:flex;flex-direction:column;gap:6px}
|
|
@@ -2168,6 +3000,7 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
|
2168
3000
|
@container (max-width:560px){
|
|
2169
3001
|
.dsh-channels-cfg__facts{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
2170
3002
|
.dsh-channels-cfg__grid,.dsh-channels-cfg__readOnly{grid-template-columns:minmax(0,1fr)}
|
|
3003
|
+
.dsh-channels-cfg__tabs{flex-wrap:nowrap;overflow-x:auto}
|
|
2171
3004
|
}
|
|
2172
3005
|
`;
|
|
2173
3006
|
/** Append the panel stylesheet once (idempotent across HMR / remounts). */
|