@modesto_ma/dsh-channels 0.6.0 → 0.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,7 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
53
53
  - **活动状态两行**(小字,**只在当前正在流式输出的那张卡上渲染**,已冻结的续卡/主卡不再重复;**互相独立、互不顶替**),让卡片在“模型不吐字”的阶段(工具执行、subagent、workflow)**不再静默**——按 `statusRefreshMs`(默认 5s)慢节奏刷新计时:
54
54
  - **状态行**(委托进度):`tool-workflow/run-start / agent-start / agent-end / run-end`(工作流进度:完成数/总数、阶段、活动成员)→ run 结束转为 `🧵 工作流 xxx 结束(完成)` 并**保留到最终卡片**(后续工具调用、正文输出都不会顶掉它);**子代理进度**(跨会话映射):子代理是独立 session(`header.parentSession` 指向主会话),其 `subagent/descriptor`(label)、`tool/call`(当前工具)被折叠为 `🤖 子代理执行中:检索实现方案 (read) · 已运行 45s`,并行子代理显示计数,完成后在最终卡片汇总为 `🤖 子代理完成:a、b`。**只显示状态,不显示输出**——子代理正文是父模型的中间产物,绝不进卡片正文
55
55
  - **工具行**(父会话当前工具调用):`tool/call` → `🔧 调用工具:bash · 已运行 Ns`(subagent 工具显示其 description、workflow 工具显示 meta.name),`tool/result` 返回即清除,**回合结束后收起**(完成态只保留结算状态行:工作流结论 / 子代理完成摘要)
56
+ - **模型请求重试**:DSH(`dsh-llm-retry`)在限流、服务端错误、超时等可重试失败后会在**同一回合内**重跑失败的那一步,卡片照常保持流式:工具行显示 `⚠️ 模型请求失败(RATE_LIMIT),N 秒后第 k/5 次重试`,等待结束后变为 `🔁 模型请求重试中(第 k/5 次)`,重试的请求开始输出即收起。失败那次已流出的半截正文/推理不进入模型历史,卡片随即撤回,重试从头输出。只有 `turn/end` 才结束卡片;重试用尽仍失败时,失败提示后附「已自动重试 N 次」。
56
57
  - 两者可同屏:如 `🧵 工作流 xxx 结束(完成)` + `🔧 调用工具:bash`
57
58
  2. **正文**:富文本 markdown;超长正文**自动分段投放**——单卡片预算用满后按干净断点(段落 / 标题 / 代码块边界)切分,且**每张卡最多 5 个表格**(飞书硬限制,错误 11310);断点会避开代码围栏与表格,**单个表格/代码块小于单卡预算时不会被从中间切断**(单块本身超过单卡预算时只能硬切,内容仍不丢失)。续片以「📄 回复分段 n」卡片跟进同一话题,**全文无丢失**;续卡被飞书拒收时自动降级为纯文本消息(内容仍送达)。**续卡只留正文**:`📄 回复分段 n` → 可选活动行(仅当它是当前活动卡)→ 正文,无面板、无页脚、无分割线
58
59
  3. **停止按钮**:回合进行中,当前正在流式输出的那张卡(主卡或最新续卡)正文下方显示「⏹ 停止」,效果同 `/stop`。首帧不带按钮;排在后面的消息的卡片在轮到它运行时才出现按钮。只有话题发起人在原聊天里点击才生效,点击后按钮变为不可点的「正在停止…」;其他人点击只会在按钮下加一行「仅发起该会话的用户可停止」。回合结束后按钮移除;经 `/stop` 或按钮停止的回合面板标题为 **已停止**,主卡页脚上方显示「⏹ 已按请求停止本轮回复」。
@@ -82,7 +83,7 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
82
83
  | 附件与工具 | 「入站附件」:接收图片和文件、使用 DSH 原生附件库(开关,关闭则存到话题目录 `attachments/`)、图片预取、首轮话题历史;「工具」:`feishu_attachment_fetch` / `feishu_topic_messages` / `feishu_send_file` 开关;「限额与超时」:预取上限、读取附件上限与超时、读取话题消息超时、引用摘录与历史超时(共 8 项,默认收起,有改动或错误时自动展开) |
83
84
  | 访问授权 | 待处理申请(置顶,一键授权 / 忽略)、调试模式(`accessMode`,开关)、白名单用户可私聊(`dmAllowWhitelist`,开关)、管理员 user_id、申请有效期;已授权群 / 用户白名单(添加表单默认收起) |
