@xmanrui/dsh-im 4.21.1 → 4.22.0

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.en.md CHANGED
@@ -141,7 +141,7 @@ Use the proxy URL required by your network and restart the Host after changing i
141
141
  | Default behavior | Description |
142
142
  | --- | --- |
143
143
  | Bot alias | Click the pencil beside a bot name to set an alias. Saving updates the display immediately, without restarting or reconnecting. The original name is retained; choose “Restore original name” or save an empty alias to restore it. Aliases only affect names shown in the local settings page. |
144
- | Bot workspace | Each bot stores its workspace independently. New bots start with the Host's current working directory, which can later be changed from the bot card. |
144
+ | Bot workspace | Each bot stores its workspace independently. New bots use `$DSH_HOME/im` (`~/.dsh/im` when unset); the directory is created automatically and new sessions appear under Ungrouped. Change it later from the bot card. An explicit `workspace` takes precedence, and `dshHome` overrides the `DSH_HOME` environment variable. |
145
145
  | Model | Every bot in each IM channel can choose a model directly below its workspace, or follow the Host default. A change applies only to later new Sessions; send `/new` and then an ordinary message in the current chat to use it. |
146
146
  | Reasoning effort | Explicitly choose an effort supported by the selected model, or follow the model default. Levels, descriptions, and defaults come from DSH. Switching models restores the new model's default effort. Each bot saves its own choice, which applies only to later new Sessions. |
147
147
  | Agent Preset | Each bot can choose an Agent Preset on its settings card. When none is chosen, new Sessions follow the Host's `agent-presets.default`. A channel-level `config.agentPreset` is only the default for later new bots on that channel. Changing the preset never modifies or clears existing Sessions; if the current chat already has a Session, send `/new` and then a regular message to create one with the new selection. |
@@ -160,6 +160,10 @@ Saved direct-message targets also offer an opt-in **Two-way Session sync** switc
160
160
 
161
161
  See the [Proactive Delivery Guide](PROACTIVE_DELIVERY.en.md) ([简体中文](PROACTIVE_DELIVERY.md)) for setup steps, native fields for each supported channel, complete call examples, management endpoints, error codes, and troubleshooting.
162
162
 
163
+ ### Client panel integration
164
+
165
+ Shells can embed the complete IM management panel through the optional `dshImClient` service and hide or restore its settings entry. DSH web keeps Settings → IM bots by default. See the [client integration guide](docs/client-integration.md) for the contract, compatibility requirements and lifecycle example.
166
+
163
167
  ### Context enhancement
164
168
 
165
169
  [Read the context enhancement guide](docs/context-enhancement.md)
