@zhin.js/adapter-qq 1.1.20 → 1.1.22

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +13 -0
  3. package/adapters/qq/index.js +27 -5
  4. package/adapters/qq/index.ts +27 -5
  5. package/agents/qq/skills/qq/SKILL.md +1 -1
  6. package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
  7. package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
  8. package/agents/qq/skills/qq-conversation/SKILL.md +3 -3
  9. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +1 -1
  10. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
  11. package/agents/qq/skills/qq-group-admin/SKILL.md +3 -3
  12. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
  13. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
  14. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +1 -1
  15. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +1 -1
  16. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +1 -1
  17. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +1 -1
  18. package/agents/qq/skills/qq-group-directory/SKILL.md +3 -3
  19. package/agents/qq/skills/qq-group-state/SKILL.md +1 -1
  20. package/agents/qq/skills/qq-messages/SKILL.md +1 -1
  21. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +1 -1
  22. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +1 -1
  23. package/lib/adapter.js +1 -1
  24. package/lib/delivery-error.d.ts +1 -1
  25. package/lib/endpoint.js +6 -0
  26. package/lib/outbound.d.ts +4 -10
  27. package/lib/outbound.js +51 -60
  28. package/lib/passive-reply.d.ts +2 -0
  29. package/lib/passive-reply.js +12 -5
  30. package/lib/protocol.d.ts +9 -1
  31. package/lib/protocol.js +7 -0
  32. package/lib/qq-console-spec.js +2 -0
  33. package/lib/self-profile.d.ts +9 -0
  34. package/lib/self-profile.js +14 -0
  35. package/lib/side-event-dispatch.js +1 -1
  36. package/lib/stream-markdown.d.ts +5 -0
  37. package/lib/stream-markdown.js +41 -0
  38. package/lib/streaming.d.ts +5 -0
  39. package/lib/streaming.js +105 -0
  40. package/package.json +22 -21
  41. package/plugin.js +1 -0
  42. package/schema.json +146 -1
  43. package/src/adapter.ts +1 -1
  44. package/src/endpoint.ts +6 -0
  45. package/src/outbound.ts +46 -55
  46. package/src/passive-reply.ts +13 -4
  47. package/src/protocol.ts +15 -1
  48. package/src/qq-console-spec.ts +2 -0
  49. package/src/self-profile.ts +20 -0
  50. package/src/side-event-dispatch.ts +1 -1
  51. package/src/stream-markdown.ts +41 -0
  52. package/src/streaming.ts +98 -0
