@zhin.js/adapter-qq 1.1.19 → 1.1.21
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 +57 -0
- package/README.md +13 -0
- package/agents/qq/skills/qq/SKILL.md +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
- package/agents/qq/skills/qq-conversation/SKILL.md +3 -3
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +1 -1
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/SKILL.md +3 -3
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +1 -1
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +1 -1
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/SKILL.md +3 -3
- package/agents/qq/skills/qq-group-state/SKILL.md +1 -1
- package/agents/qq/skills/qq-messages/SKILL.md +1 -1
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +1 -1
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +1 -1
- package/lib/adapter.js +1 -1
- package/lib/delivery-error.d.ts +1 -1
- package/lib/endpoint.js +6 -0
- package/lib/outbound.d.ts +4 -10
- package/lib/outbound.js +51 -60
- package/lib/passive-reply.d.ts +2 -0
- package/lib/passive-reply.js +12 -5
- package/lib/protocol.d.ts +9 -1
- package/lib/protocol.js +7 -0
- package/lib/qq-console-spec.js +2 -0
- package/lib/self-profile.d.ts +9 -0
- package/lib/self-profile.js +14 -0
- package/lib/stream-markdown.d.ts +5 -0
- package/lib/stream-markdown.js +41 -0
- package/lib/streaming.d.ts +5 -0
- package/lib/streaming.js +105 -0
- package/package.json +21 -21
- package/schema.json +146 -1
- package/src/adapter.ts +1 -1
- package/src/endpoint.ts +6 -0
- package/src/outbound.ts +46 -55
- package/src/passive-reply.ts +13 -4
- package/src/protocol.ts +15 -1
- package/src/qq-console-spec.ts +2 -0
- package/src/self-profile.ts +20 -0
- package/src/stream-markdown.ts +41 -0
- package/src/streaming.ts +98 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @zhin.js/adapter-qq
|
|
2
2
|
|
|
3
|
+
## 1.1.21
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- af8f803: Use the current Endpoint's platform profile for Agent account identity, with per-field configured fallbacks and scoped, identity-approved profile tools. Keep SOUL first-person and independent of account names. Profile ports preserve the source Client, cancellation, truthful update outcomes and private image authority.
|
|
8
|
+
|
|
9
|
+
Adapter Feature API 2.1 and Tool Feature API 1.1 describe the additive profile requirements; new Core, official adapters and generated plugins declare the corresponding floors. Existing compatible API-2.0/API-1.0 plugins remain accepted by the newer providers. npm releases remain patch.
|
|
10
|
+
|
|
11
|
+
Private IM assets now bind to the full canonical conversation and principal. Existing IM asset records that contain only the earlier lossy hash remain on disk until their original expiry, but cannot be safely rebound; recapture them. No automatic file deletion, platform profile mutation or workspace SOUL rewrite occurs during upgrade.
|
|
12
|
+
|
|
13
|
+
- 26fda97: Deliver validated final answers progressively through configured QQ C2C, Telegram drafts, Slack streams, Lark cards and Sandbox updates. Keep complete-message delivery for unsupported scenes, disabled adapters and outbound middleware that does not explicitly support revisions. Discord retains complete-message delivery.
|
|
14
|
+
|
|
15
|
+
Preserve source-account leases, media order and confirmed delivery facts across cancellation, capacity rollover and follow-up answer phases. Preview drafts and uncertain outcomes do not become sent history. Bound each reply's semantic snapshot to 1 MiB, retain confirmed prefixes on failure and avoid replaying published answers.
|
|
16
|
+
|
|
17
|
+
Project the latest confirmed message revision into paginated conversation history and synchronize Sandbox message updates across connections. Existing adapter implementations and complete-send callers remain compatible.
|
|
18
|
+
|
|
19
|
+
- 976886c: Preserve confirmed progressive reply content, routing and delivery evidence across cancellation, rollover and closing failures without replaying published text. Keep error dispositions and actual message references, and abort the owning provider request when its functional text consumer fails.
|
|
20
|
+
|
|
21
|
+
Enable native Sandbox Markdown through the real Core projection so Console updates retain formatting in one message. Preserve instance-level streaming opt-outs, recall tombstones and multi-page synchronization. The first safe content is immediate; later updates retain the configured pacing, including large valid intervals.
|
|
22
|
+
|
|
23
|
+
Keep Slack and Lark Markdown streaming within the current physical message. When continuation formatting cannot be verified, stop and retain confirmed content instead of replaying or silently changing it to plain text. Plain-text continuations retain their original reply route.
|
|
24
|
+
|
|
25
|
+
Use rebuildable revision metadata and indexes in the existing conversation event table for bounded SQL history reads. Original event JSON and retention remain unchanged; older writers and models keep their compatible read path. Supported adapter streaming remains enabled by default, with complete-message fallback for disabled settings or outbound middleware without incremental: true.
|
|
26
|
+
|
|
27
|
+
- 3c1912f: Restore legacy Sandbox streaming history updates while preserving revision precedence, creation order, recall boundaries and browser-cache provenance. Report empty QQ send plans as not sent before any platform call.
|
|
28
|
+
|
|
29
|
+
Seek past completed conversation-history backfill batches instead of re-scanning the migrated prefix. Preserve original event data, retention and compatible older writers.
|
|
30
|
+
|
|
31
|
+
Report terminal provider quota and billing failures after publication without regeneration, and isolate synchronous diagnostic failures from provider outcomes and retry/fallback decisions.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [b88e988]
|
|
34
|
+
- Updated dependencies [f78eb84]
|
|
35
|
+
- Updated dependencies [c15cef9]
|
|
36
|
+
- Updated dependencies [b0c02a2]
|
|
37
|
+
- Updated dependencies [af8f803]
|
|
38
|
+
- Updated dependencies [26fda97]
|
|
39
|
+
- Updated dependencies [976886c]
|
|
40
|
+
- Updated dependencies [032dc96]
|
|
41
|
+
- @zhin.js/adapter@1.1.27
|
|
42
|
+
- @zhin.js/client@1.1.23
|
|
43
|
+
- @zhin.js/im-contract@1.1.10
|
|
44
|
+
- @zhin.js/core@1.1.55
|
|
45
|
+
- @zhin.js/tool@1.1.14
|
|
46
|
+
- @zhin.js/page@1.1.13
|
|
47
|
+
- @zhin.js/host-http@1.1.13
|
|
48
|
+
- @zhin.js/agent-feature@1.1.12
|
|
49
|
+
- @zhin.js/feature-kit@1.1.11
|
|
50
|
+
- @zhin.js/permission@1.1.11
|
|
51
|
+
- @zhin.js/skill@1.1.12
|
|
52
|
+
|
|
53
|
+
## 1.1.20
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [5e878ba]
|
|
58
|
+
- @zhin.js/page@1.1.12
|
|
59
|
+
|
|
3
60
|
## 1.1.19
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -44,6 +44,19 @@ pnpm add @zhin.js/adapter-qq
|
|
|
44
44
|
|
|
45
45
|
必填字段(`endpoints[i]`):`id`、`appid`、`secret`。
|
|
46
46
|
|
|
47
|
+
## 渐进式回复
|
|
48
|
+
|
|
49
|
+
C2C 私聊默认在生成过程中更新文本或 Markdown,群聊和频道继续使用完整消息发送。Markdown 是否可用仍取决于机器人已开通的平台权限;不支持的内容组合沿用完整发送链路。图片、语音、文件和按钮通过同一回复链路按内容顺序处理。
|
|
50
|
+
|
|
51
|
+
首段立即发送,后续更新默认至少间隔 500 毫秒。可在实例配置或单个 endpoint 中设置:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
streaming: true
|
|
55
|
+
streamingIntervalMs: 500
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`streaming: false` 关闭生成中的公开更新,恢复完整消息发送。流式回复与普通回复共用被动消息来源和配额;同一流的分片不重复申领被动回复序号。取消停止后续更新,不自动撤回已确认内容;发送结果未知时不会整条重发。
|
|
59
|
+
|
|
47
60
|
## 最小配置
|
|
48
61
|
|
|
49
62
|
```yaml
|
|
@@ -31,4 +31,4 @@ scopes:
|
|
|
31
31
|
|
|
32
32
|
确认频道和成员 ID 后查询详情、维护角色或处置成员;权限与审批由各 Tool 执行。可访问频道列表使用 qq-directory。
|
|
33
33
|
|
|
34
|
-
`guild_mute_member` 作用于当前频道(0 秒 = 解除),`guild_kick_member` 每次人工审批,可选拉黑和撤回近 N
|
|
34
|
+
`guild_mute_member` 作用于当前频道(0 秒 = 解除),`guild_kick_member` 每次人工审批,可选拉黑和撤回近 N 天消息;你需具备频道管理权限,平台拒绝时不要自动重试。
|
|
@@ -5,7 +5,7 @@ import { platformPermission } from '@zhin.js/permission';
|
|
|
5
5
|
import { currentGuildChannel } from '../../../../conversation.js';
|
|
6
6
|
import { messages as t } from '../../../../messages.js';
|
|
7
7
|
export default defineAgentTool({
|
|
8
|
-
description: '禁言 / 解除禁言当前 QQ
|
|
8
|
+
description: '禁言 / 解除禁言当前 QQ 频道中的成员;需你具备频道管理权限',
|
|
9
9
|
adapter: 'qq',
|
|
10
10
|
scopes: ['channel'],
|
|
11
11
|
tags: ['qq'],
|
|
@@ -5,7 +5,7 @@ import { currentGuildChannel } from '../../../../conversation.js';
|
|
|
5
5
|
import { messages as t } from '../../../../messages.js';
|
|
6
6
|
|
|
7
7
|
export default defineAgentTool({
|
|
8
|
-
description: '禁言 / 解除禁言当前 QQ
|
|
8
|
+
description: '禁言 / 解除禁言当前 QQ 频道中的成员;需你具备频道管理权限',
|
|
9
9
|
adapter: 'qq',
|
|
10
10
|
scopes: ['channel'],
|
|
11
11
|
tags: ['qq'],
|
|
@@ -15,10 +15,10 @@ tools:
|
|
|
15
15
|
---
|
|
16
16
|
# qq-conversation
|
|
17
17
|
|
|
18
|
-
Use `conversation_info` before claiming
|
|
18
|
+
Use `conversation_info` before claiming your role or available platform permissions. Group roles come live from the official `bot_state` API for the current endpoint, not from the sender's role. A failed lookup is not evidence that you are an ordinary member.
|
|
19
19
|
|
|
20
|
-
Caller roles authorize a request;
|
|
20
|
+
Caller roles authorize a request; your platform roles determine whether QQ can execute it. They are independent, and a successful inspection does not replace the live check each management tool performs.
|
|
21
21
|
|
|
22
|
-
In guilds
|
|
22
|
+
In guilds your role is read from your guild member record (4 owner, 2 admin, 5 child-channel admin). A `channel_admin` record does not say which child channels it governs, so it cannot authorize an operation in the current channel.
|
|
23
23
|
|
|
24
24
|
The official QQ Bot API has no merged forward, poke, profile like, leave group, group announcement, essence, special title, anonymous, group files or group sign-in. Do not promise or simulate them.
|
|
@@ -14,7 +14,7 @@ function guildRole(roles) {
|
|
|
14
14
|
return ids.length ? 'member' : 'unknown';
|
|
15
15
|
}
|
|
16
16
|
export default defineAgentTool({
|
|
17
|
-
description: '读取当前 QQ
|
|
17
|
+
description: '读取当前 QQ 官方机器人会话及你自己的群/频道身份;区分调用者权限、你的平台权限和官方不支持的操作',
|
|
18
18
|
adapter: 'qq',
|
|
19
19
|
requiresApproval: 'never',
|
|
20
20
|
readOnly: true,
|
|
@@ -12,7 +12,7 @@ function guildRole(roles: readonly unknown[] | undefined): string {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export default defineAgentTool({
|
|
15
|
-
description: '读取当前 QQ
|
|
15
|
+
description: '读取当前 QQ 官方机器人会话及你自己的群/频道身份;区分调用者权限、你的平台权限和官方不支持的操作',
|
|
16
16
|
adapter: 'qq',
|
|
17
17
|
requiresApproval: 'never',
|
|
18
18
|
readOnly: true,
|
|
@@ -21,14 +21,14 @@ scopes:
|
|
|
21
21
|
---
|
|
22
22
|
# qq-group-admin
|
|
23
23
|
|
|
24
|
-
Load only when the current group and target member are explicit. Every tool targets the current conversation;
|
|
24
|
+
Load only when the current group and target member are explicit. Every tool targets the current conversation; you cannot choose another group. Each tool re-checks your live role (`bot_state`) before acting.
|
|
25
25
|
|
|
26
|
-
Official limits (enforced by QQ, not by this skill):
|
|
26
|
+
Official limits (enforced by QQ, not by this skill): you must be group owner/admin; only ordinary members (not owner, admins or bots) can be removed or muted; mute is at most 30 days; recall works only within 2 minutes; member removal and join-request APIs are invite-only (11253 = no access).
|
|
27
27
|
|
|
28
28
|
`group_kick` and `handle_join_request` need a fresh human approval each time; show the target member and the reason. A previous approval never authorizes the next action. `set_mute` always needs approval.
|
|
29
29
|
|
|
30
30
|
Automatic moderation needs a confirmed rule and target group; never invent trigger conditions from permissions alone.
|
|
31
31
|
|
|
32
|
-
`recall_member_message` keeps the master/admin/trusted/owner requester permission without per-call approval and also works in guild channels (platform-enforced admin rights). To recall
|
|
32
|
+
`recall_member_message` keeps the master/admin/trusted/owner requester permission without per-call approval and also works in guild channels (platform-enforced admin rights). To recall your own message, use `qq-messages`.
|
|
33
33
|
|
|
34
34
|
Not available on official bots: invite to group, leave group, special title, anonymous, announcement, essence, poke and group sign-in.
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { currentGroup, requireGroupManager } from '../../../../conversation.js';
|
|
5
5
|
import { messages as t } from '../../../../messages.js';
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '通过或拒绝当前 QQ
|
|
7
|
+
description: '通过或拒绝当前 QQ 群的一条入群申请;每次人工审批,需你为群管理员,官方白名单能力',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -4,7 +4,7 @@ import { currentGroup, requireGroupManager } from '../../../../conversation.js';
|
|
|
4
4
|
import { messages as t } from '../../../../messages.js';
|
|
5
5
|
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '通过或拒绝当前 QQ
|
|
7
|
+
description: '通过或拒绝当前 QQ 群的一条入群申请;每次人工审批,需你为群管理员,官方白名单能力',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { currentConversation, recallTarget, requireGroupManager, resolveMessageTarget } from '../../../../conversation.js';
|
|
5
5
|
import { messages as t } from '../../../../messages.js';
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '撤回当前 QQ
|
|
7
|
+
description: '撤回当前 QQ 群或频道子频道内他人的真实消息;需调用方具备群管发起权限,你须为管理员,官方只允许 2 分钟内',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group', 'channel'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -4,7 +4,7 @@ import { currentConversation, recallTarget, requireGroupManager, resolveMessageT
|
|
|
4
4
|
import { messages as t } from '../../../../messages.js';
|
|
5
5
|
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '撤回当前 QQ
|
|
7
|
+
description: '撤回当前 QQ 群或频道子频道内他人的真实消息;需调用方具备群管发起权限,你须为管理员,官方只允许 2 分钟内',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group', 'channel'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { currentGroup, muteQqGroupMember, QQ_GROUP_MUTE_MAX_SECONDS, requireGroupManager } from '../../../../conversation.js';
|
|
5
5
|
import { messages as t } from '../../../../messages.js';
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '禁言 / 解除禁言当前 QQ 群中的普通成员(最长 30
|
|
7
|
+
description: '禁言 / 解除禁言当前 QQ 群中的普通成员(最长 30 天);需你为群管理员',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -4,7 +4,7 @@ import { currentGroup, muteQqGroupMember, QQ_GROUP_MUTE_MAX_SECONDS, requireGrou
|
|
|
4
4
|
import { messages as t } from '../../../../messages.js';
|
|
5
5
|
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '禁言 / 解除禁言当前 QQ 群中的普通成员(最长 30
|
|
7
|
+
description: '禁言 / 解除禁言当前 QQ 群中的普通成员(最长 30 天);需你为群管理员',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['group'],
|
|
10
10
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
@@ -17,8 +17,8 @@ scopes:
|
|
|
17
17
|
---
|
|
18
18
|
# qq-group-directory
|
|
19
19
|
|
|
20
|
-
All lookups target the current group only. Members are identified by `member_openid`, which is scoped to
|
|
20
|
+
All lookups target the current group only. Members are identified by `member_openid`, which is scoped to your current account and group; never derive it from a nickname or reuse it in another group.
|
|
21
21
|
|
|
22
|
-
Member list and member detail are invite-only official capabilities. Error 11253 means
|
|
22
|
+
Member list and member detail are invite-only official capabilities. Error 11253 means you have no access; report that instead of guessing. `group_members` is paged (≤30 per page, 60 QPM): request further pages only when needed.
|
|
23
23
|
|
|
24
|
-
There is no official API for
|
|
24
|
+
There is no official API for your group list, friend list or arbitrary user profiles, so `group_list`, `friend_list` and `user_info` do not exist on QQ official bots.
|
|
@@ -16,6 +16,6 @@ scopes:
|
|
|
16
16
|
---
|
|
17
17
|
# qq-group-state
|
|
18
18
|
|
|
19
|
-
Read-only views of the current group. `list_muted` returns the global mute rule and members whose mute has not expired. `list_join_requests` and `group_blacklist` are paged invite-only APIs; error 11253 means
|
|
19
|
+
Read-only views of the current group. `list_muted` returns the global mute rule and members whose mute has not expired. `list_join_requests` and `group_blacklist` are paged invite-only APIs; error 11253 means you have no access.
|
|
20
20
|
|
|
21
21
|
Act on a join request with `qq-group-admin` → `handle_join_request`, after confirming the applicant. Group files are not exposed by the official API.
|
|
@@ -22,6 +22,6 @@ Only use real message IDs from the current conversation. Do not invent IDs or so
|
|
|
22
22
|
|
|
23
23
|
`quote_reply` sends a text reply that references one original (passive reply `msg_id` / guild `message_reference`) through the unified outbound pipeline. Use `inspect_conversation_reference` to read an incoming quote when available; it does not send messages.
|
|
24
24
|
|
|
25
|
-
`recall_own_message` accepts only
|
|
25
|
+
`recall_own_message` accepts only your own send receipt ID and needs no management role or approval. QQ only recalls messages younger than 2 minutes; never promise success before confirmation or retry an unconfirmed result. To recall another member's message, load `qq-group-admin`.
|
|
26
26
|
|
|
27
27
|
Merged forward (`forward_messages`) is not available: the official API cannot send merged-forward records.
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { recallTarget, resolveMessageTarget } from '../../../../conversation.js';
|
|
5
5
|
import { messages as t } from '../../../../messages.js';
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '
|
|
7
|
+
description: '撤回你在当前 QQ 会话中自己发送的消息;无需群管角色或逐次审批,官方只允许撤回 2 分钟内的消息',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['private', 'group', 'channel'],
|
|
10
10
|
requiresApproval: 'never',
|
|
@@ -4,7 +4,7 @@ import { recallTarget, resolveMessageTarget } from '../../../../conversation.js'
|
|
|
4
4
|
import { messages as t } from '../../../../messages.js';
|
|
5
5
|
|
|
6
6
|
export default defineAgentTool({
|
|
7
|
-
description: '
|
|
7
|
+
description: '撤回你在当前 QQ 会话中自己发送的消息;无需群管角色或逐次审批,官方只允许撤回 2 分钟内的消息',
|
|
8
8
|
adapter: 'qq',
|
|
9
9
|
scopes: ['private', 'group', 'channel'],
|
|
10
10
|
requiresApproval: 'never',
|
package/lib/adapter.js
CHANGED
|
@@ -12,7 +12,7 @@ export class QqAdapter extends Adapter {
|
|
|
12
12
|
get configurationIdentityFields() { return ['id', 'appid']; }
|
|
13
13
|
get configurationIdentityProbeFields() { return ['accessTokenUrl', 'gatewayUrl']; }
|
|
14
14
|
get configurationUpdateFields() {
|
|
15
|
-
return ['secret', 'mode', 'sandbox', 'intents', 'botKind',
|
|
15
|
+
return ['secret', 'mode', 'sandbox', 'streaming', 'streamingIntervalMs', 'intents', 'botKind',
|
|
16
16
|
'accessTokenUrl', 'gatewayUrl', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName', 'webhookPath'];
|
|
17
17
|
}
|
|
18
18
|
validateConfiguration(input) { resolveQqConfig(input); }
|
package/lib/delivery-error.d.ts
CHANGED
package/lib/endpoint.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { createQqSelfProfile } from './self-profile.js';
|
|
1
2
|
import { Bot } 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 } from '@zhin.js/im-contract';
|
|
8
10
|
import { inspectQqGroupMember, inspectQqGroupMemberProfile, inspectQqGroupScene, kickQqGroupMember, moderateQqGroupMember, muteQqGroupMember, qqGroupService, } from './group-admin.js';
|
|
9
11
|
/**
|
|
@@ -47,6 +49,8 @@ export class QqWebsocketEndpoint extends Bot {
|
|
|
47
49
|
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
48
50
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
49
51
|
get transportState() { return endpoint.#transportState; }, management: this.#management, control: this.#control,
|
|
52
|
+
streaming: options.config.streaming !== false ? createQqStreaming(() => this.#requireBot(), this.#passiveReplies, options.config.streamingIntervalMs) : undefined,
|
|
53
|
+
profile: createQqSelfProfile({ client: this, reportAccount: id => this[endpointSpec].reportSelfUserId(id) }),
|
|
50
54
|
});
|
|
51
55
|
this.#bindBot(this);
|
|
52
56
|
}
|
|
@@ -344,6 +348,8 @@ export class QqHttpEndpoint extends Bot {
|
|
|
344
348
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
345
349
|
get transportState() { return endpoint.#transportState; },
|
|
346
350
|
management: this.#management, control: this.#control,
|
|
351
|
+
streaming: options.config.streaming !== false ? createQqStreaming(() => this.#requireBot(), this.#passiveReplies, options.config.streamingIntervalMs) : undefined,
|
|
352
|
+
profile: createQqSelfProfile({ client: this, reportAccount: id => this[endpointSpec].reportSelfUserId(id) }),
|
|
347
353
|
});
|
|
348
354
|
this.#bindBot(this);
|
|
349
355
|
}
|
package/lib/outbound.d.ts
CHANGED
|
@@ -21,16 +21,10 @@ export interface FormatOutboundMessagesOptions {
|
|
|
21
21
|
readonly imageSizes?: ReadonlyMap<string, QqImageSize>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* 1. 公网 URL 图片且已知尺寸:按 QQ markdown 语法 `` 嵌入同一
|
|
29
|
-
* markdown 段(开放平台会下载转存),仍为一条消息;
|
|
30
|
-
* 2. 其余媒体(base64 / 本地渲染图:v2 /files 上传只返回 file_info,拿不到 URL;或尺寸
|
|
31
|
-
* 未知、模板 markdown 无法拼接)降级为独立富媒体消息先发,markdown + 按钮作为最后一条
|
|
32
|
-
* (返回其消息 ID)。reply 段随每条消息携带,保持被动回复。
|
|
33
|
-
* 不含媒体与 markdown/按钮冲突时与 formatOutbound 一致(单条)。
|
|
24
|
+
* Preserve the original segment order before formatOutbound merges Markdown.
|
|
25
|
+
* Public images with known dimensions become inline Markdown at their original
|
|
26
|
+
* position. Other media form physical-message barriers; reply routing is copied
|
|
27
|
+
* to every resulting message, without moving text across a media barrier.
|
|
34
28
|
*/
|
|
35
29
|
export declare function formatOutboundMessages(payload: unknown, options?: FormatOutboundMessagesOptions): QqOutboundMessage[];
|
|
36
30
|
/**
|
package/lib/outbound.js
CHANGED
|
@@ -274,75 +274,66 @@ export function formatOutbound(payload) {
|
|
|
274
274
|
const SPLIT_MEDIA_TYPES = new Set(['image', 'audio', 'video']);
|
|
275
275
|
/** 走 msg_type=2 的 markdown/按钮段 */
|
|
276
276
|
const MARKDOWN_MESSAGE_TYPES = new Set(['markdown', 'keyboard', 'button']);
|
|
277
|
-
function
|
|
278
|
-
|
|
277
|
+
function requireKeyboardMarkdown(messages) {
|
|
278
|
+
if (!messages.length)
|
|
279
|
+
throw new EndpointDeliveryError('unsupported_operation', 'QQ payload has no sendable content', 'not_sent');
|
|
280
|
+
for (const message of messages) {
|
|
281
|
+
const parts = Array.isArray(message) ? message : [message];
|
|
282
|
+
if (parts.some(part => typeof part !== 'string' && (part.type === 'keyboard' || part.type === 'button'))
|
|
283
|
+
&& !parts.some(part => typeof part !== 'string' && part.type === 'markdown')) {
|
|
284
|
+
throw new EndpointDeliveryError('unsupported_operation', 'QQ keyboard requires Markdown in the same physical message', 'not_sent');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return messages;
|
|
279
288
|
}
|
|
280
289
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* 1. 公网 URL 图片且已知尺寸:按 QQ markdown 语法 `` 嵌入同一
|
|
286
|
-
* markdown 段(开放平台会下载转存),仍为一条消息;
|
|
287
|
-
* 2. 其余媒体(base64 / 本地渲染图:v2 /files 上传只返回 file_info,拿不到 URL;或尺寸
|
|
288
|
-
* 未知、模板 markdown 无法拼接)降级为独立富媒体消息先发,markdown + 按钮作为最后一条
|
|
289
|
-
* (返回其消息 ID)。reply 段随每条消息携带,保持被动回复。
|
|
290
|
-
* 不含媒体与 markdown/按钮冲突时与 formatOutbound 一致(单条)。
|
|
290
|
+
* Preserve the original segment order before formatOutbound merges Markdown.
|
|
291
|
+
* Public images with known dimensions become inline Markdown at their original
|
|
292
|
+
* position. Other media form physical-message barriers; reply routing is copied
|
|
293
|
+
* to every resulting message, without moving text across a media barrier.
|
|
291
294
|
*/
|
|
292
295
|
export function formatOutboundMessages(payload, options = {}) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
embedded.add(part);
|
|
317
|
-
});
|
|
318
|
-
if (embedded.size > 0) {
|
|
319
|
-
merged = { type: 'markdown', data: { content: [...before, markdownContent, ...after].filter((piece) => piece.trim()).join('\n\n') } };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const replies = elems.filter(isElemOf(new Set(['reply'])));
|
|
323
|
-
const separate = [];
|
|
324
|
-
const rest = [];
|
|
325
|
-
let mergedPlaced = false;
|
|
326
|
-
for (const part of elems) {
|
|
327
|
-
if (merged && (part === markdownElem || (!markdownElem && embedded.has(part)))) {
|
|
328
|
-
if (!mergedPlaced)
|
|
329
|
-
rest.push(merged);
|
|
330
|
-
mergedPlaced = true;
|
|
331
|
-
}
|
|
332
|
-
else if (embedded.has(part)) {
|
|
296
|
+
const items = toSegmentList(payload);
|
|
297
|
+
const rich = items.some(item => typeof item !== 'string' && MARKDOWN_MESSAGE_TYPES.has(item.type));
|
|
298
|
+
const media = items.some(item => typeof item !== 'string' && SPLIT_MEDIA_TYPES.has(item.type));
|
|
299
|
+
if (!rich || !media)
|
|
300
|
+
return requireKeyboardMarkdown([formatOutbound(payload)]);
|
|
301
|
+
const replies = items.filter(item => typeof item !== 'string' && item.type === 'reply');
|
|
302
|
+
const templates = items.some(item => typeof item !== 'string' && item.type === 'markdown' && markdownTemplateId(item.data ?? {}));
|
|
303
|
+
const messages = [];
|
|
304
|
+
let pending = [];
|
|
305
|
+
const append = (items) => {
|
|
306
|
+
const message = formatOutbound(items);
|
|
307
|
+
const parts = Array.isArray(message) ? message : [message];
|
|
308
|
+
if (parts.some(part => typeof part === 'string' ? part.length > 0 : part.type !== 'reply'))
|
|
309
|
+
messages.push(message);
|
|
310
|
+
};
|
|
311
|
+
const flush = () => {
|
|
312
|
+
if (!pending.length)
|
|
313
|
+
return;
|
|
314
|
+
append([...replies, ...pending]);
|
|
315
|
+
pending = [];
|
|
316
|
+
};
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
if (typeof item !== 'string' && item.type === 'reply')
|
|
333
319
|
continue;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
320
|
+
if (typeof item !== 'string' && SPLIT_MEDIA_TYPES.has(item.type)) {
|
|
321
|
+
const source = item.type === 'image' && isMediaRef(item.data?.media) ? item.data.media : undefined;
|
|
322
|
+
const size = !templates && source?.kind === 'url' ? options.imageSizes?.get(source.value) : undefined;
|
|
323
|
+
if (source?.kind === 'url' && size) {
|
|
324
|
+
pending.push({ type: 'markdown', data: { content: `` } });
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
flush();
|
|
328
|
+
append([...replies, item]);
|
|
329
|
+
}
|
|
337
330
|
}
|
|
338
331
|
else {
|
|
339
|
-
|
|
332
|
+
pending.push(item);
|
|
340
333
|
}
|
|
341
334
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
rest.length === 1 ? rest[0] : rest,
|
|
345
|
-
];
|
|
335
|
+
flush();
|
|
336
|
+
return requireKeyboardMarkdown(messages);
|
|
346
337
|
}
|
|
347
338
|
/**
|
|
348
339
|
* 预先探测需要嵌入 markdown 的公网图片尺寸:仅当 payload 同时含图片与 markdown/按钮时,
|
package/lib/passive-reply.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare class QqPassiveReplyLedger {
|
|
|
37
37
|
* newest usable source in the conversation. Undefined when only an active message is possible.
|
|
38
38
|
*/
|
|
39
39
|
acquire(kind: QqOutboundKind, conversationId: string, preferred?: QqReplySource): PassiveReplyGrant | undefined;
|
|
40
|
+
/** Local stream preflight must not reserve a reply before any content exists. */
|
|
41
|
+
hasUsableSource(kind: QqOutboundKind, conversationId: string, preferred?: QqReplySource): boolean;
|
|
40
42
|
/** The platform said this source can no longer be replied to (expired, over the limit, invalid id). */
|
|
41
43
|
exhaust(kind: QqOutboundKind, conversationId: string, source: QqReplySource): void;
|
|
42
44
|
/** Drop conversations whose every source is past its window. */
|
package/lib/passive-reply.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/lib/qq-console-spec.js
CHANGED
|
@@ -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 {};
|