@xmanrui/dsh-im 4.19.2 → 4.20.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 +79 -1
- package/README.md +79 -1
- package/lib/client.js +594 -217
- package/lib/index.js +279 -276
- package/package.json +9 -1
- package/plugin-src/client/channels/weixin/api.js +35 -17
- package/plugin-src/client/channels/weixin/connection-error.js +68 -0
- package/plugin-src/client/channels/weixin/index.js +36 -12
- package/plugin-src/client/i18n.js +2 -0
- package/plugin-src/host/channels/shared/startup.mjs +7 -4
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +13 -1
- package/plugin-src/host/channels/weixin/index.mjs +12 -3
- package/plugin-src/host/channels/weixin/production.mjs +53 -3
- package/plugin-src/host/channels/weixin/rpc.mjs +22 -17
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-menu.mjs +8 -4
- package/src/channels/feishu/bridge.mjs +44 -13
- package/src/channels/qq/qq-bridge.mjs +12 -4
- package/src/channels/qq/qq-menu.mjs +11 -8
- package/src/channels/shared/bot-workspace-store.mjs +532 -40
- package/src/channels/shared/command-catalog.mjs +5 -0
- package/src/channels/shared/compact-command.mjs +14 -4
- package/src/channels/shared/control-command.mjs +1 -1
- package/src/channels/shared/deferred-delivery-coordinator.mjs +1 -1
- package/src/channels/shared/history-command.mjs +1 -1
- package/src/channels/shared/i18n-en/shared-a.mjs +41 -0
- package/src/channels/shared/i18n-en/wecom.mjs +1 -1
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/model-command.mjs +5 -3
- package/src/channels/shared/workspace-command.mjs +114 -9
- package/src/channels/shared/workspace-session.mjs +55 -5
- package/src/channels/telegram/telegram-runtime.mjs +1 -1
- package/src/channels/wecom/wecom-bridge.mjs +14 -46
- package/src/channels/wecom/wecom-runtime.mjs +1 -1
- package/src/channels/weixin/connection-error.en.mjs +116 -0
- package/src/channels/weixin/connection-error.mjs +204 -0
- package/src/channels/weixin/diagnostic-details.mjs +40 -0
- package/src/channels/weixin/state-store.mjs +4 -3
- package/src/channels/weixin/weixin-api.mjs +20 -8
- package/src/channels/weixin/weixin-bridge.mjs +3 -2
- package/src/channels/weixin/weixin-controller.mjs +133 -104
- package/src/channels/weixin/weixin-runtime.mjs +35 -24
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Shared diagnostic copy for the WeChat Host and settings client.
|
|
2
|
+
export default {
|
|
3
|
+
"插件版本": "Plugin version",
|
|
4
|
+
"DSH 微信管理接口返回了无法识别的响应,请重新读取状态。": "The DSH WeChat management endpoint returned an invalid response. Refresh the status.",
|
|
5
|
+
"无法读取现有登录凭据。请检查 DSH 凭据存储。": "Could not read the saved login credential. Check the DSH credential store.",
|
|
6
|
+
"登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。": "Could not save the login credential to DSH. Check whether the credential store is writable.",
|
|
7
|
+
"账号配置无法写入本机。请检查 DSH_HOME 目录权限。": "Could not save the account configuration. Check permissions on DSH_HOME.",
|
|
8
|
+
"无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。": "Could not prepare the account state or workspace. Check DSH_HOME and the workspace directory.",
|
|
9
|
+
"插件无法连接当前 Harness,请检查宿主是否已就绪。": "The plugin could not connect to this Harness. Check whether the host is ready.",
|
|
10
|
+
"Harness 健康检查超时。请确认 dsh web 未阻塞。": "The Harness health check timed out. Check whether the host is responsive.",
|
|
11
|
+
"Harness 健康检查需要身份认证。请检查代理、网关或自定义鉴权配置。": "The Harness health check requires authentication. Check proxy, gateway or custom authentication settings.",
|
|
12
|
+
"本机 Harness 请求被代理要求认证。请让回环地址绕过代理,并检查 NO_PROXY 配置。": "The proxy requires authentication for the local Harness request. Bypass the proxy for loopback addresses and check NO_PROXY.",
|
|
13
|
+
"Harness 异常拒绝了回环地址的健康检查。请检查 HTTP 代理、Harness 源码版本和构建产物。": "Harness rejected a loopback health check. Check the HTTP proxy and the running Harness build.",
|
|
14
|
+
"Harness 的 Host 信任检查拒绝了非回环地址请求。请检查 harnessBaseUrl 与 trustedHosts 配置。": "Harness rejected the non-loopback Host. Check harnessBaseUrl and trustedHosts.",
|
|
15
|
+
"健康检查收到了非 Harness 标准的 403 拒绝响应。请检查代理或网关配置。": "The health check received a non-standard HTTP 403 response. Check proxy or gateway settings.",
|
|
16
|
+
"找不到 Harness 健康检查接口。请确认 Harness 与插件版本兼容。": "The Harness health check endpoint was not found. Check Harness and plugin compatibility.",
|
|
17
|
+
"Harness 健康检查返回服务错误。请查看 DSH 日志。": "The Harness health check returned a service error. Check the DSH logs.",
|
|
18
|
+
"Harness 返回了无法识别的响应。请确认 Harness 与插件版本兼容。": "Harness returned an unrecognized response. Check Harness and plugin compatibility.",
|
|
19
|
+
"Harness 拒绝了健康检查请求。请查看 DSH 日志。": "Harness rejected the health check. Check the DSH logs.",
|
|
20
|
+
"Harness 健康检查发生未知错误。请查看 DSH 日志。": "The Harness health check failed with an unknown error. Check the DSH logs.",
|
|
21
|
+
"消息连接初始化失败。请查看 DSH 日志后重试。": "The message connection could not start. Check the DSH logs and retry.",
|
|
22
|
+
"暂时无法访问微信服务。": "The WeChat service is temporarily unreachable.",
|
|
23
|
+
"微信服务请求超时。": "The WeChat service request timed out.",
|
|
24
|
+
"微信服务请求失败(HTTP {status})。": "The WeChat service request failed (HTTP {status}).",
|
|
25
|
+
"微信服务返回了无法解析的响应。": "The WeChat service returned an unparseable response.",
|
|
26
|
+
"微信服务没有返回有效二维码。": "The WeChat service did not return a valid QR code.",
|
|
27
|
+
"微信服务返回了不受信任的扫码地址。": "The WeChat service returned an untrusted QR-code URL.",
|
|
28
|
+
"微信服务返回了无效的连接地址。": "The WeChat service returned an invalid connection URL.",
|
|
29
|
+
"微信服务返回了不受信任的连接地址。": "The WeChat service returned an untrusted connection URL.",
|
|
30
|
+
"拒绝访问不受信任的微信服务地址。": "Refusing to access an untrusted WeChat service URL.",
|
|
31
|
+
"微信服务返回了无法识别的扫码状态。": "The WeChat service returned an unrecognized QR-code status.",
|
|
32
|
+
"微信授权成功,但返回的账号凭据不完整。": "WeChat authorized the account but returned incomplete credentials.",
|
|
33
|
+
"微信服务拒绝了二维码申请。": "The WeChat service rejected the QR-code request.",
|
|
34
|
+
"已取得扫码地址,但本机二维码图片生成失败。": "The login URL was obtained, but the QR-code image could not be generated locally.",
|
|
35
|
+
"无法生成微信二维码。": "Could not generate a WeChat QR code.",
|
|
36
|
+
"这次微信绑定任务已不存在,请重新读取状态或生成二维码。": "This WeChat setup attempt no longer exists. Refresh the status or generate a new QR code.",
|
|
37
|
+
"这次微信绑定任务当前不能提交配对码,请重新读取状态。": "This setup attempt is not waiting for a pairing code. Refresh the status.",
|
|
38
|
+
"登录凭据缺失,请移除账号后重新扫码。": "The login credential is missing. Remove the account and scan a new QR code.",
|
|
39
|
+
"微信登录凭据已失效,请移除账号后重新扫码。": "The WeChat login credential has expired. Remove the account and scan a new QR code.",
|
|
40
|
+
"无法从 DSH 凭据存储移除微信登录凭据。": "Could not remove the WeChat login credential from the DSH credential store.",
|
|
41
|
+
"微信账号配置移除失败。": "Could not remove the WeChat account configuration.",
|
|
42
|
+
"无法读取微信账号状态文件。": "Could not read the WeChat account state file.",
|
|
43
|
+
"无法保存微信账号状态文件。": "Could not save the WeChat account state file.",
|
|
44
|
+
"微信账号已移除,但本机状态文件清理失败。": "The WeChat account was removed, but its local state file could not be cleaned up.",
|
|
45
|
+
"无法保存微信工作区设置。": "Could not save the WeChat workspace settings.",
|
|
46
|
+
"微信账号已移除,但工作区设置清理失败。": "The WeChat account was removed, but its workspace settings could not be cleaned up.",
|
|
47
|
+
"微信账号连接启动失败。": "The WeChat account connection could not start.",
|
|
48
|
+
"微信消息同步请求被拒绝。": "WeChat rejected the message sync request.",
|
|
49
|
+
"微信服务未确认停止通知。": "WeChat did not confirm the stop notification.",
|
|
50
|
+
"微信消息连接停止时发生错误。": "An error occurred while stopping the WeChat connection.",
|
|
51
|
+
"无法读取微信连接状态,请重新读取;之前的操作可能已完成。": "Could not read the WeChat connection status. Refresh it; the previous operation may have completed.",
|
|
52
|
+
"微信激活过程中发生未知错误。": "An unknown error occurred while activating the WeChat account.",
|
|
53
|
+
"微信操作发生未知错误。": "The WeChat operation failed with an unknown error.",
|
|
54
|
+
"微信配置格式错误,请检查 config.json 和 workspaces.json 后重启 DSH。": "The WeChat configuration is invalid. Check config.json and workspaces.json, then restart DSH.",
|
|
55
|
+
"微信配置访问权限不足,请检查数据目录权限后重启 DSH。": "The WeChat configuration could not be accessed. Check the data directory permissions and restart DSH.",
|
|
56
|
+
"微信初始化失败,请查看 DSH 启动日志。": "WeChat initialization failed. Check the DSH startup logs.",
|
|
57
|
+
"微信连接未就绪。": "The WeChat connection is not ready.",
|
|
58
|
+
"微信操作失败后,原账号状态恢复失败。": "The previous account state could not be restored after the failed WeChat operation.",
|
|
59
|
+
"原账号状态尚未确认,请重新读取状态,并反馈诊断信息。": "The previous account state has not been confirmed. Refresh the status and share the diagnostic information.",
|
|
60
|
+
"操作未完成,已恢复之前的本机状态。请处理上述问题后重试。": "The operation did not complete. The previous local state was restored. Resolve the problem above before retrying.",
|
|
61
|
+
"凭据由只读来源提供,请在启动 DSH 的配置中检查该来源。": "The credential comes from a read-only source. Check that source in the configuration used to launch DSH.",
|
|
62
|
+
"微信服务域名解析失败,请检查运行 DSH 的机器的网络和 DNS 设置后重试。": "The WeChat service domain could not be resolved. Check the network and DNS settings on the machine running DSH and retry.",
|
|
63
|
+
"请检查 DSH 数据目录和对应文件的读写权限。": "Check read and write permissions on the DSH data directory and the affected file.",
|
|
64
|
+
"磁盘空间不足,请释放空间后重试。": "The disk is full. Free up space and retry.",
|
|
65
|
+
"本机文件格式无效,请检查对应配置或状态文件;不要清空登录凭据。": "The local file format is invalid. Check the affected configuration or state file; do not clear login credentials.",
|
|
66
|
+
"请检查运行 DSH 的机器的系统时间、证书和网络设置。": "Check the system time, certificates and network settings on the machine running DSH.",
|
|
67
|
+
"请移除失效接入并重新扫码绑定。": "Remove the invalid connection and scan a new QR code to connect.",
|
|
68
|
+
"请确认当前 DSH 宿主已就绪,并检查 Harness 与插件实际运行版本及宿主日志。": "Check that this DSH host is ready, and check the running Harness and plugin versions and the host logs.",
|
|
69
|
+
"微信服务限流,请稍后重试。": "The WeChat service is rate-limiting requests. Try again later.",
|
|
70
|
+
"访问被拒绝,请检查服务访问限制;HTTP 状态本身不能确认登录凭据已失效。": "Access was denied. Check service access restrictions; the HTTP status alone does not establish that the login credential expired.",
|
|
71
|
+
"请检查运行 DSH 的机器能否访问微信服务,然后重试。": "Check whether the machine running DSH can access the WeChat service, then retry.",
|
|
72
|
+
"请按参考号查看 DSH 日志,并复制诊断信息反馈。": "Find this reference in the DSH logs and copy the diagnostic information when reporting the issue.",
|
|
73
|
+
"无法完成微信管理请求,请检查 DSH 连接后重新读取状态。": "The WeChat management request could not be completed. Check the DSH connection and refresh the status.",
|
|
74
|
+
"查询微信扫码状态失败": "Could not query WeChat QR-code status",
|
|
75
|
+
"无法完成微信管理请求": "Could not complete the WeChat management request",
|
|
76
|
+
"无法生成微信二维码": "Could not generate a WeChat QR code",
|
|
77
|
+
"状态读取失败,以下是上次读取的状态。": "Status refresh failed. The last known status is shown below.",
|
|
78
|
+
"诊断详情": "Diagnostic details",
|
|
79
|
+
"诊断信息": "Diagnostic information",
|
|
80
|
+
"复制诊断信息": "Copy diagnostic information",
|
|
81
|
+
"诊断信息已复制": "Diagnostic information copied",
|
|
82
|
+
"未取得 Host 诊断参考号。": "No diagnostic reference was received from the host.",
|
|
83
|
+
"无法访问剪贴板,请选择并复制以下诊断信息。": "Clipboard access is unavailable. Select and copy the diagnostic information below.",
|
|
84
|
+
"错误码": "Error code",
|
|
85
|
+
"失败阶段": "Failed step",
|
|
86
|
+
"底层原因": "Underlying cause",
|
|
87
|
+
"HTTP 状态": "HTTP status",
|
|
88
|
+
"微信返回码": "WeChat return code",
|
|
89
|
+
"参考号": "Reference",
|
|
90
|
+
"发生时间": "Time",
|
|
91
|
+
"加载微信配置": "Load WeChat configuration",
|
|
92
|
+
"申请二维码": "Request QR code",
|
|
93
|
+
"生成二维码图片": "Generate QR-code image",
|
|
94
|
+
"查询扫码状态": "Query QR-code status",
|
|
95
|
+
"提交配对码": "Submit pairing code",
|
|
96
|
+
"取消绑定": "Cancel setup",
|
|
97
|
+
"读取登录凭据": "Read login credential",
|
|
98
|
+
"保存登录凭据": "Save login credential",
|
|
99
|
+
"移除登录凭据": "Remove login credential",
|
|
100
|
+
"保存账号配置": "Save account configuration",
|
|
101
|
+
"移除账号配置": "Remove account configuration",
|
|
102
|
+
"读取账号状态": "Read account state",
|
|
103
|
+
"保存账号状态": "Save account state",
|
|
104
|
+
"清理账号状态": "Clean up account state",
|
|
105
|
+
"保存工作区设置": "Save workspace settings",
|
|
106
|
+
"清理工作区设置": "Clean up workspace settings",
|
|
107
|
+
"准备消息连接": "Prepare message connection",
|
|
108
|
+
"激活微信账号": "Activate WeChat account",
|
|
109
|
+
"检查 DSH 宿主": "Check DSH host",
|
|
110
|
+
"启动微信连接": "Start WeChat connection",
|
|
111
|
+
"同步微信消息": "Sync WeChat messages",
|
|
112
|
+
"停止微信连接": "Stop WeChat connection",
|
|
113
|
+
"读取连接状态": "Read connection status",
|
|
114
|
+
"恢复原账号状态": "Restore previous account state",
|
|
115
|
+
"访问 DSH 管理接口": "Access DSH management API"
|
|
116
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import packageInfo from '../../../package.json' with { type: 'json' };
|
|
3
|
+
import { t } from '../shared/i18n.mjs';
|
|
4
|
+
import { normalizeWeixinDiagnosticDetails } from './diagnostic-details.mjs';
|
|
5
|
+
|
|
6
|
+
const MESSAGES = Object.freeze({
|
|
7
|
+
'credential-read-failed': '无法读取现有登录凭据。请检查 DSH 凭据存储。',
|
|
8
|
+
'credential-save-failed': '登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。',
|
|
9
|
+
'account-config-save-failed': '账号配置无法写入本机。请检查 DSH_HOME 目录权限。',
|
|
10
|
+
'runtime-prepare-failed': '无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。',
|
|
11
|
+
'harness-connect-failed': '插件无法连接当前 Harness,请检查宿主是否已就绪。',
|
|
12
|
+
'harness-timeout': 'Harness 健康检查超时。请确认 dsh web 未阻塞。',
|
|
13
|
+
'harness-auth-required': 'Harness 健康检查需要身份认证。请检查代理、网关或自定义鉴权配置。',
|
|
14
|
+
'harness-proxy-auth-required': '本机 Harness 请求被代理要求认证。请让回环地址绕过代理,并检查 NO_PROXY 配置。',
|
|
15
|
+
'harness-loopback-forbidden': 'Harness 异常拒绝了回环地址的健康检查。请检查 HTTP 代理、Harness 源码版本和构建产物。',
|
|
16
|
+
'harness-host-untrusted': 'Harness 的 Host 信任检查拒绝了非回环地址请求。请检查 harnessBaseUrl 与 trustedHosts 配置。',
|
|
17
|
+
'harness-request-forbidden': '健康检查收到了非 Harness 标准的 403 拒绝响应。请检查代理或网关配置。',
|
|
18
|
+
'harness-api-not-found': '找不到 Harness 健康检查接口。请确认 Harness 与插件版本兼容。',
|
|
19
|
+
'harness-http-failed': 'Harness 健康检查返回服务错误。请查看 DSH 日志。',
|
|
20
|
+
'harness-response-invalid': 'Harness 返回了无法识别的响应。请确认 Harness 与插件版本兼容。',
|
|
21
|
+
'harness-rpc-rejected': 'Harness 拒绝了健康检查请求。请查看 DSH 日志。',
|
|
22
|
+
'harness-check-unknown-failed': 'Harness 健康检查发生未知错误。请查看 DSH 日志。',
|
|
23
|
+
'connection-start-failed': '消息连接初始化失败。请查看 DSH 日志后重试。',
|
|
24
|
+
|
|
25
|
+
'network-error': '暂时无法访问微信服务。',
|
|
26
|
+
'timeout': '微信服务请求超时。',
|
|
27
|
+
'http-error': '微信服务请求失败(HTTP {status})。',
|
|
28
|
+
'invalid-response': '微信服务返回了无法解析的响应。',
|
|
29
|
+
'invalid-qr': '微信服务没有返回有效二维码。',
|
|
30
|
+
'untrusted-qr': '微信服务返回了不受信任的扫码地址。',
|
|
31
|
+
'invalid-base-url': '微信服务返回了无效的连接地址。',
|
|
32
|
+
'untrusted-base-url': '微信服务返回了不受信任的连接地址。',
|
|
33
|
+
'untrusted-endpoint': '拒绝访问不受信任的微信服务地址。',
|
|
34
|
+
'invalid-login-status': '微信服务返回了无法识别的扫码状态。',
|
|
35
|
+
'incomplete-login': '微信授权成功,但返回的账号凭据不完整。',
|
|
36
|
+
'qr-request-rejected': '微信服务拒绝了二维码申请。',
|
|
37
|
+
'qr-encode-failed': '已取得扫码地址,但本机二维码图片生成失败。',
|
|
38
|
+
'qr-start-failed': '无法生成微信二维码。',
|
|
39
|
+
'provision-attempt-not-found': '这次微信绑定任务已不存在,请重新读取状态或生成二维码。',
|
|
40
|
+
'provision-state-invalid': '这次微信绑定任务当前不能提交配对码,请重新读取状态。',
|
|
41
|
+
'missing-token': '登录凭据缺失,请移除账号后重新扫码。',
|
|
42
|
+
'stale-token': '微信登录凭据已失效,请移除账号后重新扫码。',
|
|
43
|
+
'credential-remove-failed': '无法从 DSH 凭据存储移除微信登录凭据。',
|
|
44
|
+
'account-config-remove-failed': '微信账号配置移除失败。',
|
|
45
|
+
'account-state-read-failed': '无法读取微信账号状态文件。',
|
|
46
|
+
'account-state-write-failed': '无法保存微信账号状态文件。',
|
|
47
|
+
'account-state-cleanup-failed': '微信账号已移除,但本机状态文件清理失败。',
|
|
48
|
+
'workspace-save-failed': '无法保存微信工作区设置。',
|
|
49
|
+
'workspace-cleanup-failed': '微信账号已移除,但工作区设置清理失败。',
|
|
50
|
+
'start-rejected': '微信账号连接启动失败。',
|
|
51
|
+
'updates-rejected': '微信消息同步请求被拒绝。',
|
|
52
|
+
'stop-rejected': '微信服务未确认停止通知。',
|
|
53
|
+
'connection-stop-failed': '微信消息连接停止时发生错误。',
|
|
54
|
+
'status-read-failed': '无法读取微信连接状态,请重新读取;之前的操作可能已完成。',
|
|
55
|
+
'activation-unknown-failed': '微信激活过程中发生未知错误。',
|
|
56
|
+
'weixin-operation-failed': '微信操作发生未知错误。',
|
|
57
|
+
'weixin-startup-config-invalid': '微信配置格式错误,请检查 config.json 和 workspaces.json 后重启 DSH。',
|
|
58
|
+
'weixin-startup-permission-denied': '微信配置访问权限不足,请检查数据目录权限后重启 DSH。',
|
|
59
|
+
'weixin-startup-failed': '微信初始化失败,请查看 DSH 启动日志。',
|
|
60
|
+
'connection-failed': '微信连接未就绪。',
|
|
61
|
+
'rollback-failed': '微信操作失败后,原账号状态恢复失败。',
|
|
62
|
+
'workspace-not-absolute': '请输入工作区绝对路径。',
|
|
63
|
+
'workspace-not-found': '工作区目录不存在。',
|
|
64
|
+
'workspace-not-directory': '工作区路径不是目录。',
|
|
65
|
+
'workspace-bot-not-found': '找不到要修改的机器人。',
|
|
66
|
+
'agent-preset-invalid': 'Agent Preset 无效。',
|
|
67
|
+
'agent-preset-unavailable': 'Agent Preset 不存在或不可用。',
|
|
68
|
+
'model-selection-invalid': '模型无效。',
|
|
69
|
+
'model-selection-unavailable': '模型不存在或不可用。',
|
|
70
|
+
'model-reasoning-unavailable': '当前模型不支持所选思考强度,请重新选择。',
|
|
71
|
+
'context-enhancement-invalid': '上下文增强设置无效。',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const ownedStages = new WeakMap();
|
|
75
|
+
const reportedErrors = new WeakMap();
|
|
76
|
+
const CODE_STAGES = {
|
|
77
|
+
'credential-read-failed': ['credential.read', 'credential-store'],
|
|
78
|
+
'credential-save-failed': ['credential.save', 'credential-store'],
|
|
79
|
+
'credential-remove-failed': ['credential.remove', 'credential-store'],
|
|
80
|
+
'missing-token': ['credential.read', 'credential-store'],
|
|
81
|
+
'account-config-save-failed': ['account.save', 'account-config'],
|
|
82
|
+
'account-config-remove-failed': ['account.remove', 'account-config'],
|
|
83
|
+
'account-state-read-failed': ['state.load', 'account-state'],
|
|
84
|
+
'account-state-write-failed': ['state.write', 'account-state'],
|
|
85
|
+
'account-state-cleanup-failed': ['state.cleanup', 'account-state'],
|
|
86
|
+
'workspace-save-failed': ['workspace.write', 'workspace-config'],
|
|
87
|
+
'workspace-cleanup-failed': ['workspace.cleanup', 'workspace-config'],
|
|
88
|
+
'qr-encode-failed': ['qr.encode'], 'qr-request-rejected': ['qr.begin'],
|
|
89
|
+
'start-rejected': ['connection.start'], 'updates-rejected': ['connection.poll'],
|
|
90
|
+
'stop-rejected': ['connection.stop'], 'rollback-failed': ['rollback'],
|
|
91
|
+
'runtime-prepare-failed': ['runtime.prepare'], 'connection-start-failed': ['connection.start'],
|
|
92
|
+
'status-read-failed': ['status.read'],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function knownWeixinErrorCode(code) { return Object.hasOwn(MESSAGES, code ?? ''); }
|
|
96
|
+
|
|
97
|
+
/** Annotate at the operation that knows the failing resource; do not inspect exception prose. */
|
|
98
|
+
export function weixinStageError(code, cause, stage) {
|
|
99
|
+
if (reportedErrors.has(cause)) return cause;
|
|
100
|
+
const error = new Error('Weixin operation failed', { cause });
|
|
101
|
+
error.code = knownWeixinErrorCode(code) ? code : 'weixin-operation-failed';
|
|
102
|
+
const defaults = CODE_STAGES[error.code] ?? [];
|
|
103
|
+
ownedStages.set(error, { stage: stage ?? defaults[0], resource: defaults[1] });
|
|
104
|
+
return error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function chainOf(error) {
|
|
108
|
+
const chain = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
while (error && typeof error === 'object' && !seen.has(error) && chain.length < 4) {
|
|
111
|
+
seen.add(error);
|
|
112
|
+
chain.push(error);
|
|
113
|
+
error = error.cause;
|
|
114
|
+
}
|
|
115
|
+
return chain;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hintFor(code, details) {
|
|
119
|
+
if (details.rollback === 'failed' || details.rollback === 'unknown') return t('原账号状态尚未确认,请重新读取状态,并反馈诊断信息。');
|
|
120
|
+
if (details.rollback === 'succeeded') return t('操作未完成,已恢复之前的本机状态。请处理上述问题后重试。');
|
|
121
|
+
if (code.startsWith('harness-')) return t('请确认当前 DSH 宿主已就绪,并检查 Harness 与插件实际运行版本及宿主日志。');
|
|
122
|
+
if (details.reason === 'read-only') return t('凭据由只读来源提供,请在启动 DSH 的配置中检查该来源。');
|
|
123
|
+
if (details.reason === 'ENOTFOUND' || details.reason === 'EAI_AGAIN') return t('微信服务域名解析失败,请检查运行 DSH 的机器的网络和 DNS 设置后重试。');
|
|
124
|
+
if (['EACCES', 'EPERM', 'EROFS'].includes(details.reason)) return t('请检查 DSH 数据目录和对应文件的读写权限。');
|
|
125
|
+
if (details.reason === 'ENOSPC') return t('磁盘空间不足,请释放空间后重试。');
|
|
126
|
+
if (details.reason === 'invalid-json' && details.resource) return t('本机文件格式无效,请检查对应配置或状态文件;不要清空登录凭据。');
|
|
127
|
+
if (/CERT|TLS|SSL/.test(details.reason ?? '')) return t('请检查运行 DSH 的机器的系统时间、证书和网络设置。');
|
|
128
|
+
if (code === 'stale-token' || code === 'missing-token') return t('请移除失效接入并重新扫码绑定。');
|
|
129
|
+
if (details.httpStatus === 429) return t('微信服务限流,请稍后重试。');
|
|
130
|
+
if (details.httpStatus === 401 || details.httpStatus === 403) return t('访问被拒绝,请检查服务访问限制;HTTP 状态本身不能确认登录凭据已失效。');
|
|
131
|
+
if (code === 'network-error' || code === 'timeout') return t('请检查运行 DSH 的机器能否访问微信服务,然后重试。');
|
|
132
|
+
return t('请按参考号查看 DSH 日志,并复制诊断信息反馈。');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createWeixinDiagnostics({ logger = console, now = Date.now } = {}) {
|
|
136
|
+
const recent = new Map();
|
|
137
|
+
function write(level, record) {
|
|
138
|
+
try { logger[level]?.(`[dsh-weixin] ${JSON.stringify(record)}`); } catch { /* Diagnostics must not change the operation outcome. */ }
|
|
139
|
+
}
|
|
140
|
+
function report(cause, context = {}) {
|
|
141
|
+
if (reportedErrors.has(cause)) return cause;
|
|
142
|
+
const chain = chainOf(cause);
|
|
143
|
+
const selected = chain.find(error => knownWeixinErrorCode(error.code));
|
|
144
|
+
const code = selected?.code ?? (knownWeixinErrorCode(context.code) ? context.code : 'weixin-operation-failed');
|
|
145
|
+
const staged = chain.map(error => ownedStages.get(error)).find(Boolean) ?? {};
|
|
146
|
+
const defaults = CODE_STAGES[code] ?? [];
|
|
147
|
+
const reason = chain.map(error => normalizeWeixinDiagnosticDetails({ reason: error.code }).reason).find(Boolean)
|
|
148
|
+
?? (chain.some(error => error instanceof SyntaxError) ? 'invalid-json' : undefined);
|
|
149
|
+
const numeric = field => chain.map(error => normalizeWeixinDiagnosticDetails({ [field]: field === 'httpStatus' ? error.status : error[field] })[field]).find(value => value !== undefined);
|
|
150
|
+
const details = normalizeWeixinDiagnosticDetails({
|
|
151
|
+
...context, stage: staged.stage ?? defaults[0] ?? (code.startsWith('harness-') ? 'harness.check' : context.stage),
|
|
152
|
+
resource: staged.resource ?? defaults[1] ?? context.resource,
|
|
153
|
+
reason: reason ?? context.reason, httpStatus: numeric('httpStatus'), providerCode: numeric('providerCode'), pluginVersion: packageInfo.version,
|
|
154
|
+
});
|
|
155
|
+
const botId = /^wx_[a-f0-9]{24}$/.test(context.botId ?? '') ? context.botId : undefined;
|
|
156
|
+
const key = JSON.stringify([botId, details.operation, details.stage, code, details.reason, details.httpStatus, details.providerCode]);
|
|
157
|
+
const previous = recent.get(key);
|
|
158
|
+
const time = now();
|
|
159
|
+
if (context.automatic && previous && time - previous.time < 60_000) {
|
|
160
|
+
previous.retries += 1;
|
|
161
|
+
const wrapped = new Error(previous.publicError.message, { cause });
|
|
162
|
+
wrapped.code = code;
|
|
163
|
+
wrapped.publicError = structuredClone(previous.publicError);
|
|
164
|
+
reportedErrors.set(wrapped, true);
|
|
165
|
+
return wrapped;
|
|
166
|
+
}
|
|
167
|
+
if (previous?.retries) write('warn', { event: 'connection-retries', referenceId: previous.publicError.details.referenceId, retries: previous.retries });
|
|
168
|
+
details.referenceId = `WX-CONN-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
|
169
|
+
details.occurredAt = new Date(time).toISOString();
|
|
170
|
+
details.hint = hintFor(code, details);
|
|
171
|
+
const publicError = { code, message: t(MESSAGES[code], { status: details.httpStatus ?? '?' }), details };
|
|
172
|
+
const wrapped = new Error(publicError.message, { cause });
|
|
173
|
+
wrapped.code = code;
|
|
174
|
+
wrapped.publicError = publicError;
|
|
175
|
+
reportedErrors.set(wrapped, true);
|
|
176
|
+
if (context.automatic) {
|
|
177
|
+
if (recent.size >= 128) recent.delete(recent.keys().next().value);
|
|
178
|
+
recent.set(key, { time, publicError: structuredClone(publicError), retries: 0, botId });
|
|
179
|
+
}
|
|
180
|
+
write(context.warning ? 'warn' : 'error', {
|
|
181
|
+
event: context.warning ? 'connection-warning' : 'connection-failure', code, ...details,
|
|
182
|
+
...(botId ? { botId } : {}), pluginVersion: packageInfo.version, nodeVersion: process.versions.node, platform: process.platform,
|
|
183
|
+
...(/^WX-CONN-[A-F0-9]{8}$/.test(context.parentReferenceId ?? '') ? { parentReferenceId: context.parentReferenceId } : {}),
|
|
184
|
+
...Object.fromEntries(['durationMs', 'timeoutMs'].filter(field => Number.isFinite(cause?.[field]) && cause[field] >= 0).map(field => [field, cause[field]])),
|
|
185
|
+
});
|
|
186
|
+
return wrapped;
|
|
187
|
+
}
|
|
188
|
+
function clear(botId) {
|
|
189
|
+
for (const [key, entry] of recent) {
|
|
190
|
+
if (botId !== undefined && entry.botId !== botId) continue;
|
|
191
|
+
if (entry.retries) write('info', { event: 'connection-retries', referenceId: entry.publicError.details.referenceId, retries: entry.retries });
|
|
192
|
+
recent.delete(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function outcome(error, rollback) {
|
|
196
|
+
if (!reportedErrors.has(error)) return;
|
|
197
|
+
const details = normalizeWeixinDiagnosticDetails({ rollback });
|
|
198
|
+
if (!details.rollback) return;
|
|
199
|
+
error.publicError.details.rollback = details.rollback;
|
|
200
|
+
error.publicError.details.hint = hintFor(error.code, error.publicError.details);
|
|
201
|
+
write('info', { event: 'connection-outcome', referenceId: error.publicError.details.referenceId, ...details });
|
|
202
|
+
}
|
|
203
|
+
return { report, clear, outcome };
|
|
204
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shared by Host and Client: only explicitly admitted diagnostic facts cross the UI boundary.
|
|
2
|
+
const OPERATIONS = new Set([
|
|
3
|
+
'startup', 'connection.restore', 'connection.monitor', 'connection.close', 'connection.status',
|
|
4
|
+
'provision.begin', 'provision.poll', 'provision.verify', 'provision.cancel', 'bot.reconnect', 'bot.delete',
|
|
5
|
+
'bot.workspace.set', 'bot.model.set', 'bot.agent-preset.set', 'bot.context-enhancement.set', 'bot.access-policy.set', 'bot.alias.set',
|
|
6
|
+
]);
|
|
7
|
+
const STAGES = new Set([
|
|
8
|
+
'startup.load', 'qr.begin', 'qr.encode', 'qr.poll', 'qr.verify', 'qr.cancel', 'credential.read', 'credential.save', 'credential.remove',
|
|
9
|
+
'account.save', 'account.remove', 'state.load', 'state.write', 'state.cleanup', 'workspace.write', 'workspace.cleanup',
|
|
10
|
+
'runtime.prepare', 'activation', 'harness.check', 'connection.start', 'connection.poll', 'connection.stop', 'status.read', 'rollback', 'management.request',
|
|
11
|
+
]);
|
|
12
|
+
const REASONS = new Set([
|
|
13
|
+
'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ETIMEDOUT', 'EPIPE',
|
|
14
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
|
|
15
|
+
'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
16
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID', 'ERR_SSL_WRONG_VERSION_NUMBER',
|
|
17
|
+
'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'EROFS', 'ENOTDIR', 'EISDIR', 'EBUSY', 'EIO', 'EXDEV', 'EMFILE', 'ENFILE', 'EEXIST',
|
|
18
|
+
'invalid-json', 'read-only',
|
|
19
|
+
]);
|
|
20
|
+
const RESOURCES = new Set(['credential-store', 'account-config', 'account-state', 'workspace-config', 'workspace-directory']);
|
|
21
|
+
|
|
22
|
+
export function normalizeWeixinDiagnosticDetails(value) {
|
|
23
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES]]) {
|
|
26
|
+
if (allowed.has(value[field])) result[field] = value[field];
|
|
27
|
+
}
|
|
28
|
+
if (/^WX-CONN-[A-F0-9]{8}$/.test(value.referenceId ?? '')) result.referenceId = value.referenceId;
|
|
29
|
+
if (typeof value.occurredAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value.occurredAt)
|
|
30
|
+
&& Number.isFinite(Date.parse(value.occurredAt))) result.occurredAt = value.occurredAt;
|
|
31
|
+
if (Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599) result.httpStatus = value.httpStatus;
|
|
32
|
+
const provider = typeof value.providerCode === 'number' && Number.isSafeInteger(value.providerCode)
|
|
33
|
+
? String(value.providerCode) : value.providerCode;
|
|
34
|
+
if (typeof provider === 'string' && /^-?\d{1,12}$/.test(provider)) result.providerCode = provider;
|
|
35
|
+
if (typeof value.pluginVersion === 'string' && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.pluginVersion)
|
|
36
|
+
&& value.pluginVersion.length <= 64) result.pluginVersion = value.pluginVersion;
|
|
37
|
+
if (['succeeded', 'failed', 'not-attempted', 'unknown'].includes(value.rollback)) result.rollback = value.rollback;
|
|
38
|
+
if (typeof value.hint === 'string' && value.hint.trim()) result.hint = value.hint.trim().slice(0, 500);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { weixinStageError } from './connection-error.mjs';
|
|
1
2
|
import { deferredStateAccess, normalizeDeferredState } from '../shared/deferred-state.mjs';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
@@ -123,7 +124,7 @@ export class WeixinStateStore {
|
|
|
123
124
|
try {
|
|
124
125
|
this.#state = normalizeState(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
125
126
|
} catch (error) {
|
|
126
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
127
|
+
if (error?.code !== 'ENOENT') throw weixinStageError('account-state-read-failed', error);
|
|
127
128
|
this.#state = structuredClone(EMPTY_STATE);
|
|
128
129
|
await this.#persist();
|
|
129
130
|
}
|
|
@@ -271,7 +272,7 @@ export class WeixinStateStore {
|
|
|
271
272
|
try {
|
|
272
273
|
await unlink(this.#path);
|
|
273
274
|
} catch (error) {
|
|
274
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
275
|
+
if (error?.code !== 'ENOENT') throw weixinStageError('account-state-cleanup-failed', error);
|
|
275
276
|
}
|
|
276
277
|
this.#state = structuredClone(EMPTY_STATE);
|
|
277
278
|
}
|
|
@@ -285,6 +286,6 @@ export class WeixinStateStore {
|
|
|
285
286
|
await rename(temporary, this.#path);
|
|
286
287
|
});
|
|
287
288
|
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
288
|
-
await operation;
|
|
289
|
+
await operation.catch(error => { throw weixinStageError('account-state-write-failed', error); });
|
|
289
290
|
}
|
|
290
291
|
}
|
|
@@ -93,9 +93,9 @@ function uncertainWeixinDelivery(cause) {
|
|
|
93
93
|
return preserveArtifactMetadata(error, cause);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function rejectedProviderResponse(value) {
|
|
96
|
+
export function rejectedProviderResponse(value, fields = ['ret', 'errcode']) {
|
|
97
97
|
if (!value || typeof value !== 'object') return null;
|
|
98
|
-
for (const field of
|
|
98
|
+
for (const field of fields) {
|
|
99
99
|
if (value[field] !== undefined && value[field] !== 0 && value[field] !== '0') {
|
|
100
100
|
return safeProviderCode(value[field]) ?? 'rejected';
|
|
101
101
|
}
|
|
@@ -411,6 +411,7 @@ async function requestJson(fetchImpl, {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
const controller = new AbortController();
|
|
414
|
+
const startedAt = Date.now();
|
|
414
415
|
let timedOut = false;
|
|
415
416
|
const onAbort = () => controller.abort(signal?.reason);
|
|
416
417
|
if (signal?.aborted) throw abortError(signal);
|
|
@@ -441,11 +442,16 @@ async function requestJson(fetchImpl, {
|
|
|
441
442
|
}
|
|
442
443
|
} catch (error) {
|
|
443
444
|
if (signal?.aborted) throw abortError(signal);
|
|
445
|
+
let failure;
|
|
444
446
|
if (timedOut) {
|
|
445
|
-
|
|
447
|
+
failure = new WeixinApiError('timeout', '微信服务请求超时。', { cause: error });
|
|
448
|
+
} else {
|
|
449
|
+
failure = error instanceof WeixinApiError ? error
|
|
450
|
+
: new WeixinApiError('network-error', '暂时无法访问微信服务。', { cause: error });
|
|
446
451
|
}
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
failure.durationMs = Date.now() - startedAt;
|
|
453
|
+
failure.timeoutMs = timeoutMs;
|
|
454
|
+
throw failure;
|
|
449
455
|
} finally {
|
|
450
456
|
if (timer) clearTimeout(timer);
|
|
451
457
|
signal?.removeEventListener('abort', onAbort);
|
|
@@ -599,6 +605,8 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
599
605
|
signal,
|
|
600
606
|
});
|
|
601
607
|
const qrcode = nonEmptyString(response?.qrcode);
|
|
608
|
+
const providerCode = rejectedProviderResponse(response, ['errcode', 'ret']);
|
|
609
|
+
if (providerCode) throw new WeixinApiError('qr-request-rejected', '微信服务拒绝了二维码申请。', { providerCode });
|
|
602
610
|
if (!qrcode) throw new WeixinApiError('invalid-qr', '微信服务没有返回二维码令牌。');
|
|
603
611
|
return {
|
|
604
612
|
qrcode,
|
|
@@ -764,14 +772,15 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
764
772
|
timeoutMs: 10_000,
|
|
765
773
|
body: { base_info: baseInfo() },
|
|
766
774
|
});
|
|
767
|
-
|
|
768
|
-
|
|
775
|
+
const providerCode = rejectedProviderResponse(response, ['errcode', 'ret']);
|
|
776
|
+
if (providerCode) {
|
|
777
|
+
throw new WeixinApiError(providerCode === '-14' ? 'stale-token' : 'start-rejected', '微信账号连接启动失败。', { providerCode });
|
|
769
778
|
}
|
|
770
779
|
return response;
|
|
771
780
|
},
|
|
772
781
|
|
|
773
782
|
async notifyStop({ baseUrl, token, signal }) {
|
|
774
|
-
|
|
783
|
+
const response = await requestJson(fetchImpl, {
|
|
775
784
|
method: 'POST',
|
|
776
785
|
baseUrl,
|
|
777
786
|
endpoint: 'ilink/bot/msg/notifystop',
|
|
@@ -780,6 +789,9 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
780
789
|
timeoutMs: 10_000,
|
|
781
790
|
body: { base_info: baseInfo() },
|
|
782
791
|
});
|
|
792
|
+
const providerCode = rejectedProviderResponse(response, ['errcode', 'ret']);
|
|
793
|
+
if (providerCode) throw new WeixinApiError('stop-rejected', '微信服务未确认停止通知。', { providerCode });
|
|
794
|
+
return response;
|
|
783
795
|
},
|
|
784
796
|
});
|
|
785
797
|
}
|
|
@@ -567,9 +567,10 @@ export class WeixinHarnessBridge {
|
|
|
567
567
|
].find(Number.isSafeInteger);
|
|
568
568
|
if (quotedAt === undefined) return { unavailableReason: 'not-delivered' };
|
|
569
569
|
const sender = nonEmptyString(reference?.toUserId);
|
|
570
|
-
const
|
|
570
|
+
const key = sender ? conversationKey(sender) : null;
|
|
571
|
+
const sessionId = key ? this.#state.sessionFor(key) : null;
|
|
571
572
|
const session = typeof sessionId === 'string' && sessionId
|
|
572
|
-
? this.#harness.workspaceSession?.(sessionId)
|
|
573
|
+
? this.#harness.workspaceSession?.(sessionId, key)
|
|
573
574
|
: null;
|
|
574
575
|
if (typeof session?.readHistory !== 'function') {
|
|
575
576
|
return { unavailableReason: 'not-delivered' };
|