@xmanrui/dsh-im 2.1.0 → 2.2.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.en.md CHANGED
@@ -46,7 +46,7 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
46
46
  | Channel | Setup | Messaging and replies |
47
47
  | --- | --- | --- |
48
48
  | Feishu | Create a bot by QR code, or bind one with App ID + App Secret | Persistent connection for incoming messages; streaming cards show thinking, tool progress, and replies |
49
- | WeChat | Scan a QR code to bind a WeChat bot | Tencent iLink long polling for sending and receiving messages |
49
+ | WeChat | Scan a QR code to bind a WeChat bot | Tencent iLink long polling for sending and receiving messages; long replies are sent in 1,800-character chunks |
50
50
  | DingTalk | Create a bot by QR code, or bind one with Client ID + Client Secret | DingTalk Stream connection; streaming replies through AI Cards |
51
51
  | WeCom | Create an intelligent bot by QR code, or bind one with Bot ID + Secret | Official WebSocket connection; native thinking state, tool progress, and streaming replies |
52
52
  | QQ | Create a bot with mobile QQ QR scanning, or bind one with AppID + AppSecret | WebSocket connection; private chats show typing and receive one Markdown reply, while mentioned group chats receive only the final answer |
@@ -109,6 +109,18 @@ After installation, follow the built-in instructions on each channel page to sca
109
109
 
110
110
  If this machine must use a forward proxy to reach Feishu, set `HTTPS_PROXY` to a full HTTP proxy URL before starting `dsh web` (for example, `http://proxy:8080`; lowercase `https_proxy` is also supported, with `HTTP_PROXY` accepted as a fallback), then restart the Host after changing it. Feishu registration and credential verification reuse the SDK's proxy-aware HTTP client, while the message WebSocket explicitly uses that proxy; the WebSocket path does not currently read `ALL_PROXY` or `NO_PROXY`.
111
111
 
112
+ If this machine cannot reach the Telegram Bot API directly, use Node.js 22.21 or newer and enable Node's environment proxy support before starting `dsh web`:
113
+
114
+ ```sh
115
+ NODE_USE_ENV_PROXY=1 \
116
+ HTTPS_PROXY=http://proxy:8080 \
117
+ HTTP_PROXY=http://proxy:8080 \
118
+ NO_PROXY=localhost,127.0.0.1 \
119
+ dsh web
120
+ ```
121
+
122
+ Use the proxy URL required by your network and restart the Host after changing it. If Telegram Bot Token binding reports that the Bot API cannot be reached, first check the proxy URL, Node.js version, and `NO_PROXY` configuration.
123
+
112
124
  | Default behavior | Description |
113
125
  | --- | --- |
114
126
  | 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. |
@@ -116,7 +128,7 @@ If this machine must use a forward proxy to reach Feishu, set `HTTPS_PROXY` to a
116
128
 
117
129
  Each Telegram bot has its own access-mode control on its bot card. Existing and newly connected bots both default to **Compatible mode**: DMs receive replies, while group messages require a mention of or reply to the bot. Restrictions apply only after explicitly switching that bot to **Safe mode (private-chat allowlist)**. Safe mode ignores every group message and admits only numeric User IDs in that bot's allowlist. Enter one ID per line. Switching back to Compatible mode retains the allowlist without enforcing it, so it is available when Safe mode is enabled again. An empty allowlist in Safe mode rejects all inbound messages for that bot.
118
130
 
119
- Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only me**, which is also the default for newly linked bots and accepts only self-chat messages from the linked account. **Selected contacts** additionally accepts direct messages from allowlisted phone numbers and ignores groups. Enter one number with its country or region code per line; a leading `+` is optional. **Open responses** preserves the previous behavior: all direct messages are accepted, together with group mentions or replies. Switching modes retains the allowlist. An empty Selected contacts allowlist behaves like Only me, and rejected messages are ignored silently.
131
+ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only me**, which is also the default for newly linked bots and accepts only self-chat messages from the linked account. **Selected contacts** additionally accepts direct messages from allowlisted phone numbers and ignores groups. Enter one number with its country or region code per line; a leading `+` is optional. **Open responses** accepts all direct messages, group messages sent by the linked account, and mentions of or replies to that account from other group members; this also lets an owner-only group act as a separate conversation. Switching modes retains the allowlist. An empty Selected contacts allowlist behaves like Only me, and rejected messages are ignored silently.
120
132
 
121
133
  ## Bot commands
122
134
 
@@ -126,8 +138,12 @@ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only
126
138
  | `/new` | Unbind the current chat so its next ordinary message starts a new Harness Session. |
127
139
  | `/status` | Check the connection between the current bot and DeepSeek Harness. |
128
140
  | `/models` | List every currently configured model with a number. |
129
- | `/model` | Show the model used by the Session bound to this chat. |
130
- | `/model <number or provider/model-id>` | Switch the model for the Session bound to this chat. |
141
+ | `/model` | Show the model and reasoning effort used by the Session bound to this chat. |
142
+ | `/model <number or provider/model-id> [reasoning effort ID]` | Switch the Session model and optionally select an effort supported by the target model. |
143
+ | `/reasoninglist`, `/reasonings` | Equivalent aliases that list the reasoning efforts supported by the current model. |
144
+ | `/reasoning` | Show the current Session model and reasoning effort. |
145
+ | `/reasoning <number or effort ID>` | Switch the current model's reasoning effort. |
146
+ | `/reasoning --default` | Restore the current model's default reasoning effort. |
131
147
  | `/presetlist` | List the Host's currently available Agent Presets, marking the Host default and this bot's selection. |