@@ -202,7 +206,7 @@ See the [Proactive Delivery Guide](PROACTIVE_DELIVERY.en.md) ([简体中文](PRO
202
206
  | `/compact` | Immediately compact older context in the Session bound to the current chat. |
203
207
  | `/workspace <workspace index or absolute path>`, `/ws <workspace index or absolute path>` | Switch the current bot's Harness workspace by `/workspacelist` index or absolute path. |
204
208
  | `/workspacelist`, `/workspaces`, `/wsl` | List workspace absolute paths that still exist on the current Harness Host. |
205
- | `/sessionlist [workspace number or absolute path]`, `/sessions [...]` | Equivalent aliases that list every registered session ID and title in the selected workspace; omit the argument to use the current workspace. |
209
+ | `/sessionlist [workspace number or absolute path]`, `/sessions [...]` | Equivalent aliases that list session IDs and titles in the selected workspace, including ungrouped sessions in the default IM directory; omit the argument to use the current workspace. |
206
210
  | `/sessionlist --limit N`, `/sessions --limit N` | List the first N sessions in the current workspace's existing order; N must be a positive integer. |
207
211
  | `/session <Session ID>` | Bind the current chat to an existing Harness session. |
208
212
  | `/history [count]` | Preview recent messages from the bound Session in a direct chat; defaults to 3, capped at 5. |
@@ -219,6 +223,7 @@ DingTalk menus use a shared template built into the plugin; no template setup is
219
223
 
220
224
  - **Image understanding**: built-in channels with image input support can send JPEG, PNG, WebP, and GIF files sent as images to Harness, with an optional text description. Each image is limited to 5 MB, and all images in one message are limited to 20 MB in total. iMessage's MVP supports text DMs only.
221
225
  - **Switch workspaces from a bot card**: every bot card on the settings page shows its current Harness workspace. Enter an existing absolute directory path directly or open the directory picker. Switching clears only that bot's old chat mappings; it never deletes, empties, or archives old Sessions. Replies already in progress may finish, while later messages use the new workspace.
226
+ - **Default directory and ungrouped sessions**: bots using the default share `$DSH_HOME/im`, while retaining separate chat-to-Session mappings. The plugin creates no workspace group for this directory. Use `/sessions` to list its ungrouped sessions and `/session <Session ID>` or `/session N` to bind an ordinary session. New sessions in other directories use the existing workspace grouping mechanism; switching back to the default directory creates ungrouped sessions again. Existing bot directories are not migrated automatically.
222
227
  - **Choose a model and reasoning effort from a bot card**: every bot card in each IM channel offers model and effort controls below the workspace, with DSH-style provider groups, descriptions, and checkmarks. Select an available model and one of its supported efforts, or use the model default. Leaving the model unset follows the Host default. Settings are stored per bot and used only for later new Sessions; existing Sessions and replies already in progress are unchanged.
223
228
  - **Choose an Agent Preset from a bot card**: every bot card can select one of the Host's existing Agent Presets, or follow the Host default. The change applies only to that bot and only to later new Sessions; existing Sessions and replies already in progress are left unchanged.
224
229
  - **Check the connection and send a test message**: when a bot is online, clicking **Check connection** verifies the platform connection and sends a “DeepSeek Harness connection test succeeded” message to the bot's most recently remembered direct conversation; WhatsApp uses the account's self-chat. The test neither creates a Harness Session nor invokes the model. The bot must have received at least one direct message before it has a remembered test target; otherwise the page reports that no test conversation is available yet.
@@ -253,6 +258,16 @@ node bin/dsh-im.mjs install --source .
253
258
 
254
259
  IM management uses Harness browser authentication and Host/Origin trust checks by default. Once Harness allows and authenticates access from your LAN address, you can view and configure IM bots without any extra dsh-im configuration.
255
260
 
261
+ When accessing DSH through a custom domain, if IM settings report `transport failure for /api/dsh-im/...: HTTP 403`, add the browser-facing domain to your existing DSH launch command and restart DSH:
262
+
263
+ ```sh
264
+ dsh web --trusted-host dsh.example.com
265
+ ```
266
+
267
+ Replace `dsh.example.com` with the actual domain, without `http://`, `https://`, or a path. A domain without a port allows any port on that domain; use `--trusted-host dsh.example.com:8443` to restrict it to a particular access port. Repeat `--trusted-host` for multiple domains. This is a DSH option; dsh-im's `rpcAuthority: trusted-host` does not add domains to DSH's trust list. Local access through `localhost` or `127.0.0.1` normally needs no extra entry.
268
+
269
+ With a reverse proxy, preserve the browser-facing domain and port in the `Host` sent to DSH. The host parsed from an attached `Origin`, including any non-default port, must match that `Host`; adding two different hosts to the trust list still produces 403. `--trusted-host` configures the access check; the browser must still authenticate using a valid launch link.
270
+
256
271
  To additionally restrict IM management to local access, set the following in the active Web profile's `cordis.patch.yml`:
257
272
 
258
273
  ```yaml
package/README.md CHANGED
@@ -144,7 +144,7 @@ dsh web
144
144
  | 默认行为 | 说明 |
145
145
  | --- | --- |
146
146
  | 机器人别名 | 点击机器人名称旁的铅笔设置别名,保存后立即显示,无需重启或重连。原名称始终保留,可点击“恢复原名称”或清空别名后保存;仅影响本机设置页中的显示名称。 |
147
- | 机器人工作区 | 每个机器人独立保存工作区。新机器人默认使用 Host 当时的工作目录;之后可在机器人卡片中修改。 |
147
+ | 机器人工作区 | 每个机器人独立保存工作区。新机器人默认使用 `$DSH_HOME/im`(未设置时为 `~/.dsh/im`),目录自动创建,新会话显示在「未分组」;之后可在机器人卡片中修改。显式配置的 `workspace` 优先,`dshHome` 可覆盖环境变量 `DSH_HOME`。 |
148
148
  | 模型 | 每个 IM 渠道的每个机器人都可在工作区下方独立选择模型;未选择时跟随 Host 默认。切换只影响之后新建的会话;当前聊天先发送 `/new`,再发送普通消息才会使用新选择。 |
149
149
  | 思考强度 | 在模型下方显式选择该模型支持的思考强度,或跟随模型默认。档位、说明和默认值来自 DSH;切换模型后恢复新模型默认强度。每个机器人独立保存,只影响之后新建的会话。 |
150
150
  | Agent Preset | 每个机器人可在设置页卡片中选择 Agent Preset。未选择时跟随 Host 的 `agent-presets.default`;渠道级 `config.agentPreset` 只作为该渠道之后新接入机器人的默认值。切换不会修改或清空已有会话;若当前聊天已有会话,需先发送 `/new`,再发送一条普通消息,才会按新选择创建会话。 |
@@ -163,6 +163,10 @@ Logo 由 dsh-im 的浏览器适配显示,无需修改 DSH。适配保留原始
163
163
 
164
164
  设置步骤、各渠道字段、完整调用示例、管理端点、错误码与排错说明请查看[《主动投递使用指南》](PROACTIVE_DELIVERY.md)([English](PROACTIVE_DELIVERY.en.md))。
165
165
 
166
+ ### 客户端面板接入
167
+
168
+ 宿主可通过可选客户端服务 `dshImClient` 嵌入完整 IM 管理面板,并按需隐藏或恢复设置入口。dsh web 默认仍使用原来的「设置 → IM机器人」。接口、兼容要求与接入示例见[客户端接入文档](docs/client-integration.md)。
169
+
166
170
  ### 上下文增强
167
171
 
168
172
  [查看上下文增强说明](docs/上下文增强.md)
@@ -205,7 +209,7 @@ Logo 由 dsh-im 的浏览器适配显示,无需修改 DSH。适配保留原始
205
209
  | `/compact` | 立即压缩当前聊天绑定会话的较早上下文。 |
206
210
  | `/workspace <工作区序号或绝对路径>`、`/ws <工作区序号或绝对路径>` | 按 `/workspacelist` 序号或绝对路径切换当前机器人的 Harness 工作区。 |
207
211
  | `/workspacelist`、`/workspaces`、`/wsl` | 列出当前 Harness Host 上仍然存在的工作区绝对路径。 |
208
- | `/sessionlist [工作区序号或绝对路径]`、`/sessions [...]` | 两个等价命令;列出指定工作区登记的所有会话 ID 和标题,省略参数时使用当前工作区。 |
212
+ | `/sessionlist [工作区序号或绝对路径]`、`/sessions [...]` | 两个等价命令;列出指定工作区的会话 ID 和标题,默认 IM 目录也包含其中的未分组会话;省略参数时使用当前工作区。 |
209
213
  | `/sessionlist --limit N`、`/sessions --limit N` | 列出当前工作区现有顺序中的前 N 个会话;N 必须是正整数。 |
210
214
  | `/session <Session ID>` | 将当前聊天绑定到指定的已有 Harness 会话。 |
211
215
  | `/history [数量]` | 在私聊中查看当前绑定会话的最近历史消息,默认 3 条,最多 5 条。 |
@@ -222,6 +226,7 @@ Logo 由 dsh-im 的浏览器适配显示,无需修改 DSH。适配保留原始
222
226
 
223
227
  - **图片识别**:支持图片输入的内置渠道都可以把 JPEG、PNG、WebP,以及以图片文件方式发送的 GIF 交给 Harness;图片可以附带文字说明。单张图片上限为 5 MB,单条消息中的图片总大小上限为 20 MB。iMessage 首版仅支持文本私聊。
224
228
  - **在机器人卡片切换工作区**:设置页中的每张机器人卡片都会显示当前 Harness 工作区。可以直接填写已有目录的绝对路径,也可以打开目录选择器。切换只清除该机器人的旧聊天映射,不会删除、清空或归档旧 Session;已经开始的回复可以继续完成,后续消息使用新工作区。
229
+ - **默认目录与未分组会话**:所有使用默认值的机器人共用 `$DSH_HOME/im`,但各自保存聊天与会话的绑定。插件不会为该目录创建工作区分组;`/sessions` 可以列出该目录的未分组会话,`/session <Session ID>` 或 `/session N` 可以绑定普通会话。切换到其他目录后,新会话按现有机制加入对应工作区分组;切回默认目录后新会话归入「未分组」。已有机器人的目录不会自动迁移。
225
230
  - **在机器人卡片选择模型与思考强度**:每个 IM 渠道的每张机器人卡片都在工作区下方提供模型与思考强度入口,采用 DSH 风格的分组列表、档位说明和选中标记。先选择 Host 当前可用模型,再选择其支持的强度,或跟随模型默认;未选模型时整体跟随 Host 默认。设置按机器人独立保存,只用于之后新建的 Session;已有 Session 和正在生成的回复不受影响。
226
231
  - **在机器人卡片选择 Agent Preset**:设置页中的每张机器人卡片都可以选择 Host 已有的 Agent Preset,或跟随 Host 默认。切换只作用于该机器人,并且只影响之后新建的会话;已有会话和正在生成的回复不受影响。
227
232
  - **检查连接并发送测试消息**:机器人在线时,点击卡片上的「检查连接」会检查平台连接,并向该机器人最近记录的私聊发送一条“DeepSeek Harness 连接测试成功”消息;WhatsApp 会发送到账号自聊。测试消息不会创建 Harness Session,也不会调用模型。机器人必须至少收到过一条私聊才能记住测试目标,否则页面会提示尚无可用的测试会话。
@@ -256,6 +261,16 @@ node bin/dsh-im.mjs install --source .
256
261
 
257
262
  IM 管理接口默认沿用 Harness 的浏览器认证和 Host/Origin 信任检查。只要 Harness 已允许并认证当前局域网访问,便可直接查看和配置 IM 机器人,无需额外修改 dsh-im 配置。
258
263
 
264
+ 通过自定义域名访问时,如果 IM 设置页出现 `transport failure for /api/dsh-im/...: HTTP 403`,请在原 DSH 启动命令中添加浏览器访问的域名,并重启 DSH:
265
+
266
+ ```sh
267
+ dsh web --trusted-host dsh.example.com
268
+ ```
269
+
270
+ 将 `dsh.example.com` 替换为实际访问域名,不带 `http://`、`https://` 或路径。只写域名允许该域名的任意端口;如需限制到指定访问端口,使用 `--trusted-host dsh.example.com:8443`。多个域名可重复传入 `--trusted-host`。这个参数属于 DSH;dsh-im 的 `rpcAuthority: trusted-host` 本身不会把域名加入 DSH 信任列表。本机通过 `localhost` 或 `127.0.0.1` 访问通常不需要额外配置。
271
+
272
+ 使用反向代理时,代理传给 DSH 的 `Host` 应保留浏览器访问的域名及端口;浏览器携带的 `Origin` 解析出的 host(含非默认端口)必须与该 `Host` 一致,仅将两者分别加入信任列表仍会返回 403。`--trusted-host` 只配置访问校验,浏览器仍需通过有效启动链接完成认证。
273
+
259
274
  如需将 IM 管理额外限制为仅本机访问,可在当前 Web profile 的 `cordis.patch.yml` 中设置:
260
275
 
261
276
  ```yaml
@@ -0,0 +1,81 @@
1
+ # 客户端面板接入 / Client panel integration
2
+
3
+ dsh-im 发布可选客户端服务 `dshImClient`,供同一 DSH 客户端中的宿主插件嵌入完整 IM 管理面板。默认仍保留「设置 → IM机器人」;安装新版无需迁移数据或新增配置。host 侧的 `dshIm` 主动投递服务保持独立。
4
+
5
+ The optional `dshImClient` service exposes the existing IM management panel to a shell in the same DSH client. Settings → IM bots remains available by default. No migration or new configuration is required. The host-side `dshIm` delivery service is separate.
6
+
7
+ ```ts
8
+ interface DshImClient {
9
+ readonly version: 1;
10
+ render(props?: { preferredSectionId?: string }): React.ReactElement | null;
11
+ setSettingsVisible(visible: boolean): void;
12
+ settingsVisible(): boolean;
13
+ }
14
+ ```
15
+
16
+ ## 接口语义 / Contract
17
+
18
+ | 接口 | 中文 | English |
19
+ | --- | --- | --- |
20
+ | `version` | 接口版本,当前为 `1`,不是软件版本 | Interface version, currently `1`; not the package version |
21
+ | `render(props?)` | 每次返回新 element;不挂载 DOM、不隐藏设置入口。完整面板包含现有功能、语言刷新与局部错误保护 | Returns a fresh element without mounting DOM or hiding settings. Includes existing functionality, locale updates and a local error boundary |
22
+ | `setSettingsVisible(visible)` | 只接受 boolean,重复调用无副作用。false 撤销入口及等待中的注册,true 恢复 | Accepts only booleans; repeated calls are idempotent. False removes both the entry and any pending registration; true restores them |
23
+ | `settingsVisible()` | 返回入口当前是否已注册,不表示设置弹窗是否打开。等待槽位、已隐藏或已卸载时为 false | Reports whether the entry is currently registered, not whether the settings dialog is open. False while waiting, hidden or disposed |
24
+
25
+ `preferredSectionId` 只决定首次挂载的栏目,支持 `weixin`、`feishu`、`dingtalk`、`wecom`、`wecomApp`、`qq`、`slack`、`telegram`、`discord`、`whatsapp`、`imessage`、`office` 和 `global-settings`。省略或无效值回退到微信。已挂载后改变该值不会覆盖用户选择;需要重新定位时由宿主重新挂载。
26
+
27
+ `preferredSectionId` selects the initial section using the IDs above. Missing or unknown IDs default to WeChat. Changing the prop after mounting does not override the reader's selection; remount deliberately to reset it.
28
+
29
+ 开关只影响当前客户端内存,不写服务器配置或浏览器存储。因此桌面端隐藏入口不影响另一个浏览器。刷新后恢复默认,由宿主再次按需接管。隐藏入口不会停止机器人或外部已挂载的面板。V1 按一个活跃管理面板接入,不提供多个面板间的状态同步。
30
+
31
+ Visibility is local to this client instance and is never persisted. A desktop shell cannot hide the entry in another browser. Reload restores the default until the shell opts in again. Hiding settings does not stop bots or unmount an embedded panel. V1 targets one active management panel and does not synchronize multiple copies.
32
+
33
+ ## 宿主接入 / Shell lifecycle
34
+
35
+ 在宿主自己的可选集成插件中声明 `inject: ['dshImClient', ...宿主所需服务]`,检查 `version === 1`。使用与宿主共享的 React 实例,把 `im.render()` 返回的 element 挂在宿主管理的容器中。不要直接调用面板组件函数、缓存首次返回的 element 或另行打包 React。
36
+
37
+ Declare `dshImClient` in an optional integration plugin's `inject` list alongside the shell's own dependencies, and check `version === 1`. Render the returned element using the host's React instance. Do not call the component function directly, cache its first element, or bundle another React instance.
38
+
39
+ 以下为生命周期示意;`mountShellPanel` 与 `unmountShellPanel` 由宿主实现,不属于 dsh-im API:
40
+
41
+ This illustrates the lifecycle. `mountShellPanel` and `unmountShellPanel` are shell-owned functions, not dsh-im APIs:
42
+
43
+ ```js
44
+ export const inject = ['dshImClient'];
45
+
46
+ export function apply(ctx) {
47
+ const im = ctx.dshImClient;
48
+ if (im.version !== 1) return;
49
+
50
+ return ctx.effect(() => {
51
+ const unmountShellPanel = mountShellPanel({
52
+ render: () => im.render(),
53
+ onAvailable: () => im.setSettingsVisible(false),
54
+ onUnavailable: () => im.setSettingsVisible(true),
55
+ });
56
+ return () => {
57
+ im.setSettingsVisible(true);
58
+ unmountShellPanel();
59
+ };
60
+ });
61
+ }
62
+ ```
63
+
64
+ 宿主必须确认新入口和内容均可访问后才隐藏设置入口。注册函数返回 handle 可能只表示已开始等待槽位,不能据此认定新入口已可用。退出集成时恢复入口并卸载 React 内容;依赖服务退出时同样清理。旧服务引用在 provider 卸载后 `render()` 返回 null、`settingsVisible()` 返回 false、开关调用不生效。
65
+
66
+ Hide settings only after both the replacement navigation and content are available. A registration handle may only represent a pending slot subscription. On integration exit or dependency loss, restore settings and unmount the React subtree. A stale service returns null from `render()`, false from `settingsVisible()`, and ignores visibility writes after provider disposal.
67
+
68
+ ## 兼容与布局 / Compatibility and layout
69
+
70
+ - 老宿主缺少 `ctx.provide` 时只跳过新服务,原设置页正常注册。旧版 dsh-im 没有该服务时,让可选集成保持未激活即可。
71
+ - dsh-im 不依赖或注册桌面端侧栏槽位。容器的高度、留白、滚动和可达入口由宿主负责;现有面板不保证适配任意狭窄容器。
72
+ - 面板复用现有的管理 RPC、权限、目录选择和更新能力,无法借此绕过 host 权限限制。
73
+ - Without `ctx.provide`, the original settings section still registers. An optional shell integration stays inactive when an older dsh-im has no service.
74
+ - dsh-im neither imports nor registers desktop sidebar slots. The shell owns navigation, height, padding and scrolling. The existing panel is not guaranteed to fit an arbitrarily narrow container.
75
+ - Existing management RPC, permissions, directory selection and update behavior apply equally to both render paths.
76
+
77
+ ## 可选桌面接入 / Optional desktop integration
78
+
79
+ 源码仓库的 `integrations/desktop-panel` 提供可安装的桌面消费插件及安装说明,侧栏名称与设置页一致:中文「IM机器人」、英文「IM bots」。它只在桌面 iframe 中启用,普通浏览器仍保留设置入口。该独立目录不包含在 dsh-im 的 npm 发布包中,也不代表桌面端已经内置接入。
80
+
81
+ The source repository includes an installable desktop consumer and setup instructions in `integrations/desktop-panel`. Its sidebar label shares the settings translation: IM机器人 / IM bots. Only the desktop iframe opts in; standalone browsers retain settings. This separate directory is excluded from the dsh-im npm package and is not an upstream desktop integration release.
package/lib/client.js CHANGED
@@ -577,12 +577,12 @@ function callManagementRpc(connection, channel5, method, payload, signal) {
577
577
  }
578
578
 
579
579
  // plugin-src/client/index.js
580
- var React31 = __toESM(require("react"), 1);
580
+ var React32 = __toESM(require("react"), 1);
581
581
 
582
582
  // package.json
583
583
  var package_default = {
584
584
  name: "@xmanrui/dsh-im",
585
- version: "4.21.1",
585
+ version: "4.22.0",
586
586
  description: "\u628A\u5341\u4E00\u79CD IM \u6E20\u9053\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
587
587
  keywords: [
588
588
  "deepseek-harness",
@@ -696,6 +696,7 @@ var package_default = {
696
696
  "src",
697
697
  "PROACTIVE_DELIVERY.md",
698
698
  "PROACTIVE_DELIVERY.en.md",
699
+ "docs/client-integration.md",
699
700
  "LICENSE",
700
701
  "README.md",
701
702
  "README.en.md",
@@ -1132,6 +1133,7 @@ var EN = Object.freeze({
1132
1133
  "macOS Messages \u6743\u9650": "macOS Messages permissions",
1133
1134
  "IM\u673A\u5668\u4EBA": "IM bots",
1134
1135
  "IM\u673A\u5668\u4EBA\u8BBE\u7F6E": "IM bot settings",
1136
+ "IM \u9762\u677F\u52A0\u8F7D\u5931\u8D25": "Could not load the IM panel",
1135
1137
  "\u66F4\u591A\u673A\u5668\u4EBA\u8BBE\u7F6E": "More bot settings",
1136
1138
  "\u673A\u5668\u4EBA\u8BBE\u7F6E": "Bot settings",
1137
1139
  "\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
@@ -1789,6 +1791,12 @@ var EN = Object.freeze({
1789
1791
  "Slack \u5DE5\u4F5C\u533A": "Slack workspace",
1790
1792
  "Bot Token \u4E0E App Token": "Bot Token and App Token",
1791
1793
  "\u586B\u5199 Bot Token": "Enter Bot Token",
1794
+ "\u5E94\u7528\u5E73\u53F0": "App platform",
1795
+ "Lark\uFF08\u56FD\u9645\u7248\uFF09": "Lark (international)",
1796
+ "\u624B\u52A8\u63A5\u5165Lark\u673A\u5668\u4EBA": "Connect Lark bot manually",
1797
+ "\u586B\u5199 Lark \u5F00\u653E\u5E73\u53F0 App ID": "Enter the Lark Developer App ID",
1798
+ "\u586B\u5199 Lark \u5F00\u653E\u5E73\u53F0 App Secret": "Enter the Lark Developer App Secret",
1799
+ "Lark \u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002": "Lark bot credentials connected.",
1792
1800
  "\u624B\u52A8\u63A5\u5165\u98DE\u4E66\u673A\u5668\u4EBA": "Connect Feishu bot manually",
1793
1801
  "\u624B\u52A8\u63A5\u5165\u9489\u9489\u673A\u5668\u4EBA": "Connect DingTalk bot manually",
1794
1802
  "\u624B\u52A8\u63A5\u5165\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA": "Connect WeCom bot manually",
@@ -3509,7 +3517,8 @@ function CredentialBindingPanel({
3509
3517
  busy = false,
3510
3518
  error = null,
3511
3519
  onSubmit,
3512
- onCancel
3520
+ onCancel,
3521
+ children
3513
3522
  }) {
3514
3523
  const [identity, setIdentity] = React6.useState("");
3515
3524
  const [secret, setSecret] = React6.useState("");
@@ -3529,6 +3538,7 @@ function CredentialBindingPanel({
3529
3538
  "aria-labelledby": headingId
3530
3539
  },
3531
3540
  h2("h3", { id: headingId, className: "dim-credentialTitle" }, `\u624B\u52A8\u63A5\u5165${channel5}\u673A\u5668\u4EBA`),
3541
+ children,
3532
3542
  h2(
3533
3543
  "form",
3534
3544
  {
@@ -7708,6 +7718,7 @@ function FeishuSettingsTab({ rpcCall }) {
7708
7718
  const [pageBusy, setPageBusy] = React16.useState(false);
7709
7719
  const [provisionBusy, setProvisionBusy] = React16.useState(false);
7710
7720
  const [credentialOpen, setCredentialOpen] = React16.useState(false);
7721
+ const [credentialDomain, setCredentialDomain] = React16.useState("feishu");
7711
7722
  const [credentialBusy, setCredentialBusy] = React16.useState(false);
7712
7723
  const [credentialError, setCredentialError] = React16.useState(null);
7713
7724
  const [busyByBot, setBusyByBot] = React16.useState({});
@@ -7890,13 +7901,13 @@ function FeishuSettingsTab({ rpcCall }) {
7890
7901
  try {
7891
7902
  const snapshot = normalizeBotsSnapshot(await invoke(
7892
7903
  FEISHU_ENDPOINTS.bindCredentials,
7893
- { appId: identity, appSecret: secret }
7904
+ { appId: identity, appSecret: secret, domain: credentialDomain }
7894
7905
  ));
7895
7906
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
7896
7907
  mergeSnapshot(snapshot);
7897
7908
  }
7898
7909
  setCredentialOpen(false);
7899
- announce("\u98DE\u4E66\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
7910
+ announce(credentialDomain === "lark" ? "Lark \u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002" : "\u98DE\u4E66\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
7900
7911
  } catch (error) {
7901
7912
  setCredentialError(presentError3(error));
7902
7913
  } finally {
@@ -7904,7 +7915,7 @@ function FeishuSettingsTab({ rpcCall }) {
7904
7915
  if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
7905
7916
  setCredentialBusy(false);
7906
7917
  }
7907
- }, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
7918
+ }, [announce, credentialDomain, invoke, loadStatus, mergeSnapshot, workspaceFence]);
7908
7919
  const cancelProvisioning = React16.useCallback(async () => {
7909
7920
  const activeProvision = model.provisioning;
7910
7921
  const attemptId = activeProvision?.attemptId;
@@ -8240,11 +8251,12 @@ function FeishuSettingsTab({ rpcCall }) {
8240
8251
  });
8241
8252
  }
8242
8253
  const credentialContent = credentialOpen ? h2(CredentialBindingPanel, {
8243
- channel: "\u98DE\u4E66",
8254
+ key: credentialDomain,
8255
+ channel: credentialDomain === "lark" ? "Lark" : "\u98DE\u4E66",
8244
8256
  identityLabel: "App ID",
8245
- identityPlaceholder: "\u586B\u5199\u98DE\u4E66\u5F00\u653E\u5E73\u53F0 App ID",
8257
+ identityPlaceholder: credentialDomain === "lark" ? "\u586B\u5199 Lark \u5F00\u653E\u5E73\u53F0 App ID" : "\u586B\u5199\u98DE\u4E66\u5F00\u653E\u5E73\u53F0 App ID",
8246
8258
  secretLabel: "App Secret",
8247
- secretPlaceholder: "\u586B\u5199\u98DE\u4E66\u5F00\u653E\u5E73\u53F0 App Secret",
8259
+ secretPlaceholder: credentialDomain === "lark" ? "\u586B\u5199 Lark \u5F00\u653E\u5E73\u53F0 App Secret" : "\u586B\u5199\u98DE\u4E66\u5F00\u653E\u5E73\u53F0 App Secret",
8248
8260
  busy: credentialBusy,
8249
8261
  error: credentialError,
8250
8262
  onSubmit: bindCredentials,
@@ -8252,7 +8264,26 @@ function FeishuSettingsTab({ rpcCall }) {
8252
8264
  setCredentialOpen(false);
8253
8265
  setCredentialError(null);
8254
8266
  }
8255
- }) : null;
8267
+ }, h2(
8268
+ "label",
8269
+ { className: "dim-credentialField" },
8270
+ h2("span", null, "\u5E94\u7528\u5E73\u53F0"),
8271
+ h2(
8272
+ "select",
8273
+ {
8274
+ className: "dim-feishuGroupSelect",
8275
+ "aria-label": "\u5E94\u7528\u5E73\u53F0",
8276
+ value: credentialDomain,
8277
+ disabled: credentialBusy,
8278
+ onChange: (event) => {
8279
+ setCredentialDomain(event.target.value);
8280
+ setCredentialError(null);
8281
+ }
8282
+ },
8283
+ h2("option", { value: "feishu" }, "\u98DE\u4E66"),
8284
+ h2("option", { value: "lark" }, "Lark\uFF08\u56FD\u9645\u7248\uFF09")
8285
+ )
8286
+ )) : null;
8256
8287
  const setCardRef = React16.useCallback((botId, node) => {
8257
8288
  if (node) cardRefs.current.set(botId, node);
8258
8289
  else cardRefs.current.delete(botId);
@@ -18272,6 +18303,27 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
18272
18303
  );
18273
18304
  }
18274
18305
 
18306
+ // plugin-src/client/panel-error-boundary.js
18307
+ var React31 = __toESM(require("react"), 1);
18308
+ var IMPanelErrorBoundary = class extends React31.Component {
18309
+ state = { failed: false };
18310
+ static getDerivedStateFromError() {
18311
+ return { failed: true };
18312
+ }
18313
+ render() {
18314
+ if (!this.state.failed) return this.props.children;
18315
+ return h2(
18316
+ "section",
18317
+ { className: "dim-page", role: "alert" },
18318
+ h2("p", null, "IM \u9762\u677F\u52A0\u8F7D\u5931\u8D25"),
18319
+ h2("button", {
18320
+ type: "button",
18321
+ onClick: () => this.setState({ failed: false })
18322
+ }, "\u91CD\u8BD5")
18323
+ );
18324
+ }
18325
+ };
18326
+
18275
18327
  // plugin-src/client/index.js
18276
18328
  var name = "im-settings";
18277
18329
  var inject = ["slots", "connection", "locale", "workspaces"];
@@ -18422,26 +18474,27 @@ function IMSettingsTab({
18422
18474
  deliveryRpcCall,
18423
18475
  globalSettingsRpcCall,
18424
18476
  workspaceDirectoryPicker,
18477
+ preferredSectionId,
18425
18478
  browserLocation = globalThis.location,
18426
18479
  navigateToRecoveryUrl = replacePageLocation
18427
18480
  }) {
18428
- const [selected, setSelected] = React31.useState("weixin");
18429
- const [loopbackRecovery, setLoopbackRecovery] = React31.useState(null);
18430
- const [runningVersion, setRunningVersion] = React31.useState(IM_PLUGIN_VERSION);
18431
- const [deliverySettings, setDeliverySettings] = React31.useState(null);
18432
- const githubTooltipId = React31.useId();
18433
- const generalSettingsTooltipId = React31.useId();
18481
+ const [selected, setSelected] = React32.useState(() => preferredSectionId === GLOBAL_SETTINGS_TAB_ID || CHANNELS.some((channel5) => channel5.id === preferredSectionId) ? preferredSectionId : "weixin");
18482
+ const [loopbackRecovery, setLoopbackRecovery] = React32.useState(null);
18483
+ const [runningVersion, setRunningVersion] = React32.useState(IM_PLUGIN_VERSION);
18484
+ const [deliverySettings, setDeliverySettings] = React32.useState(null);
18485
+ const githubTooltipId = React32.useId();
18486
+ const generalSettingsTooltipId = React32.useId();
18434
18487
  const globalSettingsSelected = selected === GLOBAL_SETTINGS_TAB_ID;
18435
18488
  const active = CHANNELS.find((channel5) => channel5.id === selected) ?? CHANNELS[0];
18436
18489
  const activeTabId = globalSettingsSelected ? "dim-general-settings-trigger" : `dim-tab-${active.id}`;
18437
18490
  const activePanelId = globalSettingsSelected ? `dim-panel-${GLOBAL_SETTINGS_TAB_ID}` : `dim-panel-${active.id}`;
18438
- const reportLoopbackRecovery = React31.useCallback((recovery) => {
18491
+ const reportLoopbackRecovery = React32.useCallback((recovery) => {
18439
18492
  setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
18440
18493
  }, []);
18441
- const reportUpdateStatus = React31.useCallback((snapshot) => {
18494
+ const reportUpdateStatus = React32.useCallback((snapshot) => {
18442
18495
  setRunningVersion(snapshot.runningVersion);
18443
18496
  }, []);
18444
- const rpcCalls = React31.useMemo(() => createLoopbackAwareRpcCalls({
18497
+ const rpcCalls = React32.useMemo(() => createLoopbackAwareRpcCalls({
18445
18498
  dingtalkRpcCall,
18446
18499
  discordRpcCall,
18447
18500
  feishuRpcCall,
@@ -18479,7 +18532,7 @@ function IMSettingsTab({
18479
18532
  weixinRpcCall,
18480
18533
  whatsappRpcCall
18481
18534
  ]);
18482
- const botSettingsContext = React31.useMemo(() => Object.freeze({
18535
+ const botSettingsContext = React32.useMemo(() => Object.freeze({
18483
18536
  openBotSettings: setDeliverySettings
18484
18537
  }), []);
18485
18538
  return h2(
@@ -18667,31 +18720,85 @@ function apply(ctx) {
18667
18720
  listDirectory: (path, signal) => callWorkspaceDirectoryApi(ctx, "listDirectory", path, signal),
18668
18721
  pickDirectory: () => callWorkspaceDirectoryApi(ctx, "pickDirectory")
18669
18722
  });
18670
- ctx.slots.inject("settings.section", () => ctx.slots.register({
18671
- name: "settings.section",
18672
- id: "xmanrui-dsh-im",
18673
- order: 21,
18674
- label: () => t("IM\u673A\u5668\u4EBA"),
18675
- locale: IM_LOCALE_NAMESPACE,
18676
- inject: () => ({
18677
- dingtalkRpcCall,
18678
- discordRpcCall,
18679
- feishuRpcCall,
18680
- qqRpcCall,
18681
- slackRpcCall,
18682
- telegramRpcCall,
18683
- wecomRpcCall,
18684
- wecomAppRpcCall,
18685
- weixinRpcCall,
18686
- whatsappRpcCall,
18687
- imessageRpcCall,
18688
- officeRpcCall,
18689
- updateRpcCall,
18690
- deliveryRpcCall,
18691
- globalSettingsRpcCall,
18692
- workspaceDirectoryPicker
18693
- })
18694
- }, IMSettingsTab));
18723
+ const panelDependencies = {
18724
+ dingtalkRpcCall,
18725
+ discordRpcCall,
18726
+ feishuRpcCall,
18727
+ qqRpcCall,
18728
+ slackRpcCall,
18729
+ telegramRpcCall,
18730
+ wecomRpcCall,
18731
+ wecomAppRpcCall,
18732
+ weixinRpcCall,
18733
+ whatsappRpcCall,
18734
+ imessageRpcCall,
18735
+ officeRpcCall,
18736
+ updateRpcCall,
18737
+ deliveryRpcCall,
18738
+ globalSettingsRpcCall,
18739
+ workspaceDirectoryPicker
18740
+ };
18741
+ const subscribeLocale = (listener) => typeof ctx.on === "function" ? ctx.on("locale/change", listener) : () => {
18742
+ };
18743
+ const localeSnapshot = () => ctx.locale.getLocale?.()?.active ?? "";
18744
+ function IMPanel({ preferredSectionId }) {
18745
+ React32.useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
18746
+ return h2(
18747
+ IMPanelErrorBoundary,
18748
+ null,
18749
+ h2(IMSettingsTab, { ...panelDependencies, preferredSectionId })
18750
+ );
18751
+ }
18752
+ const buildPanelElement = (props = {}) => h2(IMPanel, {
18753
+ preferredSectionId: props.preferredSectionId
18754
+ });
18755
+ ctx.effect(() => {
18756
+ let disposed = false;
18757
+ let stopSettings = null;
18758
+ let registered = false;
18759
+ const setSettingsVisible = (visible) => {
18760
+ if (disposed) return;
18761
+ if (typeof visible !== "boolean") throw new TypeError("visible must be a boolean");
18762
+ if (visible === (stopSettings !== null)) return;
18763
+ if (!visible) {
18764
+ const stop = stopSettings;
18765
+ stopSettings = null;
18766
+ stop();
18767
+ return;
18768
+ }
18769
+ stopSettings = ctx.slots.inject("settings.section", () => {
18770
+ const unregister = ctx.slots.register({
18771
+ name: "settings.section",
18772
+ id: "xmanrui-dsh-im",
18773
+ order: 21,
18774
+ label: () => t("IM\u673A\u5668\u4EBA"),
18775
+ locale: IM_LOCALE_NAMESPACE,
18776
+ inject: () => panelDependencies
18777
+ }, buildPanelElement);
18778
+ registered = true;
18779
+ return () => {
18780
+ registered = false;
18781
+ unregister();
18782
+ };
18783
+ });
18784
+ };
18785
+ setSettingsVisible(true);
18786
+ if (typeof ctx.provide === "function") {
18787
+ ctx.provide("dshImClient", Object.freeze({
18788
+ version: 1,
18789
+ render: (props) => disposed ? null : buildPanelElement(props),
18790
+ setSettingsVisible,
18791
+ settingsVisible: () => !disposed && registered
18792
+ }));
18793
+ }
18794
+ return () => {
18795
+ disposed = true;
18796
+ const stop = stopSettings;
18797
+ stopSettings = null;
18798
+ registered = false;
18799
+ stop?.();
18800
+ };
18801
+ }, "im-settings: client panel service");
18695
18802
  }
18696
18803
 
18697
18804
  return module.exports;