@modesto_ma/dsh-channels 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -2
- package/cordis.patch.yml +3 -0
- package/docs/access-control-design.md +550 -0
- package/docs/assets/web-settings-panel.png +0 -0
- package/docs/assets/web-settings-restart-pending.png +0 -0
- package/docs/feasibility-and-design.md +213 -0
- package/docs/feishu-onboarding.md +268 -0
- package/docs/persistence.md +199 -0
- package/docs/refactor-plan.md +56 -0
- package/docs/review-dsh-0.2.0.md +389 -0
- package/docs/slash-commands-implementation-plan.md +410 -0
- package/docs/web-display-only-surface-roadmap.md +363 -0
- package/lib/client.js +2226 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +10217 -0
- package/lib/index.js.map +1 -0
- package/package.json +98 -4
- package/src/access/cards.ts +188 -0
- package/src/access/controller.ts +201 -0
- package/src/access/index.ts +13 -0
- package/src/access/policy.ts +103 -0
- package/src/access/requests.ts +512 -0
- package/src/access/store.ts +220 -0
- package/src/approvals.ts +8 -0
- package/src/cards/approval.ts +114 -0
- package/src/cards/index.ts +11 -0
- package/src/cards/primitives.ts +82 -0
- package/src/cards/question.ts +141 -0
- package/src/cards/streaming.ts +563 -0
- package/src/cards.ts +8 -0
- package/src/cleanup.ts +398 -0
- package/src/client/api.ts +262 -0
- package/src/client/catalog.ts +92 -0
- package/src/client/index.ts +61 -0
- package/src/client/panel.tsx +1475 -0
- package/src/client/styles.ts +98 -0
- package/src/commands/registry.ts +155 -0
- package/src/commands.ts +335 -0
- package/src/config.ts +206 -0
- package/src/domain.ts +428 -0
- package/src/dsh-types.ts +11 -0
- package/src/feishu/agent-context.ts +62 -0
- package/src/feishu/delivery.ts +188 -0
- package/src/feishu/inbound-attachments.ts +436 -0
- package/src/feishu-channel.ts +339 -0
- package/src/health.ts +233 -0
- package/src/host/agent.ts +181 -0
- package/src/host/commands.ts +63 -0
- package/src/host/core.ts +345 -0
- package/src/host/index.ts +12 -0
- package/src/host/interaction.ts +101 -0
- package/src/host/services.ts +320 -0
- package/src/index.ts +122 -0
- package/src/interactions/approvals.ts +154 -0
- package/src/interactions/pending.ts +162 -0
- package/src/interactions/questions.ts +274 -0
- package/src/questions.ts +8 -0
- package/src/routing/inbound.ts +524 -0
- package/src/service.ts +2138 -0
- package/src/session/manager.ts +352 -0
- package/src/session/repair.ts +157 -0
- package/src/session/request-policy.ts +192 -0
- package/src/session/routes.ts +165 -0
- package/src/session-commands.ts +310 -0
- package/src/session-export.ts +112 -0
- package/src/settings/catalog.ts +309 -0
- package/src/settings/contract.ts +508 -0
- package/src/settings/overlay.ts +164 -0
- package/src/settings/routes.ts +409 -0
- package/src/settings/surface.ts +773 -0
- package/src/stats.ts +485 -0
- package/src/streamer.ts +1108 -0
- package/src/workspaces.ts +199 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2226 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@modesto_ma/dsh-channels",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/settings/contract.ts
|
|
10
|
+
/**
|
|
11
|
+
* Web 设置面板的线上契约:Host 半侧的路由/面板数据与 Browser 半侧的渲染共用。
|
|
12
|
+
*
|
|
13
|
+
* 本模块**没有运行时依赖**:Host 与 Client bundle 都直接内联它。因此这里只放
|
|
14
|
+
* 常量、字段清单与可序列化的 wire 类型,不 import zod / node / DSH 内部包。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-channels/settings/contract
|
|
17
|
+
*/
|
|
18
|
+
/** Same-origin routes the settings panel talks to. */
|
|
19
|
+
const STATUS_ROUTE = "/dsh-channels/status";
|
|
20
|
+
const PANEL_ROUTE = "/dsh-channels/panel";
|
|
21
|
+
const CONFIG_ROUTE = "/dsh-channels/config";
|
|
22
|
+
const CREDENTIALS_ROUTE = "/dsh-channels/credentials";
|
|
23
|
+
const RECONNECT_ROUTE = "/dsh-channels/reconnect";
|
|
24
|
+
/** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
|
|
25
|
+
const CATALOG_ROUTE = "/dsh-channels/catalog";
|
|
26
|
+
/** 手动执行一次过期工作区清理(POST,无请求体)。 */
|
|
27
|
+
const CLEANUP_ROUTE = "/dsh-channels/cleanup";
|
|
28
|
+
/** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
|
|
29
|
+
const ACCESS_GROUPS_ROUTE = "/dsh-channels/access/groups";
|
|
30
|
+
/** 用户白名单增删(POST/DELETE,body `{ openId, name? }` / `{ openId }`)。 */
|
|
31
|
+
const ACCESS_USERS_ROUTE = "/dsh-channels/access/users";
|
|
32
|
+
/** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
|
|
33
|
+
const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
|
|
34
|
+
/** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
|
|
35
|
+
const PANEL_COPY = {
|
|
36
|
+
title: "飞书渠道",
|
|
37
|
+
description: "飞书长连接状态、应用凭据与常用配置。凭据写入 DSH 凭据库且不回显;标注「需重连」的项需重启飞书连接生效。",
|
|
38
|
+
statusCard: "连接状态",
|
|
39
|
+
credentialsCard: "飞书应用凭据",
|
|
40
|
+
modelCard: "模型与推理",
|
|
41
|
+
modelCardHelp: "模型服务商、模型 id、推理档位与新会话的 Agent 预设。下拉选项来自宿主机实时目录;保存后对新会话生效。",
|
|
42
|
+
configCard: "常用配置",
|
|
43
|
+
advancedCard: "只读与高级信息",
|
|
44
|
+
appIdLabel: "飞书 App ID",
|
|
45
|
+
appSecretLabel: "飞书 App Secret",
|
|
46
|
+
credentialsHelp: "值通过 DSH 凭据服务写入本机凭据库;面板只显示是否已配置,永不回显已存值。留空表示保持当前值。",
|
|
47
|
+
credentialsRef: "凭据引用",
|
|
48
|
+
saveCredentials: "保存凭据",
|
|
49
|
+
savingCredentials: "保存中…",
|
|
50
|
+
credentialSaved: "凭据已保存",
|
|
51
|
+
credentialSavedReconnected: "凭据已保存,已自动重连飞书通道",
|
|
52
|
+
credentialSavedPending: "凭据已保存,重启飞书连接后生效",
|
|
53
|
+
credentialRemoved: "已移除存储的凭据",
|
|
54
|
+
credentialRemovedPending: "已移除存储的凭据,重启飞书连接后生效",
|
|
55
|
+
removeCredential: "移除已存值",
|
|
56
|
+
removingCredential: "移除中…",
|
|
57
|
+
credentialConfigured: "已配置",
|
|
58
|
+
credentialMissing: "未配置",
|
|
59
|
+
credentialReadOnly: "当前凭据存储不可写",
|
|
60
|
+
appIdPlaceholder: "输入 App ID(cli_…)",
|
|
61
|
+
appSecretPlaceholder: "输入 App Secret",
|
|
62
|
+
save: "保存配置",
|
|
63
|
+
saveAll: "保存全部配置",
|
|
64
|
+
saveModel: "保存模型配置",
|
|
65
|
+
modelActionsHint: "只保存模型与推理字段",
|
|
66
|
+
saving: "保存中…",
|
|
67
|
+
clear: "清除 Web 覆盖",
|
|
68
|
+
clearing: "清除中…",
|
|
69
|
+
reconnect: "重启飞书连接",
|
|
70
|
+
reconnecting: "重连中…",
|
|
71
|
+
refresh: "刷新状态",
|
|
72
|
+
retry: "重试",
|
|
73
|
+
loading: "加载中…",
|
|
74
|
+
loadFailed: "设置面板加载失败",
|
|
75
|
+
saved: "已保存",
|
|
76
|
+
savedModel: "模型配置已保存",
|
|
77
|
+
cleared: "已清除 Web 覆盖,恢复为 profile 配置",
|
|
78
|
+
catalogFailed: "模型目录加载失败,已退回手填。",
|
|
79
|
+
catalogLoading: "正在读取宿主机选项…",
|
|
80
|
+
catalogUnavailable: "宿主机未提供可选项,请手填。",
|
|
81
|
+
noOverrides: "当前没有 Web 覆盖",
|
|
82
|
+
overridden: "Web 覆盖",
|
|
83
|
+
inherited: "继承 profile",
|
|
84
|
+
restartTag: "需重连",
|
|
85
|
+
restartBanner: "以下配置需重启飞书连接后生效:",
|
|
86
|
+
restartWarning: "重启会断开当前飞书长连接并中断进行中的回复,请确认空闲时操作。",
|
|
87
|
+
lastError: "最近一次启动失败",
|
|
88
|
+
notRunning: "未运行",
|
|
89
|
+
version: "插件版本",
|
|
90
|
+
overlayPath: "覆盖文件",
|
|
91
|
+
cleanupCard: "工作区清理",
|
|
92
|
+
cleanupCardHelp: "删除超过保留期且不再活跃的飞书工作区目录;只会删除本插件在「工作区根目录」下创建的目录,DSH 会话日志不删除。删除不可恢复:相关旧话题会失效并提示已过期,工作区列表记录同步移除。",
|
|
93
|
+
cleanupRetention: "保留期",
|
|
94
|
+
cleanupAutoLabel: "自动清理",
|
|
95
|
+
cleanupCandidates: "可清理工作区",
|
|
96
|
+
cleanupCandidateBytes: "预计释放空间",
|
|
97
|
+
cleanupRetentionOff: "关闭(不删除)",
|
|
98
|
+
cleanupAutoOn: "已开启(启动时 + 每天一次)",
|
|
99
|
+
cleanupAutoOff: "已关闭(仅手动执行)",
|
|
100
|
+
cleanupAutoInactive: "未执行(保留期为关闭)",
|
|
101
|
+
cleanupNow: "立即清理",
|
|
102
|
+
cleanupRunning: "清理中…",
|
|
103
|
+
cleanupConfirm: "确定清理过期工作区吗?该操作不可恢复,会删除这些工作区内的全部文件(包括 attachments/ 附件)。",
|
|
104
|
+
cleanupConfirmHint: "删除前会再次确认;保留期为「关闭」或没有过期工作区时按钮不可用。",
|
|
105
|
+
cleanupDisabledOff: "保留策略为「关闭」:请先在「常用配置 → 工作区与文件」中选择保留天数。",
|
|
106
|
+
cleanupDisabledEmpty: "没有超过保留期的过期工作区。",
|
|
107
|
+
cleanupPreviewFailed: "无法读取清理预览",
|
|
108
|
+
cleanupDone: "清理完成",
|
|
109
|
+
cleanupFailed: "清理失败",
|
|
110
|
+
cleanupDeleted: "已删除工作区",
|
|
111
|
+
cleanupRetired: "已清理话题路由",
|
|
112
|
+
cleanupFreed: "释放空间",
|
|
113
|
+
cleanupFailures: "以下工作区未能删除:",
|
|
114
|
+
accessCard: "访问与授权",
|
|
115
|
+
accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员 open_id 在「常用配置 → 行为」中填写,私聊机器人发送 /userid 获取。",
|
|
116
|
+
accessModeOpenWarning: "当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。",
|
|
117
|
+
accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,所有对话都会被拒绝。请先在「常用配置 → 行为 → 管理员 open_id」填写。",
|
|
118
|
+
accessAdminLabel: "管理员",
|
|
119
|
+
accessAdminMissing: "未配置",
|
|
120
|
+
accessAdminHint: "修改管理员请到「常用配置 → 行为 → 管理员 open_id」;私聊机器人发送 /userid 可查看自己的 open_id(ou_ 开头,不是 user_id/邮箱)。",
|
|
121
|
+
accessGroupsTitle: "已授权群",
|
|
122
|
+
accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
|
|
123
|
+
accessUsersTitle: "已授权用户",
|
|
124
|
+
accessUsersEmpty: "尚未授权任何用户。已授权用户可在所有已授权群里与机器人对话(私聊仍只开给管理员)。",
|
|
125
|
+
accessRequestsTitle: "待处理申请",
|
|
126
|
+
accessRequestsEmpty: "没有待处理的授权申请。",
|
|
127
|
+
accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
|
|
128
|
+
accessGroupNamePlaceholder: "群名(可选)",
|
|
129
|
+
accessUserOpenIdPlaceholder: "用户 open_id(ou_…)",
|
|
130
|
+
accessUserNamePlaceholder: "昵称(可选)",
|
|
131
|
+
accessAddGroup: "添加群",
|
|
132
|
+
accessAddUser: "添加用户",
|
|
133
|
+
accessRemove: "移除",
|
|
134
|
+
accessAllow: "授权",
|
|
135
|
+
accessDeny: "忽略",
|
|
136
|
+
accessWorking: "处理中…",
|
|
137
|
+
accessSourceCommand: "对话命令",
|
|
138
|
+
accessSourceCard: "授权卡",
|
|
139
|
+
accessSourcePanel: "设置面板",
|
|
140
|
+
accessRequestGroup: "群申请",
|
|
141
|
+
accessRequestUser: "用户申请",
|
|
142
|
+
accessRequestRequester: "申请人",
|
|
143
|
+
accessRequestExpires: "有效期至",
|
|
144
|
+
accessGroupAdded: "已添加群授权",
|
|
145
|
+
accessGroupRemoved: "已移除群授权",
|
|
146
|
+
accessUserAdded: "已添加用户授权",
|
|
147
|
+
accessUserRemoved: "已移除用户授权",
|
|
148
|
+
accessRequestAllowed: "已授权",
|
|
149
|
+
accessRequestDenied: "已忽略",
|
|
150
|
+
accessHints: "三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。",
|
|
151
|
+
accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;open_id 可私聊机器人发送 /userid 获取。"
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/client/api.ts
|
|
155
|
+
/**
|
|
156
|
+
* Same-origin HTTP client for the settings panel. No typert contract: the
|
|
157
|
+
* Host half registers plain exact routes and this module fetches them.
|
|
158
|
+
*
|
|
159
|
+
* @module dsh-channels/client/api
|
|
160
|
+
*/
|
|
161
|
+
function messageOf$1(error) {
|
|
162
|
+
return error instanceof Error ? error.message : String(error);
|
|
163
|
+
}
|
|
164
|
+
/** Pull the JSON `error` field out of a failed response body. */
|
|
165
|
+
function errorFromBody(status, text) {
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(text);
|
|
168
|
+
if (typeof parsed.error === "string" && parsed.error !== "") return parsed.error;
|
|
169
|
+
} catch {}
|
|
170
|
+
return `HTTP ${status}`;
|
|
171
|
+
}
|
|
172
|
+
async function request(path, init) {
|
|
173
|
+
try {
|
|
174
|
+
return await fetch(path, {
|
|
175
|
+
cache: "no-store",
|
|
176
|
+
credentials: "same-origin",
|
|
177
|
+
...init,
|
|
178
|
+
headers: {
|
|
179
|
+
accept: "application/json",
|
|
180
|
+
...init?.headers ?? {}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new Error(`无法连接设置接口:${messageOf$1(error)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function readJson(response) {
|
|
188
|
+
const text = await response.text();
|
|
189
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
190
|
+
return JSON.parse(text);
|
|
191
|
+
}
|
|
192
|
+
/** Full panel payload (status + editable config + read-only facts). */
|
|
193
|
+
async function fetchPanel() {
|
|
194
|
+
return readJson(await request(PANEL_ROUTE));
|
|
195
|
+
}
|
|
196
|
+
/** Status-only payload polled while the page is open. */
|
|
197
|
+
async function fetchStatus() {
|
|
198
|
+
return readJson(await request(STATUS_ROUTE));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Model-selector catalog for one route: providers, the provider's models, the
|
|
202
|
+
* model's reasoning efforts, and the preset roster. The client re-queries it
|
|
203
|
+
* whenever the draft provider/model changes.
|
|
204
|
+
*/
|
|
205
|
+
async function fetchCatalog(provider, model) {
|
|
206
|
+
const params = new URLSearchParams();
|
|
207
|
+
if (provider !== void 0 && provider !== "") params.set("provider", provider);
|
|
208
|
+
if (model !== void 0 && model !== "") params.set("model", model);
|
|
209
|
+
const query = params.toString();
|
|
210
|
+
return readJson(await request(query === "" ? CATALOG_ROUTE : `${CATALOG_ROUTE}?${query}`));
|
|
211
|
+
}
|
|
212
|
+
/** Persist a partial edit. Validation failures come back as `{ok:false}`. */
|
|
213
|
+
async function saveConfig(values) {
|
|
214
|
+
const response = await request(CONFIG_ROUTE, {
|
|
215
|
+
method: "PUT",
|
|
216
|
+
headers: { "content-type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ values })
|
|
218
|
+
});
|
|
219
|
+
const text = await response.text();
|
|
220
|
+
if (response.status === 400) try {
|
|
221
|
+
return JSON.parse(text);
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
error: errorFromBody(response.status, text)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
229
|
+
return JSON.parse(text);
|
|
230
|
+
}
|
|
231
|
+
/** Drop every Web override and return the refreshed panel. */
|
|
232
|
+
async function clearConfig() {
|
|
233
|
+
const response = await request(CONFIG_ROUTE, { method: "DELETE" });
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
if (response.status === 400) try {
|
|
236
|
+
return JSON.parse(text);
|
|
237
|
+
} catch {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: errorFromBody(response.status, text)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
244
|
+
return JSON.parse(text);
|
|
245
|
+
}
|
|
246
|
+
/** Reopen the Feishu long connection (interrupts in-flight replies). */
|
|
247
|
+
async function reconnect() {
|
|
248
|
+
const response = await request(RECONNECT_ROUTE, { method: "POST" });
|
|
249
|
+
const text = await response.text();
|
|
250
|
+
if (!response.ok) try {
|
|
251
|
+
return JSON.parse(text);
|
|
252
|
+
} catch {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: errorFromBody(response.status, text)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return JSON.parse(text);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Run one workspace cleanup. No request body: the Host derives the candidate
|
|
262
|
+
* set from the effective retention config. Irreversible, so the caller asks
|
|
263
|
+
* for confirmation first; partial failures come back inside `result.failures`.
|
|
264
|
+
*/
|
|
265
|
+
async function runCleanup() {
|
|
266
|
+
const response = await request(CLEANUP_ROUTE, { method: "POST" });
|
|
267
|
+
const text = await response.text();
|
|
268
|
+
if (!response.ok) try {
|
|
269
|
+
return JSON.parse(text);
|
|
270
|
+
} catch {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: errorFromBody(response.status, text)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return JSON.parse(text);
|
|
277
|
+
}
|
|
278
|
+
/** Store provided credential values (the request/response never carry stored values). */
|
|
279
|
+
async function saveCredentials(values) {
|
|
280
|
+
const response = await request(CREDENTIALS_ROUTE, {
|
|
281
|
+
method: "PUT",
|
|
282
|
+
headers: { "content-type": "application/json" },
|
|
283
|
+
body: JSON.stringify(values)
|
|
284
|
+
});
|
|
285
|
+
const text = await response.text();
|
|
286
|
+
if (response.status === 400) try {
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: errorFromBody(response.status, text)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
295
|
+
return JSON.parse(text);
|
|
296
|
+
}
|
|
297
|
+
/** Remove the stored value behind one credential reference. */
|
|
298
|
+
async function clearCredential(field) {
|
|
299
|
+
const response = await request(CREDENTIALS_ROUTE, {
|
|
300
|
+
method: "DELETE",
|
|
301
|
+
headers: { "content-type": "application/json" },
|
|
302
|
+
body: JSON.stringify({ field })
|
|
303
|
+
});
|
|
304
|
+
const text = await response.text();
|
|
305
|
+
if (response.status === 400) try {
|
|
306
|
+
return JSON.parse(text);
|
|
307
|
+
} catch {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
error: errorFromBody(response.status, text)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
314
|
+
return JSON.parse(text);
|
|
315
|
+
}
|
|
316
|
+
/** One access write: a 400 body already is the wire union; others throw. */
|
|
317
|
+
async function mutateAccess(path, method, body) {
|
|
318
|
+
const response = await request(path, {
|
|
319
|
+
method,
|
|
320
|
+
headers: { "content-type": "application/json" },
|
|
321
|
+
body: JSON.stringify(body)
|
|
322
|
+
});
|
|
323
|
+
const text = await response.text();
|
|
324
|
+
if (response.status === 400) try {
|
|
325
|
+
return JSON.parse(text);
|
|
326
|
+
} catch {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
error: errorFromBody(response.status, text)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (!response.ok) throw new Error(errorFromBody(response.status, text));
|
|
333
|
+
return JSON.parse(text);
|
|
334
|
+
}
|
|
335
|
+
/** Authorize one group; resolves with the refreshed access snapshot. */
|
|
336
|
+
async function addAccessGroup(chatId, name) {
|
|
337
|
+
return mutateAccess(ACCESS_GROUPS_ROUTE, "POST", name === void 0 || name === "" ? { chatId } : {
|
|
338
|
+
chatId,
|
|
339
|
+
name
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** Revoke one group authorization (takes effect on the next message). */
|
|
343
|
+
async function removeAccessGroup(chatId) {
|
|
344
|
+
return mutateAccess(ACCESS_GROUPS_ROUTE, "DELETE", { chatId });
|
|
345
|
+
}
|
|
346
|
+
/** Authorize one user open_id (global across authorized groups). */
|
|
347
|
+
async function addAccessUser(openId, name) {
|
|
348
|
+
return mutateAccess(ACCESS_USERS_ROUTE, "POST", name === void 0 || name === "" ? { openId } : {
|
|
349
|
+
openId,
|
|
350
|
+
name
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/** Revoke one user authorization (takes effect on the next message). */
|
|
354
|
+
async function removeAccessUser(openId) {
|
|
355
|
+
return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", { openId });
|
|
356
|
+
}
|
|
357
|
+
/** Settle one pending authorization request (same resolver as the Feishu card). */
|
|
358
|
+
async function resolveAccessRequest(token, decision) {
|
|
359
|
+
return mutateAccess(ACCESS_REQUESTS_ROUTE, "POST", {
|
|
360
|
+
token,
|
|
361
|
+
decision
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/client/catalog.ts
|
|
366
|
+
/**
|
|
367
|
+
* 浏览器半区的设置面板选择器目录折叠:把 Host 下发的字段描述与实时目录
|
|
368
|
+
* (`/dsh-channels/catalog`)合成通用表单需要的 select 描述。
|
|
369
|
+
*
|
|
370
|
+
* 拆成独立模块(不含 JSX/React)以便单测;`panel.tsx` 只负责串起状态与渲染。
|
|
371
|
+
*
|
|
372
|
+
* @module dsh-channels/client/catalog
|
|
373
|
+
*/
|
|
374
|
+
/** Keys whose options come from the Host selector catalog. */
|
|
375
|
+
const CATALOG_KEYS = [
|
|
376
|
+
"provider",
|
|
377
|
+
"model",
|
|
378
|
+
"reasoningEffort",
|
|
379
|
+
"agentPreset"
|
|
380
|
+
];
|
|
381
|
+
function isCatalogKey(key) {
|
|
382
|
+
return CATALOG_KEYS.includes(key);
|
|
383
|
+
}
|
|
384
|
+
/** Read one selector group back out of the Host-resolved panel fields. */
|
|
385
|
+
function selectorOf(field) {
|
|
386
|
+
if (field?.kind !== "select") return {
|
|
387
|
+
selectable: false,
|
|
388
|
+
options: []
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
selectable: true,
|
|
392
|
+
options: field.options === void 0 ? [] : [...field.options],
|
|
393
|
+
...field.disabled === true ? { disabled: true } : {},
|
|
394
|
+
...field.help === void 0 ? {} : { help: field.help }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** Seed the client catalog from the options the Host already resolved for the panel. */
|
|
398
|
+
function catalogFromFields(fields) {
|
|
399
|
+
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
400
|
+
const catalog = {
|
|
401
|
+
provider: selectorOf(byKey.get("provider")),
|
|
402
|
+
model: selectorOf(byKey.get("model")),
|
|
403
|
+
reasoningEffort: selectorOf(byKey.get("reasoningEffort")),
|
|
404
|
+
agentPreset: selectorOf(byKey.get("agentPreset"))
|
|
405
|
+
};
|
|
406
|
+
return CATALOG_KEYS.every((key) => !catalog[key].selectable) ? null : catalog;
|
|
407
|
+
}
|
|
408
|
+
/** Current-value option kept when a saved value is missing from the live list. */
|
|
409
|
+
function withCurrentOption(options, value) {
|
|
410
|
+
const [inherit, ...rest] = options;
|
|
411
|
+
const extra = {
|
|
412
|
+
value,
|
|
413
|
+
label: `${value}(当前配置,不在宿主机列表中)`
|
|
414
|
+
};
|
|
415
|
+
return inherit === void 0 ? [extra] : [
|
|
416
|
+
inherit,
|
|
417
|
+
extra,
|
|
418
|
+
...rest
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Layer the live catalog over the Host panel fields. A group the Host cannot
|
|
423
|
+
* enumerate falls back to the free-text control; the rest render as selects.
|
|
424
|
+
*
|
|
425
|
+
* `disabled` must be cleared explicitly: the Host's initial panel descriptor
|
|
426
|
+
* carries `disabled: true` for dependent selectors (model without a provider,
|
|
427
|
+
* effort without a model), and a fetched catalog that resolved the dependency
|
|
428
|
+
* has to re-enable them. Forgetting this is exactly what made the model and
|
|
429
|
+
* reasoning selects permanently unusable after choosing a provider/model.
|
|
430
|
+
*/
|
|
431
|
+
function applyCatalogFields(fields, catalog, current = {}) {
|
|
432
|
+
if (catalog === null) return [...fields];
|
|
433
|
+
return fields.map((field) => {
|
|
434
|
+
if (!isCatalogKey(field.key)) return field;
|
|
435
|
+
const selector = catalog[field.key];
|
|
436
|
+
if (!selector.selectable) return field.kind === "select" ? {
|
|
437
|
+
...field,
|
|
438
|
+
kind: "text",
|
|
439
|
+
options: void 0,
|
|
440
|
+
help: PANEL_COPY.catalogUnavailable,
|
|
441
|
+
disabled: void 0
|
|
442
|
+
} : field;
|
|
443
|
+
const value = current[field.key] ?? "";
|
|
444
|
+
const options = value !== "" && !selector.options.some((option) => option.value === value) ? withCurrentOption(selector.options, value) : [...selector.options];
|
|
445
|
+
const next = {
|
|
446
|
+
...field,
|
|
447
|
+
kind: "select",
|
|
448
|
+
options
|
|
449
|
+
};
|
|
450
|
+
if (selector.disabled === true) next.disabled = true;
|
|
451
|
+
else delete next.disabled;
|
|
452
|
+
if (selector.help === void 0) delete next.help;
|
|
453
|
+
else next.help = selector.help;
|
|
454
|
+
return next;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/client/panel.tsx
|
|
459
|
+
/**
|
|
460
|
+
* 设置 →「飞书渠道」页面。
|
|
461
|
+
*
|
|
462
|
+
* 数据全部来自 Host 的 `/dsh-channels/*` 路由:状态卡每 5 秒轮询一次,
|
|
463
|
+
* 常用配置表单由 Host 下发的字段描述(`fields`)通用渲染,Host 是唯一的
|
|
464
|
+
* 校验与持久化方。组件不 import 任何 DSH 客户端包,只依赖 react 与平台
|
|
465
|
+
* 的 `settings.section` 槽位。
|
|
466
|
+
*
|
|
467
|
+
* @module dsh-channels/client/panel
|
|
468
|
+
*/
|
|
469
|
+
const GROUP_ORDER = [
|
|
470
|
+
"workspace",
|
|
471
|
+
"behavior",
|
|
472
|
+
"content",
|
|
473
|
+
"model",
|
|
474
|
+
"streaming"
|
|
475
|
+
];
|
|
476
|
+
const GROUP_LABELS = {
|
|
477
|
+
workspace: "工作区与文件",
|
|
478
|
+
behavior: "行为",
|
|
479
|
+
content: "内容与附件",
|
|
480
|
+
model: "模型",
|
|
481
|
+
streaming: "流式卡片"
|
|
482
|
+
};
|
|
483
|
+
function messageOf(error) {
|
|
484
|
+
return error instanceof Error ? error.message : String(error);
|
|
485
|
+
}
|
|
486
|
+
/** Human-readable byte size for the cleanup preview/result (base-2 units). */
|
|
487
|
+
function formatBytes(bytes) {
|
|
488
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
489
|
+
const units = [
|
|
490
|
+
"B",
|
|
491
|
+
"KB",
|
|
492
|
+
"MB",
|
|
493
|
+
"GB",
|
|
494
|
+
"TB"
|
|
495
|
+
];
|
|
496
|
+
let value = bytes;
|
|
497
|
+
let unit = 0;
|
|
498
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
499
|
+
value /= 1024;
|
|
500
|
+
unit += 1;
|
|
501
|
+
}
|
|
502
|
+
const digits = unit === 0 || value >= 10 ? 0 : 1;
|
|
503
|
+
return `${value.toFixed(digits)} ${units[unit] ?? "B"}`;
|
|
504
|
+
}
|
|
505
|
+
/** Display label for the effective retention value, reusing the Host field options. */
|
|
506
|
+
function retentionLabel(panel, cleanup) {
|
|
507
|
+
return panel.config.fields.find((item) => item.key === "workspaceRetention")?.options?.find((option) => option.value === cleanup.retention)?.label ?? (cleanup.retention === "off" ? PANEL_COPY.cleanupRetentionOff : cleanup.retention);
|
|
508
|
+
}
|
|
509
|
+
function formatUptime(ms) {
|
|
510
|
+
if (ms === void 0 || !Number.isFinite(ms)) return "—";
|
|
511
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
512
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
513
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
514
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
515
|
+
if (days > 0) return `${days} 天 ${hours} 小时`;
|
|
516
|
+
if (hours > 0) return `${hours} 小时 ${minutes} 分`;
|
|
517
|
+
if (minutes > 0) return `${minutes} 分 ${totalSeconds % 60} 秒`;
|
|
518
|
+
return `${totalSeconds} 秒`;
|
|
519
|
+
}
|
|
520
|
+
function connectionLabel(state) {
|
|
521
|
+
switch (state) {
|
|
522
|
+
case "connected": return {
|
|
523
|
+
text: "已连接",
|
|
524
|
+
tone: "ok"
|
|
525
|
+
};
|
|
526
|
+
case "connecting": return {
|
|
527
|
+
text: "连接中",
|
|
528
|
+
tone: "warn"
|
|
529
|
+
};
|
|
530
|
+
case "reconnecting": return {
|
|
531
|
+
text: "重连中",
|
|
532
|
+
tone: "warn"
|
|
533
|
+
};
|
|
534
|
+
case "failed": return {
|
|
535
|
+
text: "连接失败",
|
|
536
|
+
tone: "bad"
|
|
537
|
+
};
|
|
538
|
+
case "idle": return {
|
|
539
|
+
text: "未连接",
|
|
540
|
+
tone: "muted"
|
|
541
|
+
};
|
|
542
|
+
default: return {
|
|
543
|
+
text: state ?? "未知",
|
|
544
|
+
tone: "muted"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function statusBadge(status) {
|
|
549
|
+
if (status.busy) return {
|
|
550
|
+
text: "重连中",
|
|
551
|
+
tone: "warn"
|
|
552
|
+
};
|
|
553
|
+
if (!status.running) return {
|
|
554
|
+
text: "未运行",
|
|
555
|
+
tone: "bad"
|
|
556
|
+
};
|
|
557
|
+
if (status.level === "healthy") return {
|
|
558
|
+
text: "运行中 · 健康",
|
|
559
|
+
tone: "ok"
|
|
560
|
+
};
|
|
561
|
+
if (status.level === "degraded") return {
|
|
562
|
+
text: "运行中 · 降级",
|
|
563
|
+
tone: "warn"
|
|
564
|
+
};
|
|
565
|
+
return {
|
|
566
|
+
text: "运行中 · 异常",
|
|
567
|
+
tone: "bad"
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/** Local `YYYY-MM-DD HH:mm` rendering for authorization timestamps. */
|
|
571
|
+
function formatAccessDate(ms) {
|
|
572
|
+
if (!Number.isFinite(ms) || ms <= 0) return "—";
|
|
573
|
+
const date = new Date(ms);
|
|
574
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
575
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
576
|
+
}
|
|
577
|
+
/** Which authorization entry produced one grant. */
|
|
578
|
+
function accessSourceLabel(source) {
|
|
579
|
+
switch (source) {
|
|
580
|
+
case "command": return PANEL_COPY.accessSourceCommand;
|
|
581
|
+
case "card": return PANEL_COPY.accessSourceCard;
|
|
582
|
+
default: return PANEL_COPY.accessSourcePanel;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Effective access-mode label, reusing the Host field options for consistency. */
|
|
586
|
+
function accessModeLabel(panel, mode) {
|
|
587
|
+
return panel.config.fields.find((item) => item.key === "accessMode")?.options?.find((option) => option.value === mode)?.label ?? mode;
|
|
588
|
+
}
|
|
589
|
+
function fieldLabel(fields, key) {
|
|
590
|
+
return fields.find((field) => field.key === key)?.label ?? key;
|
|
591
|
+
}
|
|
592
|
+
function effectiveValue(panel, key) {
|
|
593
|
+
return panel.config.effective[key] ?? panel.config.base[key];
|
|
594
|
+
}
|
|
595
|
+
/** Numeric draft kept as the raw input string; parsed on save. */
|
|
596
|
+
function numericDraft(field, raw) {
|
|
597
|
+
if (typeof raw === "number") return raw;
|
|
598
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
599
|
+
const parsed = Number(raw);
|
|
600
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
601
|
+
}
|
|
602
|
+
function validateDraft(panel, draft, only) {
|
|
603
|
+
const values = {};
|
|
604
|
+
const issues = [];
|
|
605
|
+
for (const field of panel.config.fields) {
|
|
606
|
+
if (!(field.key in draft)) continue;
|
|
607
|
+
if (only !== void 0 && !only.has(field.key)) continue;
|
|
608
|
+
const raw = draft[field.key];
|
|
609
|
+
if (field.kind === "number") {
|
|
610
|
+
const parsed = numericDraft(field, raw);
|
|
611
|
+
if (parsed === void 0) {
|
|
612
|
+
issues.push({
|
|
613
|
+
key: field.key,
|
|
614
|
+
message: "请输入数字"
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (field.min !== void 0 && parsed < field.min) {
|
|
619
|
+
issues.push({
|
|
620
|
+
key: field.key,
|
|
621
|
+
message: `不得小于 ${field.min}`
|
|
622
|
+
});
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (field.max !== void 0 && parsed > field.max) {
|
|
626
|
+
issues.push({
|
|
627
|
+
key: field.key,
|
|
628
|
+
message: `不得大于 ${field.max}`
|
|
629
|
+
});
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
values[field.key] = parsed;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (field.kind === "boolean") {
|
|
636
|
+
values[field.key] = raw === true;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
values[field.key] = String(raw ?? "");
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
values,
|
|
643
|
+
issues
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/** One generic form control driven by the Host field descriptor. */
|
|
647
|
+
function FieldRow(props) {
|
|
648
|
+
const { field, value, issues, onChange } = props;
|
|
649
|
+
const issue = issues.find((item) => item.key === field.key);
|
|
650
|
+
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
651
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
652
|
+
className: `dsh-channels-cfg__field${field.wide === true ? " dsh-channels-cfg__field--wide" : ""}`,
|
|
653
|
+
children: [
|
|
654
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
655
|
+
className: "dsh-channels-cfg__labelRow",
|
|
656
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
657
|
+
className: "dsh-channels-cfg__label",
|
|
658
|
+
children: field.label
|
|
659
|
+
}), field.restartRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
660
|
+
className: "dsh-channels-cfg__tag dsh-channels-cfg__tag--restart",
|
|
661
|
+
children: PANEL_COPY.restartTag
|
|
662
|
+
}) : null]
|
|
663
|
+
}),
|
|
664
|
+
field.kind === "boolean" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
665
|
+
className: "dsh-channels-cfg__check",
|
|
666
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
667
|
+
type: "checkbox",
|
|
668
|
+
className: "dsh-channels-cfg__checkbox",
|
|
669
|
+
checked: value === true,
|
|
670
|
+
onChange: (event) => onChange(field.key, event.target.checked)
|
|
671
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === true ? "已启用" : "已关闭" })]
|
|
672
|
+
}) : null,
|
|
673
|
+
field.kind === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
674
|
+
className: inputClass,
|
|
675
|
+
value: String(value ?? ""),
|
|
676
|
+
disabled: field.disabled === true,
|
|
677
|
+
onChange: (event) => onChange(field.key, event.target.value),
|
|
678
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
679
|
+
value: option.value,
|
|
680
|
+
children: option.label
|
|
681
|
+
}, option.value))
|
|
682
|
+
}) : null,
|
|
683
|
+
field.kind === "number" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
684
|
+
type: "number",
|
|
685
|
+
className: inputClass,
|
|
686
|
+
value: value === void 0 || value === "" ? "" : String(value),
|
|
687
|
+
min: field.min,
|
|
688
|
+
max: field.max,
|
|
689
|
+
step: field.step,
|
|
690
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
691
|
+
}) : null,
|
|
692
|
+
field.kind === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
693
|
+
type: "text",
|
|
694
|
+
className: inputClass,
|
|
695
|
+
value: String(value ?? ""),
|
|
696
|
+
placeholder: field.placeholder,
|
|
697
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
698
|
+
}) : null,
|
|
699
|
+
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
700
|
+
className: "dsh-channels-cfg__help",
|
|
701
|
+
children: field.help
|
|
702
|
+
}),
|
|
703
|
+
issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
704
|
+
className: "dsh-channels-cfg__fieldError",
|
|
705
|
+
children: issue.message
|
|
706
|
+
})
|
|
707
|
+
]
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/** Status card: badge, connection facts, and the two panel actions. */
|
|
711
|
+
function StatusCard(props) {
|
|
712
|
+
const { status, pollError, busy, onRefresh, onReconnect } = props;
|
|
713
|
+
const badge = statusBadge(status);
|
|
714
|
+
const connection = connectionLabel(status.connection?.state);
|
|
715
|
+
const facts = [
|
|
716
|
+
{
|
|
717
|
+
label: "飞书连接",
|
|
718
|
+
value: status.connection === void 0 ? "未知" : connection.text
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
label: "Bot open_id",
|
|
722
|
+
value: status.botOpenIdTail ?? "—"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
label: "运行时长",
|
|
726
|
+
value: formatUptime(status.uptimeMs)
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
label: "话题路由",
|
|
730
|
+
value: String(status.routeCount)
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
label: "活动会话",
|
|
734
|
+
value: String(status.liveAgentCount)
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
label: "在途回复",
|
|
738
|
+
value: String(status.inflightTurnCount)
|
|
739
|
+
}
|
|
740
|
+
];
|
|
741
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
742
|
+
className: "dsh-channels-cfg__card",
|
|
743
|
+
children: [
|
|
744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
745
|
+
className: "dsh-channels-cfg__cardHead",
|
|
746
|
+
children: [
|
|
747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
748
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
749
|
+
children: PANEL_COPY.statusCard
|
|
750
|
+
}),
|
|
751
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
752
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${badge.tone}`,
|
|
753
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__dot" }), badge.text]
|
|
754
|
+
}),
|
|
755
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
756
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
757
|
+
children: "每 5 秒自动刷新"
|
|
758
|
+
})
|
|
759
|
+
]
|
|
760
|
+
}),
|
|
761
|
+
status.lastStartError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
762
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
763
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.lastError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
764
|
+
className: "dsh-channels-cfg__mono",
|
|
765
|
+
children: status.lastStartError
|
|
766
|
+
})]
|
|
767
|
+
}),
|
|
768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
769
|
+
className: "dsh-channels-cfg__facts",
|
|
770
|
+
children: facts.map((fact) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
771
|
+
className: "dsh-channels-cfg__fact",
|
|
772
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
773
|
+
className: "dsh-channels-cfg__factLabel",
|
|
774
|
+
children: fact.label
|
|
775
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
776
|
+
className: "dsh-channels-cfg__factValue",
|
|
777
|
+
children: fact.value
|
|
778
|
+
})]
|
|
779
|
+
}, fact.label))
|
|
780
|
+
}),
|
|
781
|
+
status.reasons.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
782
|
+
className: `dsh-channels-cfg__help${status.level === "unhealthy" ? " dsh-channels-cfg__reasonBad" : ""}`,
|
|
783
|
+
children: ["健康判定:", status.reasons.join(";")]
|
|
784
|
+
}),
|
|
785
|
+
pollError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
786
|
+
className: "dsh-channels-cfg__fieldError",
|
|
787
|
+
children: ["状态刷新失败:", pollError]
|
|
788
|
+
}),
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
790
|
+
className: "dsh-channels-cfg__actions",
|
|
791
|
+
children: [
|
|
792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
793
|
+
type: "button",
|
|
794
|
+
className: "dsh-channels-cfg__btn",
|
|
795
|
+
onClick: onRefresh,
|
|
796
|
+
disabled: busy !== null,
|
|
797
|
+
children: PANEL_COPY.refresh
|
|
798
|
+
}),
|
|
799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
800
|
+
type: "button",
|
|
801
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
802
|
+
onClick: onReconnect,
|
|
803
|
+
disabled: busy !== null,
|
|
804
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
805
|
+
}),
|
|
806
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
807
|
+
className: "dsh-channels-cfg__help",
|
|
808
|
+
children: PANEL_COPY.restartWarning
|
|
809
|
+
})
|
|
810
|
+
]
|
|
811
|
+
})
|
|
812
|
+
]
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
/** Credential editor: writes through the Host credentials seam, never echoes values. */
|
|
816
|
+
function CredentialsCard(props) {
|
|
817
|
+
const { panel, appId, appSecret, issues, busy, onAppIdChange, onAppSecretChange, onSave, onClear } = props;
|
|
818
|
+
const appIdState = panel.config.credentials.appId;
|
|
819
|
+
const appSecretState = panel.config.credentials.appSecret;
|
|
820
|
+
const writable = appIdState.writable || appSecretState.writable;
|
|
821
|
+
const disabled = busy !== null || !writable;
|
|
822
|
+
const pending = appId.trim() !== "" || appSecret.trim() !== "";
|
|
823
|
+
const row = (field, label, state, value, type, placeholder, onChange) => {
|
|
824
|
+
const issue = issues.find((item) => item.key === field);
|
|
825
|
+
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
826
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
827
|
+
className: "dsh-channels-cfg__field",
|
|
828
|
+
children: [
|
|
829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
830
|
+
className: "dsh-channels-cfg__labelRow",
|
|
831
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
832
|
+
className: "dsh-channels-cfg__label",
|
|
833
|
+
children: label
|
|
834
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
835
|
+
className: `dsh-channels-cfg__badge dsh-channels-cfg__badge--${state.configured ? "ok" : "bad"}`,
|
|
836
|
+
children: state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing
|
|
837
|
+
})]
|
|
838
|
+
}),
|
|
839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
840
|
+
className: "dsh-channels-cfg__cred",
|
|
841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
842
|
+
type,
|
|
843
|
+
className: inputClass,
|
|
844
|
+
value,
|
|
845
|
+
placeholder,
|
|
846
|
+
autoComplete: "new-password",
|
|
847
|
+
disabled,
|
|
848
|
+
onChange: (event) => onChange(event.target.value)
|
|
849
|
+
}), state.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
850
|
+
type: "button",
|
|
851
|
+
className: "dsh-channels-cfg__btn",
|
|
852
|
+
disabled,
|
|
853
|
+
onClick: () => onClear(field),
|
|
854
|
+
children: busy === "credentials-clear" ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential
|
|
855
|
+
}) : null]
|
|
856
|
+
}),
|
|
857
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
858
|
+
className: "dsh-channels-cfg__help",
|
|
859
|
+
children: [
|
|
860
|
+
PANEL_COPY.credentialsRef,
|
|
861
|
+
":",
|
|
862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
863
|
+
className: "dsh-channels-cfg__mono",
|
|
864
|
+
children: state.ref
|
|
865
|
+
}),
|
|
866
|
+
state.source === void 0 ? "" : `(来源:${state.source})`
|
|
867
|
+
]
|
|
868
|
+
}),
|
|
869
|
+
issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
870
|
+
className: "dsh-channels-cfg__fieldError",
|
|
871
|
+
children: issue.message
|
|
872
|
+
})
|
|
873
|
+
]
|
|
874
|
+
}, field);
|
|
875
|
+
};
|
|
876
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
877
|
+
className: "dsh-channels-cfg__card",
|
|
878
|
+
children: [
|
|
879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
880
|
+
className: "dsh-channels-cfg__cardHead",
|
|
881
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
882
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
883
|
+
children: PANEL_COPY.credentialsCard
|
|
884
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
885
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
886
|
+
children: "DSH 凭据库"
|
|
887
|
+
})]
|
|
888
|
+
}),
|
|
889
|
+
row("appId", PANEL_COPY.appIdLabel, appIdState, appId, "text", PANEL_COPY.appIdPlaceholder, onAppIdChange),
|
|
890
|
+
row("appSecret", PANEL_COPY.appSecretLabel, appSecretState, appSecret, "password", PANEL_COPY.appSecretPlaceholder, onAppSecretChange),
|
|
891
|
+
writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
892
|
+
className: "dsh-channels-cfg__fieldError",
|
|
893
|
+
children: PANEL_COPY.credentialReadOnly
|
|
894
|
+
}),
|
|
895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
896
|
+
className: "dsh-channels-cfg__footer",
|
|
897
|
+
children: [
|
|
898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
899
|
+
type: "button",
|
|
900
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
901
|
+
onClick: onSave,
|
|
902
|
+
disabled: disabled || !pending,
|
|
903
|
+
children: busy === "credentials" ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials
|
|
904
|
+
}),
|
|
905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
906
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
907
|
+
className: "dsh-channels-cfg__help",
|
|
908
|
+
children: PANEL_COPY.credentialsHelp
|
|
909
|
+
})
|
|
910
|
+
]
|
|
911
|
+
})
|
|
912
|
+
]
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
/** Read-only card: credentials, storage layer, and advanced limits. */
|
|
916
|
+
function AdvancedCard(props) {
|
|
917
|
+
const { panel } = props;
|
|
918
|
+
const { readOnly } = panel.config;
|
|
919
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
920
|
+
className: "dsh-channels-cfg__card",
|
|
921
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
922
|
+
className: "dsh-channels-cfg__cardHead",
|
|
923
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
924
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
925
|
+
children: PANEL_COPY.advancedCard
|
|
926
|
+
})
|
|
927
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
928
|
+
className: "dsh-channels-cfg__readOnly",
|
|
929
|
+
children: [
|
|
930
|
+
readOnly.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
931
|
+
className: "dsh-channels-cfg__kv",
|
|
932
|
+
children: [
|
|
933
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
934
|
+
className: "dsh-channels-cfg__kvKey",
|
|
935
|
+
children: field.label
|
|
936
|
+
}),
|
|
937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
938
|
+
className: "dsh-channels-cfg__kvValue",
|
|
939
|
+
children: field.value
|
|
940
|
+
}),
|
|
941
|
+
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
942
|
+
className: "dsh-channels-cfg__help",
|
|
943
|
+
children: field.help
|
|
944
|
+
})
|
|
945
|
+
]
|
|
946
|
+
}, field.key)),
|
|
947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
948
|
+
className: "dsh-channels-cfg__kv",
|
|
949
|
+
children: [
|
|
950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
951
|
+
className: "dsh-channels-cfg__kvKey",
|
|
952
|
+
children: PANEL_COPY.overlayPath
|
|
953
|
+
}),
|
|
954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
955
|
+
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
956
|
+
children: panel.status.overlayPath
|
|
957
|
+
}),
|
|
958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
959
|
+
className: "dsh-channels-cfg__help",
|
|
960
|
+
children: "可以直接编辑该 JSON 文件;面板保存会整体重写它,「清除 Web 覆盖」会删除它。"
|
|
961
|
+
})
|
|
962
|
+
]
|
|
963
|
+
}),
|
|
964
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
965
|
+
className: "dsh-channels-cfg__kv",
|
|
966
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
967
|
+
className: "dsh-channels-cfg__kvKey",
|
|
968
|
+
children: PANEL_COPY.version
|
|
969
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
970
|
+
className: "dsh-channels-cfg__kvValue dsh-channels-cfg__mono",
|
|
971
|
+
children: panel.status.version
|
|
972
|
+
})]
|
|
973
|
+
})
|
|
974
|
+
]
|
|
975
|
+
})]
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
/** Workspace cleanup card: deletion preview + the irreversible manual action. */
|
|
979
|
+
function CleanupCard(props) {
|
|
980
|
+
const { panel, cleanup, busy, result, error, onRun } = props;
|
|
981
|
+
const autoText = cleanup.retention === "off" ? PANEL_COPY.cleanupAutoInactive : cleanup.auto ? PANEL_COPY.cleanupAutoOn : PANEL_COPY.cleanupAutoOff;
|
|
982
|
+
const blocked = cleanup.error !== void 0 ? `${PANEL_COPY.cleanupPreviewFailed}:${cleanup.error}` : cleanup.retention === "off" ? PANEL_COPY.cleanupDisabledOff : cleanup.candidates === 0 ? PANEL_COPY.cleanupDisabledEmpty : null;
|
|
983
|
+
const facts = [
|
|
984
|
+
{
|
|
985
|
+
label: PANEL_COPY.cleanupRetention,
|
|
986
|
+
value: retentionLabel(panel, cleanup)
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
label: PANEL_COPY.cleanupAutoLabel,
|
|
990
|
+
value: autoText
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
label: PANEL_COPY.cleanupCandidates,
|
|
994
|
+
value: `${cleanup.candidates} / ${cleanup.workspaces} 个`
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
label: PANEL_COPY.cleanupCandidateBytes,
|
|
998
|
+
value: formatBytes(cleanup.candidateBytes)
|
|
999
|
+
}
|
|
1000
|
+
];
|
|
1001
|
+
const running = busy === "cleanup";
|
|
1002
|
+
const failed = result !== null && (!result.ok || error !== null);
|
|
1003
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1004
|
+
className: "dsh-channels-cfg__card",
|
|
1005
|
+
children: [
|
|
1006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1007
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1008
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1009
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1010
|
+
children: PANEL_COPY.cleanupCard
|
|
1011
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1012
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1013
|
+
children: retentionLabel(panel, cleanup)
|
|
1014
|
+
})]
|
|
1015
|
+
}),
|
|
1016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1017
|
+
className: "dsh-channels-cfg__help",
|
|
1018
|
+
children: PANEL_COPY.cleanupCardHelp
|
|
1019
|
+
}),
|
|
1020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1021
|
+
className: "dsh-channels-cfg__facts",
|
|
1022
|
+
children: facts.map((fact) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1023
|
+
className: "dsh-channels-cfg__fact",
|
|
1024
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1025
|
+
className: "dsh-channels-cfg__factLabel",
|
|
1026
|
+
children: fact.label
|
|
1027
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1028
|
+
className: "dsh-channels-cfg__factValue",
|
|
1029
|
+
children: fact.value
|
|
1030
|
+
})]
|
|
1031
|
+
}, fact.label))
|
|
1032
|
+
}),
|
|
1033
|
+
blocked === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1034
|
+
className: "dsh-channels-cfg__help",
|
|
1035
|
+
children: blocked
|
|
1036
|
+
}),
|
|
1037
|
+
result === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1038
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${failed ? "error" : "ok"}`,
|
|
1039
|
+
children: [result.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1040
|
+
PANEL_COPY.cleanupDone,
|
|
1041
|
+
":",
|
|
1042
|
+
PANEL_COPY.cleanupDeleted,
|
|
1043
|
+
" ",
|
|
1044
|
+
result.deletedWorkspaces,
|
|
1045
|
+
" 个,",
|
|
1046
|
+
PANEL_COPY.cleanupRetired,
|
|
1047
|
+
" ",
|
|
1048
|
+
result.retiredRoutes,
|
|
1049
|
+
" 条,",
|
|
1050
|
+
PANEL_COPY.cleanupFreed,
|
|
1051
|
+
" ",
|
|
1052
|
+
formatBytes(result.freedBytes),
|
|
1053
|
+
"。"
|
|
1054
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.cleanupFailed, result.error === void 0 ? "" : `:${result.error}`] }), result.failures.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: PANEL_COPY.cleanupFailures }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1055
|
+
className: "dsh-channels-cfg__issues",
|
|
1056
|
+
children: result.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1057
|
+
className: "dsh-channels-cfg__mono",
|
|
1058
|
+
children: failure
|
|
1059
|
+
}, `${index}-${failure}`))
|
|
1060
|
+
})] })]
|
|
1061
|
+
}),
|
|
1062
|
+
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1063
|
+
className: "dsh-channels-cfg__fieldError",
|
|
1064
|
+
children: error
|
|
1065
|
+
}),
|
|
1066
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1067
|
+
className: "dsh-channels-cfg__actions",
|
|
1068
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1069
|
+
type: "button",
|
|
1070
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1071
|
+
onClick: onRun,
|
|
1072
|
+
disabled: running || busy !== null || blocked !== null,
|
|
1073
|
+
children: running ? PANEL_COPY.cleanupRunning : PANEL_COPY.cleanupNow
|
|
1074
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1075
|
+
className: "dsh-channels-cfg__help",
|
|
1076
|
+
children: PANEL_COPY.cleanupConfirmHint
|
|
1077
|
+
})]
|
|
1078
|
+
})
|
|
1079
|
+
]
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
/** Access card: fail-closed warning, administrator, whitelists, pending requests. */
|
|
1083
|
+
function AccessCard(props) {
|
|
1084
|
+
const { panel, busy, notice, error, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1085
|
+
const { access } = panel;
|
|
1086
|
+
const [groupChatId, setGroupChatId] = (0, react.useState)("");
|
|
1087
|
+
const [groupName, setGroupName] = (0, react.useState)("");
|
|
1088
|
+
const [userOpenId, setUserOpenId] = (0, react.useState)("");
|
|
1089
|
+
const [userName, setUserName] = (0, react.useState)("");
|
|
1090
|
+
const groups = access.groups;
|
|
1091
|
+
const users = access.users;
|
|
1092
|
+
const requests = access.requests;
|
|
1093
|
+
const working = busy === "access";
|
|
1094
|
+
const disabled = busy !== null;
|
|
1095
|
+
const submitGroup = async () => {
|
|
1096
|
+
if (groupChatId.trim() === "") return;
|
|
1097
|
+
if (await onAddGroup(groupChatId, groupName)) {
|
|
1098
|
+
setGroupChatId("");
|
|
1099
|
+
setGroupName("");
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const submitUser = async () => {
|
|
1103
|
+
if (userOpenId.trim() === "") return;
|
|
1104
|
+
if (await onAddUser(userOpenId, userName)) {
|
|
1105
|
+
setUserOpenId("");
|
|
1106
|
+
setUserName("");
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1110
|
+
className: "dsh-channels-cfg__card",
|
|
1111
|
+
children: [
|
|
1112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1113
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1114
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1115
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1116
|
+
children: PANEL_COPY.accessCard
|
|
1117
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1118
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1119
|
+
children: accessModeLabel(panel, access.mode)
|
|
1120
|
+
})]
|
|
1121
|
+
}),
|
|
1122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1123
|
+
className: "dsh-channels-cfg__help",
|
|
1124
|
+
children: PANEL_COPY.accessCardHelp
|
|
1125
|
+
}),
|
|
1126
|
+
access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1127
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1128
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning })
|
|
1129
|
+
}) : null,
|
|
1130
|
+
access.mode === "open" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1131
|
+
className: "dsh-channels-cfg__banner",
|
|
1132
|
+
children: PANEL_COPY.accessModeOpenWarning
|
|
1133
|
+
}) : null,
|
|
1134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1135
|
+
className: "dsh-channels-cfg__kv",
|
|
1136
|
+
children: [
|
|
1137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1138
|
+
className: "dsh-channels-cfg__kvKey",
|
|
1139
|
+
children: PANEL_COPY.accessAdminLabel
|
|
1140
|
+
}),
|
|
1141
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1142
|
+
className: `dsh-channels-cfg__kvValue dsh-channels-cfg__mono${access.admin.configured ? "" : " dsh-channels-cfg__reasonBad"}`,
|
|
1143
|
+
children: access.admin.openId ?? PANEL_COPY.accessAdminMissing
|
|
1144
|
+
}),
|
|
1145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1146
|
+
className: "dsh-channels-cfg__help",
|
|
1147
|
+
children: PANEL_COPY.accessAdminHint
|
|
1148
|
+
})
|
|
1149
|
+
]
|
|
1150
|
+
}),
|
|
1151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1152
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1153
|
+
children: [
|
|
1154
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1155
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1156
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1157
|
+
className: "dsh-channels-cfg__label",
|
|
1158
|
+
children: PANEL_COPY.accessGroupsTitle
|
|
1159
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1160
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1161
|
+
children: [groups.length, " 个"]
|
|
1162
|
+
})]
|
|
1163
|
+
}),
|
|
1164
|
+
groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1165
|
+
className: "dsh-channels-cfg__help",
|
|
1166
|
+
children: PANEL_COPY.accessGroupsEmpty
|
|
1167
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1168
|
+
className: "dsh-channels-cfg__accessList",
|
|
1169
|
+
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1170
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1172
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1173
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: group.name ?? group.chatId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1174
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1175
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1176
|
+
className: "dsh-channels-cfg__mono",
|
|
1177
|
+
children: group.chatId
|
|
1178
|
+
}), ` · ${accessSourceLabel(group.source)} · ${formatAccessDate(group.addedAt)}`]
|
|
1179
|
+
})]
|
|
1180
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1181
|
+
type: "button",
|
|
1182
|
+
className: "dsh-channels-cfg__btn",
|
|
1183
|
+
disabled,
|
|
1184
|
+
onClick: () => onRemoveGroup(group.chatId),
|
|
1185
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1186
|
+
})]
|
|
1187
|
+
}, group.chatId))
|
|
1188
|
+
}),
|
|
1189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1190
|
+
className: "dsh-channels-cfg__accessAdd",
|
|
1191
|
+
children: [
|
|
1192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1193
|
+
type: "text",
|
|
1194
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1195
|
+
value: groupChatId,
|
|
1196
|
+
placeholder: PANEL_COPY.accessGroupChatIdPlaceholder,
|
|
1197
|
+
disabled,
|
|
1198
|
+
onChange: (event) => setGroupChatId(event.target.value)
|
|
1199
|
+
}),
|
|
1200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1201
|
+
type: "text",
|
|
1202
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1203
|
+
value: groupName,
|
|
1204
|
+
placeholder: PANEL_COPY.accessGroupNamePlaceholder,
|
|
1205
|
+
disabled,
|
|
1206
|
+
onChange: (event) => setGroupName(event.target.value)
|
|
1207
|
+
}),
|
|
1208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1209
|
+
type: "button",
|
|
1210
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1211
|
+
disabled: disabled || groupChatId.trim() === "",
|
|
1212
|
+
onClick: () => {
|
|
1213
|
+
submitGroup();
|
|
1214
|
+
},
|
|
1215
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddGroup
|
|
1216
|
+
})
|
|
1217
|
+
]
|
|
1218
|
+
})
|
|
1219
|
+
]
|
|
1220
|
+
}),
|
|
1221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1222
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1223
|
+
children: [
|
|
1224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1225
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1226
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1227
|
+
className: "dsh-channels-cfg__label",
|
|
1228
|
+
children: PANEL_COPY.accessUsersTitle
|
|
1229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1230
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1231
|
+
children: [users.length, " 个"]
|
|
1232
|
+
})]
|
|
1233
|
+
}),
|
|
1234
|
+
users.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1235
|
+
className: "dsh-channels-cfg__help",
|
|
1236
|
+
children: PANEL_COPY.accessUsersEmpty
|
|
1237
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1238
|
+
className: "dsh-channels-cfg__accessList",
|
|
1239
|
+
children: users.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1240
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1241
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1242
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1243
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.openId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1244
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
|
+
className: "dsh-channels-cfg__mono",
|
|
1247
|
+
children: user.openId
|
|
1248
|
+
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1249
|
+
})]
|
|
1250
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1251
|
+
type: "button",
|
|
1252
|
+
className: "dsh-channels-cfg__btn",
|
|
1253
|
+
disabled,
|
|
1254
|
+
onClick: () => onRemoveUser(user.openId),
|
|
1255
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1256
|
+
})]
|
|
1257
|
+
}, user.openId))
|
|
1258
|
+
}),
|
|
1259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1260
|
+
className: "dsh-channels-cfg__accessAdd",
|
|
1261
|
+
children: [
|
|
1262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1263
|
+
type: "text",
|
|
1264
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1265
|
+
value: userOpenId,
|
|
1266
|
+
placeholder: PANEL_COPY.accessUserOpenIdPlaceholder,
|
|
1267
|
+
disabled,
|
|
1268
|
+
onChange: (event) => setUserOpenId(event.target.value)
|
|
1269
|
+
}),
|
|
1270
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1271
|
+
type: "text",
|
|
1272
|
+
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1273
|
+
value: userName,
|
|
1274
|
+
placeholder: PANEL_COPY.accessUserNamePlaceholder,
|
|
1275
|
+
disabled,
|
|
1276
|
+
onChange: (event) => setUserName(event.target.value)
|
|
1277
|
+
}),
|
|
1278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1279
|
+
type: "button",
|
|
1280
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1281
|
+
disabled: disabled || userOpenId.trim() === "",
|
|
1282
|
+
onClick: () => {
|
|
1283
|
+
submitUser();
|
|
1284
|
+
},
|
|
1285
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAddUser
|
|
1286
|
+
})
|
|
1287
|
+
]
|
|
1288
|
+
})
|
|
1289
|
+
]
|
|
1290
|
+
}),
|
|
1291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1292
|
+
className: "dsh-channels-cfg__accessSection",
|
|
1293
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1294
|
+
className: "dsh-channels-cfg__labelRow",
|
|
1295
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1296
|
+
className: "dsh-channels-cfg__label",
|
|
1297
|
+
children: PANEL_COPY.accessRequestsTitle
|
|
1298
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1299
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1300
|
+
children: [requests.length, " 条"]
|
|
1301
|
+
})]
|
|
1302
|
+
}), requests.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1303
|
+
className: "dsh-channels-cfg__help",
|
|
1304
|
+
children: PANEL_COPY.accessRequestsEmpty
|
|
1305
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1306
|
+
className: "dsh-channels-cfg__accessList",
|
|
1307
|
+
children: requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1308
|
+
className: "dsh-channels-cfg__accessRow",
|
|
1309
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1310
|
+
className: "dsh-channels-cfg__accessMain",
|
|
1311
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1312
|
+
request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser,
|
|
1313
|
+
":",
|
|
1314
|
+
request.name ?? request.targetId
|
|
1315
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1316
|
+
className: "dsh-channels-cfg__accessMeta",
|
|
1317
|
+
children: [
|
|
1318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1319
|
+
className: "dsh-channels-cfg__mono",
|
|
1320
|
+
children: request.targetId
|
|
1321
|
+
}),
|
|
1322
|
+
` · ${formatAccessDate(request.createdAt)} · ${PANEL_COPY.accessRequestExpires} ${formatAccessDate(request.expiresAt)}`,
|
|
1323
|
+
request.kind === "user" && request.requester !== void 0 ? ` · ${PANEL_COPY.accessRequestRequester} ${request.requester}` : "",
|
|
1324
|
+
request.kind === "user" && request.groupName !== void 0 ? ` · 群 ${request.groupName}` : request.kind === "user" && request.chatId !== void 0 ? ` · 群 ${request.chatId}` : ""
|
|
1325
|
+
]
|
|
1326
|
+
})]
|
|
1327
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1328
|
+
className: "dsh-channels-cfg__accessActions",
|
|
1329
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1330
|
+
type: "button",
|
|
1331
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1332
|
+
disabled,
|
|
1333
|
+
onClick: () => onResolveRequest(request.token, "allow"),
|
|
1334
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessAllow
|
|
1335
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1336
|
+
type: "button",
|
|
1337
|
+
className: "dsh-channels-cfg__btn",
|
|
1338
|
+
disabled,
|
|
1339
|
+
onClick: () => onResolveRequest(request.token, "deny"),
|
|
1340
|
+
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessDeny
|
|
1341
|
+
})]
|
|
1342
|
+
})]
|
|
1343
|
+
}, request.token))
|
|
1344
|
+
})]
|
|
1345
|
+
}),
|
|
1346
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1347
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
1348
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice })
|
|
1349
|
+
}),
|
|
1350
|
+
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1351
|
+
className: "dsh-channels-cfg__fieldError",
|
|
1352
|
+
children: error
|
|
1353
|
+
}),
|
|
1354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1355
|
+
className: "dsh-channels-cfg__help",
|
|
1356
|
+
children: PANEL_COPY.accessHints
|
|
1357
|
+
}),
|
|
1358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1359
|
+
className: "dsh-channels-cfg__help",
|
|
1360
|
+
children: PANEL_COPY.accessChatIdHint
|
|
1361
|
+
})
|
|
1362
|
+
]
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
/** The settings.section page: status card + generic config form + read-only facts. */
|
|
1366
|
+
function FeishuChannelsSection(_props) {
|
|
1367
|
+
const [load, setLoad] = (0, react.useState)({ phase: "loading" });
|
|
1368
|
+
const [draft, setDraft] = (0, react.useState)({});
|
|
1369
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
1370
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
1371
|
+
const [status, setStatus] = (0, react.useState)(null);
|
|
1372
|
+
const [pollError, setPollError] = (0, react.useState)(null);
|
|
1373
|
+
const [appIdInput, setAppIdInput] = (0, react.useState)("");
|
|
1374
|
+
const [appSecretInput, setAppSecretInput] = (0, react.useState)("");
|
|
1375
|
+
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
1376
|
+
const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
|
|
1377
|
+
const [cleanupResult, setCleanupResult] = (0, react.useState)(null);
|
|
1378
|
+
const [cleanupError, setCleanupError] = (0, react.useState)(null);
|
|
1379
|
+
const [accessNotice, setAccessNotice] = (0, react.useState)(null);
|
|
1380
|
+
const [accessError, setAccessError] = (0, react.useState)(null);
|
|
1381
|
+
const catalogTicket = (0, react.useRef)(0);
|
|
1382
|
+
const panel = load.phase === "ready" ? load.panel : null;
|
|
1383
|
+
const currentStatus = status ?? panel?.status ?? null;
|
|
1384
|
+
const loadPanel = (0, react.useCallback)(async () => {
|
|
1385
|
+
setLoad({ phase: "loading" });
|
|
1386
|
+
setNotice(null);
|
|
1387
|
+
setCleanupResult(null);
|
|
1388
|
+
setCleanupError(null);
|
|
1389
|
+
setAccessNotice(null);
|
|
1390
|
+
setAccessError(null);
|
|
1391
|
+
try {
|
|
1392
|
+
const next = await fetchPanel();
|
|
1393
|
+
setLoad({
|
|
1394
|
+
phase: "ready",
|
|
1395
|
+
panel: next
|
|
1396
|
+
});
|
|
1397
|
+
setStatus(next.status);
|
|
1398
|
+
setDraft({});
|
|
1399
|
+
setAppIdInput("");
|
|
1400
|
+
setAppSecretInput("");
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
setLoad({
|
|
1403
|
+
phase: "error",
|
|
1404
|
+
message: messageOf(error)
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}, []);
|
|
1408
|
+
(0, react.useEffect)(() => {
|
|
1409
|
+
loadPanel();
|
|
1410
|
+
}, [loadPanel]);
|
|
1411
|
+
(0, react.useEffect)(() => {
|
|
1412
|
+
if (panel === null) {
|
|
1413
|
+
setCatalog(null);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
setCatalog(catalogFromFields(panel.config.fields));
|
|
1417
|
+
setCatalogBusy(false);
|
|
1418
|
+
}, [panel]);
|
|
1419
|
+
(0, react.useEffect)(() => {
|
|
1420
|
+
if (load.phase !== "ready") return void 0;
|
|
1421
|
+
let cancelled = false;
|
|
1422
|
+
const tick = async () => {
|
|
1423
|
+
try {
|
|
1424
|
+
const next = await fetchStatus();
|
|
1425
|
+
if (cancelled) return;
|
|
1426
|
+
setStatus(next);
|
|
1427
|
+
setPollError(null);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
if (!cancelled) setPollError(messageOf(error));
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
const timer = window.setInterval(() => {
|
|
1433
|
+
tick();
|
|
1434
|
+
}, 5e3);
|
|
1435
|
+
return () => {
|
|
1436
|
+
cancelled = true;
|
|
1437
|
+
window.clearInterval(timer);
|
|
1438
|
+
};
|
|
1439
|
+
}, [load.phase]);
|
|
1440
|
+
/** Fetch the selector catalog for a draft route (latest request wins). */
|
|
1441
|
+
const refreshCatalog = (0, react.useCallback)(async (provider, model) => {
|
|
1442
|
+
catalogTicket.current += 1;
|
|
1443
|
+
const ticket = catalogTicket.current;
|
|
1444
|
+
setCatalogBusy(true);
|
|
1445
|
+
try {
|
|
1446
|
+
const next = await fetchCatalog(provider, model);
|
|
1447
|
+
if (catalogTicket.current === ticket) setCatalog(next);
|
|
1448
|
+
} catch {
|
|
1449
|
+
if (catalogTicket.current !== ticket) return;
|
|
1450
|
+
setCatalog((previous) => previous === null ? null : {
|
|
1451
|
+
provider: previous.provider,
|
|
1452
|
+
model: {
|
|
1453
|
+
selectable: false,
|
|
1454
|
+
options: []
|
|
1455
|
+
},
|
|
1456
|
+
reasoningEffort: {
|
|
1457
|
+
selectable: false,
|
|
1458
|
+
options: []
|
|
1459
|
+
},
|
|
1460
|
+
agentPreset: previous.agentPreset
|
|
1461
|
+
});
|
|
1462
|
+
setNotice({
|
|
1463
|
+
kind: "error",
|
|
1464
|
+
text: PANEL_COPY.catalogFailed
|
|
1465
|
+
});
|
|
1466
|
+
} finally {
|
|
1467
|
+
if (catalogTicket.current === ticket) setCatalogBusy(false);
|
|
1468
|
+
}
|
|
1469
|
+
}, []);
|
|
1470
|
+
/** Current form values, used to keep unlisted configured values visible. */
|
|
1471
|
+
const currentValues = (0, react.useMemo)(() => {
|
|
1472
|
+
if (panel === null) return {};
|
|
1473
|
+
const valueOf = (key) => {
|
|
1474
|
+
const raw = draft[key] ?? effectiveValue(panel, key);
|
|
1475
|
+
return typeof raw === "string" && raw !== "" ? raw : void 0;
|
|
1476
|
+
};
|
|
1477
|
+
return {
|
|
1478
|
+
provider: valueOf("provider"),
|
|
1479
|
+
model: valueOf("model"),
|
|
1480
|
+
reasoningEffort: valueOf("reasoningEffort"),
|
|
1481
|
+
agentPreset: valueOf("agentPreset")
|
|
1482
|
+
};
|
|
1483
|
+
}, [draft, panel]);
|
|
1484
|
+
const renderedFields = (0, react.useMemo)(() => applyCatalogFields(panel?.config.fields ?? [], catalog, currentValues), [
|
|
1485
|
+
panel,
|
|
1486
|
+
catalog,
|
|
1487
|
+
currentValues
|
|
1488
|
+
]);
|
|
1489
|
+
/** Whether one selector currently renders as a dropdown for this form. */
|
|
1490
|
+
const isSelectField = (0, react.useCallback)((key) => renderedFields.some((field) => field.key === key && field.kind === "select"), [renderedFields]);
|
|
1491
|
+
const updateDraft = (0, react.useCallback)((key, value) => {
|
|
1492
|
+
if (panel === null) return;
|
|
1493
|
+
const providerNow = String(draft.provider ?? effectiveValue(panel, "provider") ?? "");
|
|
1494
|
+
setNotice(null);
|
|
1495
|
+
if (key === "provider") {
|
|
1496
|
+
const nextProvider = String(value ?? "");
|
|
1497
|
+
const keepModel = nextProvider === String(effectiveValue(panel, "provider") ?? "") ? String(effectiveValue(panel, "model") ?? "") : "";
|
|
1498
|
+
setDraft((previous) => ({
|
|
1499
|
+
...previous,
|
|
1500
|
+
provider: value,
|
|
1501
|
+
model: "",
|
|
1502
|
+
reasoningEffort: ""
|
|
1503
|
+
}));
|
|
1504
|
+
if (catalog !== null && isSelectField("provider")) refreshCatalog(nextProvider, keepModel);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (key === "model") {
|
|
1508
|
+
setDraft((previous) => ({
|
|
1509
|
+
...previous,
|
|
1510
|
+
model: value,
|
|
1511
|
+
reasoningEffort: ""
|
|
1512
|
+
}));
|
|
1513
|
+
if (catalog !== null && isSelectField("model")) refreshCatalog(providerNow, String(value ?? ""));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
setDraft((previous) => ({
|
|
1517
|
+
...previous,
|
|
1518
|
+
[key]: value
|
|
1519
|
+
}));
|
|
1520
|
+
}, [
|
|
1521
|
+
catalog,
|
|
1522
|
+
draft.provider,
|
|
1523
|
+
isSelectField,
|
|
1524
|
+
panel,
|
|
1525
|
+
refreshCatalog
|
|
1526
|
+
]);
|
|
1527
|
+
const refreshStatus = (0, react.useCallback)(async () => {
|
|
1528
|
+
try {
|
|
1529
|
+
const next = await fetchStatus();
|
|
1530
|
+
setStatus(next);
|
|
1531
|
+
setPollError(null);
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
setPollError(messageOf(error));
|
|
1534
|
+
}
|
|
1535
|
+
}, []);
|
|
1536
|
+
/**
|
|
1537
|
+
* Save one scope of the form. The global footer saves every draft key; the
|
|
1538
|
+
* model card's own button saves only the four model-facing keys, so editing
|
|
1539
|
+
* the model does not require scrolling to the bottom.
|
|
1540
|
+
*/
|
|
1541
|
+
const onSave = (0, react.useCallback)(async (scope) => {
|
|
1542
|
+
if (panel === null) return;
|
|
1543
|
+
const only = scope === "model" ? new Set(CATALOG_KEYS) : void 0;
|
|
1544
|
+
const { values, issues } = validateDraft(panel, draft, only);
|
|
1545
|
+
if (issues.length > 0) {
|
|
1546
|
+
setNotice({
|
|
1547
|
+
kind: "error",
|
|
1548
|
+
text: "请先修正标红的配置项",
|
|
1549
|
+
issues
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (Object.keys(values).length === 0) return;
|
|
1554
|
+
setBusy("save");
|
|
1555
|
+
setNotice(null);
|
|
1556
|
+
try {
|
|
1557
|
+
const result = await saveConfig(values);
|
|
1558
|
+
if (!result.ok) {
|
|
1559
|
+
setNotice({
|
|
1560
|
+
kind: "error",
|
|
1561
|
+
text: result.error,
|
|
1562
|
+
issues: result.issues
|
|
1563
|
+
});
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
setLoad({
|
|
1567
|
+
phase: "ready",
|
|
1568
|
+
panel: result.panel
|
|
1569
|
+
});
|
|
1570
|
+
setStatus(result.panel.status);
|
|
1571
|
+
setCleanupResult(null);
|
|
1572
|
+
setCleanupError(null);
|
|
1573
|
+
const cleared = result.clearedKeys.length === 0 ? "" : `;已清除覆盖:${result.clearedKeys.map((key) => fieldLabel(result.panel.config.fields, key)).join("、")}`;
|
|
1574
|
+
setNotice({
|
|
1575
|
+
kind: "ok",
|
|
1576
|
+
text: `${scope === "model" ? PANEL_COPY.savedModel : PANEL_COPY.saved}${cleared}`
|
|
1577
|
+
});
|
|
1578
|
+
setDraft((previous) => {
|
|
1579
|
+
if (scope !== "model") return {};
|
|
1580
|
+
const next = { ...previous };
|
|
1581
|
+
for (const key of CATALOG_KEYS) delete next[key];
|
|
1582
|
+
return next;
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
setNotice({
|
|
1586
|
+
kind: "error",
|
|
1587
|
+
text: messageOf(error)
|
|
1588
|
+
});
|
|
1589
|
+
} finally {
|
|
1590
|
+
setBusy(null);
|
|
1591
|
+
}
|
|
1592
|
+
}, [draft, panel]);
|
|
1593
|
+
const onClear = (0, react.useCallback)(async () => {
|
|
1594
|
+
setBusy("clear");
|
|
1595
|
+
setNotice(null);
|
|
1596
|
+
try {
|
|
1597
|
+
const result = await clearConfig();
|
|
1598
|
+
if (!result.ok) {
|
|
1599
|
+
setNotice({
|
|
1600
|
+
kind: "error",
|
|
1601
|
+
text: result.error
|
|
1602
|
+
});
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
setLoad({
|
|
1606
|
+
phase: "ready",
|
|
1607
|
+
panel: result.panel
|
|
1608
|
+
});
|
|
1609
|
+
setStatus(result.panel.status);
|
|
1610
|
+
setDraft({});
|
|
1611
|
+
setCleanupResult(null);
|
|
1612
|
+
setCleanupError(null);
|
|
1613
|
+
setNotice({
|
|
1614
|
+
kind: "ok",
|
|
1615
|
+
text: PANEL_COPY.cleared
|
|
1616
|
+
});
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
setNotice({
|
|
1619
|
+
kind: "error",
|
|
1620
|
+
text: messageOf(error)
|
|
1621
|
+
});
|
|
1622
|
+
} finally {
|
|
1623
|
+
setBusy(null);
|
|
1624
|
+
}
|
|
1625
|
+
}, []);
|
|
1626
|
+
const onReconnect = (0, react.useCallback)(async () => {
|
|
1627
|
+
if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.restartWarning) : true)) return;
|
|
1628
|
+
setBusy("reconnect");
|
|
1629
|
+
setNotice(null);
|
|
1630
|
+
try {
|
|
1631
|
+
const result = await reconnect();
|
|
1632
|
+
if (!result.ok) {
|
|
1633
|
+
setNotice({
|
|
1634
|
+
kind: "error",
|
|
1635
|
+
text: result.error
|
|
1636
|
+
});
|
|
1637
|
+
await refreshStatus();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
setLoad({
|
|
1641
|
+
phase: "ready",
|
|
1642
|
+
panel: result.panel
|
|
1643
|
+
});
|
|
1644
|
+
setStatus(result.panel.status);
|
|
1645
|
+
setNotice({
|
|
1646
|
+
kind: "ok",
|
|
1647
|
+
text: "已重启飞书连接"
|
|
1648
|
+
});
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
setNotice({
|
|
1651
|
+
kind: "error",
|
|
1652
|
+
text: messageOf(error)
|
|
1653
|
+
});
|
|
1654
|
+
await refreshStatus();
|
|
1655
|
+
} finally {
|
|
1656
|
+
setBusy(null);
|
|
1657
|
+
}
|
|
1658
|
+
}, [refreshStatus]);
|
|
1659
|
+
/**
|
|
1660
|
+
* Delete stale workspaces after an explicit confirmation. The Host refreshes
|
|
1661
|
+
* the panel in the same response, so the preview counts update immediately;
|
|
1662
|
+
* partial failures stay visible in the card instead of being swallowed.
|
|
1663
|
+
*/
|
|
1664
|
+
const onRunCleanup = (0, react.useCallback)(async () => {
|
|
1665
|
+
if (panel === null) return;
|
|
1666
|
+
const { cleanup } = panel;
|
|
1667
|
+
if (!(typeof window.confirm === "function" ? window.confirm(`${PANEL_COPY.cleanupConfirm}\n本次将删除 ${cleanup.candidates} 个工作区,预计释放 ${formatBytes(cleanup.candidateBytes)}。`) : true)) return;
|
|
1668
|
+
setBusy("cleanup");
|
|
1669
|
+
setNotice(null);
|
|
1670
|
+
setCleanupResult(null);
|
|
1671
|
+
setCleanupError(null);
|
|
1672
|
+
try {
|
|
1673
|
+
const response = await runCleanup();
|
|
1674
|
+
if (!response.ok) {
|
|
1675
|
+
setCleanupError(response.error);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
setLoad({
|
|
1679
|
+
phase: "ready",
|
|
1680
|
+
panel: response.panel
|
|
1681
|
+
});
|
|
1682
|
+
setStatus(response.panel.status);
|
|
1683
|
+
setCleanupResult(response.result);
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
setCleanupError(messageOf(error));
|
|
1686
|
+
} finally {
|
|
1687
|
+
setBusy(null);
|
|
1688
|
+
}
|
|
1689
|
+
}, [panel]);
|
|
1690
|
+
/** Replace the embedded access snapshot after a whitelist mutation. */
|
|
1691
|
+
const applyAccess = (0, react.useCallback)((access) => {
|
|
1692
|
+
setLoad((previous) => previous.phase === "ready" ? {
|
|
1693
|
+
phase: "ready",
|
|
1694
|
+
panel: {
|
|
1695
|
+
...previous.panel,
|
|
1696
|
+
access
|
|
1697
|
+
}
|
|
1698
|
+
} : previous);
|
|
1699
|
+
}, []);
|
|
1700
|
+
const onAddAccessGroup = (0, react.useCallback)(async (chatId, name) => {
|
|
1701
|
+
setBusy("access");
|
|
1702
|
+
setAccessError(null);
|
|
1703
|
+
setAccessNotice(null);
|
|
1704
|
+
try {
|
|
1705
|
+
const result = await addAccessGroup(chatId, name);
|
|
1706
|
+
if (!result.ok) {
|
|
1707
|
+
setAccessError(result.error);
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
applyAccess(result.access);
|
|
1711
|
+
setAccessNotice(PANEL_COPY.accessGroupAdded);
|
|
1712
|
+
return true;
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
setAccessError(messageOf(error));
|
|
1715
|
+
return false;
|
|
1716
|
+
} finally {
|
|
1717
|
+
setBusy(null);
|
|
1718
|
+
}
|
|
1719
|
+
}, [applyAccess]);
|
|
1720
|
+
const onRemoveAccessGroup = (0, react.useCallback)(async (chatId) => {
|
|
1721
|
+
setBusy("access");
|
|
1722
|
+
setAccessError(null);
|
|
1723
|
+
setAccessNotice(null);
|
|
1724
|
+
try {
|
|
1725
|
+
const result = await removeAccessGroup(chatId);
|
|
1726
|
+
if (!result.ok) {
|
|
1727
|
+
setAccessError(result.error);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
applyAccess(result.access);
|
|
1731
|
+
setAccessNotice(PANEL_COPY.accessGroupRemoved);
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
setAccessError(messageOf(error));
|
|
1734
|
+
} finally {
|
|
1735
|
+
setBusy(null);
|
|
1736
|
+
}
|
|
1737
|
+
}, [applyAccess]);
|
|
1738
|
+
const onAddAccessUser = (0, react.useCallback)(async (openId, name) => {
|
|
1739
|
+
setBusy("access");
|
|
1740
|
+
setAccessError(null);
|
|
1741
|
+
setAccessNotice(null);
|
|
1742
|
+
try {
|
|
1743
|
+
const result = await addAccessUser(openId, name);
|
|
1744
|
+
if (!result.ok) {
|
|
1745
|
+
setAccessError(result.error);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
applyAccess(result.access);
|
|
1749
|
+
setAccessNotice(PANEL_COPY.accessUserAdded);
|
|
1750
|
+
return true;
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
setAccessError(messageOf(error));
|
|
1753
|
+
return false;
|
|
1754
|
+
} finally {
|
|
1755
|
+
setBusy(null);
|
|
1756
|
+
}
|
|
1757
|
+
}, [applyAccess]);
|
|
1758
|
+
const onRemoveAccessUser = (0, react.useCallback)(async (openId) => {
|
|
1759
|
+
setBusy("access");
|
|
1760
|
+
setAccessError(null);
|
|
1761
|
+
setAccessNotice(null);
|
|
1762
|
+
try {
|
|
1763
|
+
const result = await removeAccessUser(openId);
|
|
1764
|
+
if (!result.ok) {
|
|
1765
|
+
setAccessError(result.error);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
applyAccess(result.access);
|
|
1769
|
+
setAccessNotice(PANEL_COPY.accessUserRemoved);
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
setAccessError(messageOf(error));
|
|
1772
|
+
} finally {
|
|
1773
|
+
setBusy(null);
|
|
1774
|
+
}
|
|
1775
|
+
}, [applyAccess]);
|
|
1776
|
+
const onResolveAccessRequest = (0, react.useCallback)(async (token, decision) => {
|
|
1777
|
+
setBusy("access");
|
|
1778
|
+
setAccessError(null);
|
|
1779
|
+
setAccessNotice(null);
|
|
1780
|
+
try {
|
|
1781
|
+
const result = await resolveAccessRequest(token, decision);
|
|
1782
|
+
if (!result.ok) {
|
|
1783
|
+
setAccessError(result.error);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
applyAccess(result.access);
|
|
1787
|
+
setAccessNotice(decision === "allow" ? PANEL_COPY.accessRequestAllowed : PANEL_COPY.accessRequestDenied);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
setAccessError(messageOf(error));
|
|
1790
|
+
} finally {
|
|
1791
|
+
setBusy(null);
|
|
1792
|
+
}
|
|
1793
|
+
}, [applyAccess]);
|
|
1794
|
+
const onSaveCredentials = (0, react.useCallback)(async () => {
|
|
1795
|
+
const values = {};
|
|
1796
|
+
if (appIdInput.trim() !== "") values.appId = appIdInput;
|
|
1797
|
+
if (appSecretInput.trim() !== "") values.appSecret = appSecretInput;
|
|
1798
|
+
if (values.appId === void 0 && values.appSecret === void 0) return;
|
|
1799
|
+
setBusy("credentials");
|
|
1800
|
+
setNotice(null);
|
|
1801
|
+
try {
|
|
1802
|
+
const result = await saveCredentials(values);
|
|
1803
|
+
if (!result.ok) {
|
|
1804
|
+
setNotice({
|
|
1805
|
+
kind: "error",
|
|
1806
|
+
text: result.error,
|
|
1807
|
+
issues: result.issues
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
setLoad({
|
|
1812
|
+
phase: "ready",
|
|
1813
|
+
panel: result.panel
|
|
1814
|
+
});
|
|
1815
|
+
setStatus(result.panel.status);
|
|
1816
|
+
setAppIdInput("");
|
|
1817
|
+
setAppSecretInput("");
|
|
1818
|
+
const base = result.reconnect === "pending" ? PANEL_COPY.credentialSavedPending : result.reconnect === "auto" ? PANEL_COPY.credentialSavedReconnected : PANEL_COPY.credentialSaved;
|
|
1819
|
+
setNotice({
|
|
1820
|
+
kind: result.autoReconnectError === void 0 ? "ok" : "error",
|
|
1821
|
+
text: result.autoReconnectError === void 0 ? base : `${base};自动重连失败:${result.autoReconnectError}`
|
|
1822
|
+
});
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
setNotice({
|
|
1825
|
+
kind: "error",
|
|
1826
|
+
text: messageOf(error)
|
|
1827
|
+
});
|
|
1828
|
+
} finally {
|
|
1829
|
+
setBusy(null);
|
|
1830
|
+
}
|
|
1831
|
+
}, [appIdInput, appSecretInput]);
|
|
1832
|
+
const onClearCredential = (0, react.useCallback)(async (field) => {
|
|
1833
|
+
const label = field === "appId" ? PANEL_COPY.appIdLabel : PANEL_COPY.appSecretLabel;
|
|
1834
|
+
if (!(typeof window.confirm === "function" ? window.confirm(`确定移除已存储的 ${label}?`) : true)) return;
|
|
1835
|
+
setBusy("credentials-clear");
|
|
1836
|
+
setNotice(null);
|
|
1837
|
+
try {
|
|
1838
|
+
const result = await clearCredential(field);
|
|
1839
|
+
if (!result.ok) {
|
|
1840
|
+
setNotice({
|
|
1841
|
+
kind: "error",
|
|
1842
|
+
text: result.error,
|
|
1843
|
+
issues: result.issues
|
|
1844
|
+
});
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
setLoad({
|
|
1848
|
+
phase: "ready",
|
|
1849
|
+
panel: result.panel
|
|
1850
|
+
});
|
|
1851
|
+
setStatus(result.panel.status);
|
|
1852
|
+
setNotice({
|
|
1853
|
+
kind: "ok",
|
|
1854
|
+
text: result.reconnect === "pending" ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved
|
|
1855
|
+
});
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
setNotice({
|
|
1858
|
+
kind: "error",
|
|
1859
|
+
text: messageOf(error)
|
|
1860
|
+
});
|
|
1861
|
+
} finally {
|
|
1862
|
+
setBusy(null);
|
|
1863
|
+
}
|
|
1864
|
+
}, []);
|
|
1865
|
+
const groupedFields = (0, react.useMemo)(() => {
|
|
1866
|
+
if (panel === null) return [];
|
|
1867
|
+
return GROUP_ORDER.map((group) => ({
|
|
1868
|
+
group,
|
|
1869
|
+
label: GROUP_LABELS[group],
|
|
1870
|
+
fields: renderedFields.filter((field) => field.group === group)
|
|
1871
|
+
})).filter((entry) => entry.fields.length > 0);
|
|
1872
|
+
}, [panel, renderedFields]);
|
|
1873
|
+
const modelEntries = groupedFields.filter((entry) => entry.group === "model");
|
|
1874
|
+
const configEntries = groupedFields.filter((entry) => entry.group !== "model");
|
|
1875
|
+
const dirty = Object.keys(draft).length > 0;
|
|
1876
|
+
const modelDirty = CATALOG_KEYS.some((key) => draft[key] !== void 0);
|
|
1877
|
+
const issues = notice?.kind === "error" ? notice.issues ?? [] : [];
|
|
1878
|
+
if (load.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1879
|
+
className: "dsh-channels-cfg__loading",
|
|
1880
|
+
children: PANEL_COPY.loading
|
|
1881
|
+
});
|
|
1882
|
+
if (load.phase === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1883
|
+
className: "dsh-channels-cfg__error",
|
|
1884
|
+
children: [
|
|
1885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.loadFailed }),
|
|
1886
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: load.message }),
|
|
1887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1888
|
+
type: "button",
|
|
1889
|
+
className: "dsh-channels-cfg__btn",
|
|
1890
|
+
onClick: () => {
|
|
1891
|
+
loadPanel();
|
|
1892
|
+
},
|
|
1893
|
+
children: PANEL_COPY.retry
|
|
1894
|
+
}) })
|
|
1895
|
+
]
|
|
1896
|
+
});
|
|
1897
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1898
|
+
className: "dsh-channels-cfg",
|
|
1899
|
+
children: [
|
|
1900
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1901
|
+
className: "dsh-channels-cfg__head",
|
|
1902
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1903
|
+
className: "dsh-channels-cfg__title",
|
|
1904
|
+
children: PANEL_COPY.title
|
|
1905
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1906
|
+
className: "dsh-channels-cfg__desc",
|
|
1907
|
+
children: PANEL_COPY.description
|
|
1908
|
+
})]
|
|
1909
|
+
}),
|
|
1910
|
+
currentStatus === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusCard, {
|
|
1911
|
+
status: currentStatus,
|
|
1912
|
+
pollError,
|
|
1913
|
+
busy,
|
|
1914
|
+
onRefresh: () => {
|
|
1915
|
+
refreshStatus();
|
|
1916
|
+
},
|
|
1917
|
+
onReconnect: () => {
|
|
1918
|
+
onReconnect();
|
|
1919
|
+
}
|
|
1920
|
+
}),
|
|
1921
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1922
|
+
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${notice.kind === "ok" ? "ok" : "error"}`,
|
|
1923
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice.text })
|
|
1924
|
+
}),
|
|
1925
|
+
panel === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsCard, {
|
|
1926
|
+
panel,
|
|
1927
|
+
appId: appIdInput,
|
|
1928
|
+
appSecret: appSecretInput,
|
|
1929
|
+
issues,
|
|
1930
|
+
busy,
|
|
1931
|
+
onAppIdChange: setAppIdInput,
|
|
1932
|
+
onAppSecretChange: setAppSecretInput,
|
|
1933
|
+
onSave: () => {
|
|
1934
|
+
onSaveCredentials();
|
|
1935
|
+
},
|
|
1936
|
+
onClear: (field) => {
|
|
1937
|
+
onClearCredential(field);
|
|
1938
|
+
}
|
|
1939
|
+
}),
|
|
1940
|
+
panel === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1941
|
+
panel.config.restartPendingKeys.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1942
|
+
className: "dsh-channels-cfg__banner",
|
|
1943
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.restartBanner, panel.config.restartPendingKeys.map((key) => fieldLabel(panel.config.fields, key)).join("、")] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1944
|
+
type: "button",
|
|
1945
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1946
|
+
onClick: () => {
|
|
1947
|
+
onReconnect();
|
|
1948
|
+
},
|
|
1949
|
+
disabled: busy !== null,
|
|
1950
|
+
children: busy === "reconnect" ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect
|
|
1951
|
+
})]
|
|
1952
|
+
}),
|
|
1953
|
+
modelEntries.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1954
|
+
className: "dsh-channels-cfg__card",
|
|
1955
|
+
children: [
|
|
1956
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1957
|
+
className: "dsh-channels-cfg__cardHead",
|
|
1958
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1959
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
1960
|
+
children: PANEL_COPY.modelCard
|
|
1961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1962
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
1963
|
+
children: catalogBusy ? PANEL_COPY.catalogLoading : "选项来自宿主机目录"
|
|
1964
|
+
})]
|
|
1965
|
+
}),
|
|
1966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1967
|
+
className: "dsh-channels-cfg__help",
|
|
1968
|
+
children: PANEL_COPY.modelCardHelp
|
|
1969
|
+
}),
|
|
1970
|
+
modelEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1971
|
+
className: "dsh-channels-cfg__grid",
|
|
1972
|
+
children: entry.fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
|
|
1973
|
+
field,
|
|
1974
|
+
value: draft[field.key] ?? effectiveValue(panel, field.key),
|
|
1975
|
+
issues,
|
|
1976
|
+
onChange: updateDraft
|
|
1977
|
+
}, field.key))
|
|
1978
|
+
}, entry.group)),
|
|
1979
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1980
|
+
className: "dsh-channels-cfg__footer",
|
|
1981
|
+
children: [
|
|
1982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1983
|
+
type: "button",
|
|
1984
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1985
|
+
onClick: () => {
|
|
1986
|
+
onSave("model");
|
|
1987
|
+
},
|
|
1988
|
+
disabled: !modelDirty || busy !== null,
|
|
1989
|
+
children: busy === "save" ? PANEL_COPY.saving : PANEL_COPY.saveModel
|
|
1990
|
+
}),
|
|
1991
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
1992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1993
|
+
className: "dsh-channels-cfg__help",
|
|
1994
|
+
children: modelDirty ? "有未保存的模型修改" : PANEL_COPY.modelActionsHint
|
|
1995
|
+
})
|
|
1996
|
+
]
|
|
1997
|
+
})
|
|
1998
|
+
]
|
|
1999
|
+
}),
|
|
2000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2001
|
+
className: "dsh-channels-cfg__card",
|
|
2002
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2003
|
+
className: "dsh-channels-cfg__cardHead",
|
|
2004
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2005
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
2006
|
+
children: PANEL_COPY.configCard
|
|
2007
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2008
|
+
className: "dsh-channels-cfg__cardMeta",
|
|
2009
|
+
children: panel.config.overriddenKeys.length === 0 ? PANEL_COPY.inherited : `${PANEL_COPY.overridden} ${panel.config.overriddenKeys.length} 项`
|
|
2010
|
+
})]
|
|
2011
|
+
}), configEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2012
|
+
className: "dsh-channels-cfg__cardHead",
|
|
2013
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2014
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
2015
|
+
children: entry.label
|
|
2016
|
+
})
|
|
2017
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2018
|
+
className: "dsh-channels-cfg__grid",
|
|
2019
|
+
children: entry.fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
|
|
2020
|
+
field,
|
|
2021
|
+
value: draft[field.key] ?? effectiveValue(panel, field.key),
|
|
2022
|
+
issues,
|
|
2023
|
+
onChange: updateDraft
|
|
2024
|
+
}, field.key))
|
|
2025
|
+
})] }, entry.group))]
|
|
2026
|
+
}),
|
|
2027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2028
|
+
className: "dsh-channels-cfg__footer",
|
|
2029
|
+
children: [
|
|
2030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2031
|
+
type: "button",
|
|
2032
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
2033
|
+
onClick: () => {
|
|
2034
|
+
onSave();
|
|
2035
|
+
},
|
|
2036
|
+
disabled: !dirty || busy !== null,
|
|
2037
|
+
children: busy === "save" ? PANEL_COPY.saving : PANEL_COPY.saveAll
|
|
2038
|
+
}),
|
|
2039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2040
|
+
type: "button",
|
|
2041
|
+
className: "dsh-channels-cfg__btn",
|
|
2042
|
+
onClick: () => {
|
|
2043
|
+
onClear();
|
|
2044
|
+
},
|
|
2045
|
+
disabled: panel.config.overriddenKeys.length === 0 || busy !== null,
|
|
2046
|
+
title: panel.config.overriddenKeys.length === 0 ? PANEL_COPY.noOverrides : void 0,
|
|
2047
|
+
children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
|
|
2048
|
+
}),
|
|
2049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-channels-cfg__spacer" }),
|
|
2050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2051
|
+
className: "dsh-channels-cfg__help",
|
|
2052
|
+
children: dirty ? "有未保存的修改" : "只保存改动过的字段"
|
|
2053
|
+
})
|
|
2054
|
+
]
|
|
2055
|
+
}),
|
|
2056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CleanupCard, {
|
|
2057
|
+
panel,
|
|
2058
|
+
cleanup: panel.cleanup,
|
|
2059
|
+
busy,
|
|
2060
|
+
result: cleanupResult,
|
|
2061
|
+
error: cleanupError,
|
|
2062
|
+
onRun: () => {
|
|
2063
|
+
onRunCleanup();
|
|
2064
|
+
}
|
|
2065
|
+
}),
|
|
2066
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessCard, {
|
|
2067
|
+
panel,
|
|
2068
|
+
busy,
|
|
2069
|
+
notice: accessNotice,
|
|
2070
|
+
error: accessError,
|
|
2071
|
+
onAddGroup: onAddAccessGroup,
|
|
2072
|
+
onRemoveGroup: (chatId) => {
|
|
2073
|
+
onRemoveAccessGroup(chatId);
|
|
2074
|
+
},
|
|
2075
|
+
onAddUser: onAddAccessUser,
|
|
2076
|
+
onRemoveUser: (openId) => {
|
|
2077
|
+
onRemoveAccessUser(openId);
|
|
2078
|
+
},
|
|
2079
|
+
onResolveRequest: (token, decision) => {
|
|
2080
|
+
onResolveAccessRequest(token, decision);
|
|
2081
|
+
}
|
|
2082
|
+
}),
|
|
2083
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedCard, { panel })
|
|
2084
|
+
] })
|
|
2085
|
+
]
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
//#endregion
|
|
2089
|
+
//#region src/client/styles.ts
|
|
2090
|
+
/**
|
|
2091
|
+
* Settings panel styling. DSH semantic tokens (`--dsw-alias-*`) with neutral
|
|
2092
|
+
* fallbacks so the page follows the active theme; only pseudo-classes and
|
|
2093
|
+
* responsive rules live here (everything else is inline in the component).
|
|
2094
|
+
*
|
|
2095
|
+
* @module dsh-channels/client/styles
|
|
2096
|
+
*/
|
|
2097
|
+
const STYLE_ATTRIBUTE = "data-dsh-channels-css";
|
|
2098
|
+
const CSS = `
|
|
2099
|
+
.dsh-channels-cfg{container-type:inline-size;display:flex;flex-direction:column;gap:12px;width:100%;max-width:720px;color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:20px}
|
|
2100
|
+
.dsh-channels-cfg__head{display:flex;flex-direction:column;gap:4px}
|
|
2101
|
+
.dsh-channels-cfg__title{margin:0;font-size:16px;font-weight:500;line-height:24px}
|
|
2102
|
+
.dsh-channels-cfg__desc{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:18px}
|
|
2103
|
+
.dsh-channels-cfg__card{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:12px}
|
|
2104
|
+
.dsh-channels-cfg__cardHead{display:flex;align-items:center;gap:8px;min-height:20px;flex-wrap:wrap}
|
|
2105
|
+
.dsh-channels-cfg__cardTitle{font-size:13px;font-weight:500}
|
|
2106
|
+
.dsh-channels-cfg__cardMeta{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
2107
|
+
.dsh-channels-cfg__statusRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2108
|
+
.dsh-channels-cfg__badge{display:inline-flex;align-items:center;gap:5px;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));font-size:12px;line-height:18px;white-space:nowrap}
|
|
2109
|
+
.dsh-channels-cfg__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
|
|
2110
|
+
.dsh-channels-cfg__badge--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);border-color:currentColor}
|
|
2111
|
+
.dsh-channels-cfg__badge--warn{color:var(--dsw-alias-state-warning-primary,#a15c00);border-color:currentColor}
|
|
2112
|
+
.dsh-channels-cfg__badge--bad{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
2113
|
+
.dsh-channels-cfg__badge--muted{color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
2114
|
+
.dsh-channels-cfg__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 16px}
|
|
2115
|
+
.dsh-channels-cfg__fact{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
2116
|
+
.dsh-channels-cfg__factLabel{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
2117
|
+
.dsh-channels-cfg__factValue{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
2118
|
+
.dsh-channels-cfg__actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2119
|
+
.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}
|
|
2120
|
+
.dsh-channels-cfg__btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
|
|
2121
|
+
.dsh-channels-cfg__btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0a67d3);outline-offset:2px}
|
|
2122
|
+
.dsh-channels-cfg__btn:disabled{color:var(--dsw-alias-label-dimmed,#b0b4ba);cursor:default}
|
|
2123
|
+
.dsh-channels-cfg__btn--primary{background:var(--dsw-alias-button-primary-fill,#0a67d3);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
2124
|
+
.dsh-channels-cfg__btn--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#0b5cba)}
|
|
2125
|
+
.dsh-channels-cfg__btn--danger{color:var(--dsw-alias-state-danger-primary,#c62828);border-color:currentColor}
|
|
2126
|
+
.dsh-channels-cfg__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px;align-items:start}
|
|
2127
|
+
.dsh-channels-cfg__field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
2128
|
+
.dsh-channels-cfg__field--wide{grid-column:1/-1}
|
|
2129
|
+
.dsh-channels-cfg__labelRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
2130
|
+
.dsh-channels-cfg__label{font-size:12px;color:var(--dsw-alias-label-secondary,#5c6169)}
|
|
2131
|
+
.dsh-channels-cfg__tag{display:inline-flex;align-items:center;padding:0 5px;border-radius:4px;border:1px dashed currentColor;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a8f98);white-space:nowrap}
|
|
2132
|
+
.dsh-channels-cfg__tag--override{color:var(--dsw-alias-state-business-primary,#0a67d3);border-style:solid}
|
|
2133
|
+
.dsh-channels-cfg__tag--restart{color:var(--dsw-alias-state-warning-primary,#a15c00)}
|
|
2134
|
+
.dsh-channels-cfg__help{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px}
|
|
2135
|
+
.dsh-channels-cfg__reasonBad{color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2136
|
+
.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}
|
|
2137
|
+
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
2138
|
+
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2139
|
+
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
2140
|
+
.dsh-channels-cfg__check{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;font-size:13px}
|
|
2141
|
+
.dsh-channels-cfg__checkbox{width:15px;height:15px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0a67d3);cursor:pointer}
|
|
2142
|
+
.dsh-channels-cfg__fieldError{color:var(--dsw-alias-state-danger-primary,#c62828);font-size:11px;line-height:16px}
|
|
2143
|
+
.dsh-channels-cfg__notice{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:18px}
|
|
2144
|
+
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
2145
|
+
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
2146
|
+
.dsh-channels-cfg__banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:8px;border:1px dashed var(--dsw-alias-state-warning-primary,#a15c00);color:var(--dsw-alias-state-warning-primary,#a15c00);font-size:12px;line-height:18px}
|
|
2147
|
+
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
2148
|
+
.dsh-channels-cfg__spacer{flex:1}
|
|
2149
|
+
.dsh-channels-cfg__readOnly{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 20px}
|
|
2150
|
+
.dsh-channels-cfg__kv{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
2151
|
+
.dsh-channels-cfg__kvKey{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px;line-height:16px}
|
|
2152
|
+
.dsh-channels-cfg__kvValue{overflow-wrap:anywhere}
|
|
2153
|
+
.dsh-channels-cfg__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
2154
|
+
.dsh-channels-cfg__creds{display:flex;flex-direction:column;gap:6px}
|
|
2155
|
+
.dsh-channels-cfg__cred{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2156
|
+
.dsh-channels-cfg__cred .dsh-channels-cfg__input{flex:1 1 240px;min-width:0;width:auto}
|
|
2157
|
+
.dsh-channels-cfg__accessSection{display:flex;flex-direction:column;gap:6px}
|
|
2158
|
+
.dsh-channels-cfg__accessList{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
|
|
2159
|
+
.dsh-channels-cfg__accessRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px}
|
|
2160
|
+
.dsh-channels-cfg__accessMain{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1 1 200px;overflow-wrap:anywhere}
|
|
2161
|
+
.dsh-channels-cfg__accessMeta{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11px;line-height:16px;overflow-wrap:anywhere}
|
|
2162
|
+
.dsh-channels-cfg__accessActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
2163
|
+
.dsh-channels-cfg__accessAdd{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2164
|
+
.dsh-channels-cfg__accessInput{flex:1 1 140px;min-width:0;width:auto}
|
|
2165
|
+
.dsh-channels-cfg__error{padding:10px 12px;border:1px solid var(--dsw-alias-state-danger-primary,#c62828);border-radius:10px;color:var(--dsw-alias-state-danger-primary,#c62828);font-size:12px;line-height:18px;display:flex;flex-direction:column;gap:8px}
|
|
2166
|
+
.dsh-channels-cfg__loading{padding:16px 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:12px}
|
|
2167
|
+
.dsh-channels-cfg__issues{margin:0;padding-left:18px}
|
|
2168
|
+
@container (max-width:560px){
|
|
2169
|
+
.dsh-channels-cfg__facts{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
2170
|
+
.dsh-channels-cfg__grid,.dsh-channels-cfg__readOnly{grid-template-columns:minmax(0,1fr)}
|
|
2171
|
+
}
|
|
2172
|
+
`;
|
|
2173
|
+
/** Append the panel stylesheet once (idempotent across HMR / remounts). */
|
|
2174
|
+
function installStyles() {
|
|
2175
|
+
if (document.querySelector(`style[${STYLE_ATTRIBUTE}]`) !== null) return () => {};
|
|
2176
|
+
const style = document.createElement("style");
|
|
2177
|
+
style.setAttribute(STYLE_ATTRIBUTE, "");
|
|
2178
|
+
style.textContent = CSS;
|
|
2179
|
+
document.head.append(style);
|
|
2180
|
+
return () => {
|
|
2181
|
+
style.remove();
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
//#endregion
|
|
2185
|
+
//#region src/client/index.ts
|
|
2186
|
+
/**
|
|
2187
|
+
* dsh-channels client half: 设置 →「飞书渠道」分区。
|
|
2188
|
+
*
|
|
2189
|
+
* 只依赖平台核心的 `slots` 服务与 react(bundle 里 react 走平台模块表,
|
|
2190
|
+
* 其余全部内联)。页面数据来自 Host 半侧的 `/dsh-channels/*` 路由,因此
|
|
2191
|
+
* 不需要 typert 契约,也不 import 任何 DSH 客户端包。
|
|
2192
|
+
*
|
|
2193
|
+
* @module dsh-channels/client
|
|
2194
|
+
*/
|
|
2195
|
+
/** Cordis client plugin name. */
|
|
2196
|
+
const name = "dsh-channels";
|
|
2197
|
+
/** Required client services: the Slots registry is part of the web shell core. */
|
|
2198
|
+
const inject = ["slots"];
|
|
2199
|
+
/** Section id and nav order (built-ins: account -10, general 0, models 10, plugins 15, agent-presets 20). */
|
|
2200
|
+
const SETTINGS_SECTION_ID = "feishu-channels";
|
|
2201
|
+
const SETTINGS_SECTION_ORDER = 40;
|
|
2202
|
+
/**
|
|
2203
|
+
* Mount the settings section and its stylesheet.
|
|
2204
|
+
* @param ctx - client plugin context carrying the slots registry.
|
|
2205
|
+
*/
|
|
2206
|
+
function apply(ctx) {
|
|
2207
|
+
if (typeof ctx.effect === "function") ctx.effect(() => installStyles(), "dsh-channels: settings styles");
|
|
2208
|
+
else installStyles();
|
|
2209
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2210
|
+
name: "settings.section",
|
|
2211
|
+
id: SETTINGS_SECTION_ID,
|
|
2212
|
+
order: 40,
|
|
2213
|
+
label: () => PANEL_COPY.title
|
|
2214
|
+
}, FeishuChannelsSection));
|
|
2215
|
+
}
|
|
2216
|
+
//#endregion
|
|
2217
|
+
exports.SETTINGS_SECTION_ID = SETTINGS_SECTION_ID;
|
|
2218
|
+
exports.SETTINGS_SECTION_ORDER = SETTINGS_SECTION_ORDER;
|
|
2219
|
+
exports.apply = apply;
|
|
2220
|
+
exports.inject = inject;
|
|
2221
|
+
exports.name = name;
|
|
2222
|
+
return module.exports;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
//# sourceMappingURL=client.js.map
|