@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.1

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.
Files changed (58) hide show
  1. package/README.md +73 -55
  2. package/docs/access-control-design.md +7 -5
  3. package/docs/attachment-pipeline-v2.md +33 -41
  4. package/docs/feishu-onboarding.md +33 -33
  5. package/docs/persistence.md +6 -4
  6. package/docs/refactor-plan.md +1 -1
  7. package/docs/workspace-isolation-design.md +7 -8
  8. package/lib/client.js +173 -287
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +3001 -2627
  11. package/lib/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/access/cards.ts +9 -9
  14. package/src/access/controller.ts +10 -10
  15. package/src/access/index.ts +1 -0
  16. package/src/access/policy.ts +10 -10
  17. package/src/access/requests.ts +32 -31
  18. package/src/access/store.ts +12 -12
  19. package/src/access/targets.ts +56 -0
  20. package/src/cards/approval.ts +96 -26
  21. package/src/cards/index.ts +1 -0
  22. package/src/cards/onboarding.ts +1 -0
  23. package/src/cards/picker.ts +115 -0
  24. package/src/cards/streaming.ts +56 -7
  25. package/src/client/api.ts +5 -25
  26. package/src/client/panel/access-card.tsx +24 -15
  27. package/src/client/panel/access-tab.tsx +3 -3
  28. package/src/client/panel/connection-tab.tsx +70 -1
  29. package/src/client/panel/index.tsx +11 -62
  30. package/src/client/panel/shared.tsx +0 -23
  31. package/src/client/panel/workspace-tab.tsx +2 -18
  32. package/src/client/styles.ts +2 -0
  33. package/src/commands/registry.ts +2 -2
  34. package/src/config.ts +25 -56
  35. package/src/domain.ts +28 -125
  36. package/src/feishu/inbound-attachments.ts +1 -1
  37. package/src/feishu/manifest.ts +8 -31
  38. package/src/feishu/message-read.ts +19 -22
  39. package/src/feishu/scopes.ts +171 -0
  40. package/src/feishu/send-file.ts +543 -0
  41. package/src/feishu/slash-commands.ts +134 -0
  42. package/src/feishu/tools.ts +72 -112
  43. package/src/feishu-channel.ts +270 -9
  44. package/src/index.ts +5 -4
  45. package/src/interactions/approvals.ts +187 -7
  46. package/src/interactions/pending.ts +3 -3
  47. package/src/routing/inbound.ts +73 -106
  48. package/src/service.ts +463 -438
  49. package/src/session/manager.ts +1 -1
  50. package/src/session/routes.ts +2 -3
  51. package/src/settings/contract.ts +68 -85
  52. package/src/settings/routes.ts +3 -17
  53. package/src/settings/surface.ts +37 -88
  54. package/src/streamer.ts +94 -5
  55. package/src/workspaces.ts +21 -29
  56. package/src/cleanup.ts +0 -546
  57. package/src/client/panel/cleanup-card.tsx +0 -106
  58. 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 `{ openId, name? }` / `{ openId }`)。 */
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: "保存更改",
@@ -209,35 +222,11 @@ window.__ModuleLoader__.load({
209
222
  advancedCard: "只读与高级信息",
210
223
  advancedHelp: "这些值来自 profile 配置,不能在面板里编辑。",
211
224
  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
225
  accessCard: "访问与授权",
238
- accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 open_id。",
226
+ accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。",
239
227
  accessModeOpenWarning: "当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。",
240
- accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 open_id。",
228
+ accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。",
229
+ accessAdminOwnerNotice: "未配置管理员,已自动使用应用所有者作为管理员:",
241
230
  accessGroupsTitle: "已授权群",
242
231
  accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
243
232
  accessUsersTitle: "已授权用户",
@@ -246,7 +235,7 @@ window.__ModuleLoader__.load({
246
235
  accessRequestsEmpty: "没有待处理的授权申请。",
247
236
  accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
248
237
  accessGroupNamePlaceholder: "群名(可选)",
249
- accessUserOpenIdPlaceholder: "用户 open_id(ou_…)",
238
+ accessUserIdPlaceholder: "用户 user_id",
250
239
  accessUserNamePlaceholder: "昵称(可选)",
251
240
  accessAddGroup: "添加群",
252
241
  accessAddUser: "添加用户",
@@ -268,7 +257,7 @@ window.__ModuleLoader__.load({
268
257
  accessRequestAllowed: "已授权",
269
258
  accessRequestDenied: "已忽略",
270
259
  accessHints: "三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。",
271
- accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;open_id 可私聊机器人发送 /userid 获取。"
260
+ accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。"
272
261
  };
273
262
  //#endregion
274
263
  //#region src/client/api.ts
@@ -378,24 +367,6 @@ window.__ModuleLoader__.load({
378
367
  return JSON.parse(text);
379
368
  }
380
369
  /**
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
370
  * Ask the Host to open its native directory chooser (the panel's「选择目录…」
400
371
  * button). The request stays open while the OS dialog does; a cancel comes back
401
372
  * as `{ ok: true, canceled: true }`, not as an error.
@@ -481,16 +452,16 @@ window.__ModuleLoader__.load({
481
452
  async function removeAccessGroup(chatId) {
482
453
  return mutateAccess(ACCESS_GROUPS_ROUTE, "DELETE", { chatId });
483
454
  }
484
- /** Authorize one user open_id (global across authorized groups). */
485
- async function addAccessUser(openId, name) {
486
- return mutateAccess(ACCESS_USERS_ROUTE, "POST", name === void 0 || name === "" ? { openId } : {
487
- openId,
455
+ /** Authorize one user `user_id` (global across authorized groups). */
456
+ async function addAccessUser(userId, name) {
457
+ return mutateAccess(ACCESS_USERS_ROUTE, "POST", name === void 0 || name === "" ? { userId } : {
458
+ userId,
488
459
  name
489
460
  });
490
461
  }
491
462
  /** Revoke one user authorization (takes effect on the next message). */
492
- async function removeAccessUser(openId) {
493
- return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", { openId });
463
+ async function removeAccessUser(userId) {
464
+ return mutateAccess(ACCESS_USERS_ROUTE, "DELETE", { userId });
494
465
  }
495
466
  /** Settle one pending authorization request (same resolver as the Feishu card). */
496
467
  async function resolveAccessRequest(token, decision) {
@@ -606,25 +577,6 @@ window.__ModuleLoader__.load({
606
577
  function messageOf(error) {
607
578
  return error instanceof Error ? error.message : String(error);
608
579
  }
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
580
  /** Compact duration for the connection card. */
629
581
  function formatUptime(ms) {
630
582
  if (ms === void 0 || !Number.isFinite(ms)) return PANEL_COPY.unknownValue;
@@ -995,10 +947,6 @@ window.__ModuleLoader__.load({
995
947
  };
996
948
  }
997
949
  }
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
950
  //#endregion
1003
951
  //#region src/client/panel/access-card.tsx
1004
952
  /**
@@ -1030,7 +978,7 @@ window.__ModuleLoader__.load({
1030
978
  const { access } = panel;
1031
979
  const [groupChatId, setGroupChatId] = (0, react.useState)("");
1032
980
  const [groupName, setGroupName] = (0, react.useState)("");
1033
- const [userOpenId, setUserOpenId] = (0, react.useState)("");
981
+ const [userUserId, setUserUserId] = (0, react.useState)("");
1034
982
  const [userName, setUserName] = (0, react.useState)("");
1035
983
  const groups = access.groups;
1036
984
  const users = access.users;
@@ -1045,9 +993,9 @@ window.__ModuleLoader__.load({
1045
993
  }
1046
994
  };
1047
995
  const submitUser = async () => {
1048
- if (userOpenId.trim() === "") return;
1049
- if (await onAddUser(userOpenId, userName)) {
1050
- setUserOpenId("");
996
+ if (userUserId.trim() === "") return;
997
+ if (await onAddUser(userUserId, userName)) {
998
+ setUserUserId("");
1051
999
  setUserName("");
1052
1000
  }
1053
1001
  };
@@ -1072,9 +1020,16 @@ window.__ModuleLoader__.load({
1072
1020
  className: "dsh-channels-cfg__banner",
1073
1021
  children: PANEL_COPY.accessModeOpenWarning
1074
1022
  }) : null,
1075
- access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1023
+ access.loaded && access.mode === "guarded" && !access.admin.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
1024
  className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
1077
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.accessAdminMissingWarning })
1025
+ 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 })]
1026
+ }) : null,
1027
+ access.mode === "guarded" && access.admin.source === "owner" && access.admin.userId !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1028
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
1029
+ children: [PANEL_COPY.accessAdminOwnerNotice, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1030
+ className: "dsh-channels-cfg__mono",
1031
+ children: access.admin.userId
1032
+ })]
1078
1033
  }) : null,
1079
1034
  !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
1035
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1171,21 +1126,21 @@ window.__ModuleLoader__.load({
1171
1126
  className: "dsh-channels-cfg__accessRow",
1172
1127
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1173
1128
  className: "dsh-channels-cfg__accessMain",
1174
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.openId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1129
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: user.name ?? user.userId }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1175
1130
  className: "dsh-channels-cfg__accessMeta",
1176
1131
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1177
1132
  className: "dsh-channels-cfg__mono",
1178
- children: user.openId
1133
+ children: user.userId
1179
1134
  }), ` · ${accessSourceLabel(user.source)} · ${formatAccessDate(user.addedAt)}`]
1180
1135
  })]
1181
1136
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1182
1137
  type: "button",
1183
1138
  className: "dsh-channels-cfg__btn",
1184
1139
  disabled,
1185
- onClick: () => onRemoveUser(user.openId),
1140
+ onClick: () => onRemoveUser(user.userId),
1186
1141
  children: working ? PANEL_COPY.accessWorking : PANEL_COPY.accessRemove
1187
1142
  })]
1188
- }, user.openId))
1143
+ }, user.userId))
1189
1144
  }),
1190
1145
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1191
1146
  className: "dsh-channels-cfg__accessAdd",
@@ -1193,11 +1148,11 @@ window.__ModuleLoader__.load({
1193
1148
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1194
1149
  type: "text",
1195
1150
  className: "dsh-channels-cfg__input dsh-channels-cfg__accessInput",
1196
- value: userOpenId,
1197
- placeholder: PANEL_COPY.accessUserOpenIdPlaceholder,
1198
- "aria-label": PANEL_COPY.accessUserOpenIdPlaceholder,
1151
+ value: userUserId,
1152
+ placeholder: PANEL_COPY.accessUserIdPlaceholder,
1153
+ "aria-label": PANEL_COPY.accessUserIdPlaceholder,
1199
1154
  disabled,
1200
- onChange: (event) => setUserOpenId(event.target.value)
1155
+ onChange: (event) => setUserUserId(event.target.value)
1201
1156
  }),
1202
1157
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1203
1158
  type: "text",
@@ -1211,7 +1166,7 @@ window.__ModuleLoader__.load({
1211
1166
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1212
1167
  type: "button",
1213
1168
  className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--primary",
1214
- disabled: disabled || userOpenId.trim() === "",
1169
+ disabled: disabled || userUserId.trim() === "",
1215
1170
  onClick: () => {
1216
1171
  submitUser();
1217
1172
  },
@@ -1302,7 +1257,7 @@ window.__ModuleLoader__.load({
1302
1257
  //#endregion
1303
1258
  //#region src/client/panel/access-tab.tsx
1304
1259
  /**
1305
- * 「访问授权」tab:准入与授权字段表单(管理员 open_id / 准入模式 /
1260
+ * 「访问授权」tab:准入与授权字段表单(管理员 user_id / 准入模式 /
1306
1261
  * 申请有效期)+ 白名单与待处理申请卡。表单保存走全局 draft,白名单
1307
1262
  * 增删是独立即时 mutation。
1308
1263
  *
@@ -1587,6 +1542,7 @@ window.__ModuleLoader__.load({
1587
1542
  * - error:连接信息卡 + 最近一次连接失败 + 「重试」
1588
1543
  * - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
1589
1544
  * - connecting:禁用按钮显示「连接中…」
1545
+ * - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
1590
1546
  *
1591
1547
  * @module dsh-channels/client/panel/connection-tab
1592
1548
  */
@@ -1607,6 +1563,8 @@ window.__ModuleLoader__.load({
1607
1563
  busy,
1608
1564
  onReconnect
1609
1565
  }),
1566
+ status.scopeCheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeCheckCard, { check: status.scopeCheck }),
1567
+ status.slashCommandSync === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlashCommandSyncCard, { sync: status.slashCommandSync }),
1610
1568
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialsCard, {
1611
1569
  panel,
1612
1570
  credentials
@@ -1797,6 +1755,109 @@ window.__ModuleLoader__.load({
1797
1755
  ]
1798
1756
  });
1799
1757
  }
1758
+ /** Post-connect scope self-check: missing scopes in red, each with a console deep link. */
1759
+ function ScopeCheckCard(props) {
1760
+ const { check } = props;
1761
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1762
+ className: "dsh-channels-cfg__card",
1763
+ children: [
1764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1765
+ className: "dsh-channels-cfg__cardHead",
1766
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1767
+ className: "dsh-channels-cfg__cardTitle",
1768
+ children: PANEL_COPY.scopeCheckTitle
1769
+ })
1770
+ }),
1771
+ check.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1772
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
1773
+ children: PANEL_COPY.scopeCheckOk
1774
+ }) : null,
1775
+ check.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1776
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
1777
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1778
+ className: "dsh-channels-cfg__mono",
1779
+ children: check.error
1780
+ })]
1781
+ }) : null,
1782
+ check.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1783
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
1784
+ children: [
1785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.scopeCheckMissing }),
1786
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1787
+ className: "dsh-channels-cfg__scopeList",
1788
+ children: check.missing.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1790
+ "[",
1791
+ item.level === "required" ? PANEL_COPY.scopeCheckRequired : PANEL_COPY.scopeCheckOptional,
1792
+ "] "
1793
+ ] }),
1794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1795
+ className: "dsh-channels-cfg__mono",
1796
+ children: item.scopes.join(" / ")
1797
+ }),
1798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1799
+ ":",
1800
+ item.purpose,
1801
+ " "
1802
+ ] }),
1803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1804
+ href: item.consoleUrl,
1805
+ target: "_blank",
1806
+ rel: "noreferrer noopener",
1807
+ children: PANEL_COPY.scopeCheckOpenOne
1808
+ })
1809
+ ] }, item.scopes.join(",")))
1810
+ }),
1811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1812
+ href: check.consoleUrl,
1813
+ target: "_blank",
1814
+ rel: "noreferrer noopener",
1815
+ children: PANEL_COPY.scopeCheckOpenAll
1816
+ })
1817
+ ]
1818
+ }) : null
1819
+ ]
1820
+ });
1821
+ }
1822
+ /** Post-connect native slash-command panel sync: counts on success, the Feishu reason on failure. */
1823
+ function SlashCommandSyncCard(props) {
1824
+ const { sync } = props;
1825
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1826
+ className: "dsh-channels-cfg__card",
1827
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1828
+ className: "dsh-channels-cfg__cardHead",
1829
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1830
+ className: "dsh-channels-cfg__cardTitle",
1831
+ children: PANEL_COPY.slashCommandSyncTitle
1832
+ })
1833
+ }), sync.state === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1834
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--ok",
1835
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [PANEL_COPY.slashCommandSyncOk, " "] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1836
+ PANEL_COPY.slashCommandSyncTotal,
1837
+ " ",
1838
+ sync.total,
1839
+ " · ",
1840
+ PANEL_COPY.slashCommandSyncCreated,
1841
+ " ",
1842
+ sync.created,
1843
+ " · ",
1844
+ PANEL_COPY.slashCommandSyncUpdated,
1845
+ " ",
1846
+ sync.updated,
1847
+ " · ",
1848
+ PANEL_COPY.slashCommandSyncRemoved,
1849
+ " ",
1850
+ sync.removed
1851
+ ] })]
1852
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1853
+ className: "dsh-channels-cfg__notice dsh-channels-cfg__notice--error",
1854
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: PANEL_COPY.slashCommandSyncError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1855
+ className: "dsh-channels-cfg__mono",
1856
+ children: sync.error
1857
+ })]
1858
+ })]
1859
+ });
1860
+ }
1800
1861
  /** Config saved but only applied on the next connect: offer the one-click restart. */
