@ryantest/openclaw-qqbot 1.6.6-alpha.4 → 1.6.7-beta.2
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 +24 -15
- package/README.zh.md +24 -15
- package/dist/src/api.d.ts +32 -5
- package/dist/src/api.js +111 -12
- package/dist/src/channel.d.ts +18 -0
- package/dist/src/channel.js +85 -2
- package/dist/src/config.d.ts +33 -2
- package/dist/src/config.js +125 -1
- package/dist/src/gateway.js +566 -24
- package/dist/src/group-history.d.ts +136 -0
- package/dist/src/group-history.js +226 -0
- package/dist/src/message-gating.d.ts +53 -0
- package/dist/src/message-gating.js +107 -0
- package/dist/src/message-queue.d.ts +36 -0
- package/dist/src/message-queue.js +164 -22
- package/dist/src/outbound.d.ts +4 -4
- package/dist/src/outbound.js +18 -6
- package/dist/src/ref-index-store.js +5 -28
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.d.ts +6 -0
- package/dist/src/slash-commands.js +3 -3
- package/dist/src/tools/remind.js +17 -9
- package/dist/src/types.d.ts +90 -2
- package/dist/src/utils/audio-convert.d.ts +1 -1
- package/dist/src/utils/audio-convert.js +1 -1
- package/dist/src/utils/chunked-upload.d.ts +11 -2
- package/dist/src/utils/chunked-upload.js +63 -11
- package/dist/src/utils/media-send.js +1 -1
- package/dist/src/utils/text-parsing.js +7 -18
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.sh +11 -3
- package/scripts/upgrade-via-source.sh +63 -15
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +135 -7
- package/src/channel.ts +85 -2
- package/src/config.ts +170 -3
- package/src/gateway.ts +662 -29
- package/src/group-history.ts +328 -0
- package/src/message-gating.ts +190 -0
- package/src/message-queue.ts +201 -21
- package/src/openclaw-plugin-sdk.d.ts +65 -0
- package/src/outbound.ts +18 -6
- package/src/ref-index-store.ts +5 -27
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +3 -3
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +94 -2
- package/src/utils/audio-convert.ts +1 -1
- package/src/utils/chunked-upload.ts +76 -12
- package/src/utils/media-send.ts +1 -2
- package/src/utils/text-parsing.ts +7 -14
package/src/types.ts
CHANGED
|
@@ -26,6 +26,31 @@ export interface ResolvedQQBotAccount {
|
|
|
26
26
|
config: QQBotAccountConfig;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** 群消息策略:open=全响应 | allowlist=白名单 | disabled=不响应 */
|
|
30
|
+
export type GroupPolicy = "open" | "allowlist" | "disabled";
|
|
31
|
+
|
|
32
|
+
/** 工具策略:full=全部 | restricted=限制敏感工具 | none=禁止 */
|
|
33
|
+
export type ToolPolicy = "full" | "restricted" | "none";
|
|
34
|
+
|
|
35
|
+
/** 单个群的配置 */
|
|
36
|
+
export interface GroupConfig {
|
|
37
|
+
/** 是否需要 @机器人才响应(默认 true) */
|
|
38
|
+
requireMention?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 是否忽略 @了其他用户但没有 @机器人的消息(默认 false)。
|
|
41
|
+
* 开启后,消息中 @了其他人但未 @bot 时直接丢弃(不记录历史、不触发 AI)。
|
|
42
|
+
*/
|
|
43
|
+
ignoreOtherMentions?: boolean;
|
|
44
|
+
/** 群聊中 AI 可使用的工具范围(默认 restricted) */
|
|
45
|
+
toolPolicy?: ToolPolicy;
|
|
46
|
+
/** 群名称 */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** 群消息行为 PE(未配置时使用内置默认值) */
|
|
49
|
+
prompt?: string;
|
|
50
|
+
/** 群历史消息缓存条数(0 禁用,默认 20) */
|
|
51
|
+
historyLimit?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
/**
|
|
30
55
|
* QQ Bot 账户配置
|
|
31
56
|
*/
|
|
@@ -37,6 +62,12 @@ export interface QQBotAccountConfig {
|
|
|
37
62
|
clientSecretFile?: string;
|
|
38
63
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
39
64
|
allowFrom?: string[];
|
|
65
|
+
/** 群消息策略(默认 allowlist) */
|
|
66
|
+
groupPolicy?: GroupPolicy;
|
|
67
|
+
/** 群白名单(groupPolicy 为 allowlist 时生效) */
|
|
68
|
+
groupAllowFrom?: string[];
|
|
69
|
+
/** 群配置映射(按 groupOpenid 索引,"*" 为默认) */
|
|
70
|
+
groups?: Record<string, GroupConfig>;
|
|
40
71
|
/** 系统提示词,会添加在用户消息前面 */
|
|
41
72
|
systemPrompt?: string;
|
|
42
73
|
/** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
|
|
@@ -66,8 +97,8 @@ export interface QQBotAccountConfig {
|
|
|
66
97
|
upgradeUrl?: string;
|
|
67
98
|
/**
|
|
68
99
|
* /bot-upgrade 指令的行为模式
|
|
69
|
-
* - "doc"
|
|
70
|
-
* - "hot-reload":检测到新版本时直接执行 npm
|
|
100
|
+
* - "doc":展示升级文档链接(安全模式)
|
|
101
|
+
* - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
|
|
71
102
|
*/
|
|
72
103
|
upgradeMode?: "doc" | "hot-reload";
|
|
73
104
|
/**
|
|
@@ -199,6 +230,8 @@ export interface GroupMessageEvent {
|
|
|
199
230
|
author: {
|
|
200
231
|
id: string;
|
|
201
232
|
member_openid: string;
|
|
233
|
+
username?: string;
|
|
234
|
+
bot?: boolean;
|
|
202
235
|
};
|
|
203
236
|
content: string;
|
|
204
237
|
id: string;
|
|
@@ -210,6 +243,65 @@ export interface GroupMessageEvent {
|
|
|
210
243
|
ext?: string[];
|
|
211
244
|
};
|
|
212
245
|
attachments?: MessageAttachment[];
|
|
246
|
+
/** @提及列表 */
|
|
247
|
+
mentions?: Array<{
|
|
248
|
+
scope?: "all" | "single";
|
|
249
|
+
id?: string;
|
|
250
|
+
user_openid?: string;
|
|
251
|
+
member_openid?: string;
|
|
252
|
+
nickname?: string;
|
|
253
|
+
bot?: boolean;
|
|
254
|
+
/** 是否 @机器人自身 */
|
|
255
|
+
is_you?: boolean;
|
|
256
|
+
}>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 按钮交互事件(INTERACTION_CREATE)
|
|
261
|
+
*/
|
|
262
|
+
export interface InteractionEvent {
|
|
263
|
+
/** 事件 ID,用于回应交互(PUT /interactions/{id}) */
|
|
264
|
+
id: string;
|
|
265
|
+
/** 事件类型:11=消息按钮 12=单聊快捷菜单 */
|
|
266
|
+
type: number;
|
|
267
|
+
/** 场景:c2c / group / guild */
|
|
268
|
+
scene?: string;
|
|
269
|
+
/** 场景类型:0=频道 1=群聊 2=单聊 */
|
|
270
|
+
chat_type?: number;
|
|
271
|
+
/** 触发时间 RFC3339 */
|
|
272
|
+
timestamp?: string;
|
|
273
|
+
/** 频道 openid(仅频道场景) */
|
|
274
|
+
guild_id?: string;
|
|
275
|
+
/** 子频道 openid(仅频道场景) */
|
|
276
|
+
channel_id?: string;
|
|
277
|
+
/** 单聊用户 openid(仅 c2c 场景) */
|
|
278
|
+
user_openid?: string;
|
|
279
|
+
/** 群 openid(仅群聊场景) */
|
|
280
|
+
group_openid?: string;
|
|
281
|
+
/** 群内触发用户 openid(仅群聊场景) */
|
|
282
|
+
group_member_openid?: string;
|
|
283
|
+
version: number;
|
|
284
|
+
data: {
|
|
285
|
+
type: number;
|
|
286
|
+
resolved: {
|
|
287
|
+
/** 按钮 action.data 值 */
|
|
288
|
+
button_data?: string;
|
|
289
|
+
/** 按钮 id */
|
|
290
|
+
button_id?: string;
|
|
291
|
+
/** 操作用户 userid(仅频道场景) */
|
|
292
|
+
user_id?: string;
|
|
293
|
+
/** 自定义菜单 id(仅菜单场景) */
|
|
294
|
+
feature_id?: string;
|
|
295
|
+
/** 操作的消息 id(仅频道场景) */
|
|
296
|
+
message_id?: string;
|
|
297
|
+
/** 配置更新:群消息模式 "mention"=@机器人时激活 "always"=总是激活 */
|
|
298
|
+
require_mention?: string;
|
|
299
|
+
/** 配置更新:群消息策略 */
|
|
300
|
+
group_policy?: GroupPolicy;
|
|
301
|
+
/** 配置更新:@文本的名称提及BOT名,多个使用,分隔 */
|
|
302
|
+
mention_patterns?: string;
|
|
303
|
+
};
|
|
304
|
+
};
|
|
213
305
|
}
|
|
214
306
|
|
|
215
307
|
/**
|
|
@@ -412,7 +412,7 @@ const QQ_NATIVE_UPLOAD_FORMATS = [".wav", ".mp3", ".silk"];
|
|
|
412
412
|
* 将本地音频文件转换为 QQ Bot 可上传的 Base64
|
|
413
413
|
*
|
|
414
414
|
* QQ Bot API 支持直传 WAV、MP3、SILK 三种格式,其他格式仍需转换。
|
|
415
|
-
*
|
|
415
|
+
* 转换策略:
|
|
416
416
|
*
|
|
417
417
|
* 1. WAV / MP3 / SILK → 直传(跳过转换)
|
|
418
418
|
* 2. 有 ffmpeg → ffmpeg 万能解码为 PCM → silk-wasm 编码
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
type UploadPrepareResponse,
|
|
21
21
|
type UploadPrepareHashes,
|
|
22
22
|
type MediaUploadResponse,
|
|
23
|
+
ApiError,
|
|
24
|
+
UPLOAD_PREPARE_FALLBACK_CODE,
|
|
23
25
|
c2cUploadPrepare,
|
|
24
26
|
c2cUploadPartFinish,
|
|
25
27
|
c2cCompleteUpload,
|
|
@@ -30,8 +32,32 @@ import {
|
|
|
30
32
|
} from "../api.js";
|
|
31
33
|
import { formatFileSize } from "./file-utils.js";
|
|
32
34
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* upload_prepare 返回特定错误码(40093002)时抛出:文件超过每日累积上传限制
|
|
37
|
+
* 调用方根据携带的文件信息构造兜底文案发送给用户
|
|
38
|
+
*/
|
|
39
|
+
export class UploadDailyLimitExceededError extends Error {
|
|
40
|
+
/** 触发错误的本地文件路径 */
|
|
41
|
+
public readonly filePath: string;
|
|
42
|
+
/** 文件大小(字节) */
|
|
43
|
+
public readonly fileSize: number;
|
|
44
|
+
|
|
45
|
+
constructor(filePath: string, fileSize: number, originalMessage: string) {
|
|
46
|
+
super(originalMessage);
|
|
47
|
+
this.name = "UploadDailyLimitExceededError";
|
|
48
|
+
this.filePath = filePath;
|
|
49
|
+
this.fileSize = fileSize;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 分片上传默认并发数(服务端未返回 concurrency 时的兜底) */
|
|
54
|
+
const DEFAULT_CONCURRENT_PARTS = 1;
|
|
55
|
+
|
|
56
|
+
/** 分片上传并发上限(即使服务端返回更大的值也不超过此限制) */
|
|
57
|
+
const MAX_CONCURRENT_PARTS = 10;
|
|
58
|
+
|
|
59
|
+
/** partFinish 特定错误码重试超时上限(10 分钟),即使服务端 retry_timeout 更大也不超过此值 */
|
|
60
|
+
const MAX_PART_FINISH_RETRY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
35
61
|
|
|
36
62
|
/** 单个分片上传超时(毫秒)— 5 分钟,兼容低带宽场景 */
|
|
37
63
|
const PART_UPLOAD_TIMEOUT = 300_000;
|
|
@@ -55,8 +81,6 @@ export interface ChunkedUploadProgress {
|
|
|
55
81
|
export interface ChunkedUploadOptions {
|
|
56
82
|
/** 进度回调 */
|
|
57
83
|
onProgress?: (progress: ChunkedUploadProgress) => void;
|
|
58
|
-
/** 最大并发数(默认 2) */
|
|
59
|
-
maxConcurrent?: number;
|
|
60
84
|
/** 日志前缀 */
|
|
61
85
|
logPrefix?: string;
|
|
62
86
|
}
|
|
@@ -81,7 +105,6 @@ export async function chunkedUploadC2C(
|
|
|
81
105
|
options?: ChunkedUploadOptions,
|
|
82
106
|
): Promise<MediaUploadResponse> {
|
|
83
107
|
const prefix = options?.logPrefix ?? "[chunked-upload]";
|
|
84
|
-
const maxConcurrent = options?.maxConcurrent ?? MAX_CONCURRENT_PARTS;
|
|
85
108
|
|
|
86
109
|
// 1. 读取文件信息
|
|
87
110
|
const stat = await fs.promises.stat(filePath);
|
|
@@ -98,13 +121,34 @@ export async function chunkedUploadC2C(
|
|
|
98
121
|
// 3. 申请上传 → 获取 upload_id + block_size + 预签名链接
|
|
99
122
|
const accessToken = await getAccessToken(appId, clientSecret);
|
|
100
123
|
console.log(`${prefix} >>> Calling c2cUploadPrepare(fileType=${fileType}, fileName=${fileName}, fileSize=${fileSize}, md5=${hashes.md5}, sha1=${hashes.sha1}, md5_10m=${hashes.md5_10m})`);
|
|
101
|
-
|
|
124
|
+
let prepareResp: UploadPrepareResponse;
|
|
125
|
+
try {
|
|
126
|
+
prepareResp = await c2cUploadPrepare(accessToken, userId, fileType, fileName, fileSize, hashes);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// 命中特定错误码 → 携带文件信息抛出,由上层构造兜底文案
|
|
129
|
+
if (err instanceof ApiError && err.bizCode === UPLOAD_PREPARE_FALLBACK_CODE) {
|
|
130
|
+
console.warn(`${prefix} c2cUploadPrepare hit fallback code ${UPLOAD_PREPARE_FALLBACK_CODE}`);
|
|
131
|
+
throw new UploadDailyLimitExceededError(filePath, fileSize, err.message);
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
102
135
|
console.log(`${prefix} <<< c2cUploadPrepare response:`, JSON.stringify(prepareResp));
|
|
103
136
|
const { upload_id, parts } = prepareResp;
|
|
104
137
|
// QQ 开放平台返回的 block_size 可能是字符串,需要转为数字
|
|
105
138
|
const block_size = Number(prepareResp.block_size);
|
|
106
139
|
|
|
107
|
-
|
|
140
|
+
// 并发数:使用 API 返回的 concurrency,未返回则用默认值,且不超过上限
|
|
141
|
+
const maxConcurrent = Math.min(
|
|
142
|
+
prepareResp.concurrency ? Number(prepareResp.concurrency) : DEFAULT_CONCURRENT_PARTS,
|
|
143
|
+
MAX_CONCURRENT_PARTS,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// partFinish 特定错误码的重试超时:使用 API 返回的 retry_timeout(秒),上限 10 分钟
|
|
147
|
+
const retryTimeoutMs = prepareResp.retry_timeout
|
|
148
|
+
? Math.min(Number(prepareResp.retry_timeout) * 1000, MAX_PART_FINISH_RETRY_TIMEOUT_MS)
|
|
149
|
+
: undefined;
|
|
150
|
+
|
|
151
|
+
console.log(`${prefix} Upload prepared: upload_id=${upload_id}, block_size=${formatFileSize(block_size)}, parts=${parts.length}, concurrency=${maxConcurrent}, retryTimeout=${retryTimeoutMs ? retryTimeoutMs / 1000 + 's' : 'default'}`);
|
|
108
152
|
|
|
109
153
|
// 4. 并行上传所有分片(带并发控制)
|
|
110
154
|
let completedParts = 0;
|
|
@@ -131,7 +175,7 @@ export async function chunkedUploadC2C(
|
|
|
131
175
|
// b. 通知开放平台分片上传完成(需要重新获取 token,避免长时间上传后 token 过期)
|
|
132
176
|
const token = await getAccessToken(appId, clientSecret);
|
|
133
177
|
console.log(`${prefix} >>> Calling c2cUploadPartFinish(upload_id=${upload_id}, partIndex=${partIndex}, blockSize=${length}, md5=${md5Hex})`);
|
|
134
|
-
await c2cUploadPartFinish(token, userId, upload_id, partIndex, length, md5Hex);
|
|
178
|
+
await c2cUploadPartFinish(token, userId, upload_id, partIndex, length, md5Hex, retryTimeoutMs);
|
|
135
179
|
console.log(`${prefix} <<< c2cUploadPartFinish(partIndex=${partIndex}) done`);
|
|
136
180
|
|
|
137
181
|
// 更新进度
|
|
@@ -188,7 +232,6 @@ export async function chunkedUploadGroup(
|
|
|
188
232
|
options?: ChunkedUploadOptions,
|
|
189
233
|
): Promise<MediaUploadResponse> {
|
|
190
234
|
const prefix = options?.logPrefix ?? "[chunked-upload]";
|
|
191
|
-
const maxConcurrent = options?.maxConcurrent ?? MAX_CONCURRENT_PARTS;
|
|
192
235
|
|
|
193
236
|
// 1. 读取文件信息
|
|
194
237
|
const stat = await fs.promises.stat(filePath);
|
|
@@ -205,13 +248,34 @@ export async function chunkedUploadGroup(
|
|
|
205
248
|
// 3. 申请上传
|
|
206
249
|
const accessToken = await getAccessToken(appId, clientSecret);
|
|
207
250
|
console.log(`${prefix} >>> Calling groupUploadPrepare(fileType=${fileType}, fileName=${fileName}, fileSize=${fileSize}, md5=${hashes.md5}, sha1=${hashes.sha1}, md5_10m=${hashes.md5_10m})`);
|
|
208
|
-
|
|
251
|
+
let prepareResp: UploadPrepareResponse;
|
|
252
|
+
try {
|
|
253
|
+
prepareResp = await groupUploadPrepare(accessToken, groupId, fileType, fileName, fileSize, hashes);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// 命中特定错误码 → 携带文件信息抛出,由上层构造兜底文案
|
|
256
|
+
if (err instanceof ApiError && err.bizCode === UPLOAD_PREPARE_FALLBACK_CODE) {
|
|
257
|
+
console.warn(`${prefix} groupUploadPrepare hit fallback code ${UPLOAD_PREPARE_FALLBACK_CODE}`);
|
|
258
|
+
throw new UploadDailyLimitExceededError(filePath, fileSize, err.message);
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
209
262
|
console.log(`${prefix} <<< groupUploadPrepare response:`, JSON.stringify(prepareResp));
|
|
210
263
|
const { upload_id, parts } = prepareResp;
|
|
211
264
|
// QQ 开放平台返回的 block_size 可能是字符串,需要转为数字
|
|
212
265
|
const block_size = Number(prepareResp.block_size);
|
|
213
266
|
|
|
214
|
-
|
|
267
|
+
// 并发数:使用 API 返回的 concurrency,未返回则用默认值,且不超过上限
|
|
268
|
+
const maxConcurrent = Math.min(
|
|
269
|
+
prepareResp.concurrency ? Number(prepareResp.concurrency) : DEFAULT_CONCURRENT_PARTS,
|
|
270
|
+
MAX_CONCURRENT_PARTS,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// partFinish 特定错误码的重试超时:使用 API 返回的 retry_timeout(秒),上限 10 分钟
|
|
274
|
+
const retryTimeoutMs = prepareResp.retry_timeout
|
|
275
|
+
? Math.min(Number(prepareResp.retry_timeout) * 1000, MAX_PART_FINISH_RETRY_TIMEOUT_MS)
|
|
276
|
+
: undefined;
|
|
277
|
+
|
|
278
|
+
console.log(`${prefix} Upload prepared: upload_id=${upload_id}, block_size=${formatFileSize(block_size)}, parts=${parts.length}, concurrency=${maxConcurrent}, retryTimeout=${retryTimeoutMs ? retryTimeoutMs / 1000 + 's' : 'default'}`);
|
|
215
279
|
|
|
216
280
|
// 4. 并行上传所有分片(带并发控制)
|
|
217
281
|
let completedParts = 0;
|
|
@@ -232,7 +296,7 @@ export async function chunkedUploadGroup(
|
|
|
232
296
|
|
|
233
297
|
const token = await getAccessToken(appId, clientSecret);
|
|
234
298
|
console.log(`${prefix} >>> Calling groupUploadPartFinish(upload_id=${upload_id}, partIndex=${partIndex}, blockSize=${length}, md5=${md5Hex})`);
|
|
235
|
-
await groupUploadPartFinish(token, groupId, upload_id, partIndex, length, md5Hex);
|
|
299
|
+
await groupUploadPartFinish(token, groupId, upload_id, partIndex, length, md5Hex, retryTimeoutMs);
|
|
236
300
|
console.log(`${prefix} <<< groupUploadPartFinish(partIndex=${partIndex}) done`);
|
|
237
301
|
|
|
238
302
|
completedParts++;
|
package/src/utils/media-send.ts
CHANGED
|
@@ -364,6 +364,7 @@ export async function executeSendQueue(
|
|
|
364
364
|
};
|
|
365
365
|
|
|
366
366
|
for (const item of queue) {
|
|
367
|
+
const FALLBACK_MSG = "发送失败,请稍后重试。";
|
|
367
368
|
try {
|
|
368
369
|
if (item.type === "text") {
|
|
369
370
|
if (options.skipInterTagText) {
|
|
@@ -380,8 +381,6 @@ export async function executeSendQueue(
|
|
|
380
381
|
|
|
381
382
|
log?.info(`${prefix} executeSendQueue: sending ${item.type}: ${item.content.slice(0, 80)}...`);
|
|
382
383
|
|
|
383
|
-
const FALLBACK_MSG = "发送失败,请稍后重试。";
|
|
384
|
-
|
|
385
384
|
if (item.type === "image") {
|
|
386
385
|
const result = await sendPhoto(mediaTarget, item.content);
|
|
387
386
|
if (result.error) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { RefAttachmentSummary } from "../ref-index-store.js";
|
|
6
|
+
import { inferAttachmentType } from "../group-history.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
|
|
@@ -65,18 +66,10 @@ export function buildAttachmentSummaries(
|
|
|
65
66
|
localPaths?: Array<string | null>,
|
|
66
67
|
): RefAttachmentSummary[] | undefined {
|
|
67
68
|
if (!attachments || attachments.length === 0) return undefined;
|
|
68
|
-
return attachments.map((att, idx) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else if (ct.startsWith("application/") || ct.startsWith("text/")) type = "file";
|
|
75
|
-
return {
|
|
76
|
-
type,
|
|
77
|
-
filename: att.filename,
|
|
78
|
-
contentType: att.content_type,
|
|
79
|
-
localPath: localPaths?.[idx] ?? undefined,
|
|
80
|
-
};
|
|
81
|
-
});
|
|
69
|
+
return attachments.map((att, idx) => ({
|
|
70
|
+
type: inferAttachmentType(att.content_type),
|
|
71
|
+
filename: att.filename,
|
|
72
|
+
contentType: att.content_type,
|
|
73
|
+
localPath: localPaths?.[idx] ?? undefined,
|
|
74
|
+
}));
|
|
82
75
|
}
|