132
148
  | `/preset` | Show this bot's Agent Preset setting for new Sessions. |
133
149
  | `/preset <number or Preset ID>` | Set this bot's Agent Preset; use `/preset id:<ID>` for a numeric ID. |
@@ -142,7 +158,7 @@ Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only
142
158
  | Interactive question | Reply with an option number, option label, or custom text; separate multiple choices with commas. |
143
159
  | Remote approval | Reply with `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`. |
144
160
 
145
- Example: send `/models`, then `/model 2` to switch to the second model in the list; send `/presetlist`, then `/preset 2` to select the second Agent Preset for this bot. Other examples: `/help`, `/new`, `/status`, `/model deepseek-official/deepseek-v4-pro`, `/preset marketing-jeep`, `/preset --default`, `/steer inspect only the configuration file`, `/stop`, `/compact`, `/workspace /Users/alice/projects/my-app`, `/sessionlist 2`, `/sessionlist /Users/alice/projects/my-app`, or `/session session-id`
161
+ Example: send `/models`, then `/model 2` to switch to the second model in the list; send `/reasoninglist`, then `/reasoning 2` to switch to the current model's second reasoning effort; send `/presetlist`, then `/preset 2` to select the second Agent Preset for this bot. Other examples: `/help`, `/new`, `/status`, `/model deepseek-official/deepseek-v4-pro max`, `/reasoning --default`, `/preset marketing-jeep`, `/preset --default`, `/steer inspect only the configuration file`, `/stop`, `/compact`, `/workspace /Users/alice/projects/my-app`, `/sessionlist 2`, `/sessionlist /Users/alice/projects/my-app`, or `/session session-id`
146
162
 
147
163
  If the Slack desktop app has no native Slash Command registered with the same name, it intercepts messages that begin directly with `/`. Send the command with one leading space instead, for example ` /presetlist` or ` /preset 2`; the plugin command layer trims surrounding whitespace, so it executes exactly like the unspaced form.
148
164
 
@@ -152,8 +168,9 @@ If the Slack desktop app has no native Slash Command registered with the same na
152
168
  - `/status` takes no arguments, never prompts the model, and does not change the Session binding. It confirms that the current bot can reach DeepSeek Harness.
153
169
  - `/new` only removes the current chat's saved dsh-im Session binding; it never deletes, empties, or archives the old Session. The next ordinary message creates and binds a new Session in the current workspace. If a task is running or waiting for a question or approval, finish the interaction or use `/stop` before `/new`.
154
170
  - `/models` takes no arguments and never creates a Session. It assigns a number to every currently configured Harness model and also shows its stable, copyable `provider/model-id`. If one provider fails, models from the remaining providers are still shown.
155
- - Bare `/model` only displays the current Session model. A model can be selected by the number or exact full ID returned by `/models`, for example `/model 2`. When the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model. The switch affects only that Session; Harness also attempts to save it as the default for future Sessions, while other existing Sessions remain unchanged.
156
- - A model cannot be switched while a task is running or waiting for an approval or question answer. Wait for it to finish or use `/stop` first. A Session containing images cannot switch to a model that does not accept image input.
171
+ - Bare `/model` shows the current Session model and reasoning effort. With arguments, it accepts a number from `/models` or an exact full model ID, followed optionally by an exact reasoning effort ID published in that target model's metadata, for example `/model 2 max`. When the effort is omitted, Harness resolves the target model's current default. If the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model.
172
+ - `/reasoninglist` and `/reasonings` are equivalent. They list the efforts from the current model's metadata and mark the current and default values. `/reasoning` shows the current value; `/reasoning <number or effort ID>` accepts a listed number or an exact metadata ID; `/reasoning --default` lets Harness resolve the current model's default again. Every `/reasoning...` command requires an existing Session and never creates one or prompts the model.
173
+ - The model or reasoning effort cannot be changed while a task is running or waiting for an approval or question answer. Wait for it to finish or use `/stop` first. A change takes effect on the next model request and keeps Harness's default-saving semantics: Harness attempts to save the accepted model and effort as the default selection for future Sessions, while other existing Sessions remain unchanged. A Session containing images cannot switch to a model that does not accept image input.
157
174
  - `/presetlist` takes no arguments and never creates a Session. It reads the Host's currently available Agent Presets every time, showing their names, stable IDs, the Host default, and this bot's selection. A selected Preset that has been deleted or become broken is retained and marked unavailable instead of being cleared automatically. Only safe names and IDs are shown; paths, errors, and other Host internals are never exposed.
158
175
  - Bare `/preset` shows this bot's setting for future new Sessions; it does not inspect or change the current Session. With an argument, it accepts a number from the most recent `/presetlist` in this chat or an exact ID; use `/preset id:<ID>` for a numeric ID. A numbered selection resolves the ID from that displayed list and then validates it against the latest Host catalog, asking for a fresh list if it has changed.
