@sns-parse/core 0.3.0-alpha.1 → 0.4.0-alpha.10

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.
Files changed (47) hide show
  1. package/lib/config.d.ts +7 -0
  2. package/lib/config.js +28 -0
  3. package/lib/engine/config-io.d.ts +44 -0
  4. package/lib/engine/config-io.js +155 -0
  5. package/lib/engine/config.d.ts +9 -0
  6. package/lib/engine/config.js +265 -0
  7. package/lib/engine/fetcher.d.ts +21 -0
  8. package/lib/engine/fetcher.js +228 -0
  9. package/lib/engine/gateway.d.ts +21 -0
  10. package/lib/engine/gateway.js +19 -0
  11. package/lib/engine/parser.d.ts +2 -0
  12. package/lib/engine/parser.js +162 -0
  13. package/lib/engine/platform-config.d.ts +24 -0
  14. package/lib/engine/platform-config.js +76 -0
  15. package/lib/engine/runtime.d.ts +49 -0
  16. package/lib/engine/runtime.js +90 -0
  17. package/lib/engine/translate.d.ts +15 -0
  18. package/lib/engine/translate.js +107 -0
  19. package/lib/engine/twitter-user.d.ts +67 -0
  20. package/lib/engine/twitter-user.js +185 -0
  21. package/lib/engine/twitter.d.ts +102 -0
  22. package/lib/engine/twitter.js +791 -0
  23. package/lib/index.d.ts +21 -0
  24. package/lib/index.js +21 -0
  25. package/lib/registry.d.ts +20 -0
  26. package/lib/registry.js +77 -0
  27. package/lib/utils/cache.d.ts +9 -0
  28. package/lib/utils/cache.js +34 -0
  29. package/lib/utils/common.d.ts +7 -0
  30. package/lib/utils/common.js +58 -0
  31. package/lib/utils/concurrency.d.ts +8 -0
  32. package/lib/utils/concurrency.js +29 -0
  33. package/lib/utils/field-mapping.d.ts +2 -0
  34. package/lib/utils/field-mapping.js +29 -0
  35. package/lib/utils/format.d.ts +4 -0
  36. package/lib/utils/format.js +84 -0
  37. package/lib/utils/gif.d.ts +10 -0
  38. package/lib/utils/gif.js +104 -0
  39. package/lib/utils/image-size.d.ts +10 -0
  40. package/lib/utils/image-size.js +69 -0
  41. package/lib/utils/merge.d.ts +64 -0
  42. package/lib/utils/merge.js +402 -0
  43. package/lib/utils/tls-client.d.ts +30 -0
  44. package/lib/utils/tls-client.js +120 -0
  45. package/lib/utils/url.d.ts +10 -0
  46. package/lib/utils/url.js +89 -0
  47. package/package.json +16 -4
package/lib/index.d.ts CHANGED
@@ -8,3 +8,24 @@ export * from './sender';
8
8
  export * from './extensions';
9
9
  export * from './platform';
10
10
  export * from './host';
11
+ export * from './utils/common';
12
+ export * from './utils/url';
13
+ export * from './utils/format';
14
+ export * from './utils/cache';
15
+ export * from './utils/concurrency';
16
+ export * from './utils/field-mapping';
17
+ export * from './utils/image-size';
18
+ export * from './utils/tls-client';
19
+ export * from './utils/gif';
20
+ export * from './utils/merge';
21
+ export * from './engine/gateway';
22
+ export * from './engine/config';
23
+ export * from './engine/platform-config';
24
+ export * from './engine/translate';
25
+ export * from './engine/twitter';
26
+ export * from './engine/twitter-user';
27
+ export * from './engine/parser';
28
+ export * from './engine/fetcher';
29
+ export * from './engine/runtime';
30
+ export * from './engine/config-io';
31
+ export * from './registry';
package/lib/index.js CHANGED
@@ -24,3 +24,24 @@ __exportStar(require("./sender"), exports);
24
24
  __exportStar(require("./extensions"), exports);
25
25
  __exportStar(require("./platform"), exports);
26
26
  __exportStar(require("./host"), exports);
