mioku-plugin-media 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,15 @@
9
9
  - 返回封面图、标题、作者、简介和视频文件
10
10
  - 可选配置各平台 Cookies 以获取更高质量的视频
11
11
 
12
+ ## Cookies 配置
13
+
14
+ 配置 Cookies 可获取更高画质和完整解析能力。可使用内置扫码登录命令自动获取,或手动从浏览器复制。
15
+
16
+ ### 手动获取方式
17
+
18
+ 安装 Cookie-Editor 浏览器插件并在对应平台界面登录账号,点击 Cookie-Editor 右下方的导出,选择导出为字符串即可。
19
+
20
+
12
21
  ## 支持平台
13
22
 
14
23
  ### 哔哩哔哩
@@ -39,4 +48,4 @@
39
48
  },
40
49
  "debug": false
41
50
  }
42
- ```
51
+ ```
package/config.md CHANGED
@@ -15,7 +15,7 @@ fields:
15
15
  - key: base.cookies.kuaishou
16
16
  label: 快手 Cookie
17
17
  type: textarea
18
- description: 快手 Cookie,配置后可获取更完整的视频信息。留空则使用基础解析。
18
+ description: 快手 Cookie,配置后可正常解析作品。留空则无法解析。
19
19
 
20
20
  - key: base.cookies.xiaohongshu
21
21
  label: 小红书 Cookie
@@ -26,4 +26,9 @@ fields:
26
26
  label: 调试模式
27
27
  type: switch
28
28
  description: 开启后解析失败时直接发送错误信息,不走 AI 通知
29
+
30
+ - key: base.maxVideoDurationSeconds
31
+ label: 视频解析时长上限
32
+ type: number
33
+ description: 允许解析的视频最大时长(秒),默认 1200(20分钟)。超过限制的视频将被拒绝解析。
29
34
  ---
package/config.ts CHANGED
@@ -5,6 +5,7 @@ export interface MediaConfig {
5
5
  kuaishou: string;
6
6
  xiaohongshu: string;
7
7
  };
8
+ maxVideoDurationSeconds: number;
8
9
  debug: boolean;
9
10
  }
10
11
 
@@ -15,5 +16,6 @@ export const MEDIA_DEFAULTS: MediaConfig = {
15
16
  kuaishou: "",
16
17
  xiaohongshu: "",
17
18
  },
19
+ maxVideoDurationSeconds: 20 * 60,
18
20
  debug: false,
19
21
  };
package/index.ts CHANGED
@@ -5,7 +5,7 @@ import { MEDIA_DEFAULTS } from "./config";
5
5
  import { createMediaAmagiClient } from "./platforms/amagi-client";
6
6
  import { extractMediaUrlFromEvent, resolveShortUrl, isShortUrl } from "./platforms/url-parser";
7
7
  import { resolveMedia } from "./platforms/resolvers";
8
- import { sendMediaResult } from "./utils/message";
8
+ import { sendMediaResult, sendDurationLimitResult } from "./utils/message";
9
9
  import { handleMediaError } from "./utils/error-handler";
10
10
  import { setMediaRuntimeState, resetMediaRuntimeState } from "./runtime";
11
11
 