84
85
  | 知识库 | 知识库读 / 写开关(`feishuTools.wikiRead` / `feishuTools.wikiWrite`)、可访问范围 `wikiRoots`(每条链接一个输入框,逐条校验);「可使用的人」:知识库白名单增删(与用户白名单独立,等同 `/auth wiki add|remove`) |
85
- | 高级 | 日志分析接口(开关 `analyzeEnabled` 与访问令牌)、SDK 日志级别、Web 覆盖文件路径与「恢复 profile 配置」 |
86
+ | 高级 | SDK 日志级别、Web 覆盖文件路径与「恢复 profile 配置」 |
86
87
 
87
88
  **调试模式**:「访问授权 → 调试模式」打开即 `accessMode: open`,所有人都能和机器人聊天;打开期间面板顶部常驻黄色警告。默认关闭(`guarded`)。
88
89
 
@@ -124,11 +125,11 @@ pnpm verify:client # 用 __ModuleLoader__ shim 做 loader 冒烟
124
125
 
125
126
  ## 对话里的飞书来源
126
127
 
127
- 从飞书发起的会话(话题、会话级降级、`/renew` 新开的会话、日志平台分析话题)在 Web/桌面端对话标题旁会出现一个来源按钮:群聊显示群名,私聊显示「私聊 · 对方姓名」(读不到姓名时为「飞书私聊」)。点击后在右侧栏打开「飞书来源」标签页,内容包括:
128
+ 从飞书发起的会话(话题、会话级降级、`/renew` 新开的会话、其他插件经 `feishuTopics` 发起的群共享话题,如观象的日志分析)在 Web/桌面端对话标题旁会出现一个来源按钮:群聊显示群名,私聊显示「私聊 · 对方姓名」(读不到姓名时为「飞书私聊」)。点击后在右侧栏打开「飞书来源」标签页,内容包括:
128
129
 
129
130
  - **标题**:群聊为群头像和群名,私聊为对方头像和姓名;
130
131
  - **来源**:私聊 / 群聊 / 群聊 · 话题;群名读不到时显示群 ID;
131
- - **发起人**:头像、姓名 + `user_id`(日志平台分析话题显示请求人,并注明群内成员都可以继续);
132
+ - **发起人**:头像、姓名 + `user_id`(群共享话题显示发起人,并注明群内成员都可以继续);
132
133
  - **开始于 / 最近活跃**:飞书侧已经换到新会话(`/renew`、`/new`)或路由过期后,「最近活跃」换成「状态:飞书侧已切换到新会话或已过期」;
133
134
  - **在飞书中打开**(飞书 applink 打开对应聊天)与「刷新」。
134
135
 
@@ -136,7 +137,7 @@ Web 里直接新建的会话不显示按钮。来源在 Host 侧按会话 id 持
136
137
 
137
138
  ## 发布
138
139
 
139
- 版本号由 semantic-release 按 Conventional Commits 计算。推送(含合并 PR)到 `main` 在检查通过后**自动发布**正式版;推送到 `dev` 只跑检查,beta 版**只在手动触发时发布**,可以先累积多笔提交。
140
+ 版本号由 semantic-release 按 Conventional Commits 计算。1.0 之前,破坏性变更(`BREAKING CHANGE`)只升次版本号(如 0.7 → 0.8)。推送(含合并 PR)到 `main` 在检查通过后**自动发布**正式版;推送到 `dev` 只跑检查,beta 版**只在手动触发时发布**,可以先累积多笔提交。
140
141
 
141
142
  | 分支 | 版本 | npm dist-tag |
142
143
  | --- | --- | --- |
@@ -167,7 +168,7 @@ src/
167
168
  │ ├── streaming.ts # 流式回复卡构成(面板/活动行/正文/停止按钮/结算页脚)、分段、字节/表格预算
168
169
  │ ├── picker.ts # /model、/effort、/permissions 选择卡与按钮回传解析
169
170
  │ ├── onboarding.ts # 私聊首次触达引导卡
170
- │ ├── analyze.ts # 日志分析卡(详情页表格、状态标签)
171
+ │ ├── topic.ts # 群话题服务的根卡片(引导句、链接表格、状态标签)
171
172
  │ ├── approval.ts # 审批决策卡