159
176
  - `/preset --default` clears this bot's explicit override so future Sessions resolve the Host default when they are created. Explicitly selecting an ID that currently matches the Host default pins that ID instead. Following the Host default remains available even while the catalog cannot be read.
@@ -168,7 +185,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
168
185
  - `/session` accepts exactly one Session ID obtained from `/sessionlist`. It neither creates a session nor immediately prompts the model; later messages in the current chat continue the bound session. Regular archived sessions can be bound without being unarchived, while subagent sessions cannot be bound.
169
186
  - `/session` locates the session's unique workspace automatically. Binding inside the current workspace replaces only this chat's mapping. A cross-workspace binding switches the bot workspace, clears the old session mappings for all of that bot's chats, and then binds this chat, so it affects the bot's other chats. A reply already being generated may still finish.
170
187
  - Workspace switches and session bindings only clear or replace dsh-im chat mappings. They never delete, empty, or archive old Session contents; an old Session can still be listed and bound again.
171
- - Any user admitted by the current channel access policy can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules, while Safe mode admits only allowlisted private users. WhatsApp Only me accepts self-chat only, Selected contacts accepts self-chat plus allowlisted direct messages, and Open responses accepts every direct message plus group mentions or replies.
188
+ - Any user admitted by the current channel access policy can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules, while Safe mode admits only allowlisted private users. WhatsApp Only me accepts self-chat only, Selected contacts accepts self-chat plus allowlisted direct messages, and Open responses accepts every direct message, group messages from the linked account, and mentions or replies from other group members.
172
189
  - Agent Preset names and IDs come from the same Harness Host, and any command-authorized user can change the Preset used by all future new Sessions across this bot's chats. Expose `/presetlist` and `/preset` only to trusted users.
173
190
  - The list comes from the Harness Host's global registry and can include local absolute paths for other bots, other channels, or non-IM projects. Restrict the bot's visibility to trusted users.
174
191
  - Session results also come from the global Harness Host. Session IDs and titles can belong to other bots, other channels, or non-IM projects, and may contain sensitive metadata. Enable these commands only when every user in the bot's visibility scope is trusted.
package/README.md CHANGED
@@ -49,7 +49,7 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
49
49
  | 渠道 | 接入方式 | 消息与回复 |
50
50
  | --- | --- | --- |
51
51
  | 飞书 | 扫码创建机器人,或使用 App ID + App Secret 手动绑定 | 长连接接收消息;通过飞书流式卡片显示思考、工具进度和回答 |
52
- | 微信 | 使用微信扫码绑定机器人 | 腾讯 iLink 长轮询收发消息 |
52
+ | 微信 | 使用微信扫码绑定机器人 | 腾讯 iLink 长轮询收发消息;长回复会按 1,800 字符分段发送 |
53
53
  | 钉钉 | 扫码创建机器人,或使用 Client ID + Client Secret 手动绑定 | 钉钉 Stream 长连接;通过 AI Card 流式显示回答 |
54
54
  | 企业微信 | 使用企业微信 App 扫码创建智能机器人,或使用 Bot ID + Secret 手动绑定 | 官方 WebSocket 长连接;原生显示“正在思考中”、工具执行进度和流式回答 |
55
55
  | QQ | 使用手机 QQ 扫码创建机器人,或使用 AppID + AppSecret 手动绑定 | WebSocket 长连接;私聊显示“正在输入”并以单条 Markdown 回复,群聊被 @ 后只发送最终答案 |
@@ -112,6 +112,18 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
112
112
 
113
113
  如果本机必须通过正向代理访问飞书,请在启动 `dsh web` 前把 `HTTPS_PROXY` 设置为包含协议的 HTTP 代理 URL(例如 `http://proxy:8080`;也支持小写 `https_proxy`,并兼容使用 `HTTP_PROXY` 作为回退),修改后重启 Host。飞书注册和凭据验证会复用 SDK 的代理感知 HTTP 客户端,消息长连接会显式通过这个代理建立 WebSocket;长连接目前不读取 `ALL_PROXY` 或 `NO_PROXY`。
114
114
 
115
+ 如果本机无法直连 Telegram Bot API,请使用 Node.js 22.21 或更高版本,并在启动 `dsh web` 前启用 Node 的环境变量代理支持:
116
+
117
+ ```sh
118
+ NODE_USE_ENV_PROXY=1 \
119
+ HTTPS_PROXY=http://proxy:8080 \
120
+ HTTP_PROXY=http://proxy:8080 \
121
+ NO_PROXY=localhost,127.0.0.1 \
122
+ dsh web
123
+ ```
124
+
125
+ 代理地址按本机网络环境填写;修改代理后需要重启 Host。绑定 Telegram Bot Token 时,如果页面提示无法访问 Bot API,请优先检查代理地址、Node.js 版本和 `NO_PROXY` 配置。
126
+
115
127
  | 默认行为 | 说明 |
116
128
  | --- | --- |
117
129
  | 机器人工作区 | 每个机器人独立保存工作区。新机器人默认使用 Host 当时的工作目录;之后可在机器人卡片中修改。 |
