@xmanrui/dsh-im 4.22.0 → 4.23.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 +3 -3
- package/README.md +3 -3
- package/lib/client.js +2505 -1152
- package/lib/index.js +290 -299
- package/package.json +8 -1
- 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 +16 -3
- 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 +5 -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/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 +61 -0
- package/plugin-src/client/index.js +30 -2
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +6 -0
- 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 +5 -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/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/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- 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 +692 -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 +593 -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 +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- 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/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 +63 -18
- 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 +90 -11
- 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/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- 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
|
+
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
imageFileSourcesFromContent,
|
|
16
16
|
isModelImageRejection,
|
|
17
17
|
} from './image-prompt.mjs';
|
|
18
|
+
import { imageInputLimits } from './image-input-policy.mjs';
|
|
19
|
+
import { inboundImagesAsFiles, imageContentFromStaged, isImageAdmissionRejection, IMAGE_HOST_LIMIT_FALLBACK_PROMPT } from './image-input.mjs';
|
|
18
20
|
import { imSourceGuidance } from './im-source-guidance.mjs';
|
|
19
21
|
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
|
20
22
|
import { t } from './i18n.mjs';
|
|
@@ -706,7 +708,6 @@ function harnessTurnError(reason) {
|
|
|
706
708
|
return new HarnessTurnError('turn-interrupted', { reason });
|
|
707
709
|
}
|
|
708
710
|
if (kind === 'aborted') return new HarnessTurnError('turn-aborted', { reason });
|
|
709
|
-
if (kind === 'completed') return new HarnessTurnError('model-empty-response', { reason });
|
|
710
711
|
return new HarnessTurnError('harness-turn-failed', { reason });
|
|
711
712
|
}
|
|
712
713
|
|
|
@@ -732,6 +733,7 @@ export class HarnessClient {
|
|
|
732
733
|
#controlExecutor;
|
|
733
734
|
#sessionMaintenanceExecutor;
|
|
734
735
|
#fileIngressExecutor;
|
|
736
|
+
#imageInputPolicy;
|
|
735
737
|
#managedProcess = null;
|
|
736
738
|
#interactionRegistry;
|
|
737
739
|
#interactionOwnerships;
|
|
@@ -756,6 +758,7 @@ export class HarnessClient {
|
|
|
756
758
|
controlExecutor,
|
|
757
759
|
sessionMaintenanceExecutor,
|
|
758
760
|
fileIngressExecutor,
|
|
761
|
+
imageInputPolicy = () => undefined,
|
|
759
762
|
}) {
|
|
760
763
|
if (typeof createWebSocket !== 'function') {
|
|
761
764
|
throw new TypeError('createWebSocket must be a function');
|
|
@@ -807,6 +810,7 @@ export class HarnessClient {
|
|
|
807
810
|
this.#controlExecutor = controlExecutor;
|
|
808
811
|
this.#sessionMaintenanceExecutor = sessionMaintenanceExecutor;
|
|
809
812
|
this.#fileIngressExecutor = fileIngressExecutor;
|
|
813
|
+
this.#imageInputPolicy = imageInputPolicy;
|
|
810
814
|
this.#interactionRegistry = interactionRegistry(this.#baseUrl?.origin ?? interactionScope);
|
|
811
815
|
this.#interactionOwnerships = this.#interactionRegistry.ownerships;
|
|
812
816
|
this.#interactionClaims = this.#interactionRegistry.claims;
|
|
@@ -1436,6 +1440,10 @@ export class HarnessClient {
|
|
|
1436
1440
|
});
|
|
1437
1441
|
}
|
|
1438
1442
|
|
|
1443
|
+
async getImageInputLimits() {
|
|
1444
|
+
return imageInputLimits(await this.#imageInputPolicy());
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1439
1447
|
async ask(sessionId, prompt, options = {}) {
|
|
1440
1448
|
if (typeof options === 'number') options = { timeoutMs: options };
|
|
1441
1449
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
@@ -1451,6 +1459,7 @@ export class HarnessClient {
|
|
|
1451
1459
|
: undefined;
|
|
1452
1460
|
const control = normalizeControl(options.control);
|
|
1453
1461
|
const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
|
1462
|
+
const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
|
|
1454
1463
|
await this.ensureRunning({ signal });
|
|
1455
1464
|
const before = await this.rpc(
|
|
1456
1465
|
'session.history',
|
|
@@ -1493,6 +1502,7 @@ export class HarnessClient {
|
|
|
1493
1502
|
let interactionTask = null;
|
|
1494
1503
|
let artifactsDelivered = false;
|
|
1495
1504
|
let deliveredArtifactCount = 0;
|
|
1505
|
+
let artifactHandoffError = null;
|
|
1496
1506
|
const stagedBatches = [];
|
|
1497
1507
|
let promptAccepted = false;
|
|
1498
1508
|
let turnFinished = false;
|
|
@@ -1508,6 +1518,7 @@ export class HarnessClient {
|
|
|
1508
1518
|
await onArtifact(artifact);
|
|
1509
1519
|
deliveredArtifactCount += 1;
|
|
1510
1520
|
} catch (error) {
|
|
1521
|
+
artifactHandoffError ??= error;
|
|
1511
1522
|
outboundArtifactRegistry.release(artifact);
|
|
1512
1523
|
console.warn('[dsh-im] ignored an artifact handoff failure:', this.#logPrefix, error.message);
|
|
1513
1524
|
}
|
|
@@ -1530,6 +1541,26 @@ export class HarnessClient {
|
|
|
1530
1541
|
stagedBatches.push(staged);
|
|
1531
1542
|
prompt = appendInboundFilesToPrompt(prompt, staged);
|
|
1532
1543
|
}
|
|
1544
|
+
if (inboundImages.length > 0) {
|
|
1545
|
+
const limits = await this.getImageInputLimits();
|
|
1546
|
+
const sources = inboundImagesAsFiles(inboundImages, limits);
|
|
1547
|
+
let staged;
|
|
1548
|
+
try {
|
|
1549
|
+
staged = await this.#stageWorkspaceFiles(sessionId, sources, signal);
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
// Ingress wraps loader errors; retain the original image diagnostic.
|
|
1552
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
1553
|
+
if (error?.cause?.name === 'ImagePromptError') throw error.cause;
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
stagedBatches.push(staged);
|
|
1557
|
+
const images = await imageContentFromStaged(staged, limits, { signal });
|
|
1558
|
+
const originalContent = typeof basePrompt === 'string'
|
|
1559
|
+
? [{ type: 'text', text: basePrompt }] : basePrompt;
|
|
1560
|
+
prompt = appendInboundFilesToPrompt([...originalContent, ...images], {
|
|
1561
|
+
files: stagedBatches.flatMap((batch) => batch.files),
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1533
1564
|
if (interactionSignal) {
|
|
1534
1565
|
let markOpen;
|
|
1535
1566
|
const opened = new Promise((resolve) => { markOpen = resolve; });
|
|
@@ -1577,29 +1608,32 @@ export class HarnessClient {
|
|
|
1577
1608
|
// Session workspace and named in a text manifest — then retry once
|
|
1578
1609
|
// with a text-only prompt. The retry reuses promptRpcId so reply
|
|
1579
1610
|
// tracking, control and interaction ownership stay bound to this ask.
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
stagedImages
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1611
|
+
const originalsStaged = inboundImages.length > 0 && isImageAdmissionRejection(error)
|
|
1612
|
+
&& content.some((part) => part.type === 'image');
|
|
1613
|
+
const imageSources = !originalsStaged && isModelImageRejection(error)
|
|
1614
|
+
? imageFileSourcesFromContent(content) : [];
|
|
1615
|
+
if (!originalsStaged && imageSources.length === 0) throw error;
|
|
1616
|
+
if (!originalsStaged) {
|
|
1617
|
+
let stagedImages;
|
|
1618
|
+
try {
|
|
1619
|
+
stagedImages = await this.#stageWorkspaceFiles(sessionId, imageSources, signal);
|
|
1620
|
+
} catch (stagingError) {
|
|
1621
|
+
if (signal?.aborted) throw signal.reason ?? stagingError;
|
|
1622
|
+
console.warn(
|
|
1623
|
+
'[dsh-im] unable to restage rejected images as workspace files:',
|
|
1624
|
+
this.#logPrefix,
|
|
1625
|
+
stagingError?.message ?? String(stagingError),
|
|
1626
|
+
);
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
stagedBatches.push(stagedImages);
|
|
1595
1630
|
}
|
|
1596
|
-
stagedBatches.push(stagedImages);
|
|
1597
1631
|
const baseContent = typeof basePrompt === 'string'
|
|
1598
1632
|
? [{ type: 'text', text: basePrompt }]
|
|
1599
1633
|
: basePrompt;
|
|
1600
1634
|
const fallbackPrompt = appendInboundFilesToPrompt([
|
|
1601
1635
|
...contentWithoutImages(baseContent),
|
|
1602
|
-
{ type: 'text', text: t(IMAGE_FILE_FALLBACK_PROMPT) },
|
|
1636
|
+
{ type: 'text', text: t(isModelImageRejection(error) ? IMAGE_FILE_FALLBACK_PROMPT : IMAGE_HOST_LIMIT_FALLBACK_PROMPT) },
|
|
1603
1637
|
], { files: stagedBatches.flatMap((batch) => batch?.files ?? []) });
|
|
1604
1638
|
await sendPrompt(fallbackPrompt);
|
|
1605
1639
|
}
|
|
@@ -1659,6 +1693,12 @@ export class HarnessClient {
|
|
|
1659
1693
|
}
|
|
1660
1694
|
if (artifactCount > 0) return '';
|
|
1661
1695
|
if (ownership?.stopRequested) throw turnStoppedError();
|
|
1696
|
+
if (artifactHandoffError) throw artifactHandoffError;
|
|
1697
|
+
// A completed turn can do all its work through tools without text.
|
|
1698
|
+
// Missing end reasons retain the existing empty-reply failure.
|
|
1699
|
+
if (tracker.reason != null && harnessTurnSucceeded(tracker.reason)) {
|
|
1700
|
+
return t('本轮处理已结束,没有文本回复。');
|
|
1701
|
+
}
|
|
1662
1702
|
throw harnessTurnError(tracker.reason);
|
|
1663
1703
|
}
|
|
1664
1704
|
|
|
@@ -1681,6 +1721,11 @@ export class HarnessClient {
|
|
|
1681
1721
|
throw timeoutError;
|
|
1682
1722
|
}
|
|
1683
1723
|
} catch (error) {
|
|
1724
|
+
if (error instanceof HarnessTurnError) {
|
|
1725
|
+
error.details ??= {
|
|
1726
|
+
sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1684
1729
|
// Once cancellation was accepted, transport/poll failures and timeouts
|
|
1685
1730
|
// describe the convergence of that stop, not an unrelated ask failure.
|
|
1686
1731
|
if (!ownership?.stopRequested) throw error;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
"DSH 版本": "DSH version",
|
|
3
|
+
"依赖版本": "Dependency versions",
|
|
4
|
+
"代理已配置": "Proxy configured",
|
|
5
|
+
"原因链已截断": "Cause chain truncated",
|
|
6
|
+
"请在 macOS 系统设置中授予 DSH 完全磁盘访问权限,以读取 Messages 数据库。": "Grant DSH Full Disk Access in macOS System Settings to read the Messages database.",
|
|
7
|
+
"请在 macOS 系统设置中允许 DSH 自动控制 Messages。": "Allow DSH to control Messages in macOS System Settings.",
|
|
8
|
+
"先在 macOS 系统设置中授予 Messages 权限。": "Grant Messages permissions in macOS System Settings first.",
|
|
9
|
+
|
|
10
|
+
"加载渠道配置": "Load channel configuration",
|
|
11
|
+
"激活账号": "Activate account",
|
|
12
|
+
"启动连接": "Start connection",
|
|
13
|
+
"监测连接与消息": "Monitor connection and messages",
|
|
14
|
+
"停止连接": "Stop connection",
|
|
15
|
+
"服务返回码": "Provider code",
|
|
16
|
+
"验证凭据": "Verify credentials",
|
|
17
|
+
"检查权限": "Check permissions",
|
|
18
|
+
"加载渠道组件": "Load channel component",
|
|
19
|
+
"执行渠道操作": "Perform channel operation",
|
|
20
|
+
"接收消息": "Receive message",
|
|
21
|
+
"发送消息": "Send message",
|
|
22
|
+
"发送测试消息": "Send test message",
|
|
23
|
+
"暂未识别": "Unknown",
|
|
24
|
+
"操作已取消。": "The operation was cancelled.",
|
|
25
|
+
"本地会话状态清理失败,请查看诊断详情。": "Local session state cleanup failed. See the diagnostic details.",
|
|
26
|
+
"账号已移除,但本地状态清理失败。": "The account was removed, but local state cleanup failed.",
|
|
27
|
+
"账号已移除,但登录凭据清理失败。": "The account was removed, but credential cleanup failed.",
|
|
28
|
+
"连接已移除,但登录凭据清理失败。": "The connection was removed, but credential cleanup failed.",
|
|
29
|
+
|
|
30
|
+
"操作失败,请查看原因和诊断详情。": "The operation failed. See the cause and diagnostic details.",
|
|
31
|
+
"检测到多个底层原因,请展开诊断详情逐项检查。": "Multiple underlying causes were detected. Expand the diagnostic details to inspect them.",
|
|
32
|
+
"服务域名解析失败,请检查运行 DSH 的机器的 DNS 和网络设置。": "Service DNS resolution failed. Check DNS and network settings on the machine running DSH.",
|
|
33
|
+
"目标拒绝连接,请检查服务地址、端口和访问限制。": "The connection was refused. Check the service address, port, and access restrictions.",
|
|
34
|
+
"连接已中断,请检查网络和目标服务状态。": "The connection was interrupted. Check the network and target service.",
|
|
35
|
+
"目标网络不可达,请检查运行 DSH 的机器的网络出口和访问限制。": "The target network is unreachable. Check network routing and access restrictions on the machine running DSH.",
|
|
36
|
+
"TLS 或证书校验失败,请检查系统时间、证书链和网络设置。": "TLS or certificate validation failed. Check the system clock, certificate chain, and network settings.",
|
|
37
|
+
"请求超时,请检查目标服务和网络;诊断详情中可查看已知的耗时。": "The request timed out. Check the service and network; available timings are in the diagnostic details.",
|
|
38
|
+
"代理连接或认证失败,请检查 DSH 进程使用的代理配置。": "Proxy connection or authentication failed. Check the proxy configuration used by the DSH process.",
|
|
39
|
+
"服务认证失败,请检查账号和授权信息。": "Service authentication failed. Check the account and credentials.",
|
|
40
|
+
"操作权限不足,请检查对应资源的访问权限。": "Permission was denied. Check access to the affected resource.",
|
|
41
|
+
"所需文件或本地工具不存在,请检查安装和配置。": "A required file or local tool is missing. Check installation and configuration.",
|
|
42
|
+
"服务正在限流,请稍后重试。": "The service is rate limiting requests. Try again later.",
|
|
43
|
+
"服务拒绝了请求,请检查凭据、权限或访问限制;HTTP 状态本身不能确认登录失效。": "The service rejected the request. Check credentials, permissions, or access restrictions; the HTTP status alone does not prove that the login expired.",
|
|
44
|
+
"目标服务暂时异常,请稍后重试。": "The target service is temporarily unavailable. Try again later.",
|
|
45
|
+
"目标服务返回了 HTTP 错误,请查看诊断详情。": "The target service returned an HTTP error. See the diagnostic details.",
|
|
46
|
+
"数据格式无效,请按失败阶段检查服务响应或本地配置。": "The data format is invalid. Check the service response or local configuration indicated by the failing stage.",
|
|
47
|
+
"暂未识别具体原因,请复制诊断信息,并查看相同参考号的 DSH 日志。": "The underlying cause is unknown. Copy the diagnostics and find the matching reference in the DSH logs.",
|
|
48
|
+
"多个原因": "Multiple causes",
|
|
49
|
+
"服务域名": "Service host",
|
|
50
|
+
"请求耗时(毫秒)": "Request duration (ms)",
|
|
51
|
+
"超时阈值(毫秒)": "Timeout (ms)",
|
|
52
|
+
"Node 版本": "Node version",
|
|
53
|
+
"运行平台": "Platform",
|
|
54
|
+
"无法访问 DSH 管理接口,请检查 DSH 连接或重新登录后重试。": "Cannot access the DSH management API. Check the DSH connection or sign in again."
|
|
55
|
+
});
|