172
173
  │ └── question.ts # 结构化问答卡
173
174
  ├── approvals.ts # 审批 barrel → src/interactions/
@@ -302,8 +303,6 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
302
303
  dmAllowWhitelist: false # guarded 下用户白名单里的人也可私聊(默认关:私聊仅管理员)
303
304
  adminUserId: 'a1b2c3d4' # 管理员 user_id(可留空,留空时自动使用应用所有者;都没有时 guarded 下拒绝所有对话;面板可改)
304
305
  authRequestTtlDays: 7 # 入群/用户授权申请卡有效期(天,1–30)
305
- analyzeEnabled: false # 日志平台分析接口开关(POST /api/dsh-channels.logalyze,挂在 DSH Web 端口;改后立即生效)
306
- analyzeToken: 'DSH_CHANNELS_ANALYZE_TOKEN' # 分析接口 Bearer 令牌的凭据引用名(值在面板填写或写进 .credentials.yaml)
307
306
  loggerLevel: 'info'
308
307
  ```
309
308
 
@@ -364,7 +363,7 @@ FEISHU_APP_SECRET: xxxxxxxx
364
363
  |---|---|---|
365
364
  | 私聊 | `<baseDir>/p2p/<user_id>/` | `<工作区>/topics/<话题 id>/` |
366
365
  | 群聊 | `<baseDir>/groups/<chat_id>/<user_id>/` | 同上 |
367
- | 日志平台分析 | `<baseDir>/analyze/<应用>/<错误码>/` | 同上 |
366
+ | 其他插件经 `feishuTopics` 发起的话题 | `<baseDir>/<调用方给的路径段>/`(观象的日志分析是 `analyze/<应用>/<错误码>/`) | 同上 |
368
367
 
369
368
  - 群聊必须是「群 × 用户」:同一个人在不同群之间不共享文件,避免 A 群会话把 B 群内容带出来。
370
369
  - 会话创建时以工作区为 header cwd(保证 GUI 里归属该项目、沙箱边界稳定),随后通过 `ctx.workingDirectory` 把实际工作目录切到话题目录;冷恢复会从 Session 日志还原该目录。因此话题之间的文件互不干扰,而工作区数量只随用户增长。
@@ -375,55 +374,40 @@ FEISHU_APP_SECRET: xxxxxxxx
375
374
  - 旧版本「一个话题一个工作区」(`<baseDir>/topics/...`)不会自动迁移:旧话题继续用原目录;旧版「用户 × 聊天」共享工作区正好作为新的用户工作区复用。
376
375
  - 设计取舍与约束详见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md)。
377
376
 
378
- ## 日志平台分析接口
377
+ ## 群话题服务(feishuTopics)
379
378
 
380
- 日志平台调用插件的分析接口:插件在指定群里发一张分析卡(发起人、触发时间、应用、错误码和日志详情页表格),在卡片下开话题,把平台给的 prompt 原样交给一个新 Session(prompt 开头的斜杠命令由 DSH 展开成对应 skill),分析过程在话题内流式输出,和用户自己发起的会话一样。分析结束后分析卡的状态标签随之更新。群里通过准入的成员都可以在该话题内追问。插件只负责接收 prompt 和开会话:日志包地址写在 prompt 里,下载、解压、超时都由日志分析 skill 自己处理(Session 的沙箱需要能访问日志存储地址)。
379
+ 插件向其他 DSH 插件提供 Cordis 服务 `feishuTopics`:调用方给出目标群、发起人、根卡片内容、工作区和 prompt,插件在群里发一张根卡片,在卡片下开话题,把 prompt 原样交给一个新 Session(prompt 开头的斜杠命令由 DSH 展开成对应 skill),过程在话题内流式输出,和用户自己发起的会话一样。首轮结束后根卡片的状态标签随之更新。群里通过准入的成员都可以在该话题内追问。观象([@modesto_ma/dsh-guanxiang](https://github.com/modestoma/dsh-guanxiang))的日志平台分析接口 `POST /open-api/logalyze` 就建在这个服务上。
381
380
 
382
- **开启**:在面板「高级 → 日志分析接口」打开开关(`analyzeEnabled`,默认关,改后立即生效、不需要重连),并在同一处的「访问令牌」卡片填写令牌(建议 32 位以上随机串;写入 DSH 凭据库的 `DSH_CHANNELS_ANALYZE_TOKEN`,也可直接写进 `$DSH_HOME/.credentials.yaml`,引用名可用 `analyzeToken` 修改)。接口挂在 DSH Web 服务上(与 Web 界面同一端口),路径 `/api/dsh-channels.logalyze` 按 DSH 纯 HTTP 路由 `/api/<名字>.<方法>` 的命名;只有带 Web 的 profile(web、desktop)有这个接口,headless 没有。DSH 通过反向代理挂在前缀下时,地址随之带上前缀(如 `https://<域名>/dsh/api/dsh-channels.logalyze`)。该路由不走 DSH 登录 cookie,访问令牌是唯一凭据。开关关闭时所有请求返回 404;令牌每次请求都重新解析,轮换后下一次请求生效;令牌缺失时所有请求返回 503。
381
+ **日志平台分析接口已迁到观象**:接口的路径、请求体和响应都没有变,但开关和令牌改在观象面板「分析接口」中设置,令牌的凭据引用名改为 `GUANXIANG_ANALYZE_TOKEN`。本插件不再注册这个路由,也不再读取 `analyzeEnabled` / `analyzeToken`;`cordis.yml` 或 Web 覆盖文件里还留着这两项时,启动日志会提示一次。升级步骤:
383
382
 