@@ -119,7 +131,7 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
119
131
 
120
132
  每个 Telegram 机器人都可以在自己的卡片中切换访问模式。旧机器人和新接入机器人均默认使用**兼容模式**:私聊直接响应,群聊仅在提及机器人或回复机器人消息时响应。只有主动切换到**安全模式(私聊白名单)**后,机器人才会忽略全部群聊,并只接受该机器人白名单中的数字 User ID。白名单每行一个 ID、按机器人独立保存;切回兼容模式时会保留但不使用,再切回安全模式即可继续使用。安全模式的空白名单会拒绝该机器人的所有入站消息。
121
133
 
122
- 每个 WhatsApp 机器人也有独立的访问模式。旧机器人升级后和新接入机器人都默认使用**仅自己模式**,只响应已绑定账号的自聊消息。**指定联系人模式**额外接受白名单电话号码的私聊并忽略群聊;号码需包含国家或地区代码,每行一个,可带开头的 `+`。**开放响应模式**保留原有行为:响应所有私聊,以及群聊中的提及或回复。切换模式会保留白名单;指定联系人模式的空白名单等同于仅自己模式。未授权消息会被静默忽略。
134
+ 每个 WhatsApp 机器人也有独立的访问模式。旧机器人升级后和新接入机器人都默认使用**仅自己模式**,只响应已绑定账号的自聊消息。**指定联系人模式**额外接受白名单电话号码的私聊并忽略群聊;号码需包含国家或地区代码,每行一个,可带开头的 `+`。**开放响应模式**响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员对该账号的提及或回复;因此也可以把“仅自己”的群当作独立会话使用。切换模式会保留白名单;指定联系人模式的空白名单等同于仅自己模式。未授权消息会被静默忽略。
123
135
 
124
136
  ## 机器人命令
125
137
 
@@ -129,8 +141,12 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
129
141
  | `/new` | 解除当前聊天的会话绑定,让下一条普通消息开启全新 Harness 会话。 |
130
142
  | `/status` | 检查当前机器人与 DeepSeek Harness 的连接状态。 |
131
143
  | `/models` | 按序号列出当前配置的全部可用模型。 |
132
- | `/model` | 查看当前聊天绑定会话正在使用的模型。 |
133
- | `/model <序号或 Provider/模型ID>` | 切换当前聊天绑定会话的模型。 |
144
+ | `/model` | 查看当前聊天绑定会话正在使用的模型和推理等级。 |
145
+ | `/model <序号或 Provider/模型ID> [推理等级ID]` | 切换当前会话模型,并可同时指定目标模型支持的推理等级。 |
146
+ | `/reasoninglist`、`/reasonings` | 等价命令;列出当前模型支持的推理等级。 |
147
+ | `/reasoning` | 查看当前会话的模型和推理等级。 |
148
+ | `/reasoning <序号或等级ID>` | 切换当前模型的推理等级。 |
149
+ | `/reasoning --default` | 恢复当前模型的默认推理等级。 |
134
150
  | `/presetlist` | 按序号列出 Host 当前可用的 Agent Preset,并标记 Host 默认项和当前机器人的选择。 |
135
151
  | `/preset` | 查看当前机器人的新会话 Agent Preset 设置。 |
136
152
  | `/preset <序号或 Preset ID>` | 设置当前机器人的 Agent Preset;纯数字 ID 使用 `/preset id:<ID>`。 |
@@ -145,7 +161,7 @@ GitHub 源安装会直接拉取并构建 Git 依赖;pnpm 10 及以上版本可
145
161
  | 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 |
146
162
  | 远程审批 | 回复 `批准` / `拒绝` / `同意` / `不同意` / `yes` / `no`。 |
147
163
 
148
- 示例:先发送 `/models`,再发送 `/model 2` 切换到列表中的第 2 个模型;先发送 `/presetlist`,再发送 `/preset 2` 为当前机器人选择第 2 个 Agent Preset。其他命令示例:`/help`、`/new`、`/status`、`/model deepseek-official/deepseek-v4-pro`、`/preset marketing-jeep`、`/preset --default`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id`
164
+ 示例:先发送 `/models`,再发送 `/model 2` 切换到列表中的第 2 个模型;先发送 `/reasoninglist`,再发送 `/reasoning 2` 切换到当前模型的第 2 个推理等级;先发送 `/presetlist`,再发送 `/preset 2` 为当前机器人选择第 2 个 Agent Preset。其他命令示例:`/help`、`/new`、`/status`、`/model deepseek-official/deepseek-v4-pro max`、`/reasoning --default`、`/preset marketing-jeep`、`/preset --default`、`/steer 只检查配置文件`、`/stop`、`/compact`、`/workspace /Users/alice/projects/my-app`、`/sessionlist 2`、`/sessionlist /Users/alice/projects/my-app` 或 `/session session-id`
149
165
 
150
166
  Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/` 开头的消息。此时请加一个前导空格发送,例如 ` /presetlist` 或 ` /preset 2`;插件命令层会去除首尾空白,执行效果与无空格命令相同。
151
167
 
@@ -155,8 +171,9 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
155
171
  - `/status` 不需要参数,也不会向模型发送消息或改变会话绑定;它用于确认当前机器人能够连接 DeepSeek Harness。
