@modesto_ma/dsh-channels 0.5.0-beta.1 → 0.5.0-beta.3
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 +15 -7
- package/docs/access-control-design.md +1 -0
- package/docs/attachment-pipeline-v2.md +2 -2
- package/docs/feishu-onboarding.md +6 -2
- package/lib/client.js +271 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +3374 -167
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/store.ts +31 -1
- package/src/cards/picker.ts +9 -16
- package/src/cards/wiki.ts +124 -0
- package/src/client/panel/shared.tsx +96 -1
- package/src/client/string-list.ts +139 -0
- package/src/client/styles.ts +3 -0
- package/src/commands/registry.ts +6 -3
- package/src/config.ts +50 -6
- package/src/domain.ts +10 -0
- package/src/feishu/delivery.ts +34 -1
- package/src/feishu/message-read.ts +78 -2
- package/src/feishu/scopes.ts +35 -0
- package/src/feishu/wiki/a1.ts +97 -0
- package/src/feishu/wiki/api.ts +361 -0
- package/src/feishu/wiki/base-api.ts +267 -0
- package/src/feishu/wiki/base-tools.ts +423 -0
- package/src/feishu/wiki/consent.ts +183 -0
- package/src/feishu/wiki/links.ts +122 -0
- package/src/feishu/wiki/scope.ts +66 -0
- package/src/feishu/wiki/shared.ts +284 -0
- package/src/feishu/wiki/sheet-api.ts +250 -0
- package/src/feishu/wiki/sheet-tools.ts +493 -0
- package/src/feishu/wiki/tools.ts +897 -0
- package/src/feishu-channel.ts +23 -0
- package/src/service.ts +87 -7
- package/src/settings/contract.ts +42 -4
- package/src/settings/surface.ts +111 -10
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
8
8
|
- 工作区归属:**每用户一个工作区**(私聊 = 用户;群聊 = 群 × 用户,同群不同用户互不影响);每个话题在工作区内拥有独立工作目录(默认 `topics/<话题 id>/`,Git 仓库工作区可自动改用 worktree),因此话题之间的文件互不干扰,而工作区数量只随用户增长
|
|
9
9
|
- **入站附件(v2,零等待)**:收到消息时**会话、话题、流式卡片立即建立,不等附件下载**。触发消息中的图片自动预取为首轮原生 `ImageBlock`,模型直接可见;文件默认不下载,prompt 用 `<feishu_context>` 清单声明“附件未加载”,模型需要时调用 `feishu_attachment_fetch` 按 `message_id` 懒取;引用消息与话题历史以 `<feishu_quote>` / `<feishu_history>` 摘录,更早的消息用 `feishu_topic_messages` 翻页(发现的资源同时获得 fetch 授权)。**只发附件、不带说明的消息完全静默**:不回复、不建会话、不下载、不加表情,本地也不留任何状态;要让模型看到它,请引用那条消息再提问(多个文件可合并转发后引用),话题内的附件也可由模型用 `feishu_topic_messages` 读到。所有入站图片/文件统一存入 **DSH 原生附件库**(不可变、内容寻址去重、读取校验、随 Session 回放;插件不承担入站附件清理)。显式 `attachmentStore: 'archive'`、宿主未挂载原生附件服务或关闭 fetch 工具时退回旧版工作区 `attachments/` 落盘。完整设计见 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
10
10
|
- **把生成的文件发给用户**:模型在话题工作目录里生成图片、报告、表格等文件后,可调用 `feishu_send_file` 直接发回当前话题/会话(可识别图片 ≤10 MB 以图片消息发送,其余以文件消息发送,文件上限 30 MB)。只能发送**当前话题工作目录内**的普通文件(`..` 与符号链接都按真实路径校验,不能读到其他话题或系统文件);需开通 `im:resource` 权限,缺失时工具返回可读的 `PERMISSION_DENIED`,回合不受影响。开关:`feishuTools.sendFile`(默认开)
|
|
11
|
+
- **知识库读写**:开启 `feishuTools.wikiRead` / `feishuTools.wikiWrite`(默认都关)并在 `wikiRoots` 里配置知识空间链接 `https://<租户>.feishu.cn/wiki/space/<space_id>` 或节点链接 `https://<租户>.feishu.cn/wiki/<node_token>` 后,模型可用 `feishu_wiki_list` 浏览、`feishu_wiki_read` 读取飞书文档(Markdown,或带 block id 的飞书文档 XML,支持目录/章节/区间/关键词局部读取)、`feishu_wiki_create` 在范围内新建飞书文档、电子表格或多维表格(新节点也可作为子目录继续往下建)、`feishu_wiki_edit` 修改**本应用创建的**文档(追加、在块后插入、替换/删除块、文本替换、整篇重写;必须带上读取时的 `revision_id`,文档被他人改过会被拒绝)。电子表格用 `feishu_wiki_sheet_read` 按子表和 A1 范围读成 CSV(每行带 `[row=N]` 真实行号),`feishu_wiki_sheet_edit` 向**本应用创建的**表格写入单元格值或公式、在数据末尾追加行(追加位置会越过空行找到真正的最后一行,目标单元格非空时失败、不会覆盖;同样校验 `revision_id`;不改样式、图表和行列结构)。多维表格用 `feishu_wiki_base_read` 列数据表、读字段和记录(支持筛选、排序、关键词搜索、分页),`feishu_wiki_base_edit` 增改删记录:**本应用创建的**多维表格可增改删,其他多维表格**只能新增记录**(不改表和字段结构)。所有调用都用应用身份,并且:只有**管理员或知识库白名单**(`/auth wiki add @用户`)里的人发起的话题能用;`wikiRoots` 之外一律拒绝;打开某个文档或表格、每次新建、每次修改都会在话题里弹出**授权卡**,只有话题发起人能点(读取授权在本话题内对该文档持续有效,`/renew` 或重启后重新询问),超时(`approvalTimeoutMs`)自动拒绝;浏览目录不弹卡。需开通 `wiki:wiki`、`docx:document`、`sheets:spreadsheet`、`bitable:app`(只读可用 `wiki:wiki:readonly` / `docx:document:readonly` / `sheets:spreadsheet:readonly` / `bitable:app:readonly`),且应用要是目标知识空间的成员(可把包含机器人的群加为空间成员/管理员)
|
|
11
12
|
- **HITL 审批**:当模型请求需要授权的动作(如 bash 沙箱升级 `sandbox_permissions`)时,插件监听主机 `approval/request` 事件,在同一话题里**新发一张独立审批决策卡**:卡上直接显示将执行的命令(或工具参数)和提权目标,提供 ✅ 允许 / ❌ 拒绝 / ♾️ 本话题不再询问 三个按钮;也可以在话题里直接回复「允许」「拒绝」「全部允许」,回复其他内容视为拒绝并作为下一条指令。等待期间流式主卡显示「⏳ 等待你审批」。超时(`approvalTimeoutMs`,默认 5 分钟)自动拒绝(fail-closed),非会话发起人点击或回复无效
|
|
12
13
|
- **结构化提问**:模型调用 `ask_user_question` 时,插件监听主机 `user-questions/request` 瀑布并发一张问答卡(每个问题一组选项按钮,多选先勾选再「提交本题选择」),答案回填给模型;主机自带的应答者只有 Web 客户端,没有本插件时纯飞书会话会以 `NO_PROVIDER` 失败。超时(`questionTimeoutMs`,默认 5 分钟)或回合取消时本轮提问失败(fail-closed);没有可选项的问题直接提示用文字作答,**阻塞期间普通文本回复即作为该问题的自由文本答案**(同时支持多选后的自定义文本)
|
|
13
14
|
- **准入与授权(默认开启)**:**管理员不受白名单限制**(私聊、任意群聊——包括尚未授权的群——都能直接发起会话);其他人私聊只收到一条固定提示(`/userid` 例外),群聊需要「群已授权 + 用户在全局白名单」两层同时命中。机器人被拉进群后私聊管理员发**群信息卡**(未授权群附授权按钮,用于放行白名单用户);已授权群里未授权用户 @ 会触发用户授权卡。授权入口:飞书 `/auth` 命令、授权卡、Web 设置面板;管理员 user_id 在面板配置(私聊 `/userid` 获取),**留空时自动使用飞书应用所有者**
|
|
@@ -18,10 +19,10 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
18
19
|
|
|
19
20
|
## 斜杠命令
|
|
20
21
|
|
|
21
|
-
- `/help`:列出当前支持的斜杠命令,无需绑定 Session
|
|
22
|
+
- `/help`:列出当前支持的斜杠命令,无需绑定 Session;私聊和普通群聊中直接回复、不创建话题,话题群或已有话题内则回复在该话题中
|
|
22
23
|
- `/userid`:仅在私聊中直接回复**发送者自己的** user_id,无需绑定 Session,也不创建话题;**任何私聊用户都可以用**(准入配置/授权引导用,不消耗 token)
|
|
23
24
|
- `/groupid`:仅在群聊中直接回复当前群组 ID,无需绑定 Session,也不创建话题;普通群聊遵循 `requireMention`,话题内无需重复 @
|
|
24
|
-
- `/health`:显示 Harness 与飞书通道健康状态,无需绑定 Session
|
|
25
|
+
- `/health`:显示 Harness 与飞书通道健康状态,无需绑定 Session;回复方式同 `/help`(直接回复,话题内回在话题中)
|
|
25
26
|
- `/status`:显示当前 Session 状态与性能指标
|
|
26
27
|
- `/workspace`:显示当前 Session 工作目录
|
|
27
28
|
- `/effort`:查看当前 Session 推理强度;当前模型声明了可选档位时,回复卡附档位按钮,点一下即切换;`/effort <档位-id>` 切换,`/effort reset` 恢复配置默认
|
|
@@ -30,11 +31,11 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
30
31
|
- `/renew`:在当前话题创建并绑定新 Session
|
|
31
32
|
- `/stop`:中断当前正在运行的回合(不冷恢复 Session;排队中的后续消息保留并接着运行;没有运行中的回合时明确提示)。流式卡片上的「⏹ 停止」按钮效果相同
|
|
32
33
|
- `/permissions`:查看当前 Session 的权限预设(沙箱 + 审批策略)与可选清单;仅在 `allowPermissionSwitch: true` 时支持 `/permissions <name>` 切换,此时回复卡附预设按钮
|
|
33
|
-
- `/auth`:**仅管理员**。`/auth status` 查看准入状态;`/auth allow` / `/auth revoke` 在群内授权/撤销当前群;`/auth user add @用户` / `/auth user remove @用户`(也可直接写 `user_id`)管理全局用户白名单,@ 时自动取被 @ 成员的 user_id、忽略对机器人的 @;`/auth list` 列出白名单与待处理申请。即使群尚未授权,管理员也能在群内用 `/auth`(这是“通过对话授权群”的入口)
|
|
34
|
+
- `/auth`:**仅管理员**。`/auth status` 查看准入状态;`/auth allow` / `/auth revoke` 在群内授权/撤销当前群;`/auth user add @用户` / `/auth user remove @用户`(也可直接写 `user_id`)管理全局用户白名单,@ 时自动取被 @ 成员的 user_id、忽略对机器人的 @;`/auth wiki add @用户` / `/auth wiki remove @用户` 管理知识库白名单(与对话白名单相互独立,管理员始终可用);`/auth list` 列出白名单与待处理申请。即使群尚未授权,管理员也能在群内用 `/auth`(这是“通过对话授权群”的入口)
|
|
34
35
|
- `/history`:显示当前话题的 Session 历史(含 `ctx.sessionQuery` 折叠出的会话标题与 live 标记)
|
|
35
36
|
- `/export`:导出当前 Session 的逻辑日志
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
选择卡的选项单列排列;按钮等同于在该卡下输入对应命令(如点「deepseek-v4-pro」= `/model deepseek-v4-pro`),走同样的准入、归属与校验,执行结果原地替换该选择卡(更新失败时才另发一张结果卡);只有话题发起人点击、且卡片仍在原聊天里时才生效,话题换绑 Session 后旧卡上的按钮失效。按钮最多显示 12 个,其余可直接输入命令。
|
|
38
39
|
|
|
39
40
|
`/new` 是 `/renew` 的别名。`/help` 由命令注册表按分组生成,新增命令只需在注册表里加一行。
|
|
40
41
|
|
|
@@ -72,7 +73,7 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
72
73
|
| 标签页 | 内容 |
|
|
73
74
|
| --- | --- |
|
|
74
75
|
| 连接 | 连接状态与连接信息(长连接状态、Bot open_id、运行时长、话题路由 / 活动会话 / 在途回复)、权限自检、斜杠指令面板同步结果、未配置时的三步引导、飞书应用凭据、最近一次连接失败原因、需重连的待生效提醒 |
|
|
75
|
-
| 会话与行为 | 「行为」:私聊模式、普通群 @ 要求、话题兜底、首次私聊引导、原生斜杠指令面板同步、权限切换;「内容与附件」:附件接收、存储后端、图片预取与上限、首轮历史;「模型工具」:`feishu_attachment_fetch` / `feishu_topic_messages` / `feishu_send_file`
|
|
76
|
+
| 会话与行为 | 「行为」:私聊模式、普通群 @ 要求、话题兜底、首次私聊引导、原生斜杠指令面板同步、权限切换;「内容与附件」:附件接收、存储后端、图片预取与上限、首轮历史;「模型工具」:`feishu_attachment_fetch` / `feishu_topic_messages` / `feishu_send_file` / 知识库工具开关与上限、`wikiRoots`、被动读取预算 |
|
|
76
77
|
| 模型 | 模型服务商、模型 id、推理档位、Agent 预设(宿主目录驱动的级联下拉) |
|
|
77
78
|
| 工作区 | 工作区根目录(可点「选择目录…」调用系统目录选择框);话题目录隔离方式(`topicIsolation`)在 profile 配置中设置 |
|
|
78
79
|
| 访问授权 | 管理员 user_id、准入模式、申请有效期;已授权群 / 用户白名单与待处理申请 |
|
|
@@ -81,7 +82,7 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
81
82
|
**入站附件与模型工具(v2)**:「会话与行为」标签页新增两组可编辑字段,与 profile 配置同名、写入同一 Web 覆盖文件(`$DSH_HOME/dsh-channels/settings.json`):
|
|
82
83
|
|
|
83
84
|
- **内容与附件**:附件接收(`acceptAttachments`)、入站附件存储后端(`attachmentStore`:DSH 原生附件库 / 工作区 `attachments/`)、触发消息图片预取(`prefetchImages`)及单图 / 总量 / 整段超时上限(`prefetchImageMaxBytes` / `prefetchTotalMaxBytes` / `prefetchTimeoutMs`)、首轮话题历史条数(`historyBootstrapLimit`)。
|
|
84
|
-
- **模型工具**:`feishu_attachment_fetch` 开关与单次资源数 / 字节预算 / 超时(`feishuTools.attachmentFetch*`)、`feishu_topic_messages` 开关与超时(`feishuTools.topicMessages*`)、`feishu_send_file` 开关(`feishuTools.sendFile
|
|
85
|
+
- **模型工具**:`feishu_attachment_fetch` 开关与单次资源数 / 字节预算 / 超时(`feishuTools.attachmentFetch*`)、`feishu_topic_messages` 开关与超时(`feishuTools.topicMessages*`)、`feishu_send_file` 开关(`feishuTools.sendFile`)、知识库读/写开关(`feishuTools.wikiRead` / `feishuTools.wikiWrite`)与知识库范围 `wikiRoots`(每条链接一个输入框,逐条校验)、被动引用/历史读取预算(`promptContextTimeoutMs`)。
|
|
85
86
|
|
|
86
87
|
`feishuTools.*` 是嵌套配置:面板只把改动过的子键写进覆盖层,其余子键保持原值(Web 覆盖或 profile 配置),不会因部分保存变成未定义。字段默认值、边界与降级规则见下文配置示例和 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
87
88
|
|
|
@@ -169,6 +170,7 @@ src/
|
|
|
169
170
|
│ ├── slash-commands.ts # 连接后把命令注册表幂等同步到飞书原生斜杠指令面板
|
|
170
171
|
│ ├── tools.ts # feishu_attachment_fetch / feishu_topic_messages 与每 route 白名单
|
|
171
172
|
│ ├── send-file.ts # feishu_send_file:把话题工作目录内的图片/文件发回当前会话
|
|
173
|
+
│ ├── wiki/ # 知识库工具:links(wikiRoots 链接解析)、api(wiki/docx/docs_ai 接口)、sheet-api / base-api(电子表格、多维表格接口)、scope(范围判定)、consent(授权卡)、shared(失败码与公共步骤)、tools / sheet-tools / base-tools、a1(A1 范围)
|
|
172
174
|
│ └── agent-context.ts # agent 作用域 system prompt:native/archive 双模式附件指引
|
|
173
175
|
├── routing/
|
|
174
176
|
│ └── inbound.ts # 入站路由:去重、准入、路由决策树、建/续会话、首触引导
|
|
@@ -263,6 +265,11 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
263
265
|
topicMessages: true # feishu_topic_messages:读取引用消息 / 话题历史并授权其附件
|
|
264
266
|
topicMessagesTimeoutMs: 20000 # 单次话题读取整段超时(20 s;被动摘录用独立 3 s 预算)
|
|
265
267
|
sendFile: true # feishu_send_file:把话题工作目录内生成的图片/文件发给用户(需 im:resource)
|
|
268
|
+
wikiRead: false # feishu_wiki_list / feishu_wiki_read / feishu_wiki_sheet_read / feishu_wiki_base_read:浏览 wikiRoots 内的知识库,读取文档、电子表格、多维表格
|
|
269
|
+
wikiWrite: false # feishu_wiki_create / feishu_wiki_edit / feishu_wiki_sheet_edit / feishu_wiki_base_edit:新建文档/表格,修改本应用创建的文档和表格,向多维表格写记录
|
|
270
|
+
wikiRoots: # 知识库范围(空 = 不挂载知识库工具;最多 20 条)
|
|
271
|
+
- 'https://mi.feishu.cn/wiki/space/7xxxxxxxxxxxxxxxxx' # 整个知识空间
|
|
272
|
+
- 'https://mi.feishu.cn/wiki/AbCdEfGhIjKlMn' # 某个节点及其下所有子节点
|
|
266
273
|
welcomeOnboarding: true # 私聊首次触达发一张引导卡(按 chat 持久化去重)
|
|
267
274
|
slashCommandPanel: false # 连接时把命令同步到飞书原生 / 面板(需 app_slash_command 读写权限;改后需重连)
|
|
268
275
|
allowPermissionSwitch: false # 是否允许 /permissions <name> 从飞书侧切换权限预设(安全敏感)
|
|
@@ -312,11 +319,12 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
312
319
|
- 「获取应用信息」权限(**可选**,如 `admin:app.info:readonly` / `application:application:self_manage`,以控制台要求为准):未配置 `adminUserId` 时,插件在每次连接时调用 `application/v6/applications/:app_id` 查询应用所有者并让其成为管理员;调不通(企业内不便申请该权限等)时日志与面板会给出飞书返回的错误码,并提示在「访问授权」手动填写管理员 user_id,填写后不再依赖它
|
|
313
320
|
- 回调:**卡片回传交互 card.action.trigger**(流式卡停止按钮、`/model` 等选择卡、HITL 审批 / `ask_user_question` 问答按钮的回传事件,同样走长连接)
|
|
314
321
|
- `im:resource`(获取与上传图片或文件资源,**可选**;`im:resource:upload` 亦可):`feishu_send_file` 上传图片/文件后发给用户。未开通时该工具返回可读的 `PERMISSION_DENIED`;关闭 `feishuTools.sendFile` 后自检不再检查此项
|
|
322
|
+
- `wiki:wiki`、`docx:document`、`sheets:spreadsheet`、`bitable:app`(**可选**,仅开启知识库工具时需要;只读可用 `wiki:wiki:readonly` / `docx:document:readonly` / `sheets:spreadsheet:readonly` / `bitable:app:readonly`,也接受新版细分权限 `sheets:spreadsheet:read` / `sheets:spreadsheet:write_only` / `base:record:read` / `base:record:create` / `base:record:update` / `base:record:delete`):浏览知识库节点,读取/新建/修改飞书文档、电子表格、多维表格。应用还必须是目标知识空间的成员,否则调用返回 `NO_ACCESS`
|
|
315
323
|
- `im:message.reactions:write_only`(发送、删除消息表情回复)
|
|
316
324
|
- `im:message:update`(更新卡片)
|
|
317
325
|
|
|
318
326
|
> 消息读取权限缺失时不会报死错:引用摘录与首轮话题历史静默省略(插件生命周期内首次降级输出一条 warn,之后 debug 级),显式调用 `feishu_topic_messages` 返回可读的 `PERMISSION_DENIED`。附件下载仍依赖 `im:message` 一类的消息资源权限;下载被拒时工具与 archive 路径都会返回可读原因。
|
|
319
|
-
> **连接后权限自检**:每次连上飞书后,插件调用 `application/v6/scopes` 读取应用的租户权限授予状态,与上面的清单对比(私聊关闭时不检查 `im:message.p2p_msg:readonly`,`requireMention: false` 时 `im:message.group_msg` 升为必需;`feishuTools.sendFile` 关闭时不检查 `im:resource`;未配置管理员且应用所有者查询被飞书拒绝时追加可选项「获取应用信息」)。缺失项在 Web 设置「连接」页以红字列出(标注必需/可选与用途,每项附开发者后台权限页深链,另有一键打开全部缺失项的链接),并在日志打印一条 warn;读取失败时显示飞书返回的原因。自检只做提示,不阻断连接;开通权限后需发布新版本并重新连接才会刷新。
|
|
327
|
+
> **连接后权限自检**:每次连上飞书后,插件调用 `application/v6/scopes` 读取应用的租户权限授予状态,与上面的清单对比(私聊关闭时不检查 `im:message.p2p_msg:readonly`,`requireMention: false` 时 `im:message.group_msg` 升为必需;`feishuTools.sendFile` 关闭时不检查 `im:resource`;知识库工具未挂载时不检查 `wiki:wiki` / `docx:document` / `sheets:spreadsheet` / `bitable:app`;未配置管理员且应用所有者查询被飞书拒绝时追加可选项「获取应用信息」)。缺失项在 Web 设置「连接」页以红字列出(标注必需/可选与用途,每项附开发者后台权限页深链,另有一键打开全部缺失项的链接),并在日志打印一条 warn;读取失败时显示飞书返回的原因。自检只做提示,不阻断连接;开通权限后需发布新版本并重新连接才会刷新。
|
|
320
328
|
3. 事件订阅方式选**长连接**(WebSocket,免公网)。
|
|
321
329
|
4. 发布版本并把用户加入可用范围;将机器人拉入测试群。
|
|
322
330
|
|
|
@@ -335,6 +335,7 @@ bot 回显过滤 / chatType / 空内容 / dmMode
|
|
|
335
335
|
| `/auth revoke` | 群聊(管理员) | 把当前群移出白名单,立即生效 |
|
|
336
336
|
| `/auth user add <@用户\|user_id>` | 私聊/群聊(管理员) | 加入用户白名单;`@成员` 取 mention 里的 user_id,忽略对机器人的 @,没有 user_id 的外部用户被拒绝 |
|
|
337
337
|
| `/auth user remove <open_id>` | 同上 | 移出用户白名单 |
|
|
338
|
+
| `/auth wiki add <@用户\|user_id>` / `/auth wiki remove <@用户\|user_id>` | 同上 | 加入/移出知识库白名单(`access_wiki_users` 表):只决定该用户发起的话题能否使用知识库工具,不授予对话权限;管理员始终可用 |
|
|
338
339
|
| `/auth list` | 任意(管理员) | 列出群白名单、用户白名单与待处理申请 |
|
|
339
340
|
|
|
340
341
|
- 非管理员发 `/auth` 一律拒绝(先校验管理员身份,再解析子命令)。
|
|
@@ -228,8 +228,8 @@ native 后端下,准入通过后由 `inboundAttachmentState` 做**不下载的
|
|
|
228
228
|
| `promptContextTimeoutMs` | `3000` | 100 – 10000 ms;被动引用/历史预算 | 模型工具 |
|
|
229
229
|
| `feishuTools.attachmentFetch` | `true` | 关闭即整体回退 archive | 模型工具 |
|
|
230
230
|
| `feishuTools.attachmentFetchMaxResources` | `10` | 1 – 50 | 模型工具 |
|
|
231
|
-
| `feishuTools.attachmentFetchMaxBytes` | `104857600`(100 MiB) | 1 B
|
|
232
|
-
| `feishuTools.attachmentFetchTimeoutMs` | `120000` | 1000 –
|
|
231
|
+
| `feishuTools.attachmentFetchMaxBytes` | `104857600`(100 MiB) | ≥ 1 B,无上限 | 模型工具 |
|
|
232
|
+
| `feishuTools.attachmentFetchTimeoutMs` | `120000` | 1000 – 2147483647 ms | 模型工具 |
|
|
233
233
|
| `feishuTools.topicMessages` | `true` | 关闭只关模型工具,被动摘录仍生效 | 模型工具 |
|
|
234
234
|
| `feishuTools.topicMessagesTimeoutMs` | `20000` | 1000 – 120000 ms | 模型工具 |
|
|
235
235
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
| `im:message.reactions:write_only`(发送、删除消息表情回复) | Typing / DONE / No 表态 |
|
|
26
26
|
| `im:resource`(获取与上传图片或文件资源,**可选**;`im:resource:upload` 亦可) | 模型用 `feishu_send_file` 把生成的图片/文件发给用户;不开通时该工具返回可读错误 |
|
|
27
27
|
| `im:message:update`(更新消息卡片) | 流式卡片刷新(patch) |
|
|
28
|
+
| `wiki:wiki`、`docx:document`、`sheets:spreadsheet`、`bitable:app`(**可选**,只读可用对应的 `:readonly` 权限) | 知识库工具(`feishuTools.wikiRead` / `wikiWrite` + `wikiRoots`):浏览知识库,读取/新建/修改飞书文档、电子表格、多维表格;应用还需是目标知识空间的成员 |
|
|
28
29
|
| `im:chat:readonly` 一类(获取群信息) | 入群通知卡展示群名/成员数(具体 scope 以控制台要求为准) |
|
|
29
30
|
| 「获取应用信息」一类(如 `admin:app.info:readonly` / `application:application:self_manage`,**可选**,以控制台要求为准) | 未配置 `adminUserId` 时连接后自动把应用所有者设为管理员;不便申请时跳过即可,按 3.3 手动设置管理员 |
|
|
30
31
|
| `application:app_slash_command:read` 与 `application:app_slash_command:write`(读取/管理应用斜杠指令,可选,仅 `slashCommandPanel: true` 时需要) | 连接后把插件命令同步到飞书原生斜杠指令面板(输入 `/` 时列出,客户端约 5 分钟后刷新);缺失时只是面板没有列表,直接输入命令不受影响 |
|
|
@@ -167,8 +168,8 @@ chmod 600 $DSH_HOME/.credentials.yaml
|
|
|
167
168
|
| `promptContextTimeoutMs` | `3000` | 被动引用/首轮历史读取的整段预算(100–10000 ms);超时即省略对应块、回合照跑。模型主动调用工具的读取超时另由 `feishuTools.topicMessagesTimeoutMs` 控制 |
|
|
168
169
|
| `feishuTools.attachmentFetch` | `true` | 是否挂载 `feishu_attachment_fetch`;关闭后文件无法懒取,插件**整体回退 archive 后端** |
|
|
169
170
|
| `feishuTools.attachmentFetchMaxResources` | `10` | 单次工具调用最多取回的附件数(1–50);超出返回可读原因,可按 `file_key` 拆分重试 |
|
|
170
|
-
| `feishuTools.attachmentFetchMaxBytes` | `104857600` | 单次工具调用的总下载字节预算(100 MiB
|
|
171
|
-
| `feishuTools.attachmentFetchTimeoutMs` | `120000` | 单次 `feishu_attachment_fetch` 整段超时(1000–
|
|
171
|
+
| `feishuTools.attachmentFetchMaxBytes` | `104857600` | 单次工具调用的总下载字节预算(100 MiB,≥ 1 B,无上限);文件流式写入、不整文件缓冲 |
|
|
172
|
+
| `feishuTools.attachmentFetchTimeoutMs` | `120000` | 单次 `feishu_attachment_fetch` 整段超时(1000–2147483647 ms);到期取消并逐项报告;大文件需同步调大 |
|
|
172
173
|
| `feishuTools.topicMessages` | `true` | 是否挂载 `feishu_topic_messages`;只影响模型工具,被动引用/历史摘录仍生效 |
|
|
173
174
|
| `feishuTools.topicMessagesTimeoutMs` | `20000` | 单次 `feishu_topic_messages` 整段超时(1000–120000 ms) |
|
|
174
175
|
| `loggerLevel` | `info` | 官方 SDK 日志级别 |
|
|
@@ -190,6 +191,7 @@ chmod 600 $DSH_HOME/.credentials.yaml
|
|
|
190
191
|
- 已授权群里未授权用户 @ 机器人 → 管理员收到**用户授权申请卡**(含昵称/user_id)→ 点「授权用户」;
|
|
191
192
|
- 管理员在群里发 `/auth user add @用户`(或 `/auth user add <user_id>`);`/auth user remove @用户` 撤销;外部用户没有 user_id,无法授权;
|
|
192
193
|
- Web 面板「访问与授权 → 已授权用户」手动添加 user_id(用户可用 `/userid` 查看);
|
|
194
|
+
- 知识库工具另有一份**知识库白名单**:管理员发 `/auth wiki add @用户`(或 `<user_id>`)后,该用户发起的话题才能使用知识库工具,`/auth wiki remove @用户` 撤销;管理员始终可用;
|
|
193
195
|
6. 授权/撤销**立即生效**;白名单与待处理申请持久化在插件 `channels_routes_v2` 域,重启不丢。`/auth list` 可随时查看白名单与待处理申请。
|
|
194
196
|
|
|
195
197
|
> 说明:**管理员不受白名单限制**——机器人被拉进新群后,管理员可以直接 @ 机器人开会话,不需要先授权该群;授权群/用户只影响**非管理员**。管理员在任意群里都可以使用 `/auth`、`/groupid` 等命令。
|
|
@@ -268,6 +270,8 @@ DSH_HOME=$DSH_HOME pnpm dsh web # 默认 http://127.0.0.1:3080
|
|
|
268
270
|
| 14 | 管理员直通(免群授权) | 刚被拉进未授权群时,管理员直接 @ 机器人发消息 | 正常开会话(管理员不受白名单限制);不授权该群也不影响管理员使用 |
|
|
269
271
|
| 14a | 群授权与群内准入 | 管理员点「授权本群」(或在群内发 `/auth allow`);未授权用户 @;管理员 `/auth user add <user_id>` 后该用户再 @ | 授权前非管理员 @ 无响应(管理员除外);授权后白名单用户可分别开会话;未授权用户 @ 不建会话且管理员收到用户授权卡;授权该用户后可正常开会话 |
|
|
270
272
|
| 15 | 撤销与持久化 | `/auth revoke`(群内)/ `/auth user remove <user_id>`;重启 dsh 后再试 | 撤销后对应主体立即无法开启对话;重启后已授权群/用户仍有效(持久化) |
|
|
273
|
+
| 15a | 知识库读写 | 开启 `wikiRead`/`wikiWrite`、配置 `wikiRoots`;白名单用户在话题里让模型列目录、读一篇文档、新建一篇、再修改它 | 列目录不弹卡;第一次读取弹「📚 知识库授权」卡,只有话题发起人能点,允许后同一篇再读不弹卡;新建、修改每次都弹卡;修改人工创建的文档返回 `NOT_OWNED`;不在白名单的用户发起的话题返回 `NOT_AUTHORIZED` |
|
|
274
|
+
| 15b | 知识库表格 | 同上;让模型读一个电子表格、新建一个电子表格并写入几行再追加一行;读一个人建的多维表格并新增一条记录、尝试修改已有记录;新建一个多维表格后增改删记录 | 表格读取返回带 `[row=N]` 的 CSV;写入和追加落在正确的单元格;人建的电子表格修改返回 `NOT_OWNED`;人建的多维表格能新增记录、修改/删除返回 `NOT_OWNED`;本应用建的多维表格可增改删;每次写入都弹卡 |
|
|
271
275
|
| 16 | 话题越权防护 | 授权群里用户 A 创建话题后,用户 B 在 A 的话题里发普通消息/附件/回答提问 | B 的消息被静默丢弃,不会进入 A 的 Session、不会写入 A 的话题目录;A 自己发言正常 |
|
|
272
276
|
|
|
273
277
|
### 5.0 HITL 审批验证点(重点)
|
package/lib/client.js
CHANGED
|
@@ -185,6 +185,12 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
discardTab: "放弃本页更改",
|
|
186
186
|
saveHintClean: "没有未保存的更改。",
|
|
187
187
|
dirtyCount: "未保存项",
|
|
188
|
+
listAdd: "添加",
|
|
189
|
+
listRemove: "移除",
|
|
190
|
+
listEntry: "第 {n} 项",
|
|
191
|
+
listTooMany: "最多 {max} 项(空输入框不计)",
|
|
192
|
+
listNotArray: "必须是字符串列表",
|
|
193
|
+
listEntryNotString: "必须是字符串",
|
|
188
194
|
browseDirectory: "选择目录…",
|
|
189
195
|
browsingDirectory: "选择中…",
|
|
190
196
|
directoryPicked: "已选择目录",
|
|
@@ -564,6 +570,203 @@ window.__ModuleLoader__.load({
|
|
|
564
570
|
});
|
|
565
571
|
}
|
|
566
572
|
//#endregion
|
|
573
|
+
//#region src/feishu/wiki/links.ts
|
|
574
|
+
/**
|
|
575
|
+
* Feishu wiki (知识库) link parsing: the `wikiRoots` allow-list entries and
|
|
576
|
+
* the document references the model passes to the wiki tools.
|
|
577
|
+
*
|
|
578
|
+
* Pure module: no I/O. Hosts are not checked because tenants use their own
|
|
579
|
+
* subdomains (`<tenant>.feishu.cn`) and the API never uses the link host.
|
|
580
|
+
*
|
|
581
|
+
* @module dsh-channels/feishu/wiki/links
|
|
582
|
+
*/
|
|
583
|
+
/** Feishu wiki space id: decimal digits. */
|
|
584
|
+
const SPACE_ID_PATTERN = /^\d{1,32}$/u;
|
|
585
|
+
/** Feishu wiki node / docx token: letters and digits. */
|
|
586
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9]{8,64}$/u;
|
|
587
|
+
/** Readable reasons for {@link WikiRootLinkError}. */
|
|
588
|
+
const WIKI_ROOT_LINK_ERRORS = {
|
|
589
|
+
"not-a-url": "不是有效的链接",
|
|
590
|
+
"not-https": "必须是 https 链接",
|
|
591
|
+
"not-a-wiki-link": "只支持知识空间链接 https://<租户>.feishu.cn/wiki/space/<space_id> 或节点链接 https://<租户>.feishu.cn/wiki/<node_token>"
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Parse one `wikiRoots` entry.
|
|
595
|
+
*
|
|
596
|
+
* @param link Link as configured (surrounding whitespace is ignored).
|
|
597
|
+
* @returns The root, or the reason it is invalid.
|
|
598
|
+
*/
|
|
599
|
+
function parseWikiRootLink(link) {
|
|
600
|
+
const trimmed = link.trim();
|
|
601
|
+
let url;
|
|
602
|
+
try {
|
|
603
|
+
url = new URL(trimmed);
|
|
604
|
+
} catch {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
error: "not-a-url"
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (url.protocol !== "https:") return {
|
|
611
|
+
ok: false,
|
|
612
|
+
error: "not-https"
|
|
613
|
+
};
|
|
614
|
+
const segments = url.pathname.split("/").filter((segment) => segment !== "");
|
|
615
|
+
if (segments[0] !== "wiki") return {
|
|
616
|
+
ok: false,
|
|
617
|
+
error: "not-a-wiki-link"
|
|
618
|
+
};
|
|
619
|
+
if (segments.length === 3 && segments[1] === "space" && SPACE_ID_PATTERN.test(segments[2])) return {
|
|
620
|
+
ok: true,
|
|
621
|
+
root: {
|
|
622
|
+
kind: "space",
|
|
623
|
+
spaceId: segments[2],
|
|
624
|
+
link: trimmed
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
if (segments.length === 2 && segments[1] !== "space" && TOKEN_PATTERN.test(segments[1])) return {
|
|
628
|
+
ok: true,
|
|
629
|
+
root: {
|
|
630
|
+
kind: "node",
|
|
631
|
+
nodeToken: segments[1],
|
|
632
|
+
link: trimmed
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
error: "not-a-wiki-link"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/client/string-list.ts
|
|
642
|
+
/**
|
|
643
|
+
* 浏览器半区的 `string-list` 字段辅助:把草稿值读成条目数组,并在保存前
|
|
644
|
+
* 逐项校验,供面板即时标红。Host 保存时按同一规则复核(见
|
|
645
|
+
* `settings/surface.ts` 的 `normalizeStringList`)。
|
|
646
|
+
*
|
|
647
|
+
* 不含 JSX/React,便于单测。条目格式校验直接复用纯函数模块
|
|
648
|
+
* `feishu/wiki/links.ts`(无运行时依赖,可内联进 Client bundle)。
|
|
649
|
+
*
|
|
650
|
+
* @module dsh-channels/client/string-list
|
|
651
|
+
*/
|
|
652
|
+
/** Entry checks per item format: the readable reason, or `undefined` when valid. */
|
|
653
|
+
const ITEM_FORMAT_CHECKS = { "wiki-root-link": (entry) => {
|
|
654
|
+
const parsed = parseWikiRootLink(entry);
|
|
655
|
+
return parsed.ok ? void 0 : WIKI_ROOT_LINK_ERRORS[parsed.error];
|
|
656
|
+
} };
|
|
657
|
+
/**
|
|
658
|
+
* Entries of a `string-list` value; any other value reads as no entries.
|
|
659
|
+
*
|
|
660
|
+
* @param value - Draft or effective value of a `string-list` field.
|
|
661
|
+
* @returns The entries as stored (untrimmed).
|
|
662
|
+
*/
|
|
663
|
+
function listItems(value) {
|
|
664
|
+
return typeof value === "object" ? value : [];
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Validate the entries of one `string-list` draft the way the Host will:
|
|
668
|
+
* empty inputs are ignored, every other entry is trimmed and checked against
|
|
669
|
+
* the field's `itemFormat`, and the non-empty count is bounded by `maxItems`.
|
|
670
|
+
*
|
|
671
|
+
* @param field - The Host descriptor (`kind: 'string-list'`).
|
|
672
|
+
* @param items - The draft entries, empty inputs included.
|
|
673
|
+
* @returns Per-entry issues (with `index` into `items`) and a whole-field issue when over `maxItems`.
|
|
674
|
+
*/
|
|
675
|
+
function stringListIssues(field, items) {
|
|
676
|
+
const issues = [];
|
|
677
|
+
const check = field.itemFormat === void 0 ? void 0 : ITEM_FORMAT_CHECKS[field.itemFormat];
|
|
678
|
+
let count = 0;
|
|
679
|
+
items.forEach((item, index) => {
|
|
680
|
+
const entry = item.trim();
|
|
681
|
+
if (entry === "") return;
|
|
682
|
+
count += 1;
|
|
683
|
+
const reason = check?.(entry);
|
|
684
|
+
if (reason === void 0) return;
|
|
685
|
+
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
686
|
+
issues.push({
|
|
687
|
+
key: field.key,
|
|
688
|
+
index,
|
|
689
|
+
message: `${position}「${entry}」:${reason}`
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
if (field.maxItems !== void 0 && count > field.maxItems) issues.push({
|
|
693
|
+
key: field.key,
|
|
694
|
+
message: PANEL_COPY.listTooMany.replace("{max}", String(field.maxItems))
|
|
695
|
+
});
|
|
696
|
+
return issues;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Turn one `string-list` draft into its save payload. Entries are submitted as
|
|
700
|
+
* typed (untrimmed, empty inputs included) so Host issue indexes point at the
|
|
701
|
+
* same rendered inputs; the Host trims and drops empty inputs.
|
|
702
|
+
*
|
|
703
|
+
* @param field - The Host descriptor (`kind: 'string-list'`).
|
|
704
|
+
* @param raw - The draft value.
|
|
705
|
+
* @returns The payload entries, or the issues that block the save.
|
|
706
|
+
*/
|
|
707
|
+
function stringListDraftValue(field, raw) {
|
|
708
|
+
const items = listItems(raw);
|
|
709
|
+
const issues = stringListIssues(field, items);
|
|
710
|
+
return issues.length > 0 ? {
|
|
711
|
+
ok: false,
|
|
712
|
+
issues
|
|
713
|
+
} : {
|
|
714
|
+
ok: true,
|
|
715
|
+
value: [...items]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Inputs the list control renders: one per entry, or one blank input for an
|
|
720
|
+
* empty list so the first entry can be typed directly.
|
|
721
|
+
*
|
|
722
|
+
* @param value - Draft or effective value.
|
|
723
|
+
* @returns The input values, in order.
|
|
724
|
+
*/
|
|
725
|
+
function listRows(value) {
|
|
726
|
+
const items = listItems(value);
|
|
727
|
+
return items.length === 0 ? [""] : items;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Replace the entry at one input position.
|
|
731
|
+
*
|
|
732
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
733
|
+
* @param index - Position of the edited input.
|
|
734
|
+
* @param entry - The typed value.
|
|
735
|
+
* @returns The next draft entries.
|
|
736
|
+
*/
|
|
737
|
+
function withListEntry(rows, index, entry) {
|
|
738
|
+
return rows.map((item, position) => position === index ? entry : item);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Drop the entry at one input position.
|
|
742
|
+
*
|
|
743
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
744
|
+
* @param index - Position of the removed input.
|
|
745
|
+
* @returns The next draft entries.
|
|
746
|
+
*/
|
|
747
|
+
function withoutListEntry(rows, index) {
|
|
748
|
+
return rows.filter((_, position) => position !== index);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Append one blank input.
|
|
752
|
+
*
|
|
753
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
754
|
+
* @returns The next draft entries.
|
|
755
|
+
*/
|
|
756
|
+
function withBlankListEntry(rows) {
|
|
757
|
+
return [...rows, ""];
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Whether the add button is disabled: the inputs already reach `maxItems`.
|
|
761
|
+
*
|
|
762
|
+
* @param field - The Host descriptor.
|
|
763
|
+
* @param rows - Rendered inputs ({@link listRows}).
|
|
764
|
+
* @returns `true` when no further input may be added.
|
|
765
|
+
*/
|
|
766
|
+
function listIsFull(field, rows) {
|
|
767
|
+
return field.maxItems !== void 0 && rows.length >= field.maxItems;
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
567
770
|
//#region src/client/panel/shared.tsx
|
|
568
771
|
/**
|
|
569
772
|
* 设置面板共享层:页面状态类型、Host 字段渲染原语(FieldRow / FieldsCard /
|
|
@@ -649,6 +852,12 @@ window.__ModuleLoader__.load({
|
|
|
649
852
|
values[field.key] = raw === true;
|
|
650
853
|
continue;
|
|
651
854
|
}
|
|
855
|
+
if (field.kind === "string-list") {
|
|
856
|
+
const list = stringListDraftValue(field, raw);
|
|
857
|
+
if (list.ok) values[field.key] = list.value;
|
|
858
|
+
else issues.push(...list.issues);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
652
861
|
values[field.key] = String(raw ?? "");
|
|
653
862
|
}
|
|
654
863
|
return {
|
|
@@ -728,10 +937,61 @@ window.__ModuleLoader__.load({
|
|
|
728
937
|
})]
|
|
729
938
|
});
|
|
730
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* `string-list` control: one single-line input per entry, each with a remove
|
|
942
|
+
* button, and an add button below (disabled at `maxItems` inputs). An empty
|
|
943
|
+
* list renders one blank input so the first entry can be typed directly.
|
|
944
|
+
* Issues carrying an `index` mark that input and show the reason under it.
|
|
945
|
+
*/
|
|
946
|
+
function StringListInput(props) {
|
|
947
|
+
const { field, value, issues, disabled, onChange } = props;
|
|
948
|
+
const empty = listItems(value).length === 0;
|
|
949
|
+
const rows = listRows(value);
|
|
950
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
951
|
+
className: "dsh-channels-cfg__list",
|
|
952
|
+
children: [rows.map((item, index) => {
|
|
953
|
+
const issue = issues.find((entry) => entry.index === index);
|
|
954
|
+
const position = PANEL_COPY.listEntry.replace("{n}", String(index + 1));
|
|
955
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
956
|
+
className: "dsh-channels-cfg__listItem",
|
|
957
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
958
|
+
className: "dsh-channels-cfg__inputRow",
|
|
959
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
960
|
+
type: "text",
|
|
961
|
+
className: `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`,
|
|
962
|
+
value: item,
|
|
963
|
+
placeholder: field.placeholder,
|
|
964
|
+
"aria-label": `${field.label} ${position}`,
|
|
965
|
+
"aria-invalid": issue === void 0 ? void 0 : true,
|
|
966
|
+
disabled,
|
|
967
|
+
onChange: (event) => onChange(field.key, withListEntry(rows, index, event.target.value))
|
|
968
|
+
}), empty ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
969
|
+
type: "button",
|
|
970
|
+
className: "dsh-channels-cfg__btn dsh-channels-cfg__btn--listRemove",
|
|
971
|
+
"aria-label": `${PANEL_COPY.listRemove} ${position}`,
|
|
972
|
+
disabled,
|
|
973
|
+
onClick: () => onChange(field.key, withoutListEntry(rows, index)),
|
|
974
|
+
children: PANEL_COPY.listRemove
|
|
975
|
+
})]
|
|
976
|
+
}), issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
977
|
+
className: "dsh-channels-cfg__fieldError",
|
|
978
|
+
children: issue.message
|
|
979
|
+
})]
|
|
980
|
+
}, index);
|
|
981
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
982
|
+
type: "button",
|
|
983
|
+
className: "dsh-channels-cfg__btn",
|
|
984
|
+
disabled: disabled || listIsFull(field, rows),
|
|
985
|
+
onClick: () => onChange(field.key, withBlankListEntry(rows)),
|
|
986
|
+
children: PANEL_COPY.listAdd
|
|
987
|
+
}) })]
|
|
988
|
+
});
|
|
989
|
+
}
|
|
731
990
|
/** One generic form control driven by the Host field descriptor. */
|
|
732
991
|
function FieldRow(props) {
|
|
733
992
|
const { field, value, issues, disabled = false, onChange, onPickDirectory } = props;
|
|
734
|
-
const issue = issues.find((item) => item.key === field.key);
|
|
993
|
+
const issue = issues.find((item) => item.key === field.key && item.index === void 0);
|
|
994
|
+
const entryIssues = issues.filter((item) => item.key === field.key && item.index !== void 0);
|
|
735
995
|
const inputClass = `dsh-channels-cfg__input${issue === void 0 ? "" : " dsh-channels-cfg__input--invalid"}`;
|
|
736
996
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
737
997
|
className: `dsh-channels-cfg__field${field.wide === true ? " dsh-channels-cfg__field--wide" : ""}`,
|
|
@@ -795,6 +1055,13 @@ window.__ModuleLoader__.load({
|
|
|
795
1055
|
disabled,
|
|
796
1056
|
onChange: (event) => onChange(field.key, event.target.value)
|
|
797
1057
|
}) : null,
|
|
1058
|
+
field.kind === "string-list" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StringListInput, {
|
|
1059
|
+
field,
|
|
1060
|
+
value,
|
|
1061
|
+
issues: entryIssues,
|
|
1062
|
+
disabled,
|
|
1063
|
+
onChange
|
|
1064
|
+
}) : null,
|
|
798
1065
|
field.help === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
799
1066
|
className: "dsh-channels-cfg__help",
|
|
800
1067
|
children: field.help
|
|
@@ -2850,6 +3117,9 @@ window.__ModuleLoader__.load({
|
|
|
2850
3117
|
.dsh-channels-cfg__inputRow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
2851
3118
|
.dsh-channels-cfg__inputRow .dsh-channels-cfg__input{flex:1 1 auto;width:auto;min-width:0}
|
|
2852
3119
|
.dsh-channels-cfg__btn--browse{flex:none}
|
|
3120
|
+
.dsh-channels-cfg__list{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
3121
|
+
.dsh-channels-cfg__listItem{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
3122
|
+
.dsh-channels-cfg__btn--listRemove{flex:none}
|
|
2853
3123
|
.dsh-channels-cfg__input:focus{border-color:var(--dsw-alias-state-business-primary,#0a67d3)}
|
|
2854
3124
|
.dsh-channels-cfg__input--invalid{border-color:var(--dsw-alias-state-danger-primary,#c62828)}
|
|
2855
3125
|
select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|