@zxh126/pi-qqbot 2.3.0 → 2.4.0
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 +370 -314
- package/README.md +518 -439
- package/approver.ts +174 -174
- package/attachment.ts +502 -502
- package/billing.ts +579 -0
- package/config.ts +281 -280
- package/constants.ts +51 -51
- package/docs/README.zh-CN.md +477 -410
- package/index.ts +1856 -1571
- package/package.json +60 -60
- package/qq/api.ts +107 -107
- package/qq/gateway.ts +265 -265
- package/qq/upload.ts +288 -288
- package/remote.ts +81 -81
- package/selfupdate.ts +212 -0
- package/slash.ts +862 -826
package/config.ts
CHANGED
|
@@ -1,280 +1,281 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { CONFIG_PATH } from "./constants";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
5
|
-
DEFAULT_ATTACHMENT_MAX_FILES,
|
|
6
|
-
DEFAULT_ATTACHMENT_MAX_PER_MESSAGE,
|
|
7
|
-
DEFAULT_ATTACHMENT_RETENTION_HOURS,
|
|
8
|
-
DEFAULT_SEND_MAX_BYTES,
|
|
9
|
-
} from "./constants";
|
|
10
|
-
import { DEFAULT_COMMANDS, type CommandConfig } from "./slash";
|
|
11
|
-
import { DEFAULT_REMOTE, type RemoteConfig } from "./remote";
|
|
12
|
-
import { DEFAULT_QUEUE_MAX } from "./queue";
|
|
13
|
-
|
|
14
|
-
export interface QueueConfig {
|
|
15
|
-
/** 忙碌时最多排队多少条消息;0 = 关闭排队(忙碌时直接拒绝) */
|
|
16
|
-
max: number;
|
|
17
|
-
/** 排队任务开始执行时是否给 QQ 回执(入队回执不受此开关控制) */
|
|
18
|
-
ack: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ApproveConfig {
|
|
22
|
-
/** 需要 QQ 审批的工具名(如 bash/powershell/write/edit) */
|
|
23
|
-
tools: string[];
|
|
24
|
-
/** 会话内记住选择:同意/拒绝后同类工具不再询问(「以后都同意」不受此开关限制) */
|
|
25
|
-
remember: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ProgressConfig {
|
|
29
|
-
/** 是否自动推送进度摘要(默认开) */
|
|
30
|
-
enabled: boolean;
|
|
31
|
-
/** 推送间隔(毫秒,默认 15000;可用 PI_QQBOT_PROGRESS_MS 覆盖,便于测试) */
|
|
32
|
-
intervalMs: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface AttachmentConfig {
|
|
36
|
-
/** 是否接收附件并落盘(false = 只回一句忽略提示) */
|
|
37
|
-
enabled: boolean;
|
|
38
|
-
/** 落盘目录;空字符串 = 系统临时文件夹下的 pi-qqbot/<bot> */
|
|
39
|
-
dir: string;
|
|
40
|
-
/** 单个附件上限(字节) */
|
|
41
|
-
maxBytes: number;
|
|
42
|
-
/** 单条消息最多处理几个附件 */
|
|
43
|
-
maxPerMessage: number;
|
|
44
|
-
/** 临时文件保留时长(小时,启动时清理更早的) */
|
|
45
|
-
retentionHours: number;
|
|
46
|
-
/** 落盘目录内最多保留多少个文件(超出删最旧) */
|
|
47
|
-
maxFiles: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface QQBotConfig {
|
|
51
|
-
appId: string;
|
|
52
|
-
appSecret: string;
|
|
53
|
-
sandbox: boolean;
|
|
54
|
-
allowlist: string[];
|
|
55
|
-
markdownReply: boolean;
|
|
56
|
-
streaming: boolean;
|
|
57
|
-
toolProgress: boolean;
|
|
58
|
-
approve: ApproveConfig;
|
|
59
|
-
/** QQ 侧斜杠命令的路由开关(/reload、/new… 以及扩展命令透传) */
|
|
60
|
-
commands: CommandConfig;
|
|
61
|
-
/** 忙碌时的消息队列 */
|
|
62
|
-
queue: QueueConfig;
|
|
63
|
-
/** 任务进度摘要推送 */
|
|
64
|
-
progress: ProgressConfig;
|
|
65
|
-
/** 收到图片/语音/视频/文档/压缩包/普通文件时的落盘设置 */
|
|
66
|
-
attachments: AttachmentConfig;
|
|
67
|
-
/** 把本机文件发到 QQ */
|
|
68
|
-
files: FilesConfig;
|
|
69
|
-
/** 「以下对话来自远程用户」提醒与系统提示规则 */
|
|
70
|
-
remote: RemoteConfig;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface FilesConfig {
|
|
74
|
-
/** 是否允许把本机文件发到 QQ(关掉后工具与 /sendfile 都拒绝) */
|
|
75
|
-
enabled: boolean;
|
|
76
|
-
/** 单个文件上限(字节,默认 30MB;QQ 硬限 200MB) */
|
|
77
|
-
maxBytes: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 配置里的一个 QQ bot。
|
|
82
|
-
*
|
|
83
|
-
* 除凭据外,所有字段都是「对全局默认值的逐键覆盖」——没写的就继承顶层
|
|
84
|
-
* `queue` / `progress` / `approve` / `commands` / `sandbox` / `allowlist`。
|
|
85
|
-
*/
|
|
86
|
-
export interface QQBotsEntry {
|
|
87
|
-
/** 展示名(日志、/status、抢占提示都用它);缺省 = `bot-<appId 后 6 位>` */
|
|
88
|
-
name?: string;
|
|
89
|
-
appId: string;
|
|
90
|
-
appSecret: string;
|
|
91
|
-
sandbox?: boolean;
|
|
92
|
-
allowlist?: string[];
|
|
93
|
-
markdownReply?: boolean;
|
|
94
|
-
streaming?: boolean;
|
|
95
|
-
toolProgress?: boolean;
|
|
96
|
-
approve?: Partial<ApproveConfig>;
|
|
97
|
-
commands?: Partial<CommandConfig>;
|
|
98
|
-
queue?: Partial<QueueConfig>;
|
|
99
|
-
progress?: Partial<ProgressConfig>;
|
|
100
|
-
attachments?: Partial<AttachmentConfig>;
|
|
101
|
-
files?: Partial<FilesConfig>;
|
|
102
|
-
remote?: Partial<RemoteConfig>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 校验并归一化后的账号(凭据 + 合并好的完整配置) */
|
|
106
|
-
export interface QQBotAccount {
|
|
107
|
-
name: string;
|
|
108
|
-
appId: string;
|
|
109
|
-
/** 该账号最终生效的配置:全局 defaults + 本账号覆盖 */
|
|
110
|
-
config: QQBotConfig;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface LoadAccountsResult {
|
|
114
|
-
accounts: QQBotAccount[];
|
|
115
|
-
/** 配置错误:有错误时**不启动网关**,避免「机器人没反应又不知道为什么」 */
|
|
116
|
-
errors: string[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const DEFAULT_PROGRESS_MS = 15000;
|
|
120
|
-
|
|
121
|
-
const asObject = (value: unknown): any =>
|
|
122
|
-
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
123
|
-
|
|
124
|
-
function readFile(): any {
|
|
125
|
-
try {
|
|
126
|
-
return asObject(JSON.parse(fs.readFileSync(CONFIG_PATH(), "utf8")));
|
|
127
|
-
} catch {
|
|
128
|
-
/* 文件不存在或非法不致命,凭据可完全来自环境变量 */
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** 缺省名字:`bot-<appId 后 6 位>`,方便在多 bot 场景里一眼分辨 */
|
|
134
|
-
export function defaultAccountName(appId: string): string {
|
|
135
|
-
const tail = appId.replace(/[^0-9a-zA-Z]/g, "").slice(-6);
|
|
136
|
-
return tail ? `bot-${tail}` : "default";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** 全局 defaults(顶层字段)与账号覆盖合并成一份可直接用的配置 */
|
|
140
|
-
export function resolveConfig(
|
|
141
|
-
file: any,
|
|
142
|
-
entry: QQBotsEntry,
|
|
143
|
-
fallback?: { appId: string; appSecret: string },
|
|
144
|
-
): QQBotConfig {
|
|
145
|
-
const appId = entry.appId ?? fallback?.appId ?? "";
|
|
146
|
-
const appSecret = entry.appSecret ?? fallback?.appSecret ?? "";
|
|
147
|
-
const envInterval = process.env.PI_QQBOT_PROGRESS_MS;
|
|
148
|
-
return {
|
|
149
|
-
appId,
|
|
150
|
-
appSecret,
|
|
151
|
-
sandbox: entry.sandbox ?? file.sandbox ?? false,
|
|
152
|
-
allowlist: entry.allowlist ?? file.allowlist ?? [],
|
|
153
|
-
markdownReply: entry.markdownReply ?? file.markdownReply ?? false,
|
|
154
|
-
streaming: entry.streaming ?? file.streaming ?? false,
|
|
155
|
-
toolProgress: entry.toolProgress ?? file.toolProgress ?? false,
|
|
156
|
-
approve: {
|
|
157
|
-
tools: entry.approve?.tools ?? file.approve?.tools ?? ["bash", "powershell", "write", "edit"],
|
|
158
|
-
remember: entry.approve?.remember ?? file.approve?.remember ?? true,
|
|
159
|
-
},
|
|
160
|
-
commands: {
|
|
161
|
-
passthrough: entry.commands?.passthrough ?? file.commands?.passthrough ?? DEFAULT_COMMANDS.passthrough,
|
|
162
|
-
allowReload: entry.commands?.allowReload ?? file.commands?.allowReload ?? DEFAULT_COMMANDS.allowReload,
|
|
163
|
-
allowQuit: entry.commands?.allowQuit ?? file.commands?.allowQuit ?? DEFAULT_COMMANDS.allowQuit,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* -
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { CONFIG_PATH } from "./constants";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
5
|
+
DEFAULT_ATTACHMENT_MAX_FILES,
|
|
6
|
+
DEFAULT_ATTACHMENT_MAX_PER_MESSAGE,
|
|
7
|
+
DEFAULT_ATTACHMENT_RETENTION_HOURS,
|
|
8
|
+
DEFAULT_SEND_MAX_BYTES,
|
|
9
|
+
} from "./constants";
|
|
10
|
+
import { DEFAULT_COMMANDS, type CommandConfig } from "./slash";
|
|
11
|
+
import { DEFAULT_REMOTE, type RemoteConfig } from "./remote";
|
|
12
|
+
import { DEFAULT_QUEUE_MAX } from "./queue";
|
|
13
|
+
|
|
14
|
+
export interface QueueConfig {
|
|
15
|
+
/** 忙碌时最多排队多少条消息;0 = 关闭排队(忙碌时直接拒绝) */
|
|
16
|
+
max: number;
|
|
17
|
+
/** 排队任务开始执行时是否给 QQ 回执(入队回执不受此开关控制) */
|
|
18
|
+
ack: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ApproveConfig {
|
|
22
|
+
/** 需要 QQ 审批的工具名(如 bash/powershell/write/edit) */
|
|
23
|
+
tools: string[];
|
|
24
|
+
/** 会话内记住选择:同意/拒绝后同类工具不再询问(「以后都同意」不受此开关限制) */
|
|
25
|
+
remember: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProgressConfig {
|
|
29
|
+
/** 是否自动推送进度摘要(默认开) */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** 推送间隔(毫秒,默认 15000;可用 PI_QQBOT_PROGRESS_MS 覆盖,便于测试) */
|
|
32
|
+
intervalMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AttachmentConfig {
|
|
36
|
+
/** 是否接收附件并落盘(false = 只回一句忽略提示) */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** 落盘目录;空字符串 = 系统临时文件夹下的 pi-qqbot/<bot> */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** 单个附件上限(字节) */
|
|
41
|
+
maxBytes: number;
|
|
42
|
+
/** 单条消息最多处理几个附件 */
|
|
43
|
+
maxPerMessage: number;
|
|
44
|
+
/** 临时文件保留时长(小时,启动时清理更早的) */
|
|
45
|
+
retentionHours: number;
|
|
46
|
+
/** 落盘目录内最多保留多少个文件(超出删最旧) */
|
|
47
|
+
maxFiles: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface QQBotConfig {
|
|
51
|
+
appId: string;
|
|
52
|
+
appSecret: string;
|
|
53
|
+
sandbox: boolean;
|
|
54
|
+
allowlist: string[];
|
|
55
|
+
markdownReply: boolean;
|
|
56
|
+
streaming: boolean;
|
|
57
|
+
toolProgress: boolean;
|
|
58
|
+
approve: ApproveConfig;
|
|
59
|
+
/** QQ 侧斜杠命令的路由开关(/reload、/new… 以及扩展命令透传) */
|
|
60
|
+
commands: CommandConfig;
|
|
61
|
+
/** 忙碌时的消息队列 */
|
|
62
|
+
queue: QueueConfig;
|
|
63
|
+
/** 任务进度摘要推送 */
|
|
64
|
+
progress: ProgressConfig;
|
|
65
|
+
/** 收到图片/语音/视频/文档/压缩包/普通文件时的落盘设置 */
|
|
66
|
+
attachments: AttachmentConfig;
|
|
67
|
+
/** 把本机文件发到 QQ */
|
|
68
|
+
files: FilesConfig;
|
|
69
|
+
/** 「以下对话来自远程用户」提醒与系统提示规则 */
|
|
70
|
+
remote: RemoteConfig;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface FilesConfig {
|
|
74
|
+
/** 是否允许把本机文件发到 QQ(关掉后工具与 /sendfile 都拒绝) */
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
/** 单个文件上限(字节,默认 30MB;QQ 硬限 200MB) */
|
|
77
|
+
maxBytes: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 配置里的一个 QQ bot。
|
|
82
|
+
*
|
|
83
|
+
* 除凭据外,所有字段都是「对全局默认值的逐键覆盖」——没写的就继承顶层
|
|
84
|
+
* `queue` / `progress` / `approve` / `commands` / `sandbox` / `allowlist`。
|
|
85
|
+
*/
|
|
86
|
+
export interface QQBotsEntry {
|
|
87
|
+
/** 展示名(日志、/status、抢占提示都用它);缺省 = `bot-<appId 后 6 位>` */
|
|
88
|
+
name?: string;
|
|
89
|
+
appId: string;
|
|
90
|
+
appSecret: string;
|
|
91
|
+
sandbox?: boolean;
|
|
92
|
+
allowlist?: string[];
|
|
93
|
+
markdownReply?: boolean;
|
|
94
|
+
streaming?: boolean;
|
|
95
|
+
toolProgress?: boolean;
|
|
96
|
+
approve?: Partial<ApproveConfig>;
|
|
97
|
+
commands?: Partial<CommandConfig>;
|
|
98
|
+
queue?: Partial<QueueConfig>;
|
|
99
|
+
progress?: Partial<ProgressConfig>;
|
|
100
|
+
attachments?: Partial<AttachmentConfig>;
|
|
101
|
+
files?: Partial<FilesConfig>;
|
|
102
|
+
remote?: Partial<RemoteConfig>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 校验并归一化后的账号(凭据 + 合并好的完整配置) */
|
|
106
|
+
export interface QQBotAccount {
|
|
107
|
+
name: string;
|
|
108
|
+
appId: string;
|
|
109
|
+
/** 该账号最终生效的配置:全局 defaults + 本账号覆盖 */
|
|
110
|
+
config: QQBotConfig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface LoadAccountsResult {
|
|
114
|
+
accounts: QQBotAccount[];
|
|
115
|
+
/** 配置错误:有错误时**不启动网关**,避免「机器人没反应又不知道为什么」 */
|
|
116
|
+
errors: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DEFAULT_PROGRESS_MS = 15000;
|
|
120
|
+
|
|
121
|
+
const asObject = (value: unknown): any =>
|
|
122
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
123
|
+
|
|
124
|
+
function readFile(): any {
|
|
125
|
+
try {
|
|
126
|
+
return asObject(JSON.parse(fs.readFileSync(CONFIG_PATH(), "utf8")));
|
|
127
|
+
} catch {
|
|
128
|
+
/* 文件不存在或非法不致命,凭据可完全来自环境变量 */
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 缺省名字:`bot-<appId 后 6 位>`,方便在多 bot 场景里一眼分辨 */
|
|
134
|
+
export function defaultAccountName(appId: string): string {
|
|
135
|
+
const tail = appId.replace(/[^0-9a-zA-Z]/g, "").slice(-6);
|
|
136
|
+
return tail ? `bot-${tail}` : "default";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 全局 defaults(顶层字段)与账号覆盖合并成一份可直接用的配置 */
|
|
140
|
+
export function resolveConfig(
|
|
141
|
+
file: any,
|
|
142
|
+
entry: QQBotsEntry,
|
|
143
|
+
fallback?: { appId: string; appSecret: string },
|
|
144
|
+
): QQBotConfig {
|
|
145
|
+
const appId = entry.appId ?? fallback?.appId ?? "";
|
|
146
|
+
const appSecret = entry.appSecret ?? fallback?.appSecret ?? "";
|
|
147
|
+
const envInterval = process.env.PI_QQBOT_PROGRESS_MS;
|
|
148
|
+
return {
|
|
149
|
+
appId,
|
|
150
|
+
appSecret,
|
|
151
|
+
sandbox: entry.sandbox ?? file.sandbox ?? false,
|
|
152
|
+
allowlist: entry.allowlist ?? file.allowlist ?? [],
|
|
153
|
+
markdownReply: entry.markdownReply ?? file.markdownReply ?? false,
|
|
154
|
+
streaming: entry.streaming ?? file.streaming ?? false,
|
|
155
|
+
toolProgress: entry.toolProgress ?? file.toolProgress ?? false,
|
|
156
|
+
approve: {
|
|
157
|
+
tools: entry.approve?.tools ?? file.approve?.tools ?? ["bash", "powershell", "write", "edit"],
|
|
158
|
+
remember: entry.approve?.remember ?? file.approve?.remember ?? true,
|
|
159
|
+
},
|
|
160
|
+
commands: {
|
|
161
|
+
passthrough: entry.commands?.passthrough ?? file.commands?.passthrough ?? DEFAULT_COMMANDS.passthrough,
|
|
162
|
+
allowReload: entry.commands?.allowReload ?? file.commands?.allowReload ?? DEFAULT_COMMANDS.allowReload,
|
|
163
|
+
allowQuit: entry.commands?.allowQuit ?? file.commands?.allowQuit ?? DEFAULT_COMMANDS.allowQuit,
|
|
164
|
+
allowUpdate: entry.commands?.allowUpdate ?? file.commands?.allowUpdate ?? DEFAULT_COMMANDS.allowUpdate,
|
|
165
|
+
allowSession: entry.commands?.allowSession ?? file.commands?.allowSession ?? DEFAULT_COMMANDS.allowSession,
|
|
166
|
+
},
|
|
167
|
+
queue: {
|
|
168
|
+
max: entry.queue?.max ?? file.queue?.max ?? DEFAULT_QUEUE_MAX,
|
|
169
|
+
ack: entry.queue?.ack ?? file.queue?.ack ?? true,
|
|
170
|
+
},
|
|
171
|
+
progress: {
|
|
172
|
+
enabled: entry.progress?.enabled ?? file.progress?.enabled ?? true,
|
|
173
|
+
intervalMs: Number(
|
|
174
|
+
envInterval ?? entry.progress?.intervalMs ?? file.progress?.intervalMs ?? DEFAULT_PROGRESS_MS,
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
attachments: {
|
|
178
|
+
enabled: entry.attachments?.enabled ?? file.attachments?.enabled ?? true,
|
|
179
|
+
dir: String(entry.attachments?.dir ?? file.attachments?.dir ?? "").trim(),
|
|
180
|
+
maxBytes: Number(
|
|
181
|
+
entry.attachments?.maxBytes ?? file.attachments?.maxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
182
|
+
),
|
|
183
|
+
maxPerMessage: Number(
|
|
184
|
+
entry.attachments?.maxPerMessage ?? file.attachments?.maxPerMessage ?? DEFAULT_ATTACHMENT_MAX_PER_MESSAGE,
|
|
185
|
+
),
|
|
186
|
+
retentionHours: Number(
|
|
187
|
+
entry.attachments?.retentionHours ?? file.attachments?.retentionHours ?? DEFAULT_ATTACHMENT_RETENTION_HOURS,
|
|
188
|
+
),
|
|
189
|
+
maxFiles: Number(entry.attachments?.maxFiles ?? file.attachments?.maxFiles ?? DEFAULT_ATTACHMENT_MAX_FILES),
|
|
190
|
+
},
|
|
191
|
+
files: {
|
|
192
|
+
enabled: entry.files?.enabled ?? file.files?.enabled ?? true,
|
|
193
|
+
maxBytes: Number(entry.files?.maxBytes ?? file.files?.maxBytes ?? DEFAULT_SEND_MAX_BYTES),
|
|
194
|
+
},
|
|
195
|
+
remote: {
|
|
196
|
+
notice: entry.remote?.notice ?? file.remote?.notice ?? DEFAULT_REMOTE.notice,
|
|
197
|
+
guidelines: entry.remote?.guidelines ?? file.remote?.guidelines ?? DEFAULT_REMOTE.guidelines,
|
|
198
|
+
extra: String(entry.remote?.extra ?? file.remote?.extra ?? DEFAULT_REMOTE.extra).trim(),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 读配置,返回**按配置顺序**排好的账号列表。
|
|
205
|
+
*
|
|
206
|
+
* 支持两种格式:
|
|
207
|
+
* - v2 多 bot:`{ "qqbots": [ { "name": "work", "appId": … }, … ], "queue": {…}, … }`
|
|
208
|
+
* - v1 单 bot:`{ "appId": …, "appSecret": …, … }`(或全部来自 `QQ_BOT_APPID/SECRET`),
|
|
209
|
+
* 等价于只有一个名叫 `default` 的账号,行为与 1.x 完全一致。
|
|
210
|
+
*/
|
|
211
|
+
export function loadAccounts(): LoadAccountsResult {
|
|
212
|
+
const file = readFile();
|
|
213
|
+
const errors: string[] = [];
|
|
214
|
+
const envAppId = process.env.QQ_BOT_APPID;
|
|
215
|
+
const envAppSecret = process.env.QQ_BOT_APPSECRET;
|
|
216
|
+
|
|
217
|
+
let entries: QQBotsEntry[];
|
|
218
|
+
if (Array.isArray(file.qqbots)) {
|
|
219
|
+
if (file.appId) {
|
|
220
|
+
errors.push("配置里同时出现顶层 appId 和 qqbots 数组:请只保留一种格式(多 bot 请写进 qqbots)");
|
|
221
|
+
return { accounts: [], errors };
|
|
222
|
+
}
|
|
223
|
+
entries = file.qqbots.map((item: any) => asObject(item));
|
|
224
|
+
if (entries.length === 0) {
|
|
225
|
+
errors.push("qqbots 是空数组:至少配置一个 bot,否则插件不会接入 QQ");
|
|
226
|
+
return { accounts: [], errors };
|
|
227
|
+
}
|
|
228
|
+
} else if (file.appId || envAppId) {
|
|
229
|
+
entries = [
|
|
230
|
+
{
|
|
231
|
+
name: "default",
|
|
232
|
+
appId: envAppId ?? file.appId,
|
|
233
|
+
appSecret: envAppSecret ?? file.appSecret,
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
} else {
|
|
237
|
+
return { accounts: [], errors };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const accounts: QQBotAccount[] = [];
|
|
241
|
+
const seenNames = new Set<string>();
|
|
242
|
+
const seenAppIds = new Set<string>();
|
|
243
|
+
|
|
244
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
245
|
+
const raw = asObject(entries[index]);
|
|
246
|
+
const appId = String(raw.appId ?? "").trim();
|
|
247
|
+
const appSecret = String(raw.appSecret ?? "").trim();
|
|
248
|
+
const label = `qqbots[${index}]${raw.name ? `(name=${raw.name})` : ""}`;
|
|
249
|
+
if (!appId || !appSecret) {
|
|
250
|
+
// 单个账号缺凭据只跳过它,别让整个插件都起不来
|
|
251
|
+
errors.push(`${label} 缺少 appId 或 appSecret,已跳过`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (seenAppIds.has(appId)) {
|
|
255
|
+
errors.push(`${label} 的 appId ${appId} 重复,已跳过(否则会双连接被平台踢)`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const name = String(raw.name ?? defaultAccountName(appId)).trim() || defaultAccountName(appId);
|
|
259
|
+
if (seenNames.has(name)) {
|
|
260
|
+
errors.push(`${label} 的 name "${name}" 与其他 bot 重复,已跳过(改名后重试)`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
seenAppIds.add(appId);
|
|
264
|
+
seenNames.add(name);
|
|
265
|
+
accounts.push({ name, appId, config: resolveConfig(file, raw) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { accounts, errors };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 兼容旧调用:只取第一个账号的配置(1.x 单 bot 语义) */
|
|
272
|
+
export function loadConfig(): QQBotConfig | null {
|
|
273
|
+
const { accounts } = loadAccounts();
|
|
274
|
+
return accounts[0]?.config ?? null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 环境变量指定的账号名(调试/测试用:钉住某个 bot,不参与自动抢占) */
|
|
278
|
+
export function pinnedAccountName(): string | null {
|
|
279
|
+
const name = process.env.PI_QQBOT_ACCOUNT?.trim();
|
|
280
|
+
return name ? name : null;
|
|
281
|
+
}
|