@sns-parse/core 0.4.0-alpha.4 → 0.4.0-alpha.6

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.
@@ -104,8 +104,8 @@ function engineConfigContributions() {
104
104
  fields: [
105
105
  {
106
106
  key: 'unifiedMessageFormat', type: 'string', role: 'textarea',
107
- default: '标题:${标题}\n作者:${作者}\n简介:${简介}\n翻译(${翻译提供方},${原文语言}):${翻译}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}',
108
- description: '消息文字模板,可用变量:${标题} ${作者} ${简介} ${翻译} ${翻译提供方} ${原文语言} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者}(空行自动隐藏)',
107
+ default: '标题:${标题}\n作者:${作者}\n简介:${简介}\n正文:${正文}\n翻译(${翻译提供方},${原文语言}):${翻译}\n音乐标题:${音乐标题}\n音乐作者:${音乐作者}\n点赞:${点赞数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n图片数量:${图片数量}',
108
+ description: '消息文字模板,可用变量:${标题} ${作者} ${简介} ${正文}(纯文字作品正文,启用后该类作品不再显示为简介) ${翻译} ${翻译提供方} ${原文语言} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${音乐标题} ${音乐作者}(空行自动隐藏)',
109
109
  },
110
110
  ],
111
111
  }),
@@ -138,14 +138,45 @@ function pick(...vals) {
138
138
  return v;
139
139
  return '';
140
140
  }
141
- /** 清理描述中的 t.co 短链(Twitter 自动附加的截断 URL,无实际内容价值) */
141
+ /** BFS 深取子树中第一个非空的指定键值(X GraphQL 用户结构随版本漂移:legacy ↔ core/avatar) */
142
+ function deepPick(node, key) {
143
+ const queue = [node];
144
+ while (queue.length) {
145
+ const cur = queue.shift();
146
+ if (!cur || typeof cur !== 'object')
147
+ continue;
148
+ const v = cur[key];
149
+ if (v !== undefined && v !== null && v !== '')
150
+ return v;
151
+ for (const k of Object.keys(cur))
152
+ queue.push(cur[k]);
153
+ }
154
+ return undefined;
155
+ }
156
+ /** BFS 找子树中最长的 text 字符串(笔记正文位置随版本漂移,取最长以避开截断副本) */
157
+ function deepFindLongestText(node) {
158
+ let best;
159
+ const queue = [node];
160
+ while (queue.length) {
161
+ const cur = queue.shift();
162
+ if (!cur || typeof cur !== 'object')
163
+ continue;
164
+ if (typeof cur.text === 'string' && cur.text && (!best || cur.text.length > best.length))
165
+ best = cur.text;
166
+ for (const k of Object.keys(cur))
167
+ queue.push(cur[k]);
168
+ }
169
+ return best;
170
+ }
171
+ /** 清理描述中的 t.co 短链(Twitter 自动附加的截断 URL,无实际内容价值)。
172
+ * 注意保留段落换行:仅折叠水平空白,≥3 连续换行压成空行一档。 */
142
173
  function cleanDesc(text) {
143
174
  if (!text)
144
175
  return text;
145
- // 移除末尾的 t.co URL(含可能的空格前导)
146
- let cleaned = text.replace(/\s*https?:\/\/t\.co\/[A-Za-z0-9]+(?:\?[^\s]*)?\s*$/g, '').trim();
147
- // 移除中间出现的 t.co URL(较少见,但推文内嵌的缩短链)
148
- cleaned = cleaned.replace(/\s*https?:\/\/t\.co\/[A-Za-z0-9]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
176
+ // 去掉全部 t.co URL(文末自动附加 + 文中内嵌)
177
+ let cleaned = text.replace(/https?:\/\/t\.co\/[A-Za-z0-9]+(?:\?[^\s]*)?/g, '');
178
+ // 仅折叠空格/制表(不动换行);去掉行首尾空格;连续空行压成一个空行
179
+ cleaned = cleaned.replace(/[^\S\n]+/g, ' ').replace(/ *\n */g, '\n').replace(/\n{3,}/g, '\n\n').trim();
149
180
  return cleaned || text;
150
181
  }
151
182
  function baseParsed() {
@@ -273,9 +304,10 @@ function mapGraphql(rawResult) {
273
304
  }
274
305
  const legacy = result.legacy || {};
275
306
  const user = result.core?.user_results?.result;
276
- const ulegacy = user?.legacy || {};
277
- // 长推(note tweet):legacy.full_text 是截断版,全文在 note_tweet.note_results.note.text
278
- const text = String(pick(result.note_tweet?.note_results?.note?.text, legacy.full_text, ''));
307
+ const ures = user || {};
308
+ // 长推(note tweet):legacy.full_text 是截断版,全文在 note_tweet 子树(位置随版本漂移,深取最长 text)
309
+ // 用户字段同理:legacy ↔ core/avatar/profile_bio 漂移,深取键值
310
+ const text = String(pick(deepFindLongestText(result.note_tweet), legacy.full_text, ''));
279
311
  const p = baseParsed();
280
312
  const media = Array.isArray(legacy.entities?.media) ? legacy.entities.media : [];
281
313
  const photos = [];
@@ -319,9 +351,9 @@ function mapGraphql(rawResult) {
319
351
  p.title = cleaned.slice(0, 100);
320
352
  p.desc = cleaned;
321
353
  p.lang = legacy.lang ? String(legacy.lang) : undefined;
322
- p.author = String(pick(ulegacy.name, ulegacy.screen_name, ''));
323
- p.uid = String(pick(ulegacy.screen_name, user?.rest_id, ''));
324
- p.avatar = String(pick(ulegacy.profile_image_url_https, ''));
354
+ p.author = String(pick(deepPick(ures, 'name'), ''));
355
+ p.uid = String(pick(deepPick(ures, 'screen_name'), user?.rest_id, ''));
356
+ p.avatar = String(pick(deepPick(ures, 'profile_image_url_https'), deepPick(ures, 'image_url'), ''));
325
357
  p.like = Number(pick(legacy.favorite_count, 0)) || 0;
326
358
  p.comment = Number(pick(legacy.reply_count, 0)) || 0;
327
359
  p.share = Number(pick(legacy.retweet_count, 0)) || 0;
@@ -332,8 +364,8 @@ function mapGraphql(rawResult) {
332
364
  if (!isNaN(t))
333
365
  p.publishTime = t;
334
366
  }
335
- p.author_followers = Number(pick(ulegacy.followers_count, 0)) || 0;
336
- p.author_signature = String(pick(ulegacy.description, ''));
367
+ p.author_followers = Number(pick(deepPick(ures, 'followers_count'), 0)) || 0;
368
+ p.author_signature = String(pick(deepPick(ures, 'description'), ''));
337
369
  if (p.title && p.desc && p.desc.startsWith(p.title))
338
370
  p.title = '';
339
371
  return p;
@@ -28,10 +28,15 @@ function formatPublishTime(ms) {
28
28
  }
29
29
  function generateFormattedText(p, format, index, total) {
30
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('${正文}');
31
35
  const vars = {
32
36
  '标题': p.title,
33
37
  '作者': p.author,
34
- '简介': p.desc,
38
+ '简介': wantsBody ? '' : p.desc,
39
+ '正文': plainTextWork ? p.desc : '',
35
40
  '视频时长': p.duration > 0 ? formatDuration(p.duration) : '',
36
41
  '点赞数': String(p.like),
37
42
  '收藏数': String(p.collect),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sns-parse/core",
3
- "version": "0.4.0-alpha.4+upstream.1.6.7",
3
+ "version": "0.4.0-alpha.6+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",