@zhin.js/adapter-qq 1.1.4 → 1.1.5
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/CHANGELOG.md +35 -0
- package/README.md +27 -0
- package/lib/delivery-error.d.ts +13 -0
- package/lib/delivery-error.js +19 -0
- package/lib/endpoint.d.ts +7 -1
- package/lib/endpoint.js +163 -71
- package/lib/interaction.d.ts +7 -0
- package/lib/interaction.js +16 -0
- package/lib/outbound.js +8 -5
- package/lib/protocol.d.ts +3 -1
- package/lib/protocol.js +15 -8
- package/lib/qq-intents.js +1 -0
- package/lib/start-deadline.d.ts +2 -0
- package/lib/start-deadline.js +21 -0
- package/lib/stream-proxy.d.ts +7 -0
- package/lib/stream-proxy.js +17 -0
- package/lib/upload-diagnostics.d.ts +6 -0
- package/lib/upload-diagnostics.js +106 -0
- package/lib/ws.d.ts +2 -0
- package/lib/ws.js +37 -2
- package/package.json +15 -15
- package/schema.json +40 -0
- package/src/delivery-error.ts +20 -0
- package/src/endpoint.ts +141 -71
- package/src/interaction.ts +15 -0
- package/src/outbound.ts +9 -5
- package/src/protocol.ts +17 -8
- package/src/qq-intents.ts +1 -0
- package/src/start-deadline.ts +18 -0
- package/src/stream-proxy.ts +20 -0
- package/src/upload-diagnostics.ts +70 -0
- package/src/ws.ts +27 -2
package/lib/protocol.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** QQ Official Bot protocol helpers. Canonicalization is owned by Core. */
|
|
2
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
3
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
4
|
import { resolveQqIntents } from './qq-intents.js';
|
|
4
5
|
const logger = getLogger('qq');
|
|
@@ -20,6 +21,9 @@ export function resolveQqConfig(config) {
|
|
|
20
21
|
sandbox: config.sandbox === true,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
24
|
+
if (config.streamProxy && (!Number.isInteger(config.streamProxy.port) || config.streamProxy.port < 1 || config.streamProxy.port > 65535
|
|
25
|
+
|| typeof config.streamProxy.serverName !== 'string' || !config.streamProxy.serverName || /[\s/:@?#]/.test(config.streamProxy.serverName)))
|
|
26
|
+
throw new TypeError('Invalid QQ streamProxy port/serverName');
|
|
23
27
|
return {
|
|
24
28
|
context: 'qq',
|
|
25
29
|
mode: 'websocket',
|
|
@@ -33,6 +37,7 @@ export function resolveQqConfig(config) {
|
|
|
33
37
|
}),
|
|
34
38
|
accessTokenUrl: config.accessTokenUrl,
|
|
35
39
|
gatewayUrl: config.gatewayUrl,
|
|
40
|
+
...(config.streamProxy ? { streamProxy: config.streamProxy } : {}),
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
function requiredEndpointField(value, field) {
|
|
@@ -142,20 +147,22 @@ export function parseCompoundMessageId(messageId) {
|
|
|
142
147
|
/** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
|
|
143
148
|
export function resolveOutboundMessageId(result) {
|
|
144
149
|
if (!result || typeof result !== 'object') {
|
|
145
|
-
throw new
|
|
150
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'QQ 发送消息失败:响应为空', 'unknown');
|
|
146
151
|
}
|
|
147
152
|
const row = result;
|
|
153
|
+
if (typeof row.code === 'number' && Number.isFinite(row.code) && row.code !== 0) {
|
|
154
|
+
throw new EndpointDeliveryError('platform_rejected', `QQ platform rejected the outbound request (${row.code})`, 'rejected');
|
|
155
|
+
}
|
|
148
156
|
const nested = row.data && typeof row.data === 'object'
|
|
149
157
|
? row.data
|
|
150
158
|
: undefined;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (id == null || id === '') {
|
|
159
|
+
const id = row.id ?? row.message_id ?? nested?.id ?? nested?.message_id;
|
|
160
|
+
if ((typeof id !== 'string' && typeof id !== 'number') || String(id).trim() === '' || (typeof id === 'number' && !Number.isFinite(id))) {
|
|
154
161
|
const code = row.code;
|
|
155
|
-
const
|
|
156
|
-
throw new
|
|
157
|
-
?
|
|
158
|
-
: 'QQ 发送消息失败:响应缺少消息 ID');
|
|
162
|
+
const rejected = typeof code === 'number' && Number.isFinite(code) && code !== 0;
|
|
163
|
+
throw new EndpointDeliveryError(rejected ? 'platform_rejected' : 'delivery_unconfirmed', code != null
|
|
164
|
+
? 'QQ outbound response is unconfirmed'
|
|
165
|
+
: 'QQ 发送消息失败:响应缺少消息 ID', rejected ? 'rejected' : 'unknown');
|
|
159
166
|
}
|
|
160
167
|
return String(id);
|
|
161
168
|
}
|
package/lib/qq-intents.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** SDK startup must settle; serial endpoint readiness must never wait forever. */
|
|
2
|
+
export async function awaitQqStartup(starting, signal) {
|
|
3
|
+
let timer;
|
|
4
|
+
let aborted;
|
|
5
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
6
|
+
aborted = () => reject(new Error('QQ startup stopped'));
|
|
7
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
8
|
+
if (signal.aborted)
|
|
9
|
+
aborted();
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
await Promise.race([starting, cancelled, new Promise((_resolve, reject) => {
|
|
13
|
+
timer = setTimeout(() => reject(new Error('QQ startup timed out after 30000ms')), 30_000);
|
|
14
|
+
})]);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
if (timer)
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
signal.removeEventListener('abort', aborted);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Agent } from 'node:https';
|
|
2
|
+
export interface QqStreamProxy {
|
|
3
|
+
readonly port: number;
|
|
4
|
+
readonly serverName: string;
|
|
5
|
+
}
|
|
6
|
+
/** QQ receiver uses this agent only for WSS; discovery/OpenAPI stay unchanged. */
|
|
7
|
+
export declare function createQqStreamAgent(proxy: QqStreamProxy): Agent;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Agent } from 'node:https';
|
|
2
|
+
import tls from 'node:tls';
|
|
3
|
+
import { getLogger } from '@zhin.js/logger';
|
|
4
|
+
/** QQ receiver uses this agent only for WSS; discovery/OpenAPI stay unchanged. */
|
|
5
|
+
export function createQqStreamAgent(proxy) {
|
|
6
|
+
const agent = new Agent({ keepAlive: false });
|
|
7
|
+
agent.createConnection = options => {
|
|
8
|
+
const hostname = String(options.hostname ?? options.host ?? '');
|
|
9
|
+
if (hostname !== proxy.serverName || Number(options.port ?? 443) !== 443) {
|
|
10
|
+
getLogger('qq').warn(`Stream proxy rejected gateway hostname: ${hostname}`);
|
|
11
|
+
throw new Error('Qq Stream proxy gateway identity changed; update fixed upstream before retrying');
|
|
12
|
+
}
|
|
13
|
+
return tls.connect({ ...options, path: undefined, host: '127.0.0.1', port: proxy.port,
|
|
14
|
+
servername: hostname, rejectUnauthorized: true });
|
|
15
|
+
};
|
|
16
|
+
return agent;
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type Metadata = Record<string, string | number | boolean | undefined>;
|
|
2
|
+
/** Same complete-set rule as the SDK patch; preserve platform index in ACKs. */
|
|
3
|
+
export declare function inferQqUploadIndexBase(parts: unknown, fileSize: number, blockSize: number): 0 | 1 | undefined;
|
|
4
|
+
/** Endpoint-owned observer. Emits only whitelisted numeric/boolean metadata. */
|
|
5
|
+
export declare function installQqUploadDiagnostics(api: unknown, debug: (metadata: Metadata) => void): () => void;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const row = (value) => value !== null && (typeof value === 'object' || typeof value === 'function') ? value : {};
|
|
4
|
+
const numeric = (value) => { const n = typeof value === 'number' || typeof value === 'string' ? Number(value) : NaN; return Number.isSafeInteger(n) && n >= 0 ? n : undefined; };
|
|
5
|
+
const hash = (buffer, algorithm = 'md5') => createHash(algorithm).update(buffer).digest('hex');
|
|
6
|
+
const body = (value) => { if (typeof value !== 'string')
|
|
7
|
+
return row(value); try {
|
|
8
|
+
return row(JSON.parse(value));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
} };
|
|
13
|
+
const stage = (value) => typeof value === 'string' ? value.split('?')[0].split('/').at(-1) ?? '' : '';
|
|
14
|
+
/** Same complete-set rule as the SDK patch; preserve platform index in ACKs. */
|
|
15
|
+
export function inferQqUploadIndexBase(parts, fileSize, blockSize) {
|
|
16
|
+
if (!Number.isSafeInteger(blockSize) || blockSize <= 0 || !fileSize || !Array.isArray(parts))
|
|
17
|
+
return undefined;
|
|
18
|
+
const count = Math.ceil(fileSize / blockSize);
|
|
19
|
+
if (!parts.length || parts.length !== count)
|
|
20
|
+
return undefined;
|
|
21
|
+
let base = Infinity;
|
|
22
|
+
const indices = new Set();
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
const index = row(part).index;
|
|
25
|
+
if (typeof index !== 'number' || !Number.isSafeInteger(index))
|
|
26
|
+
return undefined;
|
|
27
|
+
base = Math.min(base, index);
|
|
28
|
+
indices.add(index);
|
|
29
|
+
}
|
|
30
|
+
if ((base !== 0 && base !== 1) || indices.size !== count)
|
|
31
|
+
return undefined;
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
const item = row(part);
|
|
34
|
+
const index = item.index;
|
|
35
|
+
if (index < base || index >= base + count || numeric(item.block_size) !== Math.min(blockSize, fileSize - (index - base) * blockSize))
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
/** Endpoint-owned observer. Emits only whitelisted numeric/boolean metadata. */
|
|
41
|
+
export function installQqUploadDiagnostics(api, debug) {
|
|
42
|
+
const bot = row(api);
|
|
43
|
+
const request = row(bot.request);
|
|
44
|
+
const interceptors = row(request.interceptors);
|
|
45
|
+
const requests = interceptors.request;
|
|
46
|
+
const responses = interceptors.response;
|
|
47
|
+
const processor = row(bot.fileProcessor);
|
|
48
|
+
const original = processor.uploadByChunks;
|
|
49
|
+
if (!requests?.use || !responses?.use || typeof original !== 'function')
|
|
50
|
+
return () => { };
|
|
51
|
+
const storage = new AsyncLocalStorage();
|
|
52
|
+
let active = true;
|
|
53
|
+
const emit = (metadata) => { if (active) {
|
|
54
|
+
try {
|
|
55
|
+
debug(metadata);
|
|
56
|
+
}
|
|
57
|
+
catch { /* diagnostics never affect delivery */ }
|
|
58
|
+
} };
|
|
59
|
+
const wrapped = function (buffer, ...args) {
|
|
60
|
+
return storage.run({ buffer, finishes: 0 }, () => original.call(this, buffer, ...args));
|
|
61
|
+
};
|
|
62
|
+
processor.uploadByChunks = wrapped;
|
|
63
|
+
// Axios request hooks run LIFO, before the SDK authentication/body transformer;
|
|
64
|
+
// success response hooks run FIFO, after the SDK's identity success hook.
|
|
65
|
+
const requestId = requests.use(value => {
|
|
66
|
+
const config = row(value);
|
|
67
|
+
const data = body(config.data);
|
|
68
|
+
const context = storage.getStore();
|
|
69
|
+
if (stage(config.url) === 'upload_prepare') {
|
|
70
|
+
emit({ op: 'qq_upload_prepare', fileType: numeric(data.file_type), fileSize: numeric(data.file_size), actualSize: context?.buffer.length,
|
|
71
|
+
...(context ? { sizeMatch: numeric(data.file_size) === context.buffer.length, hashMatch: data.md5 === hash(context.buffer) && data.sha1 === hash(context.buffer, 'sha1') && data.md5_10m === hash(context.buffer.subarray(0, 10002432)) } : {}) });
|
|
72
|
+
}
|
|
73
|
+
else if (stage(config.url) === 'upload_part_finish' && context && context.finishes++ < 8) {
|
|
74
|
+
const index = numeric(data.part_index);
|
|
75
|
+
const length = numeric(data.block_size);
|
|
76
|
+
const blockSize = context.blockSize;
|
|
77
|
+
const base = context.indexBase;
|
|
78
|
+
const expected = index !== undefined && blockSize && base !== undefined ? context.buffer.subarray((index - base) * blockSize, (index - base + 1) * blockSize) : undefined;
|
|
79
|
+
emit({ op: 'qq_upload_part_finish', index, blockSize: length, ...(expected ? { sizeMatch: length === expected.length, hashMatch: data.md5 === hash(expected) } : {}) });
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
});
|
|
83
|
+
const responseId = responses.use(value => {
|
|
84
|
+
const response = row(value);
|
|
85
|
+
const config = row(response.config);
|
|
86
|
+
if (stage(config.url) !== 'upload_prepare')
|
|
87
|
+
return value;
|
|
88
|
+
const data = row(response.data);
|
|
89
|
+
const parts = Array.isArray(data.parts) ? data.parts.slice(0, 128).map(row) : undefined;
|
|
90
|
+
const blockSize = numeric(data.block_size);
|
|
91
|
+
const context = storage.getStore();
|
|
92
|
+
if (context) {
|
|
93
|
+
context.blockSize = blockSize;
|
|
94
|
+
context.indexBase = blockSize ? inferQqUploadIndexBase(data.parts, context.buffer.length, blockSize) : undefined;
|
|
95
|
+
}
|
|
96
|
+
const indices = parts?.map(part => numeric(part.index)).filter((index) => index !== undefined) ?? [];
|
|
97
|
+
const sizes = parts?.map(part => numeric(part.block_size)).filter((size) => size !== undefined) ?? [];
|
|
98
|
+
emit({ op: 'qq_upload_prepared', blockSize, partsPresent: parts !== undefined, partsCount: Array.isArray(data.parts) ? data.parts.length : undefined, inspectedCount: parts?.length,
|
|
99
|
+
validIndices: indices.length, ...(indices.length ? { minIndex: Math.min(...indices), maxIndex: Math.max(...indices) } : {}),
|
|
100
|
+
validSizes: sizes.length, ...(sizes.length ? { minPartSize: Math.min(...sizes), maxPartSize: Math.max(...sizes) } : {}),
|
|
101
|
+
...(context && blockSize ? { coverageMatch: context.indexBase !== undefined, indexBase: context.indexBase } : {}) });
|
|
102
|
+
return value;
|
|
103
|
+
});
|
|
104
|
+
return () => { active = false; requests.eject(requestId); responses.eject(responseId); if (processor.uploadByChunks === wrapped)
|
|
105
|
+
processor.uploadByChunks = original; storage.disable(); };
|
|
106
|
+
}
|
package/lib/ws.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Bot, type ApproveJoinRequestOptions } from 'qq-official-bot';
|
|
2
|
+
import type { EndpointTransportState } from 'zhin.js/adapter';
|
|
2
3
|
import type { QqOutboundMessage } from './outbound.js';
|
|
3
4
|
import type { QqInboundMessage, ResolvedQqWebsocketConfig } from './protocol.js';
|
|
4
5
|
/** SDK business surface exposed to plugins without bypassing Endpoint lifecycle ownership. */
|
|
5
6
|
export type QqOfficialApi = Pick<Bot, 'messageService' | 'fileProcessor' | 'guildService' | 'channelService' | 'memberService' | 'permissionService' | 'reactionService' | 'scheduleService' | 'threadService' | 'audioService' | 'botService' | 'groupService' | 'menuPanelService' | 'group' | 'user' | 'channel' | 'direct' | 'guild'>;
|
|
6
7
|
/** Minimal bot surface used by the endpoint (real qq-official-bot or test mock). */
|
|
7
8
|
export interface QqBotTransport {
|
|
9
|
+
readonly transportState?: EndpointTransportState;
|
|
8
10
|
/** Full qq-official-bot business API; lifecycle remains owned by the Endpoint. */
|
|
9
11
|
readonly api: QqOfficialApi;
|
|
10
12
|
start(): Promise<void>;
|
package/lib/ws.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* QQ WebSocket transport: bot factory and inbound message normalization.
|
|
3
3
|
*/
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createQqStreamAgent } from './stream-proxy.js';
|
|
5
6
|
import { Bot, ReceiverMode, } from 'qq-official-bot';
|
|
6
7
|
/**
|
|
7
8
|
* qq-official-bot 1.2 used a separate connection manager; 1.3 moved WebSocket
|
|
@@ -184,6 +185,7 @@ function extractTextContent(message) {
|
|
|
184
185
|
}
|
|
185
186
|
export function defaultCreateBot(config) {
|
|
186
187
|
const bot = new Bot({
|
|
188
|
+
...{ handleProcessErrors: false },
|
|
187
189
|
appid: config.appid,
|
|
188
190
|
secret: config.secret,
|
|
189
191
|
mode: ReceiverMode.WEBSOCKET,
|
|
@@ -195,14 +197,47 @@ export function defaultCreateBot(config) {
|
|
|
195
197
|
...(config.accessTokenUrl ? { accessTokenUrl: config.accessTokenUrl } : {}),
|
|
196
198
|
...(config.gatewayUrl ? { gatewayUrl: config.gatewayUrl } : {}),
|
|
197
199
|
});
|
|
200
|
+
const receiver = bot.receiver;
|
|
201
|
+
receiver.config.autoReconnect = false;
|
|
202
|
+
const agent = config.streamProxy ? createQqStreamAgent(config.streamProxy) : undefined;
|
|
203
|
+
receiver.config.agent = agent;
|
|
204
|
+
const receiverListeners = [];
|
|
205
|
+
// Keep receiver error ownership after Session.start removes its temporary listener.
|
|
206
|
+
const receiverError = () => { };
|
|
207
|
+
receiver.on('error', receiverError);
|
|
198
208
|
return {
|
|
209
|
+
get transportState() {
|
|
210
|
+
const receiver = bot.receiver;
|
|
211
|
+
const state = receiver.handler?.ws?.readyState;
|
|
212
|
+
if (state === 1 && receiver.isReady && receiver.isClosed === false)
|
|
213
|
+
return 'open';
|
|
214
|
+
if (state === 0)
|
|
215
|
+
return 'connecting';
|
|
216
|
+
if (receiver.isStarted)
|
|
217
|
+
return 'reconnecting';
|
|
218
|
+
return 'closed';
|
|
219
|
+
},
|
|
199
220
|
api: bot,
|
|
200
221
|
start: () => bot.start().then(() => undefined),
|
|
201
|
-
stop: () =>
|
|
222
|
+
stop: async () => {
|
|
223
|
+
try {
|
|
224
|
+
await stopQqOfficialBot(bot);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
agent?.destroy();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
202
230
|
on: (event, listener) => {
|
|
203
|
-
|
|
231
|
+
if (event === 'transport.close') {
|
|
232
|
+
receiver.on('close', listener);
|
|
233
|
+
receiverListeners.push({ event: 'close', listener });
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
bot.on(event, listener);
|
|
204
237
|
},
|
|
205
238
|
removeAllListeners: () => {
|
|
239
|
+
for (const entry of receiverListeners.splice(0))
|
|
240
|
+
receiver.removeListener(entry.event, entry.listener);
|
|
206
241
|
bot.removeAllListeners(undefined);
|
|
207
242
|
},
|
|
208
243
|
sendPrivateMessage: (userId, message) => bot.sendPrivateMessage(userId, message),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-qq",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,24 +35,24 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"qq-official-bot": "^1.3.0",
|
|
38
|
-
"@zhin.js/adapter": "1.1.
|
|
39
|
-
"@zhin.js/agent-feature": "1.1.
|
|
40
|
-
"@zhin.js/core": "1.1.
|
|
38
|
+
"@zhin.js/adapter": "1.1.16",
|
|
39
|
+
"@zhin.js/agent-feature": "1.1.3",
|
|
40
|
+
"@zhin.js/core": "1.1.40",
|
|
41
41
|
"@zhin.js/feature-kit": "1.1.2",
|
|
42
|
-
"@zhin.js/host-http": "1.1.
|
|
43
|
-
"@zhin.js/im-contract": "1.1.
|
|
42
|
+
"@zhin.js/host-http": "1.1.3",
|
|
43
|
+
"@zhin.js/im-contract": "1.1.3",
|
|
44
44
|
"@zhin.js/logger": "1.1.1",
|
|
45
45
|
"@zhin.js/permission": "1.1.3",
|
|
46
|
-
"@zhin.js/skill": "1.1.
|
|
47
|
-
"@zhin.js/tool": "1.1.
|
|
46
|
+
"@zhin.js/skill": "1.1.3",
|
|
47
|
+
"@zhin.js/tool": "1.1.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"zod": "^4.0.0",
|
|
51
|
-
"@zhin.js/adapter": "^1.1.
|
|
52
|
-
"@zhin.js/command": "^1.1.
|
|
53
|
-
"@zhin.js/core": "^1.1.
|
|
54
|
-
"@zhin.js/middleware": "^1.1.
|
|
55
|
-
"zhin.js": "^1.1.
|
|
51
|
+
"@zhin.js/adapter": "^1.1.16",
|
|
52
|
+
"@zhin.js/command": "^1.1.4",
|
|
53
|
+
"@zhin.js/core": "^1.1.40",
|
|
54
|
+
"@zhin.js/middleware": "^1.1.3",
|
|
55
|
+
"zhin.js": "^1.1.5"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@zhin.js/command": {
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"typescript": "^6.0.3",
|
|
74
74
|
"vitest": "^4.1.10",
|
|
75
75
|
"zod": "^4.4.3",
|
|
76
|
-
"@zhin.js/host-http": "1.1.
|
|
77
|
-
"zhin.js": "1.1.
|
|
76
|
+
"@zhin.js/host-http": "1.1.3",
|
|
77
|
+
"zhin.js": "1.1.5"
|
|
78
78
|
},
|
|
79
79
|
"files": [
|
|
80
80
|
"adapters",
|
package/schema.json
CHANGED
|
@@ -137,6 +137,26 @@
|
|
|
137
137
|
"id": {
|
|
138
138
|
"type": "string",
|
|
139
139
|
"description": "QQ bot name"
|
|
140
|
+
},
|
|
141
|
+
"streamProxy": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": false,
|
|
144
|
+
"required": [
|
|
145
|
+
"port",
|
|
146
|
+
"serverName"
|
|
147
|
+
],
|
|
148
|
+
"properties": {
|
|
149
|
+
"port": {
|
|
150
|
+
"type": "integer",
|
|
151
|
+
"minimum": 1,
|
|
152
|
+
"maximum": 65535
|
|
153
|
+
},
|
|
154
|
+
"serverName": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"minLength": 1
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"description": "Optional fixed loopback TCP fault proxy for WSS only; original gateway TLS identity must match serverName."
|
|
140
160
|
}
|
|
141
161
|
},
|
|
142
162
|
"required": [
|
|
@@ -150,6 +170,26 @@
|
|
|
150
170
|
"type": "string",
|
|
151
171
|
"default": "",
|
|
152
172
|
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
173
|
+
},
|
|
174
|
+
"streamProxy": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"required": [
|
|
178
|
+
"port",
|
|
179
|
+
"serverName"
|
|
180
|
+
],
|
|
181
|
+
"properties": {
|
|
182
|
+
"port": {
|
|
183
|
+
"type": "integer",
|
|
184
|
+
"minimum": 1,
|
|
185
|
+
"maximum": 65535
|
|
186
|
+
},
|
|
187
|
+
"serverName": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"minLength": 1
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"description": "Optional fixed loopback TCP fault proxy for WSS only; original gateway TLS identity must match serverName."
|
|
153
193
|
}
|
|
154
194
|
},
|
|
155
195
|
"required": [
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
+
|
|
3
|
+
/** Project SDK request failures into stable delivery evidence without exposing request data. */
|
|
4
|
+
export function qqDeliveryFailure(error: unknown) {
|
|
5
|
+
const value = error && typeof error === 'object' ? error as { response?: { status?: unknown; data?: { code?: unknown } }; config?: { url?: unknown }; code?: unknown } : {};
|
|
6
|
+
const httpStatus = typeof value.response?.status === 'number' ? value.response.status : undefined;
|
|
7
|
+
let platformCode = typeof value.response?.data?.code === 'number' && Number.isFinite(value.response.data.code) ? value.response.data.code : undefined;
|
|
8
|
+
let path = typeof value.config?.url === 'string' ? value.config.url.split('?')[0] : '';
|
|
9
|
+
// Published SDK 1.3 flattens Axios rejection into this exact message format.
|
|
10
|
+
// Extract only operation suffix and numeric protocol code; never surface the message.
|
|
11
|
+
const flattened = error instanceof Error ? /^Request "([^"]+)" failed with code\((\d+)\):/.exec(error.message) : null;
|
|
12
|
+
if (flattened) {
|
|
13
|
+
path ||= flattened[1].split('?')[0];
|
|
14
|
+
platformCode ??= Number(flattened[2]);
|
|
15
|
+
}
|
|
16
|
+
const stage = /\/upload_prepare$/.test(path) ? 'upload_prepare' : /\/upload_part_finish$/.test(path) ? 'upload_part_finish' : /\/files$/.test(path) ? 'upload_complete' : /\/messages$/.test(path) ? 'send_message' : 'sdk_send';
|
|
17
|
+
const knownRejection = (httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500) || (!!flattened && platformCode !== undefined && ((platformCode >= 400 && platformCode < 500) || platformCode >= 1000));
|
|
18
|
+
const failure = error instanceof EndpointDeliveryError ? error : new EndpointDeliveryError(knownRejection ? 'platform_rejected' : 'delivery_unconfirmed', knownRejection ? 'QQ platform rejected the outbound request' : 'QQ outbound request outcome is unknown', knownRejection ? 'rejected' : 'unknown', { cause: error });
|
|
19
|
+
return { failure, diagnostic: { op: 'qq_delivery_failed', stage, httpStatus, platformCode, code: failure.code, disposition: failure.disposition } };
|
|
20
|
+
}
|