@modesto_ma/dsh-channels 0.5.0-beta.4 → 0.5.0-beta.6
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 +49 -28
- package/docs/attachment-pipeline-v2.md +1 -1
- package/docs/feishu-onboarding.md +6 -5
- package/lib/client.js +1874 -1510
- package/lib/client.js.map +1 -1
- package/lib/index.js +736 -353
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/api.ts +45 -2
- package/src/client/avatar.tsx +36 -0
- package/src/client/icon.tsx +25 -0
- package/src/client/index.ts +78 -23
- package/src/client/number-field.ts +97 -0
- package/src/client/origin-cache.ts +45 -0
- package/src/client/origin.tsx +201 -0
- package/src/client/panel/access-tab.tsx +90 -21
- 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 +77 -54
- package/src/client/panel/profiles.ts +73 -0
- package/src/client/panel/shared.tsx +358 -253
- package/src/client/panel/whitelist.tsx +203 -0
- package/src/client/panel/wiki-tab.tsx +33 -39
- package/src/client/styles.ts +132 -92
- package/src/domain.ts +42 -0
- package/src/feishu/scopes.ts +2 -1
- package/src/feishu-channel.ts +87 -0
- package/src/index.ts +1 -1
- package/src/routing/inbound.ts +13 -5
- package/src/service.ts +146 -3
- package/src/session/routes.ts +45 -4
- package/src/settings/contract.ts +265 -152
- package/src/settings/routes.ts +47 -0
- package/src/settings/surface.ts +201 -197
- package/src/client/panel/access-card.tsx +0 -329
- package/src/client/panel/analyze-token-card.tsx +0 -70
- package/src/client/panel/model-tab.tsx +0 -23
- package/src/client/panel/sessions-tab.tsx +0 -24
- package/src/client/panel/workspace-tab.tsx +0 -24
package/lib/client.js
CHANGED
|
@@ -4,14 +4,14 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
let react = require("react");
|
|
8
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
9
|
//#region src/settings/contract.ts
|
|
10
10
|
/**
|
|
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
|
*/
|
|
@@ -44,6 +44,14 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
* [`EditableFieldWire.browse`],保留手写输入。
|
|
45
45
|
*/
|
|
46
46
|
const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
|
|
47
|
+
/** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
|
|
48
|
+
const SESSION_ORIGIN_ROUTE = "/dsh-channels/session-origin";
|
|
49
|
+
/**
|
|
50
|
+
* `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
|
|
51
|
+
* names and avatars for the users and chats the panel shows. At most
|
|
52
|
+
* {@link PROFILES_MAX_IDS} ids per list.
|
|
53
|
+
*/
|
|
54
|
+
const PROFILES_ROUTE = "/dsh-channels/profiles";
|
|
47
55
|
/**
|
|
48
56
|
* Top-level panel tabs, in render order. The panel shows the connection state
|
|
49
57
|
* header above every tab, so switching tabs never hides whether the bridge is
|
|
@@ -56,15 +64,15 @@ window.__ModuleLoader__.load({
|
|
|
56
64
|
},
|
|
57
65
|
{
|
|
58
66
|
id: "sessions",
|
|
59
|
-
label: "
|
|
67
|
+
label: "会话"
|
|
60
68
|
},
|
|
61
69
|
{
|
|
62
70
|
id: "model",
|
|
63
71
|
label: "模型"
|
|
64
72
|
},
|
|
65
73
|
{
|
|
66
|
-
id: "
|
|
67
|
-
label: "
|
|
74
|
+
id: "attachments",
|
|
75
|
+
label: "附件与工具"
|
|
68
76
|
},
|
|
69
77
|
{
|
|
70
78
|
id: "access",
|
|
@@ -79,91 +87,91 @@ window.__ModuleLoader__.load({
|
|
|
79
87
|
label: "高级"
|
|
80
88
|
}
|
|
81
89
|
];
|
|
82
|
-
/** Which editable field groups each tab renders
|
|
90
|
+
/** Which editable field groups each tab renders, in section order. */
|
|
83
91
|
const TAB_FIELD_GROUPS = {
|
|
84
92
|
connection: [],
|
|
85
93
|
sessions: [
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
94
|
+
"trigger",
|
|
95
|
+
"workspace",
|
|
96
|
+
"cards",
|
|
97
|
+
"features"
|
|
89
98
|
],
|
|
90
99
|
model: ["model"],
|
|
91
|
-
|
|
100
|
+
attachments: [
|
|
101
|
+
"attachments",
|
|
102
|
+
"tools",
|
|
103
|
+
"limits"
|
|
104
|
+
],
|
|
92
105
|
access: ["access"],
|
|
93
106
|
wiki: ["wiki"],
|
|
94
|
-
advanced: ["
|
|
107
|
+
advanced: ["analyze", "logging"]
|
|
95
108
|
};
|
|
96
109
|
/** Section heading for one field group inside its tab. */
|
|
97
110
|
const FIELD_GROUP_LABELS = {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
model: "
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
trigger: "触发",
|
|
112
|
+
workspace: "工作区",
|
|
113
|
+
cards: "话题与卡片",
|
|
114
|
+
features: "飞书侧功能",
|
|
115
|
+
model: "模型",
|
|
116
|
+
attachments: "入站附件",
|
|
117
|
+
tools: "模型可用的飞书工具",
|
|
118
|
+
limits: "限额与超时",
|
|
119
|
+
access: "准入",
|
|
120
|
+
wiki: "知识库工具",
|
|
121
|
+
analyze: "日志分析接口",
|
|
122
|
+
logging: "日志"
|
|
106
123
|
};
|
|
124
|
+
/** Groups rendered collapsed behind one summary row (rarely changed values). */
|
|
125
|
+
const COLLAPSED_FIELD_GROUPS = ["limits"];
|
|
107
126
|
/** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
|
|
108
127
|
const PANEL_COPY = {
|
|
109
128
|
title: "飞书渠道",
|
|
110
|
-
description: "飞书长连接、应用凭据与渠道配置。凭据只写入 DSH 凭据库且不回显;标注「需重连」的项在连接建立时读取。",
|
|
111
|
-
statusCard: "连接状态",
|
|
112
129
|
phaseUnconfigured: "待配置",
|
|
113
130
|
phaseIdle: "未连接",
|
|
114
131
|
phaseConnecting: "连接中",
|
|
115
132
|
phaseConnected: "已连接",
|
|
116
133
|
phaseError: "连接失败",
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
statusSummaryError: "最近一次连接飞书失败,请检查凭据与网络后重试。",
|
|
134
|
+
statusSummaryConnected: "机器人在线,可以收发消息",
|
|
135
|
+
statusSummaryUnconfigured: "还没有填写飞书应用凭据,插件不会连接飞书",
|
|
136
|
+
statusSummaryIdle: "凭据已就绪,还没有建立连接",
|
|
137
|
+
statusSummaryConnecting: "正在连接飞书…",
|
|
138
|
+
statusSummaryError: "连接飞书失败,请检查凭据与网络后重试",
|
|
139
|
+
uptimePrefix: "运行",
|
|
124
140
|
lastError: "最近一次连接失败",
|
|
125
|
-
|
|
126
|
-
scopeCheckOk: "
|
|
127
|
-
scopeCheckMissing: "
|
|
128
|
-
|
|
141
|
+
checksTitle: "自检",
|
|
142
|
+
scopeCheckOk: "插件需要的飞书权限都已开通",
|
|
143
|
+
scopeCheckMissing: "{n} 项权限未开通",
|
|
144
|
+
scopeCheckMissingRequired: "{n} 项必需权限未开通,相关功能无法使用",
|
|
145
|
+
scopeCheckMissingOptional: "不影响收发消息,只有相关功能不可用。开通后需在开发者后台发布新版本。",
|
|
146
|
+
scopeCheckPublish: "开通后需在开发者后台发布新版本。",
|
|
147
|
+
scopeCheckOpenAll: "全部开通",
|
|
129
148
|
scopeCheckOpenOne: "去开通",
|
|
130
|
-
scopeCheckOptional: "可选",
|
|
131
149
|
scopeCheckRequired: "必需",
|
|
132
|
-
scopeCheckError: "
|
|
150
|
+
scopeCheckError: "无法读取权限开通情况",
|
|
133
151
|
slashCommandSyncTitle: "斜杠指令面板",
|
|
134
|
-
slashCommandSyncOk: "
|
|
135
|
-
|
|
136
|
-
slashCommandSyncCreated: "本次新增",
|
|
137
|
-
slashCommandSyncUpdated: "本次更新",
|
|
138
|
-
slashCommandSyncRemoved: "本次删除",
|
|
139
|
-
slashCommandSyncError: "同步飞书原生斜杠指令面板失败(不影响直接输入命令):",
|
|
152
|
+
slashCommandSyncOk: "已同步 {total} 条命令,飞书客户端约 5 分钟后刷新",
|
|
153
|
+
slashCommandSyncError: "同步失败,直接输入命令不受影响",
|
|
140
154
|
missingConfig: "缺少配置",
|
|
141
|
-
statusHintUnconfigured: "配置完成后插件会自动连接;也可以稍后在本页手动连接。",
|
|
142
|
-
connectionInfo: "连接信息",
|
|
143
|
-
connectionStateLabel: "飞书长连接",
|
|
144
|
-
botOpenId: "Bot open_id",
|
|
145
|
-
uptime: "运行时长",
|
|
146
155
|
routeCount: "话题路由",
|
|
147
156
|
liveAgentCount: "活动会话",
|
|
148
157
|
inflightTurnCount: "在途回复",
|
|
158
|
+
botOpenId: "Bot open_id",
|
|
149
159
|
healthReasons: "健康判定",
|
|
150
160
|
unknownValue: "—",
|
|
151
161
|
connect: "连接飞书",
|
|
152
162
|
connecting: "连接中…",
|
|
153
163
|
onboardingTitle: "开始使用飞书渠道",
|
|
154
|
-
onboardingIntro: "只需三步:在飞书开放平台创建自建应用,把 App ID / App Secret 填入本页,保存后自动连接。",
|
|
155
164
|
onboardingStep1Title: "创建飞书自建应用",
|
|
156
|
-
onboardingStep1Text: "
|
|
165
|
+
onboardingStep1Text: "在 open.feishu.cn/app 创建企业自建应用,开启机器人能力;事件订阅选「长连接」,订阅 im.message.receive_v1。",
|
|
157
166
|
onboardingStep2Title: "填写应用凭据",
|
|
158
|
-
onboardingStep2Text: "
|
|
159
|
-
onboardingStep3Title: "
|
|
160
|
-
onboardingStep3Text: "
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
credentialsRef: "凭据引用",
|
|
167
|
+
onboardingStep2Text: "把 App ID(cli_ 开头)和 App Secret 填到下方「应用凭据」。",
|
|
168
|
+
onboardingStep3Title: "保存后自动连接",
|
|
169
|
+
onboardingStep3Text: "两项都保存后插件会自动连接,状态变成「已连接」就能在飞书里私聊机器人。",
|
|
170
|
+
credentialsTitle: "应用凭据",
|
|
171
|
+
credentialsHint: "值只存在本机凭据库,面板不回显。",
|
|
172
|
+
appIdLabel: "App ID",
|
|
173
|
+
appSecretLabel: "App Secret",
|
|
174
|
+
credentialStore: "凭据库",
|
|
167
175
|
saveCredentials: "保存凭据",
|
|
168
176
|
savingCredentials: "保存中…",
|
|
169
177
|
credentialSaved: "凭据已保存",
|
|
@@ -173,27 +181,29 @@ window.__ModuleLoader__.load({
|
|
|
173
181
|
credentialAutoConnectFailed: "凭据已保存,但自动连接失败",
|
|
174
182
|
credentialRemoved: "已移除存储的凭据",
|
|
175
183
|
credentialRemovedPending: "已移除存储的凭据,重启飞书连接后生效",
|
|
176
|
-
|
|
184
|
+
replaceCredential: "更换",
|
|
185
|
+
setCredential: "设置",
|
|
186
|
+
cancel: "取消",
|
|
187
|
+
removeCredential: "移除",
|
|
177
188
|
removingCredential: "移除中…",
|
|
178
189
|
credentialConfigured: "已配置",
|
|
179
190
|
credentialMissing: "未配置",
|
|
180
191
|
credentialReadOnly: "当前凭据存储不可写",
|
|
181
192
|
appIdPlaceholder: "输入 App ID(cli_…)",
|
|
182
193
|
appSecretPlaceholder: "输入 App Secret",
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
sessionsTabHelp: "私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。",
|
|
186
|
-
modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
|
|
187
|
-
workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。",
|
|
188
|
-
accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
|
|
189
|
-
wikiTabHelp: "知识库工具以应用身份访问「知识库范围」内的文档、电子表格和多维表格。只有管理员或知识库白名单里的用户发起的话题能用;每次打开、新建、修改仍需话题发起人在授权卡上确认。",
|
|
190
|
-
advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
|
|
191
|
-
save: "保存更改",
|
|
194
|
+
credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
|
|
195
|
+
save: "保存",
|
|
192
196
|
saving: "保存中…",
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
discard: "放弃",
|
|
198
|
+
dirtyCount: "{n} 项未保存",
|
|
199
|
+
dirtyRestart: ",其中 {n} 项需要重连后生效",
|
|
200
|
+
details: "详情",
|
|
201
|
+
expand: "展开",
|
|
202
|
+
collapse: "收起",
|
|
203
|
+
collapsedCount: "{n} 项",
|
|
204
|
+
collapsedClean: "一般不用改",
|
|
205
|
+
collapsedChanged: "{n} 项已在面板修改",
|
|
206
|
+
restartTag: "需重连",
|
|
197
207
|
listAdd: "添加",
|
|
198
208
|
listRemove: "移除",
|
|
199
209
|
listEntry: "第 {n} 项",
|
|
@@ -205,62 +215,58 @@ window.__ModuleLoader__.load({
|
|
|
205
215
|
directoryPicked: "已选择目录",
|
|
206
216
|
directoryPickFailed: "无法打开系统目录选择器",
|
|
207
217
|
saved: "已保存",
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
saveInvalid: "请先修正标红的配置项",
|
|
219
|
+
numberInvalid: "请输入数字",
|
|
220
|
+
numberMin: "不得小于 {n}",
|
|
221
|
+
numberMax: "不得大于 {n}",
|
|
222
|
+
modelFootnote: "对之后新建的会话生效。",
|
|
223
|
+
catalogFailed: "模型目录加载失败,已退回手填。",
|
|
224
|
+
catalogLoading: "正在读取宿主机选项…",
|
|
225
|
+
catalogUnavailable: "宿主机未提供可选项,请手填。",
|
|
226
|
+
restartBanner: "以下配置要重启飞书连接后生效:",
|
|
227
|
+
restartWarning: "重启会断开当前飞书连接并中断进行中的回复,确定现在重启?",
|
|
228
|
+
reconnect: "重启连接",
|
|
218
229
|
reconnecting: "重连中…",
|
|
219
|
-
refresh: "
|
|
230
|
+
refresh: "刷新",
|
|
220
231
|
retry: "重试",
|
|
221
232
|
pollFailed: "状态刷新失败",
|
|
222
233
|
connectDone: "已连接飞书",
|
|
223
234
|
reconnectDone: "已重启飞书连接",
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
disabled: "已关闭",
|
|
227
|
-
credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
|
|
228
|
-
analyzeTokenCard: "日志分析接口令牌",
|
|
229
|
-
analyzeTokenCardHelp: "日志平台调用分析接口时 Authorization: Bearer 后面的值。经 DSH 凭据服务写入本机凭据库,面板永不回显;保存或移除后下一次请求即生效,无需重连。未配置时接口对所有请求返回 503。",
|
|
235
|
+
loading: "加载中…",
|
|
236
|
+
loadFailed: "设置面板加载失败",
|
|
230
237
|
analyzeTokenLabel: "访问令牌",
|
|
238
|
+
analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
|
|
231
239
|
analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
|
|
232
|
-
saveAnalyzeToken: "保存令牌",
|
|
233
240
|
analyzeTokenSaved: "令牌已保存,下一次请求生效",
|
|
234
241
|
analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
|
|
235
|
-
|
|
236
|
-
loading: "加载中…",
|
|
237
|
-
loadFailed: "设置面板加载失败",
|
|
238
|
-
catalogFailed: "模型目录加载失败,已退回手填。",
|
|
239
|
-
catalogLoading: "正在读取宿主机选项…",
|
|
240
|
-
catalogUnavailable: "宿主机未提供可选项,请手填。",
|
|
241
|
-
version: "插件版本",
|
|
242
|
+
overlayTitle: "Web 覆盖",
|
|
242
243
|
overlayPath: "覆盖文件",
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
244
|
+
overlayCount: "{n} 项配置由面板覆盖",
|
|
245
|
+
noOverrides: "当前没有面板覆盖的配置",
|
|
246
|
+
overlayPathHelp: "面板保存的配置写在这个 JSON 文件里,优先级高于 profile。可以直接编辑;面板保存会整体重写它。",
|
|
247
|
+
clear: "恢复 profile 配置",
|
|
248
|
+
clearing: "恢复中…",
|
|
249
|
+
clearConfirm: "确定删除所有面板覆盖,恢复为 profile 配置?",
|
|
250
|
+
cleared: "已恢复为 profile 配置",
|
|
251
|
+
debugModeWarning: "调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。",
|
|
252
|
+
accessAdminMissingWarning: "没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。",
|
|
253
|
+
accessAdminCurrent: "当前管理员",
|
|
254
|
+
accessAdminSourceConfig: "已配置",
|
|
255
|
+
accessAdminSourceOwner: "应用所有者",
|
|
256
|
+
profilesWarning: "部分飞书名字和头像未能读取:{reason}",
|
|
257
|
+
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
258
|
+
accessRequestsTitle: "待处理申请",
|
|
252
259
|
accessGroupsTitle: "已授权群",
|
|
253
|
-
accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
|
|
254
260
|
accessUsersTitle: "已授权用户",
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
|
|
261
|
+
accessGroupsEmpty: "还没有授权任何群",
|
|
262
|
+
accessUsersEmpty: "还没有授权任何用户",
|
|
263
|
+
accessGroupChatIdPlaceholder: "chat_id(oc_…,群里发 /groupid 获取)",
|
|
259
264
|
accessGroupNamePlaceholder: "群名(可选)",
|
|
260
|
-
accessUserIdPlaceholder: "
|
|
265
|
+
accessUserIdPlaceholder: "user_id(私聊机器人发 /userid 获取)",
|
|
261
266
|
accessUserNamePlaceholder: "昵称(可选)",
|
|
262
267
|
accessAddGroup: "添加群",
|
|
263
268
|
accessAddUser: "添加用户",
|
|
269
|
+
accessAdd: "添加",
|
|
264
270
|
accessRemove: "移除",
|
|
265
271
|
accessAllow: "授权",
|
|
266
272
|
accessDeny: "忽略",
|
|
@@ -268,24 +274,99 @@ window.__ModuleLoader__.load({
|
|
|
268
274
|
accessSourceCommand: "对话命令",
|
|
269
275
|
accessSourceCard: "授权卡",
|
|
270
276
|
accessSourcePanel: "设置面板",
|
|
271
|
-
accessRequestGroup: "
|
|
272
|
-
accessRequestUser: "
|
|
273
|
-
|
|
274
|
-
accessRequestExpires: "
|
|
277
|
+
accessRequestGroup: "申请授权群",
|
|
278
|
+
accessRequestUser: "申请使用",
|
|
279
|
+
accessRequestIn: "在「{group}」",
|
|
280
|
+
accessRequestExpires: "{date} 前有效",
|
|
275
281
|
accessGroupAdded: "已添加群授权",
|
|
276
282
|
accessGroupRemoved: "已移除群授权",
|
|
277
283
|
accessUserAdded: "已添加用户授权",
|
|
278
284
|
accessUserRemoved: "已移除用户授权",
|
|
279
285
|
accessRequestAllowed: "已授权",
|
|
280
286
|
accessRequestDenied: "已忽略",
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
wikiUsersTitle: "可使用的人",
|
|
288
|
+
wikiUsersAdminPlus: "管理员 + {n}",
|
|
289
|
+
wikiUsersEmpty: "目前只有管理员发起的话题能用知识库工具",
|
|
290
|
+
wikiUsersHint: "也可以在飞书发 /auth wiki add @用户",
|
|
285
291
|
wikiUserAdded: "已加入知识库白名单",
|
|
286
|
-
wikiUserRemoved: "已移出知识库白名单"
|
|
287
|
-
accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。"
|
|
292
|
+
wikiUserRemoved: "已移出知识库白名单"
|
|
288
293
|
};
|
|
294
|
+
/**
|
|
295
|
+
* Fill `{name}` placeholders of one `PANEL_COPY` template.
|
|
296
|
+
*
|
|
297
|
+
* @param template - Copy containing `{name}` placeholders.
|
|
298
|
+
* @param values - Replacement per placeholder name.
|
|
299
|
+
* @returns The filled string; unknown placeholders stay as written.
|
|
300
|
+
*/
|
|
301
|
+
function fillCopy(template, values) {
|
|
302
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
303
|
+
const value = values[name];
|
|
304
|
+
return value === void 0 ? match : String(value);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/client/icon.tsx
|
|
309
|
+
/**
|
|
310
|
+
* Paper-plane glyph drawn in `currentColor`, so it follows the row's text color.
|
|
311
|
+
* @param props - the sidebar's icon share.
|
|
312
|
+
* @returns decorative icon.
|
|
313
|
+
*/
|
|
314
|
+
function FeishuChannelsIcon({ size }) {
|
|
315
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
316
|
+
width: size,
|
|
317
|
+
height: size,
|
|
318
|
+
viewBox: "0 0 24 24",
|
|
319
|
+
fill: "none",
|
|
320
|
+
stroke: "currentColor",
|
|
321
|
+
strokeWidth: 1.6,
|
|
322
|
+
strokeLinecap: "round",
|
|
323
|
+
strokeLinejoin: "round",
|
|
324
|
+
"aria-hidden": "true",
|
|
325
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 3 3.5 10.2a.6.6 0 0 0 0 1.1l6.6 2.6 2.6 6.6a.6.6 0 0 0 1.1 0L21 3Z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m10.1 13.9 4.4-4.4" })]
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/client/avatar.tsx
|
|
330
|
+
/**
|
|
331
|
+
* Round Feishu avatar: the user or chat image when Feishu returned one,
|
|
332
|
+
* otherwise (and when the image fails to load) the first character of the
|
|
333
|
+
* display text.
|
|
334
|
+
*
|
|
335
|
+
* @module dsh-channels/client/avatar
|
|
336
|
+
*/
|
|
337
|
+
/**
|
|
338
|
+
* One avatar badge. Decorative: the adjacent label names the user or chat.
|
|
339
|
+
* @param props - `text` supplies the fallback initial, `url` the image, `size` the diameter in px (default 26).
|
|
340
|
+
* @returns the badge.
|
|
341
|
+
*/
|
|
342
|
+
function Avatar(props) {
|
|
343
|
+
const { text, url, size = 26 } = props;
|
|
344
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
345
|
+
(0, react.useEffect)(() => {
|
|
346
|
+
setFailed(false);
|
|
347
|
+
}, [url]);
|
|
348
|
+
const style = {
|
|
349
|
+
width: size,
|
|
350
|
+
height: size,
|
|
351
|
+
fontSize: Math.round(size * .46)
|
|
352
|
+
};
|
|
353
|
+
if (url !== void 0 && !failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
354
|
+
className: "dsh-channels-avatar",
|
|
355
|
+
style,
|
|
356
|
+
src: url,
|
|
357
|
+
alt: "",
|
|
358
|
+
"aria-hidden": "true",
|
|
359
|
+
referrerPolicy: "no-referrer",
|
|
360
|
+
loading: "lazy",
|
|
361
|
+
onError: () => setFailed(true)
|
|
362
|
+
});
|
|
363
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
364
|
+
className: "dsh-channels-avatar",
|
|
365
|
+
style,
|
|
366
|
+
"aria-hidden": "true",
|
|
367
|
+
children: Array.from(text)[0] ?? "?"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
289
370
|
//#endregion
|
|
290
371
|
//#region src/client/api.ts
|
|
291
372
|
/**
|
|
@@ -305,9 +386,15 @@ window.__ModuleLoader__.load({
|
|
|
305
386
|
} catch {}
|
|
306
387
|
return `HTTP ${status}`;
|
|
307
388
|
}
|
|
308
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Fetch one Host route. Routes are registered as absolute pathnames, but the
|
|
391
|
+
* browser addresses them relative to the DSH document (`<base href="./">`), so
|
|
392
|
+
* a reverse proxy that mounts DSH under a prefix such as `/dsh/` still routes
|
|
393
|
+
* them to DSH with the login cookie it scoped to that prefix.
|
|
394
|
+
*/
|
|
395
|
+
async function request(route, init) {
|
|
309
396
|
try {
|
|
310
|
-
return await fetch(
|
|
397
|
+
return await fetch(route.slice(1), {
|
|
311
398
|
cache: "no-store",
|
|
312
399
|
credentials: "same-origin",
|
|
313
400
|
...init,
|
|
@@ -333,6 +420,41 @@ window.__ModuleLoader__.load({
|
|
|
333
420
|
async function fetchStatus() {
|
|
334
421
|
return readJson(await request(STATUS_ROUTE));
|
|
335
422
|
}
|
|
423
|
+
/** Feishu origin of one Session (`origin: null` when Feishu never routed it). */
|
|
424
|
+
async function fetchSessionOrigin(sessionId) {
|
|
425
|
+
return readJson(await request(`${SESSION_ORIGIN_ROUTE}?${new URLSearchParams({ sessionId }).toString()}`));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Live Feishu names and avatars of the given users and chats, split into
|
|
429
|
+
* requests of at most {@link PROFILES_MAX_IDS} ids per list and merged.
|
|
430
|
+
* @param userIds - user `user_id`s.
|
|
431
|
+
* @param chatIds - chat `chat_id`s.
|
|
432
|
+
* @returns the profiles Feishu resolved, by id, with every request's warning.
|
|
433
|
+
*/
|
|
434
|
+
async function fetchFeishuProfiles(userIds, chatIds) {
|
|
435
|
+
const requests = [];
|
|
436
|
+
const rounds = Math.max(1, Math.ceil(userIds.length / 200), Math.ceil(chatIds.length / 200));
|
|
437
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
438
|
+
const window = (ids) => ids.slice(round * 200, (round + 1) * 200).join(",");
|
|
439
|
+
const query = new URLSearchParams({
|
|
440
|
+
users: window(userIds),
|
|
441
|
+
chats: window(chatIds)
|
|
442
|
+
}).toString();
|
|
443
|
+
requests.push(request(`${PROFILES_ROUTE}?${query}`).then((response) => readJson(response)));
|
|
444
|
+
}
|
|
445
|
+
const merged = {
|
|
446
|
+
users: {},
|
|
447
|
+
chats: {}
|
|
448
|
+
};
|
|
449
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const part of await Promise.all(requests)) {
|
|
451
|
+
Object.assign(merged.users, part.users);
|
|
452
|
+
Object.assign(merged.chats, part.chats);
|
|
453
|
+
if (part.warning !== void 0) warnings.add(part.warning);
|
|
454
|
+
}
|
|
455
|
+
if (warnings.size > 0) merged.warning = [...warnings].join(";");
|
|
456
|
+
return merged;
|
|
457
|
+
}
|
|
336
458
|
/**
|
|
337
459
|
* Model-selector catalog for one route: providers, the provider's models, the
|
|
338
460
|
* model's reasoning efforts, and the preset roster. The client re-queries it
|
|
@@ -509,6 +631,257 @@ window.__ModuleLoader__.load({
|
|
|
509
631
|
});
|
|
510
632
|
}
|
|
511
633
|
//#endregion
|
|
634
|
+
//#region src/client/origin-cache.ts
|
|
635
|
+
/**
|
|
636
|
+
* How long a "not from Feishu" answer is reused. A Session shows up in the
|
|
637
|
+
* Web list before its Feishu route is written, so an early null must expire.
|
|
638
|
+
*/
|
|
639
|
+
const NULL_ORIGIN_TTL_MS = 15e3;
|
|
640
|
+
/** One settled or pending origin lookup per Session; failures are not kept. */
|
|
641
|
+
const lookups = /* @__PURE__ */ new Map();
|
|
642
|
+
/**
|
|
643
|
+
* Origin of one Session through the shared cache. An origin is kept for the
|
|
644
|
+
* page's lifetime; a null answer only for {@link NULL_ORIGIN_TTL_MS}.
|
|
645
|
+
* @param sessionId - DSH session id.
|
|
646
|
+
* @param refresh - drop the cached answer and ask the Host again.
|
|
647
|
+
* @returns the origin, or null for a Session Feishu never routed.
|
|
648
|
+
*/
|
|
649
|
+
function loadSessionOrigin(sessionId, refresh = false) {
|
|
650
|
+
const cached = refresh ? void 0 : lookups.get(sessionId);
|
|
651
|
+
if (cached !== void 0 && (cached.nullAt === void 0 || Date.now() - cached.nullAt < NULL_ORIGIN_TTL_MS)) return cached.pending;
|
|
652
|
+
const pending = fetchSessionOrigin(sessionId).then((response) => response.origin);
|
|
653
|
+
const lookup = { pending };
|
|
654
|
+
lookups.set(sessionId, lookup);
|
|
655
|
+
pending.then((origin) => {
|
|
656
|
+
if (origin === null) lookup.nullAt = Date.now();
|
|
657
|
+
}, () => {
|
|
658
|
+
if (lookups.get(sessionId) === lookup) lookups.delete(sessionId);
|
|
659
|
+
});
|
|
660
|
+
return pending;
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/client/origin.tsx
|
|
664
|
+
/**
|
|
665
|
+
* 对话里的「飞书来源」:会话头部的入口按钮(只在飞书发起的会话里出现)与
|
|
666
|
+
* 右侧栏的来源标签页。两者共用一份按会话缓存的来源查询。
|
|
667
|
+
*
|
|
668
|
+
* @module dsh-channels/client/origin
|
|
669
|
+
*/
|
|
670
|
+
/** Right-sidebar tab kind opened by the header button. */
|
|
671
|
+
const ORIGIN_TAB_KIND = "feishu-origin";
|
|
672
|
+
/** Tab-type implementation id and keyed body/title slot key. */
|
|
673
|
+
const ORIGIN_TAB_ID = "@modesto_ma/dsh-channels/origin";
|
|
674
|
+
/** Tab chip text. */
|
|
675
|
+
const ORIGIN_TAB_TITLE = "飞书来源";
|
|
676
|
+
/**
|
|
677
|
+
* Load one Session's origin; `reload` asks the Host again. `fresh` skips the
|
|
678
|
+
* shared cache on the first load too, so names and avatars are current.
|
|
679
|
+
*/
|
|
680
|
+
function useSessionOrigin(sessionId, fresh = false) {
|
|
681
|
+
const [state, setState] = (0, react.useState)({ phase: "loading" });
|
|
682
|
+
const [reloadOf, setReloadOf] = (0, react.useState)({
|
|
683
|
+
sessionId,
|
|
684
|
+
ticket: 0
|
|
685
|
+
});
|
|
686
|
+
const ticket = reloadOf.sessionId === sessionId ? reloadOf.ticket : 0;
|
|
687
|
+
(0, react.useEffect)(() => {
|
|
688
|
+
let live = true;
|
|
689
|
+
setState((current) => current.phase === "ready" && ticket > 0 ? current : { phase: "loading" });
|
|
690
|
+
loadSessionOrigin(sessionId, fresh || ticket > 0).then((origin) => {
|
|
691
|
+
if (live) setState({
|
|
692
|
+
phase: "ready",
|
|
693
|
+
origin
|
|
694
|
+
});
|
|
695
|
+
}, (error) => {
|
|
696
|
+
if (live) setState({
|
|
697
|
+
phase: "error",
|
|
698
|
+
message: error instanceof Error ? error.message : String(error)
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
return () => {
|
|
702
|
+
live = false;
|
|
703
|
+
};
|
|
704
|
+
}, [
|
|
705
|
+
fresh,
|
|
706
|
+
sessionId,
|
|
707
|
+
ticket
|
|
708
|
+
]);
|
|
709
|
+
return [state, (0, react.useCallback)(() => {
|
|
710
|
+
setReloadOf((current) => ({
|
|
711
|
+
sessionId,
|
|
712
|
+
ticket: current.sessionId === sessionId ? current.ticket + 1 : 1
|
|
713
|
+
}));
|
|
714
|
+
}, [sessionId])];
|
|
715
|
+
}
|
|
716
|
+
/** Header chip text: group name, or 私聊 with the peer's name. */
|
|
717
|
+
function sourceLabel$1(origin) {
|
|
718
|
+
if (origin.chatType === "p2p") return origin.senderName === void 0 ? "飞书私聊" : `私聊 · ${origin.senderName}`;
|
|
719
|
+
return origin.chatName === void 0 ? "飞书群聊" : origin.chatName;
|
|
720
|
+
}
|
|
721
|
+
/** Tab heading: the group, or the private-chat peer, with its avatar. */
|
|
722
|
+
function originHeading(origin) {
|
|
723
|
+
if (origin.chatType === "p2p") return {
|
|
724
|
+
title: origin.senderName ?? "飞书私聊",
|
|
725
|
+
...origin.senderAvatarUrl === void 0 ? {} : { avatarUrl: origin.senderAvatarUrl }
|
|
726
|
+
};
|
|
727
|
+
return {
|
|
728
|
+
title: origin.chatName ?? "飞书群聊",
|
|
729
|
+
...origin.chatAvatarUrl === void 0 ? {} : { avatarUrl: origin.chatAvatarUrl }
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** Times a mounted header re-asks after a null answer, and the delay between asks. */
|
|
733
|
+
const HEADER_NULL_RETRIES = 3;
|
|
734
|
+
const HEADER_NULL_RETRY_MS = 5e3;
|
|
735
|
+
/**
|
|
736
|
+
* Header button shown only for Sessions Feishu routed; renders nothing for
|
|
737
|
+
* every other Session and while the lookup is pending or failed.
|
|
738
|
+
* @param props - session id and the tab opener.
|
|
739
|
+
* @returns the button or null.
|
|
740
|
+
*/
|
|
741
|
+
function OriginHeaderAction({ sessionId, openOrigin }) {
|
|
742
|
+
const [state, reload] = useSessionOrigin(sessionId);
|
|
743
|
+
const [retries, setRetries] = (0, react.useState)(0);
|
|
744
|
+
(0, react.useEffect)(() => {
|
|
745
|
+
setRetries(0);
|
|
746
|
+
}, [sessionId]);
|
|
747
|
+
const waiting = state.phase === "ready" && state.origin === null && retries < HEADER_NULL_RETRIES;
|
|
748
|
+
(0, react.useEffect)(() => {
|
|
749
|
+
if (!waiting) return;
|
|
750
|
+
const timer = setTimeout(() => {
|
|
751
|
+
setRetries((value) => value + 1);
|
|
752
|
+
reload();
|
|
753
|
+
}, HEADER_NULL_RETRY_MS);
|
|
754
|
+
return () => {
|
|
755
|
+
clearTimeout(timer);
|
|
756
|
+
};
|
|
757
|
+
}, [waiting, reload]);
|
|
758
|
+
if (state.phase !== "ready" || state.origin === null) return null;
|
|
759
|
+
const label = sourceLabel$1(state.origin);
|
|
760
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
761
|
+
type: "button",
|
|
762
|
+
className: "dsh-channels-origin-chip",
|
|
763
|
+
title: `来自${label},点击查看飞书来源`,
|
|
764
|
+
"aria-label": `飞书来源:${label}`,
|
|
765
|
+
onClick: openOrigin,
|
|
766
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsIcon, {
|
|
767
|
+
size: 13,
|
|
768
|
+
active: false
|
|
769
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
+
className: "dsh-channels-origin-chip__text",
|
|
771
|
+
children: label
|
|
772
|
+
})]
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function formatTime(ms) {
|
|
776
|
+
const date = new Date(ms);
|
|
777
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
778
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Right-sidebar body: where this Session came from in Feishu.
|
|
782
|
+
* @param props - the tab's Session.
|
|
783
|
+
* @returns the origin page.
|
|
784
|
+
*/
|
|
785
|
+
function OriginTab({ sessionId }) {
|
|
786
|
+
const [state, reload] = useSessionOrigin(sessionId, true);
|
|
787
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
788
|
+
className: "dsh-channels-origin",
|
|
789
|
+
children: [
|
|
790
|
+
state.phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
791
|
+
className: "dsh-channels-origin__muted",
|
|
792
|
+
children: "正在读取飞书来源…"
|
|
793
|
+
}) : null,
|
|
794
|
+
state.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
795
|
+
className: "dsh-channels-origin__error",
|
|
796
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["读取飞书来源失败:", state.message] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
797
|
+
type: "button",
|
|
798
|
+
className: "dsh-channels-origin__link",
|
|
799
|
+
onClick: reload,
|
|
800
|
+
children: "重试"
|
|
801
|
+
})]
|
|
802
|
+
}) : null,
|
|
803
|
+
state.phase === "ready" && state.origin === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
804
|
+
className: "dsh-channels-origin__muted",
|
|
805
|
+
children: ["这个会话不是从飞书发起的。", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
806
|
+
type: "button",
|
|
807
|
+
className: "dsh-channels-origin__link",
|
|
808
|
+
onClick: reload,
|
|
809
|
+
children: "刷新"
|
|
810
|
+
})]
|
|
811
|
+
}) : null,
|
|
812
|
+
state.phase === "ready" && state.origin !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OriginDetails, {
|
|
813
|
+
origin: state.origin,
|
|
814
|
+
onRefresh: reload
|
|
815
|
+
}) : null
|
|
816
|
+
]
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function OriginDetails({ origin, onRefresh }) {
|
|
820
|
+
const p2p = origin.chatType === "p2p";
|
|
821
|
+
const rows = [["来源", p2p ? "私聊" : origin.kind === "thread" ? "群聊 · 话题" : "群聊"]];
|
|
822
|
+
if (!p2p && origin.chatName === void 0) rows.push(["群 ID", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
823
|
+
className: "dsh-channels-origin__mono",
|
|
824
|
+
children: origin.chatId
|
|
825
|
+
})]);
|
|
826
|
+
rows.push([origin.shared ? "请求人" : "发起人", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
827
|
+
className: "dsh-channels-origin__who",
|
|
828
|
+
children: [
|
|
829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
830
|
+
text: origin.senderName ?? origin.senderUserId,
|
|
831
|
+
url: origin.senderAvatarUrl,
|
|
832
|
+
size: 20
|
|
833
|
+
}),
|
|
834
|
+
origin.senderName === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: origin.senderName }),
|
|
835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
836
|
+
className: "dsh-channels-origin__mono dsh-channels-origin__muted",
|
|
837
|
+
children: origin.senderUserId
|
|
838
|
+
})
|
|
839
|
+
]
|
|
840
|
+
})]);
|
|
841
|
+
const heading = originHeading(origin);
|
|
842
|
+
if (origin.shared) rows.push(["说明", "日志平台分析话题,群内成员都可以继续"]);
|
|
843
|
+
rows.push(["开始于", formatTime(origin.createdAt)]);
|
|
844
|
+
rows.push(origin.bound && origin.lastActivityAt !== void 0 ? ["最近活跃", formatTime(origin.lastActivityAt)] : ["状态", "飞书侧已切换到新会话或已过期"]);
|
|
845
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
847
|
+
className: "dsh-channels-origin__head",
|
|
848
|
+
children: [heading.avatarUrl === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsIcon, {
|
|
849
|
+
size: 16,
|
|
850
|
+
active: false
|
|
851
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
852
|
+
text: heading.title,
|
|
853
|
+
url: heading.avatarUrl,
|
|
854
|
+
size: 28
|
|
855
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
856
|
+
className: "dsh-channels-origin__title",
|
|
857
|
+
children: heading.title
|
|
858
|
+
})]
|
|
859
|
+
}),
|
|
860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
|
|
861
|
+
className: "dsh-channels-origin__list",
|
|
862
|
+
children: rows.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
863
|
+
className: "dsh-channels-origin__row",
|
|
864
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: value })]
|
|
865
|
+
}, label))
|
|
866
|
+
}),
|
|
867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
868
|
+
className: "dsh-channels-origin__actions",
|
|
869
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
870
|
+
className: "dsh-channels-origin__button",
|
|
871
|
+
href: origin.chatLink,
|
|
872
|
+
target: "_blank",
|
|
873
|
+
rel: "noreferrer",
|
|
874
|
+
children: "在飞书中打开"
|
|
875
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
876
|
+
type: "button",
|
|
877
|
+
className: "dsh-channels-origin__link",
|
|
878
|
+
onClick: onRefresh,
|
|
879
|
+
children: "刷新"
|
|
880
|
+
})]
|
|
881
|
+
})
|
|
882
|
+
] });
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
512
885
|
//#region src/client/catalog.ts
|
|
513
886
|
/**
|
|
514
887
|
* 浏览器半区的设置面板选择器目录折叠:把 Host 下发的字段描述与实时目录
|
|
@@ -602,6 +975,110 @@ window.__ModuleLoader__.load({
|
|
|
602
975
|
});
|
|
603
976
|
}
|
|
604
977
|
//#endregion
|
|
978
|
+
//#region src/client/number-field.ts
|
|
979
|
+
/**
|
|
980
|
+
* 浏览器半区的 `number` 字段辅助:按 Host 描述的 `scale` 在显示单位与存储
|
|
981
|
+
* 单位之间换算、保存前校验上下界,以及字节数的 MiB 读数。
|
|
982
|
+
*
|
|
983
|
+
* 不含 JSX/React,便于单测。Host 保存时按 config schema 复核。
|
|
984
|
+
*
|
|
985
|
+
* @module dsh-channels/client/number-field
|
|
986
|
+
*/
|
|
987
|
+
/** Drop float noise from `stored / scale` (e.g. 0.30000000000000004). */
|
|
988
|
+
function tidy(value) {
|
|
989
|
+
return Number(value.toPrecision(12));
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Value the input shows for one field.
|
|
993
|
+
*
|
|
994
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
995
|
+
* @param value - Draft (the typed string) or effective value (stored units).
|
|
996
|
+
* @returns The input text.
|
|
997
|
+
*/
|
|
998
|
+
function numberInputValue(field, value) {
|
|
999
|
+
if (typeof value === "string") return value;
|
|
1000
|
+
if (typeof value !== "number") return "";
|
|
1001
|
+
return String(field.scale === void 0 ? value : tidy(value / field.scale));
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Input `step` in display units.
|
|
1005
|
+
*
|
|
1006
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
1007
|
+
* @returns The step, or `undefined` when the Host gives none.
|
|
1008
|
+
*/
|
|
1009
|
+
function numberInputStep(field) {
|
|
1010
|
+
if (field.step === void 0) return void 0;
|
|
1011
|
+
return field.scale === void 0 ? field.step : tidy(field.step / field.scale);
|
|
1012
|
+
}
|
|
1013
|
+
/** One bound in display units with the field's unit appended. */
|
|
1014
|
+
function boundLabel(field, stored) {
|
|
1015
|
+
const shown = field.scale === void 0 ? stored : tidy(stored / field.scale);
|
|
1016
|
+
return field.unit === void 0 ? String(shown) : `${shown} ${field.unit}`;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Turn one number draft into its save payload, checking the Host bounds.
|
|
1020
|
+
*
|
|
1021
|
+
* @param field - The Host descriptor (`kind: 'number'`).
|
|
1022
|
+
* @param raw - The draft: the typed string, or a stored-unit number.
|
|
1023
|
+
* @returns The stored-unit value (`''` clears an optional field), or the reason it is rejected.
|
|
1024
|
+
*/
|
|
1025
|
+
function numberDraftValue(field, raw) {
|
|
1026
|
+
let stored;
|
|
1027
|
+
if (typeof raw === "number") stored = raw;
|
|
1028
|
+
else {
|
|
1029
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
1030
|
+
if (text === "") return field.optional === true ? {
|
|
1031
|
+
ok: true,
|
|
1032
|
+
value: ""
|
|
1033
|
+
} : {
|
|
1034
|
+
ok: false,
|
|
1035
|
+
message: PANEL_COPY.numberInvalid
|
|
1036
|
+
};
|
|
1037
|
+
const parsed = Number(text);
|
|
1038
|
+
if (!Number.isFinite(parsed)) return {
|
|
1039
|
+
ok: false,
|
|
1040
|
+
message: PANEL_COPY.numberInvalid
|
|
1041
|
+
};
|
|
1042
|
+
stored = field.scale === void 0 ? parsed : Math.round(parsed * field.scale);
|
|
1043
|
+
}
|
|
1044
|
+
if (field.min !== void 0 && stored < field.min) return {
|
|
1045
|
+
ok: false,
|
|
1046
|
+
message: fillCopy(PANEL_COPY.numberMin, { n: boundLabel(field, field.min) })
|
|
1047
|
+
};
|
|
1048
|
+
if (field.max !== void 0 && stored > field.max) return {
|
|
1049
|
+
ok: false,
|
|
1050
|
+
message: fillCopy(PANEL_COPY.numberMax, { n: boundLabel(field, field.max) })
|
|
1051
|
+
};
|
|
1052
|
+
return {
|
|
1053
|
+
ok: true,
|
|
1054
|
+
value: stored
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Human reading of a byte count, e.g. `20 MiB`.
|
|
1059
|
+
*
|
|
1060
|
+
* @param text - The input text (bytes).
|
|
1061
|
+
* @returns The reading, or `undefined` when the text is not a positive number.
|
|
1062
|
+
*/
|
|
1063
|
+
function bytesReading(text) {
|
|
1064
|
+
const bytes = Number(text);
|
|
1065
|
+
if (text.trim() === "" || !Number.isFinite(bytes) || bytes <= 0) return void 0;
|
|
1066
|
+
const units = [
|
|
1067
|
+
"B",
|
|
1068
|
+
"KiB",
|
|
1069
|
+
"MiB",
|
|
1070
|
+
"GiB",
|
|
1071
|
+
"TiB"
|
|
1072
|
+
];
|
|
1073
|
+
let value = bytes;
|
|
1074
|
+
let unit = 0;
|
|
1075
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1076
|
+
value /= 1024;
|
|
1077
|
+
unit += 1;
|
|
1078
|
+
}
|
|
1079
|
+
return `${Number(value.toFixed(value >= 100 || unit === 0 ? 0 : 1))} ${units[unit]}`;
|
|
1080
|
+
}
|
|
1081
|
+
//#endregion
|
|
605
1082
|
//#region src/feishu/wiki/links.ts
|
|
606
1083
|
/**
|
|
607
1084
|
* Feishu wiki (知识库) link parsing: the `wikiRoots` allow-list entries and
|
|
@@ -801,8 +1278,8 @@ window.__ModuleLoader__.load({
|
|
|
801
1278
|
//#endregion
|
|
802
1279
|
//#region src/client/panel/shared.tsx
|
|
803
1280
|
/**
|
|
804
|
-
* 设置面板共享层:页面状态类型、Host 字段渲染原语(
|
|
805
|
-
* SaveBar
|
|
1281
|
+
* 设置面板共享层:页面状态类型、Host 字段渲染原语(SettingRow /
|
|
1282
|
+
* FieldSections / SaveBar)、分区与列表原语、生命周期徽标与纯格式化工具。
|
|
806
1283
|
*
|
|
807
1284
|
* 这一层不发请求、不持有页面状态,只把 Host 下发的 wire 数据渲染成控件;
|
|
808
1285
|
* 页面外壳在 `index.tsx`,各 tab 只组合这些原语。
|
|
@@ -812,7 +1289,7 @@ window.__ModuleLoader__.load({
|
|
|
812
1289
|
function messageOf(error) {
|
|
813
1290
|
return error instanceof Error ? error.message : String(error);
|
|
814
1291
|
}
|
|
815
|
-
/** Compact duration for the
|
|
1292
|
+
/** Compact duration for the page header. */
|
|
816
1293
|
function formatUptime(ms) {
|
|
817
1294
|
if (ms === void 0 || !Number.isFinite(ms)) return PANEL_COPY.unknownValue;
|
|
818
1295
|
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
@@ -832,21 +1309,9 @@ window.__ModuleLoader__.load({
|
|
|
832
1309
|
function fieldLabel(fields, key) {
|
|
833
1310
|
return fields.find((field) => field.key === key)?.label ?? key;
|
|
834
1311
|
}
|
|
835
|
-
/** Effective access-mode label, reusing the Host field options for consistency. */
|
|
836
|
-
function accessModeLabel(panel, mode) {
|
|
837
|
-
return panel.config.fields.find((item) => item.key === "accessMode")?.options?.find((option) => option.value === mode)?.label ?? mode;
|
|
838
|
-
}
|
|
839
|
-
/** Numeric draft kept as the raw input string; parsed on save. */
|
|
840
|
-
function numericDraft(field, raw) {
|
|
841
|
-
if (typeof raw === "number") return raw;
|
|
842
|
-
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
843
|
-
const parsed = Number(raw);
|
|
844
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
845
|
-
}
|
|
846
1312
|
/**
|
|
847
1313
|
* Turn the dirty draft into the wire payload, keeping the Host-descriptor
|
|
848
|
-
* bounds check client-side
|
|
849
|
-
* every value on save).
|
|
1314
|
+
* bounds check client-side (the Host re-validates every value on save).
|
|
850
1315
|
*/
|
|
851
1316
|
function validateDraft(panel, draft) {
|
|
852
1317
|
const values = {};
|
|
@@ -855,29 +1320,12 @@ window.__ModuleLoader__.load({
|
|
|
855
1320
|
if (!(field.key in draft)) continue;
|
|
856
1321
|
const raw = draft[field.key];
|
|
857
1322
|
if (field.kind === "number") {
|
|
858
|
-
const parsed =
|
|
859
|
-
if (parsed
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
if (field.min !== void 0 && parsed < field.min) {
|
|
867
|
-
issues.push({
|
|
868
|
-
key: field.key,
|
|
869
|
-
message: `不得小于 ${field.min}`
|
|
870
|
-
});
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (field.max !== void 0 && parsed > field.max) {
|
|
874
|
-
issues.push({
|
|
875
|
-
key: field.key,
|
|
876
|
-
message: `不得大于 ${field.max}`
|
|
877
|
-
});
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
values[field.key] = parsed;
|
|
1323
|
+
const parsed = numberDraftValue(field, raw);
|
|
1324
|
+
if (parsed.ok) values[field.key] = parsed.value;
|
|
1325
|
+
else issues.push({
|
|
1326
|
+
key: field.key,
|
|
1327
|
+
message: parsed.message
|
|
1328
|
+
});
|
|
881
1329
|
continue;
|
|
882
1330
|
}
|
|
883
1331
|
if (field.kind === "boolean") {
|
|
@@ -897,7 +1345,7 @@ window.__ModuleLoader__.load({
|
|
|
897
1345
|
issues
|
|
898
1346
|
};
|
|
899
1347
|
}
|
|
900
|
-
/** Header
|
|
1348
|
+
/** Header pill for the Host-derived lifecycle phase. */
|
|
901
1349
|
function phaseBadge(phase) {
|
|
902
1350
|
switch (phase) {
|
|
903
1351
|
case "connected": return {
|
|
@@ -922,7 +1370,7 @@ window.__ModuleLoader__.load({
|
|
|
922
1370
|
};
|
|
923
1371
|
}
|
|
924
1372
|
}
|
|
925
|
-
/** One-line explanation
|
|
1373
|
+
/** One-line explanation of the current phase (connection tab status block). */
|
|
926
1374
|
function phaseSummary(phase) {
|
|
927
1375
|
switch (phase) {
|
|
928
1376
|
case "connected": return PANEL_COPY.statusSummaryConnected;
|
|
@@ -932,6 +1380,99 @@ window.__ModuleLoader__.load({
|
|
|
932
1380
|
default: return PANEL_COPY.statusSummaryUnconfigured;
|
|
933
1381
|
}
|
|
934
1382
|
}
|
|
1383
|
+
/** Small rounded status pill. */
|
|
1384
|
+
function Pill(props) {
|
|
1385
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1386
|
+
className: `dsh-channels-cfg__pill dsh-channels-cfg__pill--${props.tone}`,
|
|
1387
|
+
children: [props.dot === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__dot" }) : null, props.children]
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/** One titled section: a muted heading above a bordered row list. */
|
|
1391
|
+
function Section(props) {
|
|
1392
|
+
const { title, footnote, tone, children } = props;
|
|
1393
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1394
|
+
className: "dsh-channels-cfg__sec",
|
|
1395
|
+
children: [
|
|
1396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1397
|
+
className: "dsh-channels-cfg__secTitle",
|
|
1398
|
+
children: title
|
|
1399
|
+
}),
|
|
1400
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1401
|
+
className: `dsh-channels-cfg__list${tone === "warn" ? " dsh-channels-cfg__list--warn" : ""}`,
|
|
1402
|
+
children
|
|
1403
|
+
}),
|
|
1404
|
+
footnote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1405
|
+
className: "dsh-channels-cfg__secFoot",
|
|
1406
|
+
children: footnote
|
|
1407
|
+
})
|
|
1408
|
+
]
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* One setting row: label (with tags and the「详情」toggle) and a one-line
|
|
1413
|
+
* summary on the left, the control on the right. `details` renders below the
|
|
1414
|
+
* row once expanded; `wide` puts the control under the text.
|
|
1415
|
+
*/
|
|
1416
|
+
function Row(props) {
|
|
1417
|
+
const { label, tags, summary, details, control, error, wide = false, leading } = props;
|
|
1418
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1419
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1420
|
+
className: "dsh-channels-cfg__rowWrap",
|
|
1421
|
+
children: [
|
|
1422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1423
|
+
className: `dsh-channels-cfg__row${wide ? " dsh-channels-cfg__row--wide" : ""}`,
|
|
1424
|
+
children: [
|
|
1425
|
+
leading,
|
|
1426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1427
|
+
className: "dsh-channels-cfg__rowText",
|
|
1428
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1429
|
+
className: "dsh-channels-cfg__rowLabel",
|
|
1430
|
+
children: [
|
|
1431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
|
|
1432
|
+
tags,
|
|
1433
|
+
details === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1434
|
+
type: "button",
|
|
1435
|
+
className: "dsh-channels-cfg__more",
|
|
1436
|
+
"aria-expanded": open,
|
|
1437
|
+
onClick: () => setOpen(!open),
|
|
1438
|
+
children: PANEL_COPY.details
|
|
1439
|
+
})
|
|
1440
|
+
]
|
|
1441
|
+
}), summary === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1442
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
1443
|
+
children: summary
|
|
1444
|
+
})]
|
|
1445
|
+
}),
|
|
1446
|
+
control === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1447
|
+
className: "dsh-channels-cfg__rowCtl",
|
|
1448
|
+
children: control
|
|
1449
|
+
})
|
|
1450
|
+
]
|
|
1451
|
+
}),
|
|
1452
|
+
open && details !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1453
|
+
className: "dsh-channels-cfg__details",
|
|
1454
|
+
children: details
|
|
1455
|
+
}) : null,
|
|
1456
|
+
error === void 0 || error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1457
|
+
className: "dsh-channels-cfg__rowError",
|
|
1458
|
+
children: error
|
|
1459
|
+
})
|
|
1460
|
+
]
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
/** Accessible on/off switch. */
|
|
1464
|
+
function Switch(props) {
|
|
1465
|
+
const { checked, label, disabled = false, onChange } = props;
|
|
1466
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1467
|
+
type: "button",
|
|
1468
|
+
role: "switch",
|
|
1469
|
+
className: "dsh-channels-cfg__switch",
|
|
1470
|
+
"aria-checked": checked,
|
|
1471
|
+
"aria-label": label,
|
|
1472
|
+
disabled,
|
|
1473
|
+
onClick: () => onChange(!checked)
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
935
1476
|
/**
|
|
936
1477
|
* Text input paired with the host-native「选择目录…」button. The button owns
|
|
937
1478
|
* its own busy state: the OS dialog can stay open for a long time, so the
|
|
@@ -960,7 +1501,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1501
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
961
1502
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
962
1503
|
type: "button",
|
|
963
|
-
className: "dsh-channels-cfg__btn
|
|
1504
|
+
className: "dsh-channels-cfg__btn",
|
|
964
1505
|
disabled: disabled || picking,
|
|
965
1506
|
onClick: () => {
|
|
966
1507
|
pick();
|
|
@@ -971,7 +1512,7 @@ window.__ModuleLoader__.load({
|
|
|
971
1512
|
}
|
|
972
1513
|
/**
|
|
973
1514
|
* `string-list` control: one single-line input per entry, each with a remove
|
|
974
|
-
* button, and an add
|
|
1515
|
+
* button, and an add link below (disabled at `maxItems` inputs). An empty
|
|
975
1516
|
* list renders one blank input so the first entry can be typed directly.
|
|
976
1517
|
* Issues carrying an `index` mark that input and show the reason under it.
|
|
977
1518
|
*/
|
|
@@ -980,12 +1521,12 @@ window.__ModuleLoader__.load({
|
|
|
980
1521
|
const empty = listItems(value).length === 0;
|
|
981
1522
|
const rows = listRows(value);
|
|
982
1523
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
983
|
-
className: "dsh-channels-
|
|
1524
|
+
className: "dsh-channels-cfg__stringList",
|
|
984
1525
|
children: [rows.map((item, index) => {
|
|
985
1526
|
const issue = issues.find((entry) => entry.index === index);
|
|
986
1527
|
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
987
1528
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
988
|
-
className: "dsh-channels-
|
|
1529
|
+
className: "dsh-channels-cfg__stringListItem",
|
|
989
1530
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
990
1531
|
className: "dsh-channels-cfg__inputRow",
|
|
991
1532
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -999,7 +1540,7 @@ window.__ModuleLoader__.load({
|
|
|
999
1540
|
onChange: (event) => onChange(field.key, withListEntry(rows, index, event.target.value))
|
|
1000
1541
|
}), empty ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1001
1542
|
type: "button",
|
|
1002
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--
|
|
1543
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
1003
1544
|
"aria-label": `${PANEL_COPY.listRemove} ${position}`,
|
|
1004
1545
|
disabled,
|
|
1005
1546
|
onClick: () => onChange(field.key, withoutListEntry(rows, index)),
|
|
@@ -1010,874 +1551,731 @@ window.__ModuleLoader__.load({
|
|
|
1010
1551
|
children: issue.message
|
|
1011
1552
|
})]
|
|
1012
1553
|
}, index);
|
|
1013
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1554
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1014
1555
|
type: "button",
|
|
1015
|
-
className: "dsh-channels-
|
|
1556
|
+
className: "dsh-channels-cfg__link",
|
|
1016
1557
|
disabled: disabled || listIsFull(field, rows),
|
|
1017
1558
|
onClick: () => onChange(field.key, withBlankListEntry(rows)),
|
|
1018
|
-
children: PANEL_COPY.listAdd
|
|
1559
|
+
children: ["+ ", PANEL_COPY.listAdd]
|
|
1019
1560
|
}) })]
|
|
1020
1561
|
});
|
|
1021
1562
|
}
|
|
1022
|
-
/**
|
|
1023
|
-
function
|
|
1024
|
-
const { field, value,
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
1563
|
+
/** Number input with its unit (and a MiB reading for byte counts). */
|
|
1564
|
+
function NumberInput(props) {
|
|
1565
|
+
const { field, value, inputClass, disabled, onChange } = props;
|
|
1566
|
+
const text = numberInputValue(field, value);
|
|
1567
|
+
const reading = field.unit === "字节" ? bytesReading(text) : void 0;
|
|
1028
1568
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1029
|
-
className:
|
|
1569
|
+
className: "dsh-channels-cfg__inputRow",
|
|
1030
1570
|
children: [
|
|
1031
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1032
|
-
className: "dsh-channels-
|
|
1033
|
-
children:
|
|
1034
|
-
className: "dsh-channels-cfg__label",
|
|
1035
|
-
children: field.label
|
|
1036
|
-
}), field.restartRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1037
|
-
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--restart",
|
|
1038
|
-
children: PANEL_COPY.restartTag
|
|
1039
|
-
}) : null]
|
|
1571
|
+
reading === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1572
|
+
className: "dsh-channels-cfg__bytes",
|
|
1573
|
+
children: reading
|
|
1040
1574
|
}),
|
|
1041
|
-
|
|
1042
|
-
className: "dsh-channels-cfg__check",
|
|
1043
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1044
|
-
type: "checkbox",
|
|
1045
|
-
className: "dsh-channels-cfg__checkbox",
|
|
1046
|
-
checked: value === true,
|
|
1047
|
-
"aria-label": field.label,
|
|
1048
|
-
disabled,
|
|
1049
|
-
onChange: (event) => onChange(field.key, event.target.checked)
|
|
1050
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === true ? PANEL_COPY.enabled : PANEL_COPY.disabled })]
|
|
1051
|
-
}) : null,
|
|
1052
|
-
field.kind === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1053
|
-
className: inputClass,
|
|
1054
|
-
value: String(value ?? ""),
|
|
1055
|
-
disabled: disabled || field.disabled === true,
|
|
1056
|
-
"aria-label": field.label,
|
|
1057
|
-
onChange: (event) => onChange(field.key, event.target.value),
|
|
1058
|
-
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1059
|
-
value: option.value,
|
|
1060
|
-
children: option.label
|
|
1061
|
-
}, option.value))
|
|
1062
|
-
}) : null,
|
|
1063
|
-
field.kind === "number" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1064
1576
|
type: "number",
|
|
1065
|
-
className: inputClass
|
|
1066
|
-
value:
|
|
1067
|
-
min: field.min,
|
|
1068
|
-
max: field.max,
|
|
1069
|
-
step: field.step,
|
|
1070
|
-
"aria-label": field.label,
|
|
1071
|
-
disabled,
|
|
1072
|
-
onChange: (event) => onChange(field.key, event.target.value)
|
|
1073
|
-
}) : null,
|
|
1074
|
-
field.kind === "text" ? field.browse === "directory" && onPickDirectory !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectoryTextInput, {
|
|
1075
|
-
field,
|
|
1076
|
-
value,
|
|
1077
|
-
inputClass,
|
|
1078
|
-
disabled,
|
|
1079
|
-
onChange,
|
|
1080
|
-
onPick: onPickDirectory
|
|
1081
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1082
|
-
type: "text",
|
|
1083
|
-
className: inputClass,
|
|
1084
|
-
value: String(value ?? ""),
|
|
1577
|
+
className: `${inputClass} dsh-channels-cfg__input--num`,
|
|
1578
|
+
value: text,
|
|
1085
1579
|
placeholder: field.placeholder,
|
|
1580
|
+
step: numberInputStep(field),
|
|
1086
1581
|
"aria-label": field.label,
|
|
1087
1582
|
disabled,
|
|
1088
1583
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
1089
|
-
}) : null,
|
|
1090
|
-
field.kind === "string-list" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1091
|
-
field,
|
|
1092
|
-
value,
|
|
1093
|
-
issues: entryIssues,
|
|
1094
|
-
disabled,
|
|
1095
|
-
onChange
|
|
1096
|
-
}) : null,
|
|
1097
|
-
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1098
|
-
className: "dsh-channels-cfg__help",
|
|
1099
|
-
children: field.help
|
|
1100
1584
|
}),
|
|
1101
|
-
|
|
1102
|
-
className: "dsh-channels-
|
|
1103
|
-
children:
|
|
1585
|
+
field.unit === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1586
|
+
className: "dsh-channels-cfg__unit",
|
|
1587
|
+
children: field.unit
|
|
1104
1588
|
})
|
|
1105
1589
|
]
|
|
1106
1590
|
});
|
|
1107
1591
|
}
|
|
1108
|
-
/**
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
className: "dsh-channels-cfg__group",
|
|
1125
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1126
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1127
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1128
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1129
|
-
children: section.label
|
|
1130
|
-
})
|
|
1131
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1132
|
-
className: "dsh-channels-cfg__grid",
|
|
1133
|
-
children: section.fields.map((field) => {
|
|
1134
|
-
const onPick = form.onPickDirectory;
|
|
1135
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
|
|
1136
|
-
field,
|
|
1137
|
-
value: form.draft[field.key] ?? effectiveValue(form.panel, field.key),
|
|
1138
|
-
issues: form.issues,
|
|
1139
|
-
disabled: form.locked,
|
|
1140
|
-
onChange: form.onChange,
|
|
1141
|
-
onPickDirectory: onPick === void 0 ? void 0 : () => onPick(field.key)
|
|
1142
|
-
}, field.key);
|
|
1143
|
-
})
|
|
1144
|
-
})]
|
|
1145
|
-
}, section.group))
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
/** The unified per-tab action bar: save every dirty field or drop the draft. */
|
|
1149
|
-
function SaveBar(props) {
|
|
1150
|
-
const { form, tab } = props;
|
|
1151
|
-
const groups = new Set(TAB_FIELD_GROUPS[tab]);
|
|
1152
|
-
const pageDirty = form.fields.filter((field) => groups.has(field.group) && field.key in form.draft).length;
|
|
1153
|
-
const dirty = form.dirtyCount > 0;
|
|
1154
|
-
const disabled = !dirty || form.busy !== null || form.locked;
|
|
1155
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1156
|
-
className: "dsh-channels-cfg__footer",
|
|
1157
|
-
children: [
|
|
1158
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1159
|
-
type: "button",
|
|
1160
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1161
|
-
onClick: form.onSave,
|
|
1592
|
+
/** The control for one Host field descriptor. */
|
|
1593
|
+
function FieldControl(props) {
|
|
1594
|
+
const { field, value, issues, invalid, disabled, onChange, onPickDirectory } = props;
|
|
1595
|
+
const inputClass = `dsh-channels-cfg__input${invalid ? " dsh-channels-cfg__input--invalid" : ""}`;
|
|
1596
|
+
switch (field.kind) {
|
|
1597
|
+
case "boolean": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
1598
|
+
checked: value === true,
|
|
1599
|
+
label: field.label,
|
|
1600
|
+
disabled,
|
|
1601
|
+
onChange: (next) => onChange(field.key, next)
|
|
1602
|
+
});
|
|
1603
|
+
case "select": {
|
|
1604
|
+
const toggle = field.toggle;
|
|
1605
|
+
if (toggle !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
1606
|
+
checked: value === toggle.on,
|
|
1607
|
+
label: field.label,
|
|
1162
1608
|
disabled,
|
|
1163
|
-
|
|
1164
|
-
})
|
|
1165
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1609
|
+
onChange: (next) => onChange(field.key, next ? toggle.on : toggle.off)
|
|
1610
|
+
});
|
|
1611
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1612
|
+
className: `${inputClass} dsh-channels-cfg__select`,
|
|
1613
|
+
value: String(value ?? ""),
|
|
1614
|
+
disabled: disabled || field.disabled === true,
|
|
1615
|
+
"aria-label": field.label,
|
|
1616
|
+
onChange: (event) => onChange(field.key, event.target.value),
|
|
1617
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1618
|
+
value: option.value,
|
|
1619
|
+
children: option.label
|
|
1620
|
+
}, option.value))
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
case "number": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
|
|
1624
|
+
field,
|
|
1625
|
+
value,
|
|
1626
|
+
inputClass,
|
|
1627
|
+
disabled,
|
|
1628
|
+
onChange
|
|
1629
|
+
});
|
|
1630
|
+
case "text": return field.browse === "directory" && onPickDirectory !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectoryTextInput, {
|
|
1631
|
+
field,
|
|
1632
|
+
value,
|
|
1633
|
+
inputClass,
|
|
1634
|
+
disabled,
|
|
1635
|
+
onChange,
|
|
1636
|
+
onPick: onPickDirectory
|
|
1637
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1638
|
+
type: "text",
|
|
1639
|
+
className: `${inputClass}${field.wide === true ? "" : " dsh-channels-cfg__input--text"}`,
|
|
1640
|
+
value: String(value ?? ""),
|
|
1641
|
+
placeholder: field.placeholder,
|
|
1642
|
+
"aria-label": field.label,
|
|
1643
|
+
disabled,
|
|
1644
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
1645
|
+
});
|
|
1646
|
+
case "string-list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1647
|
+
field,
|
|
1648
|
+
value,
|
|
1649
|
+
issues,
|
|
1650
|
+
disabled,
|
|
1651
|
+
onChange
|
|
1652
|
+
});
|
|
1653
|
+
default: return null;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/** One Host field rendered as a setting row. */
|
|
1657
|
+
function SettingRow(props) {
|
|
1658
|
+
const { field, value, issues, disabled = false, onChange, onPickDirectory } = props;
|
|
1659
|
+
const issue = issues.find((item) => item.key === field.key && item.index === void 0);
|
|
1660
|
+
const entryIssues = issues.filter((item) => item.key === field.key && item.index !== void 0);
|
|
1661
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1662
|
+
label: field.label,
|
|
1663
|
+
tags: field.restartRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1664
|
+
className: "dsh-channels-cfg__tag",
|
|
1665
|
+
children: PANEL_COPY.restartTag
|
|
1666
|
+
}) : void 0,
|
|
1667
|
+
summary: field.summary,
|
|
1668
|
+
details: field.help,
|
|
1669
|
+
wide: field.wide === true,
|
|
1670
|
+
error: issue?.message,
|
|
1671
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldControl, {
|
|
1672
|
+
field,
|
|
1673
|
+
value,
|
|
1674
|
+
issues: entryIssues,
|
|
1675
|
+
invalid: issue !== void 0,
|
|
1676
|
+
disabled,
|
|
1677
|
+
onChange,
|
|
1678
|
+
onPickDirectory
|
|
1679
|
+
})
|
|
1194
1680
|
});
|
|
1195
1681
|
}
|
|
1196
|
-
/**
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1682
|
+
/** Rows for every field of one group. */
|
|
1683
|
+
function GroupRows(props) {
|
|
1684
|
+
const { form, fields } = props;
|
|
1685
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fields.map((field) => {
|
|
1686
|
+
const onPick = form.onPickDirectory;
|
|
1687
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
|
|
1688
|
+
field,
|
|
1689
|
+
value: form.draft[field.key] ?? effectiveValue(form.panel, field.key),
|
|
1690
|
+
issues: form.issues,
|
|
1691
|
+
disabled: form.locked,
|
|
1692
|
+
onChange: form.onChange,
|
|
1693
|
+
onPickDirectory: onPick === void 0 ? void 0 : () => onPick(field.key)
|
|
1694
|
+
}, field.key);
|
|
1695
|
+
}) });
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* A group of rarely changed fields behind one summary row. It opens on its
|
|
1699
|
+
* own when one of its fields has a validation issue.
|
|
1700
|
+
*/
|
|
1701
|
+
function CollapsedGroup(props) {
|
|
1702
|
+
const { form, fields } = props;
|
|
1703
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1704
|
+
const keys = new Set(fields.map((field) => field.key));
|
|
1705
|
+
const changed = fields.filter((field) => form.panel.config.overriddenKeys.includes(field.key) || field.key in form.draft).length;
|
|
1706
|
+
const expanded = open || form.issues.some((issue) => keys.has(issue.key));
|
|
1707
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1708
|
+
label: fillCopy(PANEL_COPY.collapsedCount, { n: fields.length }),
|
|
1709
|
+
summary: changed === 0 ? PANEL_COPY.collapsedClean : fillCopy(PANEL_COPY.collapsedChanged, { n: changed }),
|
|
1710
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1711
|
+
type: "button",
|
|
1712
|
+
className: "dsh-channels-cfg__more",
|
|
1713
|
+
"aria-expanded": expanded,
|
|
1714
|
+
onClick: () => setOpen(!expanded),
|
|
1715
|
+
children: expanded ? PANEL_COPY.collapse : PANEL_COPY.expand
|
|
1716
|
+
})
|
|
1717
|
+
}), expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupRows, {
|
|
1718
|
+
form,
|
|
1719
|
+
fields
|
|
1720
|
+
}) : null] });
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* One tab's editable fields as titled sections, one per Host field group, in
|
|
1724
|
+
* the given order. Empty groups render nothing, so a Host that moves every key
|
|
1725
|
+
* out of a group does not leave a stray heading behind. `extra` appends rows
|
|
1726
|
+
* that are not Host fields (e.g. a credential) to one group's list.
|
|
1727
|
+
*/
|
|
1728
|
+
function FieldSections(props) {
|
|
1729
|
+
const { form, groups, extra = {}, footnotes = {} } = props;
|
|
1730
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: groups.map((group) => {
|
|
1731
|
+
const fields = form.fields.filter((field) => field.group === group);
|
|
1732
|
+
const more = extra[group];
|
|
1733
|
+
if (fields.length === 0 && more === void 0) return null;
|
|
1734
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1735
|
+
title: FIELD_GROUP_LABELS[group],
|
|
1736
|
+
footnote: footnotes[group],
|
|
1737
|
+
children: [COLLAPSED_FIELD_GROUPS.includes(group) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedGroup, {
|
|
1738
|
+
form,
|
|
1739
|
+
fields
|
|
1740
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupRows, {
|
|
1741
|
+
form,
|
|
1742
|
+
fields
|
|
1743
|
+
}), more]
|
|
1744
|
+
}, group);
|
|
1745
|
+
}) });
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Sticky bar shown only while the draft has unsaved keys. Saving submits the
|
|
1749
|
+
* whole draft (every tab); discarding drops it.
|
|
1750
|
+
*/
|
|
1751
|
+
function SaveBar(props) {
|
|
1752
|
+
const { dirtyCount, restartCount, busy, locked, onSave, onDiscard } = props;
|
|
1753
|
+
if (dirtyCount === 0) return null;
|
|
1754
|
+
const disabled = busy !== null || locked;
|
|
1755
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1756
|
+
className: "dsh-channels-cfg__saveBar",
|
|
1757
|
+
role: "region",
|
|
1758
|
+
"aria-label": PANEL_COPY.save,
|
|
1759
|
+
children: [
|
|
1760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1761
|
+
className: "dsh-channels-cfg__saveMsg",
|
|
1762
|
+
children: [fillCopy(PANEL_COPY.dirtyCount, { n: dirtyCount }), restartCount === 0 ? "" : fillCopy(PANEL_COPY.dirtyRestart, { n: restartCount })]
|
|
1763
|
+
}),
|
|
1764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1765
|
+
type: "button",
|
|
1766
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1767
|
+
onClick: onDiscard,
|
|
1768
|
+
disabled,
|
|
1769
|
+
children: PANEL_COPY.discard
|
|
1770
|
+
}),
|
|
1771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1772
|
+
type: "button",
|
|
1773
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1774
|
+
onClick: onSave,
|
|
1775
|
+
disabled,
|
|
1776
|
+
children: busy === "save" ? PANEL_COPY.saving : PANEL_COPY.save
|
|
1777
|
+
})
|
|
1778
|
+
]
|
|
1201
1779
|
});
|
|
1202
1780
|
}
|
|
1203
|
-
/** Transient
|
|
1781
|
+
/** Transient banner (save / credentials / reconnect / catalog / access). */
|
|
1204
1782
|
function NoticeBar(props) {
|
|
1205
|
-
const {
|
|
1783
|
+
const { kind, text } = props;
|
|
1206
1784
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1207
|
-
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${
|
|
1208
|
-
role: "status",
|
|
1785
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${kind}`,
|
|
1786
|
+
role: kind === "error" ? "alert" : "status",
|
|
1209
1787
|
"aria-live": "polite",
|
|
1210
|
-
children:
|
|
1788
|
+
children: text
|
|
1211
1789
|
});
|
|
1212
1790
|
}
|
|
1213
|
-
/** Muted
|
|
1214
|
-
function
|
|
1791
|
+
/** Muted row inside a list (empty or not-yet-loaded state). */
|
|
1792
|
+
function EmptyRow(props) {
|
|
1215
1793
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1216
|
-
className: "dsh-channels-
|
|
1794
|
+
className: "dsh-channels-cfg__emptyRow",
|
|
1217
1795
|
children: props.text
|
|
1218
1796
|
});
|
|
1219
1797
|
}
|
|
1220
|
-
/** Human label for the official SDK's WebSocket lifecycle state. */
|
|
1221
|
-
function connectionStateLabel(state) {
|
|
1222
|
-
switch (state) {
|
|
1223
|
-
case "connected": return {
|
|
1224
|
-
text: PANEL_COPY.phaseConnected,
|
|
1225
|
-
tone: "ok"
|
|
1226
|
-
};
|
|
1227
|
-
case "connecting": return {
|
|
1228
|
-
text: PANEL_COPY.phaseConnecting,
|
|
1229
|
-
tone: "warn"
|
|
1230
|
-
};
|
|
1231
|
-
case "reconnecting": return {
|
|
1232
|
-
text: PANEL_COPY.phaseConnecting,
|
|
1233
|
-
tone: "warn"
|
|
1234
|
-
};
|
|
1235
|
-
case "failed": return {
|
|
1236
|
-
text: PANEL_COPY.phaseError,
|
|
1237
|
-
tone: "bad"
|
|
1238
|
-
};
|
|
1239
|
-
case "idle": return {
|
|
1240
|
-
text: PANEL_COPY.phaseIdle,
|
|
1241
|
-
tone: "muted"
|
|
1242
|
-
};
|
|
1243
|
-
default: return {
|
|
1244
|
-
text: state ?? PANEL_COPY.unknownValue,
|
|
1245
|
-
tone: "muted"
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
1798
|
//#endregion
|
|
1250
|
-
//#region src/client/panel/
|
|
1799
|
+
//#region src/client/panel/whitelist.tsx
|
|
1251
1800
|
/**
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
1255
|
-
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
1256
|
-
* 管理员与准入模式由同 tab 上方「准入与授权」字段表单编辑,这里不再重复。
|
|
1801
|
+
* 白名单列表:群白名单、用户白名单与知识库白名单共用的条目行、添加表单
|
|
1802
|
+
* (点「+ 添加」才展开)与待处理申请行。
|
|
1257
1803
|
*
|
|
1258
|
-
* @module dsh-channels/client/panel/
|
|
1804
|
+
* @module dsh-channels/client/panel/whitelist
|
|
1259
1805
|
*/
|
|
1260
|
-
/** Local `
|
|
1806
|
+
/** Local `MM-DD HH:mm` rendering for authorization timestamps. */
|
|
1261
1807
|
function formatAccessDate(ms) {
|
|
1262
1808
|
if (!Number.isFinite(ms) || ms <= 0) return PANEL_COPY.unknownValue;
|
|
1263
1809
|
const date = new Date(ms);
|
|
1264
1810
|
const pad = (value) => String(value).padStart(2, "0");
|
|
1265
|
-
return `${
|
|
1811
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1266
1812
|
}
|
|
1267
1813
|
/** Which authorization entry produced one grant. */
|
|
1268
|
-
function
|
|
1814
|
+
function sourceLabel(source) {
|
|
1269
1815
|
switch (source) {
|
|
1270
1816
|
case "command": return PANEL_COPY.accessSourceCommand;
|
|
1271
1817
|
case "card": return PANEL_COPY.accessSourceCard;
|
|
1272
1818
|
default: return PANEL_COPY.accessSourcePanel;
|
|
1273
1819
|
}
|
|
1274
1820
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1286
|
-
if (await
|
|
1287
|
-
|
|
1288
|
-
|
|
1821
|
+
/**
|
|
1822
|
+
* One titled whitelist with remove buttons and a collapsed add form.
|
|
1823
|
+
* `onAdd` resolves true when the entry was added (the form then closes).
|
|
1824
|
+
*/
|
|
1825
|
+
function Whitelist(props) {
|
|
1826
|
+
const { title, countLabel, entries, avatar, empty, loaded, addLabel, idPlaceholder, namePlaceholder, footnote, disabled, working, onAdd, onRemove } = props;
|
|
1827
|
+
const [adding, setAdding] = (0, react.useState)(false);
|
|
1828
|
+
const [id, setId] = (0, react.useState)("");
|
|
1829
|
+
const [name, setName] = (0, react.useState)("");
|
|
1830
|
+
const submit = async () => {
|
|
1831
|
+
if (id.trim() === "") return;
|
|
1832
|
+
if (await onAdd(id, name)) {
|
|
1833
|
+
setId("");
|
|
1834
|
+
setName("");
|
|
1835
|
+
setAdding(false);
|
|
1289
1836
|
}
|
|
1290
1837
|
};
|
|
1291
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
1292
|
-
|
|
1838
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
1839
|
+
title: loaded ? `${title} · ${countLabel ?? entries.length}` : title,
|
|
1840
|
+
footnote,
|
|
1293
1841
|
children: [
|
|
1294
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1301
|
-
children: accessModeLabel(panel, access.mode)
|
|
1302
|
-
})]
|
|
1303
|
-
}),
|
|
1304
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1305
|
-
className: "dsh-channels-cfg__help",
|
|
1306
|
-
children: PANEL_COPY.accessCardHelp
|
|
1307
|
-
}),
|
|
1308
|
-
access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1309
|
-
className: "dsh-channels-cfg__banner",
|
|
1310
|
-
children: PANEL_COPY.accessModeOpenWarning
|
|
1311
|
-
}) : null,
|
|
1312
|
-
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1313
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1314
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning }), access.admin.ownerLookupError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: access.admin.ownerLookupError })]
|
|
1315
|
-
}) : null,
|
|
1316
|
-
access.mode === "guarded" && access.admin.source === "owner" && access.admin.userId !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1317
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
1318
|
-
children: [PANEL_COPY.accessAdminOwnerNotice, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1319
|
-
className: "dsh-channels-cfg__mono",
|
|
1320
|
-
children: access.admin.userId
|
|
1321
|
-
})]
|
|
1322
|
-
}) : null,
|
|
1323
|
-
!access.loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, { text: PANEL_COPY.accessNotLoaded }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1324
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1325
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1326
|
-
children: [
|
|
1327
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1328
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1329
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1330
|
-
className: "dsh-channels-cfg__label",
|
|
1331
|
-
children: PANEL_COPY.accessGroupsTitle
|
|
1332
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1333
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1334
|
-
children: [groups.length, " 个"]
|
|
1335
|
-
})]
|
|
1336
|
-
}),
|
|
1337
|
-
groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1338
|
-
className: "dsh-channels-cfg__help",
|
|
1339
|
-
children: PANEL_COPY.accessGroupsEmpty
|
|
1340
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1341
|
-
className: "dsh-channels-cfg__accessList",
|
|
1342
|
-
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1343
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1344
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1345
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1346
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: group.name ?? group.chatId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1347
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1348
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1349
|
-
className: "dsh-channels-cfg__mono",
|
|
1350
|
-
children: group.chatId
|
|
1351
|
-
}), ` · ${accessSourceLabel(group.source)} · ${formatAccessDate(group.addedAt)}`]
|
|
1352
|
-
})]
|
|
1353
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1354
|
-
type: "button",
|
|
1355
|
-
className: "dsh-channels-cfg__btn",
|
|
1356
|
-
disabled,
|
|
1357
|
-
onClick: () => onRemoveGroup(group.chatId),
|
|
1358
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1359
|
-
})]
|
|
1360
|
-
}, group.chatId))
|
|
1361
|
-
}),
|
|
1362
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1363
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1364
|
-
children: [
|
|
1365
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1366
|
-
type: "text",
|
|
1367
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1368
|
-
value: groupChatId,
|
|
1369
|
-
placeholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1370
|
-
"aria-label": PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1371
|
-
disabled,
|
|
1372
|
-
onChange: (event) => setGroupChatId(event.target.value)
|
|
1373
|
-
}),
|
|
1374
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1375
|
-
type: "text",
|
|
1376
|
-
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1377
|
-
value: groupName,
|
|
1378
|
-
placeholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1379
|
-
"aria-label": PANEL_COPY.accessGroupNamePlaceholder,
|
|
1380
|
-
disabled,
|
|
1381
|
-
onChange: (event) => setGroupName(event.target.value)
|
|
1382
|
-
}),
|
|
1383
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1384
|
-
type: "button",
|
|
1385
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1386
|
-
disabled: disabled || groupChatId.trim() === "",
|
|
1387
|
-
onClick: () => {
|
|
1388
|
-
submitGroup();
|
|
1389
|
-
},
|
|
1390
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup
|
|
1391
|
-
})
|
|
1392
|
-
]
|
|
1393
|
-
})
|
|
1394
|
-
]
|
|
1842
|
+
!loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: PANEL_COPY.accessNotLoaded }) : null,
|
|
1843
|
+
loaded && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: empty }) : null,
|
|
1844
|
+
entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1845
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1846
|
+
text: avatar ?? entry.name ?? entry.id,
|
|
1847
|
+
url: entry.avatarUrl
|
|
1395
1848
|
}),
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1849
|
+
label: entry.name ?? entry.id,
|
|
1850
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1852
|
+
className: "dsh-channels-cfg__mono",
|
|
1853
|
+
children: entry.id
|
|
1854
|
+
}),
|
|
1855
|
+
" · ",
|
|
1856
|
+
sourceLabel(entry.source)
|
|
1857
|
+
] }),
|
|
1858
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1859
|
+
type: "button",
|
|
1860
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
1401
1861
|
disabled,
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
onRemove: onRemoveUser
|
|
1405
|
-
}),
|
|
1406
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1407
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1408
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1409
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1410
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1411
|
-
className: "dsh-channels-cfg__label",
|
|
1412
|
-
children: PANEL_COPY.accessRequestsTitle
|
|
1413
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1414
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1415
|
-
children: [requests.length, " 条"]
|
|
1416
|
-
})]
|
|
1417
|
-
}), requests.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1418
|
-
className: "dsh-channels-cfg__help",
|
|
1419
|
-
children: PANEL_COPY.accessRequestsEmpty
|
|
1420
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1421
|
-
className: "dsh-channels-cfg__accessList",
|
|
1422
|
-
children: requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1423
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1424
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1425
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1426
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1427
|
-
request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser,
|
|
1428
|
-
":",
|
|
1429
|
-
request.name ?? request.targetId
|
|
1430
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1431
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1432
|
-
children: [
|
|
1433
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1434
|
-
className: "dsh-channels-cfg__mono",
|
|
1435
|
-
children: request.targetId
|
|
1436
|
-
}),
|
|
1437
|
-
` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`,
|
|
1438
|
-
request.kind === "user" && request.requester !== void 0 ? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}` : "",
|
|
1439
|
-
request.kind === "user" && request.groupName !== void 0 ? ` · ${request.groupName}` : request.kind === "user" && request.chatId !== void 0 ? ` · ${request.chatId}` : ""
|
|
1440
|
-
]
|
|
1441
|
-
})]
|
|
1442
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1443
|
-
className: "dsh-channels-cfg__accessActions",
|
|
1444
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1445
|
-
type: "button",
|
|
1446
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1447
|
-
disabled,
|
|
1448
|
-
onClick: () => onResolveRequest(request.token, "allow"),
|
|
1449
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1450
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1451
|
-
type: "button",
|
|
1452
|
-
className: "dsh-channels-cfg__btn",
|
|
1453
|
-
disabled,
|
|
1454
|
-
onClick: () => onResolveRequest(request.token, "deny"),
|
|
1455
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1456
|
-
})]
|
|
1457
|
-
})]
|
|
1458
|
-
}, request.token))
|
|
1459
|
-
})]
|
|
1862
|
+
onClick: () => onRemove(entry.id),
|
|
1863
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1460
1864
|
})
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
className: "dsh-channels-
|
|
1464
|
-
role: "status",
|
|
1465
|
-
"aria-live": "polite",
|
|
1466
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice })
|
|
1467
|
-
}),
|
|
1468
|
-
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1469
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1470
|
-
role: "alert",
|
|
1471
|
-
children: error
|
|
1472
|
-
}),
|
|
1473
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1474
|
-
className: "dsh-channels-cfg__help",
|
|
1475
|
-
children: PANEL_COPY.accessHints
|
|
1476
|
-
}),
|
|
1477
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1478
|
-
className: "dsh-channels-cfg__help",
|
|
1479
|
-
children: PANEL_COPY.accessChatIdHint
|
|
1480
|
-
})
|
|
1481
|
-
]
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
/** One user whitelist (user `user_id` list with add/remove), shared by the access and wiki tabs. */
|
|
1485
|
-
function UserWhitelist(props) {
|
|
1486
|
-
const { title, empty, addLabel, users, disabled, working, onAdd, onRemove } = props;
|
|
1487
|
-
const [userUserId, setUserUserId] = (0, react.useState)("");
|
|
1488
|
-
const [userName, setUserName] = (0, react.useState)("");
|
|
1489
|
-
const submitUser = async () => {
|
|
1490
|
-
if (userUserId.trim() === "") return;
|
|
1491
|
-
if (await onAdd(userUserId, userName)) {
|
|
1492
|
-
setUserUserId("");
|
|
1493
|
-
setUserName("");
|
|
1494
|
-
}
|
|
1495
|
-
};
|
|
1496
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1497
|
-
className: "dsh-channels-cfg__accessSection",
|
|
1498
|
-
children: [
|
|
1499
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1500
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1501
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1502
|
-
className: "dsh-channels-cfg__label",
|
|
1503
|
-
children: title
|
|
1504
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1505
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1506
|
-
children: [users.length, " 个"]
|
|
1507
|
-
})]
|
|
1508
|
-
}),
|
|
1509
|
-
users.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1510
|
-
className: "dsh-channels-cfg__help",
|
|
1511
|
-
children: empty
|
|
1512
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1513
|
-
className: "dsh-channels-cfg__accessList",
|
|
1514
|
-
children: users.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1515
|
-
className: "dsh-channels-cfg__accessRow",
|
|
1516
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1517
|
-
className: "dsh-channels-cfg__accessMain",
|
|
1518
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.userId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1519
|
-
className: "dsh-channels-cfg__accessMeta",
|
|
1520
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1521
|
-
className: "dsh-channels-cfg__mono",
|
|
1522
|
-
children: user.userId
|
|
1523
|
-
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1524
|
-
})]
|
|
1525
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1526
|
-
type: "button",
|
|
1527
|
-
className: "dsh-channels-cfg__btn",
|
|
1528
|
-
disabled,
|
|
1529
|
-
onClick: () => onRemove(user.userId),
|
|
1530
|
-
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1531
|
-
})]
|
|
1532
|
-
}, user.userId))
|
|
1533
|
-
}),
|
|
1534
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1535
|
-
className: "dsh-channels-cfg__accessAdd",
|
|
1865
|
+
}, entry.id)),
|
|
1866
|
+
!loaded ? null : adding ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1867
|
+
className: "dsh-channels-cfg__addRow",
|
|
1536
1868
|
children: [
|
|
1537
1869
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1538
1870
|
type: "text",
|
|
1539
|
-
className: "dsh-channels-cfg__input
|
|
1540
|
-
value:
|
|
1541
|
-
placeholder:
|
|
1542
|
-
"aria-label":
|
|
1871
|
+
className: "dsh-channels-cfg__input",
|
|
1872
|
+
value: id,
|
|
1873
|
+
placeholder: idPlaceholder,
|
|
1874
|
+
"aria-label": idPlaceholder,
|
|
1543
1875
|
disabled,
|
|
1544
|
-
onChange: (event) =>
|
|
1876
|
+
onChange: (event) => setId(event.target.value)
|
|
1545
1877
|
}),
|
|
1546
1878
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1547
1879
|
type: "text",
|
|
1548
|
-
className: "dsh-channels-cfg__input
|
|
1549
|
-
value:
|
|
1550
|
-
placeholder:
|
|
1551
|
-
"aria-label":
|
|
1880
|
+
className: "dsh-channels-cfg__input",
|
|
1881
|
+
value: name,
|
|
1882
|
+
placeholder: namePlaceholder,
|
|
1883
|
+
"aria-label": namePlaceholder,
|
|
1552
1884
|
disabled,
|
|
1553
|
-
onChange: (event) =>
|
|
1885
|
+
onChange: (event) => setName(event.target.value)
|
|
1554
1886
|
}),
|
|
1555
1887
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1556
1888
|
type: "button",
|
|
1557
1889
|
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1558
|
-
disabled: disabled ||
|
|
1890
|
+
disabled: disabled || id.trim() === "",
|
|
1559
1891
|
onClick: () => {
|
|
1560
|
-
|
|
1892
|
+
submit();
|
|
1561
1893
|
},
|
|
1562
|
-
children: working ? PANEL_COPY.accessWorking :
|
|
1894
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAdd
|
|
1895
|
+
}),
|
|
1896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1897
|
+
type: "button",
|
|
1898
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1899
|
+
onClick: () => setAdding(false),
|
|
1900
|
+
children: PANEL_COPY.cancel
|
|
1563
1901
|
})
|
|
1564
1902
|
]
|
|
1903
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1904
|
+
className: "dsh-channels-cfg__addRow",
|
|
1905
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1906
|
+
type: "button",
|
|
1907
|
+
className: "dsh-channels-cfg__link",
|
|
1908
|
+
disabled,
|
|
1909
|
+
onClick: () => setAdding(true),
|
|
1910
|
+
children: ["+ ", addLabel]
|
|
1911
|
+
})
|
|
1565
1912
|
})
|
|
1566
1913
|
]
|
|
1567
1914
|
});
|
|
1568
1915
|
}
|
|
1916
|
+
/** Pending authorization requests, settled with the same resolver as the DM card. */
|
|
1917
|
+
function RequestList(props) {
|
|
1918
|
+
const { requests, profiles, disabled, working, onResolve } = props;
|
|
1919
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
1920
|
+
title: `${PANEL_COPY.accessRequestsTitle} · ${requests.length}`,
|
|
1921
|
+
tone: "warn",
|
|
1922
|
+
children: requests.map((request) => {
|
|
1923
|
+
const profile = request.kind === "user" ? profiles.user(request.targetId) : profiles.chat(request.targetId);
|
|
1924
|
+
const target = profile?.name ?? request.name ?? request.targetId;
|
|
1925
|
+
const group = request.chatId === void 0 ? request.groupName : profiles.chat(request.chatId)?.name ?? request.groupName ?? request.chatId;
|
|
1926
|
+
const where = request.kind === "user" && group !== void 0 ? fillCopy(PANEL_COPY.accessRequestIn, { group }) : "";
|
|
1927
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1928
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1929
|
+
text: target,
|
|
1930
|
+
url: profile?.avatarUrl
|
|
1931
|
+
}),
|
|
1932
|
+
label: `${target} ${where}${request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser}`,
|
|
1933
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
+
className: "dsh-channels-cfg__mono",
|
|
1935
|
+
children: request.targetId
|
|
1936
|
+
}), ` · ${formatAccessDate(request.createdAt)} · ${fillCopy(PANEL_COPY.accessRequestExpires, { date: formatAccessDate(request.expiresAt) })}`] }),
|
|
1937
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1938
|
+
className: "dsh-channels-cfg__inputRow",
|
|
1939
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1940
|
+
type: "button",
|
|
1941
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
1942
|
+
disabled,
|
|
1943
|
+
onClick: () => onResolve(request.token, "deny"),
|
|
1944
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1945
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1946
|
+
type: "button",
|
|
1947
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1948
|
+
disabled,
|
|
1949
|
+
onClick: () => onResolve(request.token, "allow"),
|
|
1950
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1951
|
+
})]
|
|
1952
|
+
})
|
|
1953
|
+
}, request.token);
|
|
1954
|
+
})
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1569
1957
|
//#endregion
|
|
1570
1958
|
//#region src/client/panel/access-tab.tsx
|
|
1571
1959
|
/**
|
|
1572
|
-
* 「访问授权」tab
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1960
|
+
* 「访问授权」tab:待处理申请(有才显示,置顶)、准入字段(调试模式 /
|
|
1961
|
+
* 管理员 / 申请有效期)与群、用户白名单。字段保存走全局 draft,白名单增删
|
|
1962
|
+
* 与申请处理是独立即时 mutation。
|
|
1963
|
+
*
|
|
1964
|
+
* `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
|
|
1965
|
+
* 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
|
|
1575
1966
|
*
|
|
1576
1967
|
* @module dsh-channels/client/panel/access-tab
|
|
1577
1968
|
*/
|
|
1578
1969
|
function AccessTab(props) {
|
|
1579
|
-
const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1970
|
+
const { form, profiles, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1971
|
+
const { access } = form.panel;
|
|
1972
|
+
const working = form.busy === "access";
|
|
1973
|
+
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
1580
1974
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1581
1975
|
className: "dsh-channels-cfg__tabBody",
|
|
1582
1976
|
children: [
|
|
1583
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
groups: TAB_FIELD_GROUPS.access
|
|
1977
|
+
accessNotice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1978
|
+
kind: "ok",
|
|
1979
|
+
text: accessNotice
|
|
1587
1980
|
}),
|
|
1588
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1589
|
-
|
|
1590
|
-
|
|
1981
|
+
accessError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1982
|
+
kind: "error",
|
|
1983
|
+
text: accessError
|
|
1591
1984
|
}),
|
|
1592
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
onRemoveGroup,
|
|
1600
|
-
onAddUser,
|
|
1601
|
-
onRemoveUser,
|
|
1602
|
-
onResolveRequest
|
|
1603
|
-
})
|
|
1604
|
-
]
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
//#endregion
|
|
1608
|
-
//#region src/client/panel/advanced-tab.tsx
|
|
1609
|
-
/**
|
|
1610
|
-
* 「高级」tab:流式刷新等低层字段 + 只读部署信息(overlayPath / version /
|
|
1611
|
-
* 部署级参数)+ 「清除 Web 覆盖」。清除覆盖是整体动作,不走 draft。
|
|
1612
|
-
*
|
|
1613
|
-
* @module dsh-channels/client/panel/advanced-tab
|
|
1614
|
-
*/
|
|
1615
|
-
function AdvancedTab(props) {
|
|
1616
|
-
const { form, onClear } = props;
|
|
1617
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1618
|
-
className: "dsh-channels-cfg__tabBody",
|
|
1619
|
-
children: [
|
|
1620
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.advancedTabHelp }),
|
|
1621
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
1622
|
-
form,
|
|
1623
|
-
groups: TAB_FIELD_GROUPS.advanced
|
|
1985
|
+
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
1986
|
+
kind: "error",
|
|
1987
|
+
text: access.admin.ownerLookupError === void 0 ? PANEL_COPY.accessAdminMissingWarning : `${PANEL_COPY.accessAdminMissingWarning}(${access.admin.ownerLookupError})`
|
|
1988
|
+
}) : null,
|
|
1989
|
+
profiles.warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1990
|
+
className: "dsh-channels-cfg__secFoot",
|
|
1991
|
+
children: fillCopy(PANEL_COPY.profilesWarning, { reason: profiles.warning })
|
|
1624
1992
|
}),
|
|
1625
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1993
|
+
access.loaded && access.requests.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestList, {
|
|
1994
|
+
requests: access.requests,
|
|
1995
|
+
profiles,
|
|
1996
|
+
disabled,
|
|
1997
|
+
working,
|
|
1998
|
+
onResolve: onResolveRequest
|
|
1999
|
+
}) : null,
|
|
2000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
1626
2001
|
form,
|
|
1627
|
-
|
|
2002
|
+
groups: TAB_FIELD_GROUPS.access,
|
|
2003
|
+
extra: { access: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminRow, {
|
|
2004
|
+
admin: access.admin,
|
|
2005
|
+
profiles
|
|
2006
|
+
}) }
|
|
2007
|
+
}),
|
|
2008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
2009
|
+
title: PANEL_COPY.accessGroupsTitle,
|
|
2010
|
+
entries: access.groups.map((group) => ({
|
|
2011
|
+
id: group.chatId,
|
|
2012
|
+
name: profiles.chat(group.chatId)?.name ?? group.name,
|
|
2013
|
+
avatarUrl: profiles.chat(group.chatId)?.avatarUrl,
|
|
2014
|
+
source: group.source
|
|
2015
|
+
})),
|
|
2016
|
+
avatar: "群",
|
|
2017
|
+
empty: PANEL_COPY.accessGroupsEmpty,
|
|
2018
|
+
loaded: access.loaded,
|
|
2019
|
+
addLabel: PANEL_COPY.accessAddGroup,
|
|
2020
|
+
idPlaceholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
2021
|
+
namePlaceholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
2022
|
+
disabled,
|
|
2023
|
+
working,
|
|
2024
|
+
onAdd: onAddGroup,
|
|
2025
|
+
onRemove: onRemoveGroup
|
|
1628
2026
|
}),
|
|
1629
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
2028
|
+
title: PANEL_COPY.accessUsersTitle,
|
|
2029
|
+
entries: access.users.map((user) => ({
|
|
2030
|
+
id: user.userId,
|
|
2031
|
+
name: profiles.user(user.userId)?.name ?? user.name,
|
|
2032
|
+
avatarUrl: profiles.user(user.userId)?.avatarUrl,
|
|
2033
|
+
source: user.source
|
|
2034
|
+
})),
|
|
2035
|
+
empty: PANEL_COPY.accessUsersEmpty,
|
|
2036
|
+
loaded: access.loaded,
|
|
2037
|
+
addLabel: PANEL_COPY.accessAddUser,
|
|
2038
|
+
idPlaceholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
2039
|
+
namePlaceholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
2040
|
+
disabled,
|
|
2041
|
+
working,
|
|
2042
|
+
onAdd: onAddUser,
|
|
2043
|
+
onRemove: onRemoveUser
|
|
1634
2044
|
})
|
|
1635
2045
|
]
|
|
1636
2046
|
});
|
|
1637
2047
|
}
|
|
1638
|
-
/**
|
|
1639
|
-
function
|
|
1640
|
-
const {
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
})]
|
|
1655
|
-
}),
|
|
2048
|
+
/** The effective administrator with their Feishu name and avatar; nothing while there is none. */
|
|
2049
|
+
function AdminRow(props) {
|
|
2050
|
+
const { admin, profiles } = props;
|
|
2051
|
+
if (admin.userId === void 0) return null;
|
|
2052
|
+
const profile = profiles.user(admin.userId);
|
|
2053
|
+
const name = profile?.name ?? admin.userId;
|
|
2054
|
+
const source = admin.source === "owner" ? PANEL_COPY.accessAdminSourceOwner : PANEL_COPY.accessAdminSourceConfig;
|
|
2055
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2056
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
2057
|
+
text: name,
|
|
2058
|
+
url: profile?.avatarUrl
|
|
2059
|
+
}),
|
|
2060
|
+
label: name,
|
|
2061
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2062
|
+
PANEL_COPY.accessAdminCurrent,
|
|
2063
|
+
" · ",
|
|
1656
2064
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1657
|
-
className: "dsh-channels-
|
|
1658
|
-
children:
|
|
1659
|
-
}),
|
|
1660
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1661
|
-
className: "dsh-channels-cfg__labelRow",
|
|
1662
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1663
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1664
|
-
children: PANEL_COPY.readOnlyCard
|
|
1665
|
-
})
|
|
1666
|
-
}),
|
|
1667
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1668
|
-
className: "dsh-channels-cfg__readOnly",
|
|
1669
|
-
children: [
|
|
1670
|
-
readOnly.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1671
|
-
className: "dsh-channels-cfg__kv",
|
|
1672
|
-
children: [
|
|
1673
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1674
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1675
|
-
children: field.label
|
|
1676
|
-
}),
|
|
1677
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1678
|
-
className: "dsh-channels-cfg__kvValue",
|
|
1679
|
-
children: field.value
|
|
1680
|
-
}),
|
|
1681
|
-
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1682
|
-
className: "dsh-channels-cfg__help",
|
|
1683
|
-
children: field.help
|
|
1684
|
-
})
|
|
1685
|
-
]
|
|
1686
|
-
}, field.key)),
|
|
1687
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1688
|
-
className: "dsh-channels-cfg__kv",
|
|
1689
|
-
children: [
|
|
1690
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1691
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1692
|
-
children: PANEL_COPY.overlayPath
|
|
1693
|
-
}),
|
|
1694
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1695
|
-
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1696
|
-
children: panel.status.overlayPath
|
|
1697
|
-
}),
|
|
1698
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1699
|
-
className: "dsh-channels-cfg__help",
|
|
1700
|
-
children: PANEL_COPY.overlayPathHelp
|
|
1701
|
-
})
|
|
1702
|
-
]
|
|
1703
|
-
}),
|
|
1704
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1705
|
-
className: "dsh-channels-cfg__kv",
|
|
1706
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1707
|
-
className: "dsh-channels-cfg__kvKey",
|
|
1708
|
-
children: PANEL_COPY.version
|
|
1709
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1710
|
-
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
1711
|
-
children: panel.status.version
|
|
1712
|
-
})]
|
|
1713
|
-
})
|
|
1714
|
-
]
|
|
2065
|
+
className: "dsh-channels-cfg__mono",
|
|
2066
|
+
children: admin.userId
|
|
1715
2067
|
}),
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
type: "button",
|
|
1720
|
-
className: "dsh-channels-cfg__btn",
|
|
1721
|
-
onClick: onClear,
|
|
1722
|
-
disabled: overridden === 0 || busy !== null || locked,
|
|
1723
|
-
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
1724
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1725
|
-
className: "dsh-channels-cfg__help",
|
|
1726
|
-
children: overridden === 0 ? PANEL_COPY.noOverrides : `${PANEL_COPY.overridden} ${overridden} 项`
|
|
1727
|
-
})]
|
|
1728
|
-
})
|
|
1729
|
-
]
|
|
2068
|
+
" · ",
|
|
2069
|
+
source
|
|
2070
|
+
] })
|
|
1730
2071
|
});
|
|
1731
2072
|
}
|
|
1732
2073
|
//#endregion
|
|
1733
2074
|
//#region src/client/panel/credentials-card.tsx
|
|
1734
2075
|
/**
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
2076
|
+
* 凭据行:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示「已配置 /
|
|
2077
|
+
* 未配置」,两个方向都不回显已存值。已配置时只显示「更换 / 移除」,点「更换」
|
|
2078
|
+
* 才出现输入框。保存动作由页面外壳持有(凭据保存独立于配置 draft)。
|
|
1738
2079
|
*
|
|
1739
2080
|
* @module dsh-channels/client/panel/credentials-card
|
|
1740
2081
|
*/
|
|
1741
|
-
|
|
2082
|
+
/** One credential slot as a setting row with its own input and actions. */
|
|
2083
|
+
function CredentialRow(props) {
|
|
2084
|
+
const { label, summary, state, value, secret, placeholder, issue, busy, disabled, onChange, onSave, onClear } = props;
|
|
2085
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
2086
|
+
const previousBusy = (0, react.useRef)(busy);
|
|
2087
|
+
(0, react.useEffect)(() => {
|
|
2088
|
+
if (previousBusy.current === "credentials" && busy === null) setEditing(false);
|
|
2089
|
+
previousBusy.current = busy;
|
|
2090
|
+
}, [busy]);
|
|
2091
|
+
const showInput = !state.configured || editing || value !== "";
|
|
2092
|
+
const source = `${PANEL_COPY.credentialStore} · ${state.ref}${state.source === void 0 ? "" : ` · ${state.source}`}`;
|
|
2093
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2094
|
+
label,
|
|
2095
|
+
tags: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2096
|
+
tone: state.configured ? "ok" : "muted",
|
|
2097
|
+
children: state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing
|
|
2098
|
+
}),
|
|
2099
|
+
summary: summary === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2100
|
+
className: "dsh-channels-cfg__mono",
|
|
2101
|
+
children: source
|
|
2102
|
+
}) : summary,
|
|
2103
|
+
error: issue?.message ?? (state.writable ? void 0 : PANEL_COPY.credentialReadOnly),
|
|
2104
|
+
control: showInput ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2105
|
+
className: "dsh-channels-cfg__inputRow",
|
|
2106
|
+
children: [
|
|
2107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2108
|
+
type: secret ? "password" : "text",
|
|
2109
|
+
className: `dsh-channels-cfg__input dsh-channels-cfg__input--text${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`,
|
|
2110
|
+
value,
|
|
2111
|
+
placeholder,
|
|
2112
|
+
autoComplete: "new-password",
|
|
2113
|
+
"aria-label": label,
|
|
2114
|
+
disabled,
|
|
2115
|
+
onChange: (event) => onChange(event.target.value)
|
|
2116
|
+
}),
|
|
2117
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2118
|
+
type: "button",
|
|
2119
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2120
|
+
disabled: disabled || value.trim() === "",
|
|
2121
|
+
onClick: onSave,
|
|
2122
|
+
children: busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials
|
|
2123
|
+
}),
|
|
2124
|
+
state.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2125
|
+
type: "button",
|
|
2126
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
2127
|
+
disabled: busy !== null,
|
|
2128
|
+
onClick: () => {
|
|
2129
|
+
setEditing(false);
|
|
2130
|
+
onChange("");
|
|
2131
|
+
},
|
|
2132
|
+
children: PANEL_COPY.cancel
|
|
2133
|
+
}) : null
|
|
2134
|
+
]
|
|
2135
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2136
|
+
className: "dsh-channels-cfg__inputRow",
|
|
2137
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2138
|
+
type: "button",
|
|
2139
|
+
className: "dsh-channels-cfg__btn",
|
|
2140
|
+
disabled,
|
|
2141
|
+
onClick: () => setEditing(true),
|
|
2142
|
+
children: PANEL_COPY.replaceCredential
|
|
2143
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2144
|
+
type: "button",
|
|
2145
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--remove",
|
|
2146
|
+
disabled,
|
|
2147
|
+
onClick: onClear,
|
|
2148
|
+
children: busy === "credentials-clear" ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential
|
|
2149
|
+
})]
|
|
2150
|
+
})
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
/** The Feishu application pair (App ID / App Secret). */
|
|
2154
|
+
function CredentialsSection(props) {
|
|
1742
2155
|
const { panel, credentials } = props;
|
|
1743
2156
|
const { appId, appSecret, issues, busy, locked, onAppIdChange, onAppSecretChange, onSave, onClear } = credentials;
|
|
1744
2157
|
const appIdState = panel.config.credentials.appId;
|
|
1745
2158
|
const appSecretState = panel.config.credentials.appSecret;
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2159
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
2160
|
+
title: PANEL_COPY.credentialsTitle,
|
|
2161
|
+
footnote: PANEL_COPY.credentialsHint,
|
|
2162
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
2163
|
+
label: PANEL_COPY.appIdLabel,
|
|
2164
|
+
state: appIdState,
|
|
2165
|
+
value: appId,
|
|
2166
|
+
secret: false,
|
|
2167
|
+
placeholder: PANEL_COPY.appIdPlaceholder,
|
|
2168
|
+
issue: issues.find((item) => item.key === "appId"),
|
|
2169
|
+
busy,
|
|
2170
|
+
disabled: busy !== null || locked || !appIdState.writable,
|
|
2171
|
+
onChange: onAppIdChange,
|
|
2172
|
+
onSave,
|
|
2173
|
+
onClear: () => onClear("appId")
|
|
2174
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
2175
|
+
label: PANEL_COPY.appSecretLabel,
|
|
2176
|
+
state: appSecretState,
|
|
2177
|
+
value: appSecret,
|
|
2178
|
+
secret: true,
|
|
2179
|
+
placeholder: PANEL_COPY.appSecretPlaceholder,
|
|
2180
|
+
issue: issues.find((item) => item.key === "appSecret"),
|
|
2181
|
+
busy,
|
|
2182
|
+
disabled: busy !== null || locked || !appSecretState.writable,
|
|
2183
|
+
onChange: onAppSecretChange,
|
|
2184
|
+
onSave,
|
|
2185
|
+
onClear: () => onClear("appSecret")
|
|
2186
|
+
})]
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
//#endregion
|
|
2190
|
+
//#region src/client/panel/advanced-tab.tsx
|
|
2191
|
+
/**
|
|
2192
|
+
* 「高级」tab:日志分析接口(监听地址字段 + 访问令牌凭据行)、SDK 日志级别
|
|
2193
|
+
* 与 Web 覆盖文件(「恢复 profile 配置」删除整个覆盖层,不走 draft)。
|
|
2194
|
+
*
|
|
2195
|
+
* @module dsh-channels/client/panel/advanced-tab
|
|
2196
|
+
*/
|
|
2197
|
+
function AdvancedTab(props) {
|
|
2198
|
+
const { form, analyzeToken, onClear } = props;
|
|
2199
|
+
const { panel, busy, locked } = form;
|
|
2200
|
+
const tokenState = panel.config.credentials.analyzeToken;
|
|
2201
|
+
const overridden = panel.config.overriddenKeys.length;
|
|
2202
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2203
|
+
className: "dsh-channels-cfg__tabBody",
|
|
2204
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2205
|
+
form,
|
|
2206
|
+
groups: TAB_FIELD_GROUPS.advanced,
|
|
2207
|
+
extra: { analyze: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
2208
|
+
label: PANEL_COPY.analyzeTokenLabel,
|
|
2209
|
+
summary: PANEL_COPY.analyzeTokenSummary,
|
|
2210
|
+
state: tokenState,
|
|
2211
|
+
value: analyzeToken.value,
|
|
2212
|
+
secret: true,
|
|
2213
|
+
placeholder: PANEL_COPY.analyzeTokenPlaceholder,
|
|
2214
|
+
issue: analyzeToken.issues.find((item) => item.key === "analyzeToken"),
|
|
2215
|
+
busy: analyzeToken.busy,
|
|
2216
|
+
disabled: analyzeToken.busy !== null || analyzeToken.locked || !tokenState.writable,
|
|
2217
|
+
onChange: analyzeToken.onChange,
|
|
2218
|
+
onSave: analyzeToken.onSave,
|
|
2219
|
+
onClear: analyzeToken.onClear
|
|
2220
|
+
}) }
|
|
2221
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
2222
|
+
title: PANEL_COPY.overlayTitle,
|
|
2223
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2224
|
+
label: PANEL_COPY.overlayPath,
|
|
2225
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2226
|
+
className: "dsh-channels-cfg__mono",
|
|
2227
|
+
children: panel.status.overlayPath
|
|
1796
2228
|
}),
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2229
|
+
details: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2230
|
+
PANEL_COPY.overlayPathHelp,
|
|
2231
|
+
" ",
|
|
2232
|
+
overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })
|
|
2233
|
+
] }),
|
|
2234
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2235
|
+
type: "button",
|
|
2236
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
2237
|
+
onClick: onClear,
|
|
2238
|
+
disabled: overridden === 0 || busy !== null || locked,
|
|
2239
|
+
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
1800
2240
|
})
|
|
1801
|
-
]
|
|
1802
|
-
}, field);
|
|
1803
|
-
};
|
|
1804
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1805
|
-
className: "dsh-channels-cfg__card",
|
|
1806
|
-
children: [
|
|
1807
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1808
|
-
className: "dsh-channels-cfg__cardHead",
|
|
1809
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1810
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1811
|
-
children: PANEL_COPY.credentialsCard
|
|
1812
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1813
|
-
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${configured ? "ok" : "muted"}`,
|
|
1814
|
-
children: configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge
|
|
1815
|
-
})]
|
|
1816
|
-
}),
|
|
1817
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1818
|
-
className: "dsh-channels-cfg__help",
|
|
1819
|
-
children: PANEL_COPY.credentialsCardHelp
|
|
1820
|
-
}),
|
|
1821
|
-
row("appId", PANEL_COPY.appIdLabel, appIdState, appId, "text", PANEL_COPY.appIdPlaceholder, onAppIdChange),
|
|
1822
|
-
row("appSecret", PANEL_COPY.appSecretLabel, appSecretState, appSecret, "password", PANEL_COPY.appSecretPlaceholder, onAppSecretChange),
|
|
1823
|
-
writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1824
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1825
|
-
children: PANEL_COPY.credentialReadOnly
|
|
1826
|
-
}),
|
|
1827
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1828
|
-
className: "dsh-channels-cfg__footer",
|
|
1829
|
-
children: [
|
|
1830
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1831
|
-
type: "button",
|
|
1832
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1833
|
-
onClick: onSave,
|
|
1834
|
-
disabled: disabled || !pending,
|
|
1835
|
-
children: busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials
|
|
1836
|
-
}),
|
|
1837
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
1838
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1839
|
-
className: "dsh-channels-cfg__help",
|
|
1840
|
-
children: PANEL_COPY.credentialsPendingHint
|
|
1841
|
-
})
|
|
1842
|
-
]
|
|
1843
2241
|
})
|
|
1844
|
-
]
|
|
2242
|
+
})]
|
|
1845
2243
|
});
|
|
1846
2244
|
}
|
|
1847
2245
|
//#endregion
|
|
1848
2246
|
//#region src/client/panel/connection-tab.tsx
|
|
1849
2247
|
/**
|
|
1850
|
-
* 「连接」tab:按 Host
|
|
2248
|
+
* 「连接」tab:按 Host 派生的生命周期阶段渲染。
|
|
1851
2249
|
*
|
|
1852
|
-
* - unconfigured
|
|
1853
|
-
* - idle
|
|
1854
|
-
* -
|
|
1855
|
-
* -
|
|
1856
|
-
* - connecting:禁用按钮显示「连接中…」
|
|
1857
|
-
* - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
|
|
2250
|
+
* - unconfigured:上手三步 + 应用凭据
|
|
2251
|
+
* - idle / error / connecting / connected:状态块(当前阶段的唯一操作 + 运行数字)
|
|
2252
|
+
* - 连接后:自检(权限缺口可展开、斜杠指令面板同步结果)
|
|
2253
|
+
* - 有需重连才生效的已保存配置:顶部「重启连接」横幅
|
|
1858
2254
|
*
|
|
1859
2255
|
* @module dsh-channels/client/panel/connection-tab
|
|
1860
2256
|
*/
|
|
1861
2257
|
function ConnectionTab(props) {
|
|
1862
2258
|
const { panel, status, busy, credentials, onReconnect } = props;
|
|
2259
|
+
const checks = status.scopeCheck !== void 0 || status.slashCommandSync !== void 0;
|
|
1863
2260
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1864
2261
|
className: "dsh-channels-cfg__tabBody",
|
|
1865
2262
|
children: [
|
|
1866
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.connectionTabHelp }),
|
|
1867
2263
|
panel.config.restartPendingKeys.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartBanner, {
|
|
1868
2264
|
panel,
|
|
1869
2265
|
busy,
|
|
1870
2266
|
locked: status.busy,
|
|
1871
2267
|
onReconnect
|
|
1872
2268
|
}),
|
|
1873
|
-
status.phase === "unconfigured" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2269
|
+
status.phase === "unconfigured" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OnboardingSection, { status }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusBlock, {
|
|
1874
2270
|
status,
|
|
1875
2271
|
busy,
|
|
1876
2272
|
onReconnect
|
|
1877
2273
|
}),
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2274
|
+
checks ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
|
|
2275
|
+
title: PANEL_COPY.checksTitle,
|
|
2276
|
+
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 })]
|
|
2277
|
+
}) : null,
|
|
2278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsSection, {
|
|
1881
2279
|
panel,
|
|
1882
2280
|
credentials
|
|
1883
2281
|
})
|
|
@@ -1885,7 +2283,7 @@ window.__ModuleLoader__.load({
|
|
|
1885
2283
|
});
|
|
1886
2284
|
}
|
|
1887
2285
|
/** First-run guide: create the app, fill the credentials, save and wait. */
|
|
1888
|
-
function
|
|
2286
|
+
function OnboardingSection(props) {
|
|
1889
2287
|
const { status } = props;
|
|
1890
2288
|
const steps = [
|
|
1891
2289
|
{
|
|
@@ -1901,22 +2299,12 @@ window.__ModuleLoader__.load({
|
|
|
1901
2299
|
text: PANEL_COPY.onboardingStep3Text
|
|
1902
2300
|
}
|
|
1903
2301
|
];
|
|
1904
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1905
|
-
className: "dsh-channels-
|
|
2302
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2303
|
+
className: "dsh-channels-cfg__hero",
|
|
1906
2304
|
children: [
|
|
1907
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1908
|
-
className: "dsh-channels-
|
|
1909
|
-
children:
|
|
1910
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1911
|
-
children: PANEL_COPY.onboardingTitle
|
|
1912
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1913
|
-
className: "dsh-channels-cfg__badge dsh-channels-cfg__badge--muted",
|
|
1914
|
-
children: PANEL_COPY.phaseUnconfigured
|
|
1915
|
-
})]
|
|
1916
|
-
}),
|
|
1917
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1918
|
-
className: "dsh-channels-cfg__help",
|
|
1919
|
-
children: PANEL_COPY.onboardingIntro
|
|
2305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2306
|
+
className: "dsh-channels-cfg__heroTop",
|
|
2307
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.onboardingTitle })
|
|
1920
2308
|
}),
|
|
1921
2309
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
1922
2310
|
className: "dsh-channels-cfg__steps",
|
|
@@ -1931,50 +2319,29 @@ window.__ModuleLoader__.load({
|
|
|
1931
2319
|
className: "dsh-channels-cfg__stepTitle",
|
|
1932
2320
|
children: step.title
|
|
1933
2321
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
-
className: "dsh-channels-
|
|
2322
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
1935
2323
|
children: step.text
|
|
1936
2324
|
})]
|
|
1937
2325
|
})]
|
|
1938
2326
|
}, step.title))
|
|
1939
2327
|
}),
|
|
1940
|
-
status.missingConfig.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1941
|
-
className: "dsh-channels-
|
|
1942
|
-
children: [
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
}),
|
|
1948
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1949
|
-
className: "dsh-channels-cfg__help",
|
|
1950
|
-
children: PANEL_COPY.onboardingDocsHint
|
|
2328
|
+
status.missingConfig.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2329
|
+
className: "dsh-channels-cfg__rowSummary",
|
|
2330
|
+
children: [
|
|
2331
|
+
PANEL_COPY.missingConfig,
|
|
2332
|
+
":",
|
|
2333
|
+
status.missingConfig.join("、")
|
|
2334
|
+
]
|
|
1951
2335
|
})
|
|
1952
2336
|
]
|
|
1953
2337
|
});
|
|
1954
2338
|
}
|
|
1955
|
-
/**
|
|
1956
|
-
function
|
|
2339
|
+
/** Phase, the one lifecycle action available in it, and the runtime counters. */
|
|
2340
|
+
function StatusBlock(props) {
|
|
1957
2341
|
const { status, busy, onReconnect } = props;
|
|
1958
2342
|
const badge = phaseBadge(status.phase);
|
|
1959
2343
|
const actionBusy = busy !== null || status.busy;
|
|
1960
|
-
const
|
|
1961
|
-
const facts = [
|
|
1962
|
-
{
|
|
1963
|
-
label: PANEL_COPY.statusCard,
|
|
1964
|
-
value: badge.text
|
|
1965
|
-
},
|
|
1966
|
-
{
|
|
1967
|
-
label: PANEL_COPY.connectionStateLabel,
|
|
1968
|
-
value: connection.text
|
|
1969
|
-
},
|
|
1970
|
-
{
|
|
1971
|
-
label: PANEL_COPY.botOpenId,
|
|
1972
|
-
value: status.botOpenIdTail ?? PANEL_COPY.unknownValue
|
|
1973
|
-
},
|
|
1974
|
-
{
|
|
1975
|
-
label: PANEL_COPY.uptime,
|
|
1976
|
-
value: formatUptime(status.uptimeMs)
|
|
1977
|
-
},
|
|
2344
|
+
const stats = [
|
|
1978
2345
|
{
|
|
1979
2346
|
label: PANEL_COPY.routeCount,
|
|
1980
2347
|
value: String(status.routeCount)
|
|
@@ -1986,188 +2353,167 @@ window.__ModuleLoader__.load({
|
|
|
1986
2353
|
{
|
|
1987
2354
|
label: PANEL_COPY.inflightTurnCount,
|
|
1988
2355
|
value: String(status.inflightTurnCount)
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
label: PANEL_COPY.botOpenId,
|
|
2359
|
+
value: status.botOpenIdTail ?? PANEL_COPY.unknownValue,
|
|
2360
|
+
mono: true
|
|
1989
2361
|
}
|
|
1990
2362
|
];
|
|
1991
|
-
|
|
1992
|
-
|
|
2363
|
+
let action = null;
|
|
2364
|
+
if (status.phase === "connecting") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2365
|
+
type: "button",
|
|
2366
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2367
|
+
disabled: true,
|
|
2368
|
+
children: PANEL_COPY.connecting
|
|
2369
|
+
});
|
|
2370
|
+
else if (status.phase === "idle" || status.phase === "error") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2371
|
+
type: "button",
|
|
2372
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2373
|
+
onClick: onReconnect,
|
|
2374
|
+
disabled: actionBusy,
|
|
2375
|
+
children: busy === "reconnect" ? PANEL_COPY.connecting : status.phase === "idle" ? PANEL_COPY.connect : PANEL_COPY.retry
|
|
2376
|
+
});
|
|
2377
|
+
else if (status.phase === "connected") action = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2378
|
+
type: "button",
|
|
2379
|
+
className: "dsh-channels-cfg__btn",
|
|
2380
|
+
onClick: onReconnect,
|
|
2381
|
+
disabled: actionBusy,
|
|
2382
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
2383
|
+
});
|
|
2384
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2385
|
+
className: "dsh-channels-cfg__hero",
|
|
1993
2386
|
children: [
|
|
1994
2387
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1995
|
-
className: "dsh-channels-
|
|
1996
|
-
children: [
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2388
|
+
className: "dsh-channels-cfg__heroTop",
|
|
2389
|
+
children: [
|
|
2390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2391
|
+
tone: badge.tone,
|
|
2392
|
+
dot: true,
|
|
2393
|
+
children: badge.text
|
|
2394
|
+
}),
|
|
2395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: phaseSummary(status.phase) }),
|
|
2396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
2397
|
+
action
|
|
2398
|
+
]
|
|
2003
2399
|
}),
|
|
2004
2400
|
status.lastStartError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2005
2401
|
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2006
|
-
children: [
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2402
|
+
children: [
|
|
2403
|
+
PANEL_COPY.lastError,
|
|
2404
|
+
":",
|
|
2405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2406
|
+
className: "dsh-channels-cfg__mono",
|
|
2407
|
+
children: status.lastStartError
|
|
2408
|
+
})
|
|
2409
|
+
]
|
|
2010
2410
|
}),
|
|
2011
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2012
|
-
className: "dsh-channels-
|
|
2013
|
-
children:
|
|
2014
|
-
className: "dsh-channels-
|
|
2015
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2016
|
-
className: "dsh-channels-
|
|
2017
|
-
children:
|
|
2018
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2019
|
-
className: "dsh-channels-cfg__factValue",
|
|
2020
|
-
children: fact.value
|
|
2411
|
+
status.phase === "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2412
|
+
className: "dsh-channels-cfg__stats",
|
|
2413
|
+
children: stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2414
|
+
className: "dsh-channels-cfg__stat",
|
|
2415
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: stat.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2416
|
+
className: stat.mono === true ? "dsh-channels-cfg__mono" : void 0,
|
|
2417
|
+
children: stat.value
|
|
2021
2418
|
})]
|
|
2022
|
-
},
|
|
2023
|
-
}),
|
|
2419
|
+
}, stat.label))
|
|
2420
|
+
}) : null,
|
|
2024
2421
|
status.reasons.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2025
|
-
className: `dsh-channels-
|
|
2422
|
+
className: `dsh-channels-cfg__rowSummary${status.level === "unhealthy" ? " dsh-channels-cfg__textBad" : ""}`,
|
|
2026
2423
|
children: [
|
|
2027
2424
|
PANEL_COPY.healthReasons,
|
|
2028
2425
|
":",
|
|
2029
2426
|
status.reasons.join(";")
|
|
2030
2427
|
]
|
|
2031
|
-
}),
|
|
2032
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2033
|
-
className: "dsh-channels-cfg__actions",
|
|
2034
|
-
children: [
|
|
2035
|
-
status.phase === "connecting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2036
|
-
type: "button",
|
|
2037
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2038
|
-
disabled: true,
|
|
2039
|
-
children: PANEL_COPY.connecting
|
|
2040
|
-
}) : null,
|
|
2041
|
-
status.phase === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2042
|
-
type: "button",
|
|
2043
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2044
|
-
onClick: onReconnect,
|
|
2045
|
-
disabled: actionBusy,
|
|
2046
|
-
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.connect
|
|
2047
|
-
}) : null,
|
|
2048
|
-
status.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2049
|
-
type: "button",
|
|
2050
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2051
|
-
onClick: onReconnect,
|
|
2052
|
-
disabled: actionBusy,
|
|
2053
|
-
children: busy === "reconnect" ? PANEL_COPY.connecting : PANEL_COPY.retry
|
|
2054
|
-
}) : null,
|
|
2055
|
-
status.phase === "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2056
|
-
type: "button",
|
|
2057
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
2058
|
-
onClick: onReconnect,
|
|
2059
|
-
disabled: actionBusy,
|
|
2060
|
-
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
2061
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2062
|
-
className: "dsh-channels-cfg__help",
|
|
2063
|
-
children: PANEL_COPY.restartWarning
|
|
2064
|
-
})] }) : null
|
|
2065
|
-
]
|
|
2066
2428
|
})
|
|
2067
2429
|
]
|
|
2068
2430
|
});
|
|
2069
2431
|
}
|
|
2070
|
-
/**
|
|
2071
|
-
function
|
|
2432
|
+
/** Status icon in front of one check row. */
|
|
2433
|
+
function CheckIcon(props) {
|
|
2434
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2435
|
+
className: `dsh-channels-cfg__checkIcon dsh-channels-cfg__checkIcon--${props.tone}`,
|
|
2436
|
+
"aria-hidden": "true",
|
|
2437
|
+
children: props.tone === "ok" ? "✓" : "!"
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
/** Post-connect scope self-check: the gap count, expandable to each scope with its console link. */
|
|
2441
|
+
function ScopeCheckRow(props) {
|
|
2072
2442
|
const { check } = props;
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2443
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2444
|
+
if (check.state === "ok") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2445
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "ok" }),
|
|
2446
|
+
label: PANEL_COPY.scopeCheckOk
|
|
2447
|
+
});
|
|
2448
|
+
if (check.state === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2449
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "bad" }),
|
|
2450
|
+
label: PANEL_COPY.scopeCheckError,
|
|
2451
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2452
|
+
className: "dsh-channels-cfg__mono",
|
|
2453
|
+
children: check.error
|
|
2454
|
+
})
|
|
2455
|
+
});
|
|
2456
|
+
const required = check.missing.filter((item) => item.level === "required").length;
|
|
2457
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2458
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: required > 0 ? "bad" : "warn" }),
|
|
2459
|
+
label: required > 0 ? fillCopy(PANEL_COPY.scopeCheckMissingRequired, { n: required }) : fillCopy(PANEL_COPY.scopeCheckMissing, { n: check.missing.length }),
|
|
2460
|
+
summary: required > 0 ? PANEL_COPY.scopeCheckPublish : PANEL_COPY.scopeCheckMissingOptional,
|
|
2461
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2462
|
+
className: "dsh-channels-cfg__inputRow",
|
|
2463
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2464
|
+
type: "button",
|
|
2465
|
+
className: "dsh-channels-cfg__more",
|
|
2466
|
+
"aria-expanded": open,
|
|
2467
|
+
onClick: () => setOpen(!open),
|
|
2468
|
+
children: open ? PANEL_COPY.collapse : PANEL_COPY.expand
|
|
2469
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2470
|
+
className: "dsh-channels-cfg__btn",
|
|
2471
|
+
href: check.consoleUrl,
|
|
2472
|
+
target: "_blank",
|
|
2473
|
+
rel: "noreferrer noopener",
|
|
2474
|
+
children: PANEL_COPY.scopeCheckOpenAll
|
|
2475
|
+
})]
|
|
2476
|
+
})
|
|
2477
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2478
|
+
className: "dsh-channels-cfg__scopeList",
|
|
2479
|
+
children: check.missing.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2481
|
+
className: "dsh-channels-cfg__mono",
|
|
2482
|
+
children: item.scopes.join(" / ")
|
|
2082
2483
|
}),
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
children: PANEL_COPY.
|
|
2484
|
+
item.level === "required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
2485
|
+
tone: "bad",
|
|
2486
|
+
children: PANEL_COPY.scopeCheckRequired
|
|
2086
2487
|
}) : null,
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckMissing }),
|
|
2098
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2099
|
-
className: "dsh-channels-cfg__scopeList",
|
|
2100
|
-
children: check.missing.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2101
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2102
|
-
"[",
|
|
2103
|
-
item.level === "required" ? PANEL_COPY.scopeCheckRequired : PANEL_COPY.scopeCheckOptional,
|
|
2104
|
-
"] "
|
|
2105
|
-
] }),
|
|
2106
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2107
|
-
className: "dsh-channels-cfg__mono",
|
|
2108
|
-
children: item.scopes.join(" / ")
|
|
2109
|
-
}),
|
|
2110
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2111
|
-
":",
|
|
2112
|
-
item.purpose,
|
|
2113
|
-
" "
|
|
2114
|
-
] }),
|
|
2115
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2116
|
-
href: item.consoleUrl,
|
|
2117
|
-
target: "_blank",
|
|
2118
|
-
rel: "noreferrer noopener",
|
|
2119
|
-
children: PANEL_COPY.scopeCheckOpenOne
|
|
2120
|
-
})
|
|
2121
|
-
] }, item.scopes.join(",")))
|
|
2122
|
-
}),
|
|
2123
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2124
|
-
href: check.consoleUrl,
|
|
2125
|
-
target: "_blank",
|
|
2126
|
-
rel: "noreferrer noopener",
|
|
2127
|
-
children: PANEL_COPY.scopeCheckOpenAll
|
|
2128
|
-
})
|
|
2129
|
-
]
|
|
2130
|
-
}) : null
|
|
2131
|
-
]
|
|
2132
|
-
});
|
|
2488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.purpose }),
|
|
2489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2490
|
+
className: "dsh-channels-cfg__link",
|
|
2491
|
+
href: item.consoleUrl,
|
|
2492
|
+
target: "_blank",
|
|
2493
|
+
rel: "noreferrer noopener",
|
|
2494
|
+
children: PANEL_COPY.scopeCheckOpenOne
|
|
2495
|
+
})
|
|
2496
|
+
] }, item.scopes.join(",")))
|
|
2497
|
+
}) : null] });
|
|
2133
2498
|
}
|
|
2134
|
-
/** Post-connect native slash-command panel sync:
|
|
2135
|
-
function
|
|
2499
|
+
/** Post-connect native slash-command panel sync: the command count, or the Feishu reason. */
|
|
2500
|
+
function SlashCommandSyncRow(props) {
|
|
2136
2501
|
const { sync } = props;
|
|
2137
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
PANEL_COPY.slashCommandSyncTotal,
|
|
2149
|
-
" ",
|
|
2150
|
-
sync.total,
|
|
2151
|
-
" · ",
|
|
2152
|
-
PANEL_COPY.slashCommandSyncCreated,
|
|
2153
|
-
" ",
|
|
2154
|
-
sync.created,
|
|
2155
|
-
" · ",
|
|
2156
|
-
PANEL_COPY.slashCommandSyncUpdated,
|
|
2157
|
-
" ",
|
|
2158
|
-
sync.updated,
|
|
2159
|
-
" · ",
|
|
2160
|
-
PANEL_COPY.slashCommandSyncRemoved,
|
|
2161
|
-
" ",
|
|
2162
|
-
sync.removed
|
|
2163
|
-
] })]
|
|
2164
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2165
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2166
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.slashCommandSyncError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2502
|
+
return sync.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2503
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "ok" }),
|
|
2504
|
+
label: PANEL_COPY.slashCommandSyncTitle,
|
|
2505
|
+
summary: fillCopy(PANEL_COPY.slashCommandSyncOk, { total: sync.total })
|
|
2506
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2507
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { tone: "warn" }),
|
|
2508
|
+
label: PANEL_COPY.slashCommandSyncTitle,
|
|
2509
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2510
|
+
PANEL_COPY.slashCommandSyncError,
|
|
2511
|
+
":",
|
|
2512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2167
2513
|
className: "dsh-channels-cfg__mono",
|
|
2168
2514
|
children: sync.error
|
|
2169
|
-
})
|
|
2170
|
-
})
|
|
2515
|
+
})
|
|
2516
|
+
] })
|
|
2171
2517
|
});
|
|
2172
2518
|
}
|
|
2173
2519
|
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
@@ -2176,258 +2522,157 @@ window.__ModuleLoader__.load({
|
|
|
2176
2522
|
const keys = panel.config.restartPendingKeys.map((key) => fieldLabel(panel.config.fields, key)).join("、");
|
|
2177
2523
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2178
2524
|
className: "dsh-channels-cfg__banner",
|
|
2179
|
-
children: [
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
className: "dsh-channels-cfg__help",
|
|
2190
|
-
children: PANEL_COPY.restartWarning
|
|
2191
|
-
})
|
|
2192
|
-
]
|
|
2525
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2526
|
+
className: "dsh-channels-cfg__bannerText",
|
|
2527
|
+
children: [PANEL_COPY.restartBanner, keys]
|
|
2528
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2529
|
+
type: "button",
|
|
2530
|
+
className: "dsh-channels-cfg__btn",
|
|
2531
|
+
onClick: onReconnect,
|
|
2532
|
+
disabled: busy !== null || locked,
|
|
2533
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
2534
|
+
})]
|
|
2193
2535
|
});
|
|
2194
2536
|
}
|
|
2195
2537
|
//#endregion
|
|
2196
|
-
//#region src/client/panel/
|
|
2197
|
-
/**
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
*
|
|
2202
|
-
* @module dsh-channels/client/panel/model-tab
|
|
2203
|
-
*/
|
|
2204
|
-
function ModelTab(props) {
|
|
2205
|
-
const { form, catalogBusy } = props;
|
|
2538
|
+
//#region src/client/panel/fields-tab.tsx
|
|
2539
|
+
/** Footnotes under one group's section, per group. */
|
|
2540
|
+
const FOOTNOTES = { model: PANEL_COPY.modelFootnote };
|
|
2541
|
+
function FieldsTab(props) {
|
|
2542
|
+
const { form, tab, note } = props;
|
|
2206
2543
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2207
2544
|
className: "dsh-channels-cfg__tabBody",
|
|
2208
|
-
children: [
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
groups: TAB_FIELD_GROUPS.model
|
|
2217
|
-
}),
|
|
2218
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
2219
|
-
form,
|
|
2220
|
-
tab: "model"
|
|
2221
|
-
})
|
|
2222
|
-
]
|
|
2545
|
+
children: [note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2546
|
+
className: "dsh-channels-cfg__secFoot",
|
|
2547
|
+
children: note
|
|
2548
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2549
|
+
form,
|
|
2550
|
+
groups: TAB_FIELD_GROUPS[tab],
|
|
2551
|
+
footnotes: FOOTNOTES
|
|
2552
|
+
})]
|
|
2223
2553
|
});
|
|
2224
2554
|
}
|
|
2225
2555
|
//#endregion
|
|
2226
|
-
//#region src/client/panel/
|
|
2556
|
+
//#region src/client/panel/profiles.ts
|
|
2227
2557
|
/**
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2558
|
+
* Live Feishu names and avatars for every user and chat the access and wiki
|
|
2559
|
+
* tabs show. The page asks the Host once on entry and again whenever the set
|
|
2560
|
+
* of shown ids changes; a failed request keeps the previous answer.
|
|
2230
2561
|
*
|
|
2231
|
-
* @module dsh-channels/client/panel/
|
|
2562
|
+
* @module dsh-channels/client/panel/profiles
|
|
2232
2563
|
*/
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
children: PANEL_COPY.analyzeTokenCardHelp
|
|
2255
|
-
}),
|
|
2256
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2257
|
-
className: "dsh-channels-cfg__field",
|
|
2258
|
-
children: [
|
|
2259
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2260
|
-
className: "dsh-channels-cfg__label",
|
|
2261
|
-
children: PANEL_COPY.analyzeTokenLabel
|
|
2262
|
-
}),
|
|
2263
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2264
|
-
className: "dsh-channels-cfg__cred",
|
|
2265
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2266
|
-
type: "password",
|
|
2267
|
-
className: inputClass,
|
|
2268
|
-
value: token.value,
|
|
2269
|
-
placeholder: PANEL_COPY.analyzeTokenPlaceholder,
|
|
2270
|
-
autoComplete: "new-password",
|
|
2271
|
-
"aria-label": PANEL_COPY.analyzeTokenLabel,
|
|
2272
|
-
disabled,
|
|
2273
|
-
onChange: (event) => token.onChange(event.target.value)
|
|
2274
|
-
}), state.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2275
|
-
type: "button",
|
|
2276
|
-
className: "dsh-channels-cfg__btn",
|
|
2277
|
-
disabled,
|
|
2278
|
-
onClick: token.onClear,
|
|
2279
|
-
children: token.busy === "credentials-clear" ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential
|
|
2280
|
-
}) : null]
|
|
2281
|
-
}),
|
|
2282
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2283
|
-
className: "dsh-channels-cfg__help",
|
|
2284
|
-
children: [
|
|
2285
|
-
PANEL_COPY.credentialsRef,
|
|
2286
|
-
":",
|
|
2287
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2288
|
-
className: "dsh-channels-cfg__mono",
|
|
2289
|
-
children: state.ref
|
|
2290
|
-
}),
|
|
2291
|
-
state.source === void 0 ? "" : ` · ${state.source}`
|
|
2292
|
-
]
|
|
2293
|
-
}),
|
|
2294
|
-
issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2295
|
-
className: "dsh-channels-cfg__fieldError",
|
|
2296
|
-
children: issue.message
|
|
2297
|
-
})
|
|
2298
|
-
]
|
|
2299
|
-
}),
|
|
2300
|
-
state.writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2301
|
-
className: "dsh-channels-cfg__fieldError",
|
|
2302
|
-
children: PANEL_COPY.credentialReadOnly
|
|
2303
|
-
}),
|
|
2304
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2305
|
-
className: "dsh-channels-cfg__footer",
|
|
2306
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2307
|
-
type: "button",
|
|
2308
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2309
|
-
onClick: token.onSave,
|
|
2310
|
-
disabled: disabled || token.value.trim() === "",
|
|
2311
|
-
children: token.busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveAnalyzeToken
|
|
2312
|
-
})
|
|
2313
|
-
})
|
|
2314
|
-
]
|
|
2315
|
-
});
|
|
2564
|
+
/**
|
|
2565
|
+
* The `user_id`s and `chat_id`s one access snapshot shows.
|
|
2566
|
+
* @param access - the panel's access snapshot.
|
|
2567
|
+
* @returns sorted, distinct ids per kind.
|
|
2568
|
+
*/
|
|
2569
|
+
function profileIds(access) {
|
|
2570
|
+
const users = /* @__PURE__ */ new Set();
|
|
2571
|
+
const chats = /* @__PURE__ */ new Set();
|
|
2572
|
+
if (access.admin.userId !== void 0) users.add(access.admin.userId);
|
|
2573
|
+
for (const user of access.users) users.add(user.userId);
|
|
2574
|
+
for (const user of access.wikiUsers) users.add(user.userId);
|
|
2575
|
+
for (const group of access.groups) chats.add(group.chatId);
|
|
2576
|
+
for (const request of access.requests) {
|
|
2577
|
+
if (request.kind === "user") users.add(request.targetId);
|
|
2578
|
+
else chats.add(request.targetId);
|
|
2579
|
+
if (request.chatId !== void 0) chats.add(request.chatId);
|
|
2580
|
+
}
|
|
2581
|
+
return {
|
|
2582
|
+
users: [...users].sort(),
|
|
2583
|
+
chats: [...chats].sort()
|
|
2584
|
+
};
|
|
2316
2585
|
}
|
|
2317
|
-
//#endregion
|
|
2318
|
-
//#region src/client/panel/sessions-tab.tsx
|
|
2319
2586
|
/**
|
|
2320
|
-
*
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
2323
|
-
*
|
|
2324
|
-
* @module dsh-channels/client/panel/sessions-tab
|
|
2587
|
+
* Profiles for one access snapshot; empty until the panel has loaded.
|
|
2588
|
+
* @param access - the panel's access snapshot, null while loading.
|
|
2589
|
+
* @returns lookups by id and the latest failure reason.
|
|
2325
2590
|
*/
|
|
2326
|
-
function
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
}),
|
|
2336
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
2337
|
-
form,
|
|
2338
|
-
tab: "sessions"
|
|
2339
|
-
}),
|
|
2340
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnalyzeTokenCard, {
|
|
2341
|
-
panel: form.panel,
|
|
2342
|
-
token: analyzeToken
|
|
2343
|
-
})
|
|
2344
|
-
]
|
|
2591
|
+
function useFeishuProfiles(access) {
|
|
2592
|
+
const ids = (0, react.useMemo)(() => access === null ? {
|
|
2593
|
+
users: [],
|
|
2594
|
+
chats: []
|
|
2595
|
+
} : profileIds(access), [access]);
|
|
2596
|
+
const key = `${ids.users.join(",")}|${ids.chats.join(",")}`;
|
|
2597
|
+
const [answer, setAnswer] = (0, react.useState)({
|
|
2598
|
+
users: {},
|
|
2599
|
+
chats: {}
|
|
2345
2600
|
});
|
|
2601
|
+
const [requestError, setRequestError] = (0, react.useState)(void 0);
|
|
2602
|
+
(0, react.useEffect)(() => {
|
|
2603
|
+
if (ids.users.length === 0 && ids.chats.length === 0) return;
|
|
2604
|
+
let current = true;
|
|
2605
|
+
fetchFeishuProfiles(ids.users, ids.chats).then((next) => {
|
|
2606
|
+
if (!current) return;
|
|
2607
|
+
setAnswer(next);
|
|
2608
|
+
setRequestError(void 0);
|
|
2609
|
+
}, (error) => {
|
|
2610
|
+
if (current) setRequestError(error instanceof Error ? error.message : String(error));
|
|
2611
|
+
});
|
|
2612
|
+
return () => {
|
|
2613
|
+
current = false;
|
|
2614
|
+
};
|
|
2615
|
+
}, [key]);
|
|
2616
|
+
return (0, react.useMemo)(() => {
|
|
2617
|
+
const warning = requestError ?? answer.warning;
|
|
2618
|
+
return {
|
|
2619
|
+
user: (userId) => answer.users[userId],
|
|
2620
|
+
chat: (chatId) => answer.chats[chatId],
|
|
2621
|
+
...warning === void 0 ? {} : { warning }
|
|
2622
|
+
};
|
|
2623
|
+
}, [answer, requestError]);
|
|
2346
2624
|
}
|
|
2347
2625
|
//#endregion
|
|
2348
2626
|
//#region src/client/panel/wiki-tab.tsx
|
|
2349
2627
|
/**
|
|
2350
|
-
* 「知识库」tab
|
|
2351
|
-
*
|
|
2628
|
+
* 「知识库」tab:知识库工具字段(读取 / 编辑开关、可访问范围)+ 知识库
|
|
2629
|
+
* 白名单(「可使用的人」)。字段保存走全局 draft,白名单增删是独立即时
|
|
2630
|
+
* mutation。
|
|
2352
2631
|
*
|
|
2353
2632
|
* @module dsh-channels/client/panel/wiki-tab
|
|
2354
2633
|
*/
|
|
2355
2634
|
function WikiTab(props) {
|
|
2356
|
-
const { form, accessNotice, accessError, onAddUser, onRemoveUser } = props;
|
|
2635
|
+
const { form, profiles, accessNotice, accessError, onAddUser, onRemoveUser } = props;
|
|
2357
2636
|
const { access } = form.panel;
|
|
2358
2637
|
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
2359
2638
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2360
2639
|
className: "dsh-channels-cfg__tabBody",
|
|
2361
2640
|
children: [
|
|
2362
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
groups: TAB_FIELD_GROUPS.wiki
|
|
2641
|
+
accessNotice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
2642
|
+
kind: "ok",
|
|
2643
|
+
text: accessNotice
|
|
2366
2644
|
}),
|
|
2367
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2368
|
-
|
|
2369
|
-
|
|
2645
|
+
accessError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
2646
|
+
kind: "error",
|
|
2647
|
+
text: accessError
|
|
2370
2648
|
}),
|
|
2371
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2372
|
-
className: "dsh-channels-
|
|
2373
|
-
children:
|
|
2374
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2375
|
-
className: "dsh-channels-cfg__cardHead",
|
|
2376
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2377
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
2378
|
-
children: PANEL_COPY.wikiUsersCard
|
|
2379
|
-
})
|
|
2380
|
-
}),
|
|
2381
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2382
|
-
className: "dsh-channels-cfg__help",
|
|
2383
|
-
children: PANEL_COPY.wikiUsersHelp
|
|
2384
|
-
}),
|
|
2385
|
-
!access.loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, { text: PANEL_COPY.accessNotLoaded }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserWhitelist, {
|
|
2386
|
-
title: PANEL_COPY.wikiUsersCard,
|
|
2387
|
-
empty: PANEL_COPY.wikiUsersEmpty,
|
|
2388
|
-
addLabel: PANEL_COPY.accessAddUser,
|
|
2389
|
-
users: access.wikiUsers,
|
|
2390
|
-
disabled,
|
|
2391
|
-
working: form.busy === "access",
|
|
2392
|
-
onAdd: onAddUser,
|
|
2393
|
-
onRemove: onRemoveUser
|
|
2394
|
-
}),
|
|
2395
|
-
accessNotice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2396
|
-
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
2397
|
-
role: "status",
|
|
2398
|
-
"aria-live": "polite",
|
|
2399
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: accessNotice })
|
|
2400
|
-
}),
|
|
2401
|
-
accessError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2402
|
-
className: "dsh-channels-cfg__fieldError",
|
|
2403
|
-
role: "alert",
|
|
2404
|
-
children: accessError
|
|
2405
|
-
})
|
|
2406
|
-
]
|
|
2407
|
-
})
|
|
2408
|
-
]
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
//#endregion
|
|
2412
|
-
//#region src/client/panel/workspace-tab.tsx
|
|
2413
|
-
/**
|
|
2414
|
-
* 「工作区」tab:工作区根目录字段与保存栏。
|
|
2415
|
-
*
|
|
2416
|
-
* @module dsh-channels/client/panel/workspace-tab
|
|
2417
|
-
*/
|
|
2418
|
-
function WorkspaceTab(props) {
|
|
2419
|
-
const { form } = props;
|
|
2420
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2421
|
-
className: "dsh-channels-cfg__tabBody",
|
|
2422
|
-
children: [
|
|
2423
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabHelp, { text: PANEL_COPY.workspaceTabHelp }),
|
|
2424
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsCard, {
|
|
2425
|
-
form,
|
|
2426
|
-
groups: TAB_FIELD_GROUPS.workspace
|
|
2649
|
+
profiles.warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2650
|
+
className: "dsh-channels-cfg__secFoot",
|
|
2651
|
+
children: fillCopy(PANEL_COPY.profilesWarning, { reason: profiles.warning })
|
|
2427
2652
|
}),
|
|
2428
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2429
2654
|
form,
|
|
2430
|
-
|
|
2655
|
+
groups: TAB_FIELD_GROUPS.wiki
|
|
2656
|
+
}),
|
|
2657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
2658
|
+
title: PANEL_COPY.wikiUsersTitle,
|
|
2659
|
+
countLabel: fillCopy(PANEL_COPY.wikiUsersAdminPlus, { n: access.wikiUsers.length }),
|
|
2660
|
+
entries: access.wikiUsers.map((user) => ({
|
|
2661
|
+
id: user.userId,
|
|
2662
|
+
name: profiles.user(user.userId)?.name ?? user.name,
|
|
2663
|
+
avatarUrl: profiles.user(user.userId)?.avatarUrl,
|
|
2664
|
+
source: user.source
|
|
2665
|
+
})),
|
|
2666
|
+
empty: PANEL_COPY.wikiUsersEmpty,
|
|
2667
|
+
loaded: access.loaded,
|
|
2668
|
+
addLabel: PANEL_COPY.accessAddUser,
|
|
2669
|
+
idPlaceholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
2670
|
+
namePlaceholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
2671
|
+
footnote: PANEL_COPY.wikiUsersHint,
|
|
2672
|
+
disabled,
|
|
2673
|
+
working: form.busy === "access",
|
|
2674
|
+
onAdd: onAddUser,
|
|
2675
|
+
onRemove: onRemoveUser
|
|
2431
2676
|
})
|
|
2432
2677
|
]
|
|
2433
2678
|
});
|
|
@@ -2435,18 +2680,32 @@ window.__ModuleLoader__.load({
|
|
|
2435
2680
|
//#endregion
|
|
2436
2681
|
//#region src/client/panel/index.tsx
|
|
2437
2682
|
/**
|
|
2438
|
-
*
|
|
2439
|
-
* tab
|
|
2683
|
+
* 左侧栏「飞书渠道」主区页面:数据加载、5 秒状态轮询、全局 draft、顶部状态头 /
|
|
2684
|
+
* tab 导航与底部保存条的外壳。
|
|
2440
2685
|
*
|
|
2441
2686
|
* 所有 Host 交互(panel / status / catalog / config / credentials / access /
|
|
2442
2687
|
* pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
2443
|
-
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `
|
|
2444
|
-
* 槽位。
|
|
2688
|
+
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `main` 槽位。
|
|
2445
2689
|
*
|
|
2446
2690
|
* @module dsh-channels/client/panel
|
|
2447
2691
|
*/
|
|
2448
|
-
/**
|
|
2449
|
-
|
|
2692
|
+
/**
|
|
2693
|
+
* Main-column page for the `main` slot: a full-height scroll region with the
|
|
2694
|
+
* centered panel column. The slot passes no owner props.
|
|
2695
|
+
* @returns the page.
|
|
2696
|
+
*/
|
|
2697
|
+
function FeishuChannelsPage() {
|
|
2698
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
2699
|
+
className: "dsh-channels-page",
|
|
2700
|
+
"aria-label": PANEL_COPY.title,
|
|
2701
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2702
|
+
className: "dsh-channels-page__content",
|
|
2703
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsPanel, {})
|
|
2704
|
+
})
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
/** Panel shell: one panel payload, one status poll, one global config draft. */
|
|
2708
|
+
function FeishuChannelsPanel() {
|
|
2450
2709
|
const [load, setLoad] = (0, react.useState)({ phase: "loading" });
|
|
2451
2710
|
const [activeTab, setActiveTab] = (0, react.useState)("connection");
|
|
2452
2711
|
const [draft, setDraft] = (0, react.useState)({});
|
|
@@ -2702,22 +2961,14 @@ window.__ModuleLoader__.load({
|
|
|
2702
2961
|
setBusy(null);
|
|
2703
2962
|
}
|
|
2704
2963
|
}, [draft, panel]);
|
|
2705
|
-
/** Drop the draft
|
|
2706
|
-
const
|
|
2964
|
+
/** Drop the whole draft (nothing is written). */
|
|
2965
|
+
const discardDraft = (0, react.useCallback)(() => {
|
|
2707
2966
|
if (panel === null) return;
|
|
2708
|
-
|
|
2709
|
-
const keys = new Set(panel.config.fields.filter((field) => groups.has(field.group)).map((field) => field.key));
|
|
2710
|
-
setDraft((previous) => {
|
|
2711
|
-
const next = { ...previous };
|
|
2712
|
-
for (const key of keys) delete next[key];
|
|
2713
|
-
return next;
|
|
2714
|
-
});
|
|
2967
|
+
setDraft({});
|
|
2715
2968
|
setNotice(null);
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
setCatalogBusy(false);
|
|
2720
|
-
}
|
|
2969
|
+
catalogTicket.current += 1;
|
|
2970
|
+
setCatalog(catalogFromFields(panel.config.fields));
|
|
2971
|
+
setCatalogBusy(false);
|
|
2721
2972
|
}, [panel]);
|
|
2722
2973
|
/**
|
|
2723
2974
|
* Open the host's native directory chooser (workspace root「选择目录…」) and
|
|
@@ -2753,6 +3004,7 @@ window.__ModuleLoader__.load({
|
|
|
2753
3004
|
}, []);
|
|
2754
3005
|
/** Drop every Web override and return the refreshed panel. */
|
|
2755
3006
|
const onClear = (0, react.useCallback)(async () => {
|
|
3007
|
+
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.clearConfirm) : true)) return;
|
|
2756
3008
|
setBusy("clear");
|
|
2757
3009
|
setNotice(null);
|
|
2758
3010
|
try {
|
|
@@ -3036,6 +3288,7 @@ window.__ModuleLoader__.load({
|
|
|
3036
3288
|
}, []);
|
|
3037
3289
|
const issues = (0, react.useMemo)(() => notice?.kind === "error" ? notice.issues ?? [] : [], [notice]);
|
|
3038
3290
|
const dirtyCount = Object.keys(draft).length;
|
|
3291
|
+
const restartCount = (0, react.useMemo)(() => (panel?.config.fields ?? []).filter((field) => field.restartRequired && field.key in draft).length, [draft, panel]);
|
|
3039
3292
|
/** Which tabs currently own a dirty key (the dot is per tab, not global). */
|
|
3040
3293
|
const dirtyTabs = (0, react.useMemo)(() => {
|
|
3041
3294
|
const tabs = /* @__PURE__ */ new Set();
|
|
@@ -3061,27 +3314,20 @@ window.__ModuleLoader__.load({
|
|
|
3061
3314
|
issues,
|
|
3062
3315
|
busy,
|
|
3063
3316
|
locked: hostBusy,
|
|
3064
|
-
dirtyCount,
|
|
3065
3317
|
onChange: updateDraft,
|
|
3066
|
-
onSave: () => {
|
|
3067
|
-
onSave();
|
|
3068
|
-
},
|
|
3069
|
-
onDiscardTab: discardTab,
|
|
3070
3318
|
onPickDirectory
|
|
3071
3319
|
};
|
|
3072
3320
|
}, [
|
|
3073
3321
|
busy,
|
|
3074
|
-
discardTab,
|
|
3075
|
-
dirtyCount,
|
|
3076
3322
|
draft,
|
|
3077
3323
|
hostBusy,
|
|
3078
3324
|
issues,
|
|
3079
3325
|
onPickDirectory,
|
|
3080
|
-
onSave,
|
|
3081
3326
|
panel,
|
|
3082
3327
|
renderedFields,
|
|
3083
3328
|
updateDraft
|
|
3084
3329
|
]);
|
|
3330
|
+
const profiles = useFeishuProfiles(panel?.access ?? null);
|
|
3085
3331
|
const credentials = (0, react.useMemo)(() => ({
|
|
3086
3332
|
appId: appIdInput,
|
|
3087
3333
|
appSecret: appSecretInput,
|
|
@@ -3167,9 +3413,17 @@ window.__ModuleLoader__.load({
|
|
|
3167
3413
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabNav, {
|
|
3168
3414
|
activeTab,
|
|
3169
3415
|
dirtyTabs,
|
|
3416
|
+
counts: { access: load.panel.access.loaded ? load.panel.access.requests.length : 0 },
|
|
3170
3417
|
onChange: setActiveTab
|
|
3171
3418
|
}),
|
|
3172
|
-
|
|
3419
|
+
load.panel.access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3420
|
+
className: "dsh-channels-cfg__banner",
|
|
3421
|
+
children: PANEL_COPY.debugModeWarning
|
|
3422
|
+
}) : null,
|
|
3423
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBar, {
|
|
3424
|
+
kind: notice.kind,
|
|
3425
|
+
text: notice.text
|
|
3426
|
+
}),
|
|
3173
3427
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3174
3428
|
role: "tabpanel",
|
|
3175
3429
|
id: `dsh-channels-tabpanel-${activeTab}`,
|
|
@@ -3184,17 +3438,22 @@ window.__ModuleLoader__.load({
|
|
|
3184
3438
|
onReconnect();
|
|
3185
3439
|
}
|
|
3186
3440
|
}) : null,
|
|
3187
|
-
activeTab === "sessions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3441
|
+
activeTab === "sessions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
3188
3442
|
form,
|
|
3189
|
-
|
|
3443
|
+
tab: "sessions"
|
|
3190
3444
|
}) : null,
|
|
3191
|
-
activeTab === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3445
|
+
activeTab === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
3192
3446
|
form,
|
|
3193
|
-
|
|
3447
|
+
tab: "model",
|
|
3448
|
+
note: catalogBusy ? PANEL_COPY.catalogLoading : void 0
|
|
3449
|
+
}) : null,
|
|
3450
|
+
activeTab === "attachments" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldsTab, {
|
|
3451
|
+
form,
|
|
3452
|
+
tab: "attachments"
|
|
3194
3453
|
}) : null,
|
|
3195
|
-
activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, { form }) : null,
|
|
3196
3454
|
activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
|
|
3197
3455
|
form,
|
|
3456
|
+
profiles,
|
|
3198
3457
|
accessNotice,
|
|
3199
3458
|
accessError,
|
|
3200
3459
|
onAddGroup: onAddAccessGroup,
|
|
@@ -3211,6 +3470,7 @@ window.__ModuleLoader__.load({
|
|
|
3211
3470
|
}) : null,
|
|
3212
3471
|
activeTab === "wiki" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WikiTab, {
|
|
3213
3472
|
form,
|
|
3473
|
+
profiles,
|
|
3214
3474
|
accessNotice,
|
|
3215
3475
|
accessError,
|
|
3216
3476
|
onAddUser: (userId, name) => addUser(addWikiUser, userId, name, PANEL_COPY.wikiUserAdded),
|
|
@@ -3220,64 +3480,81 @@ window.__ModuleLoader__.load({
|
|
|
3220
3480
|
}) : null,
|
|
3221
3481
|
activeTab === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedTab, {
|
|
3222
3482
|
form,
|
|
3483
|
+
analyzeToken,
|
|
3223
3484
|
onClear: () => {
|
|
3224
3485
|
onClear();
|
|
3225
3486
|
}
|
|
3226
3487
|
}) : null
|
|
3227
3488
|
]
|
|
3489
|
+
}),
|
|
3490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
3491
|
+
dirtyCount,
|
|
3492
|
+
restartCount,
|
|
3493
|
+
busy,
|
|
3494
|
+
locked: hostBusy,
|
|
3495
|
+
onSave: () => {
|
|
3496
|
+
onSave();
|
|
3497
|
+
},
|
|
3498
|
+
onDiscard: discardDraft
|
|
3228
3499
|
})
|
|
3229
3500
|
]
|
|
3230
3501
|
});
|
|
3231
3502
|
}
|
|
3232
|
-
/** Page header: title
|
|
3503
|
+
/** Page header: title, phase pill, version and uptime, refresh; visible on every tab. */
|
|
3233
3504
|
function PanelHeader(props) {
|
|
3234
3505
|
const { status, pollError, busy, locked, onRefresh } = props;
|
|
3235
3506
|
const badge = phaseBadge(status.phase);
|
|
3236
3507
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
3237
3508
|
className: "dsh-channels-cfg__panelHead",
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3509
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3510
|
+
className: "dsh-channels-cfg__headRow",
|
|
3511
|
+
children: [
|
|
3512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
3513
|
+
className: "dsh-channels-cfg__title",
|
|
3514
|
+
children: PANEL_COPY.title
|
|
3515
|
+
}),
|
|
3516
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pill, {
|
|
3517
|
+
tone: badge.tone,
|
|
3518
|
+
dot: true,
|
|
3519
|
+
children: badge.text
|
|
3520
|
+
}),
|
|
3521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3522
|
+
className: "dsh-channels-cfg__meta dsh-channels-cfg__mono",
|
|
3523
|
+
children: ["v", status.version]
|
|
3524
|
+
}),
|
|
3525
|
+
status.uptimeMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3526
|
+
className: "dsh-channels-cfg__meta",
|
|
3527
|
+
children: [
|
|
3528
|
+
"· ",
|
|
3529
|
+
PANEL_COPY.uptimePrefix,
|
|
3530
|
+
" ",
|
|
3531
|
+
formatUptime(status.uptimeMs)
|
|
3532
|
+
]
|
|
3533
|
+
}),
|
|
3534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
3535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3536
|
+
type: "button",
|
|
3537
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost",
|
|
3538
|
+
onClick: onRefresh,
|
|
3539
|
+
disabled: busy !== null || locked,
|
|
3540
|
+
children: PANEL_COPY.refresh
|
|
3541
|
+
})
|
|
3542
|
+
]
|
|
3543
|
+
}), pollError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3544
|
+
className: "dsh-channels-cfg__fieldError",
|
|
3545
|
+
role: "status",
|
|
3546
|
+
"aria-live": "polite",
|
|
3547
|
+
children: [
|
|
3548
|
+
PANEL_COPY.pollFailed,
|
|
3549
|
+
":",
|
|
3550
|
+
pollError
|
|
3551
|
+
]
|
|
3552
|
+
})]
|
|
3276
3553
|
});
|
|
3277
3554
|
}
|
|
3278
3555
|
/** Top-level tab navigation; the dirty dot keeps unsaved work visible per tab. */
|
|
3279
3556
|
function TabNav(props) {
|
|
3280
|
-
const { activeTab, dirtyTabs, onChange } = props;
|
|
3557
|
+
const { activeTab, dirtyTabs, counts, onChange } = props;
|
|
3281
3558
|
const refs = (0, react.useRef)([]);
|
|
3282
3559
|
/** Roving-focus keyboard navigation between tabs (WAI-ARIA tabs pattern). */
|
|
3283
3560
|
const onKeyDown = (index, event) => {
|
|
@@ -3322,11 +3599,17 @@ window.__ModuleLoader__.load({
|
|
|
3322
3599
|
className: "dsh-channels-cfg__tab",
|
|
3323
3600
|
onClick: () => onChange(tab.id),
|
|
3324
3601
|
onKeyDown: (event) => onKeyDown(index, event),
|
|
3325
|
-
children: [
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3602
|
+
children: [
|
|
3603
|
+
tab.label,
|
|
3604
|
+
(counts[tab.id] ?? 0) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3605
|
+
className: "dsh-channels-cfg__tabCount",
|
|
3606
|
+
children: counts[tab.id]
|
|
3607
|
+
}) : null,
|
|
3608
|
+
dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3609
|
+
className: "dsh-channels-cfg__tabDot",
|
|
3610
|
+
"aria-hidden": "true"
|
|
3611
|
+
}) : null
|
|
3612
|
+
]
|
|
3330
3613
|
}, tab.id);
|
|
3331
3614
|
})
|
|
3332
3615
|
});
|
|
@@ -3334,109 +3617,149 @@ window.__ModuleLoader__.load({
|
|
|
3334
3617
|
//#endregion
|
|
3335
3618
|
//#region src/client/styles.ts
|
|
3336
3619
|
/**
|
|
3337
|
-
*
|
|
3338
|
-
* fallbacks
|
|
3339
|
-
*
|
|
3620
|
+
* Feishu channel page styling. DSH semantic tokens (`--dsw-alias-*`) with neutral
|
|
3621
|
+
* fallbacks, aliased once to local `--dc-*` variables, so the page follows
|
|
3622
|
+
* the active theme.
|
|
3340
3623
|
*
|
|
3341
3624
|
* @module dsh-channels/client/styles
|
|
3342
3625
|
*/
|
|
3343
3626
|
const STYLE_ATTRIBUTE = "data-dsh-channels-css";
|
|
3344
3627
|
const CSS = `
|
|
3345
|
-
.dsh-channels-
|
|
3346
|
-
.dsh-channels-
|
|
3628
|
+
.dsh-channels-page{width:100%;height:100%;min-width:0;min-height:0;overflow:auto;scrollbar-gutter:stable;background:var(--dsw-alias-bg-base,#fff)}
|
|
3629
|
+
.dsh-channels-page__content{box-sizing:border-box;max-width:816px;margin:0 auto;padding:18px clamp(24px,4vw,48px) 48px}
|
|
3630
|
+
[data-platform='darwin'] .dsh-channels-page__content{padding-top:calc(18px + var(--dsh-frame-top-clearance,0px))}
|
|
3631
|
+
.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}
|
|
3632
|
+
.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)}
|
|
3347
3633
|
.dsh-channels-cfg__headRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3348
|
-
.dsh-channels-cfg__title{margin:0;font-size:
|
|
3349
|
-
.dsh-channels-
|
|
3350
|
-
.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))}
|
|
3351
|
-
.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}
|
|
3352
|
-
.dsh-channels-cfg__tab:hover{color:var(--dsw-alias-label-primary,#1f2328)}
|
|
3353
|
-
.dsh-channels-cfg__tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:-2px}
|
|
3354
|
-
.dsh-channels-cfg__tab[aria-selected="true"]{color:var(--dsw-alias-state-business-primary,#0a67d3);border-bottom-color:currentColor;font-weight:500}
|
|
3355
|
-
.dsh-channels-cfg__tabDot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-warning-primary,#a15c00);flex:none}
|
|
3356
|
-
.dsh-channels-cfg__tabBody{display:flex;flex-direction:column;gap:12px}
|
|
3357
|
-
.dsh-channels-cfg__tabHelp{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
3358
|
-
.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}
|
|
3359
|
-
.dsh-channels-cfg__cardHead{display:flex;align-items:center;gap:8px;min-height:20px;flex-wrap:wrap}
|
|
3360
|
-
.dsh-channels-cfg__cardTitle{font-size:13px;font-weight:500}
|
|
3361
|
-
.dsh-channels-cfg__cardMeta{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
3362
|
-
.dsh-channels-cfg__group{display:flex;flex-direction:column;gap:8px}
|
|
3363
|
-
.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}
|
|
3364
|
-
.dsh-channels-cfg__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
|
|
3365
|
-
.dsh-channels-cfg__badge--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);border-color:currentColor}
|
|
3366
|
-
.dsh-channels-cfg__badge--warn{color:var(--dsw-alias-state-warning-primary,#a15c00);border-color:currentColor}
|
|
3367
|
-
.dsh-channels-cfg__badge--bad{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
3368
|
-
.dsh-channels-cfg__badge--muted{color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3369
|
-
.dsh-channels-cfg__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 16px}
|
|
3370
|
-
.dsh-channels-cfg__fact{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3371
|
-
.dsh-channels-cfg__factLabel{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
3372
|
-
.dsh-channels-cfg__factValue{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
3373
|
-
.dsh-channels-cfg__actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3374
|
-
.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}
|
|
3375
|
-
.dsh-channels-cfg__btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
|
|
3376
|
-
.dsh-channels-cfg__btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:2px}
|
|
3377
|
-
.dsh-channels-cfg__btn:disabled{color:var(--dsw-alias-label-dimmed,#b0b4ba);cursor:default}
|
|
3378
|
-
.dsh-channels-cfg__btn--primary{background:var(--dsw-alias-button-primary-fill,#0a67d3);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
3379
|
-
.dsh-channels-cfg__btn--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#0b5cba)}
|
|
3380
|
-
.dsh-channels-cfg__btn--danger{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
3381
|
-
.dsh-channels-cfg__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px;align-items:start}
|
|
3382
|
-
.dsh-channels-cfg__field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
3383
|
-
.dsh-channels-cfg__field--wide{grid-column:1/-1}
|
|
3384
|
-
.dsh-channels-cfg__labelRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
3385
|
-
.dsh-channels-cfg__label{font-size:12px;color:var(--dsw-alias-label-secondary,#5c6169)}
|
|
3386
|
-
.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}
|
|
3387
|
-
.dsh-channels-cfg__tag--restart{color:var(--dsw-alias-state-warning-primary,#a15c00)}
|
|
3388
|
-
.dsh-channels-cfg__tag--dirty{color:var(--dsw-alias-state-warning-primary,#a15c00);border-style:solid}
|
|
3389
|
-
.dsh-channels-cfg__help{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px}
|
|
3390
|
-
.dsh-channels-cfg__reasonBad{color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
3391
|
-
.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}
|
|
3392
|
-
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
3393
|
-
.dsh-channels-cfg__inputRow .dsh-channels-cfg__input{flex:1 1 auto;width:auto;min-width:0}
|
|
3394
|
-
.dsh-channels-cfg__btn--browse{flex:none}
|
|
3395
|
-
.dsh-channels-cfg__list{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3396
|
-
.dsh-channels-cfg__listItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3397
|
-
.dsh-channels-cfg__btn--listRemove{flex:none}
|
|
3398
|
-
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
3399
|
-
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
3400
|
-
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
3401
|
-
.dsh-channels-cfg__check{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;font-size:13px}
|
|
3402
|
-
.dsh-channels-cfg__checkbox{width:15px;height:15px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0a67d3);cursor:pointer}
|
|
3403
|
-
.dsh-channels-cfg__fieldError{color:var(--dsw-alias-state-danger-primary,#c62828);font-size:11px;line-height:16px}
|
|
3404
|
-
.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}
|
|
3405
|
-
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
3406
|
-
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
3407
|
-
.dsh-channels-cfg__notice a{color:inherit;text-decoration:underline}
|
|
3408
|
-
.dsh-channels-cfg__scopeList{margin:2px 0;padding-left:18px;display:flex;flex-direction:column;gap:2px}
|
|
3409
|
-
.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}
|
|
3410
|
-
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
3634
|
+
.dsh-channels-cfg__title{margin:0 2px 0 0;font-size:20px;font-weight:500;line-height:28px}
|
|
3635
|
+
.dsh-channels-cfg__meta{color:var(--dc-fg3);font-size:12px}
|
|
3411
3636
|
.dsh-channels-cfg__spacer{flex:1}
|
|
3637
|
+
.dsh-channels-cfg__mono{font-family:var(--dc-mono);font-size:12px}
|
|
3638
|
+
.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)}
|
|
3639
|
+
.dsh-channels-cfg__pill--ok{color:var(--dc-ok)}
|
|
3640
|
+
.dsh-channels-cfg__pill--warn{color:var(--dc-warn)}
|
|
3641
|
+
.dsh-channels-cfg__pill--bad{color:var(--dc-bad)}
|
|
3642
|
+
.dsh-channels-cfg__dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
|
|
3643
|
+
.dsh-channels-cfg__tabs{display:flex;align-items:center;gap:18px;border-bottom:1px solid var(--dc-line);overflow-x:auto;scrollbar-width:none}
|
|
3644
|
+
.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}
|
|
3645
|
+
.dsh-channels-cfg__tab:hover{color:var(--dc-fg)}
|
|
3646
|
+
.dsh-channels-cfg__tab[aria-selected="true"]{color:var(--dc-fg);border-bottom-color:currentColor;font-weight:600}
|
|
3647
|
+
.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)}
|
|
3648
|
+
.dsh-channels-cfg__tabDot{width:6px;height:6px;border-radius:50%;background:var(--dc-accent);flex:none}
|
|
3649
|
+
.dsh-channels-cfg__tabBody{display:flex;flex-direction:column;gap:22px;padding:4px 0 8px}
|
|
3650
|
+
.dsh-channels-cfg__sec{display:flex;flex-direction:column;gap:6px;margin:0}
|
|
3651
|
+
.dsh-channels-cfg__secTitle{margin:0;font-size:12px;font-weight:600;line-height:18px;color:var(--dc-fg3);letter-spacing:.02em}
|
|
3652
|
+
.dsh-channels-cfg__secFoot{margin:0;color:var(--dc-fg3);font-size:12px;line-height:18px}
|
|
3653
|
+
.dsh-channels-cfg__list{display:flex;flex-direction:column;border:1px solid var(--dc-line);border-radius:10px}
|
|
3654
|
+
.dsh-channels-cfg__list>*+*{border-top:1px solid var(--dc-line)}
|
|
3655
|
+
.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)}
|
|
3656
|
+
.dsh-channels-cfg__row{display:flex;align-items:center;gap:16px;padding:11px 14px;min-height:28px}
|
|
3657
|
+
.dsh-channels-cfg__row--wide{flex-direction:column;align-items:stretch;gap:8px}
|
|
3658
|
+
.dsh-channels-cfg__rowText{display:flex;flex-direction:column;gap:1px;flex:1 1 auto;min-width:0}
|
|
3659
|
+
.dsh-channels-cfg__rowLabel{display:flex;align-items:center;gap:6px;flex-wrap:wrap;overflow-wrap:anywhere}
|
|
3660
|
+
.dsh-channels-cfg__rowSummary{color:var(--dc-fg3);font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
3661
|
+
.dsh-channels-cfg__rowCtl{flex:none;display:flex;align-items:center;justify-content:flex-end;min-width:0;max-width:62%}
|
|
3662
|
+
.dsh-channels-cfg__row--wide .dsh-channels-cfg__rowCtl{max-width:none;justify-content:stretch}
|
|
3663
|
+
.dsh-channels-cfg__row--wide .dsh-channels-cfg__rowCtl>*{flex:1 1 auto}
|
|
3664
|
+
.dsh-channels-cfg__details{padding:0 14px 12px;color:var(--dc-fg2);font-size:12px;line-height:19px;overflow-wrap:anywhere}
|
|
3665
|
+
.dsh-channels-cfg__rowError{padding:0 14px 10px;color:var(--dc-bad);font-size:12px;line-height:18px}
|
|
3666
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__rowWrap{border-top-width:0}
|
|
3667
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__rowWrap+*{border-top-width:1px}
|
|
3668
|
+
.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)}
|
|
3669
|
+
.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}
|
|
3670
|
+
.dsh-channels-cfg__more:hover{color:var(--dc-fg2)}
|
|
3671
|
+
.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}
|
|
3672
|
+
.dsh-channels-cfg__link:disabled{color:var(--dc-dim);cursor:default}
|
|
3673
|
+
.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}
|
|
3674
|
+
.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}
|
|
3675
|
+
.dsh-channels-cfg__switch[aria-checked="true"]{background:var(--dc-accent)}
|
|
3676
|
+
.dsh-channels-cfg__switch[aria-checked="true"]::after{transform:translateX(14px)}
|
|
3677
|
+
.dsh-channels-cfg__switch:disabled{opacity:.5;cursor:default}
|
|
3678
|
+
.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}
|
|
3679
|
+
.dsh-channels-cfg__input:focus{border-color:var(--dc-accent)}
|
|
3680
|
+
.dsh-channels-cfg__input--invalid{border-color:var(--dc-bad)}
|
|
3681
|
+
.dsh-channels-cfg__input--num{width:120px;text-align:right;font-variant-numeric:tabular-nums}
|
|
3682
|
+
.dsh-channels-cfg__input--text{width:260px}
|
|
3683
|
+
.dsh-channels-cfg__select{width:220px;appearance:auto;cursor:pointer}
|
|
3684
|
+
.dsh-channels-cfg__unit{min-width:36px;color:var(--dc-fg3);font-size:12px}
|
|
3685
|
+
.dsh-channels-cfg__bytes{color:var(--dc-fg3);font-size:12px;white-space:nowrap}
|
|
3686
|
+
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
3687
|
+
.dsh-channels-cfg__inputRow>.dsh-channels-cfg__input:not(.dsh-channels-cfg__input--num){flex:1 1 auto;width:auto}
|
|
3688
|
+
.dsh-channels-cfg__stringList{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3689
|
+
.dsh-channels-cfg__stringListItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3690
|
+
.dsh-channels-cfg__fieldError{color:var(--dc-bad);font-size:12px;line-height:18px}
|
|
3691
|
+
.dsh-channels-cfg__textBad{color:var(--dc-bad)}
|
|
3692
|
+
.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}
|
|
3693
|
+
.dsh-channels-cfg__btn:hover:not(:disabled){background:var(--dc-hover)}
|
|
3694
|
+
.dsh-channels-cfg__btn:disabled{color:var(--dc-dim);cursor:default}
|
|
3695
|
+
.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)}
|
|
3696
|
+
.dsh-channels-cfg__btn--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#0b5cba)}
|
|
3697
|
+
.dsh-channels-cfg__btn--primary:disabled{opacity:.45;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
3698
|
+
.dsh-channels-cfg__btn--ghost{border-color:transparent;color:var(--dc-fg2)}
|
|
3699
|
+
.dsh-channels-cfg__btn--danger{color:var(--dc-bad)}
|
|
3700
|
+
.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)}
|
|
3701
|
+
.dsh-channels-cfg__btn--remove:hover:not(:disabled){background:color-mix(in srgb,var(--dc-bad) 15%,transparent)}
|
|
3702
|
+
.dsh-channels-cfg__btn--remove:disabled{opacity:.5;color:var(--dc-bad)}
|
|
3703
|
+
.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}
|
|
3704
|
+
.dsh-channels-cfg__notice{padding:8px 12px;border-radius:8px;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
3705
|
+
.dsh-channels-cfg__notice--ok{color:var(--dc-ok);background:color-mix(in srgb,var(--dc-ok) 9%,transparent)}
|
|
3706
|
+
.dsh-channels-cfg__notice--error{color:var(--dc-bad);background:color-mix(in srgb,var(--dc-bad) 8%,transparent)}
|
|
3707
|
+
.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}
|
|
3708
|
+
.dsh-channels-cfg__bannerText{flex:1;min-width:0}
|
|
3709
|
+
.dsh-channels-cfg__hero{display:flex;flex-direction:column;gap:12px;padding:14px 16px;border:1px solid var(--dc-line);border-radius:10px}
|
|
3710
|
+
.dsh-channels-cfg__heroTop{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3711
|
+
.dsh-channels-cfg__heroTop strong{font-size:14px;font-weight:600}
|
|
3712
|
+
.dsh-channels-cfg__stats{display:flex;gap:28px;flex-wrap:wrap}
|
|
3713
|
+
.dsh-channels-cfg__stat{display:flex;flex-direction:column}
|
|
3714
|
+
.dsh-channels-cfg__stat span{color:var(--dc-fg3);font-size:12px}
|
|
3715
|
+
.dsh-channels-cfg__stat strong{font-size:18px;font-weight:600;line-height:26px;font-variant-numeric:tabular-nums}
|
|
3716
|
+
.dsh-channels-cfg__stat strong.dsh-channels-cfg__mono{font-size:14px}
|
|
3717
|
+
.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}
|
|
3718
|
+
.dsh-channels-cfg__checkIcon--ok{color:var(--dc-ok);background:color-mix(in srgb,var(--dc-ok) 12%,transparent)}
|
|
3719
|
+
.dsh-channels-cfg__checkIcon--warn{color:var(--dc-warn);background:color-mix(in srgb,var(--dc-warn) 14%,transparent)}
|
|
3720
|
+
.dsh-channels-cfg__checkIcon--bad{color:var(--dc-bad);background:color-mix(in srgb,var(--dc-bad) 12%,transparent)}
|
|
3721
|
+
.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)}
|
|
3722
|
+
.dsh-channels-cfg__scopeList li{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
|
|
3723
|
+
.dsh-channels-cfg__scopeList .dsh-channels-cfg__link{font-size:12px}
|
|
3724
|
+
.dsh-channels-cfg__list>.dsh-channels-cfg__scopeList{border-top-width:0}
|
|
3725
|
+
.dsh-channels-avatar{box-sizing:border-box;display:grid;place-items:center;flex:none;border-radius:50%;object-fit:cover;background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-secondary,#5c6169);line-height:1}
|
|
3726
|
+
.dsh-channels-cfg__addRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 14px}
|
|
3727
|
+
.dsh-channels-cfg__addRow .dsh-channels-cfg__input{flex:1 1 160px;width:auto}
|
|
3728
|
+
.dsh-channels-cfg__emptyRow{padding:11px 14px;color:var(--dc-fg3);font-size:12px}
|
|
3729
|
+
.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)}
|
|
3730
|
+
.dsh-channels-cfg__saveMsg{flex:1;min-width:0;color:var(--dc-fg2);font-size:12px}
|
|
3412
3731
|
.dsh-channels-cfg__steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
3413
3732
|
.dsh-channels-cfg__step{display:flex;align-items:flex-start;gap:10px}
|
|
3414
|
-
.dsh-channels-cfg__stepIndex{display:
|
|
3733
|
+
.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}
|
|
3415
3734
|
.dsh-channels-cfg__stepBody{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3416
|
-
.dsh-channels-cfg__stepTitle{font-
|
|
3417
|
-
.dsh-channels-
|
|
3418
|
-
.dsh-channels-
|
|
3419
|
-
.dsh-channels-
|
|
3420
|
-
.dsh-channels-
|
|
3421
|
-
.dsh-channels-
|
|
3422
|
-
.dsh-channels-
|
|
3423
|
-
.dsh-channels-
|
|
3424
|
-
.dsh-channels-
|
|
3425
|
-
.dsh-channels-
|
|
3426
|
-
.dsh-channels-
|
|
3427
|
-
.dsh-channels-
|
|
3428
|
-
.dsh-channels-
|
|
3429
|
-
.dsh-channels-
|
|
3430
|
-
.dsh-channels-
|
|
3431
|
-
.dsh-channels-
|
|
3432
|
-
.dsh-channels-
|
|
3433
|
-
.dsh-channels-
|
|
3434
|
-
.dsh-channels-
|
|
3435
|
-
.dsh-channels-
|
|
3735
|
+
.dsh-channels-cfg__stepTitle{font-weight:500}
|
|
3736
|
+
.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}
|
|
3737
|
+
.dsh-channels-cfg__loading{padding:16px 0;color:var(--dc-fg3);font-size:12px}
|
|
3738
|
+
.dsh-channels-origin-chip{display:inline-flex;align-items:center;gap:4px;max-width:180px;height:24px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#5c6169);font:inherit;font-size:12px;line-height:24px;cursor:pointer;white-space:nowrap}
|
|
3739
|
+
.dsh-channels-origin-chip:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-primary,#1f2328)}
|
|
3740
|
+
.dsh-channels-origin-chip>svg{flex:none}
|
|
3741
|
+
.dsh-channels-origin__who{display:inline-flex;align-items:center;gap:6px;vertical-align:middle}
|
|
3742
|
+
.dsh-channels-origin-chip__text{overflow:hidden;text-overflow:ellipsis}
|
|
3743
|
+
.dsh-channels-origin{box-sizing:border-box;height:100%;overflow:auto;padding:16px;color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:20px}
|
|
3744
|
+
.dsh-channels-origin__head{display:flex;align-items:center;gap:8px;margin-bottom:12px}
|
|
3745
|
+
.dsh-channels-origin__title{font-size:15px;font-weight:600;line-height:22px;overflow-wrap:anywhere}
|
|
3746
|
+
.dsh-channels-origin__list{display:flex;flex-direction:column;margin:0;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:10px}
|
|
3747
|
+
.dsh-channels-origin__row{display:flex;gap:12px;padding:9px 12px}
|
|
3748
|
+
.dsh-channels-origin__row+.dsh-channels-origin__row{border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12))}
|
|
3749
|
+
.dsh-channels-origin__row dt{flex:none;width:64px;color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3750
|
+
.dsh-channels-origin__row dd{flex:1;min-width:0;margin:0;overflow-wrap:anywhere}
|
|
3751
|
+
.dsh-channels-origin__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
3752
|
+
.dsh-channels-origin__muted{color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3753
|
+
.dsh-channels-origin__error{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-state-error-primary,#c62828);font-size:12px}
|
|
3754
|
+
.dsh-channels-origin__actions{display:flex;align-items:center;gap:14px;margin-top:14px}
|
|
3755
|
+
.dsh-channels-origin__button{display:inline-flex;align-items:center;height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:7px;color:inherit;font-size:13px;text-decoration:none}
|
|
3756
|
+
.dsh-channels-origin__button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
|
|
3757
|
+
.dsh-channels-origin__link{padding:0;border:0;background:none;color:var(--dsw-alias-state-business-primary,#0a67d3);font:inherit;font-size:13px;cursor:pointer}
|
|
3758
|
+
@media (prefers-reduced-motion:reduce){.dsh-channels-cfg__switch,.dsh-channels-cfg__switch::after{transition:none}}
|
|
3436
3759
|
@container (max-width:560px){
|
|
3437
|
-
.dsh-channels-
|
|
3438
|
-
.dsh-channels-
|
|
3439
|
-
.dsh-channels-
|
|
3760
|
+
.dsh-channels-cfg__row:not(.dsh-channels-cfg__row--wide){flex-wrap:wrap}
|
|
3761
|
+
.dsh-channels-cfg__rowCtl{max-width:100%}
|
|
3762
|
+
.dsh-channels-cfg__select,.dsh-channels-cfg__input--text{width:100%}
|
|
3440
3763
|
}
|
|
3441
3764
|
`;
|
|
3442
3765
|
/** Append the panel stylesheet once (idempotent across HMR / remounts). */
|
|
@@ -3453,11 +3776,13 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
|
3453
3776
|
//#endregion
|
|
3454
3777
|
//#region src/client/index.ts
|
|
3455
3778
|
/**
|
|
3456
|
-
* dsh-channels client half:
|
|
3779
|
+
* dsh-channels client half: 左侧栏全局入口「飞书渠道」+ 主区整页设置面板,
|
|
3780
|
+
* 以及对话内的「飞书来源」(会话头部入口 + 右侧栏标签页)。
|
|
3457
3781
|
*
|
|
3458
3782
|
* 只依赖平台核心的 `slots` 服务与 react(bundle 里 react 走平台模块表,
|
|
3459
|
-
*
|
|
3460
|
-
*
|
|
3783
|
+
* 其余全部内联);右侧栏服务存在时才注册来源标签页。页面数据来自 Host
|
|
3784
|
+
* 半侧的 `/dsh-channels/*` 路由,因此不需要 typert 契约,也不 import 任何
|
|
3785
|
+
* DSH 客户端包。
|
|
3461
3786
|
*
|
|
3462
3787
|
* @module dsh-channels/client
|
|
3463
3788
|
*/
|
|
@@ -3465,28 +3790,67 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
|
3465
3790
|
const name = "dsh-channels";
|
|
3466
3791
|
/** Required client services: the Slots registry is part of the web shell core. */
|
|
3467
3792
|
const inject = ["slots"];
|
|
3468
|
-
/** Section id and nav order (built-ins: account -10, general 0, models 10, plugins 15, agent-presets 20). */
|
|
3469
|
-
const SETTINGS_SECTION_ID = "feishu-channels";
|
|
3470
|
-
const SETTINGS_SECTION_ORDER = 40;
|
|
3471
3793
|
/**
|
|
3472
|
-
*
|
|
3794
|
+
* Main panel id: the `sidebar.panellist` row id and the matching `main` slot key.
|
|
3795
|
+
* Must not change once released: DSH keys navigation state by it.
|
|
3796
|
+
*/
|
|
3797
|
+
const PANEL_ID = "feishu-channels";
|
|
3798
|
+
/** Left-sidebar row order (built-ins: tasks 10; Plugins is pinned last by the sidebar). */
|
|
3799
|
+
const PANEL_ORDER = 40;
|
|
3800
|
+
/** Session header action order (after the preset label -10 and subagents 10). */
|
|
3801
|
+
const ORIGIN_ACTION_ORDER = 30;
|
|
3802
|
+
/**
|
|
3803
|
+
* Mount the left-sidebar entry, its main-column page, the stylesheet, and —
|
|
3804
|
+
* while the right sidebar is present — the Feishu origin header entry and tab.
|
|
3473
3805
|
* @param ctx - client plugin context carrying the slots registry.
|
|
3474
3806
|
*/
|
|
3475
3807
|
function apply(ctx) {
|
|
3476
|
-
if (typeof ctx.effect === "function") ctx.effect(() => installStyles(), "dsh-channels:
|
|
3808
|
+
if (typeof ctx.effect === "function") ctx.effect(() => installStyles(), "dsh-channels: panel styles");
|
|
3477
3809
|
else installStyles();
|
|
3478
|
-
ctx.slots.inject("
|
|
3479
|
-
name: "
|
|
3480
|
-
|
|
3810
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
3811
|
+
name: "main",
|
|
3812
|
+
key: PANEL_ID
|
|
3813
|
+
}, FeishuChannelsPage));
|
|
3814
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
3815
|
+
name: "sidebar.panellist",
|
|
3816
|
+
id: PANEL_ID,
|
|
3481
3817
|
order: 40,
|
|
3482
3818
|
label: () => PANEL_COPY.title
|
|
3483
|
-
},
|
|
3819
|
+
}, FeishuChannelsIcon));
|
|
3820
|
+
ctx.inject?.(["sidebarRightTabs", "sidebarRight"], mountOrigin);
|
|
3821
|
+
}
|
|
3822
|
+
/**
|
|
3823
|
+
* Register the origin tab type, its body and title, and the header entry that
|
|
3824
|
+
* opens it. Everything lives as long as the right-sidebar services do.
|
|
3825
|
+
* @param scope - child context holding the right-sidebar services.
|
|
3826
|
+
*/
|
|
3827
|
+
function mountOrigin(scope) {
|
|
3828
|
+
const definition = {
|
|
3829
|
+
id: ORIGIN_TAB_ID,
|
|
3830
|
+
kind: ORIGIN_TAB_KIND,
|
|
3831
|
+
title: () => ORIGIN_TAB_TITLE
|
|
3832
|
+
};
|
|
3833
|
+
scope.effect?.(() => scope.sidebarRightTabs.register(definition), "dsh-channels: origin tab type");
|
|
3834
|
+
scope.slots.inject("sidebar.right.pane.tab", () => scope.slots.register({
|
|
3835
|
+
name: "sidebar.right.pane.tab",
|
|
3836
|
+
key: ORIGIN_TAB_ID
|
|
3837
|
+
}, OriginTab));
|
|
3838
|
+
scope.slots.inject("conversation.session.header.actions", () => scope.slots.register({
|
|
3839
|
+
name: "conversation.session.header.actions",
|
|
3840
|
+
id: "dsh-channels-origin",
|
|
3841
|
+
order: 30,
|
|
3842
|
+
inject: () => ({ openOrigin: () => {
|
|
3843
|
+
scope.sidebarRight.openTab(ORIGIN_TAB_KIND);
|
|
3844
|
+
} })
|
|
3845
|
+
}, OriginHeaderAction));
|
|
3484
3846
|
}
|
|
3485
3847
|
//#endregion
|
|
3486
|
-
exports.
|
|
3487
|
-
exports.
|
|
3848
|
+
exports.ORIGIN_ACTION_ORDER = ORIGIN_ACTION_ORDER;
|
|
3849
|
+
exports.PANEL_ID = PANEL_ID;
|
|
3850
|
+
exports.PANEL_ORDER = PANEL_ORDER;
|
|
3488
3851
|
exports.apply = apply;
|
|
3489
3852
|
exports.inject = inject;
|
|
3853
|
+
exports.mountOrigin = mountOrigin;
|
|
3490
3854
|
exports.name = name;
|
|
3491
3855
|
return module.exports;
|
|
3492
3856
|
}
|