@modesto_ma/dsh-channels 0.5.0-beta.4 → 0.5.0-beta.6

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