@xmanrui/dsh-im 2.1.0 → 2.2.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 +23 -6
- package/README.md +23 -6
- package/lib/client.js +140 -7
- package/lib/index.js +195 -175
- package/package.json +1 -1
- package/plugin-src/client/i18n.js +4 -0
- package/plugin-src/client/index.js +72 -11
- package/plugin-src/client/loopback-recovery.js +75 -0
- package/plugin-src/client/styles.js +10 -0
- package/plugin-src/host/channels/shared/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +5 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -2
- package/src/channels/feishu/feishu-cards.mjs +6 -2
- package/src/channels/qq/qq-bridge.mjs +4 -2
- package/src/channels/shared/harness-client.mjs +26 -2
- package/src/channels/shared/i18n-en/feishu.mjs +2 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +8 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +39 -0
- package/src/channels/shared/model-command.mjs +356 -27
- package/src/channels/shared/text-harness-bridge.mjs +4 -2
- package/src/channels/wecom/wecom-bridge.mjs +4 -2
- package/src/channels/weixin/weixin-api.mjs +2 -1
- package/src/channels/weixin/weixin-bridge.mjs +6 -3
- package/src/channels/weixin/weixin-runtime.mjs +2 -2
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. |
|
|
@@ -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
|
|
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`
|
|
156
|
-
-
|
|
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.
|
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 当时的工作目录;之后可在机器人卡片中修改。 |
|
|
@@ -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`
|
|
159
|
-
-
|
|
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 默认。
|
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",
|
|
@@ -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(
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
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
|
);
|