@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.2
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 +83 -57
- package/docs/access-control-design.md +8 -5
- package/docs/attachment-pipeline-v2.md +33 -41
- package/docs/feishu-onboarding.md +36 -33
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +1 -1
- package/docs/workspace-isolation-design.md +7 -8
- package/lib/client.js +444 -288
- package/lib/client.js.map +1 -1
- package/lib/index.js +9017 -6794
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +43 -13
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +108 -0
- package/src/cards/streaming.ts +56 -7
- package/src/cards/wiki.ts +124 -0
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +96 -24
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/string-list.ts +139 -0
- package/src/client/styles.ts +5 -0
- package/src/commands/registry.ts +8 -5
- package/src/config.ts +55 -54
- package/src/domain.ts +36 -123
- package/src/feishu/delivery.ts +34 -1
- package/src/feishu/inbound-attachments.ts +1 -1
- package/src/feishu/manifest.ts +8 -31
- package/src/feishu/message-read.ts +97 -24
- package/src/feishu/scopes.ts +194 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +72 -112
- package/src/feishu/wiki/api.ts +346 -0
- package/src/feishu/wiki/consent.ts +183 -0
- package/src/feishu/wiki/links.ts +122 -0
- package/src/feishu/wiki/scope.ts +66 -0
- package/src/feishu/wiki/tools.ts +984 -0
- package/src/feishu-channel.ts +280 -9
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +73 -106
- package/src/service.ts +545 -440
- package/src/session/manager.ts +1 -1
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +110 -89
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +144 -94
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +21 -29
- package/src/cleanup.ts +0 -546
- package/src/client/panel/cleanup-card.tsx +0 -106
- package/src/feishu/pending-attachments.ts +0 -562
package/lib/client.js
CHANGED
|
@@ -28,11 +28,9 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const RECONNECT_ROUTE = "/dsh-channels/reconnect";
|
|
29
29
|
/** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
|
|
30
30
|
const CATALOG_ROUTE = "/dsh-channels/catalog";
|
|
31
|
-
/** 手动执行一次过期目录清理(POST,无请求体)。 */
|
|
32
|
-
const CLEANUP_ROUTE = "/dsh-channels/cleanup";
|
|
33
31
|
/** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
|
|
34
32
|
const ACCESS_GROUPS_ROUTE = "/dsh-channels/access/groups";
|
|
35
|
-
/** 用户白名单增删(POST/DELETE,body `{
|
|
33
|
+
/** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
|
|
36
34
|
const ACCESS_USERS_ROUTE = "/dsh-channels/access/users";
|
|
37
35
|
/** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
|
|
38
36
|
const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
|
|
@@ -116,6 +114,21 @@ window.__ModuleLoader__.load({
|
|
|
116
114
|
statusSummaryConnecting: "正在连接飞书长连接…",
|
|
117
115
|
statusSummaryError: "最近一次连接飞书失败,请检查凭据与网络后重试。",
|
|
118
116
|
lastError: "最近一次连接失败",
|
|
117
|
+
scopeCheckTitle: "权限自检",
|
|
118
|
+
scopeCheckOk: "已开通插件需要的全部飞书权限。",
|
|
119
|
+
scopeCheckMissing: "以下飞书权限未开通,开通后需在开发者后台发布新版本:",
|
|
120
|
+
scopeCheckOpenAll: "在开发者后台开通全部缺失权限",
|
|
121
|
+
scopeCheckOpenOne: "去开通",
|
|
122
|
+
scopeCheckOptional: "可选",
|
|
123
|
+
scopeCheckRequired: "必需",
|
|
124
|
+
scopeCheckError: "无法读取应用的权限授予状态:",
|
|
125
|
+
slashCommandSyncTitle: "斜杠指令面板",
|
|
126
|
+
slashCommandSyncOk: "插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。",
|
|
127
|
+
slashCommandSyncTotal: "命令数",
|
|
128
|
+
slashCommandSyncCreated: "本次新增",
|
|
129
|
+
slashCommandSyncUpdated: "本次更新",
|
|
130
|
+
slashCommandSyncRemoved: "本次删除",
|
|
131
|
+
slashCommandSyncError: "同步飞书原生斜杠指令面板失败(不影响直接输入命令):",
|
|
119
132
|
missingConfig: "缺少配置",
|
|
120
133
|
statusHintUnconfigured: "配置完成后插件会自动连接;也可以稍后在本页手动连接。",
|
|
121
134
|
connectionInfo: "连接信息",
|
|
@@ -161,9 +174,9 @@ window.__ModuleLoader__.load({
|
|
|
161
174
|
appSecretPlaceholder: "输入 App Secret",
|
|
162
175
|
credentialsPendingHint: "填写后点「保存凭据」;两项齐全时插件会自动连接飞书。",
|
|
163
176
|
connectionTabHelp: "连接状态与飞书应用凭据。凭据不完整时不会连接飞书。",
|
|
164
|
-
sessionsTabHelp: "
|
|
177
|
+
sessionsTabHelp: "私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。",
|
|
165
178
|
modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
|
|
166
|
-
workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH
|
|
179
|
+
workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。",
|
|
167
180
|
accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
|
|
168
181
|
advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
|
|
169
182
|
save: "保存更改",
|
|
@@ -172,6 +185,12 @@ window.__ModuleLoader__.load({
|
|
|
172
185
|
discardTab: "放弃本页更改",
|
|
173
186
|
saveHintClean: "没有未保存的更改。",
|
|
174
187
|
dirtyCount: "未保存项",
|
|
188
|
+
listAdd: "添加",
|
|
189
|
+
listRemove: "移除",
|
|
190
|
+
listEntry: "第 {n} 项",
|
|
191
|
+
listTooMany: "最多 {max} 项(空输入框不计)",
|
|
192
|
+
listNotArray: "必须是字符串列表",
|
|
193
|
+
listEntryNotString: "必须是字符串",
|
|
175
194
|
browseDirectory: "选择目录…",
|
|
176
195
|
browsingDirectory: "选择中…",
|
|
177
196
|
directoryPicked: "已选择目录",
|
|
@@ -209,35 +228,11 @@ window.__ModuleLoader__.load({
|
|
|
209
228
|
advancedCard: "只读与高级信息",
|
|
210
229
|
advancedHelp: "这些值来自 profile 配置,不能在面板里编辑。",
|
|
211
230
|
readOnlyCard: "部署级参数",
|
|
212
|
-
cleanupCard: "目录清理",
|
|
213
|
-
cleanupCardHelp: "只清理工作区与 Agent 产物:删除超过保留期且不再活跃的飞书话题目录、旧版本按话题建立的工作区目录,以及这些目录内 Agent 产生的文件;不影响 Session 附件——入站图片/文件默认存于 DSH 原生附件库,不随目录清理删除。用户工作区本身与 DSH 会话日志也不会删除。删除不可恢复:相关旧话题会失效并提示已过期。",
|
|
214
|
-
cleanupRetention: "保留期",
|
|
215
|
-
cleanupAutoLabel: "自动清理",
|
|
216
|
-
cleanupCandidates: "可清理目录",
|
|
217
|
-
cleanupWorkspaces: "插件工作区",
|
|
218
|
-
cleanupCandidateBytes: "预计释放空间",
|
|
219
|
-
cleanupRetentionOff: "关闭(不删除)",
|
|
220
|
-
cleanupAutoOn: "已开启(启动时 + 每天一次)",
|
|
221
|
-
cleanupAutoOff: "已关闭(仅手动执行)",
|
|
222
|
-
cleanupAutoInactive: "未执行(保留期为关闭)",
|
|
223
|
-
cleanupNow: "立即清理",
|
|
224
|
-
cleanupRunning: "清理中…",
|
|
225
|
-
cleanupConfirm: "确定清理过期目录吗?只清理工作区与 Agent 产物,不影响 Session 附件;但这些目录内未提交的文件会被删除,且操作不可恢复。",
|
|
226
|
-
cleanupConfirmHint: "删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。",
|
|
227
|
-
cleanupDisabledOff: "保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。",
|
|
228
|
-
cleanupDisabledEmpty: "没有超过保留期的过期目录。",
|
|
229
|
-
cleanupPreviewFailed: "无法读取清理预览",
|
|
230
|
-
cleanupDone: "清理完成",
|
|
231
|
-
cleanupFailed: "清理失败",
|
|
232
|
-
cleanupDeleted: "已删除工作区",
|
|
233
|
-
cleanupDeletedDirs: "话题目录",
|
|
234
|
-
cleanupRetired: "已清理话题路由",
|
|
235
|
-
cleanupFreed: "释放空间",
|
|
236
|
-
cleanupFailures: "以下目录未能删除:",
|
|
237
231
|
accessCard: "访问与授权",
|
|
238
|
-
accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的
|
|
232
|
+
accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。",
|
|
239
233
|
accessModeOpenWarning: "当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。",
|
|
240
|
-
accessAdminMissingWarning: "当前准入=guarded
|
|
234
|
+
accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。",
|
|
235
|
+
accessAdminOwnerNotice: "未配置管理员,已自动使用应用所有者作为管理员:",
|
|
241
236
|
accessGroupsTitle: "已授权群",
|
|
242
237
|
accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
|
|
243
238
|
accessUsersTitle: "已授权用户",
|
|
@@ -246,7 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
246
241
|
accessRequestsEmpty: "没有待处理的授权申请。",
|
|
247
242
|
accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
|
|
248
243
|
accessGroupNamePlaceholder: "群名(可选)",
|
|
249
|
-
|
|
244
|
+
accessUserIdPlaceholder: "用户 user_id",
|
|
250
245
|
accessUserNamePlaceholder: "昵称(可选)",
|
|
251
246
|
accessAddGroup: "添加群",
|
|
252
247
|
accessAddUser: "添加用户",
|
|
@@ -268,7 +263,7 @@ window.__ModuleLoader__.load({
|
|
|
268
263
|
accessRequestAllowed: "已授权",
|
|
269
264
|
accessRequestDenied: "已忽略",
|
|
270
265
|
accessHints: "三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。",
|
|
271
|
-
accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;
|
|
266
|
+
accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。"
|
|
272
267
|
};
|
|
273
268
|
//#endregion
|
|
274
269
|
//#region src/client/api.ts
|
|
@@ -378,24 +373,6 @@ window.__ModuleLoader__.load({
|
|
|
378
373
|
return JSON.parse(text);
|
|
379
374
|
}
|
|
380
375
|
/**
|
|
381
|
-
* Run one workspace cleanup. No request body: the Host derives the candidate
|
|
382
|
-
* set from the effective retention config. Irreversible, so the caller asks
|
|
383
|
-
* for confirmation first; partial failures come back inside `result.failures`.
|
|
384
|
-
*/
|
|
385
|
-
async function runCleanup() {
|
|
386
|
-
const response = await request(CLEANUP_ROUTE, { method: "POST" });
|
|
387
|
-
const text = await response.text();
|
|
388
|
-
if (!response.ok) try {
|
|
389
|
-
return JSON.parse(text);
|
|
390
|
-
} catch {
|
|
391
|
-
return {
|
|
392
|
-
ok: false,
|
|
393
|
-
error: errorFromBody(response.status, text)
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
return JSON.parse(text);
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
376
|
* Ask the Host to open its native directory chooser (the panel's「选择目录…」
|
|
400
377
|
* button). The request stays open while the OS dialog does; a cancel comes back
|
|
401
378
|
* as `{ ok: true, canceled: true }`, not as an error.
|
|
@@ -481,16 +458,16 @@ window.__ModuleLoader__.load({
|
|
|
481
458
|
async function removeAccessGroup(chatId) {
|
|
482
459
|
return mutateAccess(ACCESS_GROUPS_ROUTE, "DELETE", { chatId });
|
|
483
460
|
}
|
|
484
|
-
/** Authorize one user
|
|
485
|
-
async function addAccessUser(
|
|
486
|
-
return mutateAccess(ACCESS_USERS_ROUTE, "POST", name === void 0 || name === "" ? {
|
|
487
|
-
|
|
461
|
+
/** Authorize one user `user_id` (global across authorized groups). */
|
|
462
|
+
async function addAccessUser(userId, name) {
|
|
463
|
+
return mutateAccess(ACCESS_USERS_ROUTE, "POST", name === void 0 || name === "" ? { userId } : {
|
|
464
|
+
userId,
|
|
488
465
|
name
|
|
489
466
|
});
|
|
490
467
|
}
|
|
491
468
|
/** Revoke one user authorization (takes effect on the next message). */
|
|
492
|
-
async function removeAccessUser(
|
|
493
|
-
return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", {
|
|
469
|
+
async function removeAccessUser(userId) {
|
|
470
|
+
return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", { userId });
|
|
494
471
|
}
|
|
495
472
|
/** Settle one pending authorization request (same resolver as the Feishu card). */
|
|
496
473
|
async function resolveAccessRequest(token, decision) {
|
|
@@ -593,6 +570,203 @@ window.__ModuleLoader__.load({
|
|
|
593
570
|
});
|
|
594
571
|
}
|
|
595
572
|
//#endregion
|
|
573
|
+
//#region src/feishu/wiki/links.ts
|
|
574
|
+
/**
|
|
575
|
+
* Feishu wiki (知识库) link parsing: the `wikiRoots` allow-list entries and
|
|
576
|
+
* the document references the model passes to the wiki tools.
|
|
577
|
+
*
|
|
578
|
+
* Pure module: no I/O. Hosts are not checked because tenants use their own
|
|
579
|
+
* subdomains (`<tenant>.feishu.cn`) and the API never uses the link host.
|
|
580
|
+
*
|
|
581
|
+
* @module dsh-channels/feishu/wiki/links
|
|
582
|
+
*/
|
|
583
|
+
/** Feishu wiki space id: decimal digits. */
|
|
584
|
+
const SPACE_ID_PATTERN = /^\d{1,32}$/u;
|
|
585
|
+
/** Feishu wiki node / docx token: letters and digits. */
|
|
586
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9]{8,64}$/u;
|
|
587
|
+
/** Readable reasons for {@link WikiRootLinkError}. */
|
|
588
|
+
const WIKI_ROOT_LINK_ERRORS = {
|
|
589
|
+
"not-a-url": "不是有效的链接",
|
|
590
|
+
"not-https": "必须是 https 链接",
|
|
591
|
+
"not-a-wiki-link": "只支持知识空间链接 https://<租户>.feishu.cn/wiki/space/<space_id> 或节点链接 https://<租户>.feishu.cn/wiki/<node_token>"
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Parse one `wikiRoots` entry.
|
|
595
|
+
*
|
|
596
|
+
* @param link Link as configured (surrounding whitespace is ignored).
|
|
597
|
+
* @returns The root, or the reason it is invalid.
|
|
598
|
+
*/
|
|
599
|
+
function parseWikiRootLink(link) {
|
|
600
|
+
const trimmed = link.trim();
|
|
601
|
+
let url;
|
|
602
|
+
try {
|
|
603
|
+
url = new URL(trimmed);
|
|
604
|
+
} catch {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
error: "not-a-url"
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (url.protocol !== "https:") return {
|
|
611
|
+
ok: false,
|
|
612
|
+
error: "not-https"
|
|
613
|
+
};
|
|
614
|
+
const segments = url.pathname.split("/").filter((segment) => segment !== "");
|
|
615
|
+
if (segments[0] !== "wiki") return {
|
|
616
|
+
ok: false,
|
|
617
|
+
error: "not-a-wiki-link"
|
|
618
|
+
};
|
|
619
|
+
if (segments.length === 3 && segments[1] === "space" && SPACE_ID_PATTERN.test(segments[2])) return {
|
|
620
|
+
ok: true,
|
|
621
|
+
root: {
|
|
622
|
+
kind: "space",
|
|
623
|
+
spaceId: segments[2],
|
|
624
|
+
link: trimmed
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
if (segments.length === 2 && segments[1] !== "space" && TOKEN_PATTERN.test(segments[1])) return {
|
|
628
|
+
ok: true,
|
|
629
|
+
root: {
|
|
630
|
+
kind: "node",
|
|
631
|
+
nodeToken: segments[1],
|
|
632
|
+
link: trimmed
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
error: "not-a-wiki-link"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/client/string-list.ts
|
|
642
|
+
/**
|
|
643
|
+
* 浏览器半区的 `string-list` 字段辅助:把草稿值读成条目数组,并在保存前
|
|
644
|
+
* 逐项校验,供面板即时标红。Host 保存时按同一规则复核(见
|
|
645
|
+
* `settings/surface.ts` 的 `normalizeStringList`)。
|
|
646
|
+
*
|
|
647
|
+
* 不含 JSX/React,便于单测。条目格式校验直接复用纯函数模块
|
|
648
|
+
* `feishu/wiki/links.ts`(无运行时依赖,可内联进 Client bundle)。
|
|
649
|
+
*
|
|
650
|
+
* @module dsh-channels/client/string-list
|
|
651
|
+
*/
|
|
652
|
+
/** Entry checks per item format: the readable reason, or `undefined` when valid. */
|
|
653
|
+
const ITEM_FORMAT_CHECKS = { "wiki-root-link": (entry) => {
|
|
654
|
+
const parsed = parseWikiRootLink(entry);
|
|
655
|
+
return parsed.ok ? void 0 : WIKI_ROOT_LINK_ERRORS[parsed.error];
|
|
656
|
+
} };
|
|
657
|
+
/**
|
|
658
|
+
* Entries of a `string-list` value; any other value reads as no entries.
|
|
659
|
+
*
|
|
660
|
+
* @param value - Draft or effective value of a `string-list` field.
|
|
661
|
+
* @returns The entries as stored (untrimmed).
|
|
662
|
+
*/
|
|
663
|
+
function listItems(value) {
|
|
664
|
+
return typeof value === "object" ? value : [];
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Validate the entries of one `string-list` draft the way the Host will:
|
|
668
|
+
* empty inputs are ignored, every other entry is trimmed and checked against
|
|
669
|
+
* the field's `itemFormat`, and the non-empty count is bounded by `maxItems`.
|
|
670
|
+
*
|
|
671
|
+
* @param field - The Host descriptor (`kind: 'string-list'`).
|
|
672
|
+
* @param items - The draft entries, empty inputs included.
|
|
673
|
+
* @returns Per-entry issues (with `index` into `items`) and a whole-field issue when over `maxItems`.
|
|
674
|
+
*/
|
|
675
|
+
function stringListIssues(field, items) {
|
|
676
|
+
const issues = [];
|
|
677
|
+
const check = field.itemFormat === void 0 ? void 0 : ITEM_FORMAT_CHECKS[field.itemFormat];
|
|
678
|
+
let count = 0;
|
|
679
|
+
items.forEach((item, index) => {
|
|
680
|
+
const entry = item.trim();
|
|
681
|
+
if (entry === "") return;
|
|
682
|
+
count += 1;
|
|
683
|
+
const reason = check?.(entry);
|
|
684
|
+
if (reason === void 0) return;
|
|
685
|
+
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
686
|
+
issues.push({
|
|
687
|
+
key: field.key,
|
|
688
|
+
index,
|
|
689
|
+
message: `${position}「${entry}」:${reason}`
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
if (field.maxItems !== void 0 && count > field.maxItems) issues.push({
|
|
693
|
+
key: field.key,
|
|
694
|
+
message: PANEL_COPY.listTooMany.replace("{max}", String(field.maxItems))
|
|
695
|
+
});
|
|
696
|
+
return issues;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Turn one `string-list` draft into its save payload. Entries are submitted as
|
|
700
|
+
* typed (untrimmed, empty inputs included) so Host issue indexes point at the
|
|
701
|
+
* same rendered inputs; the Host trims and drops empty inputs.
|
|
702
|
+
*
|
|
703
|
+
* @param field - The Host descriptor (`kind: 'string-list'`).
|
|
704
|
+
* @param raw - The draft value.
|
|
705
|
+
* @returns The payload entries, or the issues that block the save.
|
|
706
|
+
*/
|
|
707
|
+
function stringListDraftValue(field, raw) {
|
|
708
|
+
const items = listItems(raw);
|
|
709
|
+
const issues = stringListIssues(field, items);
|
|
710
|
+
return issues.length > 0 ? {
|
|
711
|
+
ok: false,
|
|
712
|
+
issues
|
|
713
|
+
} : {
|
|
714
|
+
ok: true,
|
|
715
|
+
value: [...items]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Inputs the list control renders: one per entry, or one blank input for an
|
|
720
|
+
* empty list so the first entry can be typed directly.
|
|
721
|
+
*
|
|
722
|
+
* @param value - Draft or effective value.
|
|
723
|
+
* @returns The input values, in order.
|
|
724
|
+
*/
|
|
725
|
+
function listRows(value) {
|
|
726
|
+
const items = listItems(value);
|
|
727
|
+
return items.length === 0 ? [""] : items;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Replace the entry at one input position.
|
|
731
|
+
*
|
|
732
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
733
|
+
* @param index - Position of the edited input.
|
|
734
|
+
* @param entry - The typed value.
|
|
735
|
+
* @returns The next draft entries.
|
|
736
|
+
*/
|
|
737
|
+
function withListEntry(rows, index, entry) {
|
|
738
|
+
return rows.map((item, position) => position === index ? entry : item);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Drop the entry at one input position.
|
|
742
|
+
*
|
|
743
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
744
|
+
* @param index - Position of the removed input.
|
|
745
|
+
* @returns The next draft entries.
|
|
746
|
+
*/
|
|
747
|
+
function withoutListEntry(rows, index) {
|
|
748
|
+
return rows.filter((_, position) => position !== index);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Append one blank input.
|
|
752
|
+
*
|
|
753
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
754
|
+
* @returns The next draft entries.
|
|
755
|
+
*/
|
|
756
|
+
function withBlankListEntry(rows) {
|
|
757
|
+
return [...rows, ""];
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Whether the add button is disabled: the inputs already reach `maxItems`.
|
|
761
|
+
*
|
|
762
|
+
* @param field - The Host descriptor.
|
|
763
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
764
|
+
* @returns `true` when no further input may be added.
|
|
765
|
+
*/
|
|
766
|
+
function listIsFull(field, rows) {
|
|
767
|
+
return field.maxItems !== void 0 && rows.length >= field.maxItems;
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
596
770
|
//#region src/client/panel/shared.tsx
|
|
597
771
|
/**
|
|
598
772
|
* 设置面板共享层:页面状态类型、Host 字段渲染原语(FieldRow / FieldsCard /
|
|
@@ -606,25 +780,6 @@ window.__ModuleLoader__.load({
|
|
|
606
780
|
function messageOf(error) {
|
|
607
781
|
return error instanceof Error ? error.message : String(error);
|
|
608
782
|
}
|
|
609
|
-
/** Human-readable byte size for the cleanup preview/result (base-2 units). */
|
|
610
|
-
function formatBytes(bytes) {
|
|
611
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
612
|
-
const units = [
|
|
613
|
-
"B",
|
|
614
|
-
"KB",
|
|
615
|
-
"MB",
|
|
616
|
-
"GB",
|
|
617
|
-
"TB"
|
|
618
|
-
];
|
|
619
|
-
let value = bytes;
|
|
620
|
-
let unit = 0;
|
|
621
|
-
while (value >= 1024 && unit < units.length - 1) {
|
|
622
|
-
value /= 1024;
|
|
623
|
-
unit += 1;
|
|
624
|
-
}
|
|
625
|
-
const digits = unit === 0 || value >= 10 ? 0 : 1;
|
|
626
|
-
return `${value.toFixed(digits)} ${units[unit] ?? "B"}`;
|
|
627
|
-
}
|
|
628
783
|
/** Compact duration for the connection card. */
|
|
629
784
|
function formatUptime(ms) {
|
|
630
785
|
if (ms === void 0 || !Number.isFinite(ms)) return PANEL_COPY.unknownValue;
|
|
@@ -697,6 +852,12 @@ window.__ModuleLoader__.load({
|
|
|
697
852
|
values[field.key] = raw === true;
|
|
698
853
|
continue;
|
|
699
854
|
}
|
|
855
|
+
if (field.kind === "string-list") {
|
|
856
|
+
const list = stringListDraftValue(field, raw);
|
|
857
|
+
if (list.ok) values[field.key] = list.value;
|
|
858
|
+
else issues.push(...list.issues);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
700
861
|
values[field.key] = String(raw ?? "");
|
|
701
862
|
}
|
|
702
863
|
return {
|
|
@@ -776,10 +937,61 @@ window.__ModuleLoader__.load({
|
|
|
776
937
|
})]
|
|
777
938
|
});
|
|
778
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* `string-list` control: one single-line input per entry, each with a remove
|
|
942
|
+
* button, and an add button below (disabled at `maxItems` inputs). An empty
|
|
943
|
+
* list renders one blank input so the first entry can be typed directly.
|
|
944
|
+
* Issues carrying an `index` mark that input and show the reason under it.
|
|
945
|
+
*/
|
|
946
|
+
function StringListInput(props) {
|
|
947
|
+
const { field, value, issues, disabled, onChange } = props;
|
|
948
|
+
const empty = listItems(value).length === 0;
|
|
949
|
+
const rows = listRows(value);
|
|
950
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
951
|
+
className: "dsh-channels-cfg__list",
|
|
952
|
+
children: [rows.map((item, index) => {
|
|
953
|
+
const issue = issues.find((entry) => entry.index === index);
|
|
954
|
+
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
955
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
956
|
+
className: "dsh-channels-cfg__listItem",
|
|
957
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
958
|
+
className: "dsh-channels-cfg__inputRow",
|
|
959
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
960
|
+
type: "text",
|
|
961
|
+
className: `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`,
|
|
962
|
+
value: item,
|
|
963
|
+
placeholder: field.placeholder,
|
|
964
|
+
"aria-label": `${field.label} ${position}`,
|
|
965
|
+
"aria-invalid": issue === void 0 ? void 0 : true,
|
|
966
|
+
disabled,
|
|
967
|
+
onChange: (event) => onChange(field.key, withListEntry(rows, index, event.target.value))
|
|
968
|
+
}), empty ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
969
|
+
type: "button",
|
|
970
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--listRemove",
|
|
971
|
+
"aria-label": `${PANEL_COPY.listRemove} ${position}`,
|
|
972
|
+
disabled,
|
|
973
|
+
onClick: () => onChange(field.key, withoutListEntry(rows, index)),
|
|
974
|
+
children: PANEL_COPY.listRemove
|
|
975
|
+
})]
|
|
976
|
+
}), issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
977
|
+
className: "dsh-channels-cfg__fieldError",
|
|
978
|
+
children: issue.message
|
|
979
|
+
})]
|
|
980
|
+
}, index);
|
|
981
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
982
|
+
type: "button",
|
|
983
|
+
className: "dsh-channels-cfg__btn",
|
|
984
|
+
disabled: disabled || listIsFull(field, rows),
|
|
985
|
+
onClick: () => onChange(field.key, withBlankListEntry(rows)),
|
|
986
|
+
children: PANEL_COPY.listAdd
|
|
987
|
+
}) })]
|
|
988
|
+
});
|
|
989
|
+
}
|
|
779
990
|
/** One generic form control driven by the Host field descriptor. */
|
|
780
991
|
function FieldRow(props) {
|
|
781
992
|
const { field, value, issues, disabled = false, onChange, onPickDirectory } = props;
|
|
782
|
-
const issue = issues.find((item) => item.key === field.key);
|
|
993
|
+
const issue = issues.find((item) => item.key === field.key && item.index === void 0);
|
|
994
|
+
const entryIssues = issues.filter((item) => item.key === field.key && item.index !== void 0);
|
|
783
995
|
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
784
996
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
785
997
|
className: `dsh-channels-cfg__field${field.wide === true ? " dsh-channels-cfg__field--wide" : ""}`,
|
|
@@ -843,6 +1055,13 @@ window.__ModuleLoader__.load({
|
|
|
843
1055
|
disabled,
|
|
844
1056
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
845
1057
|
}) : null,
|
|
1058
|
+
field.kind === "string-list" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1059
|
+
field,
|
|
1060
|
+
value,
|
|
1061
|
+
issues: entryIssues,
|
|
1062
|
+
disabled,
|
|
1063
|
+
onChange
|
|
1064
|
+
}) : null,
|
|
846
1065
|
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
847
1066
|
className: "dsh-channels-cfg__help",
|
|
848
1067
|
children: field.help
|
|
@@ -995,10 +1214,6 @@ window.__ModuleLoader__.load({
|
|
|
995
1214
|
};
|
|
996
1215
|
}
|
|
997
1216
|
}
|
|
998
|
-
/** Retention label, reusing the Host field options so the copy stays on the Host. */
|
|
999
|
-
function retentionLabel(panel, cleanup) {
|
|
1000
|
-
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);
|
|
1001
|
-
}
|
|
1002
1217
|
//#endregion
|
|
1003
1218
|
//#region src/client/panel/access-card.tsx
|
|
1004
1219
|
/**
|
|
@@ -1030,7 +1245,7 @@ window.__ModuleLoader__.load({
|
|
|
1030
1245
|
const { access } = panel;
|
|
1031
1246
|
const [groupChatId, setGroupChatId] = (0, react.useState)("");
|
|
1032
1247
|
const [groupName, setGroupName] = (0, react.useState)("");
|
|
1033
|
-
const [
|
|
1248
|
+
const [userUserId, setUserUserId] = (0, react.useState)("");
|
|
1034
1249
|
const [userName, setUserName] = (0, react.useState)("");
|
|
1035
1250
|
const groups = access.groups;
|
|
1036
1251
|
const users = access.users;
|
|
@@ -1045,9 +1260,9 @@ window.__ModuleLoader__.load({
|
|
|
1045
1260
|
}
|
|
1046
1261
|
};
|
|
1047
1262
|
const submitUser = async () => {
|
|
1048
|
-
if (
|
|
1049
|
-
if (await onAddUser(
|
|
1050
|
-
|
|
1263
|
+
if (userUserId.trim() === "") return;
|
|
1264
|
+
if (await onAddUser(userUserId, userName)) {
|
|
1265
|
+
setUserUserId("");
|
|
1051
1266
|
setUserName("");
|
|
1052
1267
|
}
|
|
1053
1268
|
};
|
|
@@ -1072,9 +1287,16 @@ window.__ModuleLoader__.load({
|
|
|
1072
1287
|
className: "dsh-channels-cfg__banner",
|
|
1073
1288
|
children: PANEL_COPY.accessModeOpenWarning
|
|
1074
1289
|
}) : null,
|
|
1075
|
-
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1290
|
+
access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1076
1291
|
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
1077
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning })
|
|
1292
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning }), access.admin.ownerLookupError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: access.admin.ownerLookupError })]
|
|
1293
|
+
}) : null,
|
|
1294
|
+
access.mode === "guarded" && access.admin.source === "owner" && access.admin.userId !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1295
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
1296
|
+
children: [PANEL_COPY.accessAdminOwnerNotice, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1297
|
+
className: "dsh-channels-cfg__mono",
|
|
1298
|
+
children: access.admin.userId
|
|
1299
|
+
})]
|
|
1078
1300
|
}) : null,
|
|
1079
1301
|
!access.loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, { text: PANEL_COPY.accessNotLoaded }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1080
1302
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1171,21 +1393,21 @@ window.__ModuleLoader__.load({
|
|
|
1171
1393
|
className: "dsh-channels-cfg__accessRow",
|
|
1172
1394
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1173
1395
|
className: "dsh-channels-cfg__accessMain",
|
|
1174
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.
|
|
1396
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.userId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1175
1397
|
className: "dsh-channels-cfg__accessMeta",
|
|
1176
1398
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1177
1399
|
className: "dsh-channels-cfg__mono",
|
|
1178
|
-
children: user.
|
|
1400
|
+
children: user.userId
|
|
1179
1401
|
}), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
|
|
1180
1402
|
})]
|
|
1181
1403
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1182
1404
|
type: "button",
|
|
1183
1405
|
className: "dsh-channels-cfg__btn",
|
|
1184
1406
|
disabled,
|
|
1185
|
-
onClick: () => onRemoveUser(user.
|
|
1407
|
+
onClick: () => onRemoveUser(user.userId),
|
|
1186
1408
|
children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
|
|
1187
1409
|
})]
|
|
1188
|
-
}, user.
|
|
1410
|
+
}, user.userId))
|
|
1189
1411
|
}),
|
|
1190
1412
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1191
1413
|
className: "dsh-channels-cfg__accessAdd",
|
|
@@ -1193,11 +1415,11 @@ window.__ModuleLoader__.load({
|
|
|
1193
1415
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1194
1416
|
type: "text",
|
|
1195
1417
|
className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
|
|
1196
|
-
value:
|
|
1197
|
-
placeholder: PANEL_COPY.
|
|
1198
|
-
"aria-label": PANEL_COPY.
|
|
1418
|
+
value: userUserId,
|
|
1419
|
+
placeholder: PANEL_COPY.accessUserIdPlaceholder,
|
|
1420
|
+
"aria-label": PANEL_COPY.accessUserIdPlaceholder,
|
|
1199
1421
|
disabled,
|
|
1200
|
-
onChange: (event) =>
|
|
1422
|
+
onChange: (event) => setUserUserId(event.target.value)
|
|
1201
1423
|
}),
|
|
1202
1424
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1203
1425
|
type: "text",
|
|
@@ -1211,7 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1211
1433
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1212
1434
|
type: "button",
|
|
1213
1435
|
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
|
|
1214
|
-
disabled: disabled ||
|
|
1436
|
+
disabled: disabled || userUserId.trim() === "",
|
|
1215
1437
|
onClick: () => {
|
|
1216
1438
|
submitUser();
|
|
1217
1439
|
},
|
|
@@ -1302,7 +1524,7 @@ window.__ModuleLoader__.load({
|
|
|
1302
1524
|
//#endregion
|
|
1303
1525
|
//#region src/client/panel/access-tab.tsx
|
|
1304
1526
|
/**
|
|
1305
|
-
* 「访问授权」tab:准入与授权字段表单(管理员
|
|
1527
|
+
* 「访问授权」tab:准入与授权字段表单(管理员 user_id / 准入模式 /
|
|
1306
1528
|
* 申请有效期)+ 白名单与待处理申请卡。表单保存走全局 draft,白名单
|
|
1307
1529
|
* 增删是独立即时 mutation。
|
|
1308
1530
|
*
|
|
@@ -1587,6 +1809,7 @@ window.__ModuleLoader__.load({
|
|
|
1587
1809
|
* - error:连接信息卡 + 最近一次连接失败 + 「重试」
|
|
1588
1810
|
* - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
|
|
1589
1811
|
* - connecting:禁用按钮显示「连接中…」
|
|
1812
|
+
* - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
|
|
1590
1813
|
*
|
|
1591
1814
|
* @module dsh-channels/client/panel/connection-tab
|
|
1592
1815
|
*/
|
|
@@ -1607,6 +1830,8 @@ window.__ModuleLoader__.load({
|
|
|
1607
1830
|
busy,
|
|
1608
1831
|
onReconnect
|
|
1609
1832
|
}),
|
|
1833
|
+
status.scopeCheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeCheckCard, { check: status.scopeCheck }),
|
|
1834
|
+
status.slashCommandSync === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlashCommandSyncCard, { sync: status.slashCommandSync }),
|
|
1610
1835
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsCard, {
|
|
1611
1836
|
panel,
|
|
1612
1837
|
credentials
|
|
@@ -1797,6 +2022,109 @@ window.__ModuleLoader__.load({
|
|
|
1797
2022
|
]
|
|
1798
2023
|
});
|
|
1799
2024
|
}
|
|
2025
|
+
/** Post-connect scope self-check: missing scopes in red, each with a console deep link. */
|
|
2026
|
+
function ScopeCheckCard(props) {
|
|
2027
|
+
const { check } = props;
|
|
2028
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2029
|
+
className: "dsh-channels-cfg__card",
|
|
2030
|
+
children: [
|
|
2031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2032
|
+
className: "dsh-channels-cfg__cardHead",
|
|
2033
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2034
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
2035
|
+
children: PANEL_COPY.scopeCheckTitle
|
|
2036
|
+
})
|
|
2037
|
+
}),
|
|
2038
|
+
check.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2039
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
2040
|
+
children: PANEL_COPY.scopeCheckOk
|
|
2041
|
+
}) : null,
|
|
2042
|
+
check.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2043
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2044
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2045
|
+
className: "dsh-channels-cfg__mono",
|
|
2046
|
+
children: check.error
|
|
2047
|
+
})]
|
|
2048
|
+
}) : null,
|
|
2049
|
+
check.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2050
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2051
|
+
children: [
|
|
2052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckMissing }),
|
|
2053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2054
|
+
className: "dsh-channels-cfg__scopeList",
|
|
2055
|
+
children: check.missing.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2057
|
+
"[",
|
|
2058
|
+
item.level === "required" ? PANEL_COPY.scopeCheckRequired : PANEL_COPY.scopeCheckOptional,
|
|
2059
|
+
"] "
|
|
2060
|
+
] }),
|
|
2061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2062
|
+
className: "dsh-channels-cfg__mono",
|
|
2063
|
+
children: item.scopes.join(" / ")
|
|
2064
|
+
}),
|
|
2065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2066
|
+
":",
|
|
2067
|
+
item.purpose,
|
|
2068
|
+
" "
|
|
2069
|
+
] }),
|
|
2070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2071
|
+
href: item.consoleUrl,
|
|
2072
|
+
target: "_blank",
|
|
2073
|
+
rel: "noreferrer noopener",
|
|
2074
|
+
children: PANEL_COPY.scopeCheckOpenOne
|
|
2075
|
+
})
|
|
2076
|
+
] }, item.scopes.join(",")))
|
|
2077
|
+
}),
|
|
2078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2079
|
+
href: check.consoleUrl,
|
|
2080
|
+
target: "_blank",
|
|
2081
|
+
rel: "noreferrer noopener",
|
|
2082
|
+
children: PANEL_COPY.scopeCheckOpenAll
|
|
2083
|
+
})
|
|
2084
|
+
]
|
|
2085
|
+
}) : null
|
|
2086
|
+
]
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
/** Post-connect native slash-command panel sync: counts on success, the Feishu reason on failure. */
|
|
2090
|
+
function SlashCommandSyncCard(props) {
|
|
2091
|
+
const { sync } = props;
|
|
2092
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2093
|
+
className: "dsh-channels-cfg__card",
|
|
2094
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2095
|
+
className: "dsh-channels-cfg__cardHead",
|
|
2096
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2097
|
+
className: "dsh-channels-cfg__cardTitle",
|
|
2098
|
+
children: PANEL_COPY.slashCommandSyncTitle
|
|
2099
|
+
})
|
|
2100
|
+
}), sync.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2101
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
|
|
2102
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.slashCommandSyncOk, " "] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2103
|
+
PANEL_COPY.slashCommandSyncTotal,
|
|
2104
|
+
" ",
|
|
2105
|
+
sync.total,
|
|
2106
|
+
" · ",
|
|
2107
|
+
PANEL_COPY.slashCommandSyncCreated,
|
|
2108
|
+
" ",
|
|
2109
|
+
sync.created,
|
|
2110
|
+
" · ",
|
|
2111
|
+
PANEL_COPY.slashCommandSyncUpdated,
|
|
2112
|
+
" ",
|
|
2113
|
+
sync.updated,
|
|
2114
|
+
" · ",
|
|
2115
|
+
PANEL_COPY.slashCommandSyncRemoved,
|
|
2116
|
+
" ",
|
|
2117
|
+
sync.removed
|
|
2118
|
+
] })]
|
|
2119
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2120
|
+
className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
|
|
2121
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.slashCommandSyncError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2122
|
+
className: "dsh-channels-cfg__mono",
|
|
2123
|
+
children: sync.error
|
|
2124
|
+
})]
|
|
2125
|
+
})]
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
1800
2128
|
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
1801
2129
|
function RestartBanner(props) {
|
|
1802
2130
|
const { panel, busy, locked, onReconnect } = props;
|
|
@@ -1875,135 +2203,14 @@ window.__ModuleLoader__.load({
|
|
|
1875
2203
|
});
|
|
1876
2204
|
}
|
|
1877
2205
|
//#endregion
|
|
1878
|
-
//#region src/client/panel/cleanup-card.tsx
|
|
1879
|
-
/**
|
|
1880
|
-
* 目录清理卡:展示清理预览(保留期 / 自动清理 / 候选数量与空间),
|
|
1881
|
-
* 执行前二次确认,结果与部分失败逐条展示。`cleanup.error` 非空时
|
|
1882
|
-
* (例如未连接、无法扫描工作区)按钮天然禁用并显示原因。
|
|
1883
|
-
*
|
|
1884
|
-
* @module dsh-channels/client/panel/cleanup-card
|
|
1885
|
-
*/
|
|
1886
|
-
function CleanupCard(props) {
|
|
1887
|
-
const { panel, cleanup, busy, locked, result, error, onRun } = props;
|
|
1888
|
-
const autoText = cleanup.retention === "off" ? PANEL_COPY.cleanupAutoInactive : cleanup.auto ? PANEL_COPY.cleanupAutoOn : PANEL_COPY.cleanupAutoOff;
|
|
1889
|
-
const blocked = cleanup.error !== void 0 ? `${PANEL_COPY.cleanupPreviewFailed}:${cleanup.error}` : cleanup.retention === "off" ? PANEL_COPY.cleanupDisabledOff : cleanup.candidates === 0 ? PANEL_COPY.cleanupDisabledEmpty : null;
|
|
1890
|
-
const facts = [
|
|
1891
|
-
{
|
|
1892
|
-
label: PANEL_COPY.cleanupRetention,
|
|
1893
|
-
value: retentionLabel(panel, cleanup)
|
|
1894
|
-
},
|
|
1895
|
-
{
|
|
1896
|
-
label: PANEL_COPY.cleanupAutoLabel,
|
|
1897
|
-
value: autoText
|
|
1898
|
-
},
|
|
1899
|
-
{
|
|
1900
|
-
label: PANEL_COPY.cleanupCandidates,
|
|
1901
|
-
value: `${cleanup.candidates} 个`
|
|
1902
|
-
},
|
|
1903
|
-
{
|
|
1904
|
-
label: PANEL_COPY.cleanupWorkspaces,
|
|
1905
|
-
value: `${cleanup.workspaces} 个`
|
|
1906
|
-
},
|
|
1907
|
-
{
|
|
1908
|
-
label: PANEL_COPY.cleanupCandidateBytes,
|
|
1909
|
-
value: formatBytes(cleanup.candidateBytes)
|
|
1910
|
-
}
|
|
1911
|
-
];
|
|
1912
|
-
const running = busy === "cleanup";
|
|
1913
|
-
const failed = result !== null && (!result.ok || error !== null);
|
|
1914
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1915
|
-
className: "dsh-channels-cfg__card",
|
|
1916
|
-
children: [
|
|
1917
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1918
|
-
className: "dsh-channels-cfg__cardHead",
|
|
1919
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1920
|
-
className: "dsh-channels-cfg__cardTitle",
|
|
1921
|
-
children: PANEL_COPY.cleanupCard
|
|
1922
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1923
|
-
className: "dsh-channels-cfg__cardMeta",
|
|
1924
|
-
children: retentionLabel(panel, cleanup)
|
|
1925
|
-
})]
|
|
1926
|
-
}),
|
|
1927
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1928
|
-
className: "dsh-channels-cfg__help",
|
|
1929
|
-
children: PANEL_COPY.cleanupCardHelp
|
|
1930
|
-
}),
|
|
1931
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1932
|
-
className: "dsh-channels-cfg__facts",
|
|
1933
|
-
children: facts.map((fact) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1934
|
-
className: "dsh-channels-cfg__fact",
|
|
1935
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1936
|
-
className: "dsh-channels-cfg__factLabel",
|
|
1937
|
-
children: fact.label
|
|
1938
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1939
|
-
className: "dsh-channels-cfg__factValue",
|
|
1940
|
-
children: fact.value
|
|
1941
|
-
})]
|
|
1942
|
-
}, fact.label))
|
|
1943
|
-
}),
|
|
1944
|
-
blocked === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1945
|
-
className: "dsh-channels-cfg__help",
|
|
1946
|
-
children: blocked
|
|
1947
|
-
}),
|
|
1948
|
-
result === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1949
|
-
className: `dsh-channels-cfg__notice dsh-channels-cfg__notice--${failed ? "error" : "ok"}`,
|
|
1950
|
-
children: [result.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1951
|
-
PANEL_COPY.cleanupDone,
|
|
1952
|
-
":",
|
|
1953
|
-
PANEL_COPY.cleanupDeleted,
|
|
1954
|
-
" ",
|
|
1955
|
-
result.deletedWorkspaces,
|
|
1956
|
-
" 个,",
|
|
1957
|
-
PANEL_COPY.cleanupDeletedDirs,
|
|
1958
|
-
" ",
|
|
1959
|
-
result.deletedSessionDirs,
|
|
1960
|
-
" 个,",
|
|
1961
|
-
PANEL_COPY.cleanupRetired,
|
|
1962
|
-
" ",
|
|
1963
|
-
result.retiredRoutes,
|
|
1964
|
-
" 条,",
|
|
1965
|
-
PANEL_COPY.cleanupFreed,
|
|
1966
|
-
" ",
|
|
1967
|
-
formatBytes(result.freedBytes),
|
|
1968
|
-
"。"
|
|
1969
|
-
] }) : /* @__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", {
|
|
1970
|
-
className: "dsh-channels-cfg__issues",
|
|
1971
|
-
children: result.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1972
|
-
className: "dsh-channels-cfg__mono",
|
|
1973
|
-
children: failure
|
|
1974
|
-
}, `${index}-${failure}`))
|
|
1975
|
-
})] })]
|
|
1976
|
-
}),
|
|
1977
|
-
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1978
|
-
className: "dsh-channels-cfg__fieldError",
|
|
1979
|
-
children: error
|
|
1980
|
-
}),
|
|
1981
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1982
|
-
className: "dsh-channels-cfg__actions",
|
|
1983
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1984
|
-
type: "button",
|
|
1985
|
-
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
|
|
1986
|
-
onClick: onRun,
|
|
1987
|
-
disabled: running || busy !== null || locked || blocked !== null,
|
|
1988
|
-
children: running ? PANEL_COPY.cleanupRunning : PANEL_COPY.cleanupNow
|
|
1989
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1990
|
-
className: "dsh-channels-cfg__help",
|
|
1991
|
-
children: PANEL_COPY.cleanupConfirmHint
|
|
1992
|
-
})]
|
|
1993
|
-
})
|
|
1994
|
-
]
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
//#endregion
|
|
1998
2206
|
//#region src/client/panel/workspace-tab.tsx
|
|
1999
2207
|
/**
|
|
2000
|
-
* 「工作区」tab
|
|
2001
|
-
* 表单保存栏在字段卡正下方,清理卡(独立动作,不走 draft)在其后。
|
|
2208
|
+
* 「工作区」tab:工作区根目录字段与保存栏。
|
|
2002
2209
|
*
|
|
2003
2210
|
* @module dsh-channels/client/panel/workspace-tab
|
|
2004
2211
|
*/
|
|
2005
2212
|
function WorkspaceTab(props) {
|
|
2006
|
-
const { form
|
|
2213
|
+
const { form } = props;
|
|
2007
2214
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2008
2215
|
className: "dsh-channels-cfg__tabBody",
|
|
2009
2216
|
children: [
|
|
@@ -2015,15 +2222,6 @@ window.__ModuleLoader__.load({
|
|
|
2015
2222
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
|
|
2016
2223
|
form,
|
|
2017
2224
|
tab: "workspace"
|
|
2018
|
-
}),
|
|
2019
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CleanupCard, {
|
|
2020
|
-
panel: form.panel,
|
|
2021
|
-
cleanup,
|
|
2022
|
-
busy: form.busy,
|
|
2023
|
-
locked: form.locked,
|
|
2024
|
-
result: cleanupResult,
|
|
2025
|
-
error: cleanupError,
|
|
2026
|
-
onRun: onRunCleanup
|
|
2027
2225
|
})
|
|
2028
2226
|
]
|
|
2029
2227
|
});
|
|
@@ -2034,8 +2232,8 @@ window.__ModuleLoader__.load({
|
|
|
2034
2232
|
* 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft 与顶部状态头 /
|
|
2035
2233
|
* tab 导航的外壳。
|
|
2036
2234
|
*
|
|
2037
|
-
* 所有 Host 交互(panel / status / catalog / config / credentials /
|
|
2038
|
-
*
|
|
2235
|
+
* 所有 Host 交互(panel / status / catalog / config / credentials / access /
|
|
2236
|
+
* pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
2039
2237
|
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `settings.section`
|
|
2040
2238
|
* 槽位。
|
|
2041
2239
|
*
|
|
@@ -2054,8 +2252,6 @@ window.__ModuleLoader__.load({
|
|
|
2054
2252
|
const [appSecretInput, setAppSecretInput] = (0, react.useState)("");
|
|
2055
2253
|
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
2056
2254
|
const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
|
|
2057
|
-
const [cleanupResult, setCleanupResult] = (0, react.useState)(null);
|
|
2058
|
-
const [cleanupError, setCleanupError] = (0, react.useState)(null);
|
|
2059
2255
|
const [accessNotice, setAccessNotice] = (0, react.useState)(null);
|
|
2060
2256
|
const [accessError, setAccessError] = (0, react.useState)(null);
|
|
2061
2257
|
const catalogTicket = (0, react.useRef)(0);
|
|
@@ -2070,8 +2266,6 @@ window.__ModuleLoader__.load({
|
|
|
2070
2266
|
const loadPanel = (0, react.useCallback)(async () => {
|
|
2071
2267
|
setLoad({ phase: "loading" });
|
|
2072
2268
|
setNotice(null);
|
|
2073
|
-
setCleanupResult(null);
|
|
2074
|
-
setCleanupError(null);
|
|
2075
2269
|
setAccessNotice(null);
|
|
2076
2270
|
setAccessError(null);
|
|
2077
2271
|
try {
|
|
@@ -2232,8 +2426,8 @@ window.__ModuleLoader__.load({
|
|
|
2232
2426
|
}
|
|
2233
2427
|
}, []);
|
|
2234
2428
|
/**
|
|
2235
|
-
* Manual refresh: reload the whole panel payload so access lists
|
|
2236
|
-
*
|
|
2429
|
+
* Manual refresh: reload the whole panel payload so access lists and
|
|
2430
|
+
* `restartPendingKeys` catch up with the Host. The dirty draft is
|
|
2237
2431
|
* deliberately preserved — refreshing never discards user input.
|
|
2238
2432
|
*/
|
|
2239
2433
|
const onRefresh = (0, react.useCallback)(async () => {
|
|
@@ -2282,8 +2476,6 @@ window.__ModuleLoader__.load({
|
|
|
2282
2476
|
panel: result.panel
|
|
2283
2477
|
});
|
|
2284
2478
|
setStatus(result.panel.status);
|
|
2285
|
-
setCleanupResult(null);
|
|
2286
|
-
setCleanupError(null);
|
|
2287
2479
|
setDraft((previous) => {
|
|
2288
2480
|
const next = { ...previous };
|
|
2289
2481
|
for (const key of Object.keys(values)) delete next[key];
|
|
@@ -2370,8 +2562,6 @@ window.__ModuleLoader__.load({
|
|
|
2370
2562
|
});
|
|
2371
2563
|
setStatus(result.panel.status);
|
|
2372
2564
|
setDraft({});
|
|
2373
|
-
setCleanupResult(null);
|
|
2374
|
-
setCleanupError(null);
|
|
2375
2565
|
setNotice({
|
|
2376
2566
|
kind: "ok",
|
|
2377
2567
|
text: PANEL_COPY.cleared
|
|
@@ -2426,37 +2616,6 @@ window.__ModuleLoader__.load({
|
|
|
2426
2616
|
setBusy(null);
|
|
2427
2617
|
}
|
|
2428
2618
|
}, [refreshStatus, running]);
|
|
2429
|
-
/**
|
|
2430
|
-
* Delete stale workspaces after an explicit confirmation. The Host refreshes
|
|
2431
|
-
* the panel in the same response, so the preview counts update immediately;
|
|
2432
|
-
* partial failures stay visible in the card instead of being swallowed.
|
|
2433
|
-
*/
|
|
2434
|
-
const onRunCleanup = (0, react.useCallback)(async () => {
|
|
2435
|
-
if (panel === null) return;
|
|
2436
|
-
const { cleanup } = panel;
|
|
2437
|
-
if (!(typeof window.confirm === "function" ? window.confirm(`${PANEL_COPY.cleanupConfirm}\n${PANEL_COPY.cleanupCandidates}:${cleanup.candidates} · ${PANEL_COPY.cleanupCandidateBytes}:${formatBytes(cleanup.candidateBytes)}`) : true)) return;
|
|
2438
|
-
setBusy("cleanup");
|
|
2439
|
-
setNotice(null);
|
|
2440
|
-
setCleanupResult(null);
|
|
2441
|
-
setCleanupError(null);
|
|
2442
|
-
try {
|
|
2443
|
-
const response = await runCleanup();
|
|
2444
|
-
if (!response.ok) {
|
|
2445
|
-
setCleanupError(response.error);
|
|
2446
|
-
return;
|
|
2447
|
-
}
|
|
2448
|
-
setLoad({
|
|
2449
|
-
phase: "ready",
|
|
2450
|
-
panel: response.panel
|
|
2451
|
-
});
|
|
2452
|
-
setStatus(response.panel.status);
|
|
2453
|
-
setCleanupResult(response.result);
|
|
2454
|
-
} catch (error) {
|
|
2455
|
-
setCleanupError(messageOf(error));
|
|
2456
|
-
} finally {
|
|
2457
|
-
setBusy(null);
|
|
2458
|
-
}
|
|
2459
|
-
}, [panel]);
|
|
2460
2619
|
/** Replace the embedded access snapshot after a whitelist mutation. */
|
|
2461
2620
|
const applyAccess = (0, react.useCallback)((access) => {
|
|
2462
2621
|
setLoad((previous) => previous.phase === "ready" ? {
|
|
@@ -2505,12 +2664,12 @@ window.__ModuleLoader__.load({
|
|
|
2505
2664
|
setBusy(null);
|
|
2506
2665
|
}
|
|
2507
2666
|
}, [applyAccess]);
|
|
2508
|
-
const onAddAccessUser = (0, react.useCallback)(async (
|
|
2667
|
+
const onAddAccessUser = (0, react.useCallback)(async (userId, name) => {
|
|
2509
2668
|
setBusy("access");
|
|
2510
2669
|
setAccessError(null);
|
|
2511
2670
|
setAccessNotice(null);
|
|
2512
2671
|
try {
|
|
2513
|
-
const result = await addAccessUser(
|
|
2672
|
+
const result = await addAccessUser(userId, name);
|
|
2514
2673
|
if (!result.ok) {
|
|
2515
2674
|
setAccessError(result.error);
|
|
2516
2675
|
return false;
|
|
@@ -2525,12 +2684,12 @@ window.__ModuleLoader__.load({
|
|
|
2525
2684
|
setBusy(null);
|
|
2526
2685
|
}
|
|
2527
2686
|
}, [applyAccess]);
|
|
2528
|
-
const onRemoveAccessUser = (0, react.useCallback)(async (
|
|
2687
|
+
const onRemoveAccessUser = (0, react.useCallback)(async (userId) => {
|
|
2529
2688
|
setBusy("access");
|
|
2530
2689
|
setAccessError(null);
|
|
2531
2690
|
setAccessNotice(null);
|
|
2532
2691
|
try {
|
|
2533
|
-
const result = await removeAccessUser(
|
|
2692
|
+
const result = await removeAccessUser(userId);
|
|
2534
2693
|
if (!result.ok) {
|
|
2535
2694
|
setAccessError(result.error);
|
|
2536
2695
|
return;
|
|
@@ -2767,15 +2926,7 @@ window.__ModuleLoader__.load({
|
|
|
2767
2926
|
form,
|
|
2768
2927
|
catalogBusy
|
|
2769
2928
|
}) : null,
|
|
2770
|
-
activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, {
|
|
2771
|
-
form,
|
|
2772
|
-
cleanup: form.panel.cleanup,
|
|
2773
|
-
cleanupResult,
|
|
2774
|
-
cleanupError,
|
|
2775
|
-
onRunCleanup: () => {
|
|
2776
|
-
onRunCleanup();
|
|
2777
|
-
}
|
|
2778
|
-
}) : null,
|
|
2929
|
+
activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, { form }) : null,
|
|
2779
2930
|
activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
|
|
2780
2931
|
form,
|
|
2781
2932
|
accessNotice,
|
|
@@ -2785,8 +2936,8 @@ window.__ModuleLoader__.load({
|
|
|
2785
2936
|
onRemoveAccessGroup(chatId);
|
|
2786
2937
|
},
|
|
2787
2938
|
onAddUser: onAddAccessUser,
|
|
2788
|
-
onRemoveUser: (
|
|
2789
|
-
onRemoveAccessUser(
|
|
2939
|
+
onRemoveUser: (userId) => {
|
|
2940
|
+
onRemoveAccessUser(userId);
|
|
2790
2941
|
},
|
|
2791
2942
|
onResolveRequest: (token, decision) => {
|
|
2792
2943
|
onResolveAccessRequest(token, decision);
|
|
@@ -2966,6 +3117,9 @@ window.__ModuleLoader__.load({
|
|
|
2966
3117
|
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
2967
3118
|
.dsh-channels-cfg__inputRow .dsh-channels-cfg__input{flex:1 1 auto;width:auto;min-width:0}
|
|
2968
3119
|
.dsh-channels-cfg__btn--browse{flex:none}
|
|
3120
|
+
.dsh-channels-cfg__list{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3121
|
+
.dsh-channels-cfg__listItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3122
|
+
.dsh-channels-cfg__btn--listRemove{flex:none}
|
|
2969
3123
|
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
2970
3124
|
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2971
3125
|
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
@@ -2975,6 +3129,8 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
|
2975
3129
|
.dsh-channels-cfg__notice{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
2976
3130
|
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
2977
3131
|
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
3132
|
+
.dsh-channels-cfg__notice a{color:inherit;text-decoration:underline}
|
|
3133
|
+
.dsh-channels-cfg__scopeList{margin:2px 0;padding-left:18px;display:flex;flex-direction:column;gap:2px}
|
|
2978
3134
|
.dsh-channels-cfg__banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:8px;border:1px dashed var(--dsw-alias-state-warning-primary,#a15c00);color:var(--dsw-alias-state-warning-primary,#a15c00);font-size:12px;line-height:18px}
|
|
2979
3135
|
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
2980
3136
|
.dsh-channels-cfg__spacer{flex:1}
|