@zhin.js/adapter-qq 4.0.2 → 5.0.1
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 +28 -0
- package/README.md +54 -0
- package/dist/index.js +2 -2
- package/lib/adapter.d.ts +14 -3
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +25 -4
- package/lib/adapter.js.map +1 -1
- package/lib/endpoint.d.ts +6 -1
- package/lib/endpoint.d.ts.map +1 -1
- package/lib/endpoint.js +80 -3
- package/lib/endpoint.js.map +1 -1
- package/lib/group-at-normalize.d.ts.map +1 -1
- package/lib/group-at-normalize.js +2 -5
- package/lib/group-at-normalize.js.map +1 -1
- package/lib/inbound-action.d.ts +15 -0
- package/lib/inbound-action.d.ts.map +1 -0
- package/lib/inbound-action.js +57 -0
- package/lib/inbound-action.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -4
- package/lib/index.js.map +1 -1
- package/lib/mention-enrich.d.ts +12 -0
- package/lib/mention-enrich.d.ts.map +1 -0
- package/lib/mention-enrich.js +52 -0
- package/lib/mention-enrich.js.map +1 -0
- package/lib/outbound-keyboard.d.ts +4 -0
- package/lib/outbound-keyboard.d.ts.map +1 -0
- package/lib/outbound-keyboard.js +96 -0
- package/lib/outbound-keyboard.js.map +1 -0
- package/lib/outbound-markdown.d.ts +16 -4
- package/lib/outbound-markdown.d.ts.map +1 -1
- package/lib/outbound-markdown.js +116 -17
- package/lib/outbound-markdown.js.map +1 -1
- package/lib/outbound-media.d.ts +2 -0
- package/lib/outbound-media.d.ts.map +1 -1
- package/lib/outbound-media.js +2 -1
- package/lib/outbound-media.js.map +1 -1
- package/lib/outbound-mixed-media.d.ts +21 -0
- package/lib/outbound-mixed-media.d.ts.map +1 -0
- package/lib/outbound-mixed-media.js +78 -0
- package/lib/outbound-mixed-media.js.map +1 -0
- package/lib/platform-permit.js +2 -2
- package/lib/qq-bind-api.d.ts +37 -0
- package/lib/qq-bind-api.d.ts.map +1 -0
- package/lib/qq-bind-api.js +90 -0
- package/lib/qq-bind-api.js.map +1 -0
- package/lib/qq-bind-flow.d.ts +20 -0
- package/lib/qq-bind-flow.d.ts.map +1 -0
- package/lib/qq-bind-flow.js +86 -0
- package/lib/qq-bind-flow.js.map +1 -0
- package/lib/qq-bind-persist.d.ts +11 -0
- package/lib/qq-bind-persist.d.ts.map +1 -0
- package/lib/qq-bind-persist.js +52 -0
- package/lib/qq-bind-persist.js.map +1 -0
- package/lib/qq-endpoint-manager.d.ts +16 -0
- package/lib/qq-endpoint-manager.d.ts.map +1 -0
- package/lib/qq-endpoint-manager.js +159 -0
- package/lib/qq-endpoint-manager.js.map +1 -0
- package/lib/segment-mapper.d.ts +2 -0
- package/lib/segment-mapper.d.ts.map +1 -0
- package/lib/segment-mapper.js +2 -0
- package/lib/segment-mapper.js.map +1 -0
- package/lib/types.d.ts +7 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +16 -16
- package/skills/qq/PERMITS.md +1 -1
- package/src/adapter.ts +28 -4
- package/src/endpoint.ts +118 -6
- package/src/group-at-normalize.ts +2 -4
- package/src/inbound-action.ts +87 -0
- package/src/index.ts +13 -5
- package/src/mention-enrich.ts +71 -0
- package/src/outbound-keyboard.ts +118 -0
- package/src/outbound-markdown.ts +127 -17
- package/src/outbound-media.ts +2 -1
- package/src/outbound-mixed-media.ts +101 -0
- package/src/platform-permit.ts +2 -2
- package/src/qq-bind-api.ts +145 -0
- package/src/qq-bind-flow.ts +127 -0
- package/src/qq-bind-persist.ts +71 -0
- package/src/qq-endpoint-manager.ts +199 -0
- package/src/segment-mapper.ts +1 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ReceiverMode as QQReceiverMode } from './types.js';
|
|
2
|
+
import { startQqBindFlow } from './qq-bind-flow.js';
|
|
3
|
+
import { persistQqCredentialsToEnv } from './qq-bind-persist.js';
|
|
4
|
+
const DEFAULT_INTENTS = [
|
|
5
|
+
'GUILDS',
|
|
6
|
+
'GUILD_MEMBERS',
|
|
7
|
+
'GUILD_MESSAGE_REACTIONS',
|
|
8
|
+
'DIRECT_MESSAGE',
|
|
9
|
+
'GROUP_AND_C2C_EVENT',
|
|
10
|
+
'INTERACTION',
|
|
11
|
+
'MESSAGE_AUDIT',
|
|
12
|
+
'AUDIO_ACTION',
|
|
13
|
+
'PUBLIC_GUILD_MESSAGES',
|
|
14
|
+
];
|
|
15
|
+
function clonePlainConfig(value) {
|
|
16
|
+
return JSON.parse(JSON.stringify(value));
|
|
17
|
+
}
|
|
18
|
+
function resolveQqEndpointTemplate(root) {
|
|
19
|
+
const configService = root.inject('config');
|
|
20
|
+
const raw = configService?.primaryFile && configService.getRaw
|
|
21
|
+
? configService.getRaw(configService.primaryFile)
|
|
22
|
+
: undefined;
|
|
23
|
+
const existing = raw?.endpoints?.find((e) => e.context === 'qq');
|
|
24
|
+
if (!existing) {
|
|
25
|
+
return {
|
|
26
|
+
mode: QQReceiverMode.WEBSOCKET,
|
|
27
|
+
sandbox: false,
|
|
28
|
+
timeout: 10_000,
|
|
29
|
+
data_dir: './data',
|
|
30
|
+
intents: [...DEFAULT_INTENTS],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const { name: _n, appid: _a, secret: _s, context: _c, ...rest } = existing;
|
|
34
|
+
return clonePlainConfig(rest);
|
|
35
|
+
}
|
|
36
|
+
function resolveBindOperatorId(userOpenId, ctx) {
|
|
37
|
+
const fromApi = userOpenId?.trim();
|
|
38
|
+
if (fromApi)
|
|
39
|
+
return fromApi;
|
|
40
|
+
return ctx.message.$sender?.id?.trim() ?? '';
|
|
41
|
+
}
|
|
42
|
+
function applyBindOwnership(template, operatorId) {
|
|
43
|
+
const { master: _m, aiAccess, ...rest } = template;
|
|
44
|
+
if (!operatorId) {
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
47
|
+
const prior = aiAccess;
|
|
48
|
+
return {
|
|
49
|
+
...rest,
|
|
50
|
+
master: operatorId,
|
|
51
|
+
aiAccess: {
|
|
52
|
+
mode: prior?.mode ?? 'whitelist',
|
|
53
|
+
users: [operatorId],
|
|
54
|
+
groups: Array.isArray(prior?.groups) ? [...prior.groups] : [],
|
|
55
|
+
denyMessage: typeof prior?.denyMessage === 'string'
|
|
56
|
+
? prior.denyMessage
|
|
57
|
+
: '当前用户尚未开放 AI 功能,请联系开启。',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function readQqConfigs(root) {
|
|
62
|
+
const configService = root.inject('config');
|
|
63
|
+
if (!configService?.primaryFile || !configService.getRaw)
|
|
64
|
+
return [];
|
|
65
|
+
const raw = configService.getRaw(configService.primaryFile);
|
|
66
|
+
return (raw?.endpoints ?? []).filter((e) => e.context === 'qq');
|
|
67
|
+
}
|
|
68
|
+
/** 全局单例:同一时间只允许一个 QQ 扫码绑定 */
|
|
69
|
+
let activeStopFlow = null;
|
|
70
|
+
export class QqEndpointManager {
|
|
71
|
+
adapter;
|
|
72
|
+
constructor(adapter) {
|
|
73
|
+
this.adapter = adapter;
|
|
74
|
+
}
|
|
75
|
+
supportsProvision() {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
listEndpoints() {
|
|
79
|
+
return readQqConfigs(this.adapter.plugin.root);
|
|
80
|
+
}
|
|
81
|
+
cancelProvision() {
|
|
82
|
+
if (!activeStopFlow)
|
|
83
|
+
return false;
|
|
84
|
+
activeStopFlow();
|
|
85
|
+
activeStopFlow = null;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
async addEndpoint(ctx) {
|
|
89
|
+
if (activeStopFlow) {
|
|
90
|
+
throw new Error('已有进行中的 QQ 机器人绑定,请先 /endpoint cancel');
|
|
91
|
+
}
|
|
92
|
+
let endpointName;
|
|
93
|
+
const nameMatch = ctx.message.$raw?.match(/\/endpoint\s+add\s+qq\s+(\S+)/);
|
|
94
|
+
if (nameMatch?.[1]) {
|
|
95
|
+
endpointName = nameMatch[1];
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const stopFlow = startQqBindFlow({
|
|
99
|
+
onQrDisplayed: async (url) => {
|
|
100
|
+
await ctx.onStatusUpdate('请用手机 QQ 扫描二维码完成机器人绑定。', {
|
|
101
|
+
qrcode: url,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
onQrExpired: async () => {
|
|
105
|
+
await ctx.onStatusUpdate('二维码已过期,正在刷新,请扫描新二维码…');
|
|
106
|
+
},
|
|
107
|
+
onSuccess: async (credentials) => {
|
|
108
|
+
activeStopFlow = null;
|
|
109
|
+
const [{ appId, appSecret, userOpenId }] = credentials;
|
|
110
|
+
const name = endpointName?.trim() || appId;
|
|
111
|
+
await ctx.onStatusUpdate('凭据绑定成功,正在写入配置…');
|
|
112
|
+
const envKeys = persistQqCredentialsToEnv(name, appId, appSecret);
|
|
113
|
+
const template = resolveQqEndpointTemplate(ctx.root);
|
|
114
|
+
const operatorId = resolveBindOperatorId(userOpenId, ctx);
|
|
115
|
+
const owned = applyBindOwnership(template, operatorId);
|
|
116
|
+
resolve({
|
|
117
|
+
context: 'qq',
|
|
118
|
+
name,
|
|
119
|
+
appid: envKeys.appidRef,
|
|
120
|
+
secret: envKeys.secretRef,
|
|
121
|
+
...owned,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
onFailure: async (error) => {
|
|
125
|
+
activeStopFlow = null;
|
|
126
|
+
reject(error);
|
|
127
|
+
},
|
|
128
|
+
}, { source: 'zhin' });
|
|
129
|
+
activeStopFlow = stopFlow;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async editEndpoint(name, ctx) {
|
|
133
|
+
const existing = this.listEndpoints().find((e) => e.name === name);
|
|
134
|
+
if (!existing) {
|
|
135
|
+
throw new Error(`配置中不存在 qq/${name}`);
|
|
136
|
+
}
|
|
137
|
+
await ctx.onStatusUpdate(`QQ 官方机器人暂不支持在线改凭据,请 /endpoint remove qq ${name} 后重新 /endpoint add qq。`);
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
async removeEndpoint(name) {
|
|
141
|
+
return this.listEndpoints().some((e) => e.name === name);
|
|
142
|
+
}
|
|
143
|
+
async startEndpoint(name, ctx) {
|
|
144
|
+
if (!this.listEndpoints().some((e) => e.name === name)) {
|
|
145
|
+
throw new Error(`配置中不存在 qq/${name}`);
|
|
146
|
+
}
|
|
147
|
+
await ctx.onStatusUpdate(`正在连接 qq/${name}…`);
|
|
148
|
+
}
|
|
149
|
+
async stopEndpoint(name) {
|
|
150
|
+
return this.adapter.endpoints.has(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function disposeQqEndpointProvision() {
|
|
154
|
+
if (activeStopFlow) {
|
|
155
|
+
activeStopFlow();
|
|
156
|
+
activeStopFlow = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=qq-endpoint-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qq-endpoint-manager.js","sourceRoot":"","sources":["../src/qq-endpoint-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-mapper.d.ts","sourceRoot":"","sources":["../src/segment-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-mapper.js","sourceRoot":"","sources":["../src/segment-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC"}
|
package/lib/types.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type QQEndpointConfig<T extends ReceiverMode, M extends ApplicationPlatfo
|
|
|
31
31
|
* - `false`:保持纯文本
|
|
32
32
|
*/
|
|
33
33
|
outboundMarkdown?: boolean | "auto";
|
|
34
|
+
/** 本 Endpoint AI 访问控制(见 docs/advanced/content-moderation.md) */
|
|
35
|
+
aiAccess?: {
|
|
36
|
+
mode?: 'open' | 'closed' | 'whitelist';
|
|
37
|
+
users?: string[];
|
|
38
|
+
groups?: string[];
|
|
39
|
+
denyMessage?: string;
|
|
40
|
+
};
|
|
34
41
|
};
|
|
35
42
|
export interface QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform = ApplicationPlatform> {
|
|
36
43
|
$config: QQEndpointConfig<T, M>;
|
package/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,gBAAgB,CAC1B,CAAC,SAAS,YAAY,EACtB,CAAC,SAAS,mBAAmB,GAAG,mBAAmB,IACjD,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAC/B,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,gBAAgB,CAC1B,CAAC,SAAS,YAAY,EACtB,CAAC,SAAS,mBAAmB,GAAG,mBAAmB,IACjD,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAC/B,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACpC,gEAAgE;IAChE,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;QACvC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,UAAU,CACzB,CAAC,SAAS,YAAY,EACtB,CAAC,SAAS,mBAAmB,GAAG,mBAAmB;IAEnD,OAAO,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CACjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-qq",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Zhin.js adapter for QQ Official Bot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -34,28 +34,28 @@
|
|
|
34
34
|
"directory": "plugins/adapters/qq"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"qq-official-bot": "^1.2.
|
|
37
|
+
"qq-official-bot": "^1.2.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/react": "^19.2.17",
|
|
41
41
|
"@types/react-dom": "^19.2.3",
|
|
42
|
-
"lucide-react": "^1.
|
|
42
|
+
"lucide-react": "^1.22.0",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
|
-
"@zhin.js/
|
|
45
|
-
"@zhin.js/
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
48
|
-
"@zhin.js/host-router": "2.0.
|
|
49
|
-
"zhin.js": "
|
|
50
|
-
"
|
|
44
|
+
"@zhin.js/cli": "1.0.92",
|
|
45
|
+
"@zhin.js/client": "2.0.4",
|
|
46
|
+
"@zhin.js/contract": "1.0.2",
|
|
47
|
+
"@zhin.js/host-api": "2.0.4",
|
|
48
|
+
"@zhin.js/host-router": "2.0.2",
|
|
49
|
+
"@zhin.js/logger": "1.0.73",
|
|
50
|
+
"zhin.js": "4.1.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@zhin.js/client": "2.0.
|
|
54
|
-
"@zhin.js/host-api": "2.0.
|
|
55
|
-
"@zhin.js/
|
|
56
|
-
"@zhin.js/
|
|
57
|
-
"@zhin.js/
|
|
58
|
-
"zhin.js": "4.
|
|
53
|
+
"@zhin.js/client": "2.0.4",
|
|
54
|
+
"@zhin.js/host-api": "2.0.4",
|
|
55
|
+
"@zhin.js/contract": "1.0.2",
|
|
56
|
+
"@zhin.js/host-router": "2.0.2",
|
|
57
|
+
"@zhin.js/logger": "1.0.73",
|
|
58
|
+
"zhin.js": "4.1.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@zhin.js/client": {
|
package/skills/qq/PERMITS.md
CHANGED
package/src/adapter.ts
CHANGED
|
@@ -6,13 +6,30 @@ import {
|
|
|
6
6
|
Plugin,
|
|
7
7
|
} from "zhin.js";
|
|
8
8
|
import type { Router } from "@zhin.js/host-router";
|
|
9
|
+
import type { EndpointManager } from "zhin.js";
|
|
9
10
|
import { QQEndpoint } from "./endpoint.js";
|
|
11
|
+
import { QqEndpointManager } from "./qq-endpoint-manager.js";
|
|
10
12
|
import type { QQEndpointConfig, ReceiverMode } from "./types.js";
|
|
13
|
+
import type { OutboundRichSegmentPolicy } from "zhin.js";
|
|
14
|
+
import { QQ_AI_OUTBOUND_EXTENSIONS } from "zhin.js";
|
|
11
15
|
|
|
12
16
|
export class QQAdapter extends Adapter<QQEndpoint<ReceiverMode>> {
|
|
13
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;
|
|
14
30
|
|
|
15
31
|
#router?: Router;
|
|
32
|
+
#endpointManager?: QqEndpointManager;
|
|
16
33
|
|
|
17
34
|
constructor(plugin: Plugin, router?: Router) {
|
|
18
35
|
super(plugin, "qq", []);
|
|
@@ -27,9 +44,16 @@ export class QQAdapter extends Adapter<QQEndpoint<ReceiverMode>> {
|
|
|
27
44
|
return new QQEndpoint(this, config);
|
|
28
45
|
}
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
override getEndpointManager(): EndpointManager {
|
|
48
|
+
if (!this.#endpointManager) {
|
|
49
|
+
this.#endpointManager = new QqEndpointManager(this);
|
|
50
|
+
}
|
|
51
|
+
return this.#endpointManager;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── ISceneManagement 标准群管方法 ──────────────────────────────────
|
|
31
55
|
|
|
32
|
-
async
|
|
56
|
+
async removeMember(endpointId: string, sceneId: string, userId: string) {
|
|
33
57
|
const endpoint = this.endpoints.get(endpointId);
|
|
34
58
|
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
35
59
|
return endpoint.removeGuildMember(sceneId, userId, false);
|
|
@@ -41,7 +65,7 @@ export class QQAdapter extends Adapter<QQEndpoint<ReceiverMode>> {
|
|
|
41
65
|
return endpoint.muteMembers(sceneId, [userId], duration);
|
|
42
66
|
}
|
|
43
67
|
|
|
44
|
-
async
|
|
68
|
+
async setSceneMuted(endpointId: string, sceneId: string, enable = true) {
|
|
45
69
|
const endpoint = this.endpoints.get(endpointId);
|
|
46
70
|
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
47
71
|
return endpoint.muteAll(sceneId, enable ? 600 : 0);
|
|
@@ -53,7 +77,7 @@ export class QQAdapter extends Adapter<QQEndpoint<ReceiverMode>> {
|
|
|
53
77
|
return endpoint.getGuildMembers(sceneId);
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
async
|
|
80
|
+
async getSceneInfo(endpointId: string, sceneId: string) {
|
|
57
81
|
const endpoint = this.endpoints.get(endpointId);
|
|
58
82
|
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
59
83
|
return endpoint.getGuildInfo(sceneId);
|
package/src/endpoint.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SendOptions,
|
|
16
16
|
SendContent,
|
|
17
17
|
segment,
|
|
18
|
-
} from
|
|
18
|
+
} from 'zhin.js';
|
|
19
19
|
import type { MessageElement } from "zhin.js";
|
|
20
20
|
import { ReceiverMode, type QQEndpointConfig, type ApplicationPlatform } from "./types.js";
|
|
21
21
|
import type { QQAdapter } from "./adapter.js";
|
|
@@ -23,9 +23,19 @@ import { normalizeQqInboundWsPayload, type QqWsPacket } from "./inbound-normaliz
|
|
|
23
23
|
import { normalizeGroupAtPrefix } from "./group-at-normalize.js";
|
|
24
24
|
import { SDK_VERSION, SDK_VERSION_HEADER } from "./sdk-version.js";
|
|
25
25
|
import { applyCustomAuthEndpoints } from "./gateway-config.js";
|
|
26
|
-
import { normalizeOutboundMarkdown } from "./outbound-markdown.js";
|
|
26
|
+
import { normalizeOutboundMarkdown, asOutboundSegments, splitReplyPrefix } from "./outbound-markdown.js";
|
|
27
27
|
import { normalizeOutboundMedia } from "./outbound-media.js";
|
|
28
|
+
import { fromCanonicalSegments, toCanonicalSegments } from "./segment-mapper.js";
|
|
29
|
+
import { enrichCanonicalMentionNames } from "./mention-enrich.js";
|
|
30
|
+
import {
|
|
31
|
+
buildMixedMediaMessagePayload,
|
|
32
|
+
buildQqImageUploadPayload,
|
|
33
|
+
firstImageSegment,
|
|
34
|
+
shouldSendTextImageMixedMedia,
|
|
35
|
+
} from "./outbound-mixed-media.js";
|
|
28
36
|
import { normalizeQqGuildSenderForPermit } from "./platform-permit.js";
|
|
37
|
+
import { expandKeyboardSegmentsForQq } from "./outbound-keyboard.js";
|
|
38
|
+
import { formatQqActionMessage, type QqActionNoticeEvent } from "./inbound-action.js";
|
|
29
39
|
|
|
30
40
|
/** 从 qq-official-bot SendResult / 审核回包中解析出站消息 ID */
|
|
31
41
|
export function resolveOutboundMessageId(result: unknown): string {
|
|
@@ -172,6 +182,34 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
|
|
185
|
+
private async handleQQAction(event: QqActionNoticeEvent): Promise<void> {
|
|
186
|
+
try {
|
|
187
|
+
await event.reply(0);
|
|
188
|
+
const channel = event.notice_type === 'group'
|
|
189
|
+
? { id: String(event.group_id), type: 'group' as const }
|
|
190
|
+
: event.notice_type === 'guild'
|
|
191
|
+
? { id: String(event.channel_id), type: 'channel' as const }
|
|
192
|
+
: { id: String(event.operator_id), type: 'private' as const };
|
|
193
|
+
const message = formatQqActionMessage(event, {
|
|
194
|
+
endpointName: this.$config.name,
|
|
195
|
+
send: async (content) => this.adapter.sendMessage({
|
|
196
|
+
...channel,
|
|
197
|
+
context: 'qq',
|
|
198
|
+
endpoint: this.$config.name,
|
|
199
|
+
content,
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
await this.adapter.receiveMessageAwait(message);
|
|
203
|
+
this.pluginLogger.debug(
|
|
204
|
+
`qq action ${message.$channel.type}(${message.$channel.id}): ${message.$raw}`,
|
|
205
|
+
);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
this.pluginLogger.warn(
|
|
208
|
+
`qq format action failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
175
213
|
private handleGroupNotice(event: string, payload: { group_id?: string; operator_id?: string }): void {
|
|
176
214
|
this.pluginLogger.info(
|
|
177
215
|
`qq notice ${event}: group=${payload.group_id ?? '?'} operator=${payload.operator_id ?? '?'}`,
|
|
@@ -182,6 +220,9 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
182
220
|
this.on("message.group", this.handleQQMessage.bind(this));
|
|
183
221
|
this.on("message.guild", this.handleQQMessage.bind(this));
|
|
184
222
|
this.on("message.private", this.handleQQMessage.bind(this));
|
|
223
|
+
this.on("notice.friend.action", this.handleQQAction.bind(this));
|
|
224
|
+
this.on("notice.group.action", this.handleQQAction.bind(this));
|
|
225
|
+
this.on("notice.guild.action", this.handleQQAction.bind(this));
|
|
185
226
|
this.on("notice.group.increase", (e) => this.handleGroupNotice('group.add_robot', e));
|
|
186
227
|
this.on("notice.group.decrease", (e) => this.handleGroupNotice('group.del_robot', e));
|
|
187
228
|
this.on("notice.group.receive_open", (e) => this.handleGroupNotice('group.msg_receive_open', e));
|
|
@@ -253,10 +294,18 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
253
294
|
}
|
|
254
295
|
}
|
|
255
296
|
|
|
297
|
+
private toWireSendContent(content: SendContent): MessageElement[] {
|
|
298
|
+
const items = (Array.isArray(content) ? content : [content]).map((item) =>
|
|
299
|
+
typeof item === "string" ? { type: "text", data: { text: item } } : item as MessageElement,
|
|
300
|
+
);
|
|
301
|
+
return fromCanonicalSegments(toCanonicalSegments(items as never));
|
|
302
|
+
}
|
|
303
|
+
|
|
256
304
|
$formatMessage(msg: PrivateMessageEvent | GroupMessageEvent) {
|
|
257
305
|
const raw = msg as PrivateMessageEvent & GroupMessageEvent & {
|
|
258
306
|
group_openid?: string;
|
|
259
307
|
author?: { member_openid?: string; user_openid?: string; id?: string; username?: string };
|
|
308
|
+
mentions?: Array<{ id?: string; member_openid?: string; username?: string; nickname?: string }>;
|
|
260
309
|
__zhin_group_at?: boolean;
|
|
261
310
|
};
|
|
262
311
|
|
|
@@ -283,6 +332,14 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
283
332
|
content = normalizeGroupAtPrefix(content, endpointAtIds, raw.__zhin_group_at === true);
|
|
284
333
|
}
|
|
285
334
|
|
|
335
|
+
if (Array.isArray(content)) {
|
|
336
|
+
content = enrichCanonicalMentionNames(
|
|
337
|
+
toCanonicalSegments(content),
|
|
338
|
+
this.adapter,
|
|
339
|
+
raw.mentions,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
286
343
|
const result = Message.from(msg, {
|
|
287
344
|
$id: msg.message_id?.toString(),
|
|
288
345
|
$adapter: "qq" as const,
|
|
@@ -316,10 +373,20 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
316
373
|
}
|
|
317
374
|
|
|
318
375
|
async $sendMessage(options: SendOptions): Promise<string> {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
376
|
+
const expanded = expandKeyboardSegmentsForQq(options.content);
|
|
377
|
+
const canonicalized = this.toWireSendContent(expanded);
|
|
378
|
+
const mediaNorm = normalizeOutboundMedia(canonicalized);
|
|
379
|
+
const markdownMode = this.$config.outboundMarkdown;
|
|
380
|
+
|
|
381
|
+
if (
|
|
382
|
+
(options.type === "group" || options.type === "private")
|
|
383
|
+
&& !options.id.startsWith("direct:")
|
|
384
|
+
&& shouldSendTextImageMixedMedia(mediaNorm, markdownMode)
|
|
385
|
+
) {
|
|
386
|
+
return await this.$sendTextImageMixedMedia(options, mediaNorm);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const content = normalizeOutboundMarkdown(mediaNorm, markdownMode);
|
|
323
390
|
switch (options.type) {
|
|
324
391
|
case "private": {
|
|
325
392
|
if (options.id.startsWith("direct:")) {
|
|
@@ -348,6 +415,51 @@ export class QQEndpoint<T extends ReceiverMode, M extends ApplicationPlatform =
|
|
|
348
415
|
}
|
|
349
416
|
}
|
|
350
417
|
|
|
418
|
+
/** 内联图 + 文本:上传 file_info 后以 msg_type=7 图文混排发送(二维码等场景) */
|
|
419
|
+
private async $sendTextImageMixedMedia(
|
|
420
|
+
options: SendOptions,
|
|
421
|
+
content: SendContent,
|
|
422
|
+
): Promise<string> {
|
|
423
|
+
const endpointPath = this.resolveV2EndpointPath(options);
|
|
424
|
+
const { body } = splitReplyPrefix(asOutboundSegments(content));
|
|
425
|
+
const image = firstImageSegment(body);
|
|
426
|
+
if (!image) {
|
|
427
|
+
throw new Error("QQ 图文混排:未找到图片段");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const uploadPayload = buildQqImageUploadPayload(image);
|
|
431
|
+
const { data: uploadResult } = await this.request.post(
|
|
432
|
+
`${endpointPath}/files`,
|
|
433
|
+
{ ...uploadPayload, srv_send_msg: false },
|
|
434
|
+
) as { data: { file_info?: string } };
|
|
435
|
+
|
|
436
|
+
if (!uploadResult?.file_info) {
|
|
437
|
+
throw new Error("QQ 图文混排:图片上传未返回 file_info");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const messagePayload = buildMixedMediaMessagePayload(content, uploadResult.file_info);
|
|
441
|
+
const { data: result } = await this.request.post(
|
|
442
|
+
`${endpointPath}/messages`,
|
|
443
|
+
messagePayload,
|
|
444
|
+
);
|
|
445
|
+
this.pluginLogger.debug(
|
|
446
|
+
`${this.$config.name} send mixed-media ${options.type}(${options.id}):${segment.raw(content)}`,
|
|
447
|
+
);
|
|
448
|
+
const idPrefix = options.type === "group" ? "group" : "private";
|
|
449
|
+
return `${idPrefix}-${options.id}:${resolveOutboundMessageId(result)}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private resolveV2EndpointPath(options: SendOptions): string {
|
|
453
|
+
switch (options.type) {
|
|
454
|
+
case "group":
|
|
455
|
+
return `/v2/groups/${options.id}`;
|
|
456
|
+
case "private":
|
|
457
|
+
return `/v2/users/${options.id}`;
|
|
458
|
+
default:
|
|
459
|
+
throw new Error(`QQ 图文混排不支持 ${options.type}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
351
463
|
async $recallMessage(id: string): Promise<void> {
|
|
352
464
|
if (!/^(private|group|channel|direct)-([^\:]+):(.+)$/.test(id)) throw new Error(`invalid message id ${id}`);
|
|
353
465
|
const match = id.match(/^(private|group|channel|direct)-([^\:]+):(.+)$/);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { MessageElement } from "zhin.js";
|
|
2
|
+
import { readMentionSegmentTarget } from "zhin.js";
|
|
2
3
|
|
|
3
4
|
function segmentAtId(seg: MessageElement): string {
|
|
4
|
-
|
|
5
|
-
const data = seg.data as Record<string, unknown> | undefined;
|
|
6
|
-
const raw = data?.user_id ?? data?.qq ?? data?.id;
|
|
7
|
-
return raw == null ? "" : String(raw);
|
|
5
|
+
return readMentionSegmentTarget(seg);
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
function textMentionsEndpoint(text: string, endpointIds: string[]): boolean {
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|