156
172
  - `/new` 只解除当前聊天在 dsh-im 中保存的会话绑定,不会删除、清空或归档旧 Session。下一条普通消息会在当前工作区创建并绑定一个新 Session。任务正在运行或等待问题、审批时,应先完成交互或使用 `/stop`,再使用 `/new`。
157
173
  - `/models` 不需要参数,也不会创建会话。它为 Harness 当前配置的全部可用模型分配序号,同时显示可稳定复制的 `Provider/模型ID`;某个 Provider 查询失败时,其他 Provider 的结果仍会显示。
158
- - `/model` 不带参数时只查看当前会话模型;带参数时接受 `/models` 列出的序号或完整模型 ID,例如 `/model 2`。完整 ID 必须精确匹配。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。切换只影响当前会话;Harness 还会尝试把它保存为以后新会话的默认模型,已有其他会话不受影响。
159
- - 正在运行任务或等待审批、问题回答时不能切换模型;请等待完成,或先使用 `/stop`。含图片的会话无法切换到不支持图片输入的模型。
174
+ - `/model` 不带参数时查看当前会话的模型和推理等级;带参数时接受 `/models` 列出的序号或精确完整模型 ID,并可追加目标模型元数据公布的精确推理等级 ID,例如 `/model 2 max`。省略推理等级时,由 Harness 解析目标模型的当前默认值。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。
175
+ - `/reasoninglist` `/reasonings` 完全等价,按当前模型的元数据列出可选推理等级并标记当前值和默认值。`/reasoning` 查看当前值;`/reasoning <序号或等级ID>` 接受列表序号或元数据中的精确 ID;`/reasoning --default` 让 Harness 重新采用当前模型的默认推理等级。所有 `/reasoning...` 命令都要求当前聊天已有 Session,不会自行创建 Session 或触发模型回复。
176
+ - 正在运行任务或等待审批、问题回答时不能修改模型或推理等级;请等待完成,或先使用 `/stop`。修改从下一次模型请求起生效,并沿用 Harness 的默认保存语义:Harness 会尝试把已接受的模型和推理等级保存为以后新会话的默认选择,已有其他会话不受影响。含图片的会话无法切换到不支持图片输入的模型。
160
177
  - `/presetlist` 不需要参数,也不会创建会话。它每次都读取 Host 当前可用的 Agent Preset,显示名称、稳定 ID、Host 默认项和当前机器人的选择;已删除或损坏的当前选择会保留并标记为“已不可用”,不会被自动清除。列表只公开安全的名称和 ID,不公开 Preset 路径、错误或其他 Host 内部字段。
161
178
  - `/preset` 不带参数时查看当前机器人的“新会话设置”,不是查看或修改当前 Session。带参数时接受最近一次 `/presetlist` 在当前聊天中显示的序号或完整 ID;纯数字 ID 使用 `/preset id:<ID>`。选择序号时会先按该次列表解析 ID,再用 Host 最新目录复验,目录已经变化时会要求重新列出。
162
179
  - `/preset --default` 清除当前机器人的显式覆盖值,让以后新建的 Session 在创建时跟随 Host 当前默认;显式选择一个恰好等于 Host 默认的 ID 则会固定该 ID。目录暂时不可读时仍可恢复为跟随 Host 默认。
