@zhin.js/adapter-qq 5.0.1 → 5.0.3
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 +60 -0
- package/README.md +52 -408
- package/adapters/qq.ts +45 -0
- package/agent/tools/add_role.ts +23 -0
- package/agent/tools/channel_info.ts +19 -0
- package/agent/tools/create_role.ts +22 -0
- package/agent/tools/list_channels.ts +20 -0
- package/agent/tools/list_guilds.ts +17 -0
- package/agent/tools/list_roles.ts +20 -0
- package/agent/tools/member_detail.ts +20 -0
- package/agent/tools/remove_role.ts +23 -0
- package/lib/endpoint.d.ts +64 -120
- package/lib/endpoint.js +279 -564
- package/lib/index.d.ts +6 -23
- package/lib/index.js +6 -375
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +96 -0
- package/lib/protocol.js +129 -0
- package/lib/qq-agent-deps.d.ts +21 -0
- package/lib/qq-agent-deps.js +30 -0
- package/lib/webhook.d.ts +18 -0
- package/lib/webhook.js +100 -0
- package/lib/ws.d.ts +24 -0
- package/lib/ws.js +118 -0
- package/package.json +47 -33
- package/plugin.ts +13 -0
- package/schema.json +51 -0
- package/src/endpoint.ts +313 -605
- package/src/index.ts +55 -368
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +229 -0
- package/src/qq-agent-deps.ts +60 -0
- package/src/webhook.ts +127 -0
- package/src/ws.ts +172 -0
- package/client/Dashboard.tsx +0 -230
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -30
- package/lib/adapter.d.ts +0 -32
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -82
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/gateway-config.d.ts +0 -14
- package/lib/gateway-config.d.ts.map +0 -1
- package/lib/gateway-config.js +0 -32
- package/lib/gateway-config.js.map +0 -1
- package/lib/group-at-normalize.d.ts +0 -8
- package/lib/group-at-normalize.d.ts.map +0 -1
- package/lib/group-at-normalize.js +0 -68
- package/lib/group-at-normalize.js.map +0 -1
- package/lib/inbound-action.d.ts +0 -15
- package/lib/inbound-action.d.ts.map +0 -1
- package/lib/inbound-action.js +0 -57
- package/lib/inbound-action.js.map +0 -1
- package/lib/inbound-normalize.d.ts +0 -11
- package/lib/inbound-normalize.d.ts.map +0 -1
- package/lib/inbound-normalize.js +0 -47
- package/lib/inbound-normalize.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/mention-enrich.d.ts +0 -12
- package/lib/mention-enrich.d.ts.map +0 -1
- package/lib/mention-enrich.js +0 -52
- package/lib/mention-enrich.js.map +0 -1
- package/lib/outbound-keyboard.d.ts +0 -4
- package/lib/outbound-keyboard.d.ts.map +0 -1
- package/lib/outbound-keyboard.js +0 -96
- package/lib/outbound-keyboard.js.map +0 -1
- package/lib/outbound-markdown.d.ts +0 -24
- package/lib/outbound-markdown.d.ts.map +0 -1
- package/lib/outbound-markdown.js +0 -177
- package/lib/outbound-markdown.js.map +0 -1
- package/lib/outbound-media.d.ts +0 -11
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js +0 -51
- package/lib/outbound-media.js.map +0 -1
- package/lib/outbound-mixed-media.d.ts +0 -21
- package/lib/outbound-mixed-media.d.ts.map +0 -1
- package/lib/outbound-mixed-media.js +0 -78
- package/lib/outbound-mixed-media.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/qq-bind-api.d.ts +0 -37
- package/lib/qq-bind-api.d.ts.map +0 -1
- package/lib/qq-bind-api.js +0 -90
- package/lib/qq-bind-api.js.map +0 -1
- package/lib/qq-bind-flow.d.ts +0 -20
- package/lib/qq-bind-flow.d.ts.map +0 -1
- package/lib/qq-bind-flow.js +0 -86
- package/lib/qq-bind-flow.js.map +0 -1
- package/lib/qq-bind-persist.d.ts +0 -11
- package/lib/qq-bind-persist.d.ts.map +0 -1
- package/lib/qq-bind-persist.js +0 -52
- package/lib/qq-bind-persist.js.map +0 -1
- package/lib/qq-endpoint-manager.d.ts +0 -16
- package/lib/qq-endpoint-manager.d.ts.map +0 -1
- package/lib/qq-endpoint-manager.js +0 -159
- package/lib/qq-endpoint-manager.js.map +0 -1
- package/lib/sdk-version.d.ts +0 -8
- package/lib/sdk-version.d.ts.map +0 -1
- package/lib/sdk-version.js +0 -19
- package/lib/sdk-version.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -45
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -101
- package/src/gateway-config.ts +0 -48
- package/src/group-at-normalize.ts +0 -76
- package/src/inbound-action.ts +0 -87
- package/src/inbound-normalize.ts +0 -57
- package/src/mention-enrich.ts +0 -71
- package/src/outbound-keyboard.ts +0 -118
- package/src/outbound-markdown.ts +0 -203
- package/src/outbound-media.ts +0 -58
- package/src/outbound-mixed-media.ts +0 -101
- package/src/qq-bind-api.ts +0 -145
- package/src/qq-bind-flow.ts +0 -127
- package/src/qq-bind-persist.ts +0 -71
- package/src/qq-endpoint-manager.ts +0 -199
- package/src/sdk-version.ts +0 -21
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -53
- /package/{skills/qq → agent}/PERMITS.md +0 -0
- /package/{skills/qq/SKILL.md → agent/skills/qq.md} +0 -0
package/src/adapter.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ 官方适配器
|
|
3
|
-
*/
|
|
4
|
-
import {
|
|
5
|
-
Adapter,
|
|
6
|
-
Plugin,
|
|
7
|
-
} from "zhin.js";
|
|
8
|
-
import type { Router } from "@zhin.js/host-router";
|
|
9
|
-
import type { EndpointManager } from "zhin.js";
|
|
10
|
-
import { QQEndpoint } from "./endpoint.js";
|
|
11
|
-
import { QqEndpointManager } from "./qq-endpoint-manager.js";
|
|
12
|
-
import type { QQEndpointConfig, ReceiverMode } from "./types.js";
|
|
13
|
-
import type { OutboundRichSegmentPolicy } from "zhin.js";
|
|
14
|
-
import { QQ_AI_OUTBOUND_EXTENSIONS } from "zhin.js";
|
|
15
|
-
|
|
16
|
-
export class QQAdapter extends Adapter<QQEndpoint<ReceiverMode>> {
|
|
17
|
-
static override readonly capabilities = ['inbound', 'outbound'] as const;
|
|
18
|
-
static override outboundRichSegmentPolicy: OutboundRichSegmentPolicy = {
|
|
19
|
-
qrcode: 'image',
|
|
20
|
-
html: 'image',
|
|
21
|
-
markdown: 'origin',
|
|
22
|
-
};
|
|
23
|
-
static override interactivePolicy = 'native' as const;
|
|
24
|
-
static override aiOutboundCapabilities = {
|
|
25
|
-
mentions: true,
|
|
26
|
-
richSegments: ['qrcode', 'html', 'markdown', 'tts'],
|
|
27
|
-
interactive: 'native' as const,
|
|
28
|
-
};
|
|
29
|
-
static override aiOutboundExtensions = QQ_AI_OUTBOUND_EXTENSIONS;
|
|
30
|
-
|
|
31
|
-
#router?: Router;
|
|
32
|
-
#endpointManager?: QqEndpointManager;
|
|
33
|
-
|
|
34
|
-
constructor(plugin: Plugin, router?: Router) {
|
|
35
|
-
super(plugin, "qq", []);
|
|
36
|
-
this.#router = router;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getRouter(): Router | undefined {
|
|
40
|
-
return this.#router;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
createEndpoint(config: QQEndpointConfig<ReceiverMode>): QQEndpoint<ReceiverMode> {
|
|
44
|
-
return new QQEndpoint(this, config);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
override getEndpointManager(): EndpointManager {
|
|
48
|
-
if (!this.#endpointManager) {
|
|
49
|
-
this.#endpointManager = new QqEndpointManager(this);
|
|
50
|
-
}
|
|
51
|
-
return this.#endpointManager;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── ISceneManagement 标准群管方法 ──────────────────────────────────
|
|
55
|
-
|
|
56
|
-
async removeMember(endpointId: string, sceneId: string, userId: string) {
|
|
57
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
58
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
59
|
-
return endpoint.removeGuildMember(sceneId, userId, false);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async muteMember(endpointId: string, sceneId: string, userId: string, duration = 600) {
|
|
63
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
64
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
65
|
-
return endpoint.muteMembers(sceneId, [userId], duration);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async setSceneMuted(endpointId: string, sceneId: string, enable = true) {
|
|
69
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
70
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
71
|
-
return endpoint.muteAll(sceneId, enable ? 600 : 0);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async listMembers(endpointId: string, sceneId: string) {
|
|
75
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
76
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
77
|
-
return endpoint.getGuildMembers(sceneId);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getSceneInfo(endpointId: string, sceneId: string) {
|
|
81
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
82
|
-
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
83
|
-
return endpoint.getGuildInfo(sceneId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── 生命周期 ───────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
async start(): Promise<void> {
|
|
89
|
-
if (!this.#router) {
|
|
90
|
-
this.#router = (this.plugin.inject as (key: string) => Router | undefined)("router");
|
|
91
|
-
}
|
|
92
|
-
(this.plugin.useContext as (key: string, fn: (router: Router) => void) => void)(
|
|
93
|
-
"router",
|
|
94
|
-
(router) => {
|
|
95
|
-
this.#router = router;
|
|
96
|
-
},
|
|
97
|
-
);
|
|
98
|
-
await super.start();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
}
|
package/src/gateway-config.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "@zhin.js/logger";
|
|
2
|
-
import { formatCompact } from "@zhin.js/logger";
|
|
3
|
-
import type { Bot as QQOfficialBot } from "qq-official-bot";
|
|
4
|
-
import { ReceiverMode } from "qq-official-bot";
|
|
5
|
-
import type { QQEndpointConfig } from "./types.js";
|
|
6
|
-
|
|
7
|
-
/** qq-official-bot 默认 token 接口 */
|
|
8
|
-
export const DEFAULT_ACCESS_TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
|
|
9
|
-
/** qq-official-bot 默认 gateway 路径(相对 API baseURL) */
|
|
10
|
-
export const DEFAULT_GATEWAY_URL = "/gateway/bot";
|
|
11
|
-
|
|
12
|
-
type AuthManagerSession = {
|
|
13
|
-
authManager: {
|
|
14
|
-
config: {
|
|
15
|
-
accessTokenUrl?: string;
|
|
16
|
-
gatewayUrl?: string;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 将自定义 gateway / token 地址注入 SDK Auth。
|
|
23
|
-
* SDK 仅在 websocket 模式自动读取配置;middleware / webhook 需在此补丁。
|
|
24
|
-
*/
|
|
25
|
-
export function applyCustomAuthEndpoints(
|
|
26
|
-
endpoint: QQOfficialBot,
|
|
27
|
-
config: Pick<QQEndpointConfig<ReceiverMode>, "mode" | "accessTokenUrl" | "gatewayUrl">,
|
|
28
|
-
logger: Logger,
|
|
29
|
-
): void {
|
|
30
|
-
const { accessTokenUrl, gatewayUrl, mode } = config;
|
|
31
|
-
if (!accessTokenUrl && !gatewayUrl) return;
|
|
32
|
-
|
|
33
|
-
if (mode !== ReceiverMode.WEBSOCKET) {
|
|
34
|
-
const session = endpoint.sessionManager as unknown as AuthManagerSession;
|
|
35
|
-
if (accessTokenUrl) session.authManager.config.accessTokenUrl = accessTokenUrl;
|
|
36
|
-
if (gatewayUrl) session.authManager.config.gatewayUrl = gatewayUrl;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
logger.info(formatCompact({
|
|
40
|
-
op: "qq_gateway",
|
|
41
|
-
mode,
|
|
42
|
-
accessTokenUrl: accessTokenUrl ?? DEFAULT_ACCESS_TOKEN_URL,
|
|
43
|
-
gatewayUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
|
|
44
|
-
note: mode === ReceiverMode.WEBSOCKET
|
|
45
|
-
? "websocket 入站使用 gatewayUrl"
|
|
46
|
-
: "middleware/webhook 仅 accessTokenUrl 生效",
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { MessageElement } from "zhin.js";
|
|
2
|
-
import { readMentionSegmentTarget } from "zhin.js";
|
|
3
|
-
|
|
4
|
-
function segmentAtId(seg: MessageElement): string {
|
|
5
|
-
return readMentionSegmentTarget(seg);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function textMentionsEndpoint(text: string, endpointIds: string[]): boolean {
|
|
9
|
-
for (const id of endpointIds) {
|
|
10
|
-
const re = new RegExp(`@${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
11
|
-
if (re.test(text)) return true;
|
|
12
|
-
}
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function stripInlineAtEndpoint(text: string, endpointIds: string[]): string {
|
|
17
|
-
let result = text;
|
|
18
|
-
for (const id of endpointIds) {
|
|
19
|
-
if (!id) continue;
|
|
20
|
-
const re = new RegExp(`^@${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
21
|
-
result = result.replace(re, "");
|
|
22
|
-
}
|
|
23
|
-
return result.trimStart();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 统一 QQ 群 @ 机器人前缀:
|
|
28
|
-
* - 去掉正文前的 at 段与内联 @,使命令/文本从首段开始匹配
|
|
29
|
-
* - 若曾 @ 机器人,在末尾追加规范 at 段,供 AI @ 触发器识别
|
|
30
|
-
*/
|
|
31
|
-
export function normalizeGroupAtPrefix(
|
|
32
|
-
content: MessageElement[],
|
|
33
|
-
endpointAtIds: string[],
|
|
34
|
-
forceAt: boolean,
|
|
35
|
-
): MessageElement[] {
|
|
36
|
-
const ids = [...new Set(endpointAtIds.map(String).filter(Boolean))];
|
|
37
|
-
if (!ids.length || !Array.isArray(content)) return content;
|
|
38
|
-
|
|
39
|
-
let mentioned = forceAt;
|
|
40
|
-
const body: MessageElement[] = [];
|
|
41
|
-
|
|
42
|
-
for (const seg of content) {
|
|
43
|
-
if (seg.type === "at" || seg.type === "mention") {
|
|
44
|
-
const uid = segmentAtId(seg);
|
|
45
|
-
if (uid && ids.includes(uid)) {
|
|
46
|
-
mentioned = true;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
body.push(seg);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (seg.type === "text" && seg.data && typeof seg.data === "object") {
|
|
53
|
-
const raw = String((seg.data as { text?: string }).text ?? "");
|
|
54
|
-
if (textMentionsEndpoint(raw, ids)) mentioned = true;
|
|
55
|
-
const stripped = stripInlineAtEndpoint(raw, ids);
|
|
56
|
-
if (stripped) {
|
|
57
|
-
body.push({ ...seg, data: { ...(seg.data as object), text: stripped } });
|
|
58
|
-
}
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
body.push(seg);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
while (body.length > 0 && body[0]?.type === "text") {
|
|
65
|
-
const t = String((body[0].data as { text?: string })?.text ?? "").trim();
|
|
66
|
-
if (t) break;
|
|
67
|
-
body.shift();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (mentioned) {
|
|
71
|
-
const canonicalId = ids[0]!;
|
|
72
|
-
body.push({ type: "at", data: { qq: canonicalId, id: canonicalId } });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return body.length ? body : content;
|
|
76
|
-
}
|
package/src/inbound-action.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ 官方互动事件 → 标准 action 段
|
|
3
|
-
* @see https://bot.q.qq.com/wiki/develop/api-v2/server-inter/message/trans/msg-btn.html
|
|
4
|
-
*/
|
|
5
|
-
import type {
|
|
6
|
-
FriendActionNoticeEvent,
|
|
7
|
-
GroupActionNoticeEvent,
|
|
8
|
-
GuildActionNoticeEvent,
|
|
9
|
-
} from 'qq-official-bot';
|
|
10
|
-
import { Message, type MessageSegment, type SendContent } from 'zhin.js';
|
|
11
|
-
|
|
12
|
-
export type QqActionNoticeEvent =
|
|
13
|
-
| FriendActionNoticeEvent
|
|
14
|
-
| GroupActionNoticeEvent
|
|
15
|
-
| GuildActionNoticeEvent;
|
|
16
|
-
|
|
17
|
-
export interface FormatQqActionMessageOptions {
|
|
18
|
-
endpointName: string;
|
|
19
|
-
send: (content: SendContent) => Promise<string>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** `group-{openid}:{msgId}` → 平台 msg_id */
|
|
23
|
-
export function stripQqOutboundMessageId(storedId: string): string {
|
|
24
|
-
const match = /^(?:private|group|channel|direct)-[^:]+:(.+)$/.exec(storedId);
|
|
25
|
-
return match ? match[1]! : storedId;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function prependReply(content: SendContent, msgId: string): MessageSegment[] {
|
|
29
|
-
const items = Array.isArray(content) ? [...content] : [content];
|
|
30
|
-
items.unshift({ type: 'reply', data: { id: msgId } });
|
|
31
|
-
return items as MessageSegment[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createQqActionReplyHandler(
|
|
35
|
-
send: (content: SendContent) => Promise<string>,
|
|
36
|
-
) {
|
|
37
|
-
return async (content: SendContent, quote: boolean | string = false): Promise<string> => {
|
|
38
|
-
const outbound = typeof quote === 'string'
|
|
39
|
-
? prependReply(content, stripQqOutboundMessageId(quote))
|
|
40
|
-
: content;
|
|
41
|
-
return await send(outbound);
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function formatQqActionMessage(
|
|
46
|
-
event: QqActionNoticeEvent,
|
|
47
|
-
options: FormatQqActionMessageOptions,
|
|
48
|
-
): ReturnType<typeof Message.from<QqActionNoticeEvent>> {
|
|
49
|
-
const resolved = event.data?.resolved;
|
|
50
|
-
const buttonId = String(resolved?.button_id ?? '');
|
|
51
|
-
const buttonData = resolved?.button_data != null ? String(resolved.button_data) : '';
|
|
52
|
-
const payload = buttonData || buttonId;
|
|
53
|
-
const sourceMessageId = resolved?.message_id != null ? String(resolved.message_id) : undefined;
|
|
54
|
-
|
|
55
|
-
let channelType: 'private' | 'group' | 'channel' = 'private';
|
|
56
|
-
let channelId = String(event.operator_id ?? '');
|
|
57
|
-
|
|
58
|
-
if (event.notice_type === 'group') {
|
|
59
|
-
channelType = 'group';
|
|
60
|
-
channelId = String(event.group_id);
|
|
61
|
-
} else if (event.notice_type === 'guild') {
|
|
62
|
-
channelType = 'channel';
|
|
63
|
-
channelId = String(event.channel_id);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const senderId = String(event.operator_id ?? '');
|
|
67
|
-
|
|
68
|
-
return Message.from(event, {
|
|
69
|
-
$id: event.notice_id ?? event.event_id ?? `action:${Date.now()}`,
|
|
70
|
-
$adapter: 'qq',
|
|
71
|
-
$endpoint: options.endpointName,
|
|
72
|
-
$sender: { id: senderId, name: senderId },
|
|
73
|
-
$channel: { id: channelId, type: channelType },
|
|
74
|
-
$content: [{
|
|
75
|
-
type: 'action',
|
|
76
|
-
data: {
|
|
77
|
-
id: buttonId,
|
|
78
|
-
payload,
|
|
79
|
-
sourceMessageId,
|
|
80
|
-
},
|
|
81
|
-
}],
|
|
82
|
-
$raw: payload,
|
|
83
|
-
$timestamp: Date.now(),
|
|
84
|
-
$recall: async () => {},
|
|
85
|
-
$reply: createQqActionReplyHandler(options.send),
|
|
86
|
-
});
|
|
87
|
-
}
|
package/src/inbound-normalize.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ 官方 API v2 入站 WebSocket payload 归一化(group_openid → group_id 等)。
|
|
3
|
-
* qq-official-bot 解析层仍按旧字段名取值,未归一化时群 @ 可能解析失败并被 SDK 静默丢弃。
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type QqWsPacket = {
|
|
7
|
-
d?: Record<string, unknown>;
|
|
8
|
-
id?: string;
|
|
9
|
-
t?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const GROUP_MESSAGE_EVENTS = new Set([
|
|
13
|
-
'GROUP_AT_MESSAGE_CREATE',
|
|
14
|
-
'GROUP_MESSAGE_CREATE',
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export function normalizeQqInboundWsPayload(event: string, packet: QqWsPacket): void {
|
|
18
|
-
const d = packet.d;
|
|
19
|
-
if (!d || typeof d !== 'object') return;
|
|
20
|
-
|
|
21
|
-
if (GROUP_MESSAGE_EVENTS.has(event)) {
|
|
22
|
-
if (!d.group_id && typeof d.group_openid === 'string') {
|
|
23
|
-
d.group_id = d.group_openid;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const author = d.author;
|
|
27
|
-
if (author && typeof author === 'object') {
|
|
28
|
-
const a = author as Record<string, unknown>;
|
|
29
|
-
if (!a.id) {
|
|
30
|
-
a.id = a.member_openid ?? a.user_openid ?? a.id;
|
|
31
|
-
}
|
|
32
|
-
if (!a.username && !a.user_name) {
|
|
33
|
-
a.username = a.member_openid ?? a.user_openid ?? 'unknown';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!Array.isArray(d.mentions) && typeof d.content === 'string') {
|
|
38
|
-
const mentions: Array<{ id: string }> = [];
|
|
39
|
-
const re = /<@!?(\d+)>/g;
|
|
40
|
-
let m: RegExpExecArray | null;
|
|
41
|
-
while ((m = re.exec(d.content))) {
|
|
42
|
-
mentions.push({ id: m[1]! });
|
|
43
|
-
}
|
|
44
|
-
if (mentions.length) d.mentions = mentions;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (Array.isArray(d.attachments)) {
|
|
48
|
-
for (const att of d.attachments) {
|
|
49
|
-
if (att && typeof att === 'object' && (att as Record<string, unknown>).url == null) {
|
|
50
|
-
(att as Record<string, unknown>).url = '';
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
d.__zhin_group_at = event === 'GROUP_AT_MESSAGE_CREATE';
|
|
56
|
-
}
|
|
57
|
-
}
|
package/src/mention-enrich.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { Segment } from 'zhin.js';
|
|
2
|
-
import type { QQAdapter } from './adapter.js';
|
|
3
|
-
|
|
4
|
-
type MentionWire = {
|
|
5
|
-
id?: string;
|
|
6
|
-
member_openid?: string;
|
|
7
|
-
username?: string;
|
|
8
|
-
nickname?: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type QQEndpointLike = {
|
|
12
|
-
$id: string;
|
|
13
|
-
$platformUserId?: string;
|
|
14
|
-
$config: { name?: string; appid?: string; nickname?: string };
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function mentionWireId(row: MentionWire): string {
|
|
18
|
-
return String(row.id ?? row.member_openid ?? '').trim();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mentionWireName(row: MentionWire): string | undefined {
|
|
22
|
-
const name = row.username ?? row.nickname;
|
|
23
|
-
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function endpointLookupIds(endpoint: QQEndpointLike): string[] {
|
|
27
|
-
const cfg = endpoint.$config;
|
|
28
|
-
return [
|
|
29
|
-
endpoint.$platformUserId,
|
|
30
|
-
cfg.appid,
|
|
31
|
-
cfg.name,
|
|
32
|
-
endpoint.$id,
|
|
33
|
-
].filter((id): id is string => Boolean(id)).map(String);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function resolveEndpointDisplayName(endpoint: QQEndpointLike): string | undefined {
|
|
37
|
-
const cfg = endpoint.$config;
|
|
38
|
-
return cfg.nickname?.trim() || cfg.name?.trim() || endpoint.$id;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 为 canonical mention 段补齐展示名(平台 payload / endpoint 配置)。 */
|
|
42
|
-
export function enrichCanonicalMentionNames(
|
|
43
|
-
segments: Segment[],
|
|
44
|
-
adapter: QQAdapter,
|
|
45
|
-
wireMentions?: MentionWire[],
|
|
46
|
-
): Segment[] {
|
|
47
|
-
const nameById = new Map<string, string>();
|
|
48
|
-
for (const row of wireMentions ?? []) {
|
|
49
|
-
const id = mentionWireId(row);
|
|
50
|
-
const name = mentionWireName(row);
|
|
51
|
-
if (id && name) nameById.set(id, name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const endpoint of adapter.endpoints.values()) {
|
|
55
|
-
const display = resolveEndpointDisplayName(endpoint);
|
|
56
|
-
if (!display) continue;
|
|
57
|
-
for (const id of endpointLookupIds(endpoint)) {
|
|
58
|
-
if (!nameById.has(id)) nameById.set(id, display);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return segments.map((seg) => {
|
|
63
|
-
if (seg.type !== 'mention') return seg;
|
|
64
|
-
const data = seg.data as { target?: string; name?: string };
|
|
65
|
-
const target = String(data.target ?? '').trim();
|
|
66
|
-
if (!target || data.name) return seg;
|
|
67
|
-
const name = nameById.get(target);
|
|
68
|
-
if (!name) return seg;
|
|
69
|
-
return { ...seg, data: { target, name } };
|
|
70
|
-
});
|
|
71
|
-
}
|
package/src/outbound-keyboard.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 将 core keyboard 段转为 qq-official-bot 可识别的 button / markdown 段。
|
|
3
|
-
* @see https://bot.q.qq.com/wiki/develop/api-v2/server-inter/message/trans/msg-btn.html
|
|
4
|
-
*/
|
|
5
|
-
import type { ButtonData, KeyboardSegmentData } from "zhin.js";
|
|
6
|
-
import type { MessageSegment, SendContent } from "zhin.js";
|
|
7
|
-
import { isKeyboardSegment, KeyboardSegment } from "zhin.js";
|
|
8
|
-
|
|
9
|
-
function asKeyboardData(item: unknown): KeyboardSegmentData | null {
|
|
10
|
-
if (item instanceof KeyboardSegment) return item.data;
|
|
11
|
-
if (item != null && typeof item === "object" && isKeyboardSegment(item as MessageSegment)) {
|
|
12
|
-
return (item as MessageSegment).data as KeyboardSegmentData;
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type QqButton = {
|
|
18
|
-
id: string;
|
|
19
|
-
render_data: { label: string; visited_label: string; style: number };
|
|
20
|
-
action: {
|
|
21
|
-
type: number;
|
|
22
|
-
permission: { type: number };
|
|
23
|
-
data: string;
|
|
24
|
-
click_limit: number;
|
|
25
|
-
unsupport_tips: string;
|
|
26
|
-
enter?: boolean;
|
|
27
|
-
reply?: boolean;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function coreButtonToQq(btn: ButtonData): QqButton {
|
|
32
|
-
const isCommand = btn.mode === 'command';
|
|
33
|
-
const action: QqButton['action'] = {
|
|
34
|
-
type: isCommand ? 2 : 1,
|
|
35
|
-
permission: { type: 2 },
|
|
36
|
-
data: btn.payload,
|
|
37
|
-
click_limit: btn.disabled ? 0 : 10,
|
|
38
|
-
unsupport_tips: btn.disabled ? "该按钮不可用" : "",
|
|
39
|
-
};
|
|
40
|
-
if (isCommand) {
|
|
41
|
-
if (btn.command?.enter != null) action.enter = btn.command.enter;
|
|
42
|
-
if (btn.command?.reply != null) action.reply = btn.command.reply;
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
id: btn.id,
|
|
46
|
-
render_data: {
|
|
47
|
-
label: btn.label,
|
|
48
|
-
visited_label: btn.label,
|
|
49
|
-
style: 0,
|
|
50
|
-
},
|
|
51
|
-
action,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function keyboardDataToQqSegments(data: KeyboardSegmentData): MessageSegment[] {
|
|
56
|
-
const out: MessageSegment[] = [];
|
|
57
|
-
for (const row of data.rows) {
|
|
58
|
-
out.push({
|
|
59
|
-
type: "button",
|
|
60
|
-
data: {
|
|
61
|
-
buttons: row.map(coreButtonToQq),
|
|
62
|
-
},
|
|
63
|
-
} as MessageSegment);
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** QQ 官方出站:keyboard → button 行;前置 text 合并为 markdown(按钮消息需带 markdown) */
|
|
69
|
-
export function expandKeyboardSegmentsForQq(content: SendContent): SendContent {
|
|
70
|
-
const items = Array.isArray(content) ? content : [content];
|
|
71
|
-
const out: (string | MessageSegment)[] = [];
|
|
72
|
-
const textParts: string[] = [];
|
|
73
|
-
|
|
74
|
-
for (const item of items) {
|
|
75
|
-
if (typeof item === "string") {
|
|
76
|
-
textParts.push(item);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const keyboardData = asKeyboardData(item);
|
|
80
|
-
if (keyboardData) {
|
|
81
|
-
if (textParts.length > 0) {
|
|
82
|
-
out.push({
|
|
83
|
-
type: "markdown",
|
|
84
|
-
data: { content: textParts.join("\n") },
|
|
85
|
-
} as MessageSegment);
|
|
86
|
-
textParts.length = 0;
|
|
87
|
-
}
|
|
88
|
-
out.push(...keyboardDataToQqSegments(keyboardData));
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (item.type === "text") {
|
|
93
|
-
const t = item.data?.text ?? item.data?.content ?? "";
|
|
94
|
-
if (t) textParts.push(String(t));
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (textParts.length > 0) {
|
|
99
|
-
out.push({
|
|
100
|
-
type: "markdown",
|
|
101
|
-
data: { content: textParts.join("\n") },
|
|
102
|
-
} as MessageSegment);
|
|
103
|
-
textParts.length = 0;
|
|
104
|
-
}
|
|
105
|
-
out.push(item as MessageSegment);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (textParts.length > 0) {
|
|
109
|
-
out.push({
|
|
110
|
-
type: "markdown",
|
|
111
|
-
data: { content: textParts.join("\n") },
|
|
112
|
-
} as MessageSegment);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (out.length === 0) return { type: "text", data: { text: "" } };
|
|
116
|
-
if (out.length === 1) return out[0]!;
|
|
117
|
-
return out;
|
|
118
|
-
}
|