@xmanrui/dsh-im 4.34.0 → 4.34.2
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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +323 -204
- package/lib/index.js +297 -297
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +18 -3
- package/plugin-src/client/bot-settings-tabs.js +114 -0
- package/plugin-src/client/delivery-settings.js +2 -16
- package/plugin-src/client/i18n.js +7 -0
- package/plugin-src/client/styles.js +8 -3
- package/plugin-src/host/channels/feishu/production.mjs +13 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -1
- package/plugin-src/host/channels/shared/access-policy-production.mjs +14 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/modern-harness-api.mjs +2 -1
- package/src/channels/discord/config-store.mjs +21 -1
- package/src/channels/discord/discord-api.mjs +32 -0
- package/src/channels/discord/discord-controller.mjs +22 -1
- package/src/channels/feishu/bridge.mjs +40 -14
- package/src/channels/feishu/feishu-channel.mjs +11 -3
- package/src/channels/feishu/feishu-runtime.mjs +10 -2
- package/src/channels/shared/bot-workspace-store.mjs +4 -0
- package/src/channels/shared/connection-error.mjs +10 -4
- package/src/channels/shared/i18n-en/feishu.mjs +2 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +4 -0
- package/src/channels/shared/message-failure.mjs +26 -4
- package/src/channels/shared/text-harness-bridge.mjs +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import QRCode from 'qrcode';
|
|
4
|
+
import { waitForFeishuOperation } from './feishu-channel.mjs';
|
|
4
5
|
import {
|
|
5
6
|
conversationKey,
|
|
6
7
|
conversationScope,
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
import {
|
|
78
79
|
channelDeliveryFailure,
|
|
79
80
|
clearLastMessageFailure,
|
|
81
|
+
messageFailureDiagnostic,
|
|
80
82
|
messageFailureText,
|
|
81
83
|
setLastMessageFailure,
|
|
82
84
|
} from '../shared/message-failure.mjs';
|
|
@@ -720,6 +722,7 @@ export class FeishuHarnessBridge {
|
|
|
720
722
|
/** Conversation keys with an IM ask in flight (set BEFORE the turn starts). */
|
|
721
723
|
#imTurnKeys = new Set();
|
|
722
724
|
#cardDataTimeoutMs;
|
|
725
|
+
#requestTimeoutMs;
|
|
723
726
|
/** When true, approval/question interactions render as Feishu cards (buttons). */
|
|
724
727
|
#interactionCards = true;
|
|
725
728
|
|
|
@@ -744,6 +747,7 @@ export class FeishuHarnessBridge {
|
|
|
744
747
|
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
|
745
748
|
repairLinkWaitMs = REPAIR_LINK_WAIT_MS,
|
|
746
749
|
cardDataTimeoutMs = CARD_DATA_TIMEOUT_MS,
|
|
750
|
+
requestTimeoutMs = 15_000,
|
|
747
751
|
replyTimeoutMs = 600_000,
|
|
748
752
|
interactionCards = true,
|
|
749
753
|
sessionSyncTargetsFor = null,
|
|
@@ -766,7 +770,8 @@ export class FeishuHarnessBridge {
|
|
|
766
770
|
}
|
|
767
771
|
if (!Number.isFinite(repairPollIntervalMs) || repairPollIntervalMs <= 0
|
|
768
772
|
|| !Number.isFinite(repairLinkWaitMs) || repairLinkWaitMs <= 0
|
|
769
|
-
|| !Number.isFinite(cardDataTimeoutMs) || cardDataTimeoutMs <= 0
|
|
773
|
+
|| !Number.isFinite(cardDataTimeoutMs) || cardDataTimeoutMs <= 0
|
|
774
|
+
|| !Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
|
|
770
775
|
throw new TypeError('Feishu timing values must be positive numbers');
|
|
771
776
|
}
|
|
772
777
|
if (stepPushClock !== null
|
|
@@ -803,6 +808,7 @@ export class FeishuHarnessBridge {
|
|
|
803
808
|
this.#repairPollIntervalMs = repairPollIntervalMs;
|
|
804
809
|
this.#repairLinkWaitMs = repairLinkWaitMs;
|
|
805
810
|
this.#cardDataTimeoutMs = cardDataTimeoutMs;
|
|
811
|
+
this.#requestTimeoutMs = requestTimeoutMs;
|
|
806
812
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
807
813
|
this.#interactionCards = interactionCards === true;
|
|
808
814
|
this.#sessionSyncTargetsFor = typeof sessionSyncTargetsFor === 'function'
|
|
@@ -1411,11 +1417,20 @@ export class FeishuHarnessBridge {
|
|
|
1411
1417
|
return current;
|
|
1412
1418
|
}
|
|
1413
1419
|
|
|
1420
|
+
#logAccessDenied(conversationType, reason) {
|
|
1421
|
+
this.#logger.info?.('[dsh-feishu] inbound access denied', {
|
|
1422
|
+
botId: this.#accessPolicy?.botId,
|
|
1423
|
+
conversationType,
|
|
1424
|
+
reason,
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1414
1428
|
#finishAccessDecision(event, messageId, access) {
|
|
1415
1429
|
let current;
|
|
1416
1430
|
current = Promise.resolve().then(async () => {
|
|
1417
1431
|
if (this.#state.hasSeen(messageId)) return;
|
|
1418
1432
|
await this.#state.markSeen(messageId);
|
|
1433
|
+
this.#logAccessDenied(event.message.chat_type === 'p2p' ? 'direct' : 'group', access.reason);
|
|
1419
1434
|
if (access.reason === 'command-not-allowed') {
|
|
1420
1435
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
1421
1436
|
this.#status.messagesReceived += 1;
|
|
@@ -1484,7 +1499,7 @@ export class FeishuHarnessBridge {
|
|
|
1484
1499
|
const failure = setLastMessageFailure(this.#status, error, { userMessage, reason });
|
|
1485
1500
|
this.#logger?.[logLevel]?.(
|
|
1486
1501
|
`[dsh-feishu] ${logLabel} failed [${failure.referenceId}]:`,
|
|
1487
|
-
error
|
|
1502
|
+
messageFailureDiagnostic(error, failure),
|
|
1488
1503
|
);
|
|
1489
1504
|
return failure;
|
|
1490
1505
|
}
|
|
@@ -1659,6 +1674,7 @@ export class FeishuHarnessBridge {
|
|
|
1659
1674
|
|| (!hasImages && !hasFiles && NUMBER_REPLY.test(text) && this.#menus.has(key)),
|
|
1660
1675
|
});
|
|
1661
1676
|
if (!transcribedAccess.allowed) {
|
|
1677
|
+
this.#logAccessDenied(event.message.chat_type === 'p2p' ? 'direct' : 'group', transcribedAccess.reason);
|
|
1662
1678
|
if (transcribedAccess.reason === 'command-not-allowed') {
|
|
1663
1679
|
await this.#send(
|
|
1664
1680
|
event.message.chat_id,
|
|
@@ -2310,11 +2326,11 @@ export class FeishuHarnessBridge {
|
|
|
2310
2326
|
isCommand: !isInteractionResponse,
|
|
2311
2327
|
});
|
|
2312
2328
|
if (!access.allowed) {
|
|
2329
|
+
this.#logAccessDenied(conversationType, access.reason);
|
|
2313
2330
|
if (access.reason === 'command-not-allowed') {
|
|
2314
2331
|
return this.#send(route.chatId, t(COMMAND_PERMISSION_DENIED_MESSAGE))
|
|
2315
2332
|
.catch(() => undefined);
|
|
2316
2333
|
}
|
|
2317
|
-
this.#logger.warn?.('[dsh-feishu] ignoring card action blocked by access policy');
|
|
2318
2334
|
return Promise.resolve();
|
|
2319
2335
|
}
|
|
2320
2336
|
// A used card is recent even if it was first created long ago.
|
|
@@ -3117,6 +3133,14 @@ export class FeishuHarnessBridge {
|
|
|
3117
3133
|
}
|
|
3118
3134
|
}
|
|
3119
3135
|
|
|
3136
|
+
#cardRequest(stage, operation) {
|
|
3137
|
+
return waitForFeishuOperation(operation, {
|
|
3138
|
+
signal: this.#signal,
|
|
3139
|
+
timeoutMs: this.#requestTimeoutMs,
|
|
3140
|
+
stage: `${stage} (${this.#requestTimeoutMs}ms)`,
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3120
3144
|
async #sendCard(chatId, cardJson, options = {}) {
|
|
3121
3145
|
const updateMessageId = nonEmptyString(options.updateMessageId);
|
|
3122
3146
|
const replyTo = nonEmptyString(options.replyTo);
|
|
@@ -3124,14 +3148,14 @@ export class FeishuHarnessBridge {
|
|
|
3124
3148
|
// Session-sync cards target the user's openId (the synced DM is a user,
|
|
3125
3149
|
// not a chat), delivered fresh without topic/thread handling.
|
|
3126
3150
|
if (options.receiveIdType === 'open_id') {
|
|
3127
|
-
const response = await this.#client.im.v1.message.create({
|
|
3151
|
+
const response = await this.#cardRequest('card create', () => this.#client.im.v1.message.create({
|
|
3128
3152
|
params: { receive_id_type: 'open_id' },
|
|
3129
3153
|
data: {
|
|
3130
3154
|
receive_id: chatId,
|
|
3131
3155
|
msg_type: 'interactive',
|
|
3132
3156
|
content: cardJson,
|
|
3133
3157
|
},
|
|
3134
|
-
});
|
|
3158
|
+
}));
|
|
3135
3159
|
if (response?.code && response.code !== 0) {
|
|
3136
3160
|
throw new Error(`Feishu card send failed: ${response.msg || response.code}`);
|
|
3137
3161
|
}
|
|
@@ -3142,10 +3166,10 @@ export class FeishuHarnessBridge {
|
|
|
3142
3166
|
|
|
3143
3167
|
if (updateMessageId) {
|
|
3144
3168
|
try {
|
|
3145
|
-
const response = await this.#client.im.v1.message.patch({
|
|
3169
|
+
const response = await this.#cardRequest('card patch', () => this.#client.im.v1.message.patch({
|
|
3146
3170
|
path: { message_id: updateMessageId },
|
|
3147
3171
|
data: { content: cardJson },
|
|
3148
|
-
});
|
|
3172
|
+
}));
|
|
3149
3173
|
if (response?.code && response.code !== 0) {
|
|
3150
3174
|
throw new Error(`Feishu card update failed: ${response.msg || response.code}`);
|
|
3151
3175
|
}
|
|
@@ -3162,14 +3186,14 @@ export class FeishuHarnessBridge {
|
|
|
3162
3186
|
const content = cardJson;
|
|
3163
3187
|
if (replyTo) {
|
|
3164
3188
|
try {
|
|
3165
|
-
const response = await this.#client.im.v1.message.reply({
|
|
3189
|
+
const response = await this.#cardRequest('card reply', () => this.#client.im.v1.message.reply({
|
|
3166
3190
|
path: { message_id: replyTo },
|
|
3167
3191
|
data: {
|
|
3168
3192
|
msg_type: 'interactive',
|
|
3169
3193
|
content,
|
|
3170
3194
|
...(this.#replyInThreadFor(replyTo) ? { reply_in_thread: true } : {}),
|
|
3171
3195
|
},
|
|
3172
|
-
});
|
|
3196
|
+
}));
|
|
3173
3197
|
if (response?.code && response.code !== 0) {
|
|
3174
3198
|
throw new Error(`Feishu card reply failed: ${response.msg || response.code}`);
|
|
3175
3199
|
}
|
|
@@ -3185,10 +3209,10 @@ export class FeishuHarnessBridge {
|
|
|
3185
3209
|
this.#forgetTopicRoot(replyTo);
|
|
3186
3210
|
}
|
|
3187
3211
|
}
|
|
3188
|
-
const response = await this.#client.im.v1.message.create({
|
|
3212
|
+
const response = await this.#cardRequest('card create', () => this.#client.im.v1.message.create({
|
|
3189
3213
|
params: { receive_id_type: 'chat_id' },
|
|
3190
3214
|
data: { receive_id: chatId, msg_type: 'interactive', content: cardJson },
|
|
3191
|
-
});
|
|
3215
|
+
}));
|
|
3192
3216
|
if (response?.code && response.code !== 0) {
|
|
3193
3217
|
throw new Error(`Feishu card send failed: ${response.msg || response.code}`);
|
|
3194
3218
|
}
|
|
@@ -4824,7 +4848,8 @@ export class FeishuHarnessBridge {
|
|
|
4824
4848
|
);
|
|
4825
4849
|
if (card.deliveryViaOpenId) {
|
|
4826
4850
|
this.#logger.warn?.('[dsh-feishu] session-sync mirror card render failed:',
|
|
4827
|
-
error?.message ?? String(error)
|
|
4851
|
+
error?.message ?? String(error),
|
|
4852
|
+
{ sessionId: card.sessionSyncSessionId, turn: card.sessionSyncTurn });
|
|
4828
4853
|
}
|
|
4829
4854
|
}
|
|
4830
4855
|
}
|
|
@@ -4875,6 +4900,7 @@ export class FeishuHarnessBridge {
|
|
|
4875
4900
|
this.#logger.warn?.(
|
|
4876
4901
|
'[dsh-feishu] step streaming card finish failed:',
|
|
4877
4902
|
error?.message ?? String(error),
|
|
4903
|
+
{ sessionId: card.sessionSyncSessionId, turn: card.sessionSyncTurn },
|
|
4878
4904
|
);
|
|
4879
4905
|
return null;
|
|
4880
4906
|
}
|
|
@@ -4925,10 +4951,10 @@ export class FeishuHarnessBridge {
|
|
|
4925
4951
|
}
|
|
4926
4952
|
|
|
4927
4953
|
async #patchStepCard(messageId, cardJson) {
|
|
4928
|
-
const response = await this.#client.im.v1.message.patch({
|
|
4954
|
+
const response = await this.#cardRequest('step card patch', () => this.#client.im.v1.message.patch({
|
|
4929
4955
|
path: { message_id: messageId },
|
|
4930
4956
|
data: { content: cardJson },
|
|
4931
|
-
});
|
|
4957
|
+
}));
|
|
4932
4958
|
if (response?.code && response.code !== 0) {
|
|
4933
4959
|
throw new Error(`Feishu step card patch failed: ${response.msg || response.code}`);
|
|
4934
4960
|
}
|
|
@@ -80,12 +80,13 @@ function abortReason(signal) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function operationTimeout(stage) {
|
|
83
|
-
const error = new Error(`Feishu
|
|
83
|
+
const error = new Error(`Feishu ${stage} timed out.`);
|
|
84
84
|
error.code = 'provider-timeout';
|
|
85
85
|
return error;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Bound the caller's wait even when the SDK does not honor cancellation.
|
|
89
|
+
export function waitForFeishuOperation(operation, { signal, timeoutMs, stage = 'request' }) {
|
|
89
90
|
signal?.throwIfAborted();
|
|
90
91
|
const deadline = new AbortController();
|
|
91
92
|
const operationSignal = signal
|
|
@@ -108,7 +109,10 @@ function waitForFileOperation(operation, { signal, timeoutMs, stage }) {
|
|
|
108
109
|
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
109
110
|
operationSignal.addEventListener('abort', onAbort, { once: true });
|
|
110
111
|
|
|
111
|
-
Promise.resolve().then(() =>
|
|
112
|
+
Promise.resolve().then(() => {
|
|
113
|
+
operationSignal.throwIfAborted();
|
|
114
|
+
return operation(operationSignal);
|
|
115
|
+
}).then(
|
|
112
116
|
(value) => finish(resolve, value),
|
|
113
117
|
(error) => finish(reject, error),
|
|
114
118
|
);
|
|
@@ -116,6 +120,10 @@ function waitForFileOperation(operation, { signal, timeoutMs, stage }) {
|
|
|
116
120
|
});
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
function waitForFileOperation(operation, { stage, ...options }) {
|
|
124
|
+
return waitForFeishuOperation(operation, { ...options, stage: `file ${stage}` });
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function deliveryUuid(file, chatId, messageType) {
|
|
120
128
|
const seed = `${file.deliveryKey}\u0000${chatId}`;
|
|
121
129
|
const digest = createHash('sha256')
|
|
@@ -2,7 +2,7 @@ import { createConnectionDiagnostics, atConnectionStage } from '../shared/connec
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { FeishuHarnessBridge } from './bridge.mjs';
|
|
4
4
|
import { cardActionProbeCard } from './feishu-cards.mjs';
|
|
5
|
-
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
|
5
|
+
import { VerifiedFeishuChannel, waitForFeishuOperation } from './feishu-channel.mjs';
|
|
6
6
|
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
|
7
7
|
import { normalizeFeishuStepPushMode } from './step-push-mode.mjs';
|
|
8
8
|
import { createVoice } from './voice.mjs';
|
|
@@ -367,6 +367,7 @@ export class FeishuRuntime {
|
|
|
367
367
|
voice: this.#voice,
|
|
368
368
|
sessionSyncTargetsFor: this.#sessionSyncTargetsFor,
|
|
369
369
|
repair: this.#repair,
|
|
370
|
+
requestTimeoutMs: this.#requestTimeoutMs,
|
|
370
371
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
371
372
|
// Interaction cards (approval/question buttons) are on by default.
|
|
372
373
|
// Set DSH_IM_INTERACTION_CARDS=0 to fall back to plain-text replies.
|
|
@@ -703,13 +704,20 @@ export class FeishuRuntime {
|
|
|
703
704
|
const content = format === 'markdown'
|
|
704
705
|
? { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text }] } }
|
|
705
706
|
: { text };
|
|
706
|
-
const
|
|
707
|
+
const activeSignal = signal
|
|
708
|
+
? AbortSignal.any([signal, this.#abortController.signal])
|
|
709
|
+
: this.#abortController.signal;
|
|
710
|
+
const response = await waitForFeishuOperation(() => this.#client.im.v1.message.create({
|
|
707
711
|
params: { receive_id_type: receiveIdType },
|
|
708
712
|
data: {
|
|
709
713
|
receive_id: receiveId,
|
|
710
714
|
msg_type: format === 'markdown' ? 'interactive' : 'text',
|
|
711
715
|
content: JSON.stringify(content),
|
|
712
716
|
},
|
|
717
|
+
}), {
|
|
718
|
+
signal: activeSignal,
|
|
719
|
+
timeoutMs: this.#requestTimeoutMs,
|
|
720
|
+
stage: `proactive text send (${this.#requestTimeoutMs}ms)`,
|
|
713
721
|
});
|
|
714
722
|
if (response?.code && response.code !== 0) {
|
|
715
723
|
const error = new Error(`Feishu proactive delivery failed: ${response.msg || response.code}`);
|
|
@@ -2136,6 +2136,7 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2136
2136
|
stateFor,
|
|
2137
2137
|
agentPresetCatalog,
|
|
2138
2138
|
modelCatalog,
|
|
2139
|
+
validateAccessPolicyForBot,
|
|
2139
2140
|
} = {}) {
|
|
2140
2141
|
if (!controller || !workspaces || typeof stateFor !== 'function') {
|
|
2141
2142
|
throw new TypeError('controller, workspaces, and stateFor are required');
|
|
@@ -2289,6 +2290,9 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2289
2290
|
error.code = 'workspace-bot-not-found';
|
|
2290
2291
|
throw error;
|
|
2291
2292
|
}
|
|
2293
|
+
// Channel-specific identity checks must finish before projecting or
|
|
2294
|
+
// committing the new policy. Existing channels need no extra validation.
|
|
2295
|
+
await validateAccessPolicyForBot?.(botId, policy);
|
|
2292
2296
|
const [catalog, models] = await Promise.all([
|
|
2293
2297
|
resolveAgentPresetCatalog(agentPresetCatalog),
|
|
2294
2298
|
resolveModelCatalog(modelCatalog),
|
|
@@ -6,7 +6,7 @@ import { t } from './i18n.mjs';
|
|
|
6
6
|
const reported = new WeakMap();
|
|
7
7
|
const lastReport = new WeakMap();
|
|
8
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' };
|
|
9
|
+
const OWNED_REASONS = { 'connect-timeout': 'timeout', 'telegram-timeout': 'timeout', 'cli-timeout': 'timeout', 'provider-timeout': 'timeout', 'telegram-response-invalid': 'invalid-response', 'office-protocol-mismatch': 'invalid-response' };
|
|
10
10
|
const STANDARD_NAMES = new Set(['Error', 'TypeError', 'SyntaxError', 'AggregateError', 'DOMException', 'AxiosError', 'TimeoutError', 'AbortError']);
|
|
11
11
|
const KNOWN_MESSAGES = {
|
|
12
12
|
'discord-401': 'Discord Bot Token 无效,请重新填写。',
|
|
@@ -37,18 +37,24 @@ export function extractConnectionEvidence(error) {
|
|
|
37
37
|
const { chain, truncated } = connectionErrorChain(error);
|
|
38
38
|
const errors = chain.map(e => ({
|
|
39
39
|
type: STANDARD_NAMES.has(e.name) ? e.name : 'Error',
|
|
40
|
+
// Host RPC adapters carry safe diagnostic facts without the original Error graph.
|
|
40
41
|
...normalizeDiagnosticDetails({
|
|
41
|
-
reason:
|
|
42
|
+
reason: e.details?.reason, reasons: e.details?.reasons,
|
|
43
|
+
httpStatus: e.details?.httpStatus, providerCode: e.details?.providerCode,
|
|
44
|
+
}),
|
|
45
|
+
...normalizeDiagnosticDetails({
|
|
46
|
+
// DOMException TimeoutError has a numeric legacy code; its name is authoritative.
|
|
47
|
+
reason: OWNED_REASONS[e.code] ?? (e.name === 'TimeoutError' ? 'timeout' : e.code ?? (e instanceof SyntaxError ? 'invalid-json' : undefined)),
|
|
42
48
|
httpStatus: e.status ?? e.statusCode ?? e.response?.status,
|
|
43
49
|
providerCode: e.providerCode,
|
|
44
50
|
}),
|
|
45
51
|
hasCause: Boolean(e.cause && typeof e.cause === 'object'),
|
|
46
52
|
hasErrors: Array.isArray(e.errors),
|
|
47
53
|
}));
|
|
48
|
-
const allReasons = [...new Set(errors.
|
|
54
|
+
const allReasons = [...new Set(errors.flatMap(e => e.reason === 'multiple-causes' ? e.reasons ?? [] : [e.reason]).filter(r => r && !['unknown', 'multiple-causes'].includes(r)))];
|
|
49
55
|
const precise = allReasons.filter(r => !['timeout', 'http-error', 'provider-rejected', 'invalid-response'].includes(r));
|
|
50
56
|
const reasons = precise.length ? precise : allReasons;
|
|
51
|
-
const first = field => chain.map(e => normalizeDiagnosticDetails({ [field]: e[field] })[field]).find(v => v !== undefined);
|
|
57
|
+
const first = field => chain.map(e => normalizeDiagnosticDetails({ [field]: e[field] ?? e.details?.[field] })[field]).find(v => v !== undefined);
|
|
52
58
|
return {
|
|
53
59
|
chain, errors,
|
|
54
60
|
details: normalizeDiagnosticDetails({
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// English translations (feishu area). Keys are exact Chinese literals passed to t().
|
|
2
2
|
export default {
|
|
3
|
+
'这里需要填写用户的 Open ID,不能填写机器人自己的 Open ID。':
|
|
4
|
+
'Enter a user Open ID here, not the bot’s own Open ID.',
|
|
3
5
|
'已结束本轮提问,将在当前会话中继续处理图片。':
|
|
4
6
|
'The questions have been closed. Your image will be processed in this conversation.',
|
|
5
7
|
'图片已排队,但暂时无法结束当前提问。请先回答问题或发送 /stop,结束后会继续处理图片。':
|
|
@@ -104,6 +104,10 @@ export default {
|
|
|
104
104
|
'This message contains an image or file that cannot be processed. Adjust it and send it again.',
|
|
105
105
|
'任务未完成,暂时无法确定原因。请重试;若持续发生,请将参考号提供给管理员。':
|
|
106
106
|
'The task did not finish and the cause could not be determined. Try again; if it persists, give the reference ID to an administrator.',
|
|
107
|
+
'请求超时,当前操作结果尚未确认。请先检查任务状态或聊天记录,不要立即重复提交。':
|
|
108
|
+
'The request timed out and its outcome is unconfirmed. Check the task status or chat history before submitting it again.',
|
|
109
|
+
'网络请求失败,当前操作结果尚未确认。请先检查任务状态或聊天记录,不要立即重复提交。':
|
|
110
|
+
'The network request failed and its outcome is unconfirmed. Check the task status or chat history before submitting it again.',
|
|
107
111
|
'错误码:{code};参考号:{referenceId}': 'Error code: {code}; reference: {referenceId}',
|
|
108
112
|
'{label}机器人': '{label} bot',
|
|
109
113
|
'目前支持文字和图片消息。': 'Only text and image messages are supported at the moment.',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractConnectionEvidence } from './connection-error.mjs';
|
|
1
|
+
import { connectionHint, extractConnectionEvidence } from './connection-error.mjs';
|
|
2
2
|
import { diagnosticFields } from './diagnostic-details.mjs';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
@@ -90,6 +90,10 @@ const FAILURE_MESSAGES = Object.freeze({
|
|
|
90
90
|
'回复已经生成,但当前渠道暂时无法发送。请稍后重试。',
|
|
91
91
|
INPUT_INVALID:
|
|
92
92
|
'当前消息包含无法处理的图片或文件。请调整后重新发送。',
|
|
93
|
+
REQUEST_TIMEOUT:
|
|
94
|
+
'请求超时,当前操作结果尚未确认。请先检查任务状态或聊天记录,不要立即重复提交。',
|
|
95
|
+
NETWORK_ERROR:
|
|
96
|
+
'网络请求失败,当前操作结果尚未确认。请先检查任务状态或聊天记录,不要立即重复提交。',
|
|
93
97
|
INTERNAL_UNKNOWN:
|
|
94
98
|
'任务未完成,暂时无法确定原因。请重试;若持续发生,请将参考号提供给管理员。',
|
|
95
99
|
});
|
|
@@ -156,16 +160,30 @@ function failureCode(error) {
|
|
|
156
160
|
return 'CHANNEL_RATE_LIMIT';
|
|
157
161
|
}
|
|
158
162
|
if (['channel-delivery-uncertain', 'delivery-uncertain', 'artifact-delivery-uncertain']
|
|
159
|
-
.includes(code)) {
|
|
163
|
+
.includes(code) || error?.deliveryOutcome === 'unknown') {
|
|
160
164
|
return 'CHANNEL_DELIVERY_UNCERTAIN';
|
|
161
165
|
}
|
|
162
166
|
if (code === 'channel-delivery-failed' || code.startsWith('artifact-')
|
|
163
|
-
||
|
|
167
|
+
|| code === 'network-error') {
|
|
164
168
|
return 'CHANNEL_DELIVERY';
|
|
165
169
|
}
|
|
166
170
|
return 'INTERNAL_UNKNOWN';
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// Only refine the unknown fallback, using the same allowlisted evidence as the UI.
|
|
174
|
+
// Mixed unrelated causes and cancellation do not establish a network diagnosis.
|
|
175
|
+
function transportFailureCode(error, details) {
|
|
176
|
+
if (error?.name === 'AbortError' || error?.code === 'cancelled') return 'INTERNAL_UNKNOWN';
|
|
177
|
+
const reasons = details.reason === 'multiple-causes' ? details.reasons : [details.reason];
|
|
178
|
+
const timeout = reason => reason === 'timeout' || reason === 'ETIMEDOUT' || /_TIMEOUT$/u.test(reason);
|
|
179
|
+
if (reasons?.length && reasons.every(timeout)) return 'REQUEST_TIMEOUT';
|
|
180
|
+
const network = reason => timeout(reason) || [
|
|
181
|
+
'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'UND_ERR_SOCKET',
|
|
182
|
+
'ENETUNREACH', 'EHOSTUNREACH', 'ERR_NETWORK_ACCESS_DENIED', 'ERR_PROXY_CONNECTION_FAILED',
|
|
183
|
+
].includes(reason) || /CERT|TLS|SSL|ISSUER|SIGNATURE/u.test(reason);
|
|
184
|
+
return reasons?.length && reasons.every(network) ? 'NETWORK_ERROR' : 'INTERNAL_UNKNOWN';
|
|
185
|
+
}
|
|
186
|
+
|
|
169
187
|
function safeReferenceId(value) {
|
|
170
188
|
return typeof value === 'string' && /^[A-Z0-9-]{6,40}$/u.test(value)
|
|
171
189
|
? value
|
|
@@ -185,13 +203,17 @@ export function classifyMessageFailure(error, {
|
|
|
185
203
|
} = {}) {
|
|
186
204
|
const safeReason = safeFailureReason(reason);
|
|
187
205
|
const classifiedCode = failureCode(error);
|
|
188
|
-
const
|
|
206
|
+
const businessCode = classifiedCode === 'INTERNAL_UNKNOWN'
|
|
189
207
|
&& safeReason
|
|
190
208
|
&& typeof userMessage === 'string'
|
|
191
209
|
&& userMessage.trim()
|
|
192
210
|
? 'INPUT_INVALID'
|
|
193
211
|
: classifiedCode;
|
|
194
212
|
const details = extractConnectionEvidence(error).details;
|
|
213
|
+
const code = businessCode === 'INTERNAL_UNKNOWN'
|
|
214
|
+
? transportFailureCode(error, details)
|
|
215
|
+
: businessCode;
|
|
216
|
+
if (code === 'REQUEST_TIMEOUT' || code === 'NETWORK_ERROR') details.hint = connectionHint(details);
|
|
195
217
|
return Object.freeze({
|
|
196
218
|
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode ? { details } : {}),
|
|
197
219
|
code,
|
|
@@ -936,12 +936,12 @@ export class TextHarnessBridge {
|
|
|
936
936
|
const finalDeliveryUnknown = textReceipt?.deliveryOutcome === 'unknown';
|
|
937
937
|
if (textReceipt?.deliveryOutcome === 'failed') {
|
|
938
938
|
textDeliveryError = channelDeliveryFailure(
|
|
939
|
-
new Error(
|
|
939
|
+
Object.assign(new Error('Final text delivery failed'), { code: textReceipt.reason }),
|
|
940
940
|
{ uncertain: false },
|
|
941
941
|
);
|
|
942
942
|
} else if (finalDeliveryUnknown) {
|
|
943
943
|
textDeliveryError = channelDeliveryFailure(
|
|
944
|
-
new Error(
|
|
944
|
+
Object.assign(new Error('Final text delivery outcome is unknown'), { code: textReceipt.reason }),
|
|
945
945
|
);
|
|
946
946
|
}
|
|
947
947
|
// A failed final text must not discard an already registered result file.
|