@@ -171,7 +188,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
171
188
  - `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
172
189
  - `/session` 会自动定位会话唯一所属的工作区。同工作区绑定只替换当前聊天的映射;跨工作区绑定会切换该机器人的工作区、清除该机器人所有聊天的旧会话映射,再绑定当前聊天,因此会影响该机器人的其他聊天。已经开始生成的回复仍可完成。
173
190
  - 工作区切换和会话绑定只会清除或替换 dsh-im 的聊天映射,不会删除、清空或归档任何旧 Session 内容;旧 Session 仍可再次列出和绑定。
174
- - 任何通过当前渠道访问策略的用户都可以执行这些命令,不另行区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行。WhatsApp 仅自己模式只接受自聊,指定联系人模式接受自聊和白名单私聊,开放响应模式接受所有私聊及群聊中的提及或回复。
191
+ - 任何通过当前渠道访问策略的用户都可以执行这些命令,不另行区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行。WhatsApp 仅自己模式只接受自聊,指定联系人模式接受自聊和白名单私聊,开放响应模式接受所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。
175
192
  - Agent Preset 名称和 ID 来自同一个 Harness Host,且任何有命令权限的用户都能修改该机器人所有聊天未来新 Session 的 Preset;请只向可信用户开放 `/presetlist` 和 `/preset`。
176
193
  - 工作区列表来自 Harness Host 的全局登记信息,可能包含其他机器人、其他渠道或非 IM 项目的本机绝对路径。请将机器人可见范围限制给可信用户。
177
194
  - 会话列表同样来自该全局 Harness Host;会话 ID 和标题可能属于其他机器人、其他渠道或非 IM 项目,并可能包含敏感元数据。开放命令前请确保所有可见用户都可信。
package/lib/client.js CHANGED
@@ -35,6 +35,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
35
35
  var index_exports = {};
36
36
  __export(index_exports, {
37
37
  IMSettingsTab: () => IMSettingsTab,
38
+ LoopbackRecoveryNotice: () => LoopbackRecoveryNotice,
38
39
  apply: () => apply,
39
40
  inject: () => inject,
40
41
  name: () => name
@@ -245,6 +246,10 @@ var EN = Object.freeze({
245
246
  "\u672C\u673A\u6682\u65F6\u65E0\u6CD5\u8BBF\u95EE AI Office\u3002": "AI Office cannot currently be reached from this machine.",
246
247
  "AI Office \u8FDE\u63A5\u5DF2\u4E2D\u65AD\u3002": "The AI Office connection was interrupted.",
247
248
  "\u5E2E\u52A9\u4E0E\u53CD\u9988 \xB7 \u524D\u5F80 GitHub": "Help & feedback \xB7 Open GitHub",
249
+ "\u8BF7\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00": "Reopen with localhost",
250
+ "\u9875\u9762\u4F1A\u5728\u5F53\u524D\u7AEF\u53E3\u91CD\u65B0\u6253\u5F00\uFF0C\u673A\u5668\u4EBA\u914D\u7F6E\u4E0D\u4F1A\u6539\u53D8\u3002": "The page will reopen on the current port. Your bot configuration will not change.",
251
+ "\u4F7F\u7528 localhost \u91CD\u65B0\u6253\u5F00": "Reopen with localhost",
252
+ "\u5F53\u524D\u5730\u5740\u4E0E\u6D4F\u89C8\u5668\u7684\u672C\u673A\u8BF7\u6C42\u6821\u9A8C\u4E0D\u517C\u5BB9\u3002\u8BF7\u4F7F\u7528\u4E0A\u65B9\u6309\u94AE\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00\u3002": "This address is incompatible with the browser\u2019s local-request checks. Use the button above to reopen with localhost.",
248
253
  "\u5FAE\u4FE1": "WeChat",
249
254
  "\u98DE\u4E66": "Feishu",
250
255
  "\u9489\u9489": "DingTalk",
@@ -559,7 +564,7 @@ var EN = Object.freeze({
559
564
  "\u5DF2\u751F\u6548\uFF1A": "Active: ",
560
565
  "\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002": "Only respond to self-chat messages from the linked WhatsApp account.",
561
566
  "\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002": "Respond to self-chat and allowlisted direct messages; ignore group messages.",
562
- "\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002": "Respond to all direct messages and to group mentions or replies.",
567
+ "\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5176\u4ED6\u7FA4\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002": "Respond to all direct messages, group messages sent by the linked account, and mentions or replies from other group members.",
563
568
  "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801": "WhatsApp phone numbers allowed to send direct messages",
564
569
  "\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801": "One number with country or region code per line",
565
570
  "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002": "A leading + is allowed and removed when saved.",
@@ -9346,7 +9351,7 @@ function WhatsappAccessSettings({ account, busy = false, onSave }) {
9346
9351
  "span",
9347
9352
  { className: "dwa-accessTooltipItem" },
9348
9353
  h2("strong", null, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"),
9349
- h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\uFF0C\u4EE5\u53CA\u7FA4\u804A\u4E2D\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002")
9354
+ h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5176\u4ED6\u7FA4\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002")
9350
9355
  )
9351
9356
  )
9352
9357
  )
@@ -10014,6 +10019,61 @@ function WhatsappSettingsTab({ rpcCall }) {
10014
10019
  ));
10015
10020
  }
10016
10021
 
10022
+ // plugin-src/client/loopback-recovery.js
10023
+ var TRANSPORT_FORBIDDEN = /^transport failure for \/[A-Za-z0-9._~-]+\/[A-Za-z0-9_$./~-]+: HTTP 403$/;
10024
+ var LOOPBACK_RECOVERY_ERROR_CODE = "loopback-recovery-required";
10025
+ var LOOPBACK_RECOVERY_ERROR_MESSAGE = "\u5F53\u524D\u5730\u5740\u4E0E\u6D4F\u89C8\u5668\u7684\u672C\u673A\u8BF7\u6C42\u6821\u9A8C\u4E0D\u517C\u5BB9\u3002\u8BF7\u4F7F\u7528\u4E0A\u65B9\u6309\u94AE\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00\u3002";
10026
+ function isIpv4Loopback(hostname) {
10027
+ const parts = hostname.split(".");
10028
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
10029
+ }
10030
+ function createLoopbackRecovery(error, location) {
10031
+ if (!TRANSPORT_FORBIDDEN.test(error?.message ?? "")) return null;
10032
+ if (typeof location?.href !== "string") return null;
10033
+ try {
10034
+ const current = new URL(location.href);
10035
+ if (current.protocol !== "http:" || !isIpv4Loopback(current.hostname)) return null;
10036
+ current.hostname = "localhost";
10037
+ return Object.freeze({
10038
+ url: current.href,
10039
+ origin: current.origin
10040
+ });
10041
+ } catch {
10042
+ return null;
10043
+ }
10044
+ }
10045
+ function createLoopbackAwareRpcCall(rpcCall, {
10046
+ location,
10047
+ onRecovery
10048
+ } = {}) {
10049
+ if (typeof rpcCall !== "function") throw new TypeError("rpcCall must be a function");
10050
+ return async (...args) => {
10051
+ try {
10052
+ return await rpcCall(...args);
10053
+ } catch (error) {
10054
+ const recovery = createLoopbackRecovery(error, location);
10055
+ if (!recovery) throw error;
10056
+ onRecovery?.(recovery);
10057
+ const presented = new Error(LOOPBACK_RECOVERY_ERROR_MESSAGE);
10058
+ presented.code = LOOPBACK_RECOVERY_ERROR_CODE;
10059
+ presented.cause = error;
10060
+ presented.recoveryUrl = recovery.url;
10061
+ throw presented;
10062
+ }
10063
+ };
10064
+ }
10065
+ function createLoopbackAwareRpcCalls(rpcCalls, options) {
10066
+ return Object.freeze(Object.fromEntries(
10067
+ Object.entries(rpcCalls).map(([name2, rpcCall]) => [
10068
+ name2,
10069
+ typeof rpcCall === "function" ? createLoopbackAwareRpcCall(rpcCall, options) : rpcCall
10070
+ ])
10071
+ ));
10072
+ }
10073
+ function replacePageLocation(url, location = globalThis.location) {
10074
+ location?.replace?.(url);
10075
+ }
10076
+
10017
10077
  // plugin-src/client/styles.js
10018
10078
  var IM_STYLE_ID = "xmanrui-dsh-im-settings";
10019
10079
  var CSS11 = String.raw`
