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.
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { Platform, ParsedMediaUrl } from "./types";
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 = ["kuaishou.com", "www.kuaishou.com", "v.kuaishou.com"];
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(/photo\/(\d+)/);
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
- try {
367
- const controller = new AbortController();
368
- const timeout = setTimeout(() => {
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
@@ -3,7 +3,7 @@ import {
3
3
  getPluginRuntimeState,
4
4
  resetPluginRuntimeState,
5
5
  setPluginRuntimeState,
6
- } from "../../src";
6
+ } from "mioku";
7
7
 
8
8
  const PLUGIN_NAME = "media";
9
9
 
package/skills.ts CHANGED
@@ -1,7 +1,10 @@
1
- import type { AISkill, AITool } from "../../src";
2
- import type { SendNodeContentElement } from "napcat-sdk";
1
+ import type { AISkill, AITool } from "mioku";
3
2
  import { getMediaRuntimeState } from "./runtime";
4
- import { parseMediaUrl, resolveShortUrl, isShortUrl } from "./platforms/url-parser";
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: any;
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
  }
@@ -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, event, error, platform, config } = options;
28
+ const { ctx, error, platform } = options;
26
29
  const errorMessage = normalizeErrorMessage(error);
30
+ ctx.logger.error(`[media] ${platform} 解析失败: ${errorMessage}`);
27
31
 
28
- ctx.logger.error(`[media] ${platform} 解析失败: ${errorMessage}`, error);
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
- const cause = (error instanceof Error && (error as any).cause) ? (error as any).cause : null;
31
- const ckExpired = cause?.error?.errorDescription?.includes("ck可能已经失效");
39
+ if (!bot) return;
32
40
 
33
- if (ckExpired) {
34
- await event.reply(`${platform} Cookie 已失效,请联系管理员更新`, true);
35
- return;
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
- const aiService = ctx.services?.ai as any | undefined;
44
- const chatRuntime = aiService?.getChatRuntime?.();
46
+ // 构建错误提示消息
47
+ const errorText = `【${platform}】解析失败\n${errorMessage}`;
45
48
 
46
- if (chatRuntime) {
47
- try {
48
- await chatRuntime.generateNotice({
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 { 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,13 +41,20 @@ 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
 
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(platform: string, stats?: MediaStats): string {
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 (stats.likes != null) parts.push(`赞${formatCount(stats.likes)}`);
66
- if (stats.coins != null) parts.push(`币${formatCount(stats.coins)}`);
67
- if (stats.favorites != null) parts.push(`藏${formatCount(stats.favorites)}`);
68
- if (stats.shares != null) parts.push(`转${formatCount(stats.shares)}`);
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) parts.push(`藏${formatCount(stats.favorites)}`);
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(parsed: ParsedMediaUrl, result: ParsedMediaResult): ForwardDisplayOptions {
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
- { text: truncateText(result.title, 26) },
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
- const infoText = buildInfoMessage(parsed, result);
129
- nodes.push({
130
- type: "node",
131
- user_id: userId,
132
- nickname,
133
- content: [ctx.segment.text(infoText)],
134
- } as SendNodeContentElement);
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
- if (result.videoUrl) {
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 || event?.sender?.card || event?.sender?.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
- try {
206
- if (event?.message_type === "group" && event?.group_id != null) {
207
- await bot.api("send_group_forward_msg", {
208
- group_id: event.group_id,
209
- messages: forwardPayload,
210
- source: display.source,
211
- news: display.news,
212
- summary: display.summary,
213
- });
214
- return;
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
- if (event?.user_id != null) {
218
- await bot.api("send_private_forward_msg", {
219
- user_id: event.user_id,
220
- messages: forwardPayload,
221
- source: display.source,
222
- news: display.news,
223
- summary: display.summary,
224
- });
225
- return;
226
- }
227
- } catch (primaryError) {
228
- try {
229
- if (event?.message_type === "group" && event?.group_id != null) {
230
- await bot.sendGroupMsg(event.group_id, nodes);
231
- return;
232
- }
233
- if (event?.user_id != null) {
234
- await bot.sendPrivateMsg(event.user_id, nodes);
235
- return;
236
- }
237
- } catch {
238
- // 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);
239
366
  }
240
367
  }
241
368
 
242
- const infoText = buildInfoMessage(parsed, result);
243
- await event.reply(infoText);
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
  }