27
+ __exportStar(require("./utils/common"), exports);
28
+ __exportStar(require("./utils/url"), exports);
29
+ __exportStar(require("./utils/format"), exports);
30
+ __exportStar(require("./utils/cache"), exports);
31
+ __exportStar(require("./utils/concurrency"), exports);
32
+ __exportStar(require("./utils/field-mapping"), exports);
33
+ __exportStar(require("./utils/image-size"), exports);
34
+ __exportStar(require("./utils/tls-client"), exports);
35
+ __exportStar(require("./utils/gif"), exports);
36
+ __exportStar(require("./utils/merge"), exports);
37
+ __exportStar(require("./engine/gateway"), exports);
38
+ __exportStar(require("./engine/config"), exports);
39
+ __exportStar(require("./engine/platform-config"), exports);
40
+ __exportStar(require("./engine/translate"), exports);
41
+ __exportStar(require("./engine/twitter"), exports);
42
+ __exportStar(require("./engine/twitter-user"), exports);
43
+ __exportStar(require("./engine/parser"), exports);
44
+ __exportStar(require("./engine/fetcher"), exports);
45
+ __exportStar(require("./engine/runtime"), exports);
46
+ __exportStar(require("./engine/config-io"), exports);
47
+ __exportStar(require("./registry"), exports);
@@ -0,0 +1,20 @@
1
+ import type { PlatformDefinition } from './platform';
2
+ import type { ConfigContribution } from './config';
3
+ import type { VideoParserExtensions } from './extensions';
4
+ /** 收集已安装平台的定义(含 label/rules/dedicated) */
5
+ export declare function collectPlatformDefinitions(): PlatformDefinition[];
6
+ /** 已安装平台的链接规则(扁平) */
7
+ export declare function collectPlatformLinkRules(): {
8
+ pattern: RegExp;
9
+ type: string;
10
+ }[];
11
+ export interface LoadedExtension {
12
+ name: string;
13
+ contribution?: ConfigContribution;
14
+ extension?: Partial<VideoParserExtensions>;
15
+ }
16
+ /** 探测并加载全部已安装扩展 */
17
+ export declare function loadExtensions(): LoadedExtension[];
18
+ export declare function loadExtensionContributions(): ConfigContribution[];
19
+ /** 合并所有已安装扩展的能力片段 */
20
+ export declare function loadExtensionImplementations(): Partial<VideoParserExtensions>;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectPlatformDefinitions = collectPlatformDefinitions;
4
+ exports.collectPlatformLinkRules = collectPlatformLinkRules;
5
+ exports.loadExtensions = loadExtensions;
6
+ exports.loadExtensionContributions = loadExtensionContributions;
7
+ exports.loadExtensionImplementations = loadExtensionImplementations;
8
+ /**
9
+ * 已安装碎片包的动态收集(平台 @sns-parse/platform-* 与扩展 @sns-parse/ext-*)。
10
+ *
11
+ * - 可自选安装单个碎片包;安装聚合包(@sns-parse/platforms / extensions)则全部到位
12
+ * - 未安装的碎片自动缺席(链接识别、配置项与能力同时缺席)
13
+ * - koishi 兼容层与 CLI 均经此处收集定义与配置声明
14
+ */
15
+ const node_module_1 = require("node:module");
16
+ const nodeRequire = (0, node_module_1.createRequire)(typeof __filename !== 'undefined' ? __filename : process.cwd() + '/');
17
+ const PLATFORM_TYPES = [
18
+ 'bilibili', 'douyin', 'kuaishou', 'xiaohongshu', 'weibo', 'xigua', 'youtube',
19
+ 'tiktok', 'acfun', 'zhihu', 'weishi', 'huya', 'haokan', 'meipai', 'twitter',
20
+ 'instagram', 'doubao', 'doubao_image', 'jimeng', 'oasis', 'wechat_channel',
21
+ 'lishi', 'quanmin', 'pipigx', 'pipixia', 'zuiyou', 'toutiao',
22
+ ];
23
+ /** 收集已安装平台的定义(含 label/rules/dedicated) */
24
+ function collectPlatformDefinitions() {
25
+ const out = [];
26
+ for (const type of PLATFORM_TYPES) {
27
+ try {
28
+ const mod = nodeRequire(`@sns-parse/platform-${type}`);
29
+ const def = mod.default || mod[type];
30
+ if (def && def.type)
31
+ out.push(def);
32
+ }
33
+ catch {
34
+ // 未安装该平台包 → 跳过
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ /** 已安装平台的链接规则(扁平) */
40
+ function collectPlatformLinkRules() {
41
+ return collectPlatformDefinitions().flatMap(d => d.rules.map(pattern => ({ pattern, type: d.type })));
42
+ }
43
+ const EXTENSION_SPECS = [
44
+ { name: 'ext-nsfw', contributionKey: 'nsfwConfigContribution', extensionKey: 'nsfwExtension' },
45
+ { name: 'ext-merge', extensionKey: 'mergeExtension' },
46
+ { name: 'ext-translate', extensionKey: 'translateExtension' },
47
+ { name: 'ext-gif', extensionKey: 'gifExtension' },
48
+ ];
49
+ /** 探测并加载全部已安装扩展 */
50
+ function loadExtensions() {
51
+ const out = [];
52
+ for (const spec of EXTENSION_SPECS) {
53
+ try {
54
+ const mod = nodeRequire(`@sns-parse/${spec.name}`);
55
+ out.push({
56
+ name: spec.name,
57
+ contribution: spec.contributionKey ? mod[spec.contributionKey] : undefined,
58
+ extension: spec.extensionKey && typeof mod[spec.extensionKey] === 'function'
59
+ ? mod[spec.extensionKey]()
60
+ : undefined,
61
+ });
62
+ }
63
+ catch {
64
+ // 未安装该扩展 → 跳过(配置项与能力同时缺席)
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ function loadExtensionContributions() {
70
+ return loadExtensions()
71
+ .map(e => e.contribution)
72
+ .filter((c) => !!c);
73
+ }
74
+ /** 合并所有已安装扩展的能力片段 */
75
+ function loadExtensionImplementations() {
76
+ return Object.assign({}, ...loadExtensions().map(e => e.extension || {}));
77
+ }
@@ -0,0 +1,9 @@
1
+ export declare class SimpleLRUCache<V> {
2
+ private max;
3
+ private ttlMs;
4
+ private map;
5
+ constructor(max: number, ttlMs: number);
6
+ get(key: string): V | undefined;
7
+ set(key: string, value: V): void;
8
+ clear(): void;
9
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimpleLRUCache = void 0;
4
+ class SimpleLRUCache {
5
+ constructor(max, ttlMs) {
6
+ this.max = max;
7
+ this.ttlMs = ttlMs;
8
+ this.map = new Map();
9
+ }
10
+ get(key) {
11
+ const entry = this.map.get(key);
12
+ if (!entry)
13
+ return undefined;
14
+ if (Date.now() > entry.expireAt) {
15
+ this.map.delete(key);
16
+ return undefined;
17
+ }
18
+ return entry.value;
19
+ }
20
+ set(key, value) {
21
+ this.map.delete(key);
22
+ while (this.map.size >= this.max) {
23
+ const k = this.map.keys().next().value;
24
+ if (k === undefined)
25
+ break;
26
+ this.map.delete(k);
27
+ }
28
+ this.map.set(key, { value, expireAt: Date.now() + this.ttlMs });
29
+ }
30
+ clear() {
31
+ this.map.clear();
32
+ }
33
+ }
34
+ exports.SimpleLRUCache = SimpleLRUCache;
@@ -0,0 +1,7 @@
1
+ import type { VideoQuality, ParsedData } from '../types';
2
+ export declare const delay: (ms: number) => Promise<unknown>;
3
+ export declare function getErrorMessage(error: unknown): string;
4
+ export declare function parseCount(val: any): number;
5
+ export declare function pickBestQuality(videoBackup: any[]): VideoQuality[];
6
+ export declare function contentFingerprint(p: ParsedData): string;
7
+ export declare function getText(config: any, key: string): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.delay = void 0;
4
+ exports.getErrorMessage = getErrorMessage;
5
+ exports.parseCount = parseCount;
6
+ exports.pickBestQuality = pickBestQuality;
7
+ exports.contentFingerprint = contentFingerprint;
8
+ exports.getText = getText;
9
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
10
+ exports.delay = delay;
11
+ function getErrorMessage(error) {
12
+ if (error instanceof Error)
13
+ return error.message;
14
+ if (error && typeof error === 'object' && 'message' in error)
15
+ return String(error.message);
16
+ return String(error);
17
+ }
18
+ function parseCount(val) {
19
+ if (val === undefined || val === null)
20
+ return 0;
21
+ if (typeof val === 'number')
22
+ return val;
23
+ const str = String(val).trim();
24
+ if (str.includes('万')) {
25
+ const num = parseFloat(str);
26
+ return isNaN(num) ? 0 : Math.round(num * 10000);
27
+ }
28
+ if (str.includes('亿')) {
29
+ const num = parseFloat(str);
30
+ return isNaN(num) ? 0 : Math.round(num * 100000000);
31
+ }
32
+ const num = parseInt(str, 10);
33
+ return isNaN(num) ? 0 : num;
34
+ }
35
+ function pickBestQuality(videoBackup) {
36
+ if (!Array.isArray(videoBackup))
37
+ return [];
38
+ return videoBackup.filter(v => v && v.url).map(v => ({
39
+ quality: v.quality || v.label || 'unknown',
40
+ url: v.url,
41
+ bit_rate: Number(v.bit_rate || 0)
42
+ })).sort((a, b) => b.bit_rate - a.bit_rate);
43
+ }
44
+ function contentFingerprint(p) {
45
+ const imgSig = p.images?.length ? p.images.slice(0, 3).join('|') : (p.live_photo?.slice(0, 3).map(lp => lp.image).join('|') || '');
46
+ return [p.type, p.title, p.author, p.uid, p.video, imgSig].map(v => String(v ?? '')).join('::');
47
+ }
48
+ function getText(config, key) {
49
+ const defaults = {
50
+ waitingTipText: '正在解析视频,请稍候...',
51
+ unsupportedPlatformText: '不支持该平台链接',
52
+ invalidLinkText: '无效的视频链接',
53
+ parseErrorPrefix: '❌ 解析失败:',
54
+ parseErrorItemFormat: '【${url}】: ${msg}',
55
+ deduplicationTipText: '链接 ${url} 在最近 ${interval} 秒内已解析过,已跳过。',
56
+ };
57
+ return config[key] || defaults[key] || '';
58
+ }
@@ -0,0 +1,8 @@
1
+ export declare class ConcurrencyLimiter {
2
+ private max;
3
+ private running;
4
+ private queue;
5
+ constructor(max: number);
6
+ acquire(): Promise<void>;
7
+ release(): void;
8
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConcurrencyLimiter = void 0;
4
+ class ConcurrencyLimiter {
5
+ constructor(max) {
6
+ this.max = max;
7
+ this.running = 0;
8
+ this.queue = [];
9
+ }
10
+ async acquire() {
11
+ if (this.running < this.max) {
12
+ this.running++;
13
+ return;
14
+ }
15
+ return new Promise(resolve => {
16
+ this.queue.push(() => {
17
+ this.running++;
18
+ resolve();
19
+ });
20
+ });
21
+ }
22
+ release() {
23
+ this.running--;
24
+ const next = this.queue.shift();
25
+ if (next)
26
+ next();
27
+ }
28
+ }
29
+ exports.ConcurrencyLimiter = ConcurrencyLimiter;
@@ -0,0 +1,2 @@
1
+ export declare function getNestedValue(obj: any, path: string): any;
2
+ export declare function parseFieldMapping(mappingStr: string): Record<string, string> | undefined;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNestedValue = getNestedValue;
4
+ exports.parseFieldMapping = parseFieldMapping;
5
+ function getNestedValue(obj, path) {
6
+ if (!path)
7
+ return obj;
8
+ const keys = path.split('.');
9
+ let current = obj;
10
+ for (const key of keys) {
11
+ if (current === null || current === undefined)
12
+ return undefined;
13
+ current = current[key];
14
+ }
15
+ return current;
16
+ }
17
+ function parseFieldMapping(mappingStr) {
18
+ if (!mappingStr || mappingStr.trim() === '{}' || mappingStr.trim() === '')
19
+ return undefined;
20
+ try {
21
+ const obj = JSON.parse(mappingStr);
22
+ if (typeof obj === 'object' && !Array.isArray(obj))
23
+ return obj;
24
+ return undefined;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
@@ -0,0 +1,4 @@
1
+ import type { ParsedData } from '../types';
2
+ export declare function formatDuration(seconds: number): string;
3
+ export declare function formatPublishTime(ms: number): string;
4
+ export declare function generateFormattedText(p: ParsedData, format: string, index?: number, total?: number): string;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDuration = formatDuration;
4
+ exports.formatPublishTime = formatPublishTime;
5
+ exports.generateFormattedText = generateFormattedText;
6
+ const translate_1 = require("../engine/translate");
7
+ const formatVarRegex = /\$\{([^}]+)\}/g;
8
+ function formatDuration(seconds) {
9
+ if (!seconds || seconds <= 0)
10
+ return '';
11
+ const h = Math.floor(seconds / 3600);
12
+ const m = Math.floor((seconds % 3600) / 60);
13
+ const s = Math.floor(seconds % 60);
14
+ if (h > 0)
15
+ return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
16
+ return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
17
+ }
18
+ function formatPublishTime(ms) {
19
+ if (!ms)
20
+ return '';
21
+ const d = new Date(ms);
22
+ const y = d.getFullYear();
23
+ const mo = (d.getMonth() + 1).toString().padStart(2, '0');
24
+ const day = d.getDate().toString().padStart(2, '0');
25
+ const H = d.getHours().toString().padStart(2, '0');
26
+ const i = d.getMinutes().toString().padStart(2, '0');
27
+ return `${y}年${mo}月${day}日 ${H}:${i}`;
28
+ }
29
+ function generateFormattedText(p, format, index, total) {
30
+ const imageCount = p.images.length || p.live_photo.length;
31
+ // 纯文字作品(type=text):正文即内容。模板含 ${正文} 时启用新语义——
32
+ // 正文=desc、简介置空(自动隐藏),避免把正文标成「简介」;旧模板保持原行为。
33
+ const plainTextWork = p.type === 'text' && !!p.desc;
34
+ const wantsBody = plainTextWork && format.includes('${正文}');
35
+ const vars = {
36
+ '标题': p.title,
37
+ '作者': p.author,
38
+ '简介': wantsBody ? '' : p.desc,
39
+ '正文': plainTextWork ? p.desc : '',
40
+ '视频时长': p.duration > 0 ? formatDuration(p.duration) : '',
41
+ '点赞数': String(p.like),
42
+ '收藏数': String(p.collect),
43
+ '转发数': String(p.share),
44
+ '播放数': String(p.play),
45
+ '评论数': String(p.comment),
46
+ '发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
47
+ '图片数量': String(imageCount),
48
+ '作者ID': p.uid,
49
+ '音乐标题': p.music.title || '',
50
+ '音乐作者': p.music.author || '',
51
+ '翻译': p.translation || '',
52
+ // 原文语言与提供方仅在有译文时展示(同一行变量全空则整行隐藏)
53
+ '原文语言': p.translation ? (0, translate_1.langName)(p.lang) : '',
54
+ '翻译提供方': p.translationProvider || '',
55
+ };
56
+ const lines = format.split('\n');
57
+ const resultLines = [];
58
+ for (const line of lines) {
59
+ const varMatches = line.match(formatVarRegex);
60
+ if (varMatches && varMatches.length > 0) {
61
+ let allEmptyOrZero = true;
62
+ for (const match of varMatches) {
63
+ const varName = match.slice(2, -1);
64
+ const val = vars[varName];
65
+ if (val && val !== '0') {
66
+ allEmptyOrZero = false;
67
+ break;
68
+ }
69
+ }
70
+ if (allEmptyOrZero)
71
+ continue;
72
+ }
73
+ let newLine = line;
74
+ for (const [key, val] of Object.entries(vars)) {
75
+ newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), val);
76
+ }
77
+ resultLines.push(newLine);
78
+ }
79
+ let text = resultLines.join('\n').trim();
80
+ if (index !== undefined && total !== undefined && total > 1) {
81
+ text = `【${index}/${total}】\n${text}`;
82
+ }
83
+ return text;
84
+ }
@@ -0,0 +1,10 @@
1
+ import type { ParserRuntime } from '../engine/runtime';
2
+ import type { GifOptions } from '../extensions';
3
+ export type { GifOptions };
4
+ export declare function resolveFfmpeg(): string;
5
+ /** 生成 ffmpeg 滤镜串(导出供测试) */
6
+ export declare function gifFilter(width: number, fps: number): string;
7
+ /** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
8
+ export declare function gifDuration(durationSec: number, maxDurationSec: number): number;
9
+ /** 下载视频并转 GIF;任何失败返回 null */
10
+ export declare function mp4ToGif(rt: ParserRuntime, url: string, durationSec: number, opts: GifOptions): Promise<Buffer | null>;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveFfmpeg = resolveFfmpeg;
4
+ exports.gifFilter = gifFilter;
5
+ exports.gifDuration = gifDuration;
6
+ exports.mp4ToGif = mp4ToGif;
7
+ /**
8
+ * 视频转 GIF(推文动图用)。
9
+ *
10
+ * ffmpeg 管道两遍法(palettegen + paletteuse),不落盘。
11
+ * ffmpeg 不可用 / 转换失败 / 下载失败 → 返回 null,调用方回退发送原视频。
12
+ */
13
+ const child_process_1 = require("child_process");
14
+ const logger_1 = require("../logger");
15
+ let ffmpegMissingWarned = false;
16
+ /**
17
+ * ffmpeg 路径解析:内置静态二进制(optionalDependency ffmpeg-static,随插件自动安装)
18
+ * → 系统 PATH。解析结果记忆化,来源记入 debug 日志。
19
+ */
20
+ let resolvedFfmpeg = null;
21
+ function resolveFfmpeg() {
22
+ if (resolvedFfmpeg !== null)
23
+ return resolvedFfmpeg;
24
+ try {
25
+ const p = require('ffmpeg-static');
26
+ resolvedFfmpeg = typeof p === 'string' && p ? p : 'ffmpeg';
27
+ (0, logger_1.debugLog)(`ffmpeg 解析:内置静态二进制 ${resolvedFfmpeg}`);
28
+ }
29
+ catch {
30
+ resolvedFfmpeg = 'ffmpeg';
31
+ (0, logger_1.debugLog)('ffmpeg 解析:未安装 ffmpeg-static(--no-optional 安装?),回退系统 PATH');
32
+ }
33
+ return resolvedFfmpeg;
34
+ }
35
+ /** 生成 ffmpeg 滤镜串(导出供测试) */
36
+ function gifFilter(width, fps) {
37
+ const w = Math.max(120, Math.min(1080, Math.round(width)));
38
+ const f = Math.max(5, Math.min(30, Math.round(fps)));
39
+ return `fps=${f},scale=${w}:-2:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer`;
40
+ }
41
+ /** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
42
+ function gifDuration(durationSec, maxDurationSec) {
43
+ const max = Math.max(1, Math.min(60, maxDurationSec));
44
+ if (!durationSec || durationSec <= 0)
45
+ return max;
46
+ return Math.max(1, Math.min(max, Math.round(durationSec)));
47
+ }
48
+ function runFfmpeg(input, vf, durationSec) {
49
+ return new Promise((resolve) => {
50
+ const child = (0, child_process_1.spawn)(resolveFfmpeg(), [
51
+ '-hide_banner', '-loglevel', 'error',
52
+ '-i', 'pipe:0',
53
+ '-t', String(durationSec),
54
+ '-vf', vf,
55
+ '-f', 'gif', 'pipe:1',
56
+ ]);
57
+ const out = [];
58
+ let err = '';
59
+ child.stdout.on('data', (d) => out.push(d));
60
+ child.stderr.on('data', (d) => { err += d.toString(); });
61
+ child.on('error', (e) => {
62
+ if (!ffmpegMissingWarned) {
63
+ ffmpegMissingWarned = true;
64
+ logger_1.logger.warn(`GIF 转换需要 ffmpeg,但二进制不可用(动图将回退为视频发送):${e.message}。请安装 optionalDependency ffmpeg-static,或在 PATH 中提供 ffmpeg。`);
65
+ }
66
+ resolve(null);
67
+ });
68
+ child.on('close', (code) => {
69
+ if (code === 0 && out.length) {
70
+ const gif = Buffer.concat(out);
71
+ logger_1.logger.info(`动图已转 GIF 发送:${Math.round(gif.length / 1024)}KB(源视频 ${Math.round(input.length / 1024)}KB)`);
72
+ resolve(gif);
73
+ }
74
+ else {
75
+ logger_1.logger.info(`动图转 GIF 失败,回退发送原视频(ffmpeg 退出码 ${code}${err ? ':' + err.slice(0, 120) : ''})`);
76
+ resolve(null);
77
+ }
78
+ });
79
+ child.stdin.on('error', () => { });
80
+ child.stdin.end(input);
81
+ });
82
+ }
83
+ /** 下载视频并转 GIF;任何失败返回 null */
84
+ async function mp4ToGif(rt, url, durationSec, opts) {
85
+ try {
86
+ // X 媒体 CDN (video.twimg.com) 需要浏览器级 UA + Referer 才不会 403(与 vault 下载一致)
87
+ const res = await rt.http.get(url, {
88
+ responseType: 'arraybuffer',
89
+ timeout: Math.min(rt.config.timeout || 60000, 120000),
90
+ headers: {
91
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
92
+ 'Referer': 'https://twitter.com/',
93
+ },
94
+ });
95
+ const input = Buffer.from(res.data);
96
+ if (!input.length)
97
+ return null;
98
+ return await runFfmpeg(input, gifFilter(opts.maxWidth, opts.fps), gifDuration(durationSec, opts.maxDurationSec));
99
+ }
100
+ catch (e) {
101
+ logger_1.logger.info(`动图 GIF 转换跳过(源视频下载失败,回退发送原视频):${e?.message || e}`);
102
+ return null;
103
+ }
104
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 图片尺寸探测(无依赖):PNG / GIF / JPEG / WebP 头部解析。
3
+ * 仅供同源切图识别用:只读宽高,不解码像素。
4
+ */
5
+ export interface ImageSize {
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** 探测图片缓冲区尺寸;无法识别返回 null */
10
+ export declare function probeImageSize(buf: Buffer): ImageSize | null;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * 图片尺寸探测(无依赖):PNG / GIF / JPEG / WebP 头部解析。
4
+ * 仅供同源切图识别用:只读宽高,不解码像素。
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.probeImageSize = probeImageSize;
8
+ /** 探测图片缓冲区尺寸;无法识别返回 null */
9
+ function probeImageSize(buf) {
10
+ if (!buf || buf.length < 12)
11
+ return null;
12
+ // PNG: 89 50 4E 47 0D 0A 1A 0A,IHDR 宽高在 16..24
13
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
14
+ if (buf.length < 24)
15
+ return null;
16
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
17
+ }
18
+ // GIF: GIF8
19
+ if (buf.toString('ascii', 0, 4) === 'GIF8') {
20
+ if (buf.length < 10)
21
+ return null;
22
+ return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
23
+ }
24
+ // JPEG: FF D8,扫描 SOF0/1/2/...(C0-CF,除 C4/C8/CC)段
25
+ if (buf[0] === 0xff && buf[1] === 0xd8) {
26
+ let off = 2;
27
+ while (off + 9 < buf.length) {
28
+ if (buf[off] !== 0xff) {
29
+ off++;
30
+ continue;
31
+ }
32
+ const marker = buf[off + 1];
33
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
34
+ const height = buf.readUInt16BE(off + 5);
35
+ const width = buf.readUInt16BE(off + 7);
36
+ if (width && height)
37
+ return { width, height };
38
+ return null;
39
+ }
40
+ if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {
41
+ off += 2;
42
+ continue;
43
+ }
44
+ const segLen = buf.readUInt16BE(off + 2);
45
+ if (segLen < 2)
46
+ return null;
47
+ off += 2 + segLen;
48
+ }
49
+ return null;
50
+ }
51
+ // WebP: RIFF....WEBP
52
+ if (buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
53
+ const fourcc = buf.toString('ascii', 12, 16);
54
+ if (fourcc === 'VP8 ' && buf.length >= 30) {
55
+ return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
56
+ }
57
+ if (fourcc === 'VP8L' && buf.length >= 25) {
58
+ const b = buf.readUInt32LE(21);
59
+ return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
60
+ }
61
+ if (fourcc === 'VP8X' && buf.length >= 30) {
62
+ const w = 1 + (buf[24] | (buf[25] << 8) | (buf[26] << 16));
63
+ const h = 1 + (buf[27] | (buf[28] << 8) | (buf[29] << 16));
64
+ return { width: w, height: h };
65
+ }
66
+ return null;
67
+ }
68
+ return null;
69
+ }