@@ -102,6 +102,23 @@ export default definePlugin({
102
102
  }
103
103
 
104
104
  const result = await resolveMedia(amagiClient, parsed);
105
+
106
+ if (result.duration && config.maxVideoDurationSeconds > 0) {
107
+ if (result.duration > config.maxVideoDurationSeconds) {
108
+ ctx.logger.warn(
109
+ `[media] 视频时长超过限制: ${result.duration}秒 > ${config.maxVideoDurationSeconds}秒`,
110
+ );
111
+ await sendDurationLimitResult(
112
+ ctx,
113
+ event,
114
+ parsed,
115
+ result,
116
+ Math.floor(config.maxVideoDurationSeconds / 60),
117
+ );
118
+ return;
119
+ }
120
+ }
121
+
105
122
  await sendMediaResult(ctx, event, parsed, result);
106
123
  } catch (error) {
107
124
  await handleMediaError({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-media",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "流媒体解析插件,支持哔哩哔哩、抖音、小红书和快手平台",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -27,8 +27,14 @@ export function createMediaAmagiClient(config: MediaConfig): AmagiClient {
27
27
  bilibili: {
28
28
  fetcher: client.bilibili.fetcher,
29
29
  },
30
+ douyin: {
31
+ fetcher: client.douyin.fetcher,
32
+ },
30
33
  kuaishou: {
31
34
  fetcher: client.kuaishou.fetcher,
32
35
  },
36
+ xiaohongshu: {
37
+ fetcher: client.xiaohongshu.fetcher,
38
+ },
33
39
  };
34
40
  }
@@ -49,7 +49,7 @@ export class DouyinResolver implements PlatformResolver {
49
49
  detail.video?.origin_cover?.url_list?.[0] ||
50
50
  detail.video?.dynamic_cover?.url_list?.[0] ||
51
51
  "";
52
- const duration = detail.duration;
52
+ const duration = detail.duration ? Math.floor(detail.duration / 1000) : undefined;
53
53
 
54
54
  let videoUrl = "";
55
55
 
@@ -2,17 +2,74 @@ import type { ParsedMediaResult } from "../../types";
2
2
  import type { AmagiClient, ParsedMediaUrl } from "../types";
3
3
  import type { PlatformResolver } from "./types";
4
4
 
5
+ const KUAISHOU_SHORT_CODE_REGEX = /^([a-zA-Z0-9_-]+)$/;
6
+
7
+ async function resolveKuaishouShortCode(shortCode: string): Promise<string> {
8
+ const url = `https://v.kuaishou.com/${shortCode}`;
9
+ try {
10
+ const controller = new AbortController();
11
+ const timeout = setTimeout(() => controller.abort(), 10000);
12
+ const response = await fetch(url, {
13
+ redirect: "follow",
14
+ signal: controller.signal,
15
+ });
16
+ clearTimeout(timeout);
17
+ return response.url || url;
18
+ } catch {
19
+ return url;
20
+ }
21
+ }
22
+
23
+ function extractPhotoIdFromUrl(url: string): string | null {
24
+ const shortMatch = url.match(/\/short-video\/([a-zA-Z0-9_-]+)/);
25
+ if (shortMatch) return shortMatch[1];
26
+ const photoMatch = url.match(/\/photo\/([a-zA-Z0-9_-]+)/);
27
+ if (photoMatch) return photoMatch[1];
28
+ return null;
29
+ }
30
+
5
31
  export class KuaishouResolver implements PlatformResolver {
6
- async resolve(client: AmagiClient, parsed: ParsedMediaUrl): Promise<ParsedMediaResult> {
7
- const photoId = parsed.id;
32
+ async resolve(
33
+ client: AmagiClient,
34
+ parsed: ParsedMediaUrl,
35
+ ): Promise<ParsedMediaResult> {
36
+ let photoId = parsed.id;
8
37
 
38
+ // If id is a short code (just the code like "KfhlEcGV"), resolve it first
39
+ if (
40
+ !parsed.id.startsWith("http") &&
41
+ KUAISHOU_SHORT_CODE_REGEX.test(parsed.id)
42
+ ) {
43
+ const resolvedUrl = await resolveKuaishouShortCode(parsed.id);
44
+ const extracted = extractPhotoIdFromUrl(resolvedUrl);
45
+ if (extracted) {
46
+ photoId = extracted;
47
+ }
48
+ } else if (parsed.id.startsWith("http")) {
49
+ // If id is a full URL, extract photoId from URL path
50
+ const extracted = extractPhotoIdFromUrl(parsed.id);
51
+ if (extracted) {
52
+ photoId = extracted;
53
+ }
54
+ }
9
55
  const result = await client.kuaishou.fetcher.fetchVideoWork({ photoId });
56
+
10
57
  if (!result.success) {
11
- const isCookieError = result.code === 401 ||
12
- (result.data as any)?.errorDescription?.includes("ck可能已经失效") ||
13
- (result.data as any)?.errorDescription?.includes("接口返回内容为空");
14
- if (isCookieError) {
15
- throw new Error("快手Cookie无效或已过期,请检查配置");
58
+ const errorInfo = (result.error as any) || {};
59
+ const amagiError = errorInfo.amagiError as any;
60
+ const errorDesc =
61
+ amagiError?.errorDescription || errorInfo.errorDescription || "";
62
+ const errorCode = result.code as any;
63
+
64
+ if (
65
+ errorDesc.includes("ck可能已经失效") ||
66
+ errorDesc.includes("接口返回内容为空") ||
67
+ errorCode === "INVALID_COOKIE" ||
68
+ errorCode === "UNKNOWN_ERROR"
69
+ ) {
70
+ throw new Error(
71
+ "快手接口返回内容为空,可能是作品不存在、链接有误或需要登录。请确认链接是否正确,或检查作品是否公开。",
72
+ );
16
73
  }
17
74
  throw new Error(`快手作品解析失败: ${result.message}`);
18
75
  }
@@ -30,7 +87,7 @@ export class KuaishouResolver implements PlatformResolver {
30
87
  const author = authorInfo.name || "未知作者";
31
88
  const description = photo.caption || "";
32
89
  const coverUrl = photo.coverUrl || "";
33
- const duration = photo.duration;
90
+ const duration = photo.duration ? Math.floor(photo.duration / 1000) : undefined;
34
91
 
35
92
  let videoUrl = "";
36
93
 
@@ -38,10 +95,16 @@ export class KuaishouResolver implements PlatformResolver {
38
95
  videoUrl = photo.photoUrl;
39
96
  } else if (photo.croppedPhotoUrl) {
40
97
  videoUrl = photo.croppedPhotoUrl;
41
- } else if (photo.videoResource?.h264?.adaptationSet?.[0]?.representation?.[0]?.url) {
42
- videoUrl = photo.videoResource.h264.adaptationSet[0].representation[0].url;
43
- } else if (photo.videoResource?.hevc?.adaptationSet?.[0]?.representation?.[0]?.url) {
44
- videoUrl = photo.videoResource.hevc.adaptationSet[0].representation[0].url;
98
+ } else if (
99
+ photo.videoResource?.h264?.adaptationSet?.[0]?.representation?.[0]?.url
100
+ ) {
101
+ videoUrl =
102
+ photo.videoResource.h264.adaptationSet[0].representation[0].url;
103
+ } else if (
104
+ photo.videoResource?.hevc?.adaptationSet?.[0]?.representation?.[0]?.url
105
+ ) {
106
+ videoUrl =
107
+ photo.videoResource.hevc.adaptationSet[0].representation[0].url;
45
108
  } else if (photo.manifest?.adaptationSet?.[0]?.representation?.[0]?.url) {
46
109
  videoUrl = photo.manifest.adaptationSet[0].representation[0].url;
47
110
  }
@@ -49,7 +112,11 @@ export class KuaishouResolver implements PlatformResolver {
49
112
  const images: string[] = [];
50
113
  if (!videoUrl && photo.images && Array.isArray(photo.images)) {
51
114
  for (const img of photo.images) {
52
- const url = img?.url || img?.url_list?.[0] || img?.download_addr?.url_list?.[0] || "";
115
+ const url =
116
+ img?.url ||
117
+ img?.url_list?.[0] ||
118
+ img?.download_addr?.url_list?.[0] ||
119
+ "";
53
120
  if (url) images.push(url);
54
121
  }
55
122
  }
@@ -70,4 +137,4 @@ export class KuaishouResolver implements PlatformResolver {
70
137
  },
71
138
  };
72
139
  }
73
- }
140
+ }
@@ -44,9 +44,35 @@ export interface AmagiClient {
44
44
  typeMode?: "strict" | "loose";
45
45
  }): Promise<{
46
46
  success: boolean;
47
- code?: number;
47
+ code?: number | string | undefined;
48
48
  data: any;
49
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;
50
76
  }>;
51
77
  };
52
78
  };
@@ -206,7 +206,8 @@ function parseKuaishouUrl(url: string): ParsedMediaUrl | null {
206
206
  // Handle v.kuaishou.com/xxx short URLs like https://v.kuaishou.com/KfhlEcGV
207
207
  const shortCodeMatch = url.match(/:\/\/[^/]+\/([a-zA-Z0-9_-]+)/);
208
208
  if (shortCodeMatch) {
209
- return { platform: "kuaishou", id: shortCodeMatch[1], subtype: "video" };
209
+ // Store full URL so isShortUrl can detect it and resolveShortUrl can work
210
+ return { platform: "kuaishou", id: url, subtype: "video" };
210
211
  }
211
212
 
212
213
  return null;
@@ -393,29 +394,29 @@ export function extractMediaUrlFromEvent(event: any): ParsedMediaUrl | null {
393
394
 
394
395
  export function resolveShortUrl(url: string): Promise<string> {
395
396
  return new Promise((resolve) => {
396
- try {
397
- const controller = new AbortController();
398
- const timeout = setTimeout(() => {
399
- controller.abort();
400
- resolve(url);
401
- }, 5000);
402
-
403
- fetch(url, {
404
- redirect: "follow",
405
- signal: controller.signal,
406
- method: "HEAD",
407
- })
408
- .then((response) => {
409
- clearTimeout(timeout);
410
- resolve(response.url || url);
411
- })
412
- .catch(() => {
413
- clearTimeout(timeout);
414
- resolve(url);
415
- });
416
- } catch {
397
+ const controller = new AbortController();
398
+ const timeout = setTimeout(() => {
399
+ controller.abort();
417
400
  resolve(url);
418
- }
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
+ });
419
420
  });
420
421
  }
421
422
 
@@ -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) {
@@ -24,5 +27,34 @@ export async function handleMediaError(options: {
24
27
  }): Promise<void> {
25
28
  const { ctx, error, platform } = options;
26
29
  const errorMessage = normalizeErrorMessage(error);
27
- ctx.logger.error(`[media] ${platform} 解析失败: ${errorMessage}`, error);
30
+ ctx.logger.error(`[media] ${platform} 解析失败: ${errorMessage}`);
31
+
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;
38
+
39
+ if (!bot) return;
40
+
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);
45
+
46
+ // 构建错误提示消息
47
+ const errorText = `【${platform}】解析失败\n${errorMessage}`;
48
+
49
+ try {
50
+ if (event?.message_type === "group" && event?.group_id != null) {
51
+ await bot.sendGroupMsg(event.group_id, [ctx.segment.text(errorText)]);
52
+ return;
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
59
+ }
28
60
  }
package/utils/message.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { SendNodeElement, SendNodeContentElement, ForwardDisplayOptions } from "napcat-sdk";
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,7 +41,10 @@ function formatCount(count?: number): string {
37
41
  return String(count);
38
42
  }
39
43
 
40
- export function buildInfoMessage(parsed: ParsedMediaUrl, result: ParsedMediaResult): string {
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
 
@@ -68,24 +75,32 @@ export function buildInfoMessage(parsed: ParsedMediaUrl, result: ParsedMediaResu
68
75
  return lines.join("\n");
69
76
  }
70
77
 
71
- function buildSummaryText(platform: string, subtype: string | undefined, stats?: MediaStats): string {
78
+ function buildSummaryText(
79
+ platform: string,
80
+ subtype: string | undefined,
81
+ stats?: MediaStats,
82
+ ): string {
72
83
  if (!stats) return "";
73
84
 
74
85
  const parts: string[] = [];
75
86
 
76
87
  if (platform === "bilibili") {
77
88
  if (subtype === "live") {
78
- if (stats.views != null && stats.views > 0) parts.push(`在线${formatCount(stats.views)}`);
79
- if (stats.comments != null && stats.comments > 0) parts.push(`关注${formatCount(stats.comments)}`);
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)}`);
80
93
  } else {
81
94
  if (stats.likes != null) parts.push(`赞${formatCount(stats.likes)}`);
82
95
  if (stats.coins != null) parts.push(`币${formatCount(stats.coins)}`);
83
- if (stats.favorites != null) parts.push(`藏${formatCount(stats.favorites)}`);
96
+ if (stats.favorites != null)
97
+ parts.push(`藏${formatCount(stats.favorites)}`);
84
98
  if (stats.shares != null) parts.push(`转${formatCount(stats.shares)}`);
85
99
  }
86
100
  } else {
87
101
  if (stats.likes != null) parts.push(`赞${formatCount(stats.likes)}`);
88
- if (stats.favorites != null) parts.push(`藏${formatCount(stats.favorites)}`);
102
+ if (stats.favorites != null)
103
+ parts.push(`藏${formatCount(stats.favorites)}`);
89
104
  if (stats.shares != null) parts.push(`转${formatCount(stats.shares)}`);
90
105
  if (stats.comments != null) parts.push(`评${formatCount(stats.comments)}`);
91
106
  }
@@ -93,7 +108,10 @@ function buildSummaryText(platform: string, subtype: string | undefined, stats?:
93
108
  return parts.join(" ");
94
109
  }
95
110
 
96
- function buildForwardDisplay(parsed: ParsedMediaUrl, result: ParsedMediaResult): ForwardDisplayOptions {
111
+ function buildForwardDisplay(
112
+ parsed: ParsedMediaUrl,
113
+ result: ParsedMediaResult,
114
+ ): ForwardDisplayOptions {
97
115
  const displayTitle = PLATFORM_DISPLAY_TITLES[parsed.platform] || "媒体解析";
98
116
 
99
117
  let summary = buildSummaryText(parsed.platform, parsed.subtype, result.stats);
@@ -114,10 +132,7 @@ function buildForwardDisplay(parsed: ParsedMediaUrl, result: ParsedMediaResult):
114
132
 
115
133
  return {
116
134
  source: displayTitle,
117
- news: [
118
- { text: truncateText(result.title, 26) },
119
- { text: result.author },
120
- ],
135
+ news: [{ text: truncateText(result.title, 26) }, { text: result.author }],
121
136
  summary,
122
137
  };
123
138
  }
@@ -149,6 +164,15 @@ function buildForwardNodes(
149
164
  ): SendNodeElement[] {
150
165
  const nodes: SendNodeElement[] = [];
151
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
+
152
176
  if (result.coverUrl) {
153
177
  nodes.push({
154
178
  type: "node",
@@ -158,14 +182,6 @@ function buildForwardNodes(
158
182
  } as SendNodeContentElement);
159
183
  }
160
184
 
161
- const infoText = buildInfoMessage(parsed, result);
162
- nodes.push({
163
- type: "node",
164
- user_id: userId,
165
- nickname,
166
- content: [ctx.segment.text(infoText)],
167
- } as SendNodeContentElement);
168
-
169
185
  // 处理图片集/合辑
170
186
  if (result.images && result.images.length > 0) {
171
187
  // 如果既有图片又有视频/实况图,发送纯图片
@@ -254,7 +270,10 @@ export async function sendMediaResult(
254
270
  }
255
271
 
256
272
  const nickname = String(
257
- ctx?.bot?.nickname || event?.sender?.card || event?.sender?.nickname || "媒体解析",
273
+ ctx?.bot?.nickname ||
274
+ event?.sender?.card ||
275
+ event?.sender?.nickname ||
276
+ "媒体解析",
258
277
  );
259
278
  const userId = String(selfId || ctx?.bot?.bot_id || event?.self_id || 0);
260
279
 
@@ -262,53 +281,112 @@ export async function sendMediaResult(
262
281
  const forwardPayload = toOneBotForwardFormat(nodes);
263
282
  const display = buildForwardDisplay(parsed, result);
264
283
 
265
- try {
266
- if (event?.message_type === "group" && event?.group_id != null) {
267
- await bot.api("send_group_forward_msg", {
268
- group_id: event.group_id,
269
- messages: forwardPayload,
270
- source: display.source,
271
- news: display.news,
272
- summary: display.summary,
273
- });
274
- return;
275
- }
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
+ }
276
294
 
277
- if (event?.user_id != null) {
278
- await bot.api("send_private_forward_msg", {
279
- user_id: event.user_id,
280
- messages: forwardPayload,
281
- source: display.source,
282
- news: display.news,
283
- summary: display.summary,
284
- });
285
- return;
286
- }
287
- } catch (primaryError) {
288
- try {
289
- if (event?.message_type === "group" && event?.group_id != null) {
290
- const textPayload = [ctx.segment.text(buildInfoMessage(parsed, result))];
291
- if (result.coverUrl) {
292
- await bot.sendGroupMsg(event.group_id, [ctx.segment.image(result.coverUrl), ...textPayload]);
293
- } else {
294
- await bot.sendGroupMsg(event.group_id, textPayload);
295
- }
296
- return;
297
- }
298
- if (event?.user_id != null) {
299
- const textPayload = [ctx.segment.text(buildInfoMessage(parsed, result))];
300
- if (result.coverUrl) {
301
- await bot.sendPrivateMsg(event.user_id, [ctx.segment.image(result.coverUrl), ...textPayload]);
302
- } else {
303
- await bot.sendPrivateMsg(event.user_id, textPayload);
304
- }
305
- return;
306
- }
307
- } catch {
308
- // fallback to text reply
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);
309
366
  }
310
367
  }
311
368
 
312
- const infoText = buildInfoMessage(parsed, result);
313
- await event.reply(infoText);
314
- }
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
+ }
392
+ }