@zhin.js/adapter-qq 1.1.18 → 1.1.19
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 +39 -0
- package/README.md +21 -18
- package/adapters/qq/index.js +4 -38
- package/adapters/qq/index.ts +3 -46
- package/agents/qq/conversation.js +1 -1
- package/agents/qq/conversation.ts +1 -1
- package/agents/qq/skills/qq/tools/add_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/add_role/index.ts +2 -1
- package/agents/qq/skills/qq/tools/channel_info/index.js +1 -1
- package/agents/qq/skills/qq/tools/channel_info/index.ts +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.js +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.ts +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.js +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +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/tools/list_channels/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_channels/index.ts +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.ts +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.js +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.ts +1 -1
- package/agents/qq/skills/qq/tools/remove_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/remove_role/index.ts +2 -1
- 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-directory/tools/list_guilds/index.js +1 -1
- package/agents/qq/skills/qq-directory/tools/list_guilds/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +2 -2
- 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/set_mute/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +2 -2
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +1 -1
- package/commands/qq/endpoint/definition.js +41 -36
- package/commands/qq/endpoint/definition.ts +41 -67
- package/lib/adapter.d.ts +13 -0
- package/lib/adapter.js +27 -0
- package/lib/client.d.ts +7 -9
- package/lib/client.js +1 -2
- package/lib/console.d.ts +1 -6
- package/lib/console.js +29 -20
- package/lib/endpoint.d.ts +8 -27
- package/lib/endpoint.js +218 -165
- package/lib/group-admin.d.ts +14 -9
- package/lib/group-admin.js +21 -20
- package/lib/i18n.js +4 -4
- package/lib/image-size.d.ts +6 -1
- package/lib/image-size.js +2 -3
- package/lib/index.d.ts +5 -4
- package/lib/index.js +3 -3
- package/lib/interaction.js +1 -1
- package/lib/outbound.d.ts +4 -1
- package/lib/outbound.js +3 -2
- package/lib/protocol.d.ts +2 -0
- package/lib/qq-console-spec.js +12 -1
- package/lib/qq-console.d.ts +3 -4
- package/lib/qq-console.js +16 -6
- package/lib/qq-endpoint-write.d.ts +6 -10
- package/lib/qq-endpoint-write.js +5 -32
- package/lib/qq-runtime-state.d.ts +11 -10
- package/lib/qq-runtime-state.js +4 -5
- package/lib/qq-send.d.ts +7 -14
- package/lib/qq-send.js +20 -6
- package/lib/webhook.d.ts +3 -6
- package/lib/webhook.js +6 -49
- package/lib/ws.d.ts +10 -33
- package/lib/ws.js +21 -70
- package/middlewares/bot-kind-reply/index.js +1 -2
- package/middlewares/bot-kind-reply/index.ts +0 -2
- package/package.json +19 -19
- package/plugin.js +5 -5
- package/src/adapter.ts +23 -0
- package/src/client.ts +4 -12
- package/src/console.ts +27 -20
- package/src/endpoint.ts +218 -160
- package/src/group-admin.ts +28 -26
- package/src/i18n.ts +4 -4
- package/src/image-size.ts +7 -3
- package/src/index.ts +5 -5
- package/src/interaction.ts +1 -1
- package/src/outbound.ts +3 -2
- package/src/protocol.ts +2 -0
- package/src/qq-console-spec.ts +12 -1
- package/src/qq-console.ts +14 -8
- package/src/qq-endpoint-write.ts +10 -38
- package/src/qq-runtime-state.ts +11 -16
- package/src/qq-send.ts +28 -12
- package/src/webhook.ts +7 -57
- package/src/ws.ts +27 -105
package/lib/group-admin.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 没有「机器人所在群列表」「好友列表」「退群」「设管理员」「群公告」等接口。
|
|
8
8
|
*/
|
|
9
9
|
import type { GroupBotState, GroupMemberInfo } from 'qq-official-bot';
|
|
10
|
-
import type
|
|
10
|
+
import { type EndpointGroupRequest, type EndpointMemberRequest, type EndpointMuteRequest, type EndpointGroupInspectionRequest, type EndpointMemberInspectionRequest, type EndpointModerationRequest, type GroupMemberProfile, type GroupModerationMember, type GroupSceneFacts } from 'zhin.js/adapter';
|
|
11
11
|
import type { QqOfficialApi } from './ws.js';
|
|
12
12
|
/** 官方 restrict_chat_setting 上限:30 天。 */
|
|
13
13
|
export declare const QQ_GROUP_MUTE_MAX_SECONDS: number;
|
|
@@ -19,18 +19,23 @@ export declare function parseQqTime(value: unknown, { allowFuture }?: {
|
|
|
19
19
|
allowFuture?: boolean | undefined;
|
|
20
20
|
}): number | undefined;
|
|
21
21
|
/** 只读、实时:机器人在群里的状态(角色、是否允许主动消息、接收设置)。 */
|
|
22
|
-
export declare function qqGroupBotState(api: Partial<QqOfficialApi> | undefined, groupId:
|
|
23
|
-
export declare function qqGroupMember(api: Partial<QqOfficialApi> | undefined, groupId
|
|
22
|
+
export declare function qqGroupBotState(api: Partial<QqOfficialApi> | undefined, { groupId, signal }: EndpointGroupRequest): Promise<GroupBotState>;
|
|
23
|
+
export declare function qqGroupMember(api: Partial<QqOfficialApi> | undefined, { groupId, userId, signal }: EndpointMemberRequest): Promise<GroupMemberInfo>;
|
|
24
24
|
/** 禁言 / 解除禁言一个普通成员;0 = 解除。 */
|
|
25
|
-
export declare function muteQqGroupMember(api: Partial<QqOfficialApi> | undefined, groupId
|
|
25
|
+
export declare function muteQqGroupMember(api: Partial<QqOfficialApi> | undefined, { groupId, userId, durationSeconds, signal }: EndpointMuteRequest): Promise<void>;
|
|
26
26
|
/** 移出一个成员;可选同时加入群黑名单。平台未确认 success 时抛错,不把未确认当成功。 */
|
|
27
|
-
export declare function kickQqGroupMember(api: Partial<QqOfficialApi> | undefined, groupId
|
|
27
|
+
export declare function kickQqGroupMember(api: Partial<QqOfficialApi> | undefined, { groupId, userId, addToBlacklist, signal }: EndpointMemberRequest & {
|
|
28
|
+
readonly addToBlacklist?: boolean;
|
|
29
|
+
}): Promise<{
|
|
28
30
|
blacklistFailed: boolean;
|
|
29
31
|
}>;
|
|
30
32
|
/** 自动审核用:机器人与目标成员的实时角色;平台只允许管理员处置普通成员(且非机器人)。 */
|
|
31
|
-
export declare function inspectQqGroupMember(api: Partial<QqOfficialApi> | undefined, groupId
|
|
32
|
-
export declare function moderateQqGroupMember(api
|
|
33
|
+
export declare function inspectQqGroupMember(api: Partial<QqOfficialApi> | undefined, { groupId, userId, signal }: EndpointMemberInspectionRequest): Promise<GroupModerationMember>;
|
|
34
|
+
export declare function moderateQqGroupMember({ api, isActive }: {
|
|
35
|
+
readonly api: Partial<QqOfficialApi> | undefined;
|
|
36
|
+
readonly isActive: () => boolean;
|
|
37
|
+
}, { groupId, userId, action, signal }: EndpointModerationRequest): Promise<void>;
|
|
33
38
|
/** ADR 0041 场景事实:一次群信息读取;官方群信息不含群主/管理员/全员禁言,留作 unknown。 */
|
|
34
|
-
export declare function inspectQqGroupScene(api: Partial<QqOfficialApi> | undefined, groupId
|
|
39
|
+
export declare function inspectQqGroupScene(api: Partial<QqOfficialApi> | undefined, { groupId, signal }: EndpointGroupInspectionRequest): Promise<GroupSceneFacts>;
|
|
35
40
|
/** ADR 0041 成员事实:一次成员读取(群名片/头衔/禁言到期官方不提供)。 */
|
|
36
|
-
export declare function inspectQqGroupMemberProfile(api: Partial<QqOfficialApi> | undefined, groupId
|
|
41
|
+
export declare function inspectQqGroupMemberProfile(api: Partial<QqOfficialApi> | undefined, { groupId, userId, signal }: EndpointMemberInspectionRequest): Promise<GroupMemberProfile>;
|
package/lib/group-admin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createEndpointOperation, } from 'zhin.js/adapter';
|
|
1
2
|
/** 官方 restrict_chat_setting 上限:30 天。 */
|
|
2
3
|
export const QQ_GROUP_MUTE_MAX_SECONDS = 30 * 24 * 60 * 60;
|
|
3
4
|
export function qqGroupService(api) {
|
|
@@ -30,37 +31,37 @@ export function parseQqTime(value, { allowFuture = false } = {}) {
|
|
|
30
31
|
return at;
|
|
31
32
|
}
|
|
32
33
|
/** 只读、实时:机器人在群里的状态(角色、是否允许主动消息、接收设置)。 */
|
|
33
|
-
export async function qqGroupBotState(api, groupId) {
|
|
34
|
-
return qqGroupService(api).getBotState(requireId(groupId, 'group id'));
|
|
34
|
+
export async function qqGroupBotState(api, { groupId, signal }) {
|
|
35
|
+
return createEndpointOperation({ signal }).run(() => qqGroupService(api).getBotState(requireId(groupId, 'group id')));
|
|
35
36
|
}
|
|
36
|
-
export async function qqGroupMember(api, groupId, userId) {
|
|
37
|
-
return qqGroupService(api).getMemberInfo(requireId(groupId, 'group id'), requireId(userId, 'member id'));
|
|
37
|
+
export async function qqGroupMember(api, { groupId, userId, signal }) {
|
|
38
|
+
return createEndpointOperation({ signal }).run(() => qqGroupService(api).getMemberInfo(requireId(groupId, 'group id'), requireId(userId, 'member id')));
|
|
38
39
|
}
|
|
39
40
|
/** 禁言 / 解除禁言一个普通成员;0 = 解除。 */
|
|
40
|
-
export async function muteQqGroupMember(api, groupId, userId, durationSeconds) {
|
|
41
|
+
export async function muteQqGroupMember(api, { groupId, userId, durationSeconds, signal }) {
|
|
41
42
|
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 0 || durationSeconds > QQ_GROUP_MUTE_MAX_SECONDS) {
|
|
42
43
|
throw new RangeError(`QQ group mute duration must be 0..${QQ_GROUP_MUTE_MAX_SECONDS} seconds`);
|
|
43
44
|
}
|
|
44
45
|
const member = requireId(userId, 'member id');
|
|
45
|
-
await qqGroupService(api).setMemberMute(requireId(groupId, 'group id'), [durationSeconds === 0
|
|
46
|
+
await createEndpointOperation({ signal }).run(() => qqGroupService(api).setMemberMute(requireId(groupId, 'group id'), [durationSeconds === 0
|
|
46
47
|
? { op: 'del', member_openid: member, mute_expire_at: '' }
|
|
47
|
-
: { op: 'add', member_openid: member, mute_expire_at: new Date(Date.now() + durationSeconds * 1000).toISOString() }]);
|
|
48
|
+
: { op: 'add', member_openid: member, mute_expire_at: new Date(Date.now() + durationSeconds * 1000).toISOString() }]));
|
|
48
49
|
}
|
|
49
50
|
/** 移出一个成员;可选同时加入群黑名单。平台未确认 success 时抛错,不把未确认当成功。 */
|
|
50
|
-
export async function kickQqGroupMember(api, groupId, userId, addToBlacklist = false) {
|
|
51
|
+
export async function kickQqGroupMember(api, { groupId, userId, addToBlacklist = false, signal }) {
|
|
51
52
|
const member = requireId(userId, 'member id');
|
|
52
|
-
const result = await qqGroupService(api).removeMembers(requireId(groupId, 'group id'), {
|
|
53
|
+
const result = await createEndpointOperation({ signal }).run(() => qqGroupService(api).removeMembers(requireId(groupId, 'group id'), {
|
|
53
54
|
member_openids: [member],
|
|
54
55
|
...(addToBlacklist ? { add_to_member_blacklist: true } : {}),
|
|
55
|
-
});
|
|
56
|
+
}));
|
|
56
57
|
if (result?.remove_members_result !== 'success')
|
|
57
58
|
throw new Error('QQ did not confirm the member removal');
|
|
58
59
|
return { blacklistFailed: addToBlacklist && (result.add_to_member_blacklist_fail_openids ?? []).includes(member) };
|
|
59
60
|
}
|
|
60
61
|
/** 自动审核用:机器人与目标成员的实时角色;平台只允许管理员处置普通成员(且非机器人)。 */
|
|
61
|
-
export async function inspectQqGroupMember(api, groupId, userId, signal) {
|
|
62
|
+
export async function inspectQqGroupMember(api, { groupId, userId, signal }) {
|
|
62
63
|
signal.throwIfAborted();
|
|
63
|
-
const [self, member] = await Promise.all([qqGroupBotState(api, groupId), qqGroupMember(api, groupId, userId)]);
|
|
64
|
+
const [self, member] = await Promise.all([qqGroupBotState(api, { groupId, signal }), qqGroupMember(api, { groupId, userId, signal })]);
|
|
64
65
|
signal.throwIfAborted();
|
|
65
66
|
const role = member.member_openid === userId ? platformRole(member.member_role) : undefined;
|
|
66
67
|
const joinedAt = parseQqTime(member.joined_at);
|
|
@@ -71,8 +72,8 @@ export async function inspectQqGroupMember(api, groupId, userId, signal) {
|
|
|
71
72
|
...(joinedAt ? { joinedAt } : {}),
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
|
-
export async function moderateQqGroupMember(api, groupId, userId, action, signal
|
|
75
|
-
const member = await inspectQqGroupMember(api, groupId, userId, signal);
|
|
75
|
+
export async function moderateQqGroupMember({ api, isActive }, { groupId, userId, action, signal }) {
|
|
76
|
+
const member = await inspectQqGroupMember(api, { groupId: groupId, userId: userId, signal: signal });
|
|
76
77
|
signal.throwIfAborted();
|
|
77
78
|
if (!isActive() || !member.canModerate)
|
|
78
79
|
throw new Error('Group moderation permission is unavailable');
|
|
@@ -82,18 +83,18 @@ export async function moderateQqGroupMember(api, groupId, userId, action, signal
|
|
|
82
83
|
|| Date.now() - member.joinedAt > action.maxMembershipAgeMs) {
|
|
83
84
|
throw new Error('Group membership no longer matches the approved newcomer window');
|
|
84
85
|
}
|
|
85
|
-
await kickQqGroupMember(api, groupId, userId);
|
|
86
|
+
await kickQqGroupMember(api, { groupId, userId, signal });
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
if (!Number.isSafeInteger(action.durationSeconds) || action.durationSeconds <= 0) {
|
|
89
90
|
throw new Error('Invalid group mute duration');
|
|
90
91
|
}
|
|
91
|
-
await muteQqGroupMember(api, groupId, userId, Math.min(action.durationSeconds, QQ_GROUP_MUTE_MAX_SECONDS));
|
|
92
|
+
await muteQqGroupMember(api, { groupId: groupId, userId: userId, durationSeconds: Math.min(action.durationSeconds, QQ_GROUP_MUTE_MAX_SECONDS), signal });
|
|
92
93
|
}
|
|
93
94
|
/** ADR 0041 场景事实:一次群信息读取;官方群信息不含群主/管理员/全员禁言,留作 unknown。 */
|
|
94
|
-
export async function inspectQqGroupScene(api, groupId, signal) {
|
|
95
|
+
export async function inspectQqGroupScene(api, { groupId, signal }) {
|
|
95
96
|
signal.throwIfAborted();
|
|
96
|
-
const info = await qqGroupService(api).getInfo(requireId(groupId, 'group id'));
|
|
97
|
+
const info = await createEndpointOperation({ signal }).run(() => qqGroupService(api).getInfo(requireId(groupId, 'group id')));
|
|
97
98
|
signal.throwIfAborted();
|
|
98
99
|
if (info?.group_openid && info.group_openid !== groupId)
|
|
99
100
|
return {};
|
|
@@ -102,9 +103,9 @@ export async function inspectQqGroupScene(api, groupId, signal) {
|
|
|
102
103
|
return { ...(name ? { name } : {}), ...(memberCount ? { memberCount } : {}) };
|
|
103
104
|
}
|
|
104
105
|
/** ADR 0041 成员事实:一次成员读取(群名片/头衔/禁言到期官方不提供)。 */
|
|
105
|
-
export async function inspectQqGroupMemberProfile(api, groupId, userId, signal) {
|
|
106
|
+
export async function inspectQqGroupMemberProfile(api, { groupId, userId, signal }) {
|
|
106
107
|
signal.throwIfAborted();
|
|
107
|
-
const member = await qqGroupMember(api, groupId, userId);
|
|
108
|
+
const member = await qqGroupMember(api, { groupId, userId, signal });
|
|
108
109
|
signal.throwIfAborted();
|
|
109
110
|
if (member.member_openid !== userId)
|
|
110
111
|
return {};
|
package/lib/i18n.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createMessages } from 'zhin.js/i18n';
|
|
|
2
2
|
export const messages = createMessages({
|
|
3
3
|
bindTaskFailed: { 'zh-CN': '获取绑定任务失败: {error}', en: 'Could not create the binding task: {error}' },
|
|
4
4
|
"kindPrompt": {
|
|
5
|
-
"zh-CN": "请回复 public(公域)或 private
|
|
6
|
-
"en": "Reply public or private to save
|
|
5
|
+
"zh-CN": "请回复 public(公域)或 private(私域),确认后保存账号;需手动启动。",
|
|
6
|
+
"en": "Reply public or private to save the account, then start it explicitly."
|
|
7
7
|
},
|
|
8
8
|
"cancelHint": {
|
|
9
9
|
"zh-CN": "也可发 {command} 取消。",
|
|
@@ -18,8 +18,8 @@ export const messages = createMessages({
|
|
|
18
18
|
"en": "⚠️ Endpoint “{id}” is waiting for public/private selection"
|
|
19
19
|
},
|
|
20
20
|
"endpointSaved": {
|
|
21
|
-
"zh-CN": "✅ 已按 botKind={kind}
|
|
22
|
-
"en": "✅ Saved
|
|
21
|
+
"zh-CN": "✅ 已按 botKind={kind} 保存账号「{id}」。\nintents: {intents}。\n尚未启动,请显式启动此账号。",
|
|
22
|
+
"en": "✅ Saved account “{id}” with botKind={kind}.\nintents: {intents}.\nThe account is inactive; start it explicitly."
|
|
23
23
|
},
|
|
24
24
|
"existingFlow": {
|
|
25
25
|
"zh-CN": "已有进行中的 QQ 机器人绑定或待确认的公域/私域选择,请先取消后再试",
|
package/lib/image-size.d.ts
CHANGED
|
@@ -17,4 +17,9 @@ export type ImageProbeFetch = (url: string, init: {
|
|
|
17
17
|
/** 从图片文件头解析尺寸(PNG / GIF / JPEG / WebP)。 */
|
|
18
18
|
export declare function imageSizeFromBytes(bytes: Uint8Array): QqImageSize | undefined;
|
|
19
19
|
/** 读取公网图片文件头得到尺寸;网络错误、超时或格式不识别时返回 undefined。 */
|
|
20
|
-
export
|
|
20
|
+
export interface ImageProbeOptions {
|
|
21
|
+
readonly fetch?: ImageProbeFetch;
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export declare function probeImageSize(url: string, { fetch: fetchImpl, timeoutMs, signal }?: ImageProbeOptions): Promise<QqImageSize | undefined>;
|
package/lib/image-size.js
CHANGED
|
@@ -82,12 +82,11 @@ async function readHead(response) {
|
|
|
82
82
|
}
|
|
83
83
|
return out;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
export async function probeImageSize(url, fetchImpl = fetch, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
85
|
+
export async function probeImageSize(url, { fetch: fetchImpl = fetch, timeoutMs = PROBE_TIMEOUT_MS, signal } = {}) {
|
|
87
86
|
try {
|
|
88
87
|
const response = await fetchImpl(url, {
|
|
89
88
|
headers: { Range: `bytes=0-${PROBE_BYTES - 1}` },
|
|
90
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
89
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
|
|
91
90
|
});
|
|
92
91
|
if (!response.ok)
|
|
93
92
|
return undefined;
|
package/lib/index.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, res
|
|
|
2
2
|
export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, type QqBotKind, type QqIntent, } from './qq-intents.js';
|
|
3
3
|
export { formatOutbound, formatOutboundMessages, collectOutboundImageSizes, type FormatOutboundMessagesOptions, resolveMediaFile, type QqOutboundElem, type QqOutboundMessage, } from './outbound.js';
|
|
4
4
|
export { imageSizeFromBytes, probeImageSize, type QqImageSize } from './image-size.js';
|
|
5
|
-
export { QqHttpEndpoint, QqWebsocketEndpoint, type
|
|
6
|
-
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText,
|
|
7
|
-
export { bindQqBotInboundEvents,
|
|
8
|
-
export {
|
|
5
|
+
export { QqHttpEndpoint, QqWebsocketEndpoint, type QqBotTransport, type QqOfficialApi, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
|
|
6
|
+
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, qqHttpBotOptions, type QqWebhookHandler, } from './webhook.js';
|
|
7
|
+
export { bindQqBotInboundEvents, qqWebsocketBotOptions, normalizeQqMessage, } from './ws.js';
|
|
8
|
+
export { type QqClientEventMap } from './client.js';
|
|
9
9
|
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';
|
|
10
|
+
export { QqAdapter } from './adapter.js';
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields,
|
|
|
3
3
|
export { formatOutbound, formatOutboundMessages, collectOutboundImageSizes, resolveMediaFile, } from './outbound.js';
|
|
4
4
|
export { imageSizeFromBytes, probeImageSize } from './image-size.js';
|
|
5
5
|
export { QqHttpEndpoint, QqWebsocketEndpoint, } from './endpoint.js';
|
|
6
|
-
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText,
|
|
7
|
-
export { bindQqBotInboundEvents,
|
|
8
|
-
export { qqClient } from './client.js';
|
|
6
|
+
export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, qqHttpBotOptions, } from './webhook.js';
|
|
7
|
+
export { bindQqBotInboundEvents, qqWebsocketBotOptions, normalizeQqMessage, } from './ws.js';
|
|
9
8
|
export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';
|
|
9
|
+
export { QqAdapter } from './adapter.js';
|
package/lib/interaction.js
CHANGED
|
@@ -12,5 +12,5 @@ export function normalizeQqInteraction(raw) {
|
|
|
12
12
|
if (!kind || !id || !actor || (kind === 'channel' && !event.guild_id))
|
|
13
13
|
return null;
|
|
14
14
|
const sourceMessageId = typeof resolved.message_id === 'string' && resolved.message_id ? resolved.message_id : undefined;
|
|
15
|
-
return { message: { id: event.notice_id, content: '', channelKind: kind, channelId: id, authorId: actor, authorName: '', timestamp: Date.now(), ...(event.guild_id ? { guildId: event.guild_id } : {}), segments: [{ type: 'action', data: { payload: resolved.button_data } }] }, ...(sourceMessageId ? { sourceMessageId } : {}), ack: () => event.reply(0) };
|
|
15
|
+
return { message: { event: raw, id: event.notice_id, content: '', channelKind: kind, channelId: id, authorId: actor, authorName: '', timestamp: Date.now(), ...(event.guild_id ? { guildId: event.guild_id } : {}), segments: [{ type: 'action', data: { payload: resolved.button_data } }] }, ...(sourceMessageId ? { sourceMessageId } : {}), ack: () => event.reply(0) };
|
|
16
16
|
}
|
package/lib/outbound.d.ts
CHANGED
|
@@ -37,4 +37,7 @@ export declare function formatOutboundMessages(payload: unknown, options?: Forma
|
|
|
37
37
|
* 预先探测需要嵌入 markdown 的公网图片尺寸:仅当 payload 同时含图片与 markdown/按钮时,
|
|
38
38
|
* 对 http(s) URL 图片并发读取文件头;失败的图片不入表,formatOutboundMessages 会降级。
|
|
39
39
|
*/
|
|
40
|
-
export declare function collectOutboundImageSizes(payload: unknown, probe
|
|
40
|
+
export declare function collectOutboundImageSizes(payload: unknown, { probe, signal }?: {
|
|
41
|
+
readonly probe?: (url: string) => Promise<QqImageSize | undefined>;
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
}): Promise<Map<string, QqImageSize>>;
|
package/lib/outbound.js
CHANGED
|
@@ -348,7 +348,7 @@ export function formatOutboundMessages(payload, options = {}) {
|
|
|
348
348
|
* 预先探测需要嵌入 markdown 的公网图片尺寸:仅当 payload 同时含图片与 markdown/按钮时,
|
|
349
349
|
* 对 http(s) URL 图片并发读取文件头;失败的图片不入表,formatOutboundMessages 会降级。
|
|
350
350
|
*/
|
|
351
|
-
export async function collectOutboundImageSizes(payload, probe
|
|
351
|
+
export async function collectOutboundImageSizes(payload, { probe, signal } = {}) {
|
|
352
352
|
const sizes = new Map();
|
|
353
353
|
const segments = toSegmentList(payload).filter((item) => typeof item !== 'string');
|
|
354
354
|
if (!segments.some((item) => MARKDOWN_MESSAGE_TYPES.has(item.type)))
|
|
@@ -360,7 +360,8 @@ export async function collectOutboundImageSizes(payload, probe = (url) => probeI
|
|
|
360
360
|
urls.add(media.value);
|
|
361
361
|
}
|
|
362
362
|
await Promise.all([...urls].map(async (url) => {
|
|
363
|
-
|
|
363
|
+
signal?.throwIfAborted();
|
|
364
|
+
const size = await (probe ? probe(url) : probeImageSize(url, { signal }));
|
|
364
365
|
if (size)
|
|
365
366
|
sizes.set(url, size);
|
|
366
367
|
}));
|
package/lib/protocol.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface ResolvedQqHttpConfig {
|
|
|
46
46
|
export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
|
|
47
47
|
export type QqChannelKind = 'private' | 'group' | 'channel' | 'direct';
|
|
48
48
|
export interface QqInboundMessage {
|
|
49
|
+
readonly event?: unknown;
|
|
50
|
+
readonly platformSentAt?: number;
|
|
49
51
|
readonly id: string;
|
|
50
52
|
readonly content: string;
|
|
51
53
|
readonly channelKind: QqChannelKind;
|
package/lib/qq-console-spec.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
export const qqConsoleSpec = {
|
|
2
2
|
adapterKey: 'qq',
|
|
3
3
|
adapterDisplayName: 'QQ',
|
|
4
|
-
fields: [
|
|
4
|
+
fields: [
|
|
5
|
+
{ key: 'appid', required: true, description: 'QQ appid' },
|
|
6
|
+
{ key: 'secret', required: true, env: true, description: 'QQ app secret' },
|
|
7
|
+
{ key: 'mode', description: 'websocket, webhook or middleware' },
|
|
8
|
+
{ key: 'sandbox', type: 'boolean', description: 'QQ sandbox environment' },
|
|
9
|
+
{ key: 'botKind', description: 'public or private; existing explicit intents remain authoritative' },
|
|
10
|
+
{ key: 'accessTokenUrl', description: 'Configured access-token API URL' },
|
|
11
|
+
{ key: 'gatewayUrl', description: 'Configured Gateway discovery URL' },
|
|
12
|
+
{ key: 'webhookPath', description: 'Local webhook route' },
|
|
13
|
+
{ key: 'streamProxy.port', type: 'number', description: 'Configured Gateway loopback proxy port' },
|
|
14
|
+
{ key: 'streamProxy.serverName', description: 'Expected Gateway TLS hostname' },
|
|
15
|
+
],
|
|
5
16
|
canAdd: false,
|
|
6
17
|
canRemove: true,
|
|
7
18
|
docsUrl: 'https://zhin.js.org/adapters/qq',
|
package/lib/qq-console.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { type QqEndpointWriteResult } from './qq-endpoint-write.js';
|
|
1
|
+
import { type QqEndpointWriteResult, type QqEndpointSavePort } from './qq-endpoint-write.js';
|
|
2
2
|
import type { QqRuntimeState } from './qq-runtime-state.js';
|
|
3
|
-
import type { EndpointConfigurationStore } from 'zhin.js/adapter';
|
|
4
3
|
/** 启动 Console 扫码;resolve 时二维码链接已就绪。 */
|
|
5
4
|
export declare function startQqConsoleBind(state: QqRuntimeState, id?: string): Promise<{
|
|
6
5
|
readonly url: string;
|
|
7
6
|
}>;
|
|
8
|
-
/** 扫码成功后选择公域 /
|
|
9
|
-
export declare function completeQqConsoleBind(state: QqRuntimeState,
|
|
7
|
+
/** 扫码成功后选择公域 / 私域并保存未启动账号。 */
|
|
8
|
+
export declare function completeQqConsoleBind(state: QqRuntimeState, save: QqEndpointSavePort, kindValue: unknown): Promise<QqEndpointWriteResult>;
|
|
10
9
|
/** 取消 Console 绑定(也可用于收拾聊天命令发起的流程)。 */
|
|
11
10
|
export declare function cancelQqConsoleBind(state: QqRuntimeState): boolean;
|
package/lib/qq-console.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* 搬到 Console 页面。
|
|
4
4
|
*
|
|
5
5
|
* - `start` 返回扫码链接;后续状态写入 `state.consoleBind`,页面轮询 status。
|
|
6
|
-
* - 扫码成功后凭据只留在内存,等页面选择公域 /
|
|
6
|
+
* - 扫码成功后凭据只留在内存,等页面选择公域 / 私域再通过捕获的所属 Adapter CAS 保存。
|
|
7
7
|
* - 与聊天命令共享 `state.bindFlow` 单例,同一时间只允许一个绑定流程。
|
|
8
8
|
*/
|
|
9
9
|
import { PluginConsoleRpcError } from 'zhin.js';
|
|
10
|
+
import { AdapterManagementError } from 'zhin.js/adapter';
|
|
10
11
|
import { startQqBindFlow } from './qq-bind-flow.js';
|
|
11
12
|
import { writeQqEndpointWithBotKind } from './qq-endpoint-write.js';
|
|
12
13
|
import { parseQqBotKind } from './qq-intents.js';
|
|
@@ -81,8 +82,8 @@ export function startQqConsoleBind(state, id) {
|
|
|
81
82
|
state.bindFlow = { id: endpointId, stop, locale: 'zh-CN' };
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
|
-
/** 扫码成功后选择公域 /
|
|
85
|
-
export async function completeQqConsoleBind(state,
|
|
85
|
+
/** 扫码成功后选择公域 / 私域并保存未启动账号。 */
|
|
86
|
+
export async function completeQqConsoleBind(state, save, kindValue) {
|
|
86
87
|
const pending = state.consoleBind?.pending;
|
|
87
88
|
if (!pending) {
|
|
88
89
|
throw new PluginConsoleRpcError('conflict', '还没有扫码成功,请先扫码绑定');
|
|
@@ -92,12 +93,20 @@ export async function completeQqConsoleBind(state, store, kindValue) {
|
|
|
92
93
|
throw new PluginConsoleRpcError('invalid_input', '请选择公域(public)或私域(private)');
|
|
93
94
|
}
|
|
94
95
|
try {
|
|
95
|
-
const result = await writeQqEndpointWithBotKind({ ...pending, botKind },
|
|
96
|
+
const result = await writeQqEndpointWithBotKind({ ...pending, botKind }, save);
|
|
96
97
|
state.consoleBind = null;
|
|
98
|
+
state.consoleAdmission = null;
|
|
97
99
|
return result;
|
|
98
100
|
}
|
|
99
|
-
catch {
|
|
100
|
-
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof PluginConsoleRpcError)
|
|
103
|
+
throw error;
|
|
104
|
+
const code = error instanceof AdapterManagementError ? error.code : 'unavailable';
|
|
105
|
+
if (code === 'commit_unknown')
|
|
106
|
+
throw new PluginConsoleRpcError('commit_unknown', '提交结果未知,请刷新核实,勿重复提交');
|
|
107
|
+
if (code === 'conflict' || code === 'duplicate_entry' || code === 'closed')
|
|
108
|
+
throw new PluginConsoleRpcError('conflict', '账号清单已变化,请刷新核实');
|
|
109
|
+
throw new PluginConsoleRpcError('unavailable', '账号配置未能保存,请刷新状态');
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
/** 取消 Console 绑定(也可用于收拾聊天命令发起的流程)。 */
|
|
@@ -112,5 +121,6 @@ export function cancelQqConsoleBind(state) {
|
|
|
112
121
|
state.bindFlow = null;
|
|
113
122
|
state.pendingBotKind = null;
|
|
114
123
|
state.consoleBind = null;
|
|
124
|
+
state.consoleAdmission = null;
|
|
115
125
|
return active;
|
|
116
126
|
}
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* 配置里只保留 `${ENV}` 引用,并按公域 / 私域展开 intents。
|
|
4
|
-
*
|
|
5
|
-
* 命令(`qq endpoint add`)与 Console 扫码绑定共用这一处实现。
|
|
6
|
-
*/
|
|
7
|
-
import { type EndpointConfigurationStore } from 'zhin.js/adapter';
|
|
1
|
+
/** A validated QR result is saved through its owning Adapter's single CAS admission. */
|
|
2
|
+
import type { AdapterMutationResult } from 'zhin.js/adapter';
|
|
8
3
|
import { type QqBotKind } from './qq-intents.js';
|
|
4
|
+
import type { QqEndpointConfig } from './protocol.js';
|
|
9
5
|
export interface QqEndpointWriteInput {
|
|
10
6
|
readonly endpointId: string;
|
|
11
7
|
readonly appId: string;
|
|
12
8
|
readonly appSecret: string;
|
|
13
9
|
readonly botKind: QqBotKind;
|
|
14
10
|
}
|
|
11
|
+
export type QqEndpointSavePort = (configuration: QqEndpointConfig) => Promise<AdapterMutationResult>;
|
|
15
12
|
export interface QqEndpointWriteResult {
|
|
16
|
-
readonly filePath: string;
|
|
17
13
|
readonly endpointId: string;
|
|
18
14
|
readonly botKind: QqBotKind;
|
|
19
15
|
readonly intents: readonly string[];
|
|
16
|
+
readonly mutation: AdapterMutationResult;
|
|
20
17
|
}
|
|
21
|
-
|
|
22
|
-
export declare function writeQqEndpointWithBotKind(input: QqEndpointWriteInput, store: EndpointConfigurationStore): Promise<QqEndpointWriteResult>;
|
|
18
|
+
export declare function writeQqEndpointWithBotKind(input: QqEndpointWriteInput, save: QqEndpointSavePort): Promise<QqEndpointWriteResult>;
|
package/lib/qq-endpoint-write.js
CHANGED
|
@@ -1,34 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ endpoint 配置写回:扫码成功后把 appId / appSecret 写进 `.env`,
|
|
3
|
-
* 配置里只保留 `${ENV}` 引用,并按公域 / 私域展开 intents。
|
|
4
|
-
*
|
|
5
|
-
* 命令(`qq endpoint add`)与 Console 扫码绑定共用这一处实现。
|
|
6
|
-
*/
|
|
7
|
-
import { buildEndpointEnvKey } from 'zhin.js/adapter';
|
|
8
1
|
import { defaultQqEndpointIntentFields } from './qq-intents.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const { filePath } = await store.add({
|
|
15
|
-
adapterKey: 'qq',
|
|
16
|
-
entry: {
|
|
17
|
-
id: input.endpointId,
|
|
18
|
-
appid: `\${${appidKey}}`,
|
|
19
|
-
secret: `\${${secretKey}}`,
|
|
20
|
-
botKind: intentFields.botKind,
|
|
21
|
-
intents: [...intentFields.intents],
|
|
22
|
-
},
|
|
23
|
-
environment: {
|
|
24
|
-
[appidKey]: input.appId,
|
|
25
|
-
[secretKey]: input.appSecret,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
return {
|
|
29
|
-
filePath,
|
|
30
|
-
endpointId: input.endpointId,
|
|
31
|
-
botKind: intentFields.botKind,
|
|
32
|
-
intents: intentFields.intents,
|
|
33
|
-
};
|
|
2
|
+
export async function writeQqEndpointWithBotKind(input, save) {
|
|
3
|
+
const fields = defaultQqEndpointIntentFields(input.botKind);
|
|
4
|
+
const mutation = await save({ id: input.endpointId, appid: input.appId, secret: input.appSecret,
|
|
5
|
+
botKind: fields.botKind, intents: [...fields.intents] });
|
|
6
|
+
return { endpointId: input.endpointId, botKind: fields.botKind, intents: fields.intents, mutation };
|
|
34
7
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
id: string;
|
|
3
|
-
mode: string;
|
|
4
|
-
}
|
|
1
|
+
import type { QqEndpointSavePort } from './qq-endpoint-write.js';
|
|
5
2
|
export interface QqBindFlowHandle {
|
|
6
3
|
/** 绑定期望的 endpoint id(未指定时为 undefined,成功后取 appId) */
|
|
7
4
|
id?: string;
|
|
@@ -11,10 +8,10 @@ export interface QqBindFlowHandle {
|
|
|
11
8
|
readonly commandHeads?: readonly string[];
|
|
12
9
|
}
|
|
13
10
|
/**
|
|
14
|
-
*
|
|
15
|
-
* (提前写 .env 会触发 HMR,冲掉 pending。)
|
|
11
|
+
* 扫码成功后暂存在内存:确认公域/私域后调用发起时捕获的所属 Adapter CAS。
|
|
16
12
|
*/
|
|
17
13
|
export interface QqPendingBotKind {
|
|
14
|
+
readonly save: QqEndpointSavePort;
|
|
18
15
|
readonly endpointId: string;
|
|
19
16
|
readonly appId: string;
|
|
20
17
|
readonly appSecret: string;
|
|
@@ -42,13 +39,17 @@ export interface QqConsoleBindState {
|
|
|
42
39
|
};
|
|
43
40
|
}
|
|
44
41
|
export interface QqRuntimeState {
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
/** Snapshot checked when the QR flow began, never a second account directory. */
|
|
43
|
+
consoleAdmission: Readonly<{
|
|
44
|
+
adapterId: string;
|
|
45
|
+
generation: number;
|
|
46
|
+
revision: number | null;
|
|
47
|
+
}> | null;
|
|
48
|
+
/** 同一插件 generation 内只允许一个扫码绑定流程 */
|
|
48
49
|
bindFlow: QqBindFlowHandle | null;
|
|
49
50
|
/** 扫码完成后等待 botKind 选择(凭据仅在内存) */
|
|
50
51
|
pendingBotKind: QqPendingBotKind | null;
|
|
51
|
-
/** Console 页面的扫码绑定进度(与 bindFlow
|
|
52
|
+
/** Console 页面的扫码绑定进度(与 bindFlow 共享同一插件瞬态流程)。 */
|
|
52
53
|
consoleBind: QqConsoleBindState | null;
|
|
53
54
|
}
|
|
54
55
|
export declare function createQqRuntimeState(): QqRuntimeState;
|
package/lib/qq-runtime-state.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QQ
|
|
3
|
-
*
|
|
4
|
-
* adapter create 与 commands / middleware 通过 use(qqRuntimeStateToken) 共享。
|
|
2
|
+
* QQ 插件实例的瞬态扫码绑定 / 待选 botKind 流程。
|
|
3
|
+
* 账号目录与 SDK 生命周期只由所属 Adapter 持有;此处不保存 Endpoint Map。
|
|
5
4
|
*/
|
|
6
5
|
import { createToken } from 'zhin.js';
|
|
7
6
|
export function createQqRuntimeState() {
|
|
8
7
|
return {
|
|
9
|
-
|
|
8
|
+
consoleAdmission: null,
|
|
10
9
|
bindFlow: null,
|
|
11
10
|
pendingBotKind: null,
|
|
12
11
|
consoleBind: null,
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
|
-
export const qqRuntimeStateToken = createToken('zhin.qq.runtime-state', 'QQ
|
|
14
|
+
export const qqRuntimeStateToken = createToken('zhin.qq.runtime-state', 'QQ transient QR binding state');
|
package/lib/qq-send.d.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* One QQ outbound send: passive reply when a usable source exists, active message otherwise, with
|
|
3
|
-
* the fallbacks the platform's error codes allow. Shared by the websocket and webhook Endpoints.
|
|
4
|
-
*
|
|
5
|
-
* 1. Each message claims a passive source (explicit reply segment first, else the conversation's
|
|
6
|
-
* newest usable msg_id / event_id) with its own msg_seq.
|
|
7
|
-
* 2. Source unusable (expired, over the limit, invalid id): mark it and send that message once as
|
|
8
|
-
* an active message. Never after a timeout: the first request may have been delivered.
|
|
9
|
-
* 3. Markdown not allowed: resend the same message as plain text.
|
|
10
|
-
* 4. Active message refused (no permission, quota, user switched it off): remember the conversation
|
|
11
|
-
* for a while and fail fast instead of calling the platform again.
|
|
12
|
-
*/
|
|
13
|
-
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
1
|
+
import { type ConversationRef } from '@zhin.js/im-contract';
|
|
14
2
|
import type { QqOutboundMessage } from './outbound.js';
|
|
15
3
|
import { type QqActiveMessageGate, type QqPassiveReplyLedger, type QqReplySource } from './passive-reply.js';
|
|
16
4
|
import type { QqBotTransport } from './ws.js';
|
|
@@ -20,9 +8,14 @@ export interface QqSendContext {
|
|
|
20
8
|
readonly activeGate: QqActiveMessageGate;
|
|
21
9
|
readonly log: (fields: Record<string, string | number | undefined>) => void;
|
|
22
10
|
}
|
|
11
|
+
export interface QqMessagesRequest {
|
|
12
|
+
readonly conversation: ConversationRef;
|
|
13
|
+
readonly messages: readonly QqOutboundMessage[];
|
|
14
|
+
readonly signal?: AbortSignal;
|
|
15
|
+
}
|
|
23
16
|
/** The reply segment the payload already names, as a passive source. */
|
|
24
17
|
export declare function explicitReplySource(message: QqOutboundMessage): QqReplySource | undefined;
|
|
25
18
|
/** Plain-text version of a Markdown message (buttons dropped); undefined when it has no Markdown. */
|
|
26
19
|
export declare function markdownAsText(message: QqOutboundMessage): QqOutboundMessage | undefined;
|
|
27
20
|
/** Sends every message of one delivery in order; returns the last platform result. */
|
|
28
|
-
export declare function sendQqMessages(context: QqSendContext, conversation
|
|
21
|
+
export declare function sendQqMessages(context: QqSendContext, { conversation, messages, signal }: QqMessagesRequest): Promise<unknown>;
|
package/lib/qq-send.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One QQ outbound send: passive reply when a usable source exists, active message otherwise, with
|
|
3
|
+
* the fallbacks the platform's error codes allow. Shared by the websocket and webhook Endpoints.
|
|
4
|
+
*
|
|
5
|
+
* 1. Each message claims a passive source (explicit reply segment first, else the conversation's
|
|
6
|
+
* newest usable msg_id / event_id) with its own msg_seq.
|
|
7
|
+
* 2. Source unusable (expired, over the limit, invalid id): mark it and send that message once as
|
|
8
|
+
* an active message. Never after a timeout: the first request may have been delivered.
|
|
9
|
+
* 3. Markdown not allowed: resend the same message as plain text.
|
|
10
|
+
* 4. Active message refused (no permission, quota, user switched it off): remember the conversation
|
|
11
|
+
* for a while and fail fast instead of calling the platform again.
|
|
12
|
+
*/
|
|
13
|
+
import { createEndpointOperation } from 'zhin.js/adapter';
|
|
1
14
|
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
15
|
import { QQ_ACTIVE_UNAVAILABLE_CODES, QQ_MARKDOWN_DENIED_CODES, QQ_PASSIVE_SOURCE_CODES, } from './passive-reply.js';
|
|
3
16
|
import { qqDeliveryFailure } from './delivery-error.js';
|
|
@@ -39,7 +52,7 @@ export function markdownAsText(message) {
|
|
|
39
52
|
const kept = parts.filter((part) => typeof part !== 'string' && (part.type === 'reply' || part.type === 'at'));
|
|
40
53
|
return text ? [...kept, text] : undefined;
|
|
41
54
|
}
|
|
42
|
-
function dispatch(bot, kind, id, message, source, options) {
|
|
55
|
+
function dispatch(bot, { kind, id, message, source, options }) {
|
|
43
56
|
switch (kind) {
|
|
44
57
|
case 'private': return bot.sendPrivateMessage(id, message, source, options);
|
|
45
58
|
case 'group': return bot.sendGroupMessage(id, message, source, options);
|
|
@@ -54,14 +67,14 @@ function platformCode(error) {
|
|
|
54
67
|
return qqDeliveryFailure(error).diagnostic.platformCode;
|
|
55
68
|
}
|
|
56
69
|
/** Sends one message, passive if possible; returns the platform result. */
|
|
57
|
-
async function sendOne(context, kind, id, message) {
|
|
58
|
-
const passive = (body, grant) => dispatch(context.bot, kind, id, body, grant.source, { msgSeq: grant.msgSeq });
|
|
70
|
+
async function sendOne(context, { kind, id, message }) {
|
|
71
|
+
const passive = (body, grant) => context.operation.run(() => dispatch(context.bot, { kind, id, message: body, source: grant.source, options: { msgSeq: grant.msgSeq } }));
|
|
59
72
|
const active = async (body) => {
|
|
60
73
|
if (context.activeGate.blocked(kind, id)) {
|
|
61
74
|
throw new EndpointDeliveryError('platform_rejected', 'QQ active messages are unavailable for this conversation', 'not_sent');
|
|
62
75
|
}
|
|
63
76
|
try {
|
|
64
|
-
return await dispatch(context.bot, kind, id, body);
|
|
77
|
+
return await context.operation.run(() => dispatch(context.bot, { kind, id, message: body }));
|
|
65
78
|
}
|
|
66
79
|
catch (error) {
|
|
67
80
|
const code = platformCode(error);
|
|
@@ -100,10 +113,11 @@ async function sendOne(context, kind, id, message) {
|
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
/** Sends every message of one delivery in order; returns the last platform result. */
|
|
103
|
-
export async function sendQqMessages(context, conversation, messages) {
|
|
116
|
+
export async function sendQqMessages(context, { conversation, messages, signal }) {
|
|
104
117
|
const kind = qqOutboundKind(conversation);
|
|
118
|
+
const execution = { ...context, operation: createEndpointOperation({ signal }) };
|
|
105
119
|
let result;
|
|
106
120
|
for (const message of messages)
|
|
107
|
-
result = await sendOne(
|
|
121
|
+
result = await sendOne(execution, { kind, id: conversation.id, message });
|
|
108
122
|
return result;
|
|
109
123
|
}
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { Bot, ReceiverMode } from 'qq-official-bot';
|
|
2
3
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
3
4
|
import type { ResolvedQqHttpConfig } from './protocol.js';
|
|
4
|
-
|
|
5
|
-
export type QqHttpBotTransport = QqBotTransport & {
|
|
6
|
-
middleware: (ctx: unknown, next: () => Promise<void>) => Promise<unknown>;
|
|
7
|
-
};
|
|
8
|
-
export type CreateQqHttpBot = (config: ResolvedQqHttpConfig) => QqHttpBotTransport;
|
|
5
|
+
export type QqHttpBotTransport = Bot<ReceiverMode.MIDDLEWARE, 'koa'>;
|
|
9
6
|
export interface QqWebhookHandler {
|
|
10
7
|
readonly config: ResolvedQqHttpConfig;
|
|
11
8
|
getBot(): (QqHttpBotTransport & {
|
|
@@ -15,4 +12,4 @@ export interface QqWebhookHandler {
|
|
|
15
12
|
export declare function registerQqWebhookRoutes(http: HttpHost, handler: QqWebhookHandler): HttpRouteRegistration[];
|
|
16
13
|
export declare function handleQqWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: QqWebhookHandler): Promise<void>;
|
|
17
14
|
export declare function readRequestBodyText(request: IncomingMessage): Promise<string>;
|
|
18
|
-
export declare function
|
|
15
|
+
export declare function qqHttpBotOptions(config: ResolvedQqHttpConfig): ConstructorParameters<typeof Bot<ReceiverMode.MIDDLEWARE, 'koa'>>[0];
|