1801
1862
  function RestartBanner(props) {
1802
1863
  const { panel, busy, locked, onReconnect } = props;
@@ -1875,135 +1936,14 @@ window.__ModuleLoader__.load({
1875
1936
  });
1876
1937
  }
1877
1938
  //#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
1939
  //#region src/client/panel/workspace-tab.tsx
1999
1940
  /**
2000
- * 「工作区」tab:工作区根目录 / 过期保留策略 / 自动清理字段 + 目录清理卡。
2001
- * 表单保存栏在字段卡正下方,清理卡(独立动作,不走 draft)在其后。
1941
+ * 「工作区」tab:工作区根目录字段与保存栏。
2002
1942
  *
2003
1943
  * @module dsh-channels/client/panel/workspace-tab
2004
1944
  */
2005
1945
  function WorkspaceTab(props) {
2006
- const { form, cleanup, cleanupResult, cleanupError, onRunCleanup } = props;
1946
+ const { form } = props;
2007
1947
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2008
1948
  className: "dsh-channels-cfg__tabBody",
2009
1949
  children: [
@@ -2015,15 +1955,6 @@ window.__ModuleLoader__.load({
2015
1955
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, {
2016
1956
  form,
2017
1957
  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
1958
  })
2028
1959
  ]
2029
1960
  });