384
- **请求**:
383
+ 1. 把 dsh-channels 和观象一起升级(观象需要支持 `feishuTopics` 的版本)。只升级 dsh-channels 时没有插件提供这个接口,请求会得到 404。
384
+ 2. 在观象面板「分析接口」中打开开关,保存令牌。平台继续用原来的令牌值时,把同一个值存进去即可。
385
+ 3. 从 `cordis.yml` 中删掉 `analyzeEnabled` / `analyzeToken`。
385
386
 
386
- ```http
387
- POST /api/dsh-channels.logalyze
388
- Authorization: Bearer <令牌>
389
- Idempotency-Key: <平台生成的唯一值,1-200 个可见 ASCII 字符>
390
- Content-Type: application/json
387
+ 升级前已发出的分析卡、话题和追问权限都不受影响;升级时仍在「分析中」的分析卡会在重新连上飞书后改成「已中断」。
391
388
 
392
- {
393
- "requesterUserId": "a1b2c3d4",
394
- "triggeredAt": "2026-10-22 19:30",
395
- "app": "camera",
396
- "errorCode": "E1001",
397
- "logDetails": [
398
- { "name": "设备 A 崩溃日志", "url": "https://logs.example.com/detail/1" },
399
- { "name": "设备 B 崩溃日志", "url": "https://logs.example.com/detail/2" }
400
- ],
401
- "chatId": "oc_xxx",
402
- "prompt": "/log-analyze https://logs.example.com/1.zip https://logs.example.com/2.zip 请分析这些日志"
403
- }
404
- ```
405
-
406
- - `requesterUserId`:平台发起人的飞书 user_id,分析卡 @ 此人;路由记录里也以此人作为发起人。
407
- - `triggeredAt`:平台上发起分析的时间,1–64 字符,原样显示在分析卡上(时区和格式由平台决定)。
408
- - `app` / `errorCode`:只允许 `A-Z a-z 0-9 . _ -`(1–128 字符),决定工作区 `<baseDir>/analyze/<app>/<errorCode>/`;同一应用同一错误码的所有分析都在这个工作区里,每次分析各有一个话题目录。
409
- - `logDetails`:触发分析的日志在日志平台上的详情页,1–50 条,每条 `name`(1–200 字符)+ `url`(http/https)。只在分析卡的表格里列出(每页 10 行),插件不会访问这些地址。
410
- - `chatId`:目标群,必须是已授权群(`/auth group add` 或面板「访问与授权」),机器人需在群内;`accessMode: open` 时不校验。
411
- - `prompt`:1–100000 字符,原样作为首条用户消息,插件不追加任何内容。
412
- - 只接受以上 7 个字段,多余字段返回 400。
389
+ **调用方式**:类型在包子路径 `@modesto_ma/dsh-channels/topics`(只有类型,没有运行时导出),调用方用 `import type` 引用,在每次调用时用 `ctx.get('feishuTopics')` 取服务(本插件未加载或正在重载时取不到),并检查 `version`(当前为 `1`)。不要把 `feishuTopics` 写进调用方插件的主 `inject`:那样本插件每次重载(例如改配置后重连)都会连带重载调用方。需要随服务上下线挂载路由时,在子作用域里 `ctx.inject(['feishuTopics'], …)`。
413
390
 
