@modesto_ma/dsh-channels 0.5.0-beta.3 → 0.5.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -27
- package/docs/attachment-pipeline-v2.md +1 -1
- package/docs/feishu-onboarding.md +3 -3
- package/lib/client.js +1427 -1338
- package/lib/client.js.map +1 -1
- package/lib/index.js +7958 -6710
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/controller.ts +32 -1
- package/src/analyze/contract.ts +121 -0
- package/src/analyze/launcher.ts +353 -0
- package/src/analyze/server.ts +201 -0
- package/src/cards/analyze.ts +116 -0
- package/src/cards/index.ts +1 -0
- package/src/cards/streaming.ts +58 -0
- package/src/client/api.ts +15 -0
- package/src/client/number-field.ts +97 -0
- package/src/client/panel/access-tab.tsx +61 -20
- package/src/client/panel/advanced-tab.tsx +59 -68
- package/src/client/panel/connection-tab.tsx +147 -164
- package/src/client/panel/credentials-card.tsx +124 -87
- package/src/client/panel/fields-tab.tsx +27 -0
- package/src/client/panel/index.tsx +126 -55
- package/src/client/panel/shared.tsx +369 -252
- package/src/client/panel/whitelist.tsx +197 -0
- package/src/client/panel/wiki-tab.tsx +48 -0
- package/src/client/styles.ts +108 -91
- package/src/config.ts +30 -4
- package/src/domain.ts +55 -0
- package/src/feishu/scopes.ts +13 -7
- package/src/feishu/tools.ts +3 -1
- package/src/health.ts +12 -0
- package/src/host/agent.ts +5 -1
- package/src/index.ts +2 -2
- package/src/interactions/pending.ts +11 -2
- package/src/routing/inbound.ts +57 -14
- package/src/service.ts +161 -28
- package/src/settings/contract.ts +222 -146
- package/src/settings/routes.ts +66 -14
- package/src/settings/surface.ts +234 -203
- package/src/streamer.ts +156 -7
- package/src/workspaces.ts +31 -0
- package/src/client/panel/access-card.tsx +0 -303
- package/src/client/panel/model-tab.tsx +0 -23
- package/src/client/panel/sessions-tab.tsx +0 -21
- package/src/client/panel/workspace-tab.tsx +0 -24
package/lib/client.js
CHANGED
|
@@ -11,7 +11,7 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
* Web 设置面板的线上契约:Host 半侧的路由/面板数据与 Browser 半侧的渲染共用。
|
|
12
12
|
*
|
|
13
13
|
* 本模块**没有运行时依赖**:Host 与 Client bundle 都直接内联它。因此这里只放
|
|
14
|
-
*
|
|
14
|
+
* 常量、字段清单、可序列化的 wire 类型与无依赖的纯文案函数,不 import zod / node / DSH 内部包。
|
|
15
15
|
*
|
|
16
16
|
* @module dsh-channels/settings/contract
|
|
17
17
|
*/
|
|
@@ -32,6 +32,8 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
const ACCESS_GROUPS_ROUTE = "/dsh-channels/access/groups";
|
|
33
33
|
/** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
|
|
34
34
|
const ACCESS_USERS_ROUTE = "/dsh-channels/access/users";
|
|
35
|
+
/** 知识库白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
|
|
36
|
+
const ACCESS_WIKI_USERS_ROUTE = "/dsh-channels/access/wiki-users";
|
|
35
37
|
/** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
|
|
36
38
|
const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
|
|
37
39
|
/**
|
|
@@ -54,108 +56,114 @@ window.__ModuleLoader__.load({
|
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
58
|
id: "sessions",
|
|
57
|
-
label: "
|
|
59
|
+
label: "会话"
|
|
58
60
|
},
|
|
59
61
|
{
|
|
60
62
|
id: "model",
|
|
61
63
|
label: "模型"
|
|
62
64
|
},
|
|
63
65
|
{
|
|
64
|
-
id: "
|
|
65
|
-
label: "
|
|
66
|
+
id: "attachments",
|
|
67
|
+
label: "附件与工具"
|
|
66
68
|
},
|
|
67
69
|
{
|
|
68
70
|
id: "access",
|
|
69
71
|
label: "访问授权"
|
|
70
72
|
},
|
|
73
|
+
{
|
|
74
|
+
id: "wiki",
|
|
75
|
+
label: "知识库"
|
|
76
|
+
},
|
|
71
77
|
{
|
|
72
78
|
id: "advanced",
|
|
73
79
|
label: "高级"
|
|
74
80
|
}
|
|
75
81
|
];
|
|
76
|
-
/** Which editable field groups each tab renders
|
|
82
|
+
/** Which editable field groups each tab renders, in section order. */
|
|
77
83
|
const TAB_FIELD_GROUPS = {
|
|
78
84
|
connection: [],
|
|
79
85
|
sessions: [
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
86
|
+
"trigger",
|
|
87
|
+
"workspace",
|
|
88
|
+
"cards",
|
|
89
|
+
"features"
|
|
83
90
|
],
|
|
84
91
|
model: ["model"],
|
|
85
|
-
|
|
92
|
+
attachments: [
|
|
93
|
+
"attachments",
|
|
94
|
+
"tools",
|
|
95
|
+
"limits"
|
|
96
|
+
],
|
|
86
97
|
access: ["access"],
|
|
87
|
-
|
|
98
|
+
wiki: ["wiki"],
|
|
99
|
+
advanced: ["analyze", "logging"]
|
|
88
100
|
};
|
|
89
101
|
/** Section heading for one field group inside its tab. */
|
|
90
102
|
const FIELD_GROUP_LABELS = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
model: "
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
trigger: "触发",
|
|
104
|
+
workspace: "工作区",
|
|
105
|
+
cards: "话题与卡片",
|
|
106
|
+
features: "飞书侧功能",
|
|
107
|
+
model: "模型",
|
|
108
|
+
attachments: "入站附件",
|
|
109
|
+
tools: "模型可用的飞书工具",
|
|
110
|
+
limits: "限额与超时",
|
|
111
|
+
access: "准入",
|
|
112
|
+
wiki: "知识库工具",
|
|
113
|
+
analyze: "日志分析接口",
|
|
114
|
+
logging: "日志"
|
|
98
115
|
};
|
|
116
|
+
/** Groups rendered collapsed behind one summary row (rarely changed values). */
|
|
117
|
+
const COLLAPSED_FIELD_GROUPS = ["limits"];
|
|
99
118
|
/** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
|
|
100
119
|
const PANEL_COPY = {
|
|
101
120
|
title: "飞书渠道",
|
|
102
|
-
description: "飞书长连接、应用凭据与渠道配置。凭据只写入 DSH 凭据库且不回显;标注「需重连」的项在连接建立时读取。",
|
|
103
|
-
statusCard: "连接状态",
|
|
104
121
|
phaseUnconfigured: "待配置",
|
|
105
122
|
phaseIdle: "未连接",
|
|
106
123
|
phaseConnecting: "连接中",
|
|
107
124
|
phaseConnected: "已连接",
|
|
108
125
|
phaseError: "连接失败",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
statusSummaryError: "最近一次连接飞书失败,请检查凭据与网络后重试。",
|
|
126
|
+
statusSummaryConnected: "机器人在线,可以收发消息",
|
|
127
|
+
statusSummaryUnconfigured: "还没有填写飞书应用凭据,插件不会连接飞书",
|
|
128
|
+
statusSummaryIdle: "凭据已就绪,还没有建立连接",
|
|
129
|
+
statusSummaryConnecting: "正在连接飞书…",
|
|
130
|
+
statusSummaryError: "连接飞书失败,请检查凭据与网络后重试",
|
|
131
|
+
uptimePrefix: "运行",
|
|
116
132
|
lastError: "最近一次连接失败",
|
|
117
|
-
|
|
118
|
-
scopeCheckOk: "
|
|
119
|
-
scopeCheckMissing: "
|
|
120
|
-
|
|
133
|
+
checksTitle: "自检",
|
|
134
|
+
scopeCheckOk: "插件需要的飞书权限都已开通",
|
|
135
|
+
scopeCheckMissing: "{n} 项权限未开通",
|
|
136
|
+
scopeCheckMissingRequired: "{n} 项必需权限未开通,相关功能无法使用",
|
|
137
|
+
scopeCheckMissingOptional: "不影响收发消息,只有相关功能不可用。开通后需在开发者后台发布新版本。",
|
|
138
|
+
scopeCheckPublish: "开通后需在开发者后台发布新版本。",
|
|
139
|
+
scopeCheckOpenAll: "全部开通",
|
|
121
140
|
scopeCheckOpenOne: "去开通",
|
|
122
|
-
scopeCheckOptional: "可选",
|
|
123
141
|
scopeCheckRequired: "必需",
|
|
124
|
-
scopeCheckError: "
|
|
142
|
+
scopeCheckError: "无法读取权限开通情况",
|
|
125
143
|
slashCommandSyncTitle: "斜杠指令面板",
|
|
126
|
-
slashCommandSyncOk: "
|
|
127
|
-
|
|
128
|
-
slashCommandSyncCreated: "本次新增",
|
|
129
|
-
slashCommandSyncUpdated: "本次更新",
|
|
130
|
-
slashCommandSyncRemoved: "本次删除",
|
|
131
|
-
slashCommandSyncError: "同步飞书原生斜杠指令面板失败(不影响直接输入命令):",
|
|
144
|
+
slashCommandSyncOk: "已同步 {total} 条命令,飞书客户端约 5 分钟后刷新",
|
|
145
|
+
slashCommandSyncError: "同步失败,直接输入命令不受影响",
|
|
132
146
|
missingConfig: "缺少配置",
|
|
133
|
-
statusHintUnconfigured: "配置完成后插件会自动连接;也可以稍后在本页手动连接。",
|
|
134
|
-
connectionInfo: "连接信息",
|
|
135
|
-
connectionStateLabel: "飞书长连接",
|
|
136
|
-
botOpenId: "Bot open_id",
|
|
137
|
-
uptime: "运行时长",
|
|
138
147
|
routeCount: "话题路由",
|
|
139
148
|
liveAgentCount: "活动会话",
|
|
140
149
|
inflightTurnCount: "在途回复",
|
|
150
|
+
botOpenId: "Bot open_id",
|
|
141
151
|
healthReasons: "健康判定",
|
|
142
152
|
unknownValue: "—",
|
|
143
153
|
connect: "连接飞书",
|
|
144
154
|
connecting: "连接中…",
|
|
145
155
|
onboardingTitle: "开始使用飞书渠道",
|
|
146
|
-
onboardingIntro: "只需三步:在飞书开放平台创建自建应用,把 App ID / App Secret 填入本页,保存后自动连接。",
|
|
147
156
|
onboardingStep1Title: "创建飞书自建应用",
|
|
148
|
-
onboardingStep1Text: "
|
|
157
|
+
onboardingStep1Text: "在 open.feishu.cn/app 创建企业自建应用,开启机器人能力;事件订阅选「长连接」,订阅 im.message.receive_v1。",
|
|
149
158
|
onboardingStep2Title: "填写应用凭据",
|
|
150
|
-
onboardingStep2Text: "
|
|
151
|
-
onboardingStep3Title: "
|
|
152
|
-
onboardingStep3Text: "
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
credentialsRef: "凭据引用",
|
|
159
|
+
onboardingStep2Text: "把 App ID(cli_ 开头)和 App Secret 填到下方「应用凭据」。",
|
|
160
|
+
onboardingStep3Title: "保存后自动连接",
|
|
161
|
+
onboardingStep3Text: "两项都保存后插件会自动连接,状态变成「已连接」就能在飞书里私聊机器人。",
|
|
162
|
+
credentialsTitle: "应用凭据",
|
|
163
|
+
credentialsHint: "值只存在本机凭据库,面板不回显。",
|
|
164
|
+
appIdLabel: "App ID",
|
|
165
|
+
appSecretLabel: "App Secret",
|
|
166
|
+
credentialStore: "凭据库",
|
|
159
167
|
saveCredentials: "保存凭据",
|
|
160
168
|
savingCredentials: "保存中…",
|
|
161
169
|
credentialSaved: "凭据已保存",
|
|
@@ -165,26 +173,29 @@ window.__ModuleLoader__.load({
|
|
|
165
173
|
credentialAutoConnectFailed: "凭据已保存,但自动连接失败",
|
|
166
174
|
credentialRemoved: "已移除存储的凭据",
|
|
167
175
|
credentialRemovedPending: "已移除存储的凭据,重启飞书连接后生效",
|
|
168
|
-
|
|
176
|
+
replaceCredential: "更换",
|
|
177
|
+
setCredential: "设置",
|
|
178
|
+
cancel: "取消",
|
|
179
|
+
removeCredential: "移除",
|
|
169
180
|
removingCredential: "移除中…",
|
|
170
181
|
credentialConfigured: "已配置",
|
|
171
182
|
credentialMissing: "未配置",
|
|
172
183
|
credentialReadOnly: "当前凭据存储不可写",
|
|
173
184
|
appIdPlaceholder: "输入 App ID(cli_…)",
|
|
174
185
|
appSecretPlaceholder: "输入 App Secret",
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
sessionsTabHelp: "私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。",
|
|
178
|
-
modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
|
|
179
|
-
workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。",
|
|
180
|
-
accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
|
|
181
|
-
advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
|
|
182
|
-
save: "保存更改",
|
|
186
|
+
credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
|
|
187
|
+
save: "保存",
|
|
183
188
|
saving: "保存中…",
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
discard: "放弃",
|
|
190
|
+
dirtyCount: "{n} 项未保存",
|
|
191
|
+
dirtyRestart: ",其中 {n} 项需要重连后生效",
|
|
192
|
+
details: "详情",
|
|
193
|
+
expand: "展开",
|
|
194
|
+
collapse: "收起",
|
|
195
|
+
collapsedCount: "{n} 项",
|
|
196
|
+
collapsedClean: "一般不用改",
|
|
197
|
+
collapsedChanged: "{n} 项已在面板修改",
|
|
198
|
+
restartTag: "需重连",
|
|
188
199
|
listAdd: "添加",
|
|
189
200
|
listRemove: "移除",
|
|
190
201
|
listEntry: "第 {n} 项",
|
|
@@ -196,55 +207,55 @@ window.__ModuleLoader__.load({
|
|
|
196
207
|
directoryPicked: "已选择目录",
|
|
197
208
|
directoryPickFailed: "无法打开系统目录选择器",
|
|
198
209
|
saved: "已保存",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
saveInvalid: "请先修正标红的配置项",
|
|
211
|
+
numberInvalid: "请输入数字",
|
|
212
|
+
numberMin: "不得小于 {n}",
|
|
213
|
+
numberMax: "不得大于 {n}",
|
|
214
|
+
modelFootnote: "对之后新建的会话生效。",
|
|
215
|
+
catalogFailed: "模型目录加载失败,已退回手填。",
|
|
216
|
+
catalogLoading: "正在读取宿主机选项…",
|
|
217
|
+
catalogUnavailable: "宿主机未提供可选项,请手填。",
|
|
218
|
+
restartBanner: "以下配置要重启飞书连接后生效:",
|
|
219
|
+
restartWarning: "重启会断开当前飞书连接并中断进行中的回复,确定现在重启?",
|
|
220
|
+
reconnect: "重启连接",
|
|
209
221
|
reconnecting: "重连中…",
|
|
210
|
-
refresh: "
|
|
222
|
+
refresh: "刷新",
|
|
211
223
|
retry: "重试",
|
|
212
224
|
pollFailed: "状态刷新失败",
|
|
213
225
|
connectDone: "已连接飞书",
|
|
214
226
|
reconnectDone: "已重启飞书连接",
|
|
215
|
-
saveInvalid: "请先修正标红的配置项",
|
|
216
|
-
enabled: "已启用",
|
|
217
|
-
disabled: "已关闭",
|
|
218
|
-
credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
|
|
219
|
-
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
220
227
|
loading: "加载中…",
|
|
221
228
|
loadFailed: "设置面板加载失败",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
analyzeTokenLabel: "访问令牌",
|
|
230
|
+
analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
|
|
231
|
+
analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
|
|
232
|
+
analyzeTokenSaved: "令牌已保存,下一次请求生效",
|
|
233
|
+
analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
|
|
234
|
+
overlayTitle: "Web 覆盖",
|
|
226
235
|
overlayPath: "覆盖文件",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
overlayCount: "{n} 项配置由面板覆盖",
|
|
237
|
+
noOverrides: "当前没有面板覆盖的配置",
|
|
238
|
+
overlayPathHelp: "面板保存的配置写在这个 JSON 文件里,优先级高于 profile。可以直接编辑;面板保存会整体重写它。",
|
|
239
|
+
clear: "恢复 profile 配置",
|
|
240
|
+
clearing: "恢复中…",
|
|
241
|
+
clearConfirm: "确定删除所有面板覆盖,恢复为 profile 配置?",
|
|
242
|
+
cleared: "已恢复为 profile 配置",
|
|
243
|
+
debugModeWarning: "调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。",
|
|
244
|
+
accessAdminMissingWarning: "没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。",
|
|
245
|
+
accessAdminOwnerSummary: "未单独设置,正在使用应用所有者 {id}",
|
|
246
|
+
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
247
|
+
accessRequestsTitle: "待处理申请",
|
|
236
248
|
accessGroupsTitle: "已授权群",
|
|
237
|
-
accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
|
|
238
249
|
accessUsersTitle: "已授权用户",
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
|
|
250
|
+
accessGroupsEmpty: "还没有授权任何群",
|
|
251
|
+
accessUsersEmpty: "还没有授权任何用户",
|
|
252
|
+
accessGroupChatIdPlaceholder: "chat_id(oc_…,群里发 /groupid 获取)",
|
|
243
253
|
accessGroupNamePlaceholder: "群名(可选)",
|
|
244
|
-
accessUserIdPlaceholder: "
|
|
254
|
+
accessUserIdPlaceholder: "user_id(私聊机器人发 /userid 获取)",
|
|
245
255
|
accessUserNamePlaceholder: "昵称(可选)",
|
|
246
256
|
accessAddGroup: "添加群",
|
|
247
257
|
accessAddUser: "添加用户",
|
|
258
|
+
accessAdd: "添加",
|
|
248
259
|
accessRemove: "移除",
|
|
249
260
|
accessAllow: "授权",
|
|
250
261
|
accessDeny: "忽略",
|
|
@@ -252,19 +263,36 @@ window.__ModuleLoader__.load({
|
|
|
252
263
|
accessSourceCommand: "对话命令",
|
|
253
264
|
accessSourceCard: "授权卡",
|
|
254
265
|
accessSourcePanel: "设置面板",
|
|
255
|
-
accessRequestGroup: "
|
|
256
|
-
accessRequestUser: "
|
|
257
|
-
|
|
258
|
-
accessRequestExpires: "
|
|
266
|
+
accessRequestGroup: "申请授权群",
|
|
267
|
+
accessRequestUser: "申请使用",
|
|
268
|
+
accessRequestIn: "在「{group}」",
|
|
269
|
+
accessRequestExpires: "{date} 前有效",
|
|
259
270
|
accessGroupAdded: "已添加群授权",
|
|
260
271
|
accessGroupRemoved: "已移除群授权",
|
|
261
272
|
accessUserAdded: "已添加用户授权",
|
|
262
273
|
accessUserRemoved: "已移除用户授权",
|
|
263
274
|
accessRequestAllowed: "已授权",
|
|
264
275
|
accessRequestDenied: "已忽略",
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
wikiUsersTitle: "可使用的人",
|
|
277
|
+
wikiUsersAdminPlus: "管理员 + {n}",
|
|
278
|
+
wikiUsersEmpty: "目前只有管理员发起的话题能用知识库工具",
|
|
279
|
+
wikiUsersHint: "也可以在飞书发 /auth wiki add @用户",
|
|
280
|
+
wikiUserAdded: "已加入知识库白名单",
|
|
281
|
+
wikiUserRemoved: "已移出知识库白名单"
|
|
267
282
|
};
|
|
283
|
+
/**
|
|
284
|
+
* Fill `{name}` placeholders of one `PANEL_COPY` template.
|
|
285
|
+
*
|
|
286
|
+
* @param template - Copy containing `{name}` placeholders.
|
|
287
|
+
* @param values - Replacement per placeholder name.
|
|
288
|
+
* @returns The filled string; unknown placeholders stay as written.
|
|
289
|
+
*/
|
|
290
|
+
function fillCopy(template, values) {
|
|
291
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
292
|
+
const value = values[name];
|
|
293
|
+
return value === void 0 ? match : String(value);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
268
296
|
//#endregion
|
|
269
297
|
//#region src/client/api.ts
|
|
270
298
|
/**
|
|
@@ -469,6 +497,17 @@ window.__ModuleLoader__.load({
|
|
|
469
497
|
async function removeAccessUser(userId) {
|
|
470
498
|
return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", { userId });
|
|
471
499
|
}
|
|
500
|
+
/** Add one user `user_id` to the wiki whitelist. */
|
|
501
|
+
async function addWikiUser(userId, name) {
|
|
502
|
+
return mutateAccess(ACCESS_WIKI_USERS_ROUTE, "POST", name === void 0 || name === "" ? { userId } : {
|
|
503
|
+
userId,
|
|
504
|
+
name
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/** Remove one user from the wiki whitelist (takes effect on the next wiki tool call). */
|
|
508
|
+
async function removeWikiUser(userId) {
|
|
509
|
+
return mutateAccess(ACCESS_WIKI_USERS_ROUTE, "DELETE", { userId });
|
|
510
|
+
}
|
|
472
511
|
/** Settle one pending authorization request (same resolver as the Feishu card). */
|
|
473
512
|
async function resolveAccessRequest(token, decision) {
|
|
474
513
|
return mutateAccess(ACCESS_REQUESTS_ROUTE, "POST", {
|
|
@@ -570,6 +609,110 @@ window.__ModuleLoader__.load({
|
|
|
570
609
|
});
|
|
571
610
|
}
|
|
572
611
|
//#endregion
|
|
612
|
+
//#region src/client/number-field.ts
|
|
613
|
+
/**
|
|
614
|
+
* 浏览器半区的 `number` 字段辅助:按 Host 描述的 `scale` 在显示单位与存储
|
|
615
|
+
* 单位之间换算、保存前校验上下界,以及字节数的 MiB 读数。
|
|
616
|
+
*
|
|
617
|
+
* 不含 JSX/React,便于单测。Host 保存时按 config schema 复核。
|
|
618
|
+
*
|
|
619
|
+
* @module dsh-channels/client/number-field
|
|
620
|
+
*/
|
|
621
|
+
/** Drop float noise from `stored / scale` (e.g. 0.30000000000000004). */
|
|
622
|
+
function tidy(value) {
|
|
623
|
+
return Number(value.toPrecision(12));
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Value the input shows for one field.
|
|
627
|
+
*
|
|
628
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
629
|
+
* @param value - Draft (the typed string) or effective value (stored units).
|
|
630
|
+
* @returns The input text.
|
|
631
|
+
*/
|
|
632
|
+
function numberInputValue(field, value) {
|
|
633
|
+
if (typeof value === "string") return value;
|
|
634
|
+
if (typeof value !== "number") return "";
|
|
635
|
+
return String(field.scale === void 0 ? value : tidy(value / field.scale));
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Input `step` in display units.
|
|
639
|
+
*
|
|
640
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
641
|
+
* @returns The step, or `undefined` when the Host gives none.
|
|
642
|
+
*/
|
|
643
|
+
function numberInputStep(field) {
|
|
644
|
+
if (field.step === void 0) return void 0;
|
|
645
|
+
return field.scale === void 0 ? field.step : tidy(field.step / field.scale);
|
|
646
|
+
}
|
|
647
|
+
/** One bound in display units with the field's unit appended. */
|
|
648
|
+
function boundLabel(field, stored) {
|
|
649
|
+
const shown = field.scale === void 0 ? stored : tidy(stored / field.scale);
|
|
650
|
+
return field.unit === void 0 ? String(shown) : `${shown} ${field.unit}`;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Turn one number draft into its save payload, checking the Host bounds.
|
|
654
|
+
*
|
|
655
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
656
|
+
* @param raw - The draft: the typed string, or a stored-unit number.
|
|
657
|
+
* @returns The stored-unit value (`''` clears an optional field), or the reason it is rejected.
|
|
658
|
+
*/
|
|
659
|
+
function numberDraftValue(field, raw) {
|
|
660
|
+
let stored;
|
|
661
|
+
if (typeof raw === "number") stored = raw;
|
|
662
|
+
else {
|
|
663
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
664
|
+
if (text === "") return field.optional === true ? {
|
|
665
|
+
ok: true,
|
|
666
|
+
value: ""
|
|
667
|
+
} : {
|
|
668
|
+
ok: false,
|
|
669
|
+
message: PANEL_COPY.numberInvalid
|
|
670
|
+
};
|
|
671
|
+
const parsed = Number(text);
|
|
672
|
+
if (!Number.isFinite(parsed)) return {
|
|
673
|
+
ok: false,
|
|
674
|
+
message: PANEL_COPY.numberInvalid
|
|
675
|
+
};
|
|
676
|
+
stored = field.scale === void 0 ? parsed : Math.round(parsed * field.scale);
|
|
677
|
+
}
|
|
678
|
+
if (field.min !== void 0 && stored < field.min) return {
|
|
679
|
+
ok: false,
|
|
680
|
+
message: fillCopy(PANEL_COPY.numberMin, { n: boundLabel(field, field.min) })
|
|
681
|
+
};
|
|
682
|
+
if (field.max !== void 0 && stored > field.max) return {
|
|
683
|
+
ok: false,
|
|
684
|
+
message: fillCopy(PANEL_COPY.numberMax, { n: boundLabel(field, field.max) })
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
ok: true,
|
|
688
|
+
value: stored
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Human reading of a byte count, e.g. `20 MiB`.
|
|
693
|
+
*
|
|
694
|
+
* @param text - The input text (bytes).
|
|
695
|
+
* @returns The reading, or `undefined` when the text is not a positive number.
|
|
696
|
+
*/
|
|
697
|
+
function bytesReading(text) {
|
|
698
|
+
const bytes = Number(text);
|
|
699
|
+
if (text.trim() === "" || !Number.isFinite(bytes) || bytes <= 0) return void 0;
|
|
700
|
+
const units = [
|
|
701
|
+
"B",
|
|
702
|
+
"KiB",
|
|
703
|
+
"MiB",
|
|
704
|
+
"GiB",
|
|
705
|
+
"TiB"
|
|
706
|
+
];
|
|
707
|
+
let value = bytes;
|
|
708
|
+
let unit = 0;
|
|
709
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
710
|
+
value /= 1024;
|
|
711
|
+
unit += 1;
|
|
712
|
+
}
|
|
713
|
+
return `${Number(value.toFixed(value >= 100 || unit === 0 ? 0 : 1))} ${units[unit]}`;
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
573
716
|
//#region src/feishu/wiki/links.ts
|
|
574
717
|
/**
|
|
575
718
|
* Feishu wiki (知识库) link parsing: the `wikiRoots` allow-list entries and
|
|
@@ -769,8 +912,8 @@ window.__ModuleLoader__.load({
|
|
|
769
912
|
//#endregion
|
|
770
913
|
//#region src/client/panel/shared.tsx
|
|
771
914
|
/**
|
|
772
|
-
* 设置面板共享层:页面状态类型、Host 字段渲染原语(
|
|
773
|
-
* SaveBar
|
|
915
|
+
* 设置面板共享层:页面状态类型、Host 字段渲染原语(SettingRow /
|
|
916
|
+
* FieldSections / SaveBar)、分区与列表原语、生命周期徽标与纯格式化工具。
|
|
774
917
|
*
|
|
775
918
|
* 这一层不发请求、不持有页面状态,只把 Host 下发的 wire 数据渲染成控件;
|
|
776
919
|
* 页面外壳在 `index.tsx`,各 tab 只组合这些原语。
|
|
@@ -780,7 +923,7 @@ window.__ModuleLoader__.load({
|
|
|
780
923
|
function messageOf(error) {
|
|
781
924
|
return error instanceof Error ? error.message : String(error);
|
|
782
925
|
}
|
|
783
|
-
/** Compact duration for the
|
|
926
|
+
/** Compact duration for the page header. */
|
|
784
927
|
function formatUptime(ms) {
|
|
785
928
|
if (ms === void 0 || !Number.isFinite(ms)) return PANEL_COPY.unknownValue;
|
|
786
929
|
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
@@ -800,21 +943,9 @@ window.__ModuleLoader__.load({
|
|
|
800
943
|
function fieldLabel(fields, key) {
|
|
801
944
|
return fields.find((field) => field.key === key)?.label ?? key;
|
|
802
945
|
}
|
|
803
|
-
/** Effective access-mode label, reusing the Host field options for consistency. */
|
|
804
|
-
function accessModeLabel(panel, mode) {
|
|
805
|
-
return panel.config.fields.find((item) => item.key === "accessMode")?.options?.find((option) => option.value === mode)?.label ?? mode;
|
|
806
|
-
}
|
|
807
|
-
/** Numeric draft kept as the raw input string; parsed on save. */
|
|
808
|
-
function numericDraft(field, raw) {
|
|
809
|
-
if (typeof raw === "number") return raw;
|
|
810
|
-
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
811
|
-
const parsed = Number(raw);
|
|
812
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
813
|
-
}
|
|
814
946
|
/**
|
|
815
947
|
* Turn the dirty draft into the wire payload, keeping the Host-descriptor
|
|
816
|
-
* bounds check client-side
|
|
817
|
-
* every value on save).
|
|
948
|
+
* bounds check client-side (the Host re-validates every value on save).
|
|
818
949
|
*/
|
|
819
950
|
function validateDraft(panel, draft) {
|
|
820
951
|
const values = {};
|
|
@@ -823,29 +954,12 @@ window.__ModuleLoader__.load({
|
|
|
823
954
|
if (!(field.key in draft)) continue;
|
|
824
955
|
const raw = draft[field.key];
|
|
825
956
|
if (field.kind === "number") {
|
|
826
|
-
const parsed =
|
|
827
|
-
if (parsed
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (field.min !== void 0 && parsed < field.min) {
|
|
835
|
-
issues.push({
|
|
836
|
-
key: field.key,
|
|
837
|
-
message: `不得小于 ${field.min}`
|
|
838
|
-
});
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
if (field.max !== void 0 && parsed > field.max) {
|
|
842
|
-
issues.push({
|
|
843
|
-
key: field.key,
|
|
844
|
-
message: `不得大于 ${field.max}`
|
|
845
|
-
});
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
values[field.key] = parsed;
|
|
957
|
+
const parsed = numberDraftValue(field, raw);
|
|
958
|
+
if (parsed.ok) values[field.key] = parsed.value;
|
|
959
|
+
else issues.push({
|
|
960
|
+
key: field.key,
|
|
961
|
+
message: parsed.message
|
|
962
|
+
});
|
|
849
963
|
continue;
|
|
850
964
|
}
|
|
851
965
|
if (field.kind === "boolean") {
|
|
@@ -865,7 +979,7 @@ window.__ModuleLoader__.load({
|
|
|
865
979
|
issues
|
|
866
980
|
};
|
|
867
981
|
}
|
|
868
|
-
/** Header
|
|
982
|
+
/** Header pill for the Host-derived lifecycle phase. */
|
|
869
983
|
function phaseBadge(phase) {
|
|
870
984
|
switch (phase) {
|
|
871
985
|
case "connected": return {
|
|
@@ -890,7 +1004,7 @@ window.__ModuleLoader__.load({
|
|
|
890
1004
|
};
|
|
891
1005
|
}
|
|
892
1006
|
}
|
|
893
|
-
/** One-line explanation
|
|
1007
|
+
/** One-line explanation of the current phase (connection tab status block). */
|
|
894
1008
|
function phaseSummary(phase) {
|
|
895
1009
|
switch (phase) {
|
|
896
1010
|
case "connected": return PANEL_COPY.statusSummaryConnected;
|
|
@@ -900,6 +1014,99 @@ window.__ModuleLoader__.load({
|
|
|
900
1014
|
default: return PANEL_COPY.statusSummaryUnconfigured;
|
|
901
1015
|
}
|
|
902
1016
|
}
|
|
1017
|
+
/** Small rounded status pill. */
|
|
1018
|
+
function Pill(props) {
|
|
1019
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1020
|
+
className: `dsh-channels-cfg__pill dsh-channels-cfg__pill--${props.tone}`,
|
|
1021
|
+
children: [props.dot === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__dot" }) : null, props.children]
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
/** One titled section: a muted heading above a bordered row list. */
|
|
1025
|
+
function Section(props) {
|
|
1026
|
+
const { title, footnote, tone, children } = props;
|
|
1027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1028
|
+
className: "dsh-channels-cfg__sec",
|
|
1029
|
+
children: [
|
|
1030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1031
|
+
className: "dsh-channels-cfg__secTitle",
|
|
1032
|
+
children: title
|
|
1033
|
+
}),
|
|
1034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1035
|
+
className: `dsh-channels-cfg__list${tone === "warn" ? " dsh-channels-cfg__list--warn" : ""}`,
|
|
1036
|
+
children
|
|
1037
|
+
}),
|
|
1038
|
+
footnote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1039
|
+
className: "dsh-channels-cfg__secFoot",
|
|
1040
|
+
children: footnote
|
|
1041
|
+
})
|
|
1042
|
+
]
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* One setting row: label (with tags and the「详情」toggle) and a one-line
|
|
1047
|
+
* summary on the left, the control on the right. `details` renders below the
|
|
1048
|
+
* row once expanded; `wide` puts the control under the text.
|
|
1049
|
+
*/
|
|
1050
|
+
function Row(props) {
|
|
1051
|
+
const { label, tags, summary, details, control, error, wide = false, leading } = props;
|
|
1052
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1053
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1054
|
+
className: "dsh-channels-cfg__rowWrap",
|
|
1055
|
+
children: [
|
|
1056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1057
|
+
className: `dsh-channels-cfg__row${wide ? " dsh-channels-cfg__row--wide" : ""}`,
|
|
1058
|
+
children: [
|
|
1059
|
+
leading,
|
|
1060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1061
|
+
className: "dsh-channels-cfg__rowText",
|
|
1062
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1063
|
+
className: "dsh-channels-cfg__rowLabel",
|
|
1064
|
+
children: [
|
|
1065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
|
|
1066
|
+
tags,
|
|
1067
|
+
details === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1068
|
+
type: "button",
|
|
1069
|
+
className: "dsh-channels-cfg__more",
|
|
1070
|
+
"aria-expanded": open,
|
|
1071
|
+
onClick: () => setOpen(!open),
|
|
1072
|
+
children: PANEL_COPY.details
|
|
1073
|
+
})
|
|
1074
|
+
]
|
|
1075
|
+
}), summary === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1076
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
1077
|
+
children: summary
|
|
1078
|
+
})]
|
|
1079
|
+
}),
|
|
1080
|
+
control === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1081
|
+
className: "dsh-channels-cfg__rowCtl",
|
|
1082
|
+
children: control
|
|
1083
|
+
})
|
|
1084
|
+
]
|
|
1085
|
+
}),
|
|
1086
|
+
open && details !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1087
|
+
className: "dsh-channels-cfg__details",
|
|
1088
|
+
children: details
|
|
1089
|
+
}) : null,
|
|
1090
|
+
error === void 0 || error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1091
|
+
className: "dsh-channels-cfg__rowError",
|
|
1092
|
+
children: error
|
|
1093
|
+
})
|
|
1094
|
+
]
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/** Accessible on/off switch. */
|
|
1098
|
+
function Switch(props) {
|
|
1099
|
+
const { checked, label, disabled = false, onChange } = props;
|
|
1100
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1101
|
+
type: "button",
|
|
1102
|
+
role: "switch",
|
|
1103
|
+
className: "dsh-channels-cfg__switch",
|
|
1104
|
+
"aria-checked": checked,
|
|
1105
|
+
"aria-label": label,
|
|
1106
|
+
disabled,
|
|
1107
|
+
onClick: () => onChange(!checked)
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
903
1110
|
/**
|
|
904
1111
|
* Text input paired with the host-native「选择目录…」button. The button owns
|
|
905
1112
|
* its own busy state: the OS dialog can stay open for a long time, so the
|
|
@@ -928,7 +1135,7 @@ window.__ModuleLoader__.load({
|
|
|
928
1135
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
929
1136
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
930
1137
|
type: "button",
|
|
931
|
-
className: "dsh-channels-cfg__btn
|
|
1138
|
+
className: "dsh-channels-cfg__btn",
|
|
932
1139
|
disabled: disabled || picking,
|
|
933
1140
|
onClick: () => {
|
|
934
1141
|
pick();
|
|
@@ -939,7 +1146,7 @@ window.__ModuleLoader__.load({
|
|
|
939
1146
|
}
|
|
940
1147
|
/**
|
|
941
1148
|
* `string-list` control: one single-line input per entry, each with a remove
|
|
942
|
-
* button, and an add
|
|
1149
|
+
* button, and an add link below (disabled at `maxItems` inputs). An empty
|
|
943
1150
|
* list renders one blank input so the first entry can be typed directly.
|
|
944
1151
|
* Issues carrying an `index` mark that input and show the reason under it.
|
|
945
1152
|
*/
|
|
@@ -948,12 +1155,12 @@ window.__ModuleLoader__.load({
|
|
|
948
1155
|
const empty = listItems(value).length === 0;
|
|
949
1156
|
const rows = listRows(value);
|
|
950
1157
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
951
|
-
className: "dsh-channels-
|
|
1158
|
+
className: "dsh-channels-cfg__stringList",
|
|
952
1159
|
children: [rows.map((item, index) => {
|
|
953
1160
|
const issue = issues.find((entry) => entry.index === index);
|
|
954
1161
|
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
955
1162
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
956
|
-
className: "dsh-channels-
|
|
1163
|
+
className: "dsh-channels-cfg__stringListItem",
|
|
957
1164
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
958
1165
|
className: "dsh-channels-cfg__inputRow",
|
|
959
1166
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -967,7 +1174,7 @@ window.__ModuleLoader__.load({
|
|
|
967
1174
|
onChange: (event) => onChange(field.key, withListEntry(rows, index, event.target.value))
|
|
968
1175
|
}), empty ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
969
1176
|
type: "button",
|
|
970
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--
|
|
1177
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
971
1178
|
"aria-label": `${PANEL_COPY.listRemove} ${position}`,
|
|
972
1179
|
disabled,
|
|
973
1180
|
onClick: () => onChange(field.key, withoutListEntry(rows, index)),
|
|
@@ -978,861 +1185,703 @@ window.__ModuleLoader__.load({
|
|
|
978
1185
|
children: issue.message
|
|
979
1186
|
})]
|
|
980
1187
|
}, index);
|
|
981
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1188
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
982
1189
|
type: "button",
|
|
983
|
-
className: "dsh-channels-
|
|
1190
|
+
className: "dsh-channels-cfg__link",
|
|
984
1191
|
disabled: disabled || listIsFull(field, rows),
|
|
985
1192
|
onClick: () => onChange(field.key, withBlankListEntry(rows)),
|
|
986
|
-
children: PANEL_COPY.listAdd
|
|
1193
|
+
children: ["+ ", PANEL_COPY.listAdd]
|
|
987
1194
|
}) })]
|
|
988
1195
|
});
|
|
989
1196
|
}
|
|
990
|
-
/**
|
|
991
|
-
function
|
|
992
|
-
const { field, value,
|
|
993
|
-
const
|
|
994
|
-
const
|
|
995
|
-
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
1197
|
+
/** Number input with its unit (and a MiB reading for byte counts). */
|
|
1198
|
+
function NumberInput(props) {
|
|
1199
|
+
const { field, value, inputClass, disabled, onChange } = props;
|
|
1200
|
+
const text = numberInputValue(field, value);
|
|
1201
|
+
const reading = field.unit === "字节" ? bytesReading(text) : void 0;
|
|
996
1202
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
997
|
-
className:
|
|
1203
|
+
className: "dsh-channels-cfg__inputRow",
|
|
998
1204
|
children: [
|
|
999
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1000
|
-
className: "dsh-channels-
|
|
1001
|
-
children:
|
|
1002
|
-
className: "dsh-channels-cfg__label",
|
|
1003
|
-
children: field.label
|
|
1004
|
-
}), field.restartRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1005
|
-
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--restart",
|
|
1006
|
-
children: PANEL_COPY.restartTag
|
|
1007
|
-
}) : null]
|
|
1205
|
+
reading === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1206
|
+
className: "dsh-channels-cfg__bytes",
|
|
1207
|
+
children: reading
|
|
1008
1208
|
}),
|
|
1009
|
-
|
|
1010
|
-
className: "dsh-channels-cfg__check",
|
|
1011
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1012
|
-
type: "checkbox",
|
|
1013
|
-
className: "dsh-channels-cfg__checkbox",
|
|
1014
|
-
checked: value === true,
|
|
1015
|
-
"aria-label": field.label,
|
|
1016
|
-
disabled,
|
|
1017
|
-
onChange: (event) => onChange(field.key, event.target.checked)
|
|
1018
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === true ? PANEL_COPY.enabled : PANEL_COPY.disabled })]
|
|
1019
|
-
}) : null,
|
|
1020
|
-
field.kind === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1021
|
-
className: inputClass,
|
|
1022
|
-
value: String(value ?? ""),
|
|
1023
|
-
disabled: disabled || field.disabled === true,
|
|
1024
|
-
"aria-label": field.label,
|
|
1025
|
-
onChange: (event) => onChange(field.key, event.target.value),
|
|
1026
|
-
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1027
|
-
value: option.value,
|
|
1028
|
-
children: option.label
|
|
1029
|
-
}, option.value))
|
|
1030
|
-
}) : null,
|
|
1031
|
-
field.kind === "number" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1209
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1032
1210
|
type: "number",
|
|
1033
|
-
className: inputClass
|
|
1034
|
-
value:
|
|
1035
|
-
min: field.min,
|
|
1036
|
-
max: field.max,
|
|
1037
|
-
step: field.step,
|
|
1038
|
-
"aria-label": field.label,
|
|
1039
|
-
disabled,
|
|
1040
|
-
onChange: (event) => onChange(field.key, event.target.value)
|
|
1041
|
-
}) : null,
|
|
1042
|
-
field.kind === "text" ? field.browse === "directory" && onPickDirectory !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectoryTextInput, {
|
|
1043
|
-
field,
|
|
1044
|
-
value,
|
|
1045
|
-
inputClass,
|
|
1046
|
-
disabled,
|
|
1047
|
-
onChange,
|
|
1048
|
-
onPick: onPickDirectory
|
|
1049
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1050
|
-
type: "text",
|
|
1051
|
-
className: inputClass,
|
|
1052
|
-
value: String(value ?? ""),
|
|
1211
|
+
className: `${inputClass} dsh-channels-cfg__input--num`,
|
|
1212
|
+
value: text,
|
|
1053
1213
|
placeholder: field.placeholder,
|
|
1214
|
+
step: numberInputStep(field),
|
|
1054
1215
|
"aria-label": field.label,
|
|
1055
1216
|
disabled,
|
|
1056
1217
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
1057
|
-
}) : null,
|
|
1058
|
-
field.kind === "string-list" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1059
|
-
field,
|
|
1060
|
-
value,
|
|
1061
|
-
issues: entryIssues,
|
|
1062
|
-
disabled,
|
|
1063
|
-
onChange
|
|
1064
|
-
}) : null,
|
|
1065
|
-
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1066
|
-
className: "dsh-channels-cfg__help",
|
|
1067
|
-
children: field.help
|
|
1068
1218
|
}),
|
|
1069
|
-
|
|
1070
|
-
className: "dsh-channels-
|
|
1071
|
-
children:
|
|
1219
|
+
field.unit === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1220
|
+
className: "dsh-channels-cfg__unit",
|
|
1221
|
+
children: field.unit
|
|
1072
1222
|
})
|
|
1073
1223
|
]
|
|
1074
1224
|
});
|
|
1075
1225
|
}
|
|
1226
|
+
/** The control for one Host field descriptor. */
|
|
1227
|
+
function FieldControl(props) {
|
|
1228
|
+
const { field, value, issues, invalid, disabled, onChange, onPickDirectory } = props;
|
|
1229
|
+
const inputClass = `dsh-channels-cfg__input${invalid ? " dsh-channels-cfg__input--invalid" : ""}`;
|
|
1230
|
+
switch (field.kind) {
|
|
1231
|
+
case "boolean": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
1232
|
+
checked: value === true,
|
|
1233
|
+
label: field.label,
|
|
1234
|
+
disabled,
|
|
1235
|
+
onChange: (next) => onChange(field.key, next)
|
|
1236
|
+
});
|
|
1237
|
+
case "select": {
|
|
1238
|
+
const toggle = field.toggle;
|
|
1239
|
+
if (toggle !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
1240
|
+
checked: value === toggle.on,
|
|
1241
|
+
label: field.label,
|
|
1242
|
+
disabled,
|
|
1243
|
+
onChange: (next) => onChange(field.key, next ? toggle.on : toggle.off)
|
|
1244
|
+
});
|
|
1245
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1246
|
+
className: `${inputClass} dsh-channels-cfg__select`,
|
|
1247
|
+
value: String(value ?? ""),
|
|
1248
|
+
disabled: disabled || field.disabled === true,
|
|
1249
|
+
"aria-label": field.label,
|
|
1250
|
+
onChange: (event) => onChange(field.key, event.target.value),
|
|
1251
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1252
|
+
value: option.value,
|
|
1253
|
+
children: option.label
|
|
1254
|
+
}, option.value))
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
case "number": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
|
|
1258
|
+
field,
|
|
1259
|
+
value,
|
|
1260
|
+
inputClass,
|
|
1261
|
+
disabled,
|
|
1262
|
+
onChange
|
|
1263
|
+
});
|
|
1264
|
+
case "text": return field.browse === "directory" && onPickDirectory !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectoryTextInput, {
|
|
1265
|
+
field,
|
|
1266
|
+
value,
|
|
1267
|
+
inputClass,
|
|
1268
|
+
disabled,
|
|
1269
|
+
onChange,
|
|
1270
|
+
onPick: onPickDirectory
|
|
1271
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1272
|
+
type: "text",
|
|
1273
|
+
className: `${inputClass}${field.wide === true ? "" : " dsh-channels-cfg__input--text"}`,
|
|
1274
|
+
value: String(value ?? ""),
|
|
1275
|
+
placeholder: field.placeholder,
|
|
1276
|
+
"aria-label": field.label,
|
|
1277
|
+
disabled,
|
|
1278
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
1279
|
+
});
|
|
1280
|
+
case "string-list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1281
|
+
field,
|
|
1282
|
+
value,
|
|
1283
|
+
issues,
|
|
1284
|
+
disabled,
|
|
1285
|
+
onChange
|
|
1286
|
+
});
|
|
1287
|
+
default: return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/** One Host field rendered as a setting row. */
|
|
1291
|
+
function SettingRow(props) {
|
|
1292
|
+
const { field, value, issues, disabled = false, onChange, onPickDirectory } = props;
|
|
1293
|
+
const issue = issues.find((item) => item.key === field.key && item.index === void 0);
|
|
1294
|
+
const entryIssues = issues.filter((item) => item.key === field.key && item.index !== void 0);
|
|
1295
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1296
|
+
label: field.label,
|
|
1297
|
+
tags: field.restartRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1298
|
+
className: "dsh-channels-cfg__tag",
|
|
1299
|
+
children: PANEL_COPY.restartTag
|
|
1300
|
+
}) : void 0,
|
|
1301
|
+
summary: field.summary,
|
|
1302
|
+
details: field.help,
|
|
1303
|
+
wide: field.wide === true,
|
|
1304
|
+
error: issue?.message,
|
|
1305
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldControl, {
|
|
1306
|
+
field,
|
|
1307
|
+
value,
|
|
1308
|
+
issues: entryIssues,
|
|
1309
|
+
invalid: issue !== void 0,
|
|
1310
|
+
disabled,
|
|
1311
|
+
onChange,
|
|
1312
|
+
onPickDirectory
|
|
1313
|
+
})
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
/** Rows for every field of one group. */
|
|
1317
|
+
function GroupRows(props) {
|
|
1318
|
+
const { form, fields } = props;
|
|
1319
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fields.map((field) => {
|
|
1320
|
+
const onPick = form.onPickDirectory;
|
|
1321
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
|
|
1322
|
+
field,
|
|
1323
|
+
value: form.draft[field.key] ?? effectiveValue(form.panel, field.key),
|
|
1324
|
+
issues: form.issues,
|
|
1325
|
+
disabled: form.locked,
|
|
1326
|
+
onChange: form.onChange,
|
|
1327
|
+
onPickDirectory: onPick === void 0 ? void 0 : () => onPick(field.key)
|
|
1328
|
+
}, field.key);
|
|
1329
|
+
}) });
|
|
1330
|
+
}
|
|
1076
1331
|
/**
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
* every key out of a group does not leave a stray heading behind.
|
|
1332
|
+
* A group of rarely changed fields behind one summary row. It opens on its
|
|
1333
|
+
* own when one of its fields has a validation issue.
|
|
1080
1334
|
*/
|
|
1081
|
-
function
|
|
1082
|
-
const { form,
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
className: "dsh-channels-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1335
|
+
function CollapsedGroup(props) {
|
|
1336
|
+
const { form, fields } = props;
|
|
1337
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1338
|
+
const keys = new Set(fields.map((field) => field.key));
|
|
1339
|
+
const changed = fields.filter((field) => form.panel.config.overriddenKeys.includes(field.key) || field.key in form.draft).length;
|
|
1340
|
+
const expanded = open || form.issues.some((issue) => keys.has(issue.key));
|
|
1341
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1342
|
+
label: fillCopy(PANEL_COPY.collapsedCount, { n: fields.length }),
|
|
1343
|
+
summary: changed === 0 ? PANEL_COPY.collapsedClean : fillCopy(PANEL_COPY.collapsedChanged, { n: changed }),
|
|
1344
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1345
|
+
type: "button",
|
|
1346
|
+
className: "dsh-channels-cfg__more",
|
|
1347
|
+
"aria-expanded": expanded,
|
|
1348
|
+
onClick: () => setOpen(!expanded),
|
|
1349
|
+
children: expanded ? PANEL_COPY.collapse : PANEL_COPY.expand
|
|
1350
|
+
})
|
|
1351
|
+
}), expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupRows, {
|
|
1352
|
+
form,
|
|
1353
|
+
fields
|
|
1354
|
+
}) : null] });
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* One tab's editable fields as titled sections, one per Host field group, in
|
|
1358
|
+
* the given order. Empty groups render nothing, so a Host that moves every key
|
|
1359
|
+
* out of a group does not leave a stray heading behind. `extra` appends rows
|
|
1360
|
+
* that are not Host fields (e.g. a credential) to one group's list.
|
|
1361
|
+
*/
|
|
1362
|
+
function FieldSections(props) {
|
|
1363
|
+
const { form, groups, extra = {}, footnotes = {} } = props;
|
|
1364
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: groups.map((group) => {
|
|
1365
|
+
const fields = form.fields.filter((field) => field.group === group);
|
|
1366
|
+
const more = extra[group];
|
|
1367
|
+
if (fields.length === 0 && more === void 0) return null;
|
|
1368
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1369
|
+
title: FIELD_GROUP_LABELS[group],
|
|
1370
|
+
footnote: footnotes[group],
|
|
1371
|
+
children: [COLLAPSED_FIELD_GROUPS.includes(group) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedGroup, {
|
|
1372
|
+
form,
|
|
1373
|
+
fields
|
|
1374
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupRows, {
|
|
1375
|
+
form,
|
|
1376
|
+
fields
|
|
1377
|
+
}), more]
|
|
1378
|
+
}, group);
|
|
1379
|
+
}) });
|
|
1115
1380
|
}
|
|
1116
|
-
/**
|
|
1381
|
+
/**
|
|
1382
|
+
* Sticky bar shown only while the draft has unsaved keys. Saving submits the
|
|
1383
|
+
* whole draft (every tab); discarding drops it.
|
|
1384
|
+
*/
|
|
1117
1385
|
function SaveBar(props) {
|
|
1118
|
-
const {
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
const dirty = form.dirtyCount > 0;
|
|
1122
|
-
const disabled = !dirty || form.busy !== null || form.locked;
|
|
1386
|
+
const { dirtyCount, restartCount, busy, locked, onSave, onDiscard } = props;
|
|
1387
|
+
if (dirtyCount === 0) return null;
|
|
1388
|
+
const disabled = busy !== null || locked;
|
|
1123
1389
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1124
|
-
className: "dsh-channels-
|
|
1390
|
+
className: "dsh-channels-cfg__saveBar",
|
|
1391
|
+
role: "region",
|
|
1392
|
+
"aria-label": PANEL_COPY.save,
|
|
1125
1393
|
children: [
|
|
1394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1395
|
+
className: "dsh-channels-cfg__saveMsg",
|
|
1396
|
+
children: [fillCopy(PANEL_COPY.dirtyCount, { n: dirtyCount }), restartCount === 0 ? "" : fillCopy(PANEL_COPY.dirtyRestart, { n: restartCount })]
|
|
1397
|
+
}),
|
|
1126
1398
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1127
1399
|
type: "button",
|
|
1128
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--
|
|
1129
|
-
onClick:
|
|
1400
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1401
|
+
onClick: onDiscard,
|
|
1130
1402
|
disabled,
|
|
1131
|
-
children:
|
|
1403
|
+
children: PANEL_COPY.discard
|
|
1132
1404
|
}),
|
|
1133
1405
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1134
1406
|
type: "button",
|
|
1135
|
-
className: "dsh-channels-cfg__btn",
|
|
1136
|
-
onClick:
|
|
1137
|
-
disabled
|
|
1138
|
-
children: PANEL_COPY.
|
|
1139
|
-
}),
|
|
1140
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
1141
|
-
pageDirty > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1142
|
-
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--dirty",
|
|
1143
|
-
children: [
|
|
1144
|
-
PANEL_COPY.dirtyCount,
|
|
1145
|
-
" ",
|
|
1146
|
-
pageDirty
|
|
1147
|
-
]
|
|
1148
|
-
}) : dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1149
|
-
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--dirty",
|
|
1150
|
-
children: [
|
|
1151
|
-
PANEL_COPY.dirtyCount,
|
|
1152
|
-
" ",
|
|
1153
|
-
form.dirtyCount,
|
|
1154
|
-
"(其他标签页)"
|
|
1155
|
-
]
|
|
1156
|
-
}) : null,
|
|
1157
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1158
|
-
className: "dsh-channels-cfg__help",
|
|
1159
|
-
children: dirty ? PANEL_COPY.saveHintDirty : PANEL_COPY.saveHintClean
|
|
1407
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1408
|
+
onClick: onSave,
|
|
1409
|
+
disabled,
|
|
1410
|
+
children: busy === "save" ? PANEL_COPY.saving : PANEL_COPY.save
|
|
1160
1411
|
})
|
|
1161
1412
|
]
|
|
1162
1413
|
});
|
|
1163
1414
|
}
|
|
1164
|
-
/**
|
|
1165
|
-
function TabHelp(props) {
|
|
1166
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1167
|
-
className: "dsh-channels-cfg__tabHelp",
|
|
1168
|
-
children: props.text
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
/** Transient page-level banner (save / credentials / reconnect / catalog). */
|
|
1415
|
+
/** Transient banner (save / credentials / reconnect / catalog / access). */
|
|
1172
1416
|
function NoticeBar(props) {
|
|
1173
|
-
const {
|
|
1417
|
+
const { kind, text } = props;
|
|
1174
1418
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1175
|
-
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${
|
|
1176
|
-
role: "status",
|
|
1419
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${kind}`,
|
|
1420
|
+
role: kind === "error" ? "alert" : "status",
|
|
1177
1421
|
"aria-live": "polite",
|
|
1178
|
-
children:
|
|
1422
|
+
children: text
|
|
1179
1423
|
});
|
|
1180
1424
|
}
|
|
1181
|
-
/** Muted
|
|
1182
|
-
function
|
|
1425
|
+
/** Muted row inside a list (empty or not-yet-loaded state). */
|
|
1426
|
+
function EmptyRow(props) {
|
|
1183
1427
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1184
|
-
className: "dsh-channels-
|
|
1428
|
+
className: "dsh-channels-cfg__emptyRow",
|
|
1185
1429
|
children: props.text
|
|
1186
1430
|
});
|
|
1187
1431
|
}
|
|
1188
|
-
/** Human label for the official SDK's WebSocket lifecycle state. */
|
|
1189
|
-
function connectionStateLabel(state) {
|
|
1190
|
-
switch (state) {
|
|
1191
|
-
case "connected": return {
|
|
1192
|
-
text: PANEL_COPY.phaseConnected,
|
|
1193
|
-
tone: "ok"
|
|
1194
|
-
};
|
|
1195
|
-
case "connecting": return {
|
|
1196
|
-
text: PANEL_COPY.phaseConnecting,
|
|
1197
|
-
tone: "warn"
|
|
1198
|
-
};
|
|
1199
|
-
case "reconnecting": return {
|
|
1200
|
-
text: PANEL_COPY.phaseConnecting,
|
|
1201
|
-
tone: "warn"
|
|
1202
|
-
};
|
|
1203
|
-
case "failed": return {
|
|
1204
|
-
text: PANEL_COPY.phaseError,
|
|
1205
|
-
tone: "bad"
|
|
1206
|
-
};
|
|
1207
|
-
case "idle": return {
|
|
1208
|
-
text: PANEL_COPY.phaseIdle,
|
|
1209
|
-
tone: "muted"
|
|
1210
|
-
};
|
|
1211
|
-
default: return {
|
|
1212
|
-
text: state ?? PANEL_COPY.unknownValue,
|
|
1213
|
-
tone: "muted"
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
1432
|
//#endregion
|
|
1218
|
-
//#region src/client/panel/
|
|
1433
|
+
//#region src/client/panel/whitelist.tsx
|
|
1219
1434
|
/**
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
1223
|
-
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
1224
|
-
* 管理员与准入模式由同 tab 上方「准入与授权」字段表单编辑,这里不再重复。
|
|
1435
|
+
* 白名单列表:群白名单、用户白名单与知识库白名单共用的条目行、添加表单
|
|
1436
|
+
* (点「+ 添加」才展开)与待处理申请行。
|
|
1225
1437
|
*
|
|
1226
|
-
* @module dsh-channels/client/panel/
|
|
1438
|
+
* @module dsh-channels/client/panel/whitelist
|
|
1227
1439
|
*/
|
|
1228
|
-
/** Local `
|
|
1440
|
+
/** Local `MM-DD HH:mm` rendering for authorization timestamps. */
|
|
1229
1441
|
function formatAccessDate(ms) {
|
|
1230
1442
|
if (!Number.isFinite(ms) || ms <= 0) return PANEL_COPY.unknownValue;
|
|
1231
1443
|
const date = new Date(ms);
|
|
1232
1444
|
const pad = (value) => String(value).padStart(2, "0");
|
|
1233
|
-
return `${
|
|
1445
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1234
1446
|
}
|
|
1235
1447
|
/** Which authorization entry produced one grant. */
|
|
1236
|
-
function
|
|
1448
|
+
function sourceLabel(source) {
|
|
1237
1449
|
switch (source) {
|
|
1238
1450
|
case "command": return PANEL_COPY.accessSourceCommand;
|
|
1239
1451
|
case "card": return PANEL_COPY.accessSourceCard;
|
|
1240
1452
|
default: return PANEL_COPY.accessSourcePanel;
|
|
1241
1453
|
}
|
|
1242
1454
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
setUserName("");
|
|
1455
|
+
/** Round initial badge in front of a whitelist entry. */
|
|
1456
|
+
function Avatar(props) {
|
|
1457
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1458
|
+
className: "dsh-channels-cfg__avatar",
|
|
1459
|
+
"aria-hidden": "true",
|
|
1460
|
+
children: Array.from(props.text)[0] ?? "?"
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* One titled whitelist with remove buttons and a collapsed add form.
|
|
1465
|
+
* `onAdd` resolves true when the entry was added (the form then closes).
|
|
1466
|
+
*/
|
|
1467
|
+
function Whitelist(props) {
|
|
1468
|
+
const { title, countLabel, entries, avatar, empty, loaded, addLabel, idPlaceholder, namePlaceholder, footnote, disabled, working, onAdd, onRemove } = props;
|
|
1469
|
+
const [adding, setAdding] = (0, react.useState)(false);
|
|
1470
|
+
const [id, setId] = (0, react.useState)("");
|
|
1471
|
+
const [name, setName] = (0, react.useState)("");
|
|
1472
|
+
const submit = async () => {
|
|
1473
|
+
if (id.trim() === "") return;
|
|
1474
|
+
if (await onAdd(id, name)) {
|
|
1475
|
+
setId("");
|
|
1476
|
+
setName("");
|
|
1477
|
+
setAdding(false);
|
|
1267
1478
|
}
|
|
1268
1479
|
};
|
|
1269
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
1270
|
-
|
|
1480
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1481
|
+
title: loaded ? `${title} · ${countLabel ?? entries.length}` : title,
|
|
1482
|
+
footnote,
|
|
1271
1483
|
children: [
|
|
1272
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}, group.chatId))
|
|
1339
|
-
}),
|
|
1340
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1341
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1342
|
-
children: [
|
|
1343
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1344
|
-
type: "text",
|
|
1345
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1346
|
-
value: groupChatId,
|
|
1347
|
-
placeholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1348
|
-
"aria-label": PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1349
|
-
disabled,
|
|
1350
|
-
onChange: (event) => setGroupChatId(event.target.value)
|
|
1351
|
-
}),
|
|
1352
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1353
|
-
type: "text",
|
|
1354
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1355
|
-
value: groupName,
|
|
1356
|
-
placeholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1357
|
-
"aria-label": PANEL_COPY.accessGroupNamePlaceholder,
|
|
1358
|
-
disabled,
|
|
1359
|
-
onChange: (event) => setGroupName(event.target.value)
|
|
1360
|
-
}),
|
|
1361
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1362
|
-
type: "button",
|
|
1363
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1364
|
-
disabled: disabled || groupChatId.trim() === "",
|
|
1365
|
-
onClick: () => {
|
|
1366
|
-
submitGroup();
|
|
1367
|
-
},
|
|
1368
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup
|
|
1369
|
-
})
|
|
1370
|
-
]
|
|
1371
|
-
})
|
|
1372
|
-
]
|
|
1373
|
-
}),
|
|
1374
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1375
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1376
|
-
children: [
|
|
1377
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1378
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1379
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1380
|
-
className: "dsh-channels-cfg__label",
|
|
1381
|
-
children: PANEL_COPY.accessUsersTitle
|
|
1382
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1383
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1384
|
-
children: [users.length, " 个"]
|
|
1385
|
-
})]
|
|
1386
|
-
}),
|
|
1387
|
-
users.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1388
|
-
className: "dsh-channels-cfg__help",
|
|
1389
|
-
children: PANEL_COPY.accessUsersEmpty
|
|
1390
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1391
|
-
className: "dsh-channels-cfg__accessList",
|
|
1392
|
-
children: users.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1393
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1394
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1395
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1396
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.userId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1397
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1398
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1399
|
-
className: "dsh-channels-cfg__mono",
|
|
1400
|
-
children: user.userId
|
|
1401
|
-
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1402
|
-
})]
|
|
1403
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1404
|
-
type: "button",
|
|
1405
|
-
className: "dsh-channels-cfg__btn",
|
|
1406
|
-
disabled,
|
|
1407
|
-
onClick: () => onRemoveUser(user.userId),
|
|
1408
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1409
|
-
})]
|
|
1410
|
-
}, user.userId))
|
|
1411
|
-
}),
|
|
1412
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1413
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1414
|
-
children: [
|
|
1415
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1416
|
-
type: "text",
|
|
1417
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1418
|
-
value: userUserId,
|
|
1419
|
-
placeholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
1420
|
-
"aria-label": PANEL_COPY.accessUserIdPlaceholder,
|
|
1421
|
-
disabled,
|
|
1422
|
-
onChange: (event) => setUserUserId(event.target.value)
|
|
1423
|
-
}),
|
|
1424
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1425
|
-
type: "text",
|
|
1426
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1427
|
-
value: userName,
|
|
1428
|
-
placeholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
1429
|
-
"aria-label": PANEL_COPY.accessUserNamePlaceholder,
|
|
1430
|
-
disabled,
|
|
1431
|
-
onChange: (event) => setUserName(event.target.value)
|
|
1432
|
-
}),
|
|
1433
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1434
|
-
type: "button",
|
|
1435
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1436
|
-
disabled: disabled || userUserId.trim() === "",
|
|
1437
|
-
onClick: () => {
|
|
1438
|
-
submitUser();
|
|
1439
|
-
},
|
|
1440
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddUser
|
|
1441
|
-
})
|
|
1442
|
-
]
|
|
1443
|
-
})
|
|
1444
|
-
]
|
|
1445
|
-
}),
|
|
1446
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1447
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1448
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1449
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1450
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1451
|
-
className: "dsh-channels-cfg__label",
|
|
1452
|
-
children: PANEL_COPY.accessRequestsTitle
|
|
1453
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1454
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1455
|
-
children: [requests.length, " 条"]
|
|
1456
|
-
})]
|
|
1457
|
-
}), requests.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1458
|
-
className: "dsh-channels-cfg__help",
|
|
1459
|
-
children: PANEL_COPY.accessRequestsEmpty
|
|
1460
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1461
|
-
className: "dsh-channels-cfg__accessList",
|
|
1462
|
-
children: requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1463
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1464
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1465
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1466
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1467
|
-
request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser,
|
|
1468
|
-
":",
|
|
1469
|
-
request.name ?? request.targetId
|
|
1470
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1471
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1472
|
-
children: [
|
|
1473
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1474
|
-
className: "dsh-channels-cfg__mono",
|
|
1475
|
-
children: request.targetId
|
|
1476
|
-
}),
|
|
1477
|
-
` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`,
|
|
1478
|
-
request.kind === "user" && request.requester !== void 0 ? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}` : "",
|
|
1479
|
-
request.kind === "user" && request.groupName !== void 0 ? ` · ${request.groupName}` : request.kind === "user" && request.chatId !== void 0 ? ` · ${request.chatId}` : ""
|
|
1480
|
-
]
|
|
1481
|
-
})]
|
|
1482
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1483
|
-
className: "dsh-channels-cfg__accessActions",
|
|
1484
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1485
|
-
type: "button",
|
|
1486
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1487
|
-
disabled,
|
|
1488
|
-
onClick: () => onResolveRequest(request.token, "allow"),
|
|
1489
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1490
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1491
|
-
type: "button",
|
|
1492
|
-
className: "dsh-channels-cfg__btn",
|
|
1493
|
-
disabled,
|
|
1494
|
-
onClick: () => onResolveRequest(request.token, "deny"),
|
|
1495
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1496
|
-
})]
|
|
1497
|
-
})]
|
|
1498
|
-
}, request.token))
|
|
1499
|
-
})]
|
|
1484
|
+
!loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: PANEL_COPY.accessNotLoaded }) : null,
|
|
1485
|
+
loaded && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: empty }) : null,
|
|
1486
|
+
entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1487
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, { text: avatar ?? entry.name ?? entry.id }),
|
|
1488
|
+
label: entry.name ?? entry.id,
|
|
1489
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1491
|
+
className: "dsh-channels-cfg__mono",
|
|
1492
|
+
children: entry.id
|
|
1493
|
+
}),
|
|
1494
|
+
" · ",
|
|
1495
|
+
sourceLabel(entry.source)
|
|
1496
|
+
] }),
|
|
1497
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1498
|
+
type: "button",
|
|
1499
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
1500
|
+
disabled,
|
|
1501
|
+
onClick: () => onRemove(entry.id),
|
|
1502
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1503
|
+
})
|
|
1504
|
+
}, entry.id)),
|
|
1505
|
+
!loaded ? null : adding ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1506
|
+
className: "dsh-channels-cfg__addRow",
|
|
1507
|
+
children: [
|
|
1508
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1509
|
+
type: "text",
|
|
1510
|
+
className: "dsh-channels-cfg__input",
|
|
1511
|
+
value: id,
|
|
1512
|
+
placeholder: idPlaceholder,
|
|
1513
|
+
"aria-label": idPlaceholder,
|
|
1514
|
+
disabled,
|
|
1515
|
+
onChange: (event) => setId(event.target.value)
|
|
1516
|
+
}),
|
|
1517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1518
|
+
type: "text",
|
|
1519
|
+
className: "dsh-channels-cfg__input",
|
|
1520
|
+
value: name,
|
|
1521
|
+
placeholder: namePlaceholder,
|
|
1522
|
+
"aria-label": namePlaceholder,
|
|
1523
|
+
disabled,
|
|
1524
|
+
onChange: (event) => setName(event.target.value)
|
|
1525
|
+
}),
|
|
1526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1527
|
+
type: "button",
|
|
1528
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1529
|
+
disabled: disabled || id.trim() === "",
|
|
1530
|
+
onClick: () => {
|
|
1531
|
+
submit();
|
|
1532
|
+
},
|
|
1533
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAdd
|
|
1534
|
+
}),
|
|
1535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1536
|
+
type: "button",
|
|
1537
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1538
|
+
onClick: () => setAdding(false),
|
|
1539
|
+
children: PANEL_COPY.cancel
|
|
1540
|
+
})
|
|
1541
|
+
]
|
|
1542
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1543
|
+
className: "dsh-channels-cfg__addRow",
|
|
1544
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1545
|
+
type: "button",
|
|
1546
|
+
className: "dsh-channels-cfg__link",
|
|
1547
|
+
disabled,
|
|
1548
|
+
onClick: () => setAdding(true),
|
|
1549
|
+
children: ["+ ", addLabel]
|
|
1500
1550
|
})
|
|
1501
|
-
] }),
|
|
1502
|
-
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1503
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
1504
|
-
role: "status",
|
|
1505
|
-
"aria-live": "polite",
|
|
1506
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice })
|
|
1507
|
-
}),
|
|
1508
|
-
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1509
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1510
|
-
role: "alert",
|
|
1511
|
-
children: error
|
|
1512
|
-
}),
|
|
1513
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1514
|
-
className: "dsh-channels-cfg__help",
|
|
1515
|
-
children: PANEL_COPY.accessHints
|
|
1516
|
-
}),
|
|
1517
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1518
|
-
className: "dsh-channels-cfg__help",
|
|
1519
|
-
children: PANEL_COPY.accessChatIdHint
|
|
1520
1551
|
})
|
|
1521
1552
|
]
|
|
1522
1553
|
});
|
|
1523
1554
|
}
|
|
1555
|
+
/** Pending authorization requests, settled with the same resolver as the DM card. */
|
|
1556
|
+
function RequestList(props) {
|
|
1557
|
+
const { requests, disabled, working, onResolve } = props;
|
|
1558
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
1559
|
+
title: `${PANEL_COPY.accessRequestsTitle} · ${requests.length}`,
|
|
1560
|
+
tone: "warn",
|
|
1561
|
+
children: requests.map((request) => {
|
|
1562
|
+
const target = request.name ?? request.targetId;
|
|
1563
|
+
const where = request.kind === "user" && (request.groupName ?? request.chatId) !== void 0 ? fillCopy(PANEL_COPY.accessRequestIn, { group: request.groupName ?? request.chatId ?? "" }) : "";
|
|
1564
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1565
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, { text: target }),
|
|
1566
|
+
label: `${target} ${where}${request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser}`,
|
|
1567
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1568
|
+
className: "dsh-channels-cfg__mono",
|
|
1569
|
+
children: request.targetId
|
|
1570
|
+
}), ` · ${formatAccessDate(request.createdAt)} · ${fillCopy(PANEL_COPY.accessRequestExpires, { date: formatAccessDate(request.expiresAt) })}`] }),
|
|
1571
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1572
|
+
className: "dsh-channels-cfg__inputRow",
|
|
1573
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1574
|
+
type: "button",
|
|
1575
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1576
|
+
disabled,
|
|
1577
|
+
onClick: () => onResolve(request.token, "deny"),
|
|
1578
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1579
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1580
|
+
type: "button",
|
|
1581
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1582
|
+
disabled,
|
|
1583
|
+
onClick: () => onResolve(request.token, "allow"),
|
|
1584
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1585
|
+
})]
|
|
1586
|
+
})
|
|
1587
|
+
}, request.token);
|
|
1588
|
+
})
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1524
1591
|
//#endregion
|
|
1525
1592
|
//#region src/client/panel/access-tab.tsx
|
|
1526
1593
|
/**
|
|
1527
|
-
* 「访问授权」tab
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1594
|
+
* 「访问授权」tab:待处理申请(有才显示,置顶)、准入字段(调试模式 /
|
|
1595
|
+
* 管理员 / 申请有效期)与群、用户白名单。字段保存走全局 draft,白名单增删
|
|
1596
|
+
* 与申请处理是独立即时 mutation。
|
|
1597
|
+
*
|
|
1598
|
+
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
1599
|
+
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
1530
1600
|
*
|
|
1531
1601
|
* @module dsh-channels/client/panel/access-tab
|
|
1532
1602
|
*/
|
|
1533
1603
|
function AccessTab(props) {
|
|
1534
1604
|
const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1605
|
+
const { access } = form.panel;
|
|
1606
|
+
const working = form.busy === "access";
|
|
1607
|
+
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
1608
|
+
const ownerAdmin = access.mode === "guarded" && access.admin.source === "owner" ? access.admin.userId : void 0;
|
|
1609
|
+
const fields = ownerAdmin === void 0 ? form.fields : form.fields.map((field) => field.key === "adminUserId" ? {
|
|
1610
|
+
...field,
|
|
1611
|
+
summary: fillCopy(PANEL_COPY.accessAdminOwnerSummary, { id: ownerAdmin })
|
|
1612
|
+
} : field);
|
|
1535
1613
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1536
1614
|
className: "dsh-channels-cfg__tabBody",
|
|
1537
1615
|
children: [
|
|
1538
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1539
|
-
|
|
1540
|
-
|
|
1616
|
+
accessNotice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1617
|
+
kind: "ok",
|
|
1618
|
+
text: accessNotice
|
|
1619
|
+
}),
|
|
1620
|
+
accessError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1621
|
+
kind: "error",
|
|
1622
|
+
text: accessError
|
|
1623
|
+
}),
|
|
1624
|
+
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1625
|
+
kind: "error",
|
|
1626
|
+
text: access.admin.ownerLookupError === void 0 ? PANEL_COPY.accessAdminMissingWarning : `${PANEL_COPY.accessAdminMissingWarning}(${access.admin.ownerLookupError})`
|
|
1627
|
+
}) : null,
|
|
1628
|
+
access.loaded && access.requests.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestList, {
|
|
1629
|
+
requests: access.requests,
|
|
1630
|
+
disabled,
|
|
1631
|
+
working,
|
|
1632
|
+
onResolve: onResolveRequest
|
|
1633
|
+
}) : null,
|
|
1634
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
1635
|
+
form: {
|
|
1636
|
+
...form,
|
|
1637
|
+
fields
|
|
1638
|
+
},
|
|
1541
1639
|
groups: TAB_FIELD_GROUPS.access
|
|
1542
1640
|
}),
|
|
1543
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1544
|
-
|
|
1545
|
-
|
|
1641
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
1642
|
+
title: PANEL_COPY.accessGroupsTitle,
|
|
1643
|
+
entries: access.groups.map((group) => ({
|
|
1644
|
+
id: group.chatId,
|
|
1645
|
+
name: group.name,
|
|
1646
|
+
source: group.source
|
|
1647
|
+
})),
|
|
1648
|
+
avatar: "群",
|
|
1649
|
+
empty: PANEL_COPY.accessGroupsEmpty,
|
|
1650
|
+
loaded: access.loaded,
|
|
1651
|
+
addLabel: PANEL_COPY.accessAddGroup,
|
|
1652
|
+
idPlaceholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1653
|
+
namePlaceholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1654
|
+
disabled,
|
|
1655
|
+
working,
|
|
1656
|
+
onAdd: onAddGroup,
|
|
1657
|
+
onRemove: onRemoveGroup
|
|
1546
1658
|
}),
|
|
1547
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
1660
|
+
title: PANEL_COPY.accessUsersTitle,
|
|
1661
|
+
entries: access.users.map((user) => ({
|
|
1662
|
+
id: user.userId,
|
|
1663
|
+
name: user.name,
|
|
1664
|
+
source: user.source
|
|
1665
|
+
})),
|
|
1666
|
+
empty: PANEL_COPY.accessUsersEmpty,
|
|
1667
|
+
loaded: access.loaded,
|
|
1668
|
+
addLabel: PANEL_COPY.accessAddUser,
|
|
1669
|
+
idPlaceholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
1670
|
+
namePlaceholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
1671
|
+
disabled,
|
|
1672
|
+
working,
|
|
1673
|
+
onAdd: onAddUser,
|
|
1674
|
+
onRemove: onRemoveUser
|
|
1558
1675
|
})
|
|
1559
1676
|
]
|
|
1560
1677
|
});
|
|
1561
1678
|
}
|
|
1562
1679
|
//#endregion
|
|
1563
|
-
//#region src/client/panel/
|
|
1680
|
+
//#region src/client/panel/credentials-card.tsx
|
|
1564
1681
|
/**
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1682
|
+
* 凭据行:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示「已配置 /
|
|
1683
|
+
* 未配置」,两个方向都不回显已存值。已配置时只显示「更换 / 移除」,点「更换」
|
|
1684
|
+
* 才出现输入框。保存动作由页面外壳持有(凭据保存独立于配置 draft)。
|
|
1567
1685
|
*
|
|
1568
|
-
* @module dsh-channels/client/panel/
|
|
1686
|
+
* @module dsh-channels/client/panel/credentials-card
|
|
1569
1687
|
*/
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1688
|
+
/** One credential slot as a setting row with its own input and actions. */
|
|
1689
|
+
function CredentialRow(props) {
|
|
1690
|
+
const { label, summary, state, value, secret, placeholder, issue, busy, disabled, onChange, onSave, onClear } = props;
|
|
1691
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
1692
|
+
const previousBusy = (0, react.useRef)(busy);
|
|
1693
|
+
(0, react.useEffect)(() => {
|
|
1694
|
+
if (previousBusy.current === "credentials" && busy === null) setEditing(false);
|
|
1695
|
+
previousBusy.current = busy;
|
|
1696
|
+
}, [busy]);
|
|
1697
|
+
const showInput = !state.configured || editing || value !== "";
|
|
1698
|
+
const source = `${PANEL_COPY.credentialStore} · ${state.ref}${state.source === void 0 ? "" : ` · ${state.source}`}`;
|
|
1699
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1700
|
+
label,
|
|
1701
|
+
tags: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
1702
|
+
tone: state.configured ? "ok" : "muted",
|
|
1703
|
+
children: state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing
|
|
1704
|
+
}),
|
|
1705
|
+
summary: summary === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1706
|
+
className: "dsh-channels-cfg__mono",
|
|
1707
|
+
children: source
|
|
1708
|
+
}) : summary,
|
|
1709
|
+
error: issue?.message ?? (state.writable ? void 0 : PANEL_COPY.credentialReadOnly),
|
|
1710
|
+
control: showInput ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1711
|
+
className: "dsh-channels-cfg__inputRow",
|
|
1712
|
+
children: [
|
|
1713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1714
|
+
type: secret ? "password" : "text",
|
|
1715
|
+
className: `dsh-channels-cfg__input dsh-channels-cfg__input--text${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`,
|
|
1716
|
+
value,
|
|
1717
|
+
placeholder,
|
|
1718
|
+
autoComplete: "new-password",
|
|
1719
|
+
"aria-label": label,
|
|
1720
|
+
disabled,
|
|
1721
|
+
onChange: (event) => onChange(event.target.value)
|
|
1722
|
+
}),
|
|
1723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1724
|
+
type: "button",
|
|
1725
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1726
|
+
disabled: disabled || value.trim() === "",
|
|
1727
|
+
onClick: onSave,
|
|
1728
|
+
children: busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials
|
|
1729
|
+
}),
|
|
1730
|
+
state.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1731
|
+
type: "button",
|
|
1732
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1733
|
+
disabled: busy !== null,
|
|
1734
|
+
onClick: () => {
|
|
1735
|
+
setEditing(false);
|
|
1736
|
+
onChange("");
|
|
1737
|
+
},
|
|
1738
|
+
children: PANEL_COPY.cancel
|
|
1739
|
+
}) : null
|
|
1740
|
+
]
|
|
1741
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1742
|
+
className: "dsh-channels-cfg__inputRow",
|
|
1743
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1744
|
+
type: "button",
|
|
1745
|
+
className: "dsh-channels-cfg__btn",
|
|
1746
|
+
disabled,
|
|
1747
|
+
onClick: () => setEditing(true),
|
|
1748
|
+
children: PANEL_COPY.replaceCredential
|
|
1749
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1750
|
+
type: "button",
|
|
1751
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
1752
|
+
disabled,
|
|
1753
|
+
onClick: onClear,
|
|
1754
|
+
children: busy === "credentials-clear" ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential
|
|
1755
|
+
})]
|
|
1756
|
+
})
|
|
1591
1757
|
});
|
|
1592
1758
|
}
|
|
1593
|
-
/**
|
|
1594
|
-
function
|
|
1595
|
-
const { panel,
|
|
1596
|
-
const {
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
children: [
|
|
1628
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1629
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1630
|
-
children: field.label
|
|
1631
|
-
}),
|
|
1632
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1633
|
-
className: "dsh-channels-cfg__kvValue",
|
|
1634
|
-
children: field.value
|
|
1635
|
-
}),
|
|
1636
|
-
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1637
|
-
className: "dsh-channels-cfg__help",
|
|
1638
|
-
children: field.help
|
|
1639
|
-
})
|
|
1640
|
-
]
|
|
1641
|
-
}, field.key)),
|
|
1642
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1643
|
-
className: "dsh-channels-cfg__kv",
|
|
1644
|
-
children: [
|
|
1645
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1646
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1647
|
-
children: PANEL_COPY.overlayPath
|
|
1648
|
-
}),
|
|
1649
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1650
|
-
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1651
|
-
children: panel.status.overlayPath
|
|
1652
|
-
}),
|
|
1653
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1654
|
-
className: "dsh-channels-cfg__help",
|
|
1655
|
-
children: PANEL_COPY.overlayPathHelp
|
|
1656
|
-
})
|
|
1657
|
-
]
|
|
1658
|
-
}),
|
|
1659
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1660
|
-
className: "dsh-channels-cfg__kv",
|
|
1661
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1662
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1663
|
-
children: PANEL_COPY.version
|
|
1664
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1665
|
-
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1666
|
-
children: panel.status.version
|
|
1667
|
-
})]
|
|
1668
|
-
})
|
|
1669
|
-
]
|
|
1670
|
-
}),
|
|
1671
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1672
|
-
className: "dsh-channels-cfg__actions",
|
|
1673
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1674
|
-
type: "button",
|
|
1675
|
-
className: "dsh-channels-cfg__btn",
|
|
1676
|
-
onClick: onClear,
|
|
1677
|
-
disabled: overridden === 0 || busy !== null || locked,
|
|
1678
|
-
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
1679
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1680
|
-
className: "dsh-channels-cfg__help",
|
|
1681
|
-
children: overridden === 0 ? PANEL_COPY.noOverrides : `${PANEL_COPY.overridden} ${overridden} 项`
|
|
1682
|
-
})]
|
|
1683
|
-
})
|
|
1684
|
-
]
|
|
1759
|
+
/** The Feishu application pair (App ID / App Secret). */
|
|
1760
|
+
function CredentialsSection(props) {
|
|
1761
|
+
const { panel, credentials } = props;
|
|
1762
|
+
const { appId, appSecret, issues, busy, locked, onAppIdChange, onAppSecretChange, onSave, onClear } = credentials;
|
|
1763
|
+
const appIdState = panel.config.credentials.appId;
|
|
1764
|
+
const appSecretState = panel.config.credentials.appSecret;
|
|
1765
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1766
|
+
title: PANEL_COPY.credentialsTitle,
|
|
1767
|
+
footnote: PANEL_COPY.credentialsHint,
|
|
1768
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
1769
|
+
label: PANEL_COPY.appIdLabel,
|
|
1770
|
+
state: appIdState,
|
|
1771
|
+
value: appId,
|
|
1772
|
+
secret: false,
|
|
1773
|
+
placeholder: PANEL_COPY.appIdPlaceholder,
|
|
1774
|
+
issue: issues.find((item) => item.key === "appId"),
|
|
1775
|
+
busy,
|
|
1776
|
+
disabled: busy !== null || locked || !appIdState.writable,
|
|
1777
|
+
onChange: onAppIdChange,
|
|
1778
|
+
onSave,
|
|
1779
|
+
onClear: () => onClear("appId")
|
|
1780
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
1781
|
+
label: PANEL_COPY.appSecretLabel,
|
|
1782
|
+
state: appSecretState,
|
|
1783
|
+
value: appSecret,
|
|
1784
|
+
secret: true,
|
|
1785
|
+
placeholder: PANEL_COPY.appSecretPlaceholder,
|
|
1786
|
+
issue: issues.find((item) => item.key === "appSecret"),
|
|
1787
|
+
busy,
|
|
1788
|
+
disabled: busy !== null || locked || !appSecretState.writable,
|
|
1789
|
+
onChange: onAppSecretChange,
|
|
1790
|
+
onSave,
|
|
1791
|
+
onClear: () => onClear("appSecret")
|
|
1792
|
+
})]
|
|
1685
1793
|
});
|
|
1686
1794
|
}
|
|
1687
1795
|
//#endregion
|
|
1688
|
-
//#region src/client/panel/
|
|
1796
|
+
//#region src/client/panel/advanced-tab.tsx
|
|
1689
1797
|
/**
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
* (凭据保存独立于配置 draft)。
|
|
1798
|
+
* 「高级」tab:日志分析接口(监听地址字段 + 访问令牌凭据行)、SDK 日志级别
|
|
1799
|
+
* 与 Web 覆盖文件(「恢复 profile 配置」删除整个覆盖层,不走 draft)。
|
|
1693
1800
|
*
|
|
1694
|
-
* @module dsh-channels/client/panel/
|
|
1801
|
+
* @module dsh-channels/client/panel/advanced-tab
|
|
1695
1802
|
*/
|
|
1696
|
-
function
|
|
1697
|
-
const {
|
|
1698
|
-
const {
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
})
|
|
1740
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1741
|
-
className: "dsh-channels-cfg__help",
|
|
1742
|
-
children: [
|
|
1743
|
-
PANEL_COPY.credentialsRef,
|
|
1744
|
-
":",
|
|
1745
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1746
|
-
className: "dsh-channels-cfg__mono",
|
|
1747
|
-
children: state.ref
|
|
1748
|
-
}),
|
|
1749
|
-
state.source === void 0 ? "" : ` · ${state.source}`
|
|
1750
|
-
]
|
|
1751
|
-
}),
|
|
1752
|
-
issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1753
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1754
|
-
children: issue.message
|
|
1755
|
-
})
|
|
1756
|
-
]
|
|
1757
|
-
}, field);
|
|
1758
|
-
};
|
|
1759
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1760
|
-
className: "dsh-channels-cfg__card",
|
|
1761
|
-
children: [
|
|
1762
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1763
|
-
className: "dsh-channels-cfg__cardHead",
|
|
1764
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1765
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1766
|
-
children: PANEL_COPY.credentialsCard
|
|
1767
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1768
|
-
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${configured ? "ok" : "muted"}`,
|
|
1769
|
-
children: configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge
|
|
1770
|
-
})]
|
|
1771
|
-
}),
|
|
1772
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1773
|
-
className: "dsh-channels-cfg__help",
|
|
1774
|
-
children: PANEL_COPY.credentialsCardHelp
|
|
1775
|
-
}),
|
|
1776
|
-
row("appId", PANEL_COPY.appIdLabel, appIdState, appId, "text", PANEL_COPY.appIdPlaceholder, onAppIdChange),
|
|
1777
|
-
row("appSecret", PANEL_COPY.appSecretLabel, appSecretState, appSecret, "password", PANEL_COPY.appSecretPlaceholder, onAppSecretChange),
|
|
1778
|
-
writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1779
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1780
|
-
children: PANEL_COPY.credentialReadOnly
|
|
1781
|
-
}),
|
|
1782
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1783
|
-
className: "dsh-channels-cfg__footer",
|
|
1784
|
-
children: [
|
|
1785
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1786
|
-
type: "button",
|
|
1787
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1788
|
-
onClick: onSave,
|
|
1789
|
-
disabled: disabled || !pending,
|
|
1790
|
-
children: busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials
|
|
1791
|
-
}),
|
|
1792
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
1793
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1794
|
-
className: "dsh-channels-cfg__help",
|
|
1795
|
-
children: PANEL_COPY.credentialsPendingHint
|
|
1796
|
-
})
|
|
1797
|
-
]
|
|
1803
|
+
function AdvancedTab(props) {
|
|
1804
|
+
const { form, analyzeToken, onClear } = props;
|
|
1805
|
+
const { panel, busy, locked } = form;
|
|
1806
|
+
const tokenState = panel.config.credentials.analyzeToken;
|
|
1807
|
+
const overridden = panel.config.overriddenKeys.length;
|
|
1808
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1809
|
+
className: "dsh-channels-cfg__tabBody",
|
|
1810
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
1811
|
+
form,
|
|
1812
|
+
groups: TAB_FIELD_GROUPS.advanced,
|
|
1813
|
+
extra: { analyze: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
1814
|
+
label: PANEL_COPY.analyzeTokenLabel,
|
|
1815
|
+
summary: PANEL_COPY.analyzeTokenSummary,
|
|
1816
|
+
state: tokenState,
|
|
1817
|
+
value: analyzeToken.value,
|
|
1818
|
+
secret: true,
|
|
1819
|
+
placeholder: PANEL_COPY.analyzeTokenPlaceholder,
|
|
1820
|
+
issue: analyzeToken.issues.find((item) => item.key === "analyzeToken"),
|
|
1821
|
+
busy: analyzeToken.busy,
|
|
1822
|
+
disabled: analyzeToken.busy !== null || analyzeToken.locked || !tokenState.writable,
|
|
1823
|
+
onChange: analyzeToken.onChange,
|
|
1824
|
+
onSave: analyzeToken.onSave,
|
|
1825
|
+
onClear: analyzeToken.onClear
|
|
1826
|
+
}) }
|
|
1827
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
1828
|
+
title: PANEL_COPY.overlayTitle,
|
|
1829
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1830
|
+
label: PANEL_COPY.overlayPath,
|
|
1831
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1832
|
+
className: "dsh-channels-cfg__mono",
|
|
1833
|
+
children: panel.status.overlayPath
|
|
1834
|
+
}),
|
|
1835
|
+
details: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1836
|
+
PANEL_COPY.overlayPathHelp,
|
|
1837
|
+
" ",
|
|
1838
|
+
overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })
|
|
1839
|
+
] }),
|
|
1840
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1841
|
+
type: "button",
|
|
1842
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1843
|
+
onClick: onClear,
|
|
1844
|
+
disabled: overridden === 0 || busy !== null || locked,
|
|
1845
|
+
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
1846
|
+
})
|
|
1798
1847
|
})
|
|
1799
|
-
]
|
|
1848
|
+
})]
|
|
1800
1849
|
});
|
|
1801
1850
|
}
|
|
1802
1851
|
//#endregion
|
|
1803
1852
|
//#region src/client/panel/connection-tab.tsx
|
|
1804
1853
|
/**
|
|
1805
|
-
* 「连接」tab:按 Host
|
|
1854
|
+
* 「连接」tab:按 Host 派生的生命周期阶段渲染。
|
|
1806
1855
|
*
|
|
1807
|
-
* - unconfigured
|
|
1808
|
-
* - idle
|
|
1809
|
-
* -
|
|
1810
|
-
* -
|
|
1811
|
-
* - connecting:禁用按钮显示「连接中…」
|
|
1812
|
-
* - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
|
|
1856
|
+
* - unconfigured:上手三步 + 应用凭据
|
|
1857
|
+
* - idle / error / connecting / connected:状态块(当前阶段的唯一操作 + 运行数字)
|
|
1858
|
+
* - 连接后:自检(权限缺口可展开、斜杠指令面板同步结果)
|
|
1859
|
+
* - 有需重连才生效的已保存配置:顶部「重启连接」横幅
|
|
1813
1860
|
*
|
|
1814
1861
|
* @module dsh-channels/client/panel/connection-tab
|
|
1815
1862
|
*/
|
|
1816
1863
|
function ConnectionTab(props) {
|
|
1817
1864
|
const { panel, status, busy, credentials, onReconnect } = props;
|
|
1865
|
+
const checks = status.scopeCheck !== void 0 || status.slashCommandSync !== void 0;
|
|
1818
1866
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1819
1867
|
className: "dsh-channels-cfg__tabBody",
|
|
1820
1868
|
children: [
|
|
1821
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.connectionTabHelp }),
|
|
1822
1869
|
panel.config.restartPendingKeys.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartBanner, {
|
|
1823
1870
|
panel,
|
|
1824
1871
|
busy,
|
|
1825
1872
|
locked: status.busy,
|
|
1826
1873
|
onReconnect
|
|
1827
1874
|
}),
|
|
1828
|
-
status.phase === "unconfigured" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1875
|
+
status.phase === "unconfigured" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OnboardingSection, { status }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusBlock, {
|
|
1829
1876
|
status,
|
|
1830
1877
|
busy,
|
|
1831
1878
|
onReconnect
|
|
1832
1879
|
}),
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1880
|
+
checks ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1881
|
+
title: PANEL_COPY.checksTitle,
|
|
1882
|
+
children: [status.scopeCheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeCheckRow, { check: status.scopeCheck }), status.slashCommandSync === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlashCommandSyncRow, { sync: status.slashCommandSync })]
|
|
1883
|
+
}) : null,
|
|
1884
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsSection, {
|
|
1836
1885
|
panel,
|
|
1837
1886
|
credentials
|
|
1838
1887
|
})
|
|
@@ -1840,7 +1889,7 @@ window.__ModuleLoader__.load({
|
|
|
1840
1889
|
});
|
|
1841
1890
|
}
|
|
1842
1891
|
/** First-run guide: create the app, fill the credentials, save and wait. */
|
|
1843
|
-
function
|
|
1892
|
+
function OnboardingSection(props) {
|
|
1844
1893
|
const { status } = props;
|
|
1845
1894
|
const steps = [
|
|
1846
1895
|
{
|
|
@@ -1856,22 +1905,12 @@ window.__ModuleLoader__.load({
|
|
|
1856
1905
|
text: PANEL_COPY.onboardingStep3Text
|
|
1857
1906
|
}
|
|
1858
1907
|
];
|
|
1859
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1860
|
-
className: "dsh-channels-
|
|
1908
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1909
|
+
className: "dsh-channels-cfg__hero",
|
|
1861
1910
|
children: [
|
|
1862
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1863
|
-
className: "dsh-channels-
|
|
1864
|
-
children:
|
|
1865
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1866
|
-
children: PANEL_COPY.onboardingTitle
|
|
1867
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1868
|
-
className: "dsh-channels-cfg__badge dsh-channels-cfg__badge--muted",
|
|
1869
|
-
children: PANEL_COPY.phaseUnconfigured
|
|
1870
|
-
})]
|
|
1871
|
-
}),
|
|
1872
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1873
|
-
className: "dsh-channels-cfg__help",
|
|
1874
|
-
children: PANEL_COPY.onboardingIntro
|
|
1911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1912
|
+
className: "dsh-channels-cfg__heroTop",
|
|
1913
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.onboardingTitle })
|
|
1875
1914
|
}),
|
|
1876
1915
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
1877
1916
|
className: "dsh-channels-cfg__steps",
|
|
@@ -1886,50 +1925,29 @@ window.__ModuleLoader__.load({
|
|
|
1886
1925
|
className: "dsh-channels-cfg__stepTitle",
|
|
1887
1926
|
children: step.title
|
|
1888
1927
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1889
|
-
className: "dsh-channels-
|
|
1928
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
1890
1929
|
children: step.text
|
|
1891
1930
|
})]
|
|
1892
1931
|
})]
|
|
1893
1932
|
}, step.title))
|
|
1894
1933
|
}),
|
|
1895
|
-
status.missingConfig.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1896
|
-
className: "dsh-channels-
|
|
1897
|
-
children: [
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
}),
|
|
1903
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1904
|
-
className: "dsh-channels-cfg__help",
|
|
1905
|
-
children: PANEL_COPY.onboardingDocsHint
|
|
1934
|
+
status.missingConfig.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1935
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
1936
|
+
children: [
|
|
1937
|
+
PANEL_COPY.missingConfig,
|
|
1938
|
+
":",
|
|
1939
|
+
status.missingConfig.join("、")
|
|
1940
|
+
]
|
|
1906
1941
|
})
|
|
1907
1942
|
]
|
|
1908
1943
|
});
|
|
1909
1944
|
}
|
|
1910
|
-
/**
|
|
1911
|
-
function
|
|
1945
|
+
/** Phase, the one lifecycle action available in it, and the runtime counters. */
|
|
1946
|
+
function StatusBlock(props) {
|
|
1912
1947
|
const { status, busy, onReconnect } = props;
|
|
1913
1948
|
const badge = phaseBadge(status.phase);
|
|
1914
1949
|
const actionBusy = busy !== null || status.busy;
|
|
1915
|
-
const
|
|
1916
|
-
const facts = [
|
|
1917
|
-
{
|
|
1918
|
-
label: PANEL_COPY.statusCard,
|
|
1919
|
-
value: badge.text
|
|
1920
|
-
},
|
|
1921
|
-
{
|
|
1922
|
-
label: PANEL_COPY.connectionStateLabel,
|
|
1923
|
-
value: connection.text
|
|
1924
|
-
},
|
|
1925
|
-
{
|
|
1926
|
-
label: PANEL_COPY.botOpenId,
|
|
1927
|
-
value: status.botOpenIdTail ?? PANEL_COPY.unknownValue
|
|
1928
|
-
},
|
|
1929
|
-
{
|
|
1930
|
-
label: PANEL_COPY.uptime,
|
|
1931
|
-
value: formatUptime(status.uptimeMs)
|
|
1932
|
-
},
|
|
1950
|
+
const stats = [
|
|
1933
1951
|
{
|
|
1934
1952
|
label: PANEL_COPY.routeCount,
|
|
1935
1953
|
value: String(status.routeCount)
|
|
@@ -1941,188 +1959,167 @@ window.__ModuleLoader__.load({
|
|
|
1941
1959
|
{
|
|
1942
1960
|
label: PANEL_COPY.inflightTurnCount,
|
|
1943
1961
|
value: String(status.inflightTurnCount)
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
label: PANEL_COPY.botOpenId,
|
|
1965
|
+
value: status.botOpenIdTail ?? PANEL_COPY.unknownValue,
|
|
1966
|
+
mono: true
|
|
1944
1967
|
}
|
|
1945
1968
|
];
|
|
1946
|
-
|
|
1947
|
-
|
|
1969
|
+
let action = null;
|
|
1970
|
+
if (status.phase === "connecting") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1971
|
+
type: "button",
|
|
1972
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1973
|
+
disabled: true,
|
|
1974
|
+
children: PANEL_COPY.connecting
|
|
1975
|
+
});
|
|
1976
|
+
else if (status.phase === "idle" || status.phase === "error") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1977
|
+
type: "button",
|
|
1978
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1979
|
+
onClick: onReconnect,
|
|
1980
|
+
disabled: actionBusy,
|
|
1981
|
+
children: busy === "reconnect" ? PANEL_COPY.connecting : status.phase === "idle" ? PANEL_COPY.connect : PANEL_COPY.retry
|
|
1982
|
+
});
|
|
1983
|
+
else if (status.phase === "connected") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1984
|
+
type: "button",
|
|
1985
|
+
className: "dsh-channels-cfg__btn",
|
|
1986
|
+
onClick: onReconnect,
|
|
1987
|
+
disabled: actionBusy,
|
|
1988
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
1989
|
+
});
|
|
1990
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1991
|
+
className: "dsh-channels-cfg__hero",
|
|
1948
1992
|
children: [
|
|
1949
1993
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1950
|
-
className: "dsh-channels-
|
|
1951
|
-
children: [
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1994
|
+
className: "dsh-channels-cfg__heroTop",
|
|
1995
|
+
children: [
|
|
1996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
1997
|
+
tone: badge.tone,
|
|
1998
|
+
dot: true,
|
|
1999
|
+
children: badge.text
|
|
2000
|
+
}),
|
|
2001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: phaseSummary(status.phase) }),
|
|
2002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
2003
|
+
action
|
|
2004
|
+
]
|
|
1958
2005
|
}),
|
|
1959
2006
|
status.lastStartError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1960
2007
|
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1961
|
-
children: [
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2008
|
+
children: [
|
|
2009
|
+
PANEL_COPY.lastError,
|
|
2010
|
+
":",
|
|
2011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2012
|
+
className: "dsh-channels-cfg__mono",
|
|
2013
|
+
children: status.lastStartError
|
|
2014
|
+
})
|
|
2015
|
+
]
|
|
1965
2016
|
}),
|
|
1966
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1967
|
-
className: "dsh-channels-
|
|
1968
|
-
children:
|
|
1969
|
-
className: "dsh-channels-
|
|
1970
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1971
|
-
className: "dsh-channels-
|
|
1972
|
-
children:
|
|
1973
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1974
|
-
className: "dsh-channels-cfg__factValue",
|
|
1975
|
-
children: fact.value
|
|
2017
|
+
status.phase === "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2018
|
+
className: "dsh-channels-cfg__stats",
|
|
2019
|
+
children: stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2020
|
+
className: "dsh-channels-cfg__stat",
|
|
2021
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: stat.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2022
|
+
className: stat.mono === true ? "dsh-channels-cfg__mono" : void 0,
|
|
2023
|
+
children: stat.value
|
|
1976
2024
|
})]
|
|
1977
|
-
},
|
|
1978
|
-
}),
|
|
2025
|
+
}, stat.label))
|
|
2026
|
+
}) : null,
|
|
1979
2027
|
status.reasons.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1980
|
-
className: `dsh-channels-
|
|
2028
|
+
className: `dsh-channels-cfg__rowSummary${status.level === "unhealthy" ? " dsh-channels-cfg__textBad" : ""}`,
|
|
1981
2029
|
children: [
|
|
1982
2030
|
PANEL_COPY.healthReasons,
|
|
1983
2031
|
":",
|
|
1984
2032
|
status.reasons.join(";")
|
|
1985
2033
|
]
|
|
1986
|
-
}),
|
|
1987
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1988
|
-
className: "dsh-channels-cfg__actions",
|
|
1989
|
-
children: [
|
|
1990
|
-
status.phase === "connecting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1991
|
-
type: "button",
|
|
1992
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1993
|
-
disabled: true,
|
|
1994
|
-
children: PANEL_COPY.connecting
|
|
1995
|
-
}) : null,
|
|
1996
|
-
status.phase === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1997
|
-
type: "button",
|
|
1998
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1999
|
-
onClick: onReconnect,
|
|
2000
|
-
disabled: actionBusy,
|
|
2001
|
-
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.connect
|
|
2002
|
-
}) : null,
|
|
2003
|
-
status.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2004
|
-
type: "button",
|
|
2005
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2006
|
-
onClick: onReconnect,
|
|
2007
|
-
disabled: actionBusy,
|
|
2008
|
-
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.retry
|
|
2009
|
-
}) : null,
|
|
2010
|
-
status.phase === "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2011
|
-
type: "button",
|
|
2012
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
2013
|
-
onClick: onReconnect,
|
|
2014
|
-
disabled: actionBusy,
|
|
2015
|
-
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
2016
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2017
|
-
className: "dsh-channels-cfg__help",
|
|
2018
|
-
children: PANEL_COPY.restartWarning
|
|
2019
|
-
})] }) : null
|
|
2020
|
-
]
|
|
2021
2034
|
})
|
|
2022
2035
|
]
|
|
2023
2036
|
});
|
|
2024
2037
|
}
|
|
2025
|
-
/**
|
|
2026
|
-
function
|
|
2038
|
+
/** Status icon in front of one check row. */
|
|
2039
|
+
function CheckIcon(props) {
|
|
2040
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2041
|
+
className: `dsh-channels-cfg__checkIcon dsh-channels-cfg__checkIcon--${props.tone}`,
|
|
2042
|
+
"aria-hidden": "true",
|
|
2043
|
+
children: props.tone === "ok" ? "✓" : "!"
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
/** Post-connect scope self-check: the gap count, expandable to each scope with its console link. */
|
|
2047
|
+
function ScopeCheckRow(props) {
|
|
2027
2048
|
const { check } = props;
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2049
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2050
|
+
if (check.state === "ok") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2051
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "ok" }),
|
|
2052
|
+
label: PANEL_COPY.scopeCheckOk
|
|
2053
|
+
});
|
|
2054
|
+
if (check.state === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2055
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "bad" }),
|
|
2056
|
+
label: PANEL_COPY.scopeCheckError,
|
|
2057
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2058
|
+
className: "dsh-channels-cfg__mono",
|
|
2059
|
+
children: check.error
|
|
2060
|
+
})
|
|
2061
|
+
});
|
|
2062
|
+
const required = check.missing.filter((item) => item.level === "required").length;
|
|
2063
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2064
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: required > 0 ? "bad" : "warn" }),
|
|
2065
|
+
label: required > 0 ? fillCopy(PANEL_COPY.scopeCheckMissingRequired, { n: required }) : fillCopy(PANEL_COPY.scopeCheckMissing, { n: check.missing.length }),
|
|
2066
|
+
summary: required > 0 ? PANEL_COPY.scopeCheckPublish : PANEL_COPY.scopeCheckMissingOptional,
|
|
2067
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2068
|
+
className: "dsh-channels-cfg__inputRow",
|
|
2069
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2070
|
+
type: "button",
|
|
2071
|
+
className: "dsh-channels-cfg__more",
|
|
2072
|
+
"aria-expanded": open,
|
|
2073
|
+
onClick: () => setOpen(!open),
|
|
2074
|
+
children: open ? PANEL_COPY.collapse : PANEL_COPY.expand
|
|
2075
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2076
|
+
className: "dsh-channels-cfg__btn",
|
|
2077
|
+
href: check.consoleUrl,
|
|
2078
|
+
target: "_blank",
|
|
2079
|
+
rel: "noreferrer noopener",
|
|
2080
|
+
children: PANEL_COPY.scopeCheckOpenAll
|
|
2081
|
+
})]
|
|
2082
|
+
})
|
|
2083
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2084
|
+
className: "dsh-channels-cfg__scopeList",
|
|
2085
|
+
children: check.missing.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2087
|
+
className: "dsh-channels-cfg__mono",
|
|
2088
|
+
children: item.scopes.join(" / ")
|
|
2037
2089
|
}),
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
children: PANEL_COPY.
|
|
2041
|
-
}) : null,
|
|
2042
|
-
check.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2043
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2044
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2045
|
-
className: "dsh-channels-cfg__mono",
|
|
2046
|
-
children: check.error
|
|
2047
|
-
})]
|
|
2090
|
+
item.level === "required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2091
|
+
tone: "bad",
|
|
2092
|
+
children: PANEL_COPY.scopeCheckRequired
|
|
2048
2093
|
}) : null,
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
"] "
|
|
2060
|
-
] }),
|
|
2061
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2062
|
-
className: "dsh-channels-cfg__mono",
|
|
2063
|
-
children: item.scopes.join(" / ")
|
|
2064
|
-
}),
|
|
2065
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2066
|
-
":",
|
|
2067
|
-
item.purpose,
|
|
2068
|
-
" "
|
|
2069
|
-
] }),
|
|
2070
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2071
|
-
href: item.consoleUrl,
|
|
2072
|
-
target: "_blank",
|
|
2073
|
-
rel: "noreferrer noopener",
|
|
2074
|
-
children: PANEL_COPY.scopeCheckOpenOne
|
|
2075
|
-
})
|
|
2076
|
-
] }, item.scopes.join(",")))
|
|
2077
|
-
}),
|
|
2078
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2079
|
-
href: check.consoleUrl,
|
|
2080
|
-
target: "_blank",
|
|
2081
|
-
rel: "noreferrer noopener",
|
|
2082
|
-
children: PANEL_COPY.scopeCheckOpenAll
|
|
2083
|
-
})
|
|
2084
|
-
]
|
|
2085
|
-
}) : null
|
|
2086
|
-
]
|
|
2087
|
-
});
|
|
2094
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.purpose }),
|
|
2095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2096
|
+
className: "dsh-channels-cfg__link",
|
|
2097
|
+
href: item.consoleUrl,
|
|
2098
|
+
target: "_blank",
|
|
2099
|
+
rel: "noreferrer noopener",
|
|
2100
|
+
children: PANEL_COPY.scopeCheckOpenOne
|
|
2101
|
+
})
|
|
2102
|
+
] }, item.scopes.join(",")))
|
|
2103
|
+
}) : null] });
|
|
2088
2104
|
}
|
|
2089
|
-
/** Post-connect native slash-command panel sync:
|
|
2090
|
-
function
|
|
2105
|
+
/** Post-connect native slash-command panel sync: the command count, or the Feishu reason. */
|
|
2106
|
+
function SlashCommandSyncRow(props) {
|
|
2091
2107
|
const { sync } = props;
|
|
2092
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
PANEL_COPY.slashCommandSyncTotal,
|
|
2104
|
-
" ",
|
|
2105
|
-
sync.total,
|
|
2106
|
-
" · ",
|
|
2107
|
-
PANEL_COPY.slashCommandSyncCreated,
|
|
2108
|
-
" ",
|
|
2109
|
-
sync.created,
|
|
2110
|
-
" · ",
|
|
2111
|
-
PANEL_COPY.slashCommandSyncUpdated,
|
|
2112
|
-
" ",
|
|
2113
|
-
sync.updated,
|
|
2114
|
-
" · ",
|
|
2115
|
-
PANEL_COPY.slashCommandSyncRemoved,
|
|
2116
|
-
" ",
|
|
2117
|
-
sync.removed
|
|
2118
|
-
] })]
|
|
2119
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2120
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2121
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.slashCommandSyncError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2108
|
+
return sync.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2109
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "ok" }),
|
|
2110
|
+
label: PANEL_COPY.slashCommandSyncTitle,
|
|
2111
|
+
summary: fillCopy(PANEL_COPY.slashCommandSyncOk, { total: sync.total })
|
|
2112
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2113
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "warn" }),
|
|
2114
|
+
label: PANEL_COPY.slashCommandSyncTitle,
|
|
2115
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2116
|
+
PANEL_COPY.slashCommandSyncError,
|
|
2117
|
+
":",
|
|
2118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2122
2119
|
className: "dsh-channels-cfg__mono",
|
|
2123
2120
|
children: sync.error
|
|
2124
|
-
})
|
|
2125
|
-
})
|
|
2121
|
+
})
|
|
2122
|
+
] })
|
|
2126
2123
|
});
|
|
2127
2124
|
}
|
|
2128
2125
|
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
@@ -2131,97 +2128,82 @@ window.__ModuleLoader__.load({
|
|
|
2131
2128
|
const keys = panel.config.restartPendingKeys.map((key) => fieldLabel(panel.config.fields, key)).join("、");
|
|
2132
2129
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2133
2130
|
className: "dsh-channels-cfg__banner",
|
|
2134
|
-
children: [
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
className: "dsh-channels-cfg__help",
|
|
2145
|
-
children: PANEL_COPY.restartWarning
|
|
2146
|
-
})
|
|
2147
|
-
]
|
|
2131
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2132
|
+
className: "dsh-channels-cfg__bannerText",
|
|
2133
|
+
children: [PANEL_COPY.restartBanner, keys]
|
|
2134
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2135
|
+
type: "button",
|
|
2136
|
+
className: "dsh-channels-cfg__btn",
|
|
2137
|
+
onClick: onReconnect,
|
|
2138
|
+
disabled: busy !== null || locked,
|
|
2139
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
2140
|
+
})]
|
|
2148
2141
|
});
|
|
2149
2142
|
}
|
|
2150
2143
|
//#endregion
|
|
2151
|
-
//#region src/client/panel/
|
|
2152
|
-
/**
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
*
|
|
2157
|
-
* @module dsh-channels/client/panel/model-tab
|
|
2158
|
-
*/
|
|
2159
|
-
function ModelTab(props) {
|
|
2160
|
-
const { form, catalogBusy } = props;
|
|
2144
|
+
//#region src/client/panel/fields-tab.tsx
|
|
2145
|
+
/** Footnotes under one group's section, per group. */
|
|
2146
|
+
const FOOTNOTES = { model: PANEL_COPY.modelFootnote };
|
|
2147
|
+
function FieldsTab(props) {
|
|
2148
|
+
const { form, tab, note } = props;
|
|
2161
2149
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2162
2150
|
className: "dsh-channels-cfg__tabBody",
|
|
2163
|
-
children: [
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
groups: TAB_FIELD_GROUPS.model
|
|
2172
|
-
}),
|
|
2173
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
2174
|
-
form,
|
|
2175
|
-
tab: "model"
|
|
2176
|
-
})
|
|
2177
|
-
]
|
|
2151
|
+
children: [note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2152
|
+
className: "dsh-channels-cfg__secFoot",
|
|
2153
|
+
children: note
|
|
2154
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2155
|
+
form,
|
|
2156
|
+
groups: TAB_FIELD_GROUPS[tab],
|
|
2157
|
+
footnotes: FOOTNOTES
|
|
2158
|
+
})]
|
|
2178
2159
|
});
|
|
2179
2160
|
}
|
|
2180
2161
|
//#endregion
|
|
2181
|
-
//#region src/client/panel/
|
|
2162
|
+
//#region src/client/panel/wiki-tab.tsx
|
|
2182
2163
|
/**
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2164
|
+
* 「知识库」tab:知识库工具字段(读取 / 编辑开关、可访问范围)+ 知识库
|
|
2165
|
+
* 白名单(「可使用的人」)。字段保存走全局 draft,白名单增删是独立即时
|
|
2166
|
+
* mutation。
|
|
2185
2167
|
*
|
|
2186
|
-
* @module dsh-channels/client/panel/
|
|
2168
|
+
* @module dsh-channels/client/panel/wiki-tab
|
|
2187
2169
|
*/
|
|
2188
|
-
function
|
|
2189
|
-
const { form } = props;
|
|
2170
|
+
function WikiTab(props) {
|
|
2171
|
+
const { form, accessNotice, accessError, onAddUser, onRemoveUser } = props;
|
|
2172
|
+
const { access } = form.panel;
|
|
2173
|
+
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
2190
2174
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2191
2175
|
className: "dsh-channels-cfg__tabBody",
|
|
2192
2176
|
children: [
|
|
2193
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
groups: TAB_FIELD_GROUPS.sessions
|
|
2177
|
+
accessNotice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
2178
|
+
kind: "ok",
|
|
2179
|
+
text: accessNotice
|
|
2197
2180
|
}),
|
|
2198
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
})
|
|
2202
|
-
]
|
|
2203
|
-
});
|
|
2204
|
-
}
|
|
2205
|
-
//#endregion
|
|
2206
|
-
//#region src/client/panel/workspace-tab.tsx
|
|
2207
|
-
/**
|
|
2208
|
-
* 「工作区」tab:工作区根目录字段与保存栏。
|
|
2209
|
-
*
|
|
2210
|
-
* @module dsh-channels/client/panel/workspace-tab
|
|
2211
|
-
*/
|
|
2212
|
-
function WorkspaceTab(props) {
|
|
2213
|
-
const { form } = props;
|
|
2214
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2215
|
-
className: "dsh-channels-cfg__tabBody",
|
|
2216
|
-
children: [
|
|
2217
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.workspaceTabHelp }),
|
|
2218
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
2219
|
-
form,
|
|
2220
|
-
groups: TAB_FIELD_GROUPS.workspace
|
|
2181
|
+
accessError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
2182
|
+
kind: "error",
|
|
2183
|
+
text: accessError
|
|
2221
2184
|
}),
|
|
2222
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2223
2186
|
form,
|
|
2224
|
-
|
|
2187
|
+
groups: TAB_FIELD_GROUPS.wiki
|
|
2188
|
+
}),
|
|
2189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
2190
|
+
title: PANEL_COPY.wikiUsersTitle,
|
|
2191
|
+
countLabel: fillCopy(PANEL_COPY.wikiUsersAdminPlus, { n: access.wikiUsers.length }),
|
|
2192
|
+
entries: access.wikiUsers.map((user) => ({
|
|
2193
|
+
id: user.userId,
|
|
2194
|
+
name: user.name,
|
|
2195
|
+
source: user.source
|
|
2196
|
+
})),
|
|
2197
|
+
empty: PANEL_COPY.wikiUsersEmpty,
|
|
2198
|
+
loaded: access.loaded,
|
|
2199
|
+
addLabel: PANEL_COPY.accessAddUser,
|
|
2200
|
+
idPlaceholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
2201
|
+
namePlaceholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
2202
|
+
footnote: PANEL_COPY.wikiUsersHint,
|
|
2203
|
+
disabled,
|
|
2204
|
+
working: form.busy === "access",
|
|
2205
|
+
onAdd: onAddUser,
|
|
2206
|
+
onRemove: onRemoveUser
|
|
2225
2207
|
})
|
|
2226
2208
|
]
|
|
2227
2209
|
});
|
|
@@ -2229,8 +2211,8 @@ window.__ModuleLoader__.load({
|
|
|
2229
2211
|
//#endregion
|
|
2230
2212
|
//#region src/client/panel/index.tsx
|
|
2231
2213
|
/**
|
|
2232
|
-
* 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft
|
|
2233
|
-
* tab
|
|
2214
|
+
* 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft、顶部状态头 /
|
|
2215
|
+
* tab 导航与底部保存条的外壳。
|
|
2234
2216
|
*
|
|
2235
2217
|
* 所有 Host 交互(panel / status / catalog / config / credentials / access /
|
|
2236
2218
|
* pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
@@ -2250,6 +2232,7 @@ window.__ModuleLoader__.load({
|
|
|
2250
2232
|
const [pollError, setPollError] = (0, react.useState)(null);
|
|
2251
2233
|
const [appIdInput, setAppIdInput] = (0, react.useState)("");
|
|
2252
2234
|
const [appSecretInput, setAppSecretInput] = (0, react.useState)("");
|
|
2235
|
+
const [analyzeTokenInput, setAnalyzeTokenInput] = (0, react.useState)("");
|
|
2253
2236
|
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
2254
2237
|
const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
|
|
2255
2238
|
const [accessNotice, setAccessNotice] = (0, react.useState)(null);
|
|
@@ -2278,6 +2261,7 @@ window.__ModuleLoader__.load({
|
|
|
2278
2261
|
setDraft({});
|
|
2279
2262
|
setAppIdInput("");
|
|
2280
2263
|
setAppSecretInput("");
|
|
2264
|
+
setAnalyzeTokenInput("");
|
|
2281
2265
|
} catch (error) {
|
|
2282
2266
|
setLoad({
|
|
2283
2267
|
phase: "error",
|
|
@@ -2494,22 +2478,14 @@ window.__ModuleLoader__.load({
|
|
|
2494
2478
|
setBusy(null);
|
|
2495
2479
|
}
|
|
2496
2480
|
}, [draft, panel]);
|
|
2497
|
-
/** Drop the draft
|
|
2498
|
-
const
|
|
2481
|
+
/** Drop the whole draft (nothing is written). */
|
|
2482
|
+
const discardDraft = (0, react.useCallback)(() => {
|
|
2499
2483
|
if (panel === null) return;
|
|
2500
|
-
|
|
2501
|
-
const keys = new Set(panel.config.fields.filter((field) => groups.has(field.group)).map((field) => field.key));
|
|
2502
|
-
setDraft((previous) => {
|
|
2503
|
-
const next = { ...previous };
|
|
2504
|
-
for (const key of keys) delete next[key];
|
|
2505
|
-
return next;
|
|
2506
|
-
});
|
|
2484
|
+
setDraft({});
|
|
2507
2485
|
setNotice(null);
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
setCatalogBusy(false);
|
|
2512
|
-
}
|
|
2486
|
+
catalogTicket.current += 1;
|
|
2487
|
+
setCatalog(catalogFromFields(panel.config.fields));
|
|
2488
|
+
setCatalogBusy(false);
|
|
2513
2489
|
}, [panel]);
|
|
2514
2490
|
/**
|
|
2515
2491
|
* Open the host's native directory chooser (workspace root「选择目录…」) and
|
|
@@ -2545,6 +2521,7 @@ window.__ModuleLoader__.load({
|
|
|
2545
2521
|
}, []);
|
|
2546
2522
|
/** Drop every Web override and return the refreshed panel. */
|
|
2547
2523
|
const onClear = (0, react.useCallback)(async () => {
|
|
2524
|
+
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.clearConfirm) : true)) return;
|
|
2548
2525
|
setBusy("clear");
|
|
2549
2526
|
setNotice(null);
|
|
2550
2527
|
try {
|
|
@@ -2664,18 +2641,19 @@ window.__ModuleLoader__.load({
|
|
|
2664
2641
|
setBusy(null);
|
|
2665
2642
|
}
|
|
2666
2643
|
}, [applyAccess]);
|
|
2667
|
-
|
|
2644
|
+
/** Add one user to a whitelist; resolves true when the input should clear. */
|
|
2645
|
+
const addUser = (0, react.useCallback)(async (add, userId, name, notice) => {
|
|
2668
2646
|
setBusy("access");
|
|
2669
2647
|
setAccessError(null);
|
|
2670
2648
|
setAccessNotice(null);
|
|
2671
2649
|
try {
|
|
2672
|
-
const result = await
|
|
2650
|
+
const result = await add(userId, name);
|
|
2673
2651
|
if (!result.ok) {
|
|
2674
2652
|
setAccessError(result.error);
|
|
2675
2653
|
return false;
|
|
2676
2654
|
}
|
|
2677
2655
|
applyAccess(result.access);
|
|
2678
|
-
setAccessNotice(
|
|
2656
|
+
setAccessNotice(notice);
|
|
2679
2657
|
return true;
|
|
2680
2658
|
} catch (error) {
|
|
2681
2659
|
setAccessError(messageOf(error));
|
|
@@ -2684,18 +2662,18 @@ window.__ModuleLoader__.load({
|
|
|
2684
2662
|
setBusy(null);
|
|
2685
2663
|
}
|
|
2686
2664
|
}, [applyAccess]);
|
|
2687
|
-
const
|
|
2665
|
+
const removeUser = (0, react.useCallback)(async (remove, userId, notice) => {
|
|
2688
2666
|
setBusy("access");
|
|
2689
2667
|
setAccessError(null);
|
|
2690
2668
|
setAccessNotice(null);
|
|
2691
2669
|
try {
|
|
2692
|
-
const result = await
|
|
2670
|
+
const result = await remove(userId);
|
|
2693
2671
|
if (!result.ok) {
|
|
2694
2672
|
setAccessError(result.error);
|
|
2695
2673
|
return;
|
|
2696
2674
|
}
|
|
2697
2675
|
applyAccess(result.access);
|
|
2698
|
-
setAccessNotice(
|
|
2676
|
+
setAccessNotice(notice);
|
|
2699
2677
|
} catch (error) {
|
|
2700
2678
|
setAccessError(messageOf(error));
|
|
2701
2679
|
} finally {
|
|
@@ -2759,6 +2737,40 @@ window.__ModuleLoader__.load({
|
|
|
2759
2737
|
setBusy(null);
|
|
2760
2738
|
}
|
|
2761
2739
|
}, [appIdInput, appSecretInput]);
|
|
2740
|
+
/** Save the analyze API token; it applies to the next request, no reconnect. */
|
|
2741
|
+
const onSaveAnalyzeToken = (0, react.useCallback)(async () => {
|
|
2742
|
+
if (analyzeTokenInput.trim() === "") return;
|
|
2743
|
+
setBusy("credentials");
|
|
2744
|
+
setNotice(null);
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await saveCredentials({ analyzeToken: analyzeTokenInput });
|
|
2747
|
+
if (!result.ok) {
|
|
2748
|
+
setNotice({
|
|
2749
|
+
kind: "error",
|
|
2750
|
+
text: result.error,
|
|
2751
|
+
issues: result.issues
|
|
2752
|
+
});
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
setLoad({
|
|
2756
|
+
phase: "ready",
|
|
2757
|
+
panel: result.panel
|
|
2758
|
+
});
|
|
2759
|
+
setStatus(result.panel.status);
|
|
2760
|
+
setAnalyzeTokenInput("");
|
|
2761
|
+
setNotice({
|
|
2762
|
+
kind: "ok",
|
|
2763
|
+
text: PANEL_COPY.analyzeTokenSaved
|
|
2764
|
+
});
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
setNotice({
|
|
2767
|
+
kind: "error",
|
|
2768
|
+
text: messageOf(error)
|
|
2769
|
+
});
|
|
2770
|
+
} finally {
|
|
2771
|
+
setBusy(null);
|
|
2772
|
+
}
|
|
2773
|
+
}, [analyzeTokenInput]);
|
|
2762
2774
|
const onClearCredential = (0, react.useCallback)(async (field) => {
|
|
2763
2775
|
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.credentialRemoveConfirm) : true)) return;
|
|
2764
2776
|
setBusy("credentials-clear");
|
|
@@ -2780,7 +2792,7 @@ window.__ModuleLoader__.load({
|
|
|
2780
2792
|
setStatus(result.panel.status);
|
|
2781
2793
|
setNotice({
|
|
2782
2794
|
kind: "ok",
|
|
2783
|
-
text: result.reconnect === "pending" ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved
|
|
2795
|
+
text: field === "analyzeToken" ? PANEL_COPY.analyzeTokenRemoved : result.reconnect === "pending" ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved
|
|
2784
2796
|
});
|
|
2785
2797
|
} catch (error) {
|
|
2786
2798
|
setNotice({
|
|
@@ -2793,6 +2805,7 @@ window.__ModuleLoader__.load({
|
|
|
2793
2805
|
}, []);
|
|
2794
2806
|
const issues = (0, react.useMemo)(() => notice?.kind === "error" ? notice.issues ?? [] : [], [notice]);
|
|
2795
2807
|
const dirtyCount = Object.keys(draft).length;
|
|
2808
|
+
const restartCount = (0, react.useMemo)(() => (panel?.config.fields ?? []).filter((field) => field.restartRequired && field.key in draft).length, [draft, panel]);
|
|
2796
2809
|
/** Which tabs currently own a dirty key (the dot is per tab, not global). */
|
|
2797
2810
|
const dirtyTabs = (0, react.useMemo)(() => {
|
|
2798
2811
|
const tabs = /* @__PURE__ */ new Set();
|
|
@@ -2818,23 +2831,15 @@ window.__ModuleLoader__.load({
|
|
|
2818
2831
|
issues,
|
|
2819
2832
|
busy,
|
|
2820
2833
|
locked: hostBusy,
|
|
2821
|
-
dirtyCount,
|
|
2822
2834
|
onChange: updateDraft,
|
|
2823
|
-
onSave: () => {
|
|
2824
|
-
onSave();
|
|
2825
|
-
},
|
|
2826
|
-
onDiscardTab: discardTab,
|
|
2827
2835
|
onPickDirectory
|
|
2828
2836
|
};
|
|
2829
2837
|
}, [
|
|
2830
2838
|
busy,
|
|
2831
|
-
discardTab,
|
|
2832
|
-
dirtyCount,
|
|
2833
2839
|
draft,
|
|
2834
2840
|
hostBusy,
|
|
2835
2841
|
issues,
|
|
2836
2842
|
onPickDirectory,
|
|
2837
|
-
onSave,
|
|
2838
2843
|
panel,
|
|
2839
2844
|
renderedFields,
|
|
2840
2845
|
updateDraft
|
|
@@ -2862,6 +2867,26 @@ window.__ModuleLoader__.load({
|
|
|
2862
2867
|
onClearCredential,
|
|
2863
2868
|
onSaveCredentials
|
|
2864
2869
|
]);
|
|
2870
|
+
const analyzeToken = (0, react.useMemo)(() => ({
|
|
2871
|
+
value: analyzeTokenInput,
|
|
2872
|
+
issues,
|
|
2873
|
+
busy,
|
|
2874
|
+
locked: hostBusy,
|
|
2875
|
+
onChange: setAnalyzeTokenInput,
|
|
2876
|
+
onSave: () => {
|
|
2877
|
+
onSaveAnalyzeToken();
|
|
2878
|
+
},
|
|
2879
|
+
onClear: () => {
|
|
2880
|
+
onClearCredential("analyzeToken");
|
|
2881
|
+
}
|
|
2882
|
+
}), [
|
|
2883
|
+
analyzeTokenInput,
|
|
2884
|
+
busy,
|
|
2885
|
+
hostBusy,
|
|
2886
|
+
issues,
|
|
2887
|
+
onClearCredential,
|
|
2888
|
+
onSaveAnalyzeToken
|
|
2889
|
+
]);
|
|
2865
2890
|
if (load.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2866
2891
|
className: "dsh-channels-cfg",
|
|
2867
2892
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -2904,9 +2929,17 @@ window.__ModuleLoader__.load({
|
|
|
2904
2929
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabNav, {
|
|
2905
2930
|
activeTab,
|
|
2906
2931
|
dirtyTabs,
|
|
2932
|
+
counts: { access: load.panel.access.loaded ? load.panel.access.requests.length : 0 },
|
|
2907
2933
|
onChange: setActiveTab
|
|
2908
2934
|
}),
|
|
2909
|
-
|
|
2935
|
+
load.panel.access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2936
|
+
className: "dsh-channels-cfg__banner",
|
|
2937
|
+
children: PANEL_COPY.debugModeWarning
|
|
2938
|
+
}) : null,
|
|
2939
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
2940
|
+
kind: notice.kind,
|
|
2941
|
+
text: notice.text
|
|
2942
|
+
}),
|
|
2910
2943
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2911
2944
|
role: "tabpanel",
|
|
2912
2945
|
id: `dsh-channels-tabpanel-${activeTab}`,
|
|
@@ -2921,12 +2954,19 @@ window.__ModuleLoader__.load({
|
|
|
2921
2954
|
onReconnect();
|
|
2922
2955
|
}
|
|
2923
2956
|
}) : null,
|
|
2924
|
-
activeTab === "sessions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2925
|
-
|
|
2957
|
+
activeTab === "sessions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
2958
|
+
form,
|
|
2959
|
+
tab: "sessions"
|
|
2960
|
+
}) : null,
|
|
2961
|
+
activeTab === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
2926
2962
|
form,
|
|
2927
|
-
|
|
2963
|
+
tab: "model",
|
|
2964
|
+
note: catalogBusy ? PANEL_COPY.catalogLoading : void 0
|
|
2965
|
+
}) : null,
|
|
2966
|
+
activeTab === "attachments" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
2967
|
+
form,
|
|
2968
|
+
tab: "attachments"
|
|
2928
2969
|
}) : null,
|
|
2929
|
-
activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, { form }) : null,
|
|
2930
2970
|
activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
|
|
2931
2971
|
form,
|
|
2932
2972
|
accessNotice,
|
|
@@ -2935,74 +2975,100 @@ window.__ModuleLoader__.load({
|
|
|
2935
2975
|
onRemoveGroup: (chatId) => {
|
|
2936
2976
|
onRemoveAccessGroup(chatId);
|
|
2937
2977
|
},
|
|
2938
|
-
onAddUser:
|
|
2978
|
+
onAddUser: (userId, name) => addUser(addAccessUser, userId, name, PANEL_COPY.accessUserAdded),
|
|
2939
2979
|
onRemoveUser: (userId) => {
|
|
2940
|
-
|
|
2980
|
+
removeUser(removeAccessUser, userId, PANEL_COPY.accessUserRemoved);
|
|
2941
2981
|
},
|
|
2942
2982
|
onResolveRequest: (token, decision) => {
|
|
2943
2983
|
onResolveAccessRequest(token, decision);
|
|
2944
2984
|
}
|
|
2945
2985
|
}) : null,
|
|
2986
|
+
activeTab === "wiki" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WikiTab, {
|
|
2987
|
+
form,
|
|
2988
|
+
accessNotice,
|
|
2989
|
+
accessError,
|
|
2990
|
+
onAddUser: (userId, name) => addUser(addWikiUser, userId, name, PANEL_COPY.wikiUserAdded),
|
|
2991
|
+
onRemoveUser: (userId) => {
|
|
2992
|
+
removeUser(removeWikiUser, userId, PANEL_COPY.wikiUserRemoved);
|
|
2993
|
+
}
|
|
2994
|
+
}) : null,
|
|
2946
2995
|
activeTab === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedTab, {
|
|
2947
2996
|
form,
|
|
2997
|
+
analyzeToken,
|
|
2948
2998
|
onClear: () => {
|
|
2949
2999
|
onClear();
|
|
2950
3000
|
}
|
|
2951
3001
|
}) : null
|
|
2952
3002
|
]
|
|
3003
|
+
}),
|
|
3004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
3005
|
+
dirtyCount,
|
|
3006
|
+
restartCount,
|
|
3007
|
+
busy,
|
|
3008
|
+
locked: hostBusy,
|
|
3009
|
+
onSave: () => {
|
|
3010
|
+
onSave();
|
|
3011
|
+
},
|
|
3012
|
+
onDiscard: discardDraft
|
|
2953
3013
|
})
|
|
2954
3014
|
]
|
|
2955
3015
|
});
|
|
2956
3016
|
}
|
|
2957
|
-
/** Page header: title
|
|
3017
|
+
/** Page header: title, phase pill, version and uptime, refresh; visible on every tab. */
|
|
2958
3018
|
function PanelHeader(props) {
|
|
2959
3019
|
const { status, pollError, busy, locked, onRefresh } = props;
|
|
2960
3020
|
const badge = phaseBadge(status.phase);
|
|
2961
3021
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2962
3022
|
className: "dsh-channels-cfg__panelHead",
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3023
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3024
|
+
className: "dsh-channels-cfg__headRow",
|
|
3025
|
+
children: [
|
|
3026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
3027
|
+
className: "dsh-channels-cfg__title",
|
|
3028
|
+
children: PANEL_COPY.title
|
|
3029
|
+
}),
|
|
3030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
3031
|
+
tone: badge.tone,
|
|
3032
|
+
dot: true,
|
|
3033
|
+
children: badge.text
|
|
3034
|
+
}),
|
|
3035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3036
|
+
className: "dsh-channels-cfg__meta dsh-channels-cfg__mono",
|
|
3037
|
+
children: ["v", status.version]
|
|
3038
|
+
}),
|
|
3039
|
+
status.uptimeMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3040
|
+
className: "dsh-channels-cfg__meta",
|
|
3041
|
+
children: [
|
|
3042
|
+
"· ",
|
|
3043
|
+
PANEL_COPY.uptimePrefix,
|
|
3044
|
+
" ",
|
|
3045
|
+
formatUptime(status.uptimeMs)
|
|
3046
|
+
]
|
|
3047
|
+
}),
|
|
3048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
3049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3050
|
+
type: "button",
|
|
3051
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
3052
|
+
onClick: onRefresh,
|
|
3053
|
+
disabled: busy !== null || locked,
|
|
3054
|
+
children: PANEL_COPY.refresh
|
|
3055
|
+
})
|
|
3056
|
+
]
|
|
3057
|
+
}), pollError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3058
|
+
className: "dsh-channels-cfg__fieldError",
|
|
3059
|
+
role: "status",
|
|
3060
|
+
"aria-live": "polite",
|
|
3061
|
+
children: [
|
|
3062
|
+
PANEL_COPY.pollFailed,
|
|
3063
|
+
":",
|
|
3064
|
+
pollError
|
|
3065
|
+
]
|
|
3066
|
+
})]
|
|
3001
3067
|
});
|
|
3002
3068
|
}
|
|
3003
3069
|
/** Top-level tab navigation; the dirty dot keeps unsaved work visible per tab. */
|
|
3004
3070
|
function TabNav(props) {
|
|
3005
|
-
const { activeTab, dirtyTabs, onChange } = props;
|
|
3071
|
+
const { activeTab, dirtyTabs, counts, onChange } = props;
|
|
3006
3072
|
const refs = (0, react.useRef)([]);
|
|
3007
3073
|
/** Roving-focus keyboard navigation between tabs (WAI-ARIA tabs pattern). */
|
|
3008
3074
|
const onKeyDown = (index, event) => {
|
|
@@ -3047,11 +3113,17 @@ window.__ModuleLoader__.load({
|
|
|
3047
3113
|
className: "dsh-channels-cfg__tab",
|
|
3048
3114
|
onClick: () => onChange(tab.id),
|
|
3049
3115
|
onKeyDown: (event) => onKeyDown(index, event),
|
|
3050
|
-
children: [
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3116
|
+
children: [
|
|
3117
|
+
tab.label,
|
|
3118
|
+
(counts[tab.id] ?? 0) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3119
|
+
className: "dsh-channels-cfg__tabCount",
|
|
3120
|
+
children: counts[tab.id]
|
|
3121
|
+
}) : null,
|
|
3122
|
+
dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3123
|
+
className: "dsh-channels-cfg__tabDot",
|
|
3124
|
+
"aria-hidden": "true"
|
|
3125
|
+
}) : null
|
|
3126
|
+
]
|
|
3055
3127
|
}, tab.id);
|
|
3056
3128
|
})
|
|
3057
3129
|
});
|
|
@@ -3060,108 +3132,125 @@ window.__ModuleLoader__.load({
|
|
|
3060
3132
|
//#region src/client/styles.ts
|
|
3061
3133
|
/**
|
|
3062
3134
|
* Settings panel styling. DSH semantic tokens (`--dsw-alias-*`) with neutral
|
|
3063
|
-
* fallbacks
|
|
3064
|
-
*
|
|
3135
|
+
* fallbacks, aliased once to local `--dc-*` variables, so the page follows
|
|
3136
|
+
* the active theme.
|
|
3065
3137
|
*
|
|
3066
3138
|
* @module dsh-channels/client/styles
|
|
3067
3139
|
*/
|
|
3068
3140
|
const STYLE_ATTRIBUTE = "data-dsh-channels-css";
|
|
3069
3141
|
const CSS = `
|
|
3070
|
-
.dsh-channels-cfg{container-type:inline-size;display:flex;flex-direction:column;gap:
|
|
3071
|
-
.dsh-channels-cfg__panelHead{position:sticky;top:0;z-index:2;display:flex;flex-direction:column;gap:4px;padding:10px 0
|
|
3142
|
+
.dsh-channels-cfg{--dc-fg:var(--dsw-alias-label-primary,#1f2328);--dc-fg2:var(--dsw-alias-label-secondary,#5c6169);--dc-fg3:var(--dsw-alias-label-tertiary,#8a8f98);--dc-dim:var(--dsw-alias-label-dimmed,#b0b4ba);--dc-bg:var(--dsw-alias-bg-base,#fff);--dc-line:var(--dsw-alias-border-l2,rgba(0,0,0,.12));--dc-hover:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));--dc-accent:var(--dsw-alias-state-business-primary,#0a67d3);--dc-ok:var(--dsw-alias-state-success-primary,#0a7d33);--dc-warn:var(--dsw-alias-state-warn-primary,#a15c00);--dc-bad:var(--dsw-alias-state-error-primary,#c62828);--dc-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;container-type:inline-size;display:flex;flex-direction:column;gap:14px;width:100%;max-width:720px;color:var(--dc-fg);font-size:13px;line-height:20px}
|
|
3143
|
+
.dsh-channels-cfg__panelHead{position:sticky;top:0;z-index:2;display:flex;flex-direction:column;gap:4px;padding:10px 0 6px;background:var(--dc-bg)}
|
|
3072
3144
|
.dsh-channels-cfg__headRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3073
|
-
.dsh-channels-cfg__title{margin:0;font-size:
|
|
3074
|
-
.dsh-channels-
|
|
3075
|
-
.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))}
|
|
3076
|
-
.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}
|
|
3077
|
-
.dsh-channels-cfg__tab:hover{color:var(--dsw-alias-label-primary,#1f2328)}
|
|
3078
|
-
.dsh-channels-cfg__tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:-2px}
|
|
3079
|
-
.dsh-channels-cfg__tab[aria-selected="true"]{color:var(--dsw-alias-state-business-primary,#0a67d3);border-bottom-color:currentColor;font-weight:500}
|
|
3080
|
-
.dsh-channels-cfg__tabDot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-warning-primary,#a15c00);flex:none}
|
|
3081
|
-
.dsh-channels-cfg__tabBody{display:flex;flex-direction:column;gap:12px}
|
|
3082
|
-
.dsh-channels-cfg__tabHelp{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
3083
|
-
.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}
|
|
3084
|
-
.dsh-channels-cfg__cardHead{display:flex;align-items:center;gap:8px;min-height:20px;flex-wrap:wrap}
|
|
3085
|
-
.dsh-channels-cfg__cardTitle{font-size:13px;font-weight:500}
|
|
3086
|
-
.dsh-channels-cfg__cardMeta{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
3087
|
-
.dsh-channels-cfg__group{display:flex;flex-direction:column;gap:8px}
|
|
3088
|
-
.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}
|
|
3089
|
-
.dsh-channels-cfg__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
|
|
3090
|
-
.dsh-channels-cfg__badge--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);border-color:currentColor}
|
|
3091
|
-
.dsh-channels-cfg__badge--warn{color:var(--dsw-alias-state-warning-primary,#a15c00);border-color:currentColor}
|
|
3092
|
-
.dsh-channels-cfg__badge--bad{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
3093
|
-
.dsh-channels-cfg__badge--muted{color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3094
|
-
.dsh-channels-cfg__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 16px}
|
|
3095
|
-
.dsh-channels-cfg__fact{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3096
|
-
.dsh-channels-cfg__factLabel{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
3097
|
-
.dsh-channels-cfg__factValue{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
3098
|
-
.dsh-channels-cfg__actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3099
|
-
.dsh-channels-cfg__btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
3100
|
-
.dsh-channels-cfg__btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
|
|
3101
|
-
.dsh-channels-cfg__btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:2px}
|
|
3102
|
-
.dsh-channels-cfg__btn:disabled{color:var(--dsw-alias-label-dimmed,#b0b4ba);cursor:default}
|
|
3103
|
-
.dsh-channels-cfg__btn--primary{background:var(--dsw-alias-button-primary-fill,#0a67d3);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
3104
|
-
.dsh-channels-cfg__btn--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#0b5cba)}
|
|
3105
|
-
.dsh-channels-cfg__btn--danger{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
3106
|
-
.dsh-channels-cfg__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px;align-items:start}
|
|
3107
|
-
.dsh-channels-cfg__field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
3108
|
-
.dsh-channels-cfg__field--wide{grid-column:1/-1}
|
|
3109
|
-
.dsh-channels-cfg__labelRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
3110
|
-
.dsh-channels-cfg__label{font-size:12px;color:var(--dsw-alias-label-secondary,#5c6169)}
|
|
3111
|
-
.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}
|
|
3112
|
-
.dsh-channels-cfg__tag--restart{color:var(--dsw-alias-state-warning-primary,#a15c00)}
|
|
3113
|
-
.dsh-channels-cfg__tag--dirty{color:var(--dsw-alias-state-warning-primary,#a15c00);border-style:solid}
|
|
3114
|
-
.dsh-channels-cfg__help{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px}
|
|
3115
|
-
.dsh-channels-cfg__reasonBad{color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
3116
|
-
.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}
|
|
3117
|
-
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
3118
|
-
.dsh-channels-cfg__inputRow .dsh-channels-cfg__input{flex:1 1 auto;width:auto;min-width:0}
|
|
3119
|
-
.dsh-channels-cfg__btn--browse{flex:none}
|
|
3120
|
-
.dsh-channels-cfg__list{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3121
|
-
.dsh-channels-cfg__listItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3122
|
-
.dsh-channels-cfg__btn--listRemove{flex:none}
|
|
3123
|
-
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
3124
|
-
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
3125
|
-
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
3126
|
-
.dsh-channels-cfg__check{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;font-size:13px}
|
|
3127
|
-
.dsh-channels-cfg__checkbox{width:15px;height:15px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0a67d3);cursor:pointer}
|
|
3128
|
-
.dsh-channels-cfg__fieldError{color:var(--dsw-alias-state-danger-primary,#c62828);font-size:11px;line-height:16px}
|
|
3129
|
-
.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}
|
|
3130
|
-
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
3131
|
-
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
3132
|
-
.dsh-channels-cfg__notice a{color:inherit;text-decoration:underline}
|
|
3133
|
-
.dsh-channels-cfg__scopeList{margin:2px 0;padding-left:18px;display:flex;flex-direction:column;gap:2px}
|
|
3134
|
-
.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}
|
|
3135
|
-
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
3145
|
+
.dsh-channels-cfg__title{margin:0 2px 0 0;font-size:17px;font-weight:600;line-height:26px}
|
|
3146
|
+
.dsh-channels-cfg__meta{color:var(--dc-fg3);font-size:12px}
|
|
3136
3147
|
.dsh-channels-cfg__spacer{flex:1}
|
|
3148
|
+
.dsh-channels-cfg__mono{font-family:var(--dc-mono);font-size:12px}
|
|
3149
|
+
.dsh-channels-cfg__pill{display:inline-flex;align-items:center;gap:5px;padding:0 8px;border-radius:999px;font-size:12px;line-height:20px;font-weight:400;white-space:nowrap;color:var(--dc-fg3);background:color-mix(in srgb,currentColor 10%,transparent)}
|
|
3150
|
+
.dsh-channels-cfg__pill--ok{color:var(--dc-ok)}
|
|
3151
|
+
.dsh-channels-cfg__pill--warn{color:var(--dc-warn)}
|
|
3152
|
+
.dsh-channels-cfg__pill--bad{color:var(--dc-bad)}
|
|
3153
|
+
.dsh-channels-cfg__dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
|
|
3154
|
+
.dsh-channels-cfg__tabs{display:flex;align-items:center;gap:18px;border-bottom:1px solid var(--dc-line);overflow-x:auto;scrollbar-width:none}
|
|
3155
|
+
.dsh-channels-cfg__tab{display:inline-flex;align-items:center;gap:5px;min-height:34px;padding:0;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dc-fg2);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
3156
|
+
.dsh-channels-cfg__tab:hover{color:var(--dc-fg)}
|
|
3157
|
+
.dsh-channels-cfg__tab[aria-selected="true"]{color:var(--dc-fg);border-bottom-color:currentColor;font-weight:600}
|
|
3158
|
+
.dsh-channels-cfg__tabCount{padding:0 5px;border-radius:8px;font-size:11px;line-height:16px;font-weight:500;color:var(--dc-warn);background:color-mix(in srgb,var(--dc-warn) 12%,transparent)}
|
|
3159
|
+
.dsh-channels-cfg__tabDot{width:6px;height:6px;border-radius:50%;background:var(--dc-accent);flex:none}
|
|
3160
|
+
.dsh-channels-cfg__tabBody{display:flex;flex-direction:column;gap:22px;padding:4px 0 8px}
|
|
3161
|
+
.dsh-channels-cfg__sec{display:flex;flex-direction:column;gap:6px;margin:0}
|
|
3162
|
+
.dsh-channels-cfg__secTitle{margin:0;font-size:12px;font-weight:600;line-height:18px;color:var(--dc-fg3);letter-spacing:.02em}
|
|
3163
|
+
.dsh-channels-cfg__secFoot{margin:0;color:var(--dc-fg3);font-size:12px;line-height:18px}
|
|
3164
|
+
.dsh-channels-cfg__list{display:flex;flex-direction:column;border:1px solid var(--dc-line);border-radius:10px}
|
|
3165
|
+
.dsh-channels-cfg__list>*+*{border-top:1px solid var(--dc-line)}
|
|
3166
|
+
.dsh-channels-cfg__list--warn{border-color:color-mix(in srgb,var(--dc-warn) 45%,transparent);background:color-mix(in srgb,var(--dc-warn) 7%,transparent)}
|
|
3167
|
+
.dsh-channels-cfg__row{display:flex;align-items:center;gap:16px;padding:11px 14px;min-height:28px}
|
|
3168
|
+
.dsh-channels-cfg__row--wide{flex-direction:column;align-items:stretch;gap:8px}
|
|
3169
|
+
.dsh-channels-cfg__rowText{display:flex;flex-direction:column;gap:1px;flex:1 1 auto;min-width:0}
|
|
3170
|
+
.dsh-channels-cfg__rowLabel{display:flex;align-items:center;gap:6px;flex-wrap:wrap;overflow-wrap:anywhere}
|
|
3171
|
+
.dsh-channels-cfg__rowSummary{color:var(--dc-fg3);font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
3172
|
+
.dsh-channels-cfg__rowCtl{flex:none;display:flex;align-items:center;justify-content:flex-end;min-width:0;max-width:62%}
|
|
3173
|
+
.dsh-channels-cfg__row--wide .dsh-channels-cfg__rowCtl{max-width:none;justify-content:stretch}
|
|
3174
|
+
.dsh-channels-cfg__row--wide .dsh-channels-cfg__rowCtl>*{flex:1 1 auto}
|
|
3175
|
+
.dsh-channels-cfg__details{padding:0 14px 12px;color:var(--dc-fg2);font-size:12px;line-height:19px;overflow-wrap:anywhere}
|
|
3176
|
+
.dsh-channels-cfg__rowError{padding:0 14px 10px;color:var(--dc-bad);font-size:12px;line-height:18px}
|
|
3177
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__rowWrap{border-top-width:0}
|
|
3178
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__rowWrap+*{border-top-width:1px}
|
|
3179
|
+
.dsh-channels-cfg__tag{display:inline-flex;align-items:center;padding:0 5px;border-radius:4px;font-size:11px;line-height:16px;white-space:nowrap;color:var(--dc-warn);background:color-mix(in srgb,var(--dc-warn) 12%,transparent)}
|
|
3180
|
+
.dsh-channels-cfg__more{padding:0;border:0;background:none;color:var(--dc-fg3);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px;white-space:nowrap}
|
|
3181
|
+
.dsh-channels-cfg__more:hover{color:var(--dc-fg2)}
|
|
3182
|
+
.dsh-channels-cfg__link{padding:0;border:0;background:none;color:var(--dc-accent);font:inherit;font-size:13px;cursor:pointer;text-decoration:none;white-space:nowrap}
|
|
3183
|
+
.dsh-channels-cfg__link:disabled{color:var(--dc-dim);cursor:default}
|
|
3184
|
+
.dsh-channels-cfg__switch{position:relative;flex:none;width:34px;height:20px;padding:0;border:0;border-radius:10px;background:var(--dc-dim);cursor:pointer;transition:background .15s}
|
|
3185
|
+
.dsh-channels-cfg__switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
|
|
3186
|
+
.dsh-channels-cfg__switch[aria-checked="true"]{background:var(--dc-accent)}
|
|
3187
|
+
.dsh-channels-cfg__switch[aria-checked="true"]::after{transform:translateX(14px)}
|
|
3188
|
+
.dsh-channels-cfg__switch:disabled{opacity:.5;cursor:default}
|
|
3189
|
+
.dsh-channels-cfg__input{box-sizing:border-box;width:100%;min-width:0;height:30px;padding:0 10px;border:1px solid var(--dc-line);border-radius:7px;background:var(--dsw-alias-bg-layer-1,transparent);color:inherit;font:inherit;font-size:13px;outline:none}
|
|
3190
|
+
.dsh-channels-cfg__input:focus{border-color:var(--dc-accent)}
|
|
3191
|
+
.dsh-channels-cfg__input--invalid{border-color:var(--dc-bad)}
|
|
3192
|
+
.dsh-channels-cfg__input--num{width:120px;text-align:right;font-variant-numeric:tabular-nums}
|
|
3193
|
+
.dsh-channels-cfg__input--text{width:260px}
|
|
3194
|
+
.dsh-channels-cfg__select{width:220px;appearance:auto;cursor:pointer}
|
|
3195
|
+
.dsh-channels-cfg__unit{min-width:36px;color:var(--dc-fg3);font-size:12px}
|
|
3196
|
+
.dsh-channels-cfg__bytes{color:var(--dc-fg3);font-size:12px;white-space:nowrap}
|
|
3197
|
+
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
3198
|
+
.dsh-channels-cfg__inputRow>.dsh-channels-cfg__input:not(.dsh-channels-cfg__input--num){flex:1 1 auto;width:auto}
|
|
3199
|
+
.dsh-channels-cfg__stringList{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3200
|
+
.dsh-channels-cfg__stringListItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3201
|
+
.dsh-channels-cfg__fieldError{color:var(--dc-bad);font-size:12px;line-height:18px}
|
|
3202
|
+
.dsh-channels-cfg__textBad{color:var(--dc-bad)}
|
|
3203
|
+
.dsh-channels-cfg__btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:28px;padding:0 12px;border:1px solid var(--dc-line);border-radius:7px;background:transparent;color:inherit;font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;text-decoration:none}
|
|
3204
|
+
.dsh-channels-cfg__btn:hover:not(:disabled){background:var(--dc-hover)}
|
|
3205
|
+
.dsh-channels-cfg__btn:disabled{color:var(--dc-dim);cursor:default}
|
|
3206
|
+
.dsh-channels-cfg__btn--primary{background:var(--dsw-alias-button-primary-fill,var(--dc-accent));border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
3207
|
+
.dsh-channels-cfg__btn--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#0b5cba)}
|
|
3208
|
+
.dsh-channels-cfg__btn--primary:disabled{opacity:.45;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
3209
|
+
.dsh-channels-cfg__btn--ghost{border-color:transparent;color:var(--dc-fg2)}
|
|
3210
|
+
.dsh-channels-cfg__btn--danger{color:var(--dc-bad)}
|
|
3211
|
+
.dsh-channels-cfg__btn--remove{color:var(--dc-bad);border-color:color-mix(in srgb,var(--dc-bad) 55%,transparent);background:color-mix(in srgb,var(--dc-bad) 8%,transparent)}
|
|
3212
|
+
.dsh-channels-cfg__btn--remove:hover:not(:disabled){background:color-mix(in srgb,var(--dc-bad) 15%,transparent)}
|
|
3213
|
+
.dsh-channels-cfg__btn--remove:disabled{opacity:.5;color:var(--dc-bad)}
|
|
3214
|
+
.dsh-channels-cfg__btn:focus-visible,.dsh-channels-cfg__tab:focus-visible,.dsh-channels-cfg__switch:focus-visible,.dsh-channels-cfg__link:focus-visible,.dsh-channels-cfg__more:focus-visible{outline:2px solid var(--dc-accent);outline-offset:2px}
|
|
3215
|
+
.dsh-channels-cfg__notice{padding:8px 12px;border-radius:8px;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
3216
|
+
.dsh-channels-cfg__notice--ok{color:var(--dc-ok);background:color-mix(in srgb,var(--dc-ok) 9%,transparent)}
|
|
3217
|
+
.dsh-channels-cfg__notice--error{color:var(--dc-bad);background:color-mix(in srgb,var(--dc-bad) 8%,transparent)}
|
|
3218
|
+
.dsh-channels-cfg__banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border-radius:8px;background:color-mix(in srgb,var(--dc-warn) 12%,transparent);color:var(--dc-fg);font-size:12px;line-height:18px}
|
|
3219
|
+
.dsh-channels-cfg__bannerText{flex:1;min-width:0}
|
|
3220
|
+
.dsh-channels-cfg__hero{display:flex;flex-direction:column;gap:12px;padding:14px 16px;border:1px solid var(--dc-line);border-radius:10px}
|
|
3221
|
+
.dsh-channels-cfg__heroTop{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3222
|
+
.dsh-channels-cfg__heroTop strong{font-size:14px;font-weight:600}
|
|
3223
|
+
.dsh-channels-cfg__stats{display:flex;gap:28px;flex-wrap:wrap}
|
|
3224
|
+
.dsh-channels-cfg__stat{display:flex;flex-direction:column}
|
|
3225
|
+
.dsh-channels-cfg__stat span{color:var(--dc-fg3);font-size:12px}
|
|
3226
|
+
.dsh-channels-cfg__stat strong{font-size:18px;font-weight:600;line-height:26px;font-variant-numeric:tabular-nums}
|
|
3227
|
+
.dsh-channels-cfg__stat strong.dsh-channels-cfg__mono{font-size:14px}
|
|
3228
|
+
.dsh-channels-cfg__checkIcon{display:grid;place-items:center;flex:none;width:18px;height:18px;border-radius:50%;font-size:11px;font-weight:700;align-self:flex-start;margin-top:1px}
|
|
3229
|
+
.dsh-channels-cfg__checkIcon--ok{color:var(--dc-ok);background:color-mix(in srgb,var(--dc-ok) 12%,transparent)}
|
|
3230
|
+
.dsh-channels-cfg__checkIcon--warn{color:var(--dc-warn);background:color-mix(in srgb,var(--dc-warn) 14%,transparent)}
|
|
3231
|
+
.dsh-channels-cfg__checkIcon--bad{color:var(--dc-bad);background:color-mix(in srgb,var(--dc-bad) 12%,transparent)}
|
|
3232
|
+
.dsh-channels-cfg__scopeList{margin:0;padding:0 14px 12px 46px;list-style:none;display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dc-fg2)}
|
|
3233
|
+
.dsh-channels-cfg__scopeList li{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
|
|
3234
|
+
.dsh-channels-cfg__scopeList .dsh-channels-cfg__link{font-size:12px}
|
|
3235
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__scopeList{border-top-width:0}
|
|
3236
|
+
.dsh-channels-cfg__avatar{display:grid;place-items:center;flex:none;width:26px;height:26px;border-radius:50%;background:var(--dc-hover);color:var(--dc-fg2);font-size:12px}
|
|
3237
|
+
.dsh-channels-cfg__addRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 14px}
|
|
3238
|
+
.dsh-channels-cfg__addRow .dsh-channels-cfg__input{flex:1 1 160px;width:auto}
|
|
3239
|
+
.dsh-channels-cfg__emptyRow{padding:11px 14px;color:var(--dc-fg3);font-size:12px}
|
|
3240
|
+
.dsh-channels-cfg__saveBar{position:sticky;bottom:0;z-index:2;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 0;border-top:1px solid var(--dc-line);background:var(--dc-bg)}
|
|
3241
|
+
.dsh-channels-cfg__saveMsg{flex:1;min-width:0;color:var(--dc-fg2);font-size:12px}
|
|
3137
3242
|
.dsh-channels-cfg__steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
3138
3243
|
.dsh-channels-cfg__step{display:flex;align-items:flex-start;gap:10px}
|
|
3139
|
-
.dsh-channels-cfg__stepIndex{display:
|
|
3244
|
+
.dsh-channels-cfg__stepIndex{display:grid;place-items:center;flex:none;width:20px;height:20px;border-radius:50%;color:var(--dc-accent);background:color-mix(in srgb,var(--dc-accent) 12%,transparent);font-size:12px;font-variant-numeric:tabular-nums}
|
|
3140
3245
|
.dsh-channels-cfg__stepBody{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3141
|
-
.dsh-channels-cfg__stepTitle{font-
|
|
3142
|
-
.dsh-channels-
|
|
3143
|
-
.dsh-channels-
|
|
3144
|
-
.dsh-channels-
|
|
3145
|
-
.dsh-channels-cfg__kvKey{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
3146
|
-
.dsh-channels-cfg__kvValue{overflow-wrap:anywhere}
|
|
3147
|
-
.dsh-channels-cfg__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
3148
|
-
.dsh-channels-cfg__cred{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3149
|
-
.dsh-channels-cfg__cred .dsh-channels-cfg__input{flex:1 1 240px;min-width:0;width:auto}
|
|
3150
|
-
.dsh-channels-cfg__accessSection{display:flex;flex-direction:column;gap:6px}
|
|
3151
|
-
.dsh-channels-cfg__accessList{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
|
|
3152
|
-
.dsh-channels-cfg__accessRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px}
|
|
3153
|
-
.dsh-channels-cfg__accessMain{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1 1 200px;overflow-wrap:anywhere}
|
|
3154
|
-
.dsh-channels-cfg__accessMeta{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px;overflow-wrap:anywhere}
|
|
3155
|
-
.dsh-channels-cfg__accessActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
3156
|
-
.dsh-channels-cfg__accessAdd{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3157
|
-
.dsh-channels-cfg__accessInput{flex:1 1 140px;min-width:0;width:auto}
|
|
3158
|
-
.dsh-channels-cfg__error{padding:10px 12px;border:1px solid var(--dsw-alias-state-danger-primary,#c62828);border-radius:10px;color:var(--dsw-alias-state-danger-primary,#c62828);font-size:12px;line-height:18px;display:flex;flex-direction:column;gap:8px}
|
|
3159
|
-
.dsh-channels-cfg__loading{padding:16px 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
3160
|
-
.dsh-channels-cfg__issues{margin:0;padding-left:18px}
|
|
3246
|
+
.dsh-channels-cfg__stepTitle{font-weight:500}
|
|
3247
|
+
.dsh-channels-cfg__error{padding:10px 12px;border:1px solid var(--dc-bad);border-radius:10px;color:var(--dc-bad);font-size:12px;line-height:18px;display:flex;flex-direction:column;gap:8px}
|
|
3248
|
+
.dsh-channels-cfg__loading{padding:16px 0;color:var(--dc-fg3);font-size:12px}
|
|
3249
|
+
@media (prefers-reduced-motion:reduce){.dsh-channels-cfg__switch,.dsh-channels-cfg__switch::after{transition:none}}
|
|
3161
3250
|
@container (max-width:560px){
|
|
3162
|
-
.dsh-channels-
|
|
3163
|
-
.dsh-channels-
|
|
3164
|
-
.dsh-channels-
|
|
3251
|
+
.dsh-channels-cfg__row:not(.dsh-channels-cfg__row--wide){flex-wrap:wrap}
|
|
3252
|
+
.dsh-channels-cfg__rowCtl{max-width:100%}
|
|
3253
|
+
.dsh-channels-cfg__select,.dsh-channels-cfg__input--text{width:100%}
|
|
3165
3254
|
}
|
|
3166
3255
|
`;
|
|
3167
3256
|
/** Append the panel stylesheet once (idempotent across HMR / remounts). */
|