@@ -2034,8 +1965,8 @@ window.__ModuleLoader__.load({
2034
1965
  * 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft 与顶部状态头 /
2035
1966
  * tab 导航的外壳。
2036
1967
  *
2037
- * 所有 Host 交互(panel / status / catalog / config / credentials / cleanup /
2038
- * access / pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
1968
+ * 所有 Host 交互(panel / status / catalog / config / credentials / access /
1969
+ * pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
2039
1970
  * 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `settings.section`
2040
1971
  * 槽位。
2041
1972
  *
@@ -2054,8 +1985,6 @@ window.__ModuleLoader__.load({
2054
1985
  const [appSecretInput, setAppSecretInput] = (0, react.useState)("");
2055
1986
  const [catalog, setCatalog] = (0, react.useState)(null);
2056
1987
  const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
2057
- const [cleanupResult, setCleanupResult] = (0, react.useState)(null);
2058
- const [cleanupError, setCleanupError] = (0, react.useState)(null);
2059
1988
  const [accessNotice, setAccessNotice] = (0, react.useState)(null);
2060
1989
  const [accessError, setAccessError] = (0, react.useState)(null);
2061
1990
  const catalogTicket = (0, react.useRef)(0);
@@ -2070,8 +1999,6 @@ window.__ModuleLoader__.load({
2070
1999
  const loadPanel = (0, react.useCallback)(async () => {
2071
2000
  setLoad({ phase: "loading" });
2072
2001
  setNotice(null);
2073
- setCleanupResult(null);
2074
- setCleanupError(null);
2075
2002
  setAccessNotice(null);
2076
2003
  setAccessError(null);
2077
2004
  try {
@@ -2232,8 +2159,8 @@ window.__ModuleLoader__.load({
2232
2159
  }
2233
2160
  }, []);
2234
2161
  /**
2235
- * Manual refresh: reload the whole panel payload so access lists, the cleanup
2236
- * preview and `restartPendingKeys` catch up with the Host. The dirty draft is
2162
+ * Manual refresh: reload the whole panel payload so access lists and
2163
+ * `restartPendingKeys` catch up with the Host. The dirty draft is
2237
2164
  * deliberately preserved — refreshing never discards user input.
2238
2165
  */
2239
2166
  const onRefresh = (0, react.useCallback)(async () => {
@@ -2282,8 +2209,6 @@ window.__ModuleLoader__.load({
2282
2209
  panel: result.panel
2283
2210
  });
2284
2211
  setStatus(result.panel.status);
2285
- setCleanupResult(null);
2286
- setCleanupError(null);
2287
2212
  setDraft((previous) => {
2288
2213
  const next = { ...previous };
2289
2214
  for (const key of Object.keys(values)) delete next[key];
@@ -2370,8 +2295,6 @@ window.__ModuleLoader__.load({
2370
2295
  });
2371
2296
  setStatus(result.panel.status);
2372
2297
  setDraft({});
2373
- setCleanupResult(null);
2374
- setCleanupError(null);
2375
2298
  setNotice({
2376
2299
  kind: "ok",
2377
2300
  text: PANEL_COPY.cleared
@@ -2426,37 +2349,6 @@ window.__ModuleLoader__.load({
2426
2349
  setBusy(null);
2427
2350
  }
2428
2351
  }, [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
2352
  /** Replace the embedded access snapshot after a whitelist mutation. */
2461
2353
  const applyAccess = (0, react.useCallback)((access) => {
2462
2354
  setLoad((previous) => previous.phase === "ready" ? {
@@ -2505,12 +2397,12 @@ window.__ModuleLoader__.load({
2505
2397
  setBusy(null);
2506
2398
  }
2507
2399
  }, [applyAccess]);
2508
- const onAddAccessUser = (0, react.useCallback)(async (openId, name) => {
2400
+ const onAddAccessUser = (0, react.useCallback)(async (userId, name) => {
2509
2401
  setBusy("access");
2510
2402
  setAccessError(null);
2511
2403
  setAccessNotice(null);
2512
2404
  try {
2513
- const result = await addAccessUser(openId, name);
2405
+ const result = await addAccessUser(userId, name);
2514
2406
  if (!result.ok) {
2515
2407
  setAccessError(result.error);
2516
2408
  return false;
@@ -2525,12 +2417,12 @@ window.__ModuleLoader__.load({
2525
2417
  setBusy(null);
2526
2418
  }
2527
2419
  }, [applyAccess]);
2528
- const onRemoveAccessUser = (0, react.useCallback)(async (openId) => {
2420
+ const onRemoveAccessUser = (0, react.useCallback)(async (userId) => {
2529
2421
  setBusy("access");
2530
2422
  setAccessError(null);
2531
2423
  setAccessNotice(null);
2532
2424
  try {
2533
- const result = await removeAccessUser(openId);
2425
+ const result = await removeAccessUser(userId);
2534
2426
  if (!result.ok) {
2535
2427
  setAccessError(result.error);
2536
2428
  return;
@@ -2767,15 +2659,7 @@ window.__ModuleLoader__.load({
2767
2659
  form,
2768
2660
  catalogBusy
2769
2661
  }) : 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,
2662
+ activeTab === "workspace" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTab, { form }) : null,
2779
2663
  activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
2780
2664
  form,
2781
2665
  accessNotice,
@@ -2785,8 +2669,8 @@ window.__ModuleLoader__.load({
2785
2669
  onRemoveAccessGroup(chatId);
2786
2670
  },
2787
2671
  onAddUser: onAddAccessUser,
2788
- onRemoveUser: (openId) => {
2789
- onRemoveAccessUser(openId);
2672
+ onRemoveUser: (userId) => {
2673
+ onRemoveAccessUser(userId);
2790
2674
  },
2791
2675
  onResolveRequest: (token, decision) => {
2792
2676
  onResolveAccessRequest(token, decision);
@@ -2975,6 +2859,8 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
2975
2859
  .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
2860
  .dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
2977
2861
  .dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
2862
+ .dsh-channels-cfg__notice a{color:inherit;text-decoration:underline}
2863
+ .dsh-channels-cfg__scopeList{margin:2px 0;padding-left:18px;display:flex;flex-direction:column;gap:2px}
2978
2864
  .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
2865
  .dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
2980
2866
  .dsh-channels-cfg__spacer{flex:1}