@@ -10073,6 +10133,14 @@ var CSS11 = String.raw`
10073
10133
  .dim-channelNote { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; line-height: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
10074
10134
  .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-border-l1, #eef0f3); }
10075
10135
  .dim-panel { min-width: 0; container-type: inline-size; }
10136
+ .dim-loopbackRecovery { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 14px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 8%, var(--dsw-alias-bg-layer-1, #fff)); }
10137
+ .dim-loopbackRecoveryCopy { min-width: 0; }
10138
+ .dim-loopbackRecoveryCopy strong { display: block; font-size: 14px; line-height: 20px; font-weight: 650; }
10139
+ .dim-loopbackRecoveryCopy p { margin: 3px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
10140
+ .dim-loopbackRecoveryCopy code { display: block; overflow: hidden; margin-top: 5px; color: var(--dsw-alias-label-secondary, #646a73); font: 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
10141
+ .dim-loopbackRecoveryAction { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; padding: 0 12px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; cursor: pointer; }
10142
+ .dim-loopbackRecoveryAction:hover { border-color: #0958d9; background: #0958d9; }
10143
+ .dim-loopbackRecoveryAction:focus-visible { outline: 2px solid color-mix(in srgb, #1677ff 62%, white); outline-offset: 2px; }
10076
10144
  .dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
10077
10145
  .dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
10078
10146
  .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; justify-content: stretch; gap: 8px; }
@@ -10282,6 +10350,8 @@ var CSS11 = String.raw`
10282
10350
  .dim-title p { white-space: normal; }
10283
10351
  .dim-githubTooltip { right: auto; left: 0; }
10284
10352
  .dim-rail { grid-template-columns: minmax(0, 1fr); }
10353
+ .dim-loopbackRecovery { align-items: stretch; flex-direction: column; gap: 12px; }
10354
+ .dim-loopbackRecoveryAction { width: 100%; }
10285
10355
  .dim-directoryPickerBackdrop { padding: 10px; }
10286
10356
  .dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
10287
10357
  .dim-directoryPickerHeader { padding: 18px 17px 14px; }
@@ -10398,6 +10468,27 @@ function ChannelLogo({ channel: channel4 }) {
10398
10468
  if (channel4 === "whatsapp") return h2(WhatsappLogo);
10399
10469
  return h2(OfficeLogo);
10400
10470
  }
