mioku-plugin-media 1.0.0 → 2.1.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/README.md +10 -1
- package/config.md +6 -1
- package/config.ts +2 -0
- package/index.ts +19 -2
- package/package.json +10 -1
- package/platforms/amagi-client.ts +18 -2
- package/platforms/resolvers/bilibili.ts +101 -12
- package/platforms/resolvers/douyin.ts +137 -8
- package/platforms/resolvers/index.ts +2 -2
- package/platforms/resolvers/kuaishou.ts +89 -7
- package/platforms/resolvers/xiaohongshu.ts +56 -1
- package/platforms/types.ts +72 -1
- package/platforms/url-parser.ts +59 -25
- package/runtime.ts +1 -1
- package/skills.ts +6 -3
- package/types.ts +22 -1
- package/utils/error-handler.ts +26 -33
- package/utils/message.ts +206 -58
package/platforms/types.ts
CHANGED
|
@@ -3,6 +3,77 @@ export type Platform = "bilibili" | "douyin" | "kuaishou" | "xiaohongshu";
|
|
|
3
3
|
export interface ParsedMediaUrl {
|
|
4
4
|
platform: Platform;
|
|
5
5
|
id: string;
|
|
6
|
-
subtype?: "video" | "article" | "note" | "bangumi";
|
|
6
|
+
subtype?: "video" | "article" | "note" | "bangumi" | "live";
|
|
7
7
|
extra?: Record<string, string>;
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
export interface AmagiClient {
|
|
11
|
+
bilibili: {
|
|
12
|
+
fetcher: {
|
|
13
|
+
fetchVideoInfo(options: { bvid: string; typeMode?: "strict" | "loose" }): Promise<{
|
|
14
|
+
success: boolean;
|
|
15
|
+
data?: any;
|
|
16
|
+
message?: string;
|
|
17
|
+
}>;
|
|
18
|
+
fetchVideoStreamUrl(options: { avid: number; cid: number; typeMode?: "strict" | "loose" }): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
data?: any;
|
|
21
|
+
message?: string;
|
|
22
|
+
}>;
|
|
23
|
+
fetchLiveRoomInfo(options: { room_id: string; typeMode?: "strict" | "loose" }): Promise<{
|
|
24
|
+
success: boolean;
|
|
25
|
+
data?: any;
|
|
26
|
+
message?: string;
|
|
27
|
+
}>;
|
|
28
|
+
fetchLiveRoomInitInfo(options: { room_id: string; typeMode?: "strict" | "loose" }): Promise<{
|
|
29
|
+
success: boolean;
|
|
30
|
+
data?: any;
|
|
31
|
+
message?: string;
|
|
32
|
+
}>;
|
|
33
|
+
fetchUserCard(options: { host_mid: number; typeMode?: "strict" | "loose" }): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
data?: any;
|
|
36
|
+
message?: string;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
kuaishou: {
|
|
41
|
+
fetcher: {
|
|
42
|
+
fetchVideoWork(options: {
|
|
43
|
+
photoId: string;
|
|
44
|
+
typeMode?: "strict" | "loose";
|
|
45
|
+
}): Promise<{
|
|
46
|
+
success: boolean;
|
|
47
|
+
code?: number | string | undefined;
|
|
48
|
+
data: any;
|
|
49
|
+
message?: string;
|
|
50
|
+
error?: any;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
douyin: {
|
|
55
|
+
fetcher: {
|
|
56
|
+
parseWork(options: {
|
|
57
|
+
aweme_id: string;
|
|
58
|
+
typeMode?: "strict" | "loose";
|
|
59
|
+
}): Promise<{
|
|
60
|
+
success: boolean;
|
|
61
|
+
data?: any;
|
|
62
|
+
message?: string;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
xiaohongshu: {
|
|
67
|
+
fetcher: {
|
|
68
|
+
fetchNoteDetail(options: {
|
|
69
|
+
note_id: string;
|
|
70
|
+
xsec_token: string;
|
|
71
|
+
typeMode?: "strict" | "loose";
|
|
72
|
+
}): Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
data?: any;
|
|
75
|
+
message?: string;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
package/platforms/url-parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ParsedMediaUrl } from "./types";
|
|
2
2
|
|
|
3
3
|
const BILIBILI_DOMAINS = [
|
|
4
4
|
"bilibili.com",
|
|
@@ -20,7 +20,14 @@ const DOUYIN_DOMAINS = [
|
|
|
20
20
|
"jingxuan.douyin.com",
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
const KUAISHOU_DOMAINS = [
|
|
23
|
+
const KUAISHOU_DOMAINS = [
|
|
24
|
+
"kuaishou.com",
|
|
25
|
+
"www.kuaishou.com",
|
|
26
|
+
"v.kuaishou.com",
|
|
27
|
+
"v.m.chenzhongtech.com",
|
|
28
|
+
"m.chenzhongtech.com",
|
|
29
|
+
"chenzhongtech.com",
|
|
30
|
+
];
|
|
24
31
|
|
|
25
32
|
const XIAOHONGSHU_DOMAINS = [
|
|
26
33
|
"xiaohongshu.com",
|
|
@@ -31,8 +38,10 @@ const XIAOHONGSHU_DOMAINS = [
|
|
|
31
38
|
const BV_REGEX = /\b(BV[a-zA-Z0-9]{10,})\b/;
|
|
32
39
|
const AV_REGEX = /\b(av(\d+))\b/i;
|
|
33
40
|
const URL_REGEX = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
|
|
41
|
+
const BILIBILI_LIVE_REGEX = /\/(\d+)(?:\?|$)/;
|
|
34
42
|
const DOUYIN_ID_REGEX = /\/video\/(\d+)/;
|
|
35
43
|
const KUAISHOU_ID_REGEX = /\/short-video\/([a-zA-Z0-9_-]+)/;
|
|
44
|
+
const KUAISHOU_PHOTO_REGEX = /(?:\/fw)?\/photo\/([a-zA-Z0-9_-]+)/;
|
|
36
45
|
const XHS_NOTE_REGEX = /\/explore\/([a-f0-9]{24})/;
|
|
37
46
|
const XHS_DISCOVERY_REGEX = /\/discovery\/item\/([a-f0-9]{24})/;
|
|
38
47
|
const XHSLINK_NOTE_REGEX = /\/([a-f0-9]{24})/;
|
|
@@ -133,6 +142,21 @@ function parseBilibiliUrl(url: string): ParsedMediaUrl | null {
|
|
|
133
142
|
if (articleMatch) {
|
|
134
143
|
return { platform: "bilibili", id: articleMatch[1], subtype: "article" };
|
|
135
144
|
}
|
|
145
|
+
|
|
146
|
+
const liveMatch = url.match(/\/live\/(\d+)/);
|
|
147
|
+
if (liveMatch) {
|
|
148
|
+
return { platform: "bilibili", id: liveMatch[1], subtype: "live" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
hostname === "live.bilibili.com" ||
|
|
153
|
+
hostname.endsWith(".live.bilibili.com")
|
|
154
|
+
) {
|
|
155
|
+
const roomMatch = url.match(/\/(\d+)(?:\?|$)/);
|
|
156
|
+
if (roomMatch) {
|
|
157
|
+
return { platform: "bilibili", id: roomMatch[1], subtype: "live" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
return null;
|
|
@@ -174,11 +198,18 @@ function parseKuaishouUrl(url: string): ParsedMediaUrl | null {
|
|
|
174
198
|
return { platform: "kuaishou", id: shortMatch[1], subtype: "video" };
|
|
175
199
|
}
|
|
176
200
|
|
|
177
|
-
const photoMatch = url.match(
|
|
201
|
+
const photoMatch = url.match(KUAISHOU_PHOTO_REGEX);
|
|
178
202
|
if (photoMatch) {
|
|
179
203
|
return { platform: "kuaishou", id: photoMatch[1], subtype: "video" };
|
|
180
204
|
}
|
|
181
205
|
|
|
206
|
+
// Handle v.kuaishou.com/xxx short URLs like https://v.kuaishou.com/KfhlEcGV
|
|
207
|
+
const shortCodeMatch = url.match(/:\/\/[^/]+\/([a-zA-Z0-9_-]+)/);
|
|
208
|
+
if (shortCodeMatch) {
|
|
209
|
+
// Store full URL so isShortUrl can detect it and resolveShortUrl can work
|
|
210
|
+
return { platform: "kuaishou", id: url, subtype: "video" };
|
|
211
|
+
}
|
|
212
|
+
|
|
182
213
|
return null;
|
|
183
214
|
}
|
|
184
215
|
|
|
@@ -363,29 +394,29 @@ export function extractMediaUrlFromEvent(event: any): ParsedMediaUrl | null {
|
|
|
363
394
|
|
|
364
395
|
export function resolveShortUrl(url: string): Promise<string> {
|
|
365
396
|
return new Promise((resolve) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
controller.abort();
|
|
370
|
-
resolve(url);
|
|
371
|
-
}, 5000);
|
|
372
|
-
|
|
373
|
-
fetch(url, {
|
|
374
|
-
redirect: "follow",
|
|
375
|
-
signal: controller.signal,
|
|
376
|
-
method: "HEAD",
|
|
377
|
-
})
|
|
378
|
-
.then((response) => {
|
|
379
|
-
clearTimeout(timeout);
|
|
380
|
-
resolve(response.url || url);
|
|
381
|
-
})
|
|
382
|
-
.catch(() => {
|
|
383
|
-
clearTimeout(timeout);
|
|
384
|
-
resolve(url);
|
|
385
|
-
});
|
|
386
|
-
} catch {
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const timeout = setTimeout(() => {
|
|
399
|
+
controller.abort();
|
|
387
400
|
resolve(url);
|
|
388
|
-
}
|
|
401
|
+
}, 10000);
|
|
402
|
+
|
|
403
|
+
fetch(url, {
|
|
404
|
+
redirect: "follow",
|
|
405
|
+
signal: controller.signal,
|
|
406
|
+
})
|
|
407
|
+
.then((response) => {
|
|
408
|
+
clearTimeout(timeout);
|
|
409
|
+
const finalUrl = response.url;
|
|
410
|
+
if (finalUrl && finalUrl !== url) {
|
|
411
|
+
resolve(finalUrl);
|
|
412
|
+
} else {
|
|
413
|
+
resolve(url);
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.catch(() => {
|
|
417
|
+
clearTimeout(timeout);
|
|
418
|
+
resolve(url);
|
|
419
|
+
});
|
|
389
420
|
});
|
|
390
421
|
}
|
|
391
422
|
|
|
@@ -399,5 +430,8 @@ export function isShortUrl(parsed: ParsedMediaUrl): boolean {
|
|
|
399
430
|
if (parsed.platform === "douyin" && parsed.id.startsWith("http")) {
|
|
400
431
|
return true;
|
|
401
432
|
}
|
|
433
|
+
if (parsed.platform === "kuaishou" && parsed.id.startsWith("http")) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
402
436
|
return false;
|
|
403
437
|
}
|
package/runtime.ts
CHANGED
package/skills.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { AISkill, AITool } from "
|
|
2
|
-
import type { SendNodeContentElement } from "napcat-sdk";
|
|
1
|
+
import type { AISkill, AITool } from "mioku";
|
|
3
2
|
import { getMediaRuntimeState } from "./runtime";
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseMediaUrl,
|
|
5
|
+
resolveShortUrl,
|
|
6
|
+
isShortUrl,
|
|
7
|
+
} from "./platforms/url-parser";
|
|
5
8
|
import { resolveMedia } from "./platforms/resolvers";
|
|
6
9
|
import { buildInfoMessage, sendMediaResult } from "./utils/message";
|
|
7
10
|
|
package/types.ts
CHANGED
|
@@ -20,9 +20,30 @@ export interface ParsedMediaResult {
|
|
|
20
20
|
videoUrl: string;
|
|
21
21
|
duration?: number;
|
|
22
22
|
stats?: MediaStats;
|
|
23
|
+
liveStatus?: string;
|
|
24
|
+
/** 图集/合辑的图片列表 */
|
|
25
|
+
images?: string[];
|
|
26
|
+
/** 图集/合辑的短视频/实况图视频URL列表 */
|
|
27
|
+
videoUrls?: string[];
|
|
28
|
+
/** 图集/合辑的背景音乐URL */
|
|
29
|
+
musicUrl?: string;
|
|
30
|
+
/** 是否包含实况图 */
|
|
31
|
+
hasLivePhoto?: boolean;
|
|
32
|
+
/** 是否为合辑(图集中的多图组合) */
|
|
33
|
+
isSlides?: boolean;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
export interface MediaRuntimeState {
|
|
26
37
|
config: MediaConfig;
|
|
27
|
-
amagiClient:
|
|
38
|
+
amagiClient: {
|
|
39
|
+
kuaishou: {
|
|
40
|
+
fetcher: {
|
|
41
|
+
fetchVideoWork(options: { photoId: string; typeMode?: "strict" | "loose" }): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
data: any;
|
|
44
|
+
message?: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
28
49
|
}
|
package/utils/error-handler.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { MiokiContext } from "mioki";
|
|
2
2
|
import type { MediaConfig } from "../types";
|
|
3
|
+
import type { ParsedMediaUrl } from "../platforms/types";
|
|
4
|
+
import type { ParsedMediaResult } from "../types";
|
|
5
|
+
import type { SendNodeElement, SendNodeContentElement } from "napcat-sdk";
|
|
3
6
|
|
|
4
7
|
function normalizeErrorMessage(error: unknown): string {
|
|
5
8
|
if (error instanceof Error && error.message) {
|
|
@@ -22,46 +25,36 @@ export async function handleMediaError(options: {
|
|
|
22
25
|
platform: string;
|
|
23
26
|
config: MediaConfig;
|
|
24
27
|
}): Promise<void> {
|
|
25
|
-
const { ctx,
|
|
28
|
+
const { ctx, error, platform } = options;
|
|
26
29
|
const errorMessage = normalizeErrorMessage(error);
|
|
30
|
+
ctx.logger.error(`[media] ${platform} 解析失败: ${errorMessage}`);
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
// 尝试获取 bot 发送错误信息给用户
|
|
33
|
+
const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
|
|
34
|
+
const bot =
|
|
35
|
+
selfId != null && typeof ctx?.pickBot === "function"
|
|
36
|
+
? ctx.pickBot(selfId)
|
|
37
|
+
: undefined;
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
const ckExpired = cause?.error?.errorDescription?.includes("ck可能已经失效");
|
|
39
|
+
if (!bot) return;
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (config.debug) {
|
|
39
|
-
await event.reply(`${platform} 解析失败: ${errorMessage}`, true);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
41
|
+
const nickname = String(
|
|
42
|
+
ctx?.bot?.nickname || event?.sender?.card || event?.sender?.nickname || "媒体解析",
|
|
43
|
+
);
|
|
44
|
+
const userId = String(selfId || ctx?.bot?.bot_id || event?.self_id || 0);
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
const
|
|
46
|
+
// 构建错误提示消息
|
|
47
|
+
const errorText = `【${platform}】解析失败\n${errorMessage}`;
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await
|
|
49
|
-
event,
|
|
50
|
-
instruction: `媒体解析插件在解析${platform}内容时失败,错误信息: ${errorMessage}。请简短告知用户解析失败,并建议稍后重试或检查链接是否有效。`,
|
|
51
|
-
send: true,
|
|
52
|
-
promptInjections: [
|
|
53
|
-
{
|
|
54
|
-
title: "Media Plugin Notice",
|
|
55
|
-
content:
|
|
56
|
-
"A media link parsing action was triggered. Judge whether the user likely intended this action or triggered it accidentally. If it looks accidental or like a casual mention, weave a natural reply into the conversation without mentioning the plugin, tools, or commands. If the user seems to want this feature, respond helpfully. Keep the response concise.",
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
});
|
|
49
|
+
try {
|
|
50
|
+
if (event?.message_type === "group" && event?.group_id != null) {
|
|
51
|
+
await bot.sendGroupMsg(event.group_id, [ctx.segment.text(errorText)]);
|
|
60
52
|
return;
|
|
61
|
-
} catch (noticeError) {
|
|
62
|
-
ctx.logger.error(`[media] AI notice 发送失败: ${normalizeErrorMessage(noticeError)}`, noticeError);
|
|
63
53
|
}
|
|
54
|
+
if (event?.user_id != null) {
|
|
55
|
+
await bot.sendPrivateMsg(event.user_id, [ctx.segment.text(errorText)]);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore send errors
|
|
64
59
|
}
|
|
65
|
-
|
|
66
|
-
await event.reply(`解析失败,请稍后重试或检查链接是否有效`, true);
|
|
67
60
|
}
|
package/utils/message.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
SendNodeElement,
|
|
3
|
+
SendNodeContentElement,
|
|
4
|
+
ForwardDisplayOptions,
|
|
5
|
+
} from "napcat-sdk";
|
|
2
6
|
import type { ParsedMediaResult, MediaStats } from "../types";
|
|
3
7
|
import type { ParsedMediaUrl } from "../platforms/types";
|
|
4
8
|
|
|
@@ -37,13 +41,20 @@ function formatCount(count?: number): string {
|
|
|
37
41
|
return String(count);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
export function buildInfoMessage(
|
|
44
|
+
export function buildInfoMessage(
|
|
45
|
+
parsed: ParsedMediaUrl,
|
|
46
|
+
result: ParsedMediaResult,
|
|
47
|
+
): string {
|
|
41
48
|
const platform = PLATFORM_NAMES[parsed.platform] || parsed.platform;
|
|
42
49
|
const lines: string[] = [];
|
|
43
50
|
|
|
44
51
|
lines.push(`【${platform}】${result.title}`);
|
|
45
52
|
lines.push(`作者:${result.author}`);
|
|
46
53
|
|
|
54
|
+
if (result.liveStatus) {
|
|
55
|
+
lines.push(`状态:${result.liveStatus}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
if (result.duration && result.duration > 0) {
|
|
48
59
|
lines.push(`时长:${formatDuration(result.duration)}`);
|
|
49
60
|
}
|
|
@@ -53,22 +64,43 @@ export function buildInfoMessage(parsed: ParsedMediaUrl, result: ParsedMediaResu
|
|
|
53
64
|
lines.push(`简介:${desc}`);
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
// 对于图片内容,显示数量信息
|
|
68
|
+
if (result.images && result.images.length > 0) {
|
|
69
|
+
lines.push(`图片:${result.images.length}张`);
|
|
70
|
+
}
|
|
71
|
+
if (result.videoUrls && result.videoUrls.length > 0) {
|
|
72
|
+
lines.push(`视频:${result.videoUrls.length}个`);
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
return lines.join("\n");
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
function buildSummaryText(
|
|
78
|
+
function buildSummaryText(
|
|
79
|
+
platform: string,
|
|
80
|
+
subtype: string | undefined,
|
|
81
|
+
stats?: MediaStats,
|
|
82
|
+
): string {
|
|
60
83
|
if (!stats) return "";
|
|
61
84
|
|
|
62
85
|
const parts: string[] = [];
|
|
63
86
|
|
|
64
87
|
if (platform === "bilibili") {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
if (subtype === "live") {
|
|
89
|
+
if (stats.views != null && stats.views > 0)
|
|
90
|
+
parts.push(`在线${formatCount(stats.views)}`);
|
|
91
|
+
if (stats.comments != null && stats.comments > 0)
|
|
92
|
+
parts.push(`关注${formatCount(stats.comments)}`);
|
|
93
|
+
} else {
|
|
94
|
+
if (stats.likes != null) parts.push(`赞${formatCount(stats.likes)}`);
|
|
95
|
+
if (stats.coins != null) parts.push(`币${formatCount(stats.coins)}`);
|
|
96
|
+
if (stats.favorites != null)
|
|
97
|
+
parts.push(`藏${formatCount(stats.favorites)}`);
|
|
98
|
+
if (stats.shares != null) parts.push(`转${formatCount(stats.shares)}`);
|
|
99
|
+
}
|
|
69
100
|
} else {
|
|
70
101
|
if (stats.likes != null) parts.push(`赞${formatCount(stats.likes)}`);
|
|
71
|
-
if (stats.favorites != null)
|
|
102
|
+
if (stats.favorites != null)
|
|
103
|
+
parts.push(`藏${formatCount(stats.favorites)}`);
|
|
72
104
|
if (stats.shares != null) parts.push(`转${formatCount(stats.shares)}`);
|
|
73
105
|
if (stats.comments != null) parts.push(`评${formatCount(stats.comments)}`);
|
|
74
106
|
}
|
|
@@ -76,16 +108,32 @@ function buildSummaryText(platform: string, stats?: MediaStats): string {
|
|
|
76
108
|
return parts.join(" ");
|
|
77
109
|
}
|
|
78
110
|
|
|
79
|
-
function buildForwardDisplay(
|
|
111
|
+
function buildForwardDisplay(
|
|
112
|
+
parsed: ParsedMediaUrl,
|
|
113
|
+
result: ParsedMediaResult,
|
|
114
|
+
): ForwardDisplayOptions {
|
|
80
115
|
const displayTitle = PLATFORM_DISPLAY_TITLES[parsed.platform] || "媒体解析";
|
|
81
116
|
|
|
117
|
+
let summary = buildSummaryText(parsed.platform, parsed.subtype, result.stats);
|
|
118
|
+
|
|
119
|
+
if (result.liveStatus) {
|
|
120
|
+
summary = summary ? `${result.liveStatus} ${summary}` : result.liveStatus;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 对于图片集/合辑,增加内容数量信息
|
|
124
|
+
if (result.images && result.images.length > 0) {
|
|
125
|
+
const imageInfo = `${result.images.length}张图片`;
|
|
126
|
+
summary = summary ? `${imageInfo} ${summary}` : imageInfo;
|
|
127
|
+
}
|
|
128
|
+
if (result.videoUrls && result.videoUrls.length > 0) {
|
|
129
|
+
const videoInfo = `${result.videoUrls.length}个视频`;
|
|
130
|
+
summary = summary ? `${videoInfo} ${summary}` : videoInfo;
|
|
131
|
+
}
|
|
132
|
+
|
|
82
133
|
return {
|
|
83
134
|
source: displayTitle,
|
|
84
|
-
news: [
|
|
85
|
-
|
|
86
|
-
{ text: result.author },
|
|
87
|
-
],
|
|
88
|
-
summary: buildSummaryText(parsed.platform, result.stats),
|
|
135
|
+
news: [{ text: truncateText(result.title, 26) }, { text: result.author }],
|
|
136
|
+
summary,
|
|
89
137
|
};
|
|
90
138
|
}
|
|
91
139
|
|
|
@@ -116,6 +164,15 @@ function buildForwardNodes(
|
|
|
116
164
|
): SendNodeElement[] {
|
|
117
165
|
const nodes: SendNodeElement[] = [];
|
|
118
166
|
|
|
167
|
+
// 简介文字优先
|
|
168
|
+
const infoText = buildInfoMessage(parsed, result);
|
|
169
|
+
nodes.push({
|
|
170
|
+
type: "node",
|
|
171
|
+
user_id: userId,
|
|
172
|
+
nickname,
|
|
173
|
+
content: [ctx.segment.text(infoText)],
|
|
174
|
+
} as SendNodeContentElement);
|
|
175
|
+
|
|
119
176
|
if (result.coverUrl) {
|
|
120
177
|
nodes.push({
|
|
121
178
|
type: "node",
|
|
@@ -125,15 +182,34 @@ function buildForwardNodes(
|
|
|
125
182
|
} as SendNodeContentElement);
|
|
126
183
|
}
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
185
|
+
// 处理图片集/合辑
|
|
186
|
+
if (result.images && result.images.length > 0) {
|
|
187
|
+
// 如果既有图片又有视频/实况图,发送纯图片
|
|
188
|
+
// 否则直接发送图片
|
|
189
|
+
for (const imageUrl of result.images) {
|
|
190
|
+
nodes.push({
|
|
191
|
+
type: "node",
|
|
192
|
+
user_id: userId,
|
|
193
|
+
nickname,
|
|
194
|
+
content: [ctx.segment.image(imageUrl)],
|
|
195
|
+
} as SendNodeContentElement);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
135
198
|
|
|
136
|
-
|
|
199
|
+
// 处理短视频/实况图视频
|
|
200
|
+
if (result.videoUrls && result.videoUrls.length > 0) {
|
|
201
|
+
for (const videoUrl of result.videoUrls) {
|
|
202
|
+
nodes.push({
|
|
203
|
+
type: "node",
|
|
204
|
+
user_id: userId,
|
|
205
|
+
nickname,
|
|
206
|
+
content: [(ctx.segment as any).video(videoUrl)],
|
|
207
|
+
} as SendNodeContentElement);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 处理常规视频(单个视频)
|
|
212
|
+
if (result.videoUrl && (!result.videoUrls || result.videoUrls.length === 0)) {
|
|
137
213
|
nodes.push({
|
|
138
214
|
type: "node",
|
|
139
215
|
user_id: userId,
|
|
@@ -194,7 +270,10 @@ export async function sendMediaResult(
|
|
|
194
270
|
}
|
|
195
271
|
|
|
196
272
|
const nickname = String(
|
|
197
|
-
ctx?.bot?.nickname ||
|
|
273
|
+
ctx?.bot?.nickname ||
|
|
274
|
+
event?.sender?.card ||
|
|
275
|
+
event?.sender?.nickname ||
|
|
276
|
+
"媒体解析",
|
|
198
277
|
);
|
|
199
278
|
const userId = String(selfId || ctx?.bot?.bot_id || event?.self_id || 0);
|
|
200
279
|
|
|
@@ -202,43 +281,112 @@ export async function sendMediaResult(
|
|
|
202
281
|
const forwardPayload = toOneBotForwardFormat(nodes);
|
|
203
282
|
const display = buildForwardDisplay(parsed, result);
|
|
204
283
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
284
|
+
if (event?.message_type === "group" && event?.group_id != null) {
|
|
285
|
+
await bot.api("send_group_forward_msg", {
|
|
286
|
+
group_id: event.group_id,
|
|
287
|
+
messages: forwardPayload,
|
|
288
|
+
source: display.source,
|
|
289
|
+
news: display.news,
|
|
290
|
+
summary: display.summary,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
216
294
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
295
|
+
if (event?.user_id != null) {
|
|
296
|
+
await bot.api("send_private_forward_msg", {
|
|
297
|
+
user_id: event.user_id,
|
|
298
|
+
messages: forwardPayload,
|
|
299
|
+
source: display.source,
|
|
300
|
+
news: display.news,
|
|
301
|
+
summary: display.summary,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function sendDurationLimitResult(
|
|
307
|
+
ctx: any,
|
|
308
|
+
event: any,
|
|
309
|
+
parsed: ParsedMediaUrl,
|
|
310
|
+
result: ParsedMediaResult,
|
|
311
|
+
limitMinutes: number,
|
|
312
|
+
): Promise<void> {
|
|
313
|
+
const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
|
|
314
|
+
const bot =
|
|
315
|
+
selfId != null && typeof ctx?.pickBot === "function"
|
|
316
|
+
? ctx.pickBot(selfId)
|
|
317
|
+
: undefined;
|
|
318
|
+
|
|
319
|
+
if (!bot) {
|
|
320
|
+
await event.reply(
|
|
321
|
+
`【${PLATFORM_NAMES[parsed.platform] || parsed.platform}】视频太大了,发不出来~`,
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const nickname = String(
|
|
327
|
+
ctx?.bot?.nickname ||
|
|
328
|
+
event?.sender?.card ||
|
|
329
|
+
event?.sender?.nickname ||
|
|
330
|
+
"媒体解析",
|
|
331
|
+
);
|
|
332
|
+
const userId = String(selfId || ctx?.bot?.bot_id || event?.self_id || 0);
|
|
333
|
+
|
|
334
|
+
const nodes: SendNodeElement[] = [];
|
|
335
|
+
|
|
336
|
+
const platform = PLATFORM_NAMES[parsed.platform] || parsed.platform;
|
|
337
|
+
const mins = Math.floor((result.duration || 0) / 60);
|
|
338
|
+
const secs = (result.duration || 0) % 60;
|
|
339
|
+
const infoText = `【${platform}】${result.title}\n作者:${result.author}\n时长:${mins}分${secs}秒\n\n哎嘿,视频太大了发不出来~请选择更短的视频(不超过 ${limitMinutes} 分钟)`;
|
|
340
|
+
|
|
341
|
+
nodes.push({
|
|
342
|
+
type: "node",
|
|
343
|
+
user_id: userId,
|
|
344
|
+
nickname,
|
|
345
|
+
content: [ctx.segment.text(infoText)],
|
|
346
|
+
} as SendNodeContentElement);
|
|
347
|
+
|
|
348
|
+
if (result.coverUrl) {
|
|
349
|
+
nodes.push({
|
|
350
|
+
type: "node",
|
|
351
|
+
user_id: userId,
|
|
352
|
+
nickname,
|
|
353
|
+
content: [ctx.segment.image(result.coverUrl)],
|
|
354
|
+
} as SendNodeContentElement);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 只发图片,不发视频
|
|
358
|
+
if (result.images && result.images.length > 0) {
|
|
359
|
+
for (const imageUrl of result.images) {
|
|
360
|
+
nodes.push({
|
|
361
|
+
type: "node",
|
|
362
|
+
user_id: userId,
|
|
363
|
+
nickname,
|
|
364
|
+
content: [ctx.segment.image(imageUrl)],
|
|
365
|
+
} as SendNodeContentElement);
|
|
239
366
|
}
|
|
240
367
|
}
|
|
241
368
|
|
|
242
|
-
const
|
|
243
|
-
|
|
369
|
+
const forwardPayload = toOneBotForwardFormat(nodes);
|
|
370
|
+
const displayTitle = PLATFORM_DISPLAY_TITLES[parsed.platform] || "媒体解析";
|
|
371
|
+
|
|
372
|
+
if (event?.message_type === "group" && event?.group_id != null) {
|
|
373
|
+
await bot.api("send_group_forward_msg", {
|
|
374
|
+
group_id: event.group_id,
|
|
375
|
+
messages: forwardPayload,
|
|
376
|
+
source: displayTitle,
|
|
377
|
+
news: [{ text: truncateText(result.title, 26) }, { text: result.author }],
|
|
378
|
+
summary: `视频太长无法发送(${mins}分${secs}秒)`,
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (event?.user_id != null) {
|
|
384
|
+
await bot.api("send_private_forward_msg", {
|
|
385
|
+
user_id: event.user_id,
|
|
386
|
+
messages: forwardPayload,
|
|
387
|
+
source: displayTitle,
|
|
388
|
+
news: [{ text: truncateText(result.title, 26) }, { text: result.author }],
|
|
389
|
+
summary: `视频太长无法发送(${mins}分${secs}秒)`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
244
392
|
}
|