@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.
- package/lib/config.d.ts +7 -0
- package/lib/config.js +28 -0
- package/lib/engine/config-io.d.ts +44 -0
- package/lib/engine/config-io.js +155 -0
- package/lib/engine/config.d.ts +9 -0
- package/lib/engine/config.js +265 -0
- package/lib/engine/fetcher.d.ts +21 -0
- package/lib/engine/fetcher.js +228 -0
- package/lib/engine/gateway.d.ts +21 -0
- package/lib/engine/gateway.js +19 -0
- package/lib/engine/parser.d.ts +2 -0
- package/lib/engine/parser.js +162 -0
- package/lib/engine/platform-config.d.ts +24 -0
- package/lib/engine/platform-config.js +76 -0
- package/lib/engine/runtime.d.ts +49 -0
- package/lib/engine/runtime.js +90 -0
- package/lib/engine/translate.d.ts +15 -0
- package/lib/engine/translate.js +107 -0
- package/lib/engine/twitter-user.d.ts +67 -0
- package/lib/engine/twitter-user.js +185 -0
- package/lib/engine/twitter.d.ts +102 -0
- package/lib/engine/twitter.js +791 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +21 -0
- package/lib/registry.d.ts +20 -0
- package/lib/registry.js +77 -0
- package/lib/utils/cache.d.ts +9 -0
- package/lib/utils/cache.js +34 -0
- package/lib/utils/common.d.ts +7 -0
- package/lib/utils/common.js +58 -0
- package/lib/utils/concurrency.d.ts +8 -0
- package/lib/utils/concurrency.js +29 -0
- package/lib/utils/field-mapping.d.ts +2 -0
- package/lib/utils/field-mapping.js +29 -0
- package/lib/utils/format.d.ts +4 -0
- package/lib/utils/format.js +84 -0
- package/lib/utils/gif.d.ts +10 -0
- package/lib/utils/gif.js +104 -0
- package/lib/utils/image-size.d.ts +10 -0
- package/lib/utils/image-size.js +69 -0
- package/lib/utils/merge.d.ts +64 -0
- package/lib/utils/merge.js +402 -0
- package/lib/utils/tls-client.d.ts +30 -0
- package/lib/utils/tls-client.js +120 -0
- package/lib/utils/url.d.ts +10 -0
- package/lib/utils/url.js +89 -0
- package/package.json +16 -4
package/lib/config.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ConfigField {
|
|
|
22
22
|
/** koishi 的 role:多行文本 / 密钥 */
|
|
23
23
|
role?: 'textarea' | 'secret';
|
|
24
24
|
hidden?: boolean;
|
|
25
|
+
/** 宿主 Schema 生成时的必填标记(仅 UI 层语义) */
|
|
26
|
+
required?: boolean;
|
|
25
27
|
min?: number;
|
|
26
28
|
max?: number;
|
|
27
29
|
step?: number;
|
|
@@ -48,3 +50,8 @@ export declare function defineConfig(contribution: ConfigContribution): ConfigCo
|
|
|
48
50
|
* 用于 core 汇总所有扩展与平台声明的配置。
|
|
49
51
|
*/
|
|
50
52
|
export declare function mergeConfigContributions(lists: ConfigContribution[][]): ConfigContribution[];
|
|
53
|
+
/**
|
|
54
|
+
* 由配置声明提取默认值表(CLI / 无 Schema 宿主用):
|
|
55
|
+
* 平台开关等 object 字段展开为逐键默认值(如 { douyin: true, ... })。
|
|
56
|
+
*/
|
|
57
|
+
export declare function defaultsFromContributions(contribs: ConfigContribution[]): Record<string, any>;
|
package/lib/config.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.defineConfig = defineConfig;
|
|
13
13
|
exports.mergeConfigContributions = mergeConfigContributions;
|
|
14
|
+
exports.defaultsFromContributions = defaultsFromContributions;
|
|
14
15
|
/** 类型辅助:声明即校验 */
|
|
15
16
|
function defineConfig(contribution) {
|
|
16
17
|
return contribution;
|
|
@@ -36,3 +37,30 @@ function mergeConfigContributions(lists) {
|
|
|
36
37
|
}
|
|
37
38
|
return out;
|
|
38
39
|
}
|
|
40
|
+
function applyFieldDefault(target, f) {
|
|
41
|
+
if (f.type === 'object' && f.fields) {
|
|
42
|
+
const obj = {};
|
|
43
|
+
for (const sub of f.fields)
|
|
44
|
+
applyFieldDefault(obj, sub);
|
|
45
|
+
// object 自身 default 与子字段 default 合并(子字段优先)
|
|
46
|
+
target[f.key] = isPlainObject(f.default) ? { ...f.default, ...obj } : obj;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (f.default !== undefined)
|
|
50
|
+
target[f.key] = f.default;
|
|
51
|
+
}
|
|
52
|
+
function isPlainObject(v) {
|
|
53
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 由配置声明提取默认值表(CLI / 无 Schema 宿主用):
|
|
57
|
+
* 平台开关等 object 字段展开为逐键默认值(如 { douyin: true, ... })。
|
|
58
|
+
*/
|
|
59
|
+
function defaultsFromContributions(contribs) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const c of contribs) {
|
|
62
|
+
for (const f of c.fields)
|
|
63
|
+
applyFieldDefault(out, f);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const CONFIG_ENVELOPE_KIND = "sns-parse-config";
|
|
2
|
+
export declare const CONFIG_ENVELOPE_VERSION = 1;
|
|
3
|
+
/** 脱敏占位;导入时遇此值跳过(保留现有密钥) */
|
|
4
|
+
export declare const MASK = "***";
|
|
5
|
+
export interface ConfigEnvelope {
|
|
6
|
+
kind: typeof CONFIG_ENVELOPE_KIND;
|
|
7
|
+
version: number;
|
|
8
|
+
exportedAt: string;
|
|
9
|
+
pluginName: string;
|
|
10
|
+
redacted: boolean;
|
|
11
|
+
config: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
/** 判断键名是否为敏感字段(另含 customHeaders[].value 等,由调用方逐层递归统一处理) */
|
|
14
|
+
export declare function isSecretKey(key: string): boolean;
|
|
15
|
+
/** 深度克隆并按敏感键名打码;数组原样保留(元素递归) */
|
|
16
|
+
export declare function redactConfig(config: any): any;
|
|
17
|
+
/** 构造导出信封 */
|
|
18
|
+
export declare function createConfigEnvelope(config: any, opts?: {
|
|
19
|
+
pluginName?: string;
|
|
20
|
+
includeSecrets?: boolean;
|
|
21
|
+
}): ConfigEnvelope;
|
|
22
|
+
/** 序列化信封(pretty JSON) */
|
|
23
|
+
export declare function serializeConfigEnvelope(env: ConfigEnvelope): string;
|
|
24
|
+
export declare function isConfigEnvelope(x: any): x is ConfigEnvelope;
|
|
25
|
+
/**
|
|
26
|
+
* 解析导入输入:接受
|
|
27
|
+
* - 信封 JSON
|
|
28
|
+
* - 裸配置对象 JSON
|
|
29
|
+
* - 旧版导出(无 kind,直接是配置)
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseConfigInput(input: string): {
|
|
32
|
+
config: Record<string, any>;
|
|
33
|
+
envelope: boolean;
|
|
34
|
+
pluginName?: string;
|
|
35
|
+
};
|
|
36
|
+
/** 深度合并:对象递归合并,数组/标量直接替换;MASK 值跳过(保留现有) */
|
|
37
|
+
export declare function mergeConfig(base: any, patch: any): any;
|
|
38
|
+
/** 统计将被覆盖的顶层键,供 import/migrate 输出摘要 */
|
|
39
|
+
export declare function diffKeys(base: any, patch: any): string[];
|
|
40
|
+
export declare function overrideFilePath(baseDir: string | undefined, name: string): string;
|
|
41
|
+
export declare function readOverride(baseDir: string | undefined, name: string): Record<string, any> | null;
|
|
42
|
+
export declare function writeOverride(baseDir: string | undefined, name: string, config: Record<string, any>, pluginName?: string): string;
|
|
43
|
+
/** 读覆盖文件并合并到 schema 配置之上(启动时调用) */
|
|
44
|
+
export declare function applyOverrideToConfig(config: any, baseDir: string | undefined, name: string): any;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MASK = exports.CONFIG_ENVELOPE_VERSION = exports.CONFIG_ENVELOPE_KIND = void 0;
|
|
4
|
+
exports.isSecretKey = isSecretKey;
|
|
5
|
+
exports.redactConfig = redactConfig;
|
|
6
|
+
exports.createConfigEnvelope = createConfigEnvelope;
|
|
7
|
+
exports.serializeConfigEnvelope = serializeConfigEnvelope;
|
|
8
|
+
exports.isConfigEnvelope = isConfigEnvelope;
|
|
9
|
+
exports.parseConfigInput = parseConfigInput;
|
|
10
|
+
exports.mergeConfig = mergeConfig;
|
|
11
|
+
exports.diffKeys = diffKeys;
|
|
12
|
+
exports.overrideFilePath = overrideFilePath;
|
|
13
|
+
exports.readOverride = readOverride;
|
|
14
|
+
exports.writeOverride = writeOverride;
|
|
15
|
+
exports.applyOverrideToConfig = applyOverrideToConfig;
|
|
16
|
+
/**
|
|
17
|
+
* 配置导出 / 导入 / 迁移。
|
|
18
|
+
*
|
|
19
|
+
* 目的:新旧包使用不同命名空间(旧 video-parser-all / 新 sns-parse),
|
|
20
|
+
* koishi 配置键随之不同,因此提供可跨命名空间移植的配置信封。
|
|
21
|
+
*
|
|
22
|
+
* - 导出:默认脱敏(密钥打码 MASK),--include-secrets 才输出明文
|
|
23
|
+
* - 导入:深度合并进当前配置;MASK 值跳过(保留现有密钥)
|
|
24
|
+
* - 持久化:自管覆盖文件 <baseDir>/data/<name>/config.override.json
|
|
25
|
+
*/
|
|
26
|
+
const fs_1 = require("fs");
|
|
27
|
+
const path_1 = require("path");
|
|
28
|
+
exports.CONFIG_ENVELOPE_KIND = 'sns-parse-config';
|
|
29
|
+
exports.CONFIG_ENVELOPE_VERSION = 1;
|
|
30
|
+
/** 脱敏占位;导入时遇此值跳过(保留现有密钥) */
|
|
31
|
+
exports.MASK = '***';
|
|
32
|
+
/** 需要脱敏的键名(大小写不敏感) */
|
|
33
|
+
const SECRET_KEYS = new Set([
|
|
34
|
+
'apikey', 'secretkey', 'secretid', 'accesskeyid', 'accesskeysecret',
|
|
35
|
+
'authtoken', 'ct0', 'twitterauthtoken', 'twitterct0', 'password',
|
|
36
|
+
]);
|
|
37
|
+
function isPlainObject(v) {
|
|
38
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
39
|
+
}
|
|
40
|
+
/** 判断键名是否为敏感字段(另含 customHeaders[].value 等,由调用方逐层递归统一处理) */
|
|
41
|
+
function isSecretKey(key) {
|
|
42
|
+
return SECRET_KEYS.has(key.toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
/** 深度克隆并按敏感键名打码;数组原样保留(元素递归) */
|
|
45
|
+
function redactConfig(config) {
|
|
46
|
+
if (Array.isArray(config))
|
|
47
|
+
return config.map(redactConfig);
|
|
48
|
+
if (!isPlainObject(config))
|
|
49
|
+
return config;
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [k, v] of Object.entries(config)) {
|
|
52
|
+
if (isSecretKey(k) && typeof v === 'string' && v)
|
|
53
|
+
out[k] = exports.MASK;
|
|
54
|
+
else
|
|
55
|
+
out[k] = redactConfig(v);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** 构造导出信封 */
|
|
60
|
+
function createConfigEnvelope(config, opts = {}) {
|
|
61
|
+
const cfg = opts.includeSecrets ? JSON.parse(JSON.stringify(config ?? {})) : redactConfig(config);
|
|
62
|
+
return {
|
|
63
|
+
kind: exports.CONFIG_ENVELOPE_KIND,
|
|
64
|
+
version: exports.CONFIG_ENVELOPE_VERSION,
|
|
65
|
+
exportedAt: new Date().toISOString(),
|
|
66
|
+
pluginName: opts.pluginName || '',
|
|
67
|
+
redacted: !opts.includeSecrets,
|
|
68
|
+
config: cfg,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** 序列化信封(pretty JSON) */
|
|
72
|
+
function serializeConfigEnvelope(env) {
|
|
73
|
+
return JSON.stringify(env, null, 2);
|
|
74
|
+
}
|
|
75
|
+
function isConfigEnvelope(x) {
|
|
76
|
+
return !!x && typeof x === 'object' && x.kind === exports.CONFIG_ENVELOPE_KIND;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 解析导入输入:接受
|
|
80
|
+
* - 信封 JSON
|
|
81
|
+
* - 裸配置对象 JSON
|
|
82
|
+
* - 旧版导出(无 kind,直接是配置)
|
|
83
|
+
*/
|
|
84
|
+
function parseConfigInput(input) {
|
|
85
|
+
let data;
|
|
86
|
+
const text = (input || '').trim();
|
|
87
|
+
if (!text)
|
|
88
|
+
throw new Error('导入内容为空');
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
throw new Error(`导入内容不是合法 JSON:${e?.message || e}`);
|
|
94
|
+
}
|
|
95
|
+
if (isConfigEnvelope(data)) {
|
|
96
|
+
return { config: data.config || {}, envelope: true, pluginName: data.pluginName };
|
|
97
|
+
}
|
|
98
|
+
if (isPlainObject(data))
|
|
99
|
+
return { config: data, envelope: false };
|
|
100
|
+
throw new Error('导入内容既不是配置信封也不是配置对象');
|
|
101
|
+
}
|
|
102
|
+
/** 深度合并:对象递归合并,数组/标量直接替换;MASK 值跳过(保留现有) */
|
|
103
|
+
function mergeConfig(base, patch) {
|
|
104
|
+
if (!isPlainObject(patch))
|
|
105
|
+
return patch;
|
|
106
|
+
const out = isPlainObject(base) ? { ...base } : {};
|
|
107
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
108
|
+
if (v === exports.MASK)
|
|
109
|
+
continue;
|
|
110
|
+
out[k] = isPlainObject(v) && isPlainObject(out[k]) ? mergeConfig(out[k], v) : v;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** 统计将被覆盖的顶层键,供 import/migrate 输出摘要 */
|
|
115
|
+
function diffKeys(base, patch) {
|
|
116
|
+
if (!isPlainObject(patch))
|
|
117
|
+
return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
120
|
+
if (v === exports.MASK)
|
|
121
|
+
continue;
|
|
122
|
+
if (isPlainObject(v) && isPlainObject(base?.[k]))
|
|
123
|
+
out.push(...diffKeys(base[k], v).map(sub => `${k}.${sub}`));
|
|
124
|
+
else
|
|
125
|
+
out.push(k);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/* ---------- 覆盖文件持久化 ---------- */
|
|
130
|
+
function overrideFilePath(baseDir, name) {
|
|
131
|
+
const root = baseDir || process.cwd();
|
|
132
|
+
return (0, path_1.join)(root, 'data', name, 'config.override.json');
|
|
133
|
+
}
|
|
134
|
+
function readOverride(baseDir, name) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = (0, fs_1.readFileSync)(overrideFilePath(baseDir, name), 'utf8');
|
|
137
|
+
const data = JSON.parse(raw);
|
|
138
|
+
return isConfigEnvelope(data) ? data.config : (isPlainObject(data) ? data : null);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function writeOverride(baseDir, name, config, pluginName) {
|
|
145
|
+
const file = overrideFilePath(baseDir, name);
|
|
146
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
|
|
147
|
+
const env = createConfigEnvelope(config, { pluginName: pluginName || name, includeSecrets: true });
|
|
148
|
+
(0, fs_1.writeFileSync)(file, serializeConfigEnvelope(env), 'utf8');
|
|
149
|
+
return file;
|
|
150
|
+
}
|
|
151
|
+
/** 读覆盖文件并合并到 schema 配置之上(启动时调用) */
|
|
152
|
+
function applyOverrideToConfig(config, baseDir, name) {
|
|
153
|
+
const override = readOverride(baseDir, name);
|
|
154
|
+
return override ? mergeConfig(config, override) : config;
|
|
155
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎层配置声明(中立 DSL):fetcher/parser/compose/sender/gateway 等引擎
|
|
3
|
+
* 自身读取的配置项,由 core 统一声明;koishi / CLI 兼容层据此生成 Schema 与默认值。
|
|
4
|
+
*
|
|
5
|
+
* 组序即 UI 展示序(koishi 兼容层在「发送策略」后插入扩展与平台的动态声明)。
|
|
6
|
+
*/
|
|
7
|
+
import { type ConfigContribution } from '../config';
|
|
8
|
+
/** 引擎层配置声明(有序;组名即 UI 分组标题) */
|
|
9
|
+
export declare function engineConfigContributions(): ConfigContribution[];
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.engineConfigContributions = engineConfigContributions;
|
|
4
|
+
/**
|
|
5
|
+
* 引擎层配置声明(中立 DSL):fetcher/parser/compose/sender/gateway 等引擎
|
|
6
|
+
* 自身读取的配置项,由 core 统一声明;koishi / CLI 兼容层据此生成 Schema 与默认值。
|
|
7
|
+
*
|
|
8
|
+
* 组序即 UI 展示序(koishi 兼容层在「发送策略」后插入扩展与平台的动态声明)。
|
|
9
|
+
*/
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const PROXY_FIELDS = [
|
|
12
|
+
{ key: 'enabled', type: 'boolean', default: false, description: '启用代理' },
|
|
13
|
+
{
|
|
14
|
+
key: 'protocol', type: 'union', default: 'http', description: '协议',
|
|
15
|
+
values: [
|
|
16
|
+
{ value: 'http', description: 'HTTP' },
|
|
17
|
+
{ value: 'https', description: 'HTTPS' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{ key: 'host', type: 'string', default: '127.0.0.1', description: '地址' },
|
|
21
|
+
{ key: 'port', type: 'number', default: 7890, description: '端口' },
|
|
22
|
+
{
|
|
23
|
+
key: 'auth', type: 'object', description: '认证',
|
|
24
|
+
fields: [
|
|
25
|
+
{ key: 'username', type: 'string', default: '', description: '用户名' },
|
|
26
|
+
{ key: 'password', type: 'string', default: '', description: '密码' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const AUTH_HEADER_FIELDS = [
|
|
31
|
+
{
|
|
32
|
+
key: 'authHeaderType', type: 'union', default: 'Bearer', description: '认证头类型',
|
|
33
|
+
values: [
|
|
34
|
+
{ value: 'Bearer', description: 'Bearer' },
|
|
35
|
+
{ value: 'X-API-Key', description: 'X-API-Key' },
|
|
36
|
+
{ value: 'Custom', description: '自定义' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{ key: 'customHeaderName', type: 'string', default: 'X-API-Key', description: '自定义头名称' },
|
|
40
|
+
];
|
|
41
|
+
const FIELD_MAPPING_FIELD = {
|
|
42
|
+
key: 'fieldMapping', type: 'string', role: 'textarea',
|
|
43
|
+
default: '{}', description: '字段映射 JSON',
|
|
44
|
+
};
|
|
45
|
+
const API_PLATFORM_VALUES = [
|
|
46
|
+
{ value: 'bilibili', description: '哔哩哔哩' },
|
|
47
|
+
{ value: 'douyin', description: '抖音' },
|
|
48
|
+
{ value: 'kuaishou', description: '快手' },
|
|
49
|
+
{ value: 'xiaohongshu', description: '小红书' },
|
|
50
|
+
{ value: 'weibo', description: '微博' },
|
|
51
|
+
{ value: 'xigua', description: '西瓜视频' },
|
|
52
|
+
{ value: 'youtube', description: 'YouTube' },
|
|
53
|
+
{ value: 'tiktok', description: 'TikTok' },
|
|
54
|
+
{ value: 'acfun', description: 'AcFun(A站)' },
|
|
55
|
+
{ value: 'zhihu', description: '知乎' },
|
|
56
|
+
{ value: 'weishi', description: '微视' },
|
|
57
|
+
{ value: 'huya', description: '虎牙' },
|
|
58
|
+
{ value: 'haokan', description: '好看视频' },
|
|
59
|
+
{ value: 'meipai', description: '美拍' },
|
|
60
|
+
{ value: 'twitter', description: 'Twitter/X' },
|
|
61
|
+
{ value: 'instagram', description: 'Instagram' },
|
|
62
|
+
{ value: 'doubao', description: '豆包' },
|
|
63
|
+
{ value: 'doubao_image', description: '豆包图片' },
|
|
64
|
+
{ value: 'jimeng', description: '即梦' },
|
|
65
|
+
{ value: 'oasis', description: '绿洲' },
|
|
66
|
+
{ value: 'wechat_channel', description: '视频号' },
|
|
67
|
+
{ value: 'lishi', description: '梨视频' },
|
|
68
|
+
{ value: 'quanmin', description: '全民直播' },
|
|
69
|
+
{ value: 'pipigx', description: '皮皮搞笑' },
|
|
70
|
+
{ value: 'pipixia', description: '皮皮虾' },
|
|
71
|
+
{ value: 'zuiyou', description: '最右' },
|
|
72
|
+
{ value: 'toutiao', description: '今日头条' },
|
|
73
|
+
];
|
|
74
|
+
const GLOBAL_FIELD_MAPPING_DEFAULT = [
|
|
75
|
+
'{',
|
|
76
|
+
' "title": "data.title",',
|
|
77
|
+
' "desc": "data.description",',
|
|
78
|
+
' "author": "data.author.name",',
|
|
79
|
+
' "uid": "data.author.id",',
|
|
80
|
+
' "avatar": "data.author.avatar",',
|
|
81
|
+
' "cover": "data.cover_url",',
|
|
82
|
+
' "video": "data.video_url",',
|
|
83
|
+
' "video_backup": "data.video_qualities",',
|
|
84
|
+
' "videos": "data.videos",',
|
|
85
|
+
' "type": "data.type",',
|
|
86
|
+
' "like": "data.statistics.likes",',
|
|
87
|
+
' "comment": "data.statistics.comments",',
|
|
88
|
+
' "collect": "data.statistics.favorites",',
|
|
89
|
+
' "share": "data.statistics.shares",',
|
|
90
|
+
' "play": "data.statistics.plays",',
|
|
91
|
+
' "duration": "data.duration",',
|
|
92
|
+
' "publishTime": "data.create_time",',
|
|
93
|
+
' "music_title": "data.music.title",',
|
|
94
|
+
' "music_author": "data.music.author",',
|
|
95
|
+
' "music_cover": "data.music.cover",',
|
|
96
|
+
' "music_url": "data.music.url"',
|
|
97
|
+
'}',
|
|
98
|
+
].join('\n');
|
|
99
|
+
/** 引擎层配置声明(有序;组名即 UI 分组标题) */
|
|
100
|
+
function engineConfigContributions() {
|
|
101
|
+
return [
|
|
102
|
+
(0, config_1.defineConfig)({
|
|
103
|
+
group: '消息格式',
|
|
104
|
+
fields: [
|
|
105
|
+
{
|
|
106
|
+
key: 'unifiedMessageFormat', type: 'string', role: 'textarea',
|
|
107
|
+
default: '标题:${标题}\n作者:${作者}\n简介:${简介}\n正文:${正文}\n翻译(${翻译提供方},${原文语言}):${翻译}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}',
|
|
108
|
+
description: '消息文字模板,可用变量:${标题} ${作者} ${简介} ${正文}(纯文字作品正文,启用后该类作品不再显示为简介) ${翻译} ${翻译提供方} ${原文语言} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者}(空行自动隐藏)',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
(0, config_1.defineConfig)({
|
|
113
|
+
group: '媒体发送',
|
|
114
|
+
fields: [
|
|
115
|
+
{ key: 'showImageText', type: 'boolean', default: true, description: '发送文字内容' },
|
|
116
|
+
{ key: 'showCoverImage', type: 'boolean', default: true, description: '发送封面图片' },
|
|
117
|
+
{ key: 'showCoverFile', type: 'boolean', default: true, description: '封面以图片形式发送(关闭则只发链接)' },
|
|
118
|
+
{ key: 'showCoverText', type: 'boolean', default: true, description: '封面图前显示文字提示' },
|
|
119
|
+
{ key: 'coverText', type: 'string', default: '封面:', description: '封面提示文字' },
|
|
120
|
+
{ key: 'showImageFileNew', type: 'boolean', default: true, description: '图片以图片形式发送(关闭则只发链接)' },
|
|
121
|
+
{ key: 'showAuthorAvatar', type: 'boolean', default: true, description: '发送作者头像' },
|
|
122
|
+
{ key: 'showAuthorAvatarFile', type: 'boolean', default: true, description: '头像以图片形式发送(关闭则只发链接)' },
|
|
123
|
+
{ key: 'showAuthorAvatarText', type: 'boolean', default: true, description: '头像前显示文字提示' },
|
|
124
|
+
{ key: 'authorAvatarText', type: 'string', default: '作者头像:', description: '头像提示文字' },
|
|
125
|
+
{ key: 'showMusicCover', type: 'boolean', default: true, description: '发送音乐封面' },
|
|
126
|
+
{ key: 'showVideoFile', type: 'boolean', default: true, description: '视频以视频形式发送(关闭则只发链接)' },
|
|
127
|
+
{ key: 'sendLiveMessage', type: 'boolean', default: true, description: '直播作品发文字消息(不发视频)' },
|
|
128
|
+
{ key: 'mergeSameOriginImages', type: 'boolean', default: true, description: '同源切图内容识别合并(纯像素内容裁决,不依赖链接/文件名/色调风格):网格/竖堆/横拼三种布局各自经接缝连续性验证(低频趋势延续+纹理能量可验证性),取证据最强者合并;均不过回退逐张;需 ffmpeg' },
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
(0, config_1.defineConfig)({
|
|
132
|
+
group: '音乐语音(需 silk 和 ffmpeg)',
|
|
133
|
+
fields: [
|
|
134
|
+
{ key: 'showMusicVoice', type: 'boolean', default: false, description: '音乐链接以语音形式发送' },
|
|
135
|
+
{ key: 'showMusicVoiceFile', type: 'boolean', default: true, description: '音乐是否以语音形式发送(关闭则只发送链接)' },
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
(0, config_1.defineConfig)({
|
|
139
|
+
group: 'GIF 转换',
|
|
140
|
+
fields: [
|
|
141
|
+
{ key: 'gifConvertEnabled', type: 'boolean', default: true, description: '推文动图转 GIF 发送(ffmpeg 随插件自动安装,失败回退原视频)' },
|
|
142
|
+
{ key: 'gifMaxWidth', type: 'number', min: 120, max: 1080, step: 1, default: 480, description: 'GIF 宽度 (px)' },
|
|
143
|
+
{ key: 'gifFps', type: 'number', min: 5, max: 30, step: 1, default: 15, description: 'GIF 帧率' },
|
|
144
|
+
{ key: 'gifMaxDurationSec', type: 'number', min: 1, max: 60, step: 1, default: 15, description: 'GIF 时长上限 (s)' },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
(0, config_1.defineConfig)({
|
|
148
|
+
group: '性能与限制',
|
|
149
|
+
fields: [
|
|
150
|
+
{ key: 'maxDescLength', type: 'number', min: 0, step: 1, default: 200, description: '简介长度上限' },
|
|
151
|
+
{ key: 'maxConcurrent', type: 'number', min: 1, step: 1, default: 3, description: '解析最大并发数' },
|
|
152
|
+
],
|
|
153
|
+
}),
|
|
154
|
+
(0, config_1.defineConfig)({
|
|
155
|
+
group: '发送策略',
|
|
156
|
+
fields: [
|
|
157
|
+
{
|
|
158
|
+
key: 'sendStrategy', type: 'union', default: 'single', description: '发送策略',
|
|
159
|
+
values: [
|
|
160
|
+
{ value: 'single', description: '单条整合(默认;图片过多/含视频自动回退合并转发)' },
|
|
161
|
+
{ value: 'split', description: '逐条分开发送(旧版行为)' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{ key: 'singleSendMaxImages', type: 'number', min: 1, default: 10, description: '单条消息最大图片数(超出回退合并转发)' },
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
(0, config_1.defineConfig)({
|
|
168
|
+
group: '网络与请求',
|
|
169
|
+
fields: [
|
|
170
|
+
{ key: 'timeout', type: 'number', min: 0, step: 1, default: 180000, description: 'API 请求超时 (ms)' },
|
|
171
|
+
{ key: 'videoSendTimeout', type: 'number', min: 0, step: 1, default: 180000, description: '消息发送超时 (ms)' },
|
|
172
|
+
{ key: 'userAgent', type: 'string', default: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', description: 'User-Agent' },
|
|
173
|
+
{ key: 'proxy', type: 'object', description: 'HTTP/HTTPS 代理', fields: PROXY_FIELDS },
|
|
174
|
+
{
|
|
175
|
+
key: 'customHeaders', type: 'array', default: [], description: '自定义请求头',
|
|
176
|
+
itemFields: [
|
|
177
|
+
{ key: 'name', type: 'string', required: true, description: '头名称' },
|
|
178
|
+
{ key: 'value', type: 'string', required: true, description: '头值' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}),
|
|
183
|
+
(0, config_1.defineConfig)({
|
|
184
|
+
group: '发送与重试',
|
|
185
|
+
fields: [
|
|
186
|
+
{ key: 'ignoreSendError', type: 'boolean', default: true, description: '忽略发送失败' },
|
|
187
|
+
{ key: 'retryTimes', type: 'number', min: 0, default: 3, description: '重试次数' },
|
|
188
|
+
{ key: 'retryInterval', type: 'number', min: 0, default: 1000, description: '重试间隔 (ms)' },
|
|
189
|
+
{ key: 'enableForward', type: 'boolean', default: false, description: '合并转发(OneBot/Satori)' },
|
|
190
|
+
],
|
|
191
|
+
}),
|
|
192
|
+
(0, config_1.defineConfig)({
|
|
193
|
+
group: '缓存与临时文件',
|
|
194
|
+
fields: [
|
|
195
|
+
{ key: 'deduplicationInterval', type: 'number', min: 0, default: 180, description: '去重间隔 (s)' },
|
|
196
|
+
{ key: 'enableDeduplication', type: 'boolean', default: true, description: '启用重复解析检测与提示' },
|
|
197
|
+
{ key: 'cacheTTL', type: 'number', min: 0, default: 600, description: '缓存时间 (s)' },
|
|
198
|
+
],
|
|
199
|
+
}),
|
|
200
|
+
(0, config_1.defineConfig)({
|
|
201
|
+
group: 'API 与平台',
|
|
202
|
+
fields: [
|
|
203
|
+
{ key: 'primaryApiUrl', type: 'string', default: 'https://api.bugpk.com/api/short_videos', hidden: true },
|
|
204
|
+
{ key: 'backupApiUrl', type: 'string', default: 'https://api.bugpk.com/api/svparse', hidden: true },
|
|
205
|
+
{
|
|
206
|
+
key: 'customApis', type: 'array', default: [], description: '覆盖内置平台 API',
|
|
207
|
+
itemFields: [
|
|
208
|
+
{ key: 'platform', type: 'union', required: true, description: '平台', values: API_PLATFORM_VALUES },
|
|
209
|
+
{ key: 'apiUrl', type: 'string', description: 'API 地址' },
|
|
210
|
+
{ key: 'apiKey', type: 'string', default: '', description: 'API Key' },
|
|
211
|
+
...AUTH_HEADER_FIELDS.map(f => ({ ...f })),
|
|
212
|
+
FIELD_MAPPING_FIELD,
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: 'customPlatforms', type: 'array', default: [], description: '自定义新平台',
|
|
217
|
+
itemFields: [
|
|
218
|
+
{ key: 'name', type: 'string', required: true, description: '平台名称' },
|
|
219
|
+
{ key: 'exampleUrl', type: 'string', description: '示例链接' },
|
|
220
|
+
{ key: 'keywords', type: 'string', required: true, description: '关键词(逗号分隔)' },
|
|
221
|
+
{ key: 'apiUrl', type: 'string', required: true, description: '解析 API' },
|
|
222
|
+
{ key: 'apiKey', type: 'string', default: '', description: 'API Key' },
|
|
223
|
+
...AUTH_HEADER_FIELDS.map(f => ({ ...f })),
|
|
224
|
+
FIELD_MAPPING_FIELD,
|
|
225
|
+
{ key: 'proxy', type: 'object', description: '独立代理(覆盖全局代理)', fields: PROXY_FIELDS },
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: 'globalFieldMapping', type: 'string', role: 'textarea',
|
|
230
|
+
default: GLOBAL_FIELD_MAPPING_DEFAULT, description: '全局字段映射 JSON',
|
|
231
|
+
},
|
|
232
|
+
{ key: 'twitterAuthToken', type: 'string', default: '', role: 'secret', description: 'X 登录态 auth_token(解析需登录推文用,受 Cloudflare 指纹限制可能 403)' },
|
|
233
|
+
{ key: 'twitterCt0', type: 'string', default: '', role: 'secret', description: 'X 登录态 ct0(与 auth_token 配对)' },
|
|
234
|
+
{ key: 'tweetTranslateEnabled', type: 'boolean', default: true, description: '外语推文自动翻译(配置 X 登录态时用网页同源 Grok 翻译,否则通用翻译;附译文行)' },
|
|
235
|
+
{
|
|
236
|
+
key: 'tweetTranslateLang', type: 'union', default: 'zh', description: '翻译目标语言',
|
|
237
|
+
values: [
|
|
238
|
+
{ value: 'zh', description: '简体中文' },
|
|
239
|
+
{ value: 'zh-TW', description: '繁體中文' },
|
|
240
|
+
{ value: 'en', description: 'English' },
|
|
241
|
+
{ value: 'ja', description: '日本語' },
|
|
242
|
+
{ value: 'ko', description: '한국어' },
|
|
243
|
+
{ value: 'fr', description: 'Français' },
|
|
244
|
+
{ value: 'de', description: 'Deutsch' },
|
|
245
|
+
{ value: 'ru', description: 'Русский' },
|
|
246
|
+
{ value: 'es', description: 'Español' },
|
|
247
|
+
{ value: 'pt', description: 'Português' },
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
{ key: 'apiKey', type: 'string', default: '', role: 'secret', description: '新网关 API Key(https://api-new.ifphp.com/auth/login 注册获取;留空用旧网关)' },
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
(0, config_1.defineConfig)({
|
|
254
|
+
group: '界面文本',
|
|
255
|
+
fields: [
|
|
256
|
+
{ key: 'waitingTipText', type: 'string', default: '正在解析视频,请稍候...', description: '等待提示' },
|
|
257
|
+
{ key: 'unsupportedPlatformText', type: 'string', default: '不支持该平台链接', description: '不支持提示' },
|
|
258
|
+
{ key: 'invalidLinkText', type: 'string', default: '无效的视频链接', description: '无效链接提示' },
|
|
259
|
+
{ key: 'parseErrorPrefix', type: 'string', default: '❌ 解析失败:', description: '错误前缀' },
|
|
260
|
+
{ key: 'parseErrorItemFormat', type: 'string', default: '【${url}】: ${msg}', description: '错误格式' },
|
|
261
|
+
{ key: 'deduplicationTipText', type: 'string', default: '链接 ${url} 在最近 ${interval} 秒内已解析过,已跳过。', description: '重复解析提示,支持变量 ${url} ${interval}' },
|
|
262
|
+
],
|
|
263
|
+
}),
|
|
264
|
+
];
|
|
265
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ParserRuntime } from './runtime';
|
|
2
|
+
import type { ParsedData } from '../types';
|
|
3
|
+
export declare function fetchApi(rt: ParserRuntime, url: string, type: string, fieldMapping?: Record<string, string>, platformConf?: any): Promise<ParsedData>;
|
|
4
|
+
export declare function parseUrl(rt: ParserRuntime, url: string, type: string, fieldMapping?: Record<string, string>, platformConf?: any): Promise<{
|
|
5
|
+
success: true;
|
|
6
|
+
data: ParsedData;
|
|
7
|
+
} | {
|
|
8
|
+
success: false;
|
|
9
|
+
msg: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function processSingleUrl(rt: ParserRuntime, url: string, type: string, fieldMapping?: Record<string, string>, platformConf?: any): Promise<{
|
|
12
|
+
success: true;
|
|
13
|
+
data: {
|
|
14
|
+
text: string;
|
|
15
|
+
parsed: ParsedData;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
success: false;
|
|
19
|
+
msg: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}>;
|