@@ -36,6 +36,17 @@ export class QqPassiveReplyLedger {
36
36
  * newest usable source in the conversation. Undefined when only an active message is possible.
37
37
  */
38
38
  acquire(kind, conversationId, preferred) {
39
+ const entry = this.#usableSource(kind, conversationId, preferred);
40
+ if (!entry)
41
+ return undefined;
42
+ entry.replies += 1;
43
+ return { source: entry.source, msgSeq: entry.replies };
44
+ }
45
+ /** Local stream preflight must not reserve a reply before any content exists. */
46
+ hasUsableSource(kind, conversationId, preferred) {
47
+ return this.#usableSource(kind, conversationId, preferred) !== undefined;
48
+ }
49
+ #usableSource(kind, conversationId, preferred) {
39
50
  const limit = QQ_PASSIVE_LIMITS[kind];
40
51
  const now = this.now();
41
52
  const entries = this.#conversations.get(`${kind}:${conversationId}`) ?? [];
@@ -43,12 +54,8 @@ export class QqPassiveReplyLedger {
43
54
  && entry.replies < limit.maxReplies
44
55
  && now - entry.receivedAt < limit.windowMs - SAFETY_MARGIN_MS;
45
56
  const preferredKey = preferred ? sourceKey(preferred) : undefined;
46
- const entry = (preferredKey ? entries.find(item => sourceKey(item.source) === preferredKey && usable(item)) : undefined)
57
+ return (preferredKey ? entries.find(item => sourceKey(item.source) === preferredKey && usable(item)) : undefined)
47
58
  ?? [...entries].reverse().find(usable);
48
- if (!entry)
49
- return undefined;
50
- entry.replies += 1;
51
- return { source: entry.source, msgSeq: entry.replies };
52
59
  }
53
60
  /** The platform said this source can no longer be replied to (expired, over the limit, invalid id). */
54
61
  exhaust(kind, conversationId, source) {
package/lib/protocol.d.ts CHANGED
@@ -1,14 +1,18 @@
1
+ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
1
2
  /** QQ Official Bot protocol helpers. Canonicalization is owned by Core. */
2
3
  import { type ConversationRef } from '@zhin.js/im-contract';
3
4
  import { type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
4
5
  /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
5
- export interface QqEndpointConfig {
6
+ export interface QqEndpointConfig extends EndpointProfileConfiguration {
6
7
  readonly id: string;
7
8
  readonly appid: string;
8
9
  readonly secret: string;
9
10
  /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
10
11
  readonly mode?: 'websocket' | 'webhook' | 'middleware';
11
12
  readonly sandbox?: boolean;
13
+ /** Publish supported C2C replies while generated; false keeps complete delivery. */
14
+ readonly streaming?: boolean;
15
+ readonly streamingIntervalMs?: number;
12
16
  /** Explicit intents; when omitted, derived from `botKind` (default public). */
13
17
  readonly intents?: readonly string[];
14
18
  /** public | private — drives default intents when `intents` omitted. */
@@ -27,6 +31,8 @@ export interface ResolvedQqWebsocketConfig {
27
31
  readonly appid: string;
28
32
  readonly secret: string;
29
33
  readonly sandbox: boolean;
34
+ readonly streaming?: boolean;
35
+ readonly streamingIntervalMs?: number;
30
36
  readonly intents?: readonly string[];
31
37
  readonly accessTokenUrl?: string;
32
38
  readonly gatewayUrl?: string;
@@ -41,6 +47,8 @@ export interface ResolvedQqHttpConfig {
41
47
  readonly secret: string;
42
48
  readonly webhookPath: string;
43
49
  readonly sandbox: boolean;
50
+ readonly streaming?: boolean;
51
+ readonly streamingIntervalMs?: number;
44
52
  readonly notify?: ResolvedAdapterNotifyTarget;
45
53
  }
46
54
  export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
package/lib/protocol.js CHANGED
@@ -9,6 +9,9 @@ export function resolveQqConfig(config) {
9
9
  const appid = requiredEndpointField(config.appid, 'appid');
10
10
  const secret = requiredEndpointField(config.secret, 'secret');
11
11
  const mode = config.mode ?? 'websocket';
12
+ const streamingIntervalMs = config.streamingIntervalMs ?? 500;
13
+ if (!Number.isFinite(streamingIntervalMs) || streamingIntervalMs < 0)
14
+ throw new TypeError('streamingIntervalMs must be a non-negative finite number');
12
15
  const notify = resolveAdapterNotifyTarget(config.notify);
13
16
  if (mode === 'webhook' || mode === 'middleware') {
14
17
  const webhookPathRaw = config.webhookPath ?? '/qq/webhook';
@@ -21,6 +24,8 @@ export function resolveQqConfig(config) {
21
24
  secret,
22
25
  webhookPath,
23
26
  sandbox: config.sandbox === true,
27
+ streaming: config.streaming !== false,
28
+ streamingIntervalMs,
24
29
  ...(notify ? { notify } : {}),
25
30
  };
26
31
  }
@@ -34,6 +39,8 @@ export function resolveQqConfig(config) {
34
39
  appid,
35
40
  secret,
36
41
  sandbox: config.sandbox === true,
42
+ streaming: config.streaming !== false,
43
+ streamingIntervalMs,
37
44
  intents: resolveQqIntents({
38
45
  intents: config.intents,
39
46
  botKind: config.botKind,
@@ -6,6 +6,8 @@ export const qqConsoleSpec = {
6
6
  { key: 'secret', required: true, env: true, description: 'QQ app secret' },
7
7
  { key: 'mode', description: 'websocket, webhook or middleware' },
8
8
  { key: 'sandbox', type: 'boolean', description: 'QQ sandbox environment' },
9
+ { key: 'streaming', type: 'boolean', description: 'Publish supported C2C replies while generating (default true)' },
10
+ { key: 'streamingIntervalMs', type: 'number', description: 'Delay between reply updates in milliseconds (default 500)' },
9
11
  { key: 'botKind', description: 'public or private; existing explicit intents remain authoritative' },
10
12
  { key: 'accessTokenUrl', description: 'Configured access-token API URL' },
11
13
  { key: 'gatewayUrl', description: 'Configured Gateway discovery URL' },
@@ -0,0 +1,9 @@
1
+ import { type EndpointProfile } from 'zhin.js/adapter';
2
+ import type { Bot } from 'qq-official-bot';
3
+ interface QqSelfProfileOptions {
4
+ readonly client: Pick<Bot, 'getSelfInfo'>;
5
+ readonly reportAccount: (id: string) => void;
6
+ }
7
+ /** QQ official Bot.Info has an actual bot id, display username and public avatar URL. */
8
+ export declare function createQqSelfProfile({ client, reportAccount }: QqSelfProfileOptions): EndpointProfile;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ import { createEndpointOperation } from 'zhin.js/adapter';
2
+ /** QQ official Bot.Info has an actual bot id, display username and public avatar URL. */
3
+ export function createQqSelfProfile({ client, reportAccount }) {
4
+ return Object.freeze({
5
+ writableFields: Object.freeze([]),
6
+ async read({ signal }) {
7
+ const self = await createEndpointOperation({ signal }).prepare(() => client.getSelfInfo());
8
+ if (!self?.id || typeof self.username !== 'string')
9
+ throw new Error('QQ returned an incomplete bot profile');
10
+ reportAccount(self.id);
11
+ return { nickname: self.username, ...(typeof self.avatar === 'string' ? { avatar: self.avatar } : {}) };
12
+ },
13
+ });
14
+ }
@@ -1,4 +1,4 @@
1
- import { receiveOneBotLikeSideEvent } from '@zhin.js/core';
1
+ import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
2
2
  import { formatCompact } from '@zhin.js/logger';
3
3
  export function bindQqBotSideEvents(bot, dispatch) {
4
4
  // qq-official-bot emits every dotted prefix. Listening at `notice` once avoids
@@ -0,0 +1,5 @@
1
+ /** Only source-level fenced context is portable to a fresh native Markdown message. */
2
+ export declare function qqMarkdownContinuation(precedingText: string): {
3
+ readonly opening: string;
4
+ };
5
+ export declare function qqMarkdownClosing(source: string): string;
@@ -0,0 +1,41 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
+ /** Only source-level fenced context is portable to a fresh native Markdown message. */
3
+ export function qqMarkdownContinuation(precedingText) {
4
+ if (!precedingText)
5
+ return { opening: '' };
6
+ const fence = openFence(precedingText);
7
+ if (fence) {
8
+ if (!precedingText.includes('\n', fence.offset))
9
+ throw unsupported();
10
+ return { opening: `${fence.marker}${fence.info}\n` };
11
+ }
12
+ if (/(?:^|\n) {0,3}(?:`{3,}|~{3,})[ \t]*\n?$/.test(precedingText))
13
+ return { opening: '' };
14
+ const boundary = precedingText.lastIndexOf('\n\n');
15
+ const block = precedingText.slice(boundary >= 0 ? boundary + 2 : 0);
16
+ // No native contract permits retroactively recreating an unfinished inline
17
+ // construct in another message. Admit plain continuations and closed blocks.
18
+ if (/[`*_\[\]~]/.test(block))
19
+ throw unsupported();
20
+ return { opening: '' };
21
+ }
22
+ export function qqMarkdownClosing(source) {
23
+ const fence = openFence(source);
24
+ return fence ? `${source.endsWith('\n') ? '' : '\n'}${fence.marker}` : '';
25
+ }
26
+ function openFence(source) {
27
+ let active;
28
+ for (const line of source.matchAll(/^ {0,3}(`{3,}|~{3,})([^\n]*)/gm)) {
29
+ const marker = line[1], info = line[2];
30
+ if (active) {
31
+ if (marker[0] === active.marker[0] && marker.length >= active.marker.length && !info.trim())
32
+ active = undefined;
33
+ }
34
+ else if (marker[0] !== '`' || !info.includes('`'))
35
+ active = { marker, info, offset: line.index };
36
+ }
37
+ return active;
38
+ }
39
+ function unsupported() {
40
+ return new EndpointDeliveryError('unsupported_operation', 'QQ Markdown continuation requires a plain block boundary or a complete fenced-code header', 'not_sent');
41
+ }
@@ -0,0 +1,5 @@
1
+ import type { Bot } from 'qq-official-bot';
2
+ import { type EndpointStreaming } from 'zhin.js/adapter';
3
+ import { type QqPassiveReplyLedger } from './passive-reply.js';
4
+ /** C2C native streams share the ordinary passive ledger, source and sequence. */
5
+ export declare function createQqStreaming(getBot: () => Bot, ledger: QqPassiveReplyLedger, minIntervalMs?: number): EndpointStreaming;
@@ -0,0 +1,105 @@
1
+ import { createEndpointOperation } from 'zhin.js/adapter';
2
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
3
+ import { qqDeliveryFailure } from './delivery-error.js';
4
+ import { QQ_PASSIVE_SOURCE_CODES } from './passive-reply.js';
5
+ import { qqOutboundKind, resolveOutboundMessageId } from './protocol.js';
6
+ import { qqMarkdownClosing, qqMarkdownContinuation } from './stream-markdown.js';
7
+ function supportsQqStream(request) {
8
+ return qqOutboundKind(request.conversation) === 'private';
9
+ }
10
+ /** C2C native streams share the ordinary passive ledger, source and sequence. */
11
+ export function createQqStreaming(getBot, ledger, minIntervalMs = 500) {
12
+ return {
13
+ mode: 'native',
14
+ minIntervalMs,
15
+ supports: supportsQqStream,
16
+ open(request) {
17
+ if (!supportsQqStream(request))
18
+ return undefined;
19
+ // stream_messages has no verified canonical mention/keyboard projection.
20
+ // Keep these combinations on the complete-message codec instead of guessing.
21
+ if (request.prefix?.some(segment => segment.type !== 'reply'))
22
+ return undefined;
23
+ const reply = request.prefix?.find(segment => segment.type === 'reply');
24
+ const explicitId = reply && 'message_id' in reply.data ? reply.data.message_id : undefined;
25
+ const preferred = typeof explicitId === 'string' && explicitId
26
+ ? { id: explicitId }
27
+ : request.source?.messageId ? { id: request.source.messageId } : undefined;
28
+ if (!ledger.hasUsableSource('private', request.conversation.id, preferred))
29
+ return undefined;
30
+ const precedingText = request.continuation?.precedingText ?? '';
31
+ const opening = request.format === 'markdown' ? qqMarkdownContinuation(precedingText).opening : '';
32
+ const cancellation = new AbortController();
33
+ const signal = request.signal ? AbortSignal.any([request.signal, cancellation.signal]) : cancellation.signal;
34
+ const operation = createEndpointOperation({ signal });
35
+ let native;
36
+ let grant;
37
+ let acceptedText = '';
38
+ let messageId;
39
+ let exhausted = false;
40
+ let contentUnavailable = Boolean(opening);
41
+ let openingSent = false;
42
+ let failure;
43
+ let completion;
44
+ const acknowledgement = () => ({
45
+ status: messageId ? 'sent' : 'buffered', acceptedText, ...(messageId ? { messageId } : {}),
46
+ ...(exhausted ? { exhausted: true } : {}),
47
+ ...(contentUnavailable ? { contentUnavailable: true } : {}),
48
+ });
49
+ const send = async (text, final) => {
50
+ if (failure)
51
+ throw failure;
52
+ if (signal.aborted)
53
+ throw new EndpointDeliveryError('endpoint_inactive', 'QQ stream cancelled', 'not_sent');
54
+ if (!text.startsWith(acceptedText))
55
+ throw new EndpointDeliveryError('invalid_payload', 'QQ stream cannot replace its accepted prefix', 'not_sent');
56
+ if (!final && (text === acceptedText || exhausted))
57
+ return acknowledgement();
58
+ if (final && exhausted && text !== acceptedText)
59
+ throw new EndpointDeliveryError('stream_capacity', 'QQ exhausted stream must finish its accepted prefix', 'not_sent');
60
+ if (!native) {
61
+ if (!text)
62
+ return acknowledgement();
63
+ grant = ledger.acquire('private', request.conversation.id, preferred);
64
+ if (!grant)
65
+ throw new EndpointDeliveryError('unsupported_operation', 'QQ stream has no usable passive reply source', 'not_sent');
66
+ native = getBot().createPrivateStream(request.conversation.id, {
67
+ source: grant.source, msgSeq: grant.msgSeq,
68
+ contentType: request.format, inputMode: 'append',
69
+ });
70
+ }
71
+ try {
72
+ const closing = final && request.format === 'markdown' ? qqMarkdownClosing(precedingText + text) : '';
73
+ // An exhausted container permits only the terminating state frame;
74
+ // no extra formatting characters may exceed its confirmed capacity.
75
+ const suffix = exhausted ? '' : closing;
76
+ const delta = (openingSent ? '' : opening) + text.slice(acceptedText.length) + suffix;
77
+ const result = await operation.run(() => final ? native.end(delta) : native.write(delta));
78
+ const id = resolveOutboundMessageId(result);
79
+ messageId = `private-${request.conversation.id}:${id}`;
80
+ acceptedText = text;
81
+ openingSent = true;
82
+ contentUnavailable ||= Boolean(closing);
83
+ // Zero is exhausted regardless of the platform's otherwise unspecified unit.
84
+ exhausted = result.remain_msg_len === 0;
85
+ return acknowledgement();
86
+ }
87
+ catch (error) {
88
+ const result = qqDeliveryFailure(error);
89
+ if (grant && result.diagnostic.platformCode !== undefined && QQ_PASSIVE_SOURCE_CODES.has(result.diagnostic.platformCode)) {
90
+ ledger.exhaust('private', request.conversation.id, grant.source);
91
+ }
92
+ failure = result.failure;
93
+ throw failure;
94
+ }
95
+ };
96
+ return {
97
+ write: text => completion
98
+ ? Promise.reject(new EndpointDeliveryError('stream_closed', 'QQ stream is finishing', 'not_sent'))
99
+ : send(text, false),
100
+ finish: text => completion ??= send(text, true),
101
+ cancel: () => { cancellation.abort(); },
102
+ };
103
+ },
104
+ };
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,27 +34,28 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "qq-official-bot": "^1.3.3",
37
- "@zhin.js/adapter": "1.1.26",
38
- "@zhin.js/agent-feature": "1.1.11",
39
- "@zhin.js/client": "1.1.22",
37
+ "@zhin.js/adapter-onebot-events": "0.0.1",
38
+ "@zhin.js/adapter": "1.1.28",
39
+ "@zhin.js/agent-feature": "1.1.13",
40
+ "@zhin.js/client": "1.1.24",
40
41
  "@zhin.js/console-contract": "1.1.3",
41
- "@zhin.js/core": "1.1.54",
42
- "@zhin.js/feature-kit": "1.1.10",
43
- "@zhin.js/host-http": "1.1.12",
44
- "@zhin.js/im-contract": "1.1.9",
42
+ "@zhin.js/core": "1.1.56",
43
+ "@zhin.js/feature-kit": "1.1.12",
44
+ "@zhin.js/host-http": "1.1.14",
45
+ "@zhin.js/im-contract": "1.1.11",
45
46
  "@zhin.js/logger": "1.1.2",
46
- "@zhin.js/page": "1.1.12",
47
- "@zhin.js/permission": "1.1.10",
48
- "@zhin.js/skill": "1.1.11",
49
- "@zhin.js/tool": "1.1.13"
47
+ "@zhin.js/page": "1.1.14",
48
+ "@zhin.js/permission": "1.1.12",
49
+ "@zhin.js/skill": "1.1.13",
50
+ "@zhin.js/tool": "1.1.15"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "zod": "^4.0.0",
53
- "@zhin.js/adapter": "^1.1.26",
54
- "@zhin.js/command": "^1.1.12",
55
- "@zhin.js/core": "^1.1.54",
56
- "@zhin.js/middleware": "^1.1.11",
57
- "zhin.js": "^1.1.19"
54
+ "@zhin.js/adapter": "^1.1.28",
55
+ "@zhin.js/command": "^1.1.14",
56
+ "@zhin.js/core": "^1.1.56",
57
+ "@zhin.js/middleware": "^1.1.13",
58
+ "zhin.js": "^1.1.21"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
61
  "@zhin.js/command": {
@@ -75,8 +76,8 @@
75
76
  "typescript": "^6.0.3",
76
77
  "vitest": "^4.1.10",
77
78
  "zod": "^4.4.3",
78
- "@zhin.js/host-http": "1.1.12",
79
- "zhin.js": "1.1.19"
79
+ "@zhin.js/host-http": "1.1.14",
80
+ "zhin.js": "1.1.21"
80
81
  },
81
82
  "files": [
82
83
  "adapters",
@@ -109,7 +110,7 @@
109
110
  "features": [
110
111
  {
111
112
  "package": "@zhin.js/adapter",
112
- "api": "^2.0.0"
113
+ "api": "^2.1.0"
113
114
  },
114
115
  {
115
116
  "package": "@zhin.js/command",
@@ -121,7 +122,7 @@
121
122
  },
122
123
  {
123
124
  "package": "@zhin.js/tool",
124
- "api": "^1.0.0"
125
+ "api": "^1.1.0"
125
126
  },
126
127
  {
127
128
  "package": "@zhin.js/skill",
package/plugin.js CHANGED
@@ -7,6 +7,7 @@ import { createQqConsoleRpc } from "./lib/console.js";
7
7
  import { cancelQqConsoleBind } from "./lib/qq-console.js";
8
8
  export default definePlugin({
9
9
  name: 'qq',
10
+ configUpdate: { featurePaths: [['nickname'], ['profile'], ['endpoints']] },
10
11
  metadata: {
11
12
  displayName: 'QQ Official WebSocket Adapter',
12
13
  },
package/schema.json CHANGED
@@ -284,6 +284,46 @@
284
284
  "endpoint",
285
285
  "id"
286
286
  ]
287
+ },
288
+ "streaming": {
289
+ "type": "boolean",
290
+ "description": "在支持的会话中渐进发送回复。设为 false 时仅发送完整回复。",
291
+ "x-i18n": {
292
+ "en": {
293
+ "description": "Publish supported replies while generating. Set false to send only complete replies."
294
+ }
295
+ }
296
+ },
297
+ "streamingIntervalMs": {
298
+ "type": "number",
299
+ "minimum": 0,
300
+ "description": "后续回复更新的最小间隔(毫秒);首段立即发送。",
301
+ "x-i18n": {
302
+ "en": {
303
+ "description": "Minimum delay in milliseconds between reply updates; the first update is immediate."
304
+ }
305
+ }
306
+ },
307
+ "nickname": {
308
+ "type": [
309
+ "string",
310
+ "null"
311
+ ],
312
+ "description": "账号昵称兜底",
313
+ "x-i18n": {
314
+ "en": {
315
+ "description": "Fallback account nickname"
316
+ }
317
+ }
318
+ },
319
+ "profile": {
320
+ "$ref": "#/$defs/selfProfileFallback",
321
+ "description": "账号资料兜底",
322
+ "x-i18n": {
323
+ "en": {
324
+ "description": "Fallback account profile"
325
+ }
326
+ }
287
327
  }
288
328
  },
289
329
  "required": [
@@ -414,9 +454,114 @@
414
454
  "endpoint",
415
455
  "id"
416
456
  ]
457
+ },
458
+ "streaming": {
459
+ "type": "boolean",
460
+ "default": true,
461
+ "description": "在支持的会话中渐进发送回复。设为 false 时仅发送完整回复。",
462
+ "x-i18n": {
463
+ "en": {
464
+ "description": "Publish supported replies while generating. Set false to send only complete replies."
465
+ }
466
+ }
467
+ },
468
+ "streamingIntervalMs": {
469
+ "type": "number",
470
+ "minimum": 0,
471
+ "default": 500,
472
+ "description": "后续回复更新的最小间隔(毫秒);首段立即发送。",
473
+ "x-i18n": {
474
+ "en": {
475
+ "description": "Minimum delay in milliseconds between reply updates; the first update is immediate."
476
+ }
477
+ }
478
+ },
479
+ "nickname": {
480
+ "type": [
481
+ "string",
482
+ "null"
483
+ ],
484
+ "description": "账号昵称兜底",
485
+ "x-i18n": {
486
+ "en": {
487
+ "description": "Fallback account nickname"
488
+ }
489
+ }
490
+ },
491
+ "profile": {
492
+ "$ref": "#/$defs/selfProfileFallback",
493
+ "description": "账号资料兜底",
494
+ "x-i18n": {
495
+ "en": {
496
+ "description": "Fallback account profile"
497
+ }
498
+ }
417
499
  }
418
500
  },
419
501
  "required": [
420
502
  "endpoints"
421
- ]
503
+ ],
504
+ "$defs": {
505
+ "selfProfileFallback": {
506
+ "type": "object",
507
+ "additionalProperties": false,
508
+ "properties": {
509
+ "gender": {
510
+ "type": [
511
+ "string",
512
+ "null"
513
+ ],
514
+ "enum": [
515
+ "male",
516
+ "female",
517
+ "other",
518
+ "unknown",
519
+ null
520
+ ]
521
+ },
522
+ "age": {
523
+ "type": [
524
+ "integer",
525
+ "null"
526
+ ],
527
+ "minimum": 0
528
+ },
529
+ "birthday": {
530
+ "type": [
531
+ "string",
532
+ "null"
533
+ ],
534
+ "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$"
535
+ },
536
+ "signature": {
537
+ "type": [
538
+ "string",
539
+ "null"
540
+ ]
541
+ },
542
+ "avatar": {
543
+ "type": [
544
+ "string",
545
+ "null"
546
+ ],
547
+ "pattern": "^(https?://[^\\s/?#]+(?:[/?#].*)?|)$"
548
+ },
549
+ "interests": {
550
+ "type": [
551
+ "array",
552
+ "null"
553
+ ],
554
+ "items": {
555
+ "type": "string"
556
+ }
557
+ },
558
+ "bio": {
559
+ "type": [
560
+ "string",
561
+ "null"
562
+ ]
563
+ }
564
+ }
565
+ }
566
+ }
422
567
  }
package/src/adapter.ts CHANGED
@@ -9,7 +9,7 @@ export class QqAdapter extends Adapter<QqHttpEndpoint | QqWebsocketEndpoint, QqE
9
9
  constructor(private readonly context: AdapterContext<QqEndpointConfig>) { super(); }
10
10
  override get configurationIdentityFields(): readonly string[] { return ['id', 'appid']; }
11
11
  override get configurationIdentityProbeFields(): readonly string[] { return ['accessTokenUrl', 'gatewayUrl']; }
12
- override get configurationUpdateFields(): readonly string[] { return ['secret', 'mode', 'sandbox', 'intents', 'botKind',
12
+ override get configurationUpdateFields(): readonly string[] { return ['secret', 'mode', 'sandbox', 'streaming', 'streamingIntervalMs', 'intents', 'botKind',
13
13
  'accessTokenUrl', 'gatewayUrl', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName', 'webhookPath']; }
14
14
  protected override validateConfiguration(input: QqEndpointConfig): void { resolveQqConfig(input); }
15
15
  protected createEndpoint(input: QqEndpointConfig, execution: EndpointCreationContext): QqHttpEndpoint | QqWebsocketEndpoint {
package/src/endpoint.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { createQqSelfProfile } from './self-profile.js';
1
2
  import { Bot, ReceiverMode } from 'qq-official-bot';
2
3
  import { installQqUploadDiagnostics } from './upload-diagnostics.js';
3
4
  import { projectQqGuildSenderRoles } from './platform-permit.js';
4
5
  import { qqDeliveryFailure } from './delivery-error.js';
5
6
  import { normalizeQqInteraction } from './interaction.js';
6
7
  import { awaitQqStartup } from './start-deadline.js';
8
+ import { createQqStreaming } from './streaming.js';
7
9
  import { EndpointDeliveryError, type MessageRef } from '@zhin.js/im-contract';
8
10
  import {
9
11
  inspectQqGroupMember,
@@ -111,6 +113,8 @@ export class QqWebsocketEndpoint extends Bot<ReceiverMode.WEBSOCKET> implements
111
113
  start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
112
114
  stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
113
115
  get transportState() { return endpoint.#transportState; }, management: this.#management, control: this.#control,
116
+ streaming: options.config.streaming !== false ? createQqStreaming(() => this.#requireBot(), this.#passiveReplies, options.config.streamingIntervalMs) : undefined,
117
+ profile: createQqSelfProfile({ client: this, reportAccount: id => this[endpointSpec].reportSelfUserId(id) }),
114
118
  });
115
119
  this.#bindBot(this);
116
120
  }
@@ -416,6 +420,8 @@ export class QqHttpEndpoint extends Bot<ReceiverMode.MIDDLEWARE, 'koa'> implemen
416
420
  stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
417
421
  get transportState() { return endpoint.#transportState; },
418
422
  management: this.#management, control: this.#control,
423
+ streaming: options.config.streaming !== false ? createQqStreaming(() => this.#requireBot(), this.#passiveReplies, options.config.streamingIntervalMs) : undefined,
424
+ profile: createQqSelfProfile({ client: this, reportAccount: id => this[endpointSpec].reportSelfUserId(id) }),
419
425
  });
420
426
  this.#bindBot(this);
421
427
  }