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