@xmanrui/dsh-im 4.22.0 → 4.24.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 +5 -4
- package/README.md +5 -4
- package/lib/client.js +2584 -1153
- package/lib/index.js +290 -298
- package/package.json +14 -2
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +23 -7
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +8 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +69 -1
- package/plugin-src/client/index.js +40 -4
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +8 -2
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +14 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +13 -5
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +699 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +604 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +39 -11
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +287 -57
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +150 -25
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +215 -14
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import packageInfo from '../../../package.json' with { type: 'json' };
|
|
3
|
+
import { normalizeDiagnosticDetails } from './diagnostic-details.mjs';
|
|
4
|
+
import { t } from './i18n.mjs';
|
|
5
|
+
|
|
6
|
+
const reported = new WeakMap();
|
|
7
|
+
const lastReport = new WeakMap();
|
|
8
|
+
const stages = new WeakMap();
|
|
9
|
+
const OWNED_REASONS = { 'connect-timeout': 'timeout', 'telegram-timeout': 'timeout', 'cli-timeout': 'timeout', 'telegram-response-invalid': 'invalid-response', 'office-protocol-mismatch': 'invalid-response' };
|
|
10
|
+
const STANDARD_NAMES = new Set(['Error', 'TypeError', 'SyntaxError', 'AggregateError', 'DOMException', 'AxiosError', 'TimeoutError', 'AbortError']);
|
|
11
|
+
const KNOWN_MESSAGES = {
|
|
12
|
+
'discord-401': 'Discord Bot Token 无效,请重新填写。',
|
|
13
|
+
'discord-intents': 'Discord Gateway Intents 配置不正确,请检查 Developer Portal 的 Bot 设置。',
|
|
14
|
+
'webhook-configured': '该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。',
|
|
15
|
+
'invalid-device-token': 'AI Office Device Token 无效。',
|
|
16
|
+
'office-hook-unavailable': 'AI Office Hook 尚未上线或地址不正确。',
|
|
17
|
+
'messages-database-permission-required': '请在 macOS 系统设置中授予 DSH 完全磁盘访问权限,以读取 Messages 数据库。',
|
|
18
|
+
'messages-automation-permission-required': '请在 macOS 系统设置中允许 DSH 自动控制 Messages。',
|
|
19
|
+
'messages-permission-required': '先在 macOS 系统设置中授予 Messages 权限。',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Inspect a bounded error graph, including SDK aggregate failures. Never copy exception text. */
|
|
23
|
+
export function connectionErrorChain(error) {
|
|
24
|
+
const queue = [error], seen = new Set(), chain = [];
|
|
25
|
+
while (queue.length && chain.length < 16) {
|
|
26
|
+
const entry = queue.shift();
|
|
27
|
+
if (!entry || typeof entry !== 'object' || seen.has(entry)) continue;
|
|
28
|
+
seen.add(entry);
|
|
29
|
+
chain.push(entry);
|
|
30
|
+
if (entry.cause && typeof entry.cause === 'object') queue.push(entry.cause);
|
|
31
|
+
if (Array.isArray(entry.errors)) queue.push(...entry.errors.slice(0, 16));
|
|
32
|
+
}
|
|
33
|
+
return { chain, truncated: queue.some(e => e && typeof e === 'object' && !seen.has(e)) || chain.some(e => Array.isArray(e.errors) && e.errors.length > 16) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractConnectionEvidence(error) {
|
|
37
|
+
const { chain, truncated } = connectionErrorChain(error);
|
|
38
|
+
const errors = chain.map(e => ({
|
|
39
|
+
type: STANDARD_NAMES.has(e.name) ? e.name : 'Error',
|
|
40
|
+
...normalizeDiagnosticDetails({
|
|
41
|
+
reason: OWNED_REASONS[e.code] ?? e.code ?? (e.name === 'TimeoutError' ? 'timeout' : e instanceof SyntaxError ? 'invalid-json' : undefined),
|
|
42
|
+
httpStatus: e.status ?? e.statusCode ?? e.response?.status,
|
|
43
|
+
providerCode: e.providerCode,
|
|
44
|
+
}),
|
|
45
|
+
hasCause: Boolean(e.cause && typeof e.cause === 'object'),
|
|
46
|
+
hasErrors: Array.isArray(e.errors),
|
|
47
|
+
}));
|
|
48
|
+
const allReasons = [...new Set(errors.map(e => e.reason).filter(r => r && !['unknown', 'multiple-causes'].includes(r)))];
|
|
49
|
+
const precise = allReasons.filter(r => !['timeout', 'http-error', 'provider-rejected', 'invalid-response'].includes(r));
|
|
50
|
+
const reasons = precise.length ? precise : allReasons;
|
|
51
|
+
const first = field => chain.map(e => normalizeDiagnosticDetails({ [field]: e[field] })[field]).find(v => v !== undefined);
|
|
52
|
+
return {
|
|
53
|
+
chain, errors,
|
|
54
|
+
details: normalizeDiagnosticDetails({
|
|
55
|
+
reason: reasons.length > 1 ? 'multiple-causes' : reasons[0] ?? 'unknown',
|
|
56
|
+
...(reasons.length > 1 ? { reasons } : {}),
|
|
57
|
+
httpStatus: errors.find(e => e.httpStatus !== undefined)?.httpStatus,
|
|
58
|
+
providerCode: errors.find(e => e.providerCode !== undefined)?.providerCode,
|
|
59
|
+
durationMs: first('durationMs'), timeoutMs: first('timeoutMs'), host: first('host'), truncated,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function atConnectionStage(stage, operation, resource) {
|
|
65
|
+
try { return await operation(); }
|
|
66
|
+
catch (error) { throw connectionStageError(error, stage, resource); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function connectionStageError(cause, stage, resource) {
|
|
70
|
+
if (!cause || typeof cause !== 'object') cause = new Error('Operation failed');
|
|
71
|
+
if (!stages.has(cause)) stages.set(cause, normalizeDiagnosticDetails({ stage, resource }));
|
|
72
|
+
return cause;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Reuse the Controller's report when the same exception reaches RPC. */
|
|
76
|
+
export function reportedConnectionError(error) {
|
|
77
|
+
return connectionErrorChain(error).chain.map(e => reported.get(e) ?? lastReport.get(e)).find(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function diagnosticRpcResult(diagnostics, cause, result, context) {
|
|
81
|
+
if (cause?.name === 'AbortError') return { ok: false, error: { code: 'cancelled', message: t('操作已取消。'), details: {} } };
|
|
82
|
+
if (result?.ok !== false || ['cancelled', 'bad-request'].includes(result.error?.code)) return result;
|
|
83
|
+
const error = reportedConnectionError(cause) ?? diagnostics.report(cause, { ...context, publicError: result.error });
|
|
84
|
+
return { ...result, error: context?.untrustedPublicError ? error.publicError : { ...error.publicError, ...result.error, details: error.publicError.details } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function connectionHint(details) {
|
|
88
|
+
const r = details.reason;
|
|
89
|
+
if (r === 'multiple-causes') return t('检测到多个底层原因,请展开诊断详情逐项检查。');
|
|
90
|
+
if (['ENOTFOUND', 'EAI_AGAIN'].includes(r)) return t('服务域名解析失败,请检查运行 DSH 的机器的 DNS 和网络设置。');
|
|
91
|
+
if (r === 'ECONNREFUSED') return t('目标拒绝连接,请检查服务地址、端口和访问限制。');
|
|
92
|
+
if (['ECONNRESET', 'EPIPE', 'UND_ERR_SOCKET'].includes(r)) return t('连接已中断,请检查网络和目标服务状态。');
|
|
93
|
+
if (['ENETUNREACH', 'EHOSTUNREACH', 'ERR_NETWORK_ACCESS_DENIED'].includes(r)) return t('目标网络不可达,请检查运行 DSH 的机器的网络出口和访问限制。');
|
|
94
|
+
if (/CERT|TLS|SSL|ISSUER|SIGNATURE/.test(r ?? '')) return t('TLS 或证书校验失败,请检查系统时间、证书链和网络设置。');
|
|
95
|
+
if (/TIMEOUT/.test(r ?? '') || r === 'ETIMEDOUT' || r === 'timeout') return t('请求超时,请检查目标服务和网络;诊断详情中可查看已知的耗时。');
|
|
96
|
+
if (r === 'ERR_PROXY_CONNECTION_FAILED' || details.httpStatus === 407) return t('代理连接或认证失败,请检查 DSH 进程使用的代理配置。');
|
|
97
|
+
if (['EAUTH', 'AUTHENTICATIONFAILED'].includes(r)) return t('服务认证失败,请检查账号和授权信息。');
|
|
98
|
+
if (['EACCES', 'EPERM', 'EROFS'].includes(r)) return t('操作权限不足,请检查对应资源的访问权限。');
|
|
99
|
+
if (r === 'ENOSPC') return t('磁盘空间不足,请释放空间后重试。');
|
|
100
|
+
if (r === 'ENOENT') return t('所需文件或本地工具不存在,请检查安装和配置。');
|
|
101
|
+
if (r === 'invalid-json' || r === 'invalid-response') return t('数据格式无效,请按失败阶段检查服务响应或本地配置。');
|
|
102
|
+
if (details.httpStatus === 429) return t('服务正在限流,请稍后重试。');
|
|
103
|
+
if ([401, 403].includes(details.httpStatus)) return t('服务拒绝了请求,请检查凭据、权限或访问限制;HTTP 状态本身不能确认登录失效。');
|
|
104
|
+
if (details.httpStatus >= 500) return t('目标服务暂时异常,请稍后重试。');
|
|
105
|
+
if (details.httpStatus) return t('目标服务返回了 HTTP 错误,请查看诊断详情。');
|
|
106
|
+
return t('暂未识别具体原因,请复制诊断信息,并查看相同参考号的 DSH 日志。');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function operationStage(operation) {
|
|
110
|
+
return ({ 'connector.test': 'connection.test', 'connector.configure': 'credential.verify', 'connector.reconnect': 'connection.start', 'connector.remove': 'account.remove', startup: 'startup.load', 'connection.restore': 'connection.start', 'connection.monitor': 'connection.poll',
|
|
111
|
+
'connection.status': 'status.read', 'connection.close': 'connection.stop', 'provision.begin': 'qr.begin',
|
|
112
|
+
'provision.poll': 'qr.poll', 'provision.verify': 'qr.verify', 'bot.auth.start': 'qr.begin', 'bot.auth.poll': 'qr.poll',
|
|
113
|
+
'bot.reconnect': 'connection.start', 'bot.delete': 'account.remove', 'permissions.status': 'permission.check',
|
|
114
|
+
'bot.bind-credentials': 'credential.verify', 'bot.bind-mailbox': 'credential.verify', 'bot.bind-native': 'permission.check',
|
|
115
|
+
'connection.test': 'connection.test', 'message.send': 'message.send', 'message.receive': 'message.receive',
|
|
116
|
+
})[operation] ?? 'operation';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One reporter per channel owner. Existing safe business guidance can be supplied as publicError. */
|
|
120
|
+
export function createConnectionDiagnostics({ channel, logger = console, now = Date.now, prefix = 'IM-CONN', describe } = {}) {
|
|
121
|
+
const recent = new Map();
|
|
122
|
+
const write = (level, record) => { try { logger[level]?.(`[dsh-${channel}] ${JSON.stringify(record)}`); } catch { /* Logging cannot break an operation. */ } };
|
|
123
|
+
function report(cause, context = {}) {
|
|
124
|
+
if (reported.has(cause)) return cause;
|
|
125
|
+
if (context.reuse && reportedConnectionError(cause)) return reportedConnectionError(cause);
|
|
126
|
+
const evidence = extractConnectionEvidence(cause);
|
|
127
|
+
const prior = evidence.chain.map(e => reported.get(e)).find(Boolean);
|
|
128
|
+
if (prior) return prior;
|
|
129
|
+
const described = describe?.(cause, context, evidence) ?? {};
|
|
130
|
+
const known = evidence.chain.find(e => Object.hasOwn(KNOWN_MESSAGES, e.code ?? ''));
|
|
131
|
+
let safe = described.publicError ?? context.publicError ?? {};
|
|
132
|
+
if (known && !safe.code?.startsWith('stream-')) safe = { ...safe, code: known.code, message: t(KNOWN_MESSAGES[known.code]) };
|
|
133
|
+
// These legacy handlers used exception messages as their public error. Keep only owned guidance.
|
|
134
|
+
if (['email', 'imessage', 'office'].includes(channel) && !known && context.untrustedPublicError) {
|
|
135
|
+
safe = { code: `${channel}-operation-failed`, message: t('操作失败,请查看原因和诊断详情。') };
|
|
136
|
+
}
|
|
137
|
+
const owned = evidence.chain.map(e => stages.get(e)).find(Boolean) ?? {};
|
|
138
|
+
const details = normalizeDiagnosticDetails({
|
|
139
|
+
...evidence.details, ...context, ...safe.details, ...described.details, ...owned,
|
|
140
|
+
channel, stage: owned.stage ?? described.details?.stage ?? safe.details?.stage ?? context.stage ?? operationStage(context.operation),
|
|
141
|
+
pluginVersion: packageInfo.version, nodeVersion: process.versions.node, platform: process.platform,
|
|
142
|
+
});
|
|
143
|
+
const code = safe.code ?? context.code ?? `${channel}-operation-failed`;
|
|
144
|
+
const hint = described.hint ?? safe.hint ?? safe.details?.hint ?? connectionHint(details);
|
|
145
|
+
const message = t(safe.message) ?? t('操作失败,请查看原因和诊断详情。');
|
|
146
|
+
const key = JSON.stringify([context.botId, context.attemptId, details.operation, details.stage, code, details.reason, details.reasons, details.httpStatus, details.providerCode, details.resource, details.file, details.field, details.issue]);
|
|
147
|
+
const previous = recent.get(key), time = now();
|
|
148
|
+
const wrap = publicError => {
|
|
149
|
+
const error = new Error(publicError.message, { cause });
|
|
150
|
+
error.code = publicError.code;
|
|
151
|
+
error.publicError = structuredClone(publicError);
|
|
152
|
+
reported.set(error, error);
|
|
153
|
+
if (cause && typeof cause === 'object') lastReport.set(cause, error);
|
|
154
|
+
return error;
|
|
155
|
+
};
|
|
156
|
+
if (context.automatic && previous && time - previous.time < 60_000) {
|
|
157
|
+
previous.retries += 1;
|
|
158
|
+
return wrap(previous.publicError);
|
|
159
|
+
}
|
|
160
|
+
if (previous?.retries) write('warn', { event: 'connection-retries', referenceId: previous.publicError.details.referenceId, retries: previous.retries });
|
|
161
|
+
details.referenceId = normalizeDiagnosticDetails({ referenceId: safe.referenceId }).referenceId ?? `${prefix}-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
|
162
|
+
details.occurredAt = new Date(time).toISOString();
|
|
163
|
+
details.hint = hint;
|
|
164
|
+
const publicError = { code, message, details };
|
|
165
|
+
if (context.automatic) {
|
|
166
|
+
if (recent.size >= 128) recent.delete(recent.keys().next().value);
|
|
167
|
+
recent.set(key, { time, publicError, retries: 0, botId: context.botId });
|
|
168
|
+
}
|
|
169
|
+
write(context.warning ? 'warn' : 'error', {
|
|
170
|
+
event: context.warning ? 'connection-warning' : 'connection-failure', code, ...details,
|
|
171
|
+
errors: evidence.errors,
|
|
172
|
+
...(normalizeDiagnosticDetails({ referenceId: context.parentReferenceId }).referenceId ? { parentReferenceId: context.parentReferenceId } : {}),
|
|
173
|
+
});
|
|
174
|
+
return wrap(publicError);
|
|
175
|
+
}
|
|
176
|
+
function clear(botId) {
|
|
177
|
+
for (const [key, entry] of recent) {
|
|
178
|
+
if (botId !== undefined && entry.botId !== botId) continue;
|
|
179
|
+
if (entry.retries) write('info', { event: 'connection-retries', referenceId: entry.publicError.details.referenceId, retries: entry.retries });
|
|
180
|
+
recent.delete(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function outcome(error, rollback) {
|
|
184
|
+
if (!reported.has(error)) return;
|
|
185
|
+
if (!normalizeDiagnosticDetails({ rollback }).rollback) return;
|
|
186
|
+
error.publicError.details.rollback = rollback;
|
|
187
|
+
error.publicError.details.hint = rollback === 'succeeded' ? t('操作未完成,已恢复之前的本机状态。请处理上述问题后重试。') : t('原账号状态尚未确认,请重新读取状态,并反馈诊断信息。');
|
|
188
|
+
write('info', { event: 'connection-outcome', referenceId: error.publicError.details.referenceId, rollback });
|
|
189
|
+
}
|
|
190
|
+
return { report, clear, outcome };
|
|
191
|
+
}
|
|
@@ -45,12 +45,14 @@ export async function sendRememberedConnectionTest({ state, send, text, channelL
|
|
|
45
45
|
return { sent: true };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function publicConnectionTestResult(error) {
|
|
48
|
+
export function publicConnectionTestResult(error, { diagnostics, botId } = {}) {
|
|
49
49
|
if (!error) return Object.freeze({ sent: true });
|
|
50
50
|
return Object.freeze({
|
|
51
51
|
sent: false,
|
|
52
52
|
code: error?.code === 'test-target-unavailable'
|
|
53
53
|
? 'test-target-unavailable'
|
|
54
54
|
: 'test-message-failed',
|
|
55
|
+
...(diagnostics && error?.code !== 'test-target-unavailable'
|
|
56
|
+
? { error: diagnostics.report(error, { operation: 'connection.test', stage: 'connection.test', botId }).publicError } : {}),
|
|
55
57
|
});
|
|
56
58
|
}
|
|
@@ -2,6 +2,14 @@ import { deferredStateAccess, normalizeDeferredState } from './deferred-state.mj
|
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Additional persisted keys a subclass may own. Kept intentionally small and
|
|
7
|
+
* explicit: a subclass reads and writes them through extensionState().
|
|
8
|
+
*/
|
|
9
|
+
const EXTENSION_KEYS = Object.freeze([
|
|
10
|
+
'emailBindings', 'mailCursor', 'pendingAuth', 'threadIds',
|
|
11
|
+
]);
|
|
12
|
+
|
|
5
13
|
const EMPTY_STATE = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], cursor: null });
|
|
6
14
|
|
|
7
15
|
function normalizeState(value) {
|
|
@@ -14,7 +22,7 @@ function normalizeState(value) {
|
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
|
-
|
|
25
|
+
const normalized = {
|
|
18
26
|
version: 1,
|
|
19
27
|
sessions,
|
|
20
28
|
...(value.deferred ? { deferred: normalizeDeferredState(value.deferred) } : {}),
|
|
@@ -23,6 +31,12 @@ function normalizeState(value) {
|
|
|
23
31
|
: [],
|
|
24
32
|
cursor: Number.isSafeInteger(value.cursor) && value.cursor >= 0 ? value.cursor : null,
|
|
25
33
|
};
|
|
34
|
+
// Channel-specific extensions survive a reload; each is normalized by the
|
|
35
|
+
// subclass that owns it (see extensionKeys()).
|
|
36
|
+
for (const key of EXTENSION_KEYS) {
|
|
37
|
+
if (Object.hasOwn(value, key)) normalized[key] = value[key];
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
export class ConversationStateStore {
|
|
@@ -83,6 +97,23 @@ export class ConversationStateStore {
|
|
|
83
97
|
await this.#persist();
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Take back a `markSeen` whose delivery never happened.
|
|
102
|
+
*
|
|
103
|
+
* `markSeen` means "this message was handled". A turn that failed before
|
|
104
|
+
* producing anything is not handled, so keeping the mark makes the id a
|
|
105
|
+
* permanent tombstone: every later poll short-circuits on `hasSeen` and the
|
|
106
|
+
* mail is never retried. The bridge calls this to separate "attempted" from
|
|
107
|
+
* "done". A message that already succeeded is never unmarked.
|
|
108
|
+
*/
|
|
109
|
+
async unmarkSeen(messageId) {
|
|
110
|
+
const index = this.#state.seenMessageIds.indexOf(messageId);
|
|
111
|
+
if (index === -1) return false;
|
|
112
|
+
this.#state.seenMessageIds.splice(index, 1);
|
|
113
|
+
await this.#persist();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
cursor() {
|
|
87
118
|
return this.#state.cursor;
|
|
88
119
|
}
|
|
@@ -97,6 +128,20 @@ export class ConversationStateStore {
|
|
|
97
128
|
return structuredClone(this.#state);
|
|
98
129
|
}
|
|
99
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Live view of the private state for subclasses that persist their own keys.
|
|
133
|
+
* Returns the real object (not a clone) so an extension can be written in
|
|
134
|
+
* place; call persist() afterwards.
|
|
135
|
+
*/
|
|
136
|
+
extensionState() {
|
|
137
|
+
return this.#state;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Flush the current state to disk after an extension write. */
|
|
141
|
+
persist() {
|
|
142
|
+
return this.#persist();
|
|
143
|
+
}
|
|
144
|
+
|
|
100
145
|
async remove() {
|
|
101
146
|
try {
|
|
102
147
|
await unlink(this.#path);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Shared by Host and Client: only explicitly admitted diagnostic facts cross the UI boundary.
|
|
2
|
+
const OPERATIONS = new Set([
|
|
3
|
+
'bot.bind-credentials', 'bot.bind-native', 'bot.bind-mailbox', 'bot.mailbox.update', 'bot.auth.start', 'bot.auth.poll',
|
|
4
|
+
'permissions.status', 'bot.session-binding.get', 'bot.session-binding.set', 'bot.session.list', 'connection.test', 'message.receive', 'message.send',
|
|
5
|
+
'connector.configure', 'connector.reconnect', 'connector.test', 'connector.remove', 'startup', 'connection.restore', 'connection.monitor', 'connection.close', 'connection.status',
|
|
6
|
+
'provision.begin', 'provision.poll', 'provision.verify', 'provision.cancel', 'bot.reconnect', 'bot.delete',
|
|
7
|
+
'bot.workspace.set', 'bot.model.set', 'bot.agent-preset.set', 'bot.context-enhancement.set', 'bot.access-policy.set', 'bot.alias.set',
|
|
8
|
+
]);
|
|
9
|
+
const STAGES = new Set([
|
|
10
|
+
'credential.verify', 'permission.check', 'sdk.load', 'operation', 'message.receive', 'message.send', 'connection.test', 'startup.load', 'qr.begin', 'qr.encode', 'qr.poll', 'qr.verify', 'qr.cancel', 'credential.read', 'credential.save', 'credential.remove',
|
|
11
|
+
'account.save', 'account.remove', 'state.load', 'state.write', 'state.cleanup', 'workspace.write', 'workspace.cleanup',
|
|
12
|
+
'runtime.prepare', 'activation', 'harness.check', 'connection.start', 'connection.poll', 'connection.stop', 'status.read', 'rollback', 'management.request',
|
|
13
|
+
]);
|
|
14
|
+
const REASONS = new Set([
|
|
15
|
+
'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ETIMEDOUT', 'EPIPE',
|
|
16
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
|
|
17
|
+
'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
18
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID', 'ERR_SSL_WRONG_VERSION_NUMBER',
|
|
19
|
+
'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'EROFS', 'ENOTDIR', 'EISDIR', 'EBUSY', 'EIO', 'EXDEV', 'EMFILE', 'ENFILE', 'EEXIST',
|
|
20
|
+
'unknown', 'multiple-causes', 'timeout', 'http-error', 'provider-rejected', 'invalid-response', 'ERR_PROXY_CONNECTION_FAILED', 'ERR_TLS_HANDSHAKE_TIMEOUT', 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', 'ERR_NETWORK_ACCESS_DENIED', 'AUTHENTICATIONFAILED', 'EAUTH', 'invalid-json', 'invalid-config', 'read-only',
|
|
21
|
+
]);
|
|
22
|
+
const RESOURCES = new Set(['credential-store', 'account-config', 'account-state', 'workspace-config', 'workspace-directory']);
|
|
23
|
+
const CONFIG_FILES = new Set(['config.json', 'workspaces.json']);
|
|
24
|
+
export const CONFIG_ISSUE_LABELS = Object.freeze({
|
|
25
|
+
'expected-object': '应为 JSON 对象。',
|
|
26
|
+
'expected-array': '应为 JSON 数组。',
|
|
27
|
+
'unsupported-version': '配置版本缺失或不受当前插件支持。',
|
|
28
|
+
'invalid-string': '字段缺失或不是非空字符串。',
|
|
29
|
+
'invalid-identifier': '标识符格式不符合要求。',
|
|
30
|
+
'identity-mismatch': '标识符与 accountId 派生结果不一致。',
|
|
31
|
+
'duplicate-identity': '账号标识重复。',
|
|
32
|
+
'invalid-api-url': '微信服务地址不是有效 URL。',
|
|
33
|
+
'untrusted-api-url': '微信服务地址必须使用受信任的 HTTPS 域名和端口。',
|
|
34
|
+
'invalid-workspace-path': '工作区路径必须是当前操作系统的绝对路径。',
|
|
35
|
+
'invalid-agent-preset': 'Agent Preset 标识无效。',
|
|
36
|
+
'invalid-model-selection': '模型设置须包含有效的 provider、model 和可选 reasoningEffort。',
|
|
37
|
+
'invalid-delivery-target': '投递目标的标识、结构或路由不符合当前配置版本要求。',
|
|
38
|
+
'unexpected-field': '当前配置版本不允许此字段。',
|
|
39
|
+
});
|
|
40
|
+
// Only schema-owned field names and numeric entry positions may leave the Host.
|
|
41
|
+
// Map keys are replaced with zero-based positions to avoid exposing identities.
|
|
42
|
+
const CONFIG_FIELD = /^(?:\$|version|accounts(?:\[\d{1,10}\](?:\.(?:accountId|ownerUserId|botId|tokenRef|baseUrl))?)?|(?:workspaces|agentPresets|models)(?:\[\d{1,10}\]\.(?:key|value))?|deliveryTargets(?:\[\d{1,10}\]\.(?:key|targets(?:\[\d{1,10}\])?))?)$/;
|
|
43
|
+
|
|
44
|
+
export function normalizeDiagnosticDetails(value) {
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES]]) {
|
|
48
|
+
if (allowed.has(value[field])) result[field] = value[field];
|
|
49
|
+
}
|
|
50
|
+
if (DIAGNOSTIC_CHANNELS.includes(value.channel)) result.channel = value.channel;
|
|
51
|
+
if (Array.isArray(value.reasons)) {
|
|
52
|
+
const reasons = [...new Set(value.reasons.filter(v => REASONS.has(v) && !['unknown', 'multiple-causes'].includes(v)))].slice(0, 16);
|
|
53
|
+
if (reasons.length) result.reasons = reasons;
|
|
54
|
+
}
|
|
55
|
+
for (const field of ['durationMs', 'timeoutMs']) if (Number.isFinite(value[field]) && value[field] >= 0) result[field] = Math.round(value[field]);
|
|
56
|
+
for (const field of ['nodeVersion', 'dshVersion']) if (typeof value[field] === 'string' && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value[field]) && value[field].length < 80) result[field] = value[field];
|
|
57
|
+
if (['darwin', 'linux', 'win32', 'freebsd'].includes(value.platform)) result.platform = value.platform;
|
|
58
|
+
if (['ilinkai.weixin.qq.com', 'oapi.dingtalk.com', 'api.dingtalk.com', 'api.telegram.org', 'discord.com', 'slack.com', 'open.feishu.cn', 'qyapi.weixin.qq.com'].includes(value.host)) result.host = value.host;
|
|
59
|
+
if (value.dependencies && typeof value.dependencies === 'object') {
|
|
60
|
+
const dependencies = {};
|
|
61
|
+
for (const name of ['dingtalkStream', 'axios', 'httpsProxyAgent', 'agentBase']) {
|
|
62
|
+
const version = value.dependencies[name];
|
|
63
|
+
if (typeof version === 'string' && /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version) && version.length <= 80) dependencies[name] = version;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(dependencies).length) result.dependencies = dependencies;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value.proxyConfigured === 'boolean') result.proxyConfigured = value.proxyConfigured;
|
|
68
|
+
if (value.truncated === true) result.truncated = true;
|
|
69
|
+
if (CONFIG_FILES.has(value.file)) result.file = value.file;
|
|
70
|
+
if (typeof value.field === 'string' && CONFIG_FIELD.test(value.field)) result.field = value.field;
|
|
71
|
+
if (typeof value.issue === 'string' && Object.hasOwn(CONFIG_ISSUE_LABELS, value.issue)) result.issue = value.issue;
|
|
72
|
+
if (/^(?:(?:WX|DT|IM)-CONN|MF)-[A-F0-9]{8}$/.test(value.referenceId ?? '')) result.referenceId = value.referenceId;
|
|
73
|
+
if (typeof value.occurredAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value.occurredAt)
|
|
74
|
+
&& Number.isFinite(Date.parse(value.occurredAt))) result.occurredAt = value.occurredAt;
|
|
75
|
+
if (Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599) result.httpStatus = value.httpStatus;
|
|
76
|
+
const provider = typeof value.providerCode === 'number' && Number.isSafeInteger(value.providerCode)
|
|
77
|
+
? String(value.providerCode) : value.providerCode;
|
|
78
|
+
if (typeof provider === 'string' && /^-?\d{1,12}$/.test(provider)) result.providerCode = provider;
|
|
79
|
+
if (typeof value.pluginVersion === 'string' && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.pluginVersion)
|
|
80
|
+
&& value.pluginVersion.length <= 64) result.pluginVersion = value.pluginVersion;
|
|
81
|
+
if (['succeeded', 'failed', 'not-attempted', 'unknown'].includes(value.rollback)) result.rollback = value.rollback;
|
|
82
|
+
if (typeof value.hint === 'string' && value.hint.trim()) result.hint = value.hint.trim().slice(0, 500);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const DIAGNOSTIC_CHANNELS = Object.freeze(['weixin', 'feishu', 'dingtalk', 'wecom', 'qq', 'slack', 'telegram', 'discord', 'whatsapp', 'wecom-app', 'imessage', 'email', 'office']);
|
|
87
|
+
|
|
88
|
+
export function diagnosticFields(value) {
|
|
89
|
+
if (!value?.details) return {};
|
|
90
|
+
const details = normalizeDiagnosticDetails({ ...value?.details,
|
|
91
|
+
referenceId: value?.details?.referenceId ?? value?.referenceId,
|
|
92
|
+
hint: value?.details?.hint ?? value?.hint,
|
|
93
|
+
});
|
|
94
|
+
return Object.keys(details).length ? { details } : {};
|
|
95
|
+
}
|