414
- **响应**:校验通过、分析卡发出后立即返回 `202`:
415
-
416
- ```json
417
- { "ok": true, "chatId": "oc_xxx", "messageId": "om_xxx" }
391
+ ```ts
392
+ const topics = ctx.get('feishuTopics') as FeishuTopicService | undefined
393
+ const result = await topics?.launch({
394
+ key, fingerprint, chatId, requesterUserId,
395
+ card: { title: '日志分析', taskName: '分析', lead: [{ text: '于 … 发起了对 ' }, { text: 'camera', bold: true }, …], links },
396
+ workspace: { segments: ['analyze', 'camera', 'E1001'], title: '日志分析 · camera · E1001' },
397
+ prompt,
398
+ })
418
399
  ```
419
400
 
420
- `messageId` 是分析卡的消息 id,分析话题挂在它下面。同步错误都带中文 `error`:`401` 令牌无效、`404` 接口开关未打开、`400` 参数或 `Idempotency-Key` 不合法、`403` 目标群未授权、`409` 同一个 `Idempotency-Key` 已被内容不同的请求使用、`413` 请求体超过 1 MiB、`415` 不是 JSON、`502` 分析卡发送失败(如机器人不在群里)、`503` 飞书未连接或未配置令牌。
421
-
422
- **去重**:同一个 `Idempotency-Key` 再次请求(包括并发重试)原样返回第一次的结果,不会再发卡、再分析。记录永久保存在 `channels_routes_v2` 域的 `analyze_requests` 表。分析卡发出后的失败(话题创建、Session 创建)不会回滚这条记录:分析卡的状态变成「失败」并写明原因,需要重试时请换一个新的 `Idempotency-Key`。
401
+ - `key` / `fingerprint`:幂等键(1–200 个可见 ASCII 字符)与调用方定义的请求指纹。同一个键再次发起(包括并发)返回第一次的根卡片(`replayed: true`),指纹不同则拒绝(`key-conflict`)。记录永久保存在 `channels_routes_v2` 域的 `analyze_requests` 表(沿用旧接口的表名);根卡片发出后的失败不会回滚记录,根卡片显示「失败」和原因,需要重试时换一个新键。
402
+ - `chatId`:目标群,必须是已授权群(`/auth group add` 或面板「访问与授权」),机器人需在群内;`accessMode: open` 时不校验。
403
+ - `card`:标题、任务名(状态文字里用,如「分析中」「分析已完成」)、跟在 @发起人 后的引导句(可加粗片段)、可选的链接表格(最多 50 行,每页 10 行)。
404
+ - `workspace`:`<baseDir>` 下的 1–8 个路径段(只允许 `A-Z a-z 0-9 . _ -`,第一段不能是本插件自用的 `p2p`、`groups`、`topics`、`_staging`)和它在 DSH 工作区列表里的标题。路径段相同的发起共用一个工作区,各有一个话题目录。
405
+ - `prompt`:1–100000 字符,原样作为首条用户消息。
406
+ - 结果:成功时返回根卡片 `messageId`;失败时返回 `code` 和中文 `message`:`invalid`(参数不合法)、`not-connected`(飞书未连接或正在关闭)、`forbidden-chat`(目标群未授权)、`key-conflict`、`send-failed`(根卡片发送失败,如机器人不在群里)。
423
407
 
424
- **群里看到的**:先是分析卡「@发起人 于 <triggeredAt> 发起了对 X 应用 Y 错误码不良日志的分析」,下面是日志详情页表格,标题栏标签为「分析中」;紧接着话题内一张流式卡片(它同时创建话题),首轮分析在这里流式输出,带停止按钮。首轮结束后分析卡的标签变成「已完成」「已停止」或「失败」,并加上 DONE / No 表情;之后的追问不再改动分析卡。启动失败时话题里的流式卡片和分析卡都会写明原因。分析过程中出现的审批卡、提问卡发在话题内。DSH 在首轮进行中重启或重连飞书时,话题里的流式卡片照常由中断修复处理;重新连上后,插件把仍是「分析中」的分析卡改成「已中断」(橙色),提示在话题内回复即可继续,原来的 Session 会接着追问。分析卡状态同时记在 `analyze_requests` 记录里(连同重画卡片所需的发起人、时间、应用、错误码和详情页)。
408
+ **群里看到的**:先是根卡片「@发起人 <引导句>」,下面是链接表格,标题栏标签为「<任务名>中」;紧接着话题内一张流式卡片(它同时创建话题),首轮在这里流式输出,带停止按钮。首轮结束后根卡片的标签变成「已完成」「已停止」或「失败」,并加上 DONE / No 表情;之后的追问不再改动根卡片。启动失败时话题里的流式卡片和根卡片都会写明原因。过程中出现的审批卡、提问卡发在话题内。DSH 在首轮进行中重启或重连飞书时,话题里的流式卡片照常由中断修复处理;重新连上后,插件把仍是「<任务名>中」的根卡片改成「已中断」(橙色),提示在话题内回复即可继续,原来的 Session 会接着追问。根卡片状态同时记在发起记录里(连同重画卡片所需的内容)。
425
409
 
426
- **追问与权限**:分析话题的路由是**群内共享**的(路由记录 `shared: true`):群里任何通过准入的成员(管理员,或该群已授权且本人在用户白名单)都可以在话题内直接回复追问(无需 @),也可以点停止按钮、回答审批卡与提问卡、在话题里执行会话命令;未授权的人仍被静默忽略。追问的回复照常流式输出。话题之间互不读取,和普通话题一样。分析结果 markdown 怎么回传给平台由日志分析插件自己负责,本插件不参与。
410
+ **追问与权限**:这类话题的路由是**群内共享**的(路由记录 `shared: true`):群里任何通过准入的成员(管理员,或该群已授权且本人在用户白名单)都可以在话题内直接回复追问(无需 @),也可以点停止按钮、回答审批卡与提问卡、在话题里执行会话命令;未授权的人仍被静默忽略。追问的回复照常流式输出。话题之间互不读取,和普通话题一样。结果怎么回传给外部系统由调用方插件负责,本插件不参与。
427
411
 
428
412
  ## 行为与降级
429
413
 
package/lib/client.js CHANGED
@@ -120,7 +120,7 @@ window.__ModuleLoader__.load({
120
120
  ],
121
121
  access: ["access"],
122
122
  wiki: ["wiki"],
123
- advanced: ["analyze", "logging"]
123
+ advanced: ["logging"]
124
124
  };
125
125
  /** Section heading for one field group inside its tab. */
126
126
  const FIELD_GROUP_LABELS = {
@@ -134,7 +134,6 @@ window.__ModuleLoader__.load({
134
134
  limits: "限额与超时",
135
135
  access: "准入",
136
136
  wiki: "知识库工具",
137
- analyze: "日志分析接口",
138
137
  logging: "日志"
139
138
  };
140
139
  /** Groups rendered collapsed behind one summary row (rarely changed values). */
@@ -250,11 +249,6 @@ window.__ModuleLoader__.load({
250
249
  reconnectDone: "已重启飞书连接",
251
250
  loading: "加载中…",
252
251
  loadFailed: "设置面板加载失败",
253
- analyzeTokenLabel: "访问令牌",
254
- analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
255
- analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
256
- analyzeTokenSaved: "令牌已保存,下一次请求生效",
257
- analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
258
252
  overlayTitle: "Web 覆盖",
259
253
  overlayPath: "覆盖文件",
260
254
  overlayCount: "{n} 项配置由面板覆盖",
@@ -855,7 +849,7 @@ window.__ModuleLoader__.load({
855
849
  ]
856
850
  })]);
857
851
  const heading = originHeading(origin);
858
- if (origin.shared) rows.push(["说明", "日志平台分析话题,群内成员都可以继续"]);
852
+ if (origin.shared) rows.push(["说明", "群共享话题,群内成员都可以继续"]);
859
853
  rows.push(["开始于", formatTime(origin.createdAt)]);
860
854
  rows.push(origin.bound && origin.lastActivityAt !== void 0 ? ["最近活跃", formatTime(origin.lastActivityAt)] : ["状态", "飞书侧已切换到新会话或已过期"]);
861
855
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -2087,6 +2081,47 @@ window.__ModuleLoader__.load({
2087
2081
  });
2088
2082
  }
2089
2083
  //#endregion
2084
+ //#region src/client/panel/advanced-tab.tsx
2085
+ /**
2086
+ * 「高级」tab:SDK 日志级别与 Web 覆盖文件(「恢复 profile 配置」删除整个
2087
+ * 覆盖层,不走 draft)。
2088
+ *
2089
+ * @module dsh-channels/client/panel/advanced-tab
2090
+ */
2091
+ function AdvancedTab(props) {
2092
+ const { form, onClear } = props;
2093
+ const { panel, busy, locked } = form;
2094
+ const overridden = panel.config.overriddenKeys.length;
2095
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2096
+ className: "dsh-channels-cfg__tabBody",
2097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
2098
+ form,
2099
+ groups: TAB_FIELD_GROUPS.advanced
2100
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
2101
+ title: PANEL_COPY.overlayTitle,
2102
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
2103
+ label: PANEL_COPY.overlayPath,
2104
+ summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2105
+ className: "dsh-channels-cfg__mono",
2106
+ children: panel.status.overlayPath
2107
+ }),
2108
+ details: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2109
+ PANEL_COPY.overlayPathHelp,
2110
+ " ",
2111
+ overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })
2112
+ ] }),
2113
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2114
+ type: "button",
2115
+ className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
2116
+ onClick: onClear,
2117
+ disabled: overridden === 0 || busy !== null || locked,
2118
+ children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
2119
+ })
2120
+ })
2121
+ })]
2122
+ });
2123
+ }
2124
+ //#endregion
2090
2125
  //#region src/client/panel/credentials-card.tsx
2091
2126
  /**
2092
2127
  * 凭据行:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示「已配置 /
@@ -2203,62 +2238,6 @@ window.__ModuleLoader__.load({
2203
2238
  });
2204
2239
  }
2205
2240
  //#endregion
2206
- //#region src/client/panel/advanced-tab.tsx
2207
- /**
2208
- * 「高级」tab:日志分析接口(开关 + 访问令牌凭据行)、SDK 日志级别
2209
- * 与 Web 覆盖文件(「恢复 profile 配置」删除整个覆盖层,不走 draft)。
2210
- *
2211
- * @module dsh-channels/client/panel/advanced-tab
2212
- */
2213
- function AdvancedTab(props) {
2214
- const { form, analyzeToken, onClear } = props;
2215
- const { panel, busy, locked } = form;
2216
- const tokenState = panel.config.credentials.analyzeToken;
2217
- const overridden = panel.config.overriddenKeys.length;
2218
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2219
- className: "dsh-channels-cfg__tabBody",
2220
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
2221
- form,
2222
- groups: TAB_FIELD_GROUPS.advanced,
2223
- extra: { analyze: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
2224
- label: PANEL_COPY.analyzeTokenLabel,
2225
- summary: PANEL_COPY.analyzeTokenSummary,
2226
- state: tokenState,
2227
- value: analyzeToken.value,
2228
- secret: true,
2229
- placeholder: PANEL_COPY.analyzeTokenPlaceholder,
2230
- issue: analyzeToken.issues.find((item) => item.key === "analyzeToken"),
2231
- busy: analyzeToken.busy,
2232
- disabled: analyzeToken.busy !== null || analyzeToken.locked || !tokenState.writable,
2233
- onChange: analyzeToken.onChange,
2234
- onSave: analyzeToken.onSave,
2235
- onClear: analyzeToken.onClear
2236
- }) }
2237
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
2238
- title: PANEL_COPY.overlayTitle,
2239
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
2240
- label: PANEL_COPY.overlayPath,
2241
- summary: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2242
- className: "dsh-channels-cfg__mono",
2243
- children: panel.status.overlayPath
2244
- }),
2245
- details: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2246
- PANEL_COPY.overlayPathHelp,
2247
- " ",
2248
- overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })
2249
- ] }),
2250
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2251
- type: "button",
2252
- className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--danger",
2253
- onClick: onClear,
2254
- disabled: overridden === 0 || busy !== null || locked,
2255
- children: busy === "clear" ? PANEL_COPY.clearing : PANEL_COPY.clear
2256
- })
2257
- })
2258
- })]
2259
- });
2260
- }
2261
- //#endregion
2262
2241
  //#region src/client/panel/connection-tab.tsx
2263
2242
  /**
2264
2243
  * 「连接」tab:按 Host 派生的生命周期阶段渲染。
@@ -2731,7 +2710,6 @@ window.__ModuleLoader__.load({
2731
2710
  const [pollError, setPollError] = (0, react.useState)(null);
2732
2711
  const [appIdInput, setAppIdInput] = (0, react.useState)("");
2733
2712
  const [appSecretInput, setAppSecretInput] = (0, react.useState)("");
2734
- const [analyzeTokenInput, setAnalyzeTokenInput] = (0, react.useState)("");
2735
2713
  const [catalog, setCatalog] = (0, react.useState)(null);
2736
2714
  const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
2737
2715
  const [accessNotice, setAccessNotice] = (0, react.useState)(null);
@@ -2760,7 +2738,6 @@ window.__ModuleLoader__.load({
2760
2738
  setDraft({});
2761
2739
  setAppIdInput("");
2762
2740
  setAppSecretInput("");
2763
- setAnalyzeTokenInput("");
2764
2741
  } catch (error) {
2765
2742
  setLoad({
2766
2743
  phase: "error",
@@ -3236,40 +3213,6 @@ window.__ModuleLoader__.load({
3236
3213
  setBusy(null);
3237
3214
  }
3238
3215
  }, [appIdInput, appSecretInput]);
3239
- /** Save the analyze API token; it applies to the next request, no reconnect. */
3240
- const onSaveAnalyzeToken = (0, react.useCallback)(async () => {
3241
- if (analyzeTokenInput.trim() === "") return;
3242
- setBusy("credentials");
3243
- setNotice(null);
3244
- try {
3245
- const result = await saveCredentials({ analyzeToken: analyzeTokenInput });
3246
- if (!result.ok) {
3247
- setNotice({
3248
- kind: "error",
3249
- text: result.error,
3250
- issues: result.issues
3251
- });
3252
- return;
3253
- }
3254
- setLoad({
3255
- phase: "ready",
3256
- panel: result.panel
3257
- });
3258
- setStatus(result.panel.status);
3259
- setAnalyzeTokenInput("");
3260
- setNotice({
3261
- kind: "ok",
3262
- text: PANEL_COPY.analyzeTokenSaved
3263
- });
3264
- } catch (error) {
3265
- setNotice({
3266
- kind: "error",
3267
- text: messageOf(error)
3268
- });
3269
- } finally {
3270
- setBusy(null);
3271
- }
3272
- }, [analyzeTokenInput]);
3273
3216
  const onClearCredential = (0, react.useCallback)(async (field) => {
3274
3217
  if (!(typeof window.confirm === "function" ? window.confirm(PANEL_COPY.credentialRemoveConfirm) : true)) return;
3275
3218
  setBusy("credentials-clear");
@@ -3291,7 +3234,7 @@ window.__ModuleLoader__.load({
3291
3234
  setStatus(result.panel.status);
3292
3235
  setNotice({
3293
3236
  kind: "ok",
3294
- text: field === "analyzeToken" ? PANEL_COPY.analyzeTokenRemoved : result.reconnect === "pending" ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved
3237
+ text: result.reconnect === "pending" ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved
3295
3238
  });
3296
3239
  } catch (error) {
3297
3240
  setNotice({
@@ -3367,26 +3310,6 @@ window.__ModuleLoader__.load({
3367
3310
  onClearCredential,
3368
3311
  onSaveCredentials
3369
3312
  ]);
3370
- const analyzeToken = (0, react.useMemo)(() => ({
3371
- value: analyzeTokenInput,
3372
- issues,
3373
- busy,
3374
- locked: hostBusy,
3375
- onChange: setAnalyzeTokenInput,
3376
- onSave: () => {
3377
- onSaveAnalyzeToken();
3378
- },
3379
- onClear: () => {
3380
- onClearCredential("analyzeToken");
3381
- }
3382
- }), [
3383
- analyzeTokenInput,
3384
- busy,
3385
- hostBusy,
3386
- issues,
3387
- onClearCredential,
3388
- onSaveAnalyzeToken
3389
- ]);
3390
3313
  if (load.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3391
3314
  className: "dsh-channels-cfg",
3392
3315
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3496,7 +3419,6 @@ window.__ModuleLoader__.load({
3496
3419
  }) : null,
3497
3420
  activeTab === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedTab, {
3498
3421
  form,
3499
- analyzeToken,
3500
3422
  onClear: () => {
3501
3423
  onClear();
3502
3424
  }