@alemonjs/qq-bot 2.1.31 → 2.1.33
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/README.md +1950 -25
- package/dist/assets/index.css +1 -1
- package/dist/assets/index.js +3 -3
- package/dist/index.html +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.js +23 -1
- package/lib/desktop.js +8 -1
- package/lib/hook.d.ts +2 -1
- package/lib/index.js +5 -8
- package/lib/index.webhook.js +3 -0
- package/lib/index.websoket.d.ts +2 -0
- package/lib/index.websoket.js +20 -18
- package/lib/message/group/C2C_MESSAGE_CREATE.d.ts +7 -0
- package/lib/message/group/GROUP_AT_MESSAGE_CREATE.d.ts +7 -0
- package/lib/message/group/GROUP_JOIN_REQUEST.d.ts +23 -0
- package/lib/message/group/GROUP_JOIN_REQUEST.js +1 -0
- package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +7 -0
- package/lib/register.d.ts +10 -1
- package/lib/register.js +508 -53
- package/lib/sdk/api.d.ts +116 -3
- package/lib/sdk/api.js +390 -17
- package/lib/sdk/client.webhook.js +14 -14
- package/lib/sdk/client.websoket.d.ts +2 -1
- package/lib/sdk/client.websoket.js +321 -110
- package/lib/sdk/config.d.ts +3 -1
- package/lib/sdk/config.js +6 -2
- package/lib/sdk/instance.js +2 -0
- package/lib/sdk/intents.d.ts +1 -1
- package/lib/sdk/intents.js +2 -0
- package/lib/sdk/message.group.d.ts +2 -0
- package/lib/sdk/registry.d.ts +25 -0
- package/lib/sdk/registry.js +116 -0
- package/lib/sdk/session.d.ts +18 -0
- package/lib/sdk/session.js +56 -0
- package/lib/sdk/typing.d.ts +125 -1
- package/lib/sends.d.ts +6 -2
- package/lib/sends.js +136 -57
- package/lib/upload.d.ts +21 -0
- package/lib/upload.js +60 -0
- package/package.json +6 -5
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cbpPlatform } from 'alemonjs';
|
|
2
|
+
import { QQBotClients } from './client.websoket.js';
|
|
3
|
+
import type { Options } from './typing.js';
|
|
4
|
+
export declare class QQBotRegistry {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(defaultBot?: string, cbp?: ReturnType<typeof cbpPlatform>);
|
|
7
|
+
add(botId: string, options: Options): QQBotClients;
|
|
8
|
+
get(botId?: string): QQBotClients;
|
|
9
|
+
getConnectionStatus(botId?: string): {
|
|
10
|
+
Platform: string;
|
|
11
|
+
state: import("./api.js").QQBotConnectionState;
|
|
12
|
+
bots: {
|
|
13
|
+
state: import("./api.js").QQBotConnectionState;
|
|
14
|
+
transport: "websocket" | "webhook" | "proxy" | null;
|
|
15
|
+
reconnectAttempts: number;
|
|
16
|
+
heartbeatAcknowledged: boolean;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
sequence?: number;
|
|
19
|
+
resumed: boolean;
|
|
20
|
+
lastError?: string;
|
|
21
|
+
BotId: string;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
disconnect(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { cbpPlatform, createResult, ResultCode } from 'alemonjs';
|
|
2
|
+
import { getQQBotConfig } from '../config.js';
|
|
3
|
+
import { register } from '../register.js';
|
|
4
|
+
import { QQBotClients } from './client.websoket.js';
|
|
5
|
+
|
|
6
|
+
class QQBotRegistry {
|
|
7
|
+
#bots = new Map();
|
|
8
|
+
#streams = new Map();
|
|
9
|
+
#defaultBot;
|
|
10
|
+
#cbp;
|
|
11
|
+
constructor(defaultBot, cbp) {
|
|
12
|
+
this.#defaultBot = defaultBot;
|
|
13
|
+
if (cbp) {
|
|
14
|
+
this.#cbp = cbp;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const config = getQQBotConfig();
|
|
18
|
+
const port = process.env.port || config.port || 17117;
|
|
19
|
+
this.#cbp = cbpPlatform(`ws://127.0.0.1:${port}`);
|
|
20
|
+
}
|
|
21
|
+
this.#cbp.onactions((data, consume) => void this.#onAction(data, consume));
|
|
22
|
+
this.#cbp.onapis((data, consume) => void this.#onApi(data, consume));
|
|
23
|
+
}
|
|
24
|
+
add(botId, options) {
|
|
25
|
+
if (!botId)
|
|
26
|
+
throw new Error('QQ BotId is required');
|
|
27
|
+
if (this.#bots.has(botId))
|
|
28
|
+
throw new Error(`Duplicate QQ BotId: ${botId}`);
|
|
29
|
+
const client = new QQBotClients({ ...options, app_id: botId });
|
|
30
|
+
client.setStreamLifecycleListener(streamId => this.#streams.delete(streamId));
|
|
31
|
+
const adapter = register(client, { botId, cbp: this.#cbp, bindActions: false });
|
|
32
|
+
this.#bots.set(botId, { client, adapter });
|
|
33
|
+
return client;
|
|
34
|
+
}
|
|
35
|
+
get(botId) {
|
|
36
|
+
return botId ? this.#bots.get(botId)?.client : undefined;
|
|
37
|
+
}
|
|
38
|
+
getConnectionStatus(botId) {
|
|
39
|
+
const bots = [...this.#bots].flatMap(([BotId, entry]) => {
|
|
40
|
+
if (botId && botId !== BotId)
|
|
41
|
+
return [];
|
|
42
|
+
return [{ BotId, ...entry.client.getConnectionStatus() }];
|
|
43
|
+
});
|
|
44
|
+
const state = bots.some(item => item.state === 'ready')
|
|
45
|
+
? 'ready'
|
|
46
|
+
: bots.some(item => item.state === 'connecting' || item.state === 'reconnecting')
|
|
47
|
+
? 'connecting'
|
|
48
|
+
: bots[0]?.state || 'idle';
|
|
49
|
+
return { Platform: 'qq-bot', state, bots };
|
|
50
|
+
}
|
|
51
|
+
disconnect() {
|
|
52
|
+
for (const { client } of this.#bots.values())
|
|
53
|
+
client.disconnect();
|
|
54
|
+
this.#streams.clear();
|
|
55
|
+
}
|
|
56
|
+
#requestedBotId(data) {
|
|
57
|
+
const payload = data?.payload || {};
|
|
58
|
+
return payload.target?.BotId || payload.BotId || payload.event?.BotId || payload.event?.Platform?.BotId;
|
|
59
|
+
}
|
|
60
|
+
#select(data) {
|
|
61
|
+
const requested = this.#requestedBotId(data);
|
|
62
|
+
if (requested) {
|
|
63
|
+
const entry = this.#bots.get(String(requested));
|
|
64
|
+
return entry ? { entry } : { error: `BotId ${requested} is not configured (available: ${[...this.#bots.keys()].join(', ') || 'none'})` };
|
|
65
|
+
}
|
|
66
|
+
if (this.#bots.size === 1)
|
|
67
|
+
return { entry: this.#bots.values().next().value };
|
|
68
|
+
if (this.#defaultBot)
|
|
69
|
+
return { entry: this.#bots.get(this.#defaultBot) };
|
|
70
|
+
return { error: 'BotId is required because multiple QQ bots are active and no default_bot is configured' };
|
|
71
|
+
}
|
|
72
|
+
async #onAction(data, consume) {
|
|
73
|
+
if (data?.action === 'connection.status') {
|
|
74
|
+
const botId = data?.payload?.BotId;
|
|
75
|
+
const status = this.getConnectionStatus(botId);
|
|
76
|
+
if (botId && !status.bots.length) {
|
|
77
|
+
consume([createResult(ResultCode.FailParams, `BotId ${botId} is not configured`, null)]);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
consume([createResult(ResultCode.Ok, data.action, status)]);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { entry, error } = this.#select(data);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await entry.adapter.onAction(data, consume);
|
|
90
|
+
}
|
|
91
|
+
async #onApi(data, consume) {
|
|
92
|
+
const params = data?.payload?.params;
|
|
93
|
+
const key = String(data?.payload?.key || '');
|
|
94
|
+
const method = key.split('.').at(-1);
|
|
95
|
+
const streamId = method === 'streamUpdate' || method === 'streamComplete' || method === 'streamCancel' ? params?.[0] : undefined;
|
|
96
|
+
const requested = (Array.isArray(params) ? params[0]?.BotId : undefined) || (streamId ? this.#streams.get(String(streamId)) : undefined);
|
|
97
|
+
const { entry, error } = this.#select({ payload: { BotId: requested, event: data?.payload?.event } });
|
|
98
|
+
if (!entry) {
|
|
99
|
+
consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await entry.adapter.onApi(data, results => {
|
|
103
|
+
const result = results.find((item) => item.code === ResultCode.Ok)?.data;
|
|
104
|
+
if (method === 'streamOpen' && result?.streamId)
|
|
105
|
+
this.#streams.set(String(result.streamId), requested || this.#botIdFor(entry));
|
|
106
|
+
if (streamId && (method === 'streamComplete' || method === 'streamCancel'))
|
|
107
|
+
this.#streams.delete(String(streamId));
|
|
108
|
+
consume(results);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
#botIdFor(entry) {
|
|
112
|
+
return [...this.#bots.entries()].find(([, value]) => value === entry)?.[0] || '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { QQBotRegistry };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type PersistedSession = {
|
|
2
|
+
botId: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
sequence: number;
|
|
5
|
+
};
|
|
6
|
+
export interface SessionStore {
|
|
7
|
+
load(botId: string): Promise<PersistedSession | null>;
|
|
8
|
+
save(session: PersistedSession): Promise<void>;
|
|
9
|
+
clear(botId: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare class FileSessionStore implements SessionStore {
|
|
12
|
+
#private;
|
|
13
|
+
private readonly root;
|
|
14
|
+
constructor(root?: string);
|
|
15
|
+
load(botId: string): Promise<PersistedSession | null>;
|
|
16
|
+
save(session: PersistedSession): Promise<void>;
|
|
17
|
+
clear(botId: string): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
class FileSessionStore {
|
|
6
|
+
root;
|
|
7
|
+
#queues = new Map();
|
|
8
|
+
#generations = new Map();
|
|
9
|
+
constructor(root = join(process.cwd(), '.data', 'qq-bot', 'sessions')) {
|
|
10
|
+
this.root = root;
|
|
11
|
+
}
|
|
12
|
+
#path(botId) {
|
|
13
|
+
return join(this.root, `${encodeURIComponent(botId)}.json`);
|
|
14
|
+
}
|
|
15
|
+
async load(botId) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(await readFile(this.#path(botId), 'utf8'));
|
|
18
|
+
return value?.botId === botId && typeof value.sessionId === 'string' && Number.isFinite(value.sequence) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async save(session) {
|
|
25
|
+
const generation = this.#generations.get(session.botId) || 0;
|
|
26
|
+
await this.#enqueue(session.botId, async () => {
|
|
27
|
+
if (generation !== (this.#generations.get(session.botId) || 0))
|
|
28
|
+
return;
|
|
29
|
+
const path = this.#path(session.botId);
|
|
30
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
31
|
+
await mkdir(dirname(path), { recursive: true });
|
|
32
|
+
await writeFile(temp, JSON.stringify(session), 'utf8');
|
|
33
|
+
await rename(temp, path);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async clear(botId) {
|
|
37
|
+
this.#generations.set(botId, (this.#generations.get(botId) || 0) + 1);
|
|
38
|
+
await this.#enqueue(botId, async () => {
|
|
39
|
+
await unlink(this.#path(botId)).catch(() => undefined);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
#enqueue(botId, task) {
|
|
43
|
+
const previous = this.#queues.get(botId) || Promise.resolve();
|
|
44
|
+
const next = previous.catch(() => undefined).then(task);
|
|
45
|
+
this.#queues.set(botId, next);
|
|
46
|
+
void next
|
|
47
|
+
.finally(() => {
|
|
48
|
+
if (this.#queues.get(botId) === next)
|
|
49
|
+
this.#queues.delete(botId);
|
|
50
|
+
})
|
|
51
|
+
.catch(() => undefined);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { FileSessionStore };
|
package/lib/sdk/typing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IntentsEnum } from './intents';
|
|
2
|
-
|
|
2
|
+
import type { SessionStore } from './session.js';
|
|
3
|
+
export type MessageType = 0 | 1 | 2 | 3 | 4 | 6 | 7;
|
|
3
4
|
export type FileType = 1 | 2 | 3 | 4;
|
|
4
5
|
export interface ButtonType {
|
|
5
6
|
id: string;
|
|
@@ -54,6 +55,128 @@ export interface ApiRequestData {
|
|
|
54
55
|
event_id?: any;
|
|
55
56
|
msg_id?: string;
|
|
56
57
|
msg_seq?: number;
|
|
58
|
+
is_wakeup?: boolean;
|
|
59
|
+
force_verify_image_resource?: boolean;
|
|
60
|
+
input_notify?: {
|
|
61
|
+
input_type: number;
|
|
62
|
+
input_second: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface VerifyInfo {
|
|
66
|
+
method: string;
|
|
67
|
+
verify_message?: string;
|
|
68
|
+
review_qa_list?: {
|
|
69
|
+
question: string;
|
|
70
|
+
answer: string;
|
|
71
|
+
}[];
|
|
72
|
+
}
|
|
73
|
+
export interface JoinRequest {
|
|
74
|
+
join_request_id: string;
|
|
75
|
+
risk_tips?: string;
|
|
76
|
+
union_openid?: string;
|
|
77
|
+
member_openid: string;
|
|
78
|
+
username: string;
|
|
79
|
+
apply_at: string;
|
|
80
|
+
apply_source: string;
|
|
81
|
+
invited_by?: string;
|
|
82
|
+
bot: boolean;
|
|
83
|
+
verify_info?: VerifyInfo;
|
|
84
|
+
auto_approved?: {
|
|
85
|
+
strategy_id: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export interface GlobalMuteRule {
|
|
89
|
+
mode: 'none' | 'always' | 'schedule';
|
|
90
|
+
schedule_rules?: {
|
|
91
|
+
task_id: string;
|
|
92
|
+
start_at: string;
|
|
93
|
+
end_at: string;
|
|
94
|
+
enabled: boolean;
|
|
95
|
+
}[];
|
|
96
|
+
recurring_rules?: {
|
|
97
|
+
task_id: string;
|
|
98
|
+
weekdays: number[];
|
|
99
|
+
start_time: string;
|
|
100
|
+
end_time: string;
|
|
101
|
+
enabled: boolean;
|
|
102
|
+
}[];
|
|
103
|
+
}
|
|
104
|
+
export interface MemberMuteState {
|
|
105
|
+
member_openid: string;
|
|
106
|
+
mute_expire_at: string;
|
|
107
|
+
username: string;
|
|
108
|
+
union_openid: string;
|
|
109
|
+
}
|
|
110
|
+
export interface SetMemberMuteState {
|
|
111
|
+
op: 'add' | 'update' | 'del';
|
|
112
|
+
member_openid: string;
|
|
113
|
+
mute_expire_at?: string;
|
|
114
|
+
}
|
|
115
|
+
export interface JoinApprovalStrategy {
|
|
116
|
+
strategy_id: string;
|
|
117
|
+
group_openids: string[];
|
|
118
|
+
group_ids: string[];
|
|
119
|
+
whitelist_user_count: number;
|
|
120
|
+
is_enable: 'on' | 'off';
|
|
121
|
+
expire_at: string;
|
|
122
|
+
created_at: string;
|
|
123
|
+
updated_at: string;
|
|
124
|
+
remark: string;
|
|
125
|
+
}
|
|
126
|
+
export interface GroupAction {
|
|
127
|
+
op: 'add' | 'del';
|
|
128
|
+
group_openids?: string[];
|
|
129
|
+
group_ids?: string[];
|
|
130
|
+
}
|
|
131
|
+
export interface UploadPrepareData {
|
|
132
|
+
file_type: FileType;
|
|
133
|
+
file_name: string;
|
|
134
|
+
file_size: string;
|
|
135
|
+
md5: string;
|
|
136
|
+
sha1: string;
|
|
137
|
+
md5_10m: string;
|
|
138
|
+
}
|
|
139
|
+
export interface UploadPart {
|
|
140
|
+
index: number;
|
|
141
|
+
presigned_url: string;
|
|
142
|
+
block_size: string;
|
|
143
|
+
}
|
|
144
|
+
export interface UploadConfig {
|
|
145
|
+
concurrency: number;
|
|
146
|
+
retry_timeout: number;
|
|
147
|
+
retry_delay: number;
|
|
148
|
+
}
|
|
149
|
+
export interface UploadPrepareResult {
|
|
150
|
+
upload_id: string;
|
|
151
|
+
block_size: string;
|
|
152
|
+
parts: UploadPart[];
|
|
153
|
+
upload_config: UploadConfig;
|
|
154
|
+
}
|
|
155
|
+
export interface UploadPartFinishData {
|
|
156
|
+
upload_id: string;
|
|
157
|
+
part_index: number;
|
|
158
|
+
block_size: string;
|
|
159
|
+
md5: string;
|
|
160
|
+
}
|
|
161
|
+
export interface StreamMessageData {
|
|
162
|
+
input_mode?: 'append' | 'replace';
|
|
163
|
+
input_state?: 1 | 10;
|
|
164
|
+
content_type?: 'text' | 'markdown';
|
|
165
|
+
content_raw?: string;
|
|
166
|
+
event_id?: string;
|
|
167
|
+
msg_id?: string;
|
|
168
|
+
msg_seq?: number;
|
|
169
|
+
index?: number;
|
|
170
|
+
stream_msg_id?: string;
|
|
171
|
+
is_wakeup?: boolean;
|
|
172
|
+
}
|
|
173
|
+
export interface StreamMessageResult {
|
|
174
|
+
id: string;
|
|
175
|
+
timestamp: string;
|
|
176
|
+
ext_info?: {
|
|
177
|
+
ref_idx?: string;
|
|
178
|
+
};
|
|
179
|
+
remain_msg_len?: number;
|
|
57
180
|
}
|
|
58
181
|
export interface Options {
|
|
59
182
|
app_id: string;
|
|
@@ -68,4 +191,5 @@ export interface Options {
|
|
|
68
191
|
gatewayURL?: string;
|
|
69
192
|
base_url_gateway?: string;
|
|
70
193
|
base_url_app_access_token?: string;
|
|
194
|
+
sessionStore?: SessionStore;
|
|
71
195
|
}
|
package/lib/sends.d.ts
CHANGED
|
@@ -5,12 +5,16 @@ export declare const GROUP_AT_MESSAGE_CREATE: (client: Client, event: {
|
|
|
5
5
|
ChannelId: string;
|
|
6
6
|
MessageId?: string;
|
|
7
7
|
_tag?: string;
|
|
8
|
-
}, val: DataEnums[]
|
|
8
|
+
}, val: DataEnums[], options?: {
|
|
9
|
+
forceVerifyImageResource?: boolean;
|
|
10
|
+
}) => Promise<ClientAPIMessageResult[]>;
|
|
9
11
|
export declare const C2C_MESSAGE_CREATE: (client: Client, event: {
|
|
10
12
|
UserId: string;
|
|
11
13
|
MessageId?: string;
|
|
12
14
|
_tag?: string;
|
|
13
|
-
}, val: DataEnums[]
|
|
15
|
+
}, val: DataEnums[], options?: {
|
|
16
|
+
forceVerifyImageResource?: boolean;
|
|
17
|
+
}) => Promise<ClientAPIMessageResult[]>;
|
|
14
18
|
export declare const DIRECT_MESSAGE_CREATE: (client: Client, event: {
|
|
15
19
|
UserId: string;
|
|
16
20
|
MessageId?: string;
|
package/lib/sends.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
|
-
import { createResult, ResultCode } from 'alemonjs';
|
|
2
|
+
import { createResult, ResultCode, logger } from 'alemonjs';
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import { dataEnumToText, markdownToText, buttonsToText } from './format.js';
|
|
5
5
|
import { getQQBotConfig } from './config.js';
|
|
6
6
|
|
|
7
7
|
const MAX_BUTTON_ROWS = 5;
|
|
8
8
|
const MAX_BUTTONS_PER_ROW = 5;
|
|
9
|
+
const BUTTON_STYLE_MAP = {
|
|
10
|
+
gray: 0,
|
|
11
|
+
blue: 1,
|
|
12
|
+
red: 3,
|
|
13
|
+
'blue-fill': 4
|
|
14
|
+
};
|
|
15
|
+
const resolveButtonStyle = (style) => {
|
|
16
|
+
if (typeof style === 'number') {
|
|
17
|
+
return style;
|
|
18
|
+
}
|
|
19
|
+
if (typeof style === 'string') {
|
|
20
|
+
if (BUTTON_STYLE_MAP[style] !== undefined) {
|
|
21
|
+
return BUTTON_STYLE_MAP[style];
|
|
22
|
+
}
|
|
23
|
+
if (style.trim() !== '' && !Number.isNaN(Number(style))) {
|
|
24
|
+
return Number(style);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
};
|
|
9
29
|
const createButtonsData = (rows, startId = 0) => {
|
|
10
30
|
let id = startId;
|
|
11
31
|
const clippedRows = rows.slice(0, MAX_BUTTON_ROWS);
|
|
@@ -18,25 +38,45 @@ const createButtonsData = (rows, startId = 0) => {
|
|
|
18
38
|
const typing = options?.type ?? 'command';
|
|
19
39
|
const typeMap = { command: 2, link: 0, call: 1 };
|
|
20
40
|
const rowData = options?.rawData ?? {};
|
|
41
|
+
const data = options?.data;
|
|
42
|
+
const modal = options?.modal ??
|
|
43
|
+
(typeof data === 'object' && data
|
|
44
|
+
? {
|
|
45
|
+
content: data.click,
|
|
46
|
+
confirmText: data.confirm,
|
|
47
|
+
cancelText: data.cancel
|
|
48
|
+
}
|
|
49
|
+
: undefined);
|
|
50
|
+
const action = {
|
|
51
|
+
type: typeMap[typing],
|
|
52
|
+
permission: {
|
|
53
|
+
type: typeof options.permission?.type === 'undefined' ? 2 : options?.permission?.type,
|
|
54
|
+
specify_user_ids: options?.permission?.userIds,
|
|
55
|
+
specify_role_ids: options?.permission?.roleIds
|
|
56
|
+
},
|
|
57
|
+
unsupport_tips: options?.toolTip ?? '',
|
|
58
|
+
data: typeof data === 'string' ? data : '',
|
|
59
|
+
at_bot_show_channel_list: options?.atBotShowChannelList ?? false,
|
|
60
|
+
enter: options?.autoEnter ?? false,
|
|
61
|
+
reply: options?.reply ?? false,
|
|
62
|
+
anchor: options?.anchor ?? 0,
|
|
63
|
+
click_limit: options?.clickLimit
|
|
64
|
+
};
|
|
65
|
+
if (modal) {
|
|
66
|
+
action.modal = {
|
|
67
|
+
content: modal.content ?? '是否确认操作?',
|
|
68
|
+
confirm_text: modal.confirmText ?? '是',
|
|
69
|
+
cancel_text: modal.cancelText ?? '否'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
21
72
|
return {
|
|
22
73
|
id: String(id),
|
|
23
74
|
render_data: {
|
|
24
75
|
label: value,
|
|
25
76
|
visited_label: value,
|
|
26
|
-
style: options
|
|
27
|
-
},
|
|
28
|
-
action: {
|
|
29
|
-
type: typeMap[typing],
|
|
30
|
-
permission: {
|
|
31
|
-
type: typeof options.permission?.type === 'undefined' ? 2 : options?.permission?.type,
|
|
32
|
-
specify_user_ids: options?.permission?.userIds,
|
|
33
|
-
specify_role_ids: options?.permission?.roleIds
|
|
34
|
-
},
|
|
35
|
-
unsupport_tips: options?.toolTip ?? '',
|
|
36
|
-
data: options?.data ?? '',
|
|
37
|
-
at_bot_show_channel_list: false,
|
|
38
|
-
enter: options?.autoEnter ?? false
|
|
77
|
+
style: resolveButtonStyle(options?.style)
|
|
39
78
|
},
|
|
79
|
+
action,
|
|
40
80
|
...rowData
|
|
41
81
|
};
|
|
42
82
|
})
|
|
@@ -188,6 +228,9 @@ const extractContent = (val, mode) => {
|
|
|
188
228
|
'Image',
|
|
189
229
|
'ImageFile',
|
|
190
230
|
'ImageURL',
|
|
231
|
+
'Audio',
|
|
232
|
+
'Video',
|
|
233
|
+
'Attachment',
|
|
191
234
|
'Markdown',
|
|
192
235
|
'MarkdownOriginal',
|
|
193
236
|
'BT.group',
|
|
@@ -232,6 +275,7 @@ const buildMdAndButtonsParams = (val) => {
|
|
|
232
275
|
return null;
|
|
233
276
|
}
|
|
234
277
|
const params = {};
|
|
278
|
+
let keyboardExtra;
|
|
235
279
|
for (const item of items) {
|
|
236
280
|
if (item.type === 'ButtonTemplate') {
|
|
237
281
|
if (item?.value) {
|
|
@@ -239,6 +283,13 @@ const buildMdAndButtonsParams = (val) => {
|
|
|
239
283
|
}
|
|
240
284
|
}
|
|
241
285
|
else if (item.type === 'BT.group' && typeof item?.value !== 'string') {
|
|
286
|
+
const groupOptions = item?.options ?? {};
|
|
287
|
+
if (groupOptions.smallButton || groupOptions.rawData) {
|
|
288
|
+
keyboardExtra = {
|
|
289
|
+
smallButton: keyboardExtra?.smallButton || groupOptions.smallButton,
|
|
290
|
+
rawData: groupOptions.rawData
|
|
291
|
+
};
|
|
292
|
+
}
|
|
242
293
|
if (params['keyboard']?.content?.rows) {
|
|
243
294
|
const existingRows = params['keyboard'].content.rows;
|
|
244
295
|
const currentId = params['keyboard'].content.nextId ?? existingRows.length;
|
|
@@ -274,6 +325,14 @@ const buildMdAndButtonsParams = (val) => {
|
|
|
274
325
|
}
|
|
275
326
|
}
|
|
276
327
|
}
|
|
328
|
+
if (keyboardExtra && params['keyboard']?.content) {
|
|
329
|
+
if (keyboardExtra.smallButton) {
|
|
330
|
+
params['keyboard'].content.style = { font_size: 'small' };
|
|
331
|
+
}
|
|
332
|
+
if (keyboardExtra.rawData) {
|
|
333
|
+
Object.assign(params['keyboard'].content, keyboardExtra.rawData);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
277
336
|
if (params['keyboard']?.content?.nextId !== undefined) {
|
|
278
337
|
delete params['keyboard'].content.nextId;
|
|
279
338
|
}
|
|
@@ -298,46 +357,66 @@ const buildArkParams = (val) => {
|
|
|
298
357
|
}
|
|
299
358
|
return params;
|
|
300
359
|
};
|
|
360
|
+
const MEDIA_FILE_TYPE = {
|
|
361
|
+
Image: 1,
|
|
362
|
+
ImageFile: 1,
|
|
363
|
+
ImageURL: 1,
|
|
364
|
+
Video: 2,
|
|
365
|
+
Audio: 3,
|
|
366
|
+
Attachment: 4
|
|
367
|
+
};
|
|
368
|
+
const filterMedia = (val) => {
|
|
369
|
+
return val.filter(item => MEDIA_FILE_TYPE[item.type] !== undefined);
|
|
370
|
+
};
|
|
301
371
|
const filterImages = (val) => {
|
|
302
372
|
return val.filter(item => item.type === 'Image' || item.type === 'ImageFile' || item.type === 'ImageURL');
|
|
303
373
|
};
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
374
|
+
const resolveFileData = async (item) => {
|
|
375
|
+
if (item.type === 'ImageURL') {
|
|
376
|
+
return await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
|
|
377
|
+
}
|
|
378
|
+
if (item.type === 'ImageFile') {
|
|
379
|
+
return readFileSync(item.value, 'base64');
|
|
380
|
+
}
|
|
381
|
+
const value = item.value;
|
|
382
|
+
if (typeof value === 'string') {
|
|
383
|
+
if (value.startsWith('https://') || value.startsWith('http://')) {
|
|
384
|
+
return await axios.get(value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
|
|
310
385
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
fileData = await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
|
|
314
|
-
}
|
|
315
|
-
else if (typeof item.value === 'string' && item.value.startsWith('file://')) {
|
|
316
|
-
const localFilePath = item.value.replace('file://', '');
|
|
317
|
-
fileData = readFileSync(localFilePath, 'base64');
|
|
318
|
-
}
|
|
319
|
-
else if (typeof item.value === 'string' && item.value.startsWith('base64://')) {
|
|
320
|
-
fileData = item.value.replace('base64://', '');
|
|
321
|
-
}
|
|
322
|
-
else if (Buffer.isBuffer(item.value)) {
|
|
323
|
-
fileData = item.value.toString('base64');
|
|
324
|
-
}
|
|
325
|
-
else if (typeof item.value === 'string') {
|
|
326
|
-
fileData = item.value;
|
|
327
|
-
}
|
|
386
|
+
if (value.startsWith('buffer://')) {
|
|
387
|
+
return value.replace('buffer://', '');
|
|
328
388
|
}
|
|
329
|
-
|
|
330
|
-
|
|
389
|
+
if (value.startsWith('file://')) {
|
|
390
|
+
return readFileSync(value.replace('file://', ''), 'base64');
|
|
331
391
|
}
|
|
332
|
-
if (
|
|
333
|
-
|
|
392
|
+
if (value.startsWith('base64://')) {
|
|
393
|
+
return value.replace('base64://', '');
|
|
334
394
|
}
|
|
335
|
-
|
|
336
|
-
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
if (Buffer.isBuffer(value)) {
|
|
398
|
+
return value.toString('base64');
|
|
399
|
+
}
|
|
400
|
+
return undefined;
|
|
401
|
+
};
|
|
402
|
+
const resolveMediaUrl = async (media, uploadMedia) => {
|
|
403
|
+
for (const item of media) {
|
|
404
|
+
const fileData = await resolveFileData(item);
|
|
405
|
+
if (fileData) {
|
|
406
|
+
const fileInfo = await uploadMedia({ file_type: MEDIA_FILE_TYPE[item.type] ?? 1, file_data: fileData }).then(res => res?.file_info);
|
|
407
|
+
if (fileInfo) {
|
|
408
|
+
return fileInfo;
|
|
409
|
+
}
|
|
337
410
|
}
|
|
338
411
|
}
|
|
339
412
|
return undefined;
|
|
340
413
|
};
|
|
414
|
+
const stripMediaPlaceholders = (text) => text
|
|
415
|
+
.replace(/\[附件[^\]]*\]/g, '')
|
|
416
|
+
.replace(/\[音频\]/g, '')
|
|
417
|
+
.replace(/\[视频\]/g, '')
|
|
418
|
+
.replace(/\n{2,}/g, '\n')
|
|
419
|
+
.trim();
|
|
341
420
|
const flattenMdToText = (content, val) => {
|
|
342
421
|
const mdItems = val.filter(item => item.type === 'Markdown');
|
|
343
422
|
const btnItems = val.filter(item => item.type === 'BT.group');
|
|
@@ -357,19 +436,19 @@ const flattenMdToText = (content, val) => {
|
|
|
357
436
|
.join('\n')
|
|
358
437
|
.replace(/^[^\S\n\r]+|[^\S\n\r]+$/g, '');
|
|
359
438
|
};
|
|
360
|
-
const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMessage, label) => {
|
|
439
|
+
const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMessage, label, options) => {
|
|
361
440
|
const config = getQQBotConfig();
|
|
362
441
|
const mdToText = config.markdownToText === true;
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
const
|
|
366
|
-
if (!
|
|
367
|
-
return [createResult(ResultCode.Fail, '
|
|
442
|
+
const media = filterMedia(val);
|
|
443
|
+
if (media.length > 0) {
|
|
444
|
+
const fileInfo = await resolveMediaUrl(media, uploadMedia);
|
|
445
|
+
if (!fileInfo) {
|
|
446
|
+
return [createResult(ResultCode.Fail, '媒体上传失败', null)];
|
|
368
447
|
}
|
|
369
|
-
const
|
|
448
|
+
const mediaContent = stripMediaPlaceholders(flattenMdToText(content, val));
|
|
370
449
|
const res = await sendMessage({
|
|
371
|
-
content:
|
|
372
|
-
media: { file_info:
|
|
450
|
+
content: mediaContent,
|
|
451
|
+
media: { file_info: fileInfo },
|
|
373
452
|
msg_type: 7,
|
|
374
453
|
...baseParams
|
|
375
454
|
});
|
|
@@ -392,7 +471,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
|
|
|
392
471
|
if (mdParams.markdown?.content && content) {
|
|
393
472
|
mdParams.markdown.content = content + '\n' + mdParams.markdown.content;
|
|
394
473
|
}
|
|
395
|
-
const res = await sendMessage({ content, msg_type: 2, ...mdParams, ...baseParams });
|
|
474
|
+
const res = await sendMessage({ content, msg_type: 2, ...mdParams, ...baseParams, force_verify_image_resource: options?.forceVerifyImageResource });
|
|
396
475
|
return [createResult(ResultCode.Ok, label, { id: res.id })];
|
|
397
476
|
}
|
|
398
477
|
const arkParams = buildArkParams(val);
|
|
@@ -473,21 +552,21 @@ const sendGuildMessage = async (content, val, baseParams, sendMessage, label) =>
|
|
|
473
552
|
}
|
|
474
553
|
return [];
|
|
475
554
|
};
|
|
476
|
-
const GROUP_AT_MESSAGE_CREATE = async (client, event, val) => {
|
|
555
|
+
const GROUP_AT_MESSAGE_CREATE = async (client, event, val, options) => {
|
|
477
556
|
const baseParams = buildBaseParams(event._tag, event.MessageId, 'INTERACTION_CREATE_GROUP');
|
|
478
557
|
const content = extractContent(val, 'group');
|
|
479
558
|
try {
|
|
480
|
-
return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByGroup(event.ChannelId, data), data => client.groupOpenMessages(event.ChannelId, data), 'client.groupOpenMessages');
|
|
559
|
+
return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByGroup(event.ChannelId, data), data => client.groupOpenMessages(event.ChannelId, data), 'client.groupOpenMessages', options);
|
|
481
560
|
}
|
|
482
561
|
catch (err) {
|
|
483
562
|
return [createResult(ResultCode.Fail, err?.response?.data ?? err?.message ?? err, null)];
|
|
484
563
|
}
|
|
485
564
|
};
|
|
486
|
-
const C2C_MESSAGE_CREATE = async (client, event, val) => {
|
|
565
|
+
const C2C_MESSAGE_CREATE = async (client, event, val, options) => {
|
|
487
566
|
const baseParams = buildBaseParams(event._tag, event.MessageId, 'INTERACTION_CREATE_C2C');
|
|
488
567
|
const content = extractContent(val, 'group');
|
|
489
568
|
try {
|
|
490
|
-
return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByUser(event.UserId, data), data => client.usersOpenMessages(event.UserId, data), 'client.usersOpenMessages');
|
|
569
|
+
return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByUser(event.UserId, data), data => client.usersOpenMessages(event.UserId, data), 'client.usersOpenMessages', options);
|
|
491
570
|
}
|
|
492
571
|
catch (err) {
|
|
493
572
|
return [createResult(ResultCode.Fail, err?.response?.data ?? err?.message ?? err, null)];
|