10471
+ function LoopbackRecoveryNotice({ recovery, onNavigate = replacePageLocation }) {
10472
+ return h2(
10473
+ "div",
10474
+ {
10475
+ className: "dim-loopbackRecovery",
10476
+ role: "alert"
10477
+ },
10478
+ h2(
10479
+ "div",
10480
+ { className: "dim-loopbackRecoveryCopy" },
10481
+ h2("strong", null, "\u8BF7\u6539\u7528 localhost \u91CD\u65B0\u6253\u5F00"),
10482
+ h2("p", null, "\u9875\u9762\u4F1A\u5728\u5F53\u524D\u7AEF\u53E3\u91CD\u65B0\u6253\u5F00\uFF0C\u673A\u5668\u4EBA\u914D\u7F6E\u4E0D\u4F1A\u6539\u53D8\u3002"),
10483
+ h2("code", null, recovery.origin)
10484
+ ),
10485
+ h2("button", {
10486
+ type: "button",
10487
+ className: "dim-loopbackRecoveryAction",
10488
+ onClick: () => onNavigate(recovery.url)
10489
+ }, "\u4F7F\u7528 localhost \u91CD\u65B0\u6253\u5F00")
10490
+ );
10491
+ }
10401
10492
  function IMSettingsTab({
10402
10493
  dingtalkRpcCall,
10403
10494
  discordRpcCall,
@@ -10409,11 +10500,45 @@ function IMSettingsTab({
10409
10500
  weixinRpcCall,
10410
10501
  whatsappRpcCall,
10411
10502
  officeRpcCall,
10412
- workspaceDirectoryPicker
10503
+ workspaceDirectoryPicker,
10504
+ browserLocation = globalThis.location,
10505
+ navigateToRecoveryUrl = replacePageLocation
10413
10506
  }) {
10414
10507
  const [selected, setSelected] = React20.useState("weixin");
10508
+ const [loopbackRecovery, setLoopbackRecovery] = React20.useState(null);
10415
10509
  const githubTooltipId = React20.useId();
10416
10510
  const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
10511
+ const reportLoopbackRecovery = React20.useCallback((recovery) => {
10512
+ setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
10513
+ }, []);
10514
+ const rpcCalls = React20.useMemo(() => createLoopbackAwareRpcCalls({
10515
+ dingtalkRpcCall,
10516
+ discordRpcCall,
10517
+ feishuRpcCall,
10518
+ qqRpcCall,
10519
+ slackRpcCall,
10520
+ telegramRpcCall,
10521
+ wecomRpcCall,
10522
+ weixinRpcCall,
10523
+ whatsappRpcCall,
10524
+ officeRpcCall
10525
+ }, {
10526
+ location: browserLocation,
10527
+ onRecovery: reportLoopbackRecovery
10528
+ }), [
10529
+ browserLocation,
10530
+ dingtalkRpcCall,
10531
+ discordRpcCall,
10532
+ feishuRpcCall,
10533
+ officeRpcCall,
10534
+ qqRpcCall,
10535
+ reportLoopbackRecovery,
10536
+ slackRpcCall,
10537
+ telegramRpcCall,
10538
+ wecomRpcCall,
10539
+ weixinRpcCall,
10540
+ whatsappRpcCall
10541
+ ]);
10417
10542
  return h2(
10418
10543
  WorkspaceDirectoryPickerContext.Provider,
10419
10544
  { value: workspaceDirectoryPicker },
@@ -10480,12 +10605,20 @@ function IMSettingsTab({
10480
10605
  ))
10481
10606
  ),
10482
10607
  h2("div", { className: "dim-divider", "aria-hidden": "true" }),
10483
- h2("main", {
10484
- className: "dim-panel",
10485
- role: "tabpanel",
10486
- id: `dim-panel-${active.id}`,
10487
- "aria-labelledby": `dim-tab-${active.id}`
10488
- }, active.id === "weixin" ? h2(WeixinSettingsTab, { rpcCall: weixinRpcCall }) : active.id === "feishu" ? h2(FeishuSettingsTab, { rpcCall: feishuRpcCall }) : active.id === "dingtalk" ? h2(DingtalkSettingsTab, { rpcCall: dingtalkRpcCall }) : active.id === "wecom" ? h2(WecomSettingsTab, { rpcCall: wecomRpcCall }) : active.id === "qq" ? h2(QqSettingsTab, { rpcCall: qqRpcCall }) : active.id === "slack" ? h2(SlackSettingsTab, { rpcCall: slackRpcCall }) : active.id === "telegram" ? h2(TelegramSettingsTab, { rpcCall: telegramRpcCall }) : active.id === "discord" ? h2(DiscordSettingsTab, { rpcCall: discordRpcCall }) : active.id === "whatsapp" ? h2(WhatsappSettingsTab, { rpcCall: whatsappRpcCall }) : h2(OfficeSettingsTab, { rpcCall: officeRpcCall }))
10608
+ h2(
10609
+ "main",
10610
+ {
10611
+ className: "dim-panel",
10612
+ role: "tabpanel",
10613
+ id: `dim-panel-${active.id}`,
10614
+ "aria-labelledby": `dim-tab-${active.id}`
10615
+ },
10616
+ loopbackRecovery ? h2(LoopbackRecoveryNotice, {
10617
+ recovery: loopbackRecovery,
10618
+ onNavigate: navigateToRecoveryUrl
10619
+ }) : null,
10620
+ active.id === "weixin" ? h2(WeixinSettingsTab, { rpcCall: rpcCalls.weixinRpcCall }) : active.id === "feishu" ? h2(FeishuSettingsTab, { rpcCall: rpcCalls.feishuRpcCall }) : active.id === "dingtalk" ? h2(DingtalkSettingsTab, { rpcCall: rpcCalls.dingtalkRpcCall }) : active.id === "wecom" ? h2(WecomSettingsTab, { rpcCall: rpcCalls.wecomRpcCall }) : active.id === "qq" ? h2(QqSettingsTab, { rpcCall: rpcCalls.qqRpcCall }) : active.id === "slack" ? h2(SlackSettingsTab, { rpcCall: rpcCalls.slackRpcCall }) : active.id === "telegram" ? h2(TelegramSettingsTab, { rpcCall: rpcCalls.telegramRpcCall }) : active.id === "discord" ? h2(DiscordSettingsTab, { rpcCall: rpcCalls.discordRpcCall }) : active.id === "whatsapp" ? h2(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall }) : h2(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall })
10621
+ )
10489
10622
  )
10490
10623
  )
10491
10624
  );