@sns-parse/core 0.4.0-alpha.8 → 0.5.0-alpha.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.
@@ -13,7 +13,6 @@ const common_1 = require("../utils/common");
13
13
  const format_1 = require("../utils/format");
14
14
  const parser_1 = require("./parser");
15
15
  const platform_config_1 = require("./platform-config");
16
- const twitter_1 = require("./twitter");
17
16
  const translate_1 = require("./translate");
18
17
  const tls_client_1 = require("../utils/tls-client");
19
18
  const gateway_1 = require("./gateway");
@@ -26,35 +25,35 @@ async function fetchApi(rt, url, type, fieldMapping, platformConf) {
26
25
  return cached.data;
27
26
  }
28
27
  const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || (0, platform_config_1.getPlatformConfig)(rt, type);
29
- // X / Twitter:统一网关均走原生 syndication 解析(除非用户自定义了 API)
30
- if (type === 'twitter' && !dedicatedUrl) {
31
- (0, logger_1.debugLog)('twitter 走原生 syndication 解析:', url);
32
- const twCreds = (config.twitterAuthToken && config.twitterCt0)
33
- ? { authToken: String(config.twitterAuthToken), ct0: String(config.twitterCt0) }
34
- : undefined;
28
+ // 平台原生解析钩子(如 X/Twitter 的 syndication/GraphQL):实现由平台包提供,
29
+ // 用户自定义专属 API 时优先专属 API。core 只调度,不持有平台实现。
30
+ const nativeDef = (rt.defs || []).find(d => d.type === type && typeof d.parse === 'function');
31
+ if (nativeDef && nativeDef.parse && !dedicatedUrl) {
32
+ (0, logger_1.debugLog)(`${type} 走平台原生解析(${nativeDef.label || nativeDef.type}):`, url);
35
33
  // 代理串通:axios 走 runtime 实例;tlsget-rs(GraphQL/Grok 翻译)显式透传代理地址
36
34
  const tlsProxy = (proxyConfig.enabled && proxyConfig.host)
37
35
  ? `${proxyConfig.protocol || 'http'}://${proxyConfig.auth?.username ? `${encodeURIComponent(proxyConfig.auth.username)}:${encodeURIComponent(proxyConfig.auth.password || '')}@` : ''}${proxyConfig.host}:${proxyConfig.port || 7890}`
38
36
  : undefined;
39
37
  const tlsGetWithProxy = (u, o) => (0, tls_client_1.tlsGet)(u, { ...o, proxy: tlsProxy });
40
- const parsed = await (0, twitter_1.parseTwitter)(url, http, twCreds, tlsGetWithProxy);
41
- // 推文翻译:目标语种与推文语种相同时跳过;Grok(需登录态)优先,通用翻译兜底
38
+ const ctx = { http, config, getGraphql: tlsGetWithProxy };
39
+ const parsed = await nativeDef.parse(url, ctx);
40
+ // 推文翻译:目标语种与推文语种相同时跳过;平台原生翻译(Grok,需登录态)优先,通用翻译兜底
42
41
  if (config.tweetTranslateEnabled && parsed.desc) {
43
42
  const target = config.tweetTranslateLang || 'zh';
44
43
  if (!(0, translate_1.shouldSkipTranslate)(parsed.lang, target)) {
45
44
  let translated = null;
46
45
  let provider = '';
47
- if (twCreds) {
48
- const grok = await (0, twitter_1.fetchGrokTranslation)(url, target, twCreds, tlsGetWithProxy).catch(() => null);
49
- if (grok) {
50
- translated = grok.text;
51
- provider = 'Grok';
52
- if (!parsed.lang && grok.sourceLang)
53
- parsed.lang = grok.sourceLang;
54
- }
46
+ const nativeTr = typeof nativeDef.translate === 'function'
47
+ ? await nativeDef.translate(ctx, url, parsed.desc, target).catch(() => null)
48
+ : null;
49
+ if (nativeTr) {
50
+ translated = nativeTr.text;
51
+ provider = 'Grok';
52
+ if (!parsed.lang && nativeTr.sourceLang)
53
+ parsed.lang = nativeTr.sourceLang;
55
54
  }
56
55
  if (!translated) {
57
- const generic = await (0, translate_1.translateText)(rt, parsed.desc, target, parsed.lang);
56
+ const generic = await rt.extensions.translate?.(rt, parsed.desc, target, parsed.lang);
58
57
  if (generic) {
59
58
  translated = generic.text;
60
59
  provider = generic.provider;
@@ -4,7 +4,8 @@
4
4
  * 平台定义列表(defs)由调用方传入:
5
5
  * - koishi 兼容层传内置 BUILTIN_PLATFORMS(本地 definitions)
6
6
  * - CLI 传 collectPlatformDefinitions()(已安装的 @sns-parse/platform-*)
7
- * 默认扩展实现同样注入:koishi 传含 NSFW 的完整默认实现,CLI 传 createCoreExtensions()。
7
+ * 默认扩展实现同样由宿主注入(loadExtensionImplementations() 发现已安装 ext-*);
8
+ * core 只负责调度,不持有任何功能实现。
8
9
  */
9
10
  import { AxiosInstance } from 'axios';
10
11
  import type { ParsedData, CustomPlatformConfig } from '../types';
@@ -33,6 +34,8 @@ export interface ParserRuntime {
33
34
  pattern: RegExp;
34
35
  type: string;
35
36
  }[];
37
+ /** 平台定义列表(原生解析/翻译钩子由 fetcher 查找使用) */
38
+ defs: PlatformDefinition[];
36
39
  /** 平台专属端点映射(由 defs 计算;getPlatformConfig 使用) */
37
40
  dedicatedApis: DedicatedApiMaps;
38
41
  /** 扩展能力(默认实现 + 宿主覆盖) */
@@ -45,5 +48,3 @@ export interface CreateRuntimeOptions {
45
48
  defaultExtensions?: VideoParserExtensions;
46
49
  }
47
50
  export declare function createRuntime(source: any, config: any, opts?: CreateRuntimeOptions): ParserRuntime;
48
- /** core 自带扩展实现(无 NSFW:CLI / 嵌入式宿主用;koishi 层在其上叠加 NSFW) */
49
- export declare function createCoreExtensions(): VideoParserExtensions;
@@ -4,14 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createRuntime = createRuntime;
7
- exports.createCoreExtensions = createCoreExtensions;
8
7
  /**
9
8
  * 运行时构建(自宿主 runtime 下沉,平台无关)。
10
9
  *
11
10
  * 平台定义列表(defs)由调用方传入:
12
11
  * - koishi 兼容层传内置 BUILTIN_PLATFORMS(本地 definitions)
13
12
  * - CLI 传 collectPlatformDefinitions()(已安装的 @sns-parse/platform-*)
14
- * 默认扩展实现同样注入:koishi 传含 NSFW 的完整默认实现,CLI 传 createCoreExtensions()。
13
+ * 默认扩展实现同样由宿主注入(loadExtensionImplementations() 发现已安装 ext-*);
14
+ * core 只负责调度,不持有任何功能实现。
15
15
  */
16
16
  const axios_1 = __importDefault(require("axios"));
17
17
  const https_proxy_agent_1 = require("https-proxy-agent");
@@ -20,9 +20,6 @@ const cache_1 = require("../utils/cache");
20
20
  const field_mapping_1 = require("../utils/field-mapping");
21
21
  const platform_config_1 = require("./platform-config");
22
22
  const gateway_1 = require("./gateway");
23
- const merge_1 = require("../utils/merge");
24
- const gif_1 = require("../utils/gif");
25
- const translate_1 = require("./translate");
26
23
  function createRuntime(source, config, opts = {}) {
27
24
  const host = (0, host_1.createHost)(source);
28
25
  const ctx = host.context;
@@ -76,15 +73,8 @@ function createRuntime(source, config, opts = {}) {
76
73
  contentDedupCache,
77
74
  customPlatforms,
78
75
  allRules,
76
+ defs,
79
77
  dedicatedApis,
80
78
  extensions,
81
79
  };
82
80
  }
83
- /** core 自带扩展实现(无 NSFW:CLI / 嵌入式宿主用;koishi 层在其上叠加 NSFW) */
84
- function createCoreExtensions() {
85
- return {
86
- mergeImages: merge_1.mergeImages,
87
- mp4ToGif: gif_1.mp4ToGif,
88
- translate: translate_1.translateText,
89
- };
90
- }
@@ -1,15 +1,10 @@
1
1
  /**
2
- * 推文翻译:Google gtx 免费端点(无 Key、GET-only)。
3
- *
4
- * X 网页端的 Grok 翻译走私有通道(translations/show.json 已 404,GraphQL
5
- * 仅返回 is_translatable 标志),无公开端点可调,故用通用翻译源兜底。
6
- * 译文存入 ParsedData.translation,经 ${翻译} 模板变量展示,空值行自动隐藏。
2
+ * 语种决策与展示工具(纯函数,属调度/编排层的共享工具)。
3
+ * 翻译实现(Google gtx + MyMemory)在 @sns-parse/ext-translate;
4
+ * 平台原生翻译(如 X 网页同源 Grok)在对应平台包的 translate 钩子。
7
5
  */
8
- import type { ParserRuntime } from './runtime';
9
6
  import type { TranslateResult } from '../extensions';
10
7
  export type { TranslateResult };
11
- /** 判断是否需要跳过翻译(目标语系与推文语种相同 / 无语言内容) */
8
+ /** 判断是否需要跳过翻译(目标语种与源语种相同 / 无文本内容) */
12
9
  export declare function shouldSkipTranslate(tweetLang: string | undefined, target: string): boolean;
13
10
  export declare function langName(code?: string): string;
14
- /** 翻译文本;失败返回 null(调用方保持无译文继续发送)。gtx 优先,MyMemory 兜底 */
15
- export declare function translateText(rt: ParserRuntime, text: string, target: string, sourceLang?: string): Promise<TranslateResult | null>;
@@ -2,15 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.shouldSkipTranslate = shouldSkipTranslate;
4
4
  exports.langName = langName;
5
- exports.translateText = translateText;
6
- const logger_1 = require("../logger");
7
- /** 判断是否需要跳过翻译(目标语系与推文语种相同 / 无语言内容) */
5
+ /** 判断是否需要跳过翻译(目标语种与源语种相同 / 无文本内容) */
8
6
  function shouldSkipTranslate(tweetLang, target) {
9
7
  if (!tweetLang)
10
8
  return false;
11
9
  const l = tweetLang.toLowerCase();
12
10
  if (l === 'zxx' || l === 'und')
13
- return l === 'zxx'; // zxx=无语言内容;und=未定则仍尝试
11
+ return l === 'zxx'; // zxx=无文本内容;und=未识别可尝试
14
12
  const t = target.toLowerCase();
15
13
  if (t === 'zh')
16
14
  return l === 'zh' || l.startsWith('zh-');
@@ -20,7 +18,7 @@ function shouldSkipTranslate(tweetLang, target) {
20
18
  return l === 'en';
21
19
  return l === t;
22
20
  }
23
- /** 常见语种代码 → 中文名(未知代码原样展示) */
21
+ /** 语种代码 → 展示名(未知返回原代码) */
24
22
  const LANG_NAMES = {
25
23
  fr: '法语', ja: '日语', ko: '韩语', en: '英语', zh: '中文', es: '西班牙语', pt: '葡萄牙语',
26
24
  de: '德语', ru: '俄语', ar: '阿拉伯语', it: '意大利语', th: '泰语', vi: '越南语',
@@ -37,71 +35,3 @@ function langName(code) {
37
35
  return '';
38
36
  return LANG_NAMES[base] || code;
39
37
  }
40
- /** 翻译文本;失败返回 null(调用方保持无译文继续发送)。gtx 优先,MyMemory 兜底 */
41
- async function translateText(rt, text, target, sourceLang) {
42
- if (!text)
43
- return null;
44
- const out = await viaGtx(rt, text, target);
45
- if (out)
46
- return { text: out, provider: 'Google' };
47
- const backup = await viaMyMemory(rt, text, target, sourceLang);
48
- if (backup)
49
- return { text: backup, provider: 'MyMemory' };
50
- return null;
51
- }
52
- /** Google gtx 免费端点(无 Key;sl=auto 自动检测源语种) */
53
- async function viaGtx(rt, text, target) {
54
- const url = 'https://translate.googleapis.com/translate_a/single' +
55
- `?client=gtx&sl=auto&dt=t&tl=${encodeURIComponent(target)}` +
56
- `&q=${encodeURIComponent(text.slice(0, 2000))}`;
57
- try {
58
- const res = await rt.http.get(url, {
59
- timeout: 15000,
60
- headers: { 'User-Agent': rt.config.userAgent },
61
- });
62
- const data = res.data;
63
- // gtx 响应形如 [[["译文","原文",null,null,10]],null,"fr",...]
64
- if (Array.isArray(data?.[0])) {
65
- const out = data[0].map((seg) => (typeof seg?.[0] === 'string' ? seg[0] : '')).join('').trim();
66
- if (out)
67
- return out;
68
- }
69
- (0, logger_1.debugLog)('gtx 翻译响应结构异常,尝试备用通道');
70
- return null;
71
- }
72
- catch (e) {
73
- (0, logger_1.debugLog)(`gtx 翻译失败:${e?.message || e}`);
74
- return null;
75
- }
76
- }
77
- /** MyMemory 备用(免费无 Key;需已知源语种,不支持 auto) */
78
- async function viaMyMemory(rt, text, target, sourceLang) {
79
- const sl = normalizeLang(sourceLang);
80
- if (!sl)
81
- return null;
82
- const tl = target.toLowerCase();
83
- const url = 'https://api.mymemory.translated.net/get' +
84
- `?q=${encodeURIComponent(text.slice(0, 500))}&langpair=${encodeURIComponent(sl + '|' + tl)}`;
85
- try {
86
- const res = await rt.http.get(url, { timeout: 15000, headers: { 'User-Agent': rt.config.userAgent } });
87
- const out = res.data?.responseData?.translatedText;
88
- if (typeof out === 'string' && out.trim() && !/^MYMEMORY WARNING/i.test(out))
89
- return out.trim();
90
- (0, logger_1.debugLog)('MyMemory 翻译响应异常,放弃译文');
91
- return null;
92
- }
93
- catch (e) {
94
- logger_1.logger.info(`推文翻译失败(跳过译文继续发送):${e?.message || e}`);
95
- return null;
96
- }
97
- }
98
- function normalizeLang(lang) {
99
- if (!lang)
100
- return null;
101
- const l = lang.toLowerCase();
102
- if (l === 'und' || l === 'zxx')
103
- return null;
104
- if (l.startsWith('zh'))
105
- return 'zh-CN';
106
- return l.split('-')[0];
107
- }
package/lib/index.d.ts CHANGED
@@ -16,14 +16,10 @@ export * from './utils/concurrency';
16
16
  export * from './utils/field-mapping';
17
17
  export * from './utils/image-size';
18
18
  export * from './utils/tls-client';
19
- export * from './utils/gif';
20
- export * from './utils/merge';
21
19
  export * from './engine/gateway';
22
20
  export * from './engine/config';
23
21
  export * from './engine/platform-config';
24
22
  export * from './engine/translate';
25
- export * from './engine/twitter';
26
- export * from './engine/twitter-user';
27
23
  export * from './engine/parser';
28
24
  export * from './engine/fetcher';
29
25
  export * from './engine/runtime';
package/lib/index.js CHANGED
@@ -32,14 +32,10 @@ __exportStar(require("./utils/concurrency"), exports);
32
32
  __exportStar(require("./utils/field-mapping"), exports);
33
33
  __exportStar(require("./utils/image-size"), exports);
34
34
  __exportStar(require("./utils/tls-client"), exports);
35
- __exportStar(require("./utils/gif"), exports);
36
- __exportStar(require("./utils/merge"), exports);
37
35
  __exportStar(require("./engine/gateway"), exports);
38
36
  __exportStar(require("./engine/config"), exports);
39
37
  __exportStar(require("./engine/platform-config"), exports);
40
38
  __exportStar(require("./engine/translate"), exports);
41
- __exportStar(require("./engine/twitter"), exports);
42
- __exportStar(require("./engine/twitter-user"), exports);
43
39
  __exportStar(require("./engine/parser"), exports);
44
40
  __exportStar(require("./engine/fetcher"), exports);
45
41
  __exportStar(require("./engine/runtime"), exports);
package/lib/platform.d.ts CHANGED
@@ -1,9 +1,22 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import type { ParsedData } from './types';
1
3
  import { type ConfigContribution } from './config';
4
+ /** 平台原生解析上下文(fetcher 透传:HTTP 实例、配置、GraphQL GET 器) */
5
+ export interface PlatformParseContext {
6
+ http: AxiosInstance;
7
+ config: any;
8
+ /** TLS 指纹 GET 器(tlsget-rs;平台实现用于鉴权 GraphQL) */
9
+ getGraphql?: (url: string, opts: any) => Promise<{
10
+ status: number;
11
+ data: any;
12
+ }>;
13
+ }
2
14
  /**
3
15
  * 平台定义契约:每个平台的自描述单元,是「每平台一个包」的基础。
4
16
  *
5
- * 一个 PlatformDefinition 应自包含该平台的全部静态信息(链接规则、专属 API、
6
- * 提示等);动态解析逻辑(normalize/parseNative)在后续阶段以可选钩子加入。
17
+ * 一个 PlatformDefinition 自包含该平台的全部静态信息(链接规则、专属 API、
18
+ * 提示等);有原生解析能力的平台(如 X/Twitter 的 syndication/GraphQL 路径)
19
+ * 通过 parse/translate 钩子提供实现——core 只调度,不持有平台实现。
7
20
  */
8
21
  export interface PlatformDefinition {
9
22
  /** 平台类型标识(与 config.platformEnabled 的键一致) */
@@ -23,6 +36,13 @@ export interface PlatformDefinition {
23
36
  label?: string;
24
37
  /** 解析失败时的平台特定引导提示 */
25
38
  hints?: string[];
39
+ /** 原生解析钩子:不经网关,由平台包自实现(如 X syndication/GraphQL) */
40
+ parse?(url: string, ctx: PlatformParseContext): Promise<ParsedData>;
41
+ /** 原生翻译钩子(如 X 网页同源 Grok 翻译);失败/不支持返回 null 回落通用翻译 */
42
+ translate?(ctx: PlatformParseContext, url: string, text: string, target: string): Promise<{
43
+ text: string;
44
+ sourceLang?: string;
45
+ } | null>;
26
46
  }
27
47
  /**
28
48
  * 由平台定义聚合出「平台相关配置」(平台开关 / 专属 API 优先)。
@@ -28,7 +28,10 @@ export interface SeamVerdict {
28
28
  /** 结论文案 */
29
29
  reason: string;
30
30
  }
31
- /** 内容验证:所有接缝(趋势+纹理)均通过才成立;返回最大接缝比值与逐缝证据 */
31
+ /** 内容验证:所有接缝(趋势+纹理)均通过才成立;接缝总数 ≥3 时允许至多一条
32
+ * 「边界容差缝」(比值 ∈ [3,5)——平台独立重压缩的典型错位幅度;多条缝相互
33
+ * 印证才可信,2 缝场景证据不足不放行;平坦不可验证缝不参与容差)。
34
+ * 返回最大接缝比值与逐缝证据。 */
32
35
  export declare function verifyLayout(layout: MergeLayout, grays: Buffer[]): {
33
36
  pass: boolean;
34
37
  score: number;
@@ -37,6 +37,12 @@ const COL_LEN = 64;
37
37
  /** 接缝差异 / 内部自然差异 的判定阈值(母图切片 ≈1,无关图片 ≫1;下限 3 防平坦图除零) */
38
38
  const BOUNDARY_THRESH = 3.0;
39
39
  const MAD_FLOOR = 3.0;
40
+ /** 单缝边界容差:平台(如 X)对各分片独立重压缩会让个别接缝低频轻微错位。
41
+ * 仅当接缝总数 ≥3(≥4 图,多条缝相互印证)且恰好一条落在
42
+ * [BOUNDARY_THRESH, BORDERLINE_THRESH)、其余全部通过时放行;平坦不可验证缝不参与容差 */
43
+ const BORDERLINE_THRESH = 5.0;
44
+ const BORDERLINE_MAX = 1;
45
+ const BORDERLINE_MIN_SEAMS = 3;
40
46
  /** 边界带纹理能量下限:低于此值视为「不可验证接缝」(相似背景/平边照片会骗过亮度对比) */
41
47
  const TEXTURE_MIN = 4.0;
42
48
  /* ---------- 尺寸候选 ---------- */
@@ -184,7 +190,10 @@ function seamVerdictH(a, b, from, to) {
184
190
  [colOf(b, 2), colOf(b, 1)],
185
191
  ]);
186
192
  }
187
- /** 内容验证:所有接缝(趋势+纹理)均通过才成立;返回最大接缝比值与逐缝证据 */
193
+ /** 内容验证:所有接缝(趋势+纹理)均通过才成立;接缝总数 ≥3 时允许至多一条
194
+ * 「边界容差缝」(比值 ∈ [3,5)——平台独立重压缩的典型错位幅度;多条缝相互
195
+ * 印证才可信,2 缝场景证据不足不放行;平坦不可验证缝不参与容差)。
196
+ * 返回最大接缝比值与逐缝证据。 */
188
197
  function verifyLayout(layout, grays) {
189
198
  const seams = [];
190
199
  const n = grays.length;
@@ -208,7 +217,11 @@ function verifyLayout(layout, grays) {
208
217
  if (!seams.length)
209
218
  return { pass: false, score: Infinity, seams };
210
219
  const worst = Math.max(...seams.map((v) => v.score));
211
- return { pass: seams.every((v) => v.ok), score: worst, seams };
220
+ const failed = seams.filter((v) => !v.ok);
221
+ const borderline = failed.filter((v) => v.score < BORDERLINE_THRESH);
222
+ const pass = seams.every((v) => v.ok)
223
+ || (seams.length >= BORDERLINE_MIN_SEAMS && borderline.length <= BORDERLINE_MAX && failed.length === borderline.length);
224
+ return { pass, score: worst, seams };
212
225
  }
213
226
  /** 内容识别:候选布局逐一经接缝连续性验证,取通过者中比值最小的一个 */
214
227
  function detectMergeLayout(sizes, grays) {
@@ -232,8 +245,9 @@ function pickMergeLayout(sizes, grays) {
232
245
  let best = null;
233
246
  for (const c of candidates) {
234
247
  const { pass, score, seams } = verifyLayout(c, grays);
248
+ const tolerated = seams.filter((v) => !v.ok).length;
235
249
  const detail = seams.map((v) => `[${v.dir === 'V' ? '竖' : '横'}缝 ${v.from}→${v.to}] ${v.reason}`).join(';');
236
- (0, logger_1.debugLog)(`同源合并证据·${layoutName(c)}:${pass ? '通过' : '拒绝'},worst=${score === Infinity ? '∞' : score.toFixed(2)} ⟶ ${detail}`);
250
+ (0, logger_1.debugLog)(`同源合并证据·${layoutName(c)}:${pass ? '通过' : '拒绝'}${pass && tolerated ? `(含 ${tolerated} 条边界容差缝)` : ''},worst=${score === Infinity ? '∞' : score.toFixed(2)} ⟶ ${detail}`);
237
251
  if (pass && (!best || score < best.score))
238
252
  best = { layout: c, score, seams };
239
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sns-parse/core",
3
- "version": "0.4.0-alpha.8+upstream.1.6.7",
3
+ "version": "0.5.0-alpha.1+upstream.1.6.7",
4
4
  "description": "sns-parse 核心:契约、宿主抽象、配置 DSL 与解析引擎(fetcher/parser/翻译/X 原生解析/合并/GIF/TLS 指纹客户端)",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",