cortico-world-forward-reader 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-forward-reader",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "转发拆解 World:把 QQ 的「合并转发」一层层拆到底,嵌套在里面的转发也展开,输出带缩进的人话。只读,不发消息。",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -40,17 +40,25 @@ function senderName(sender) {
40
40
  return sender.card || sender.nickname || (sender.user_id ? String(sender.user_id) : '?');
41
41
  }
42
42
 
43
+ /** 今天的时间只给 HH:MM;跨天的带上日期,免得整份记录分不清哪天。 */
43
44
  function clockOf(time) {
44
45
  if (!time) return '';
45
46
  const d = new Date(Number(time) * 1000);
46
47
  if (Number.isNaN(d.getTime())) return '';
47
48
  const hh = String(d.getHours()).padStart(2, '0');
48
49
  const mm = String(d.getMinutes()).padStart(2, '0');
49
- return `${hh}:${mm}`;
50
+ const now = new Date();
51
+ const sameDay =
52
+ d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
53
+ if (sameDay) return `${hh}:${mm}`;
54
+ const mo = String(d.getMonth() + 1).padStart(2, '0');
55
+ const dd = String(d.getDate()).padStart(2, '0');
56
+ const year = d.getFullYear() === now.getFullYear() ? '' : `${d.getFullYear()}-`;
57
+ return `${year}${mo}-${dd} ${hh}:${mm}`;
50
58
  }
51
59
 
52
60
  /** 把一条消息的段合成一行,并挑出里面嵌套的转发段。 */
53
- function foldMessage(segments) {
61
+ function foldMessage(segments, opts = {}) {
54
62
  const parts = [];
55
63
  const nested = [];
56
64
  for (const seg of segments || []) {
@@ -59,7 +67,7 @@ function foldMessage(segments) {
59
67
  nested.push(seg);
60
68
  continue;
61
69
  }
62
- const piece = segmentText(seg);
70
+ const piece = segmentText(seg, opts);
63
71
  if (piece) parts.push(piece);
64
72
  }
65
73
  return { text: parts.join('').trim(), nested };
@@ -126,7 +134,8 @@ export class ForwardReaderWorld {
126
134
  const cfg = this.cfg;
127
135
  const rawRef = String(args.ref ?? '')
128
136
  .trim()
129
- .replace(/^#/, '');
137
+ .replace(/^#/, '')
138
+ .trim();
130
139
  if (!rawRef) return fail('要给消息编号(QQ 消息行首的 #后面那串数字)。');
131
140
  // QQ 的 message_id 有负数的(合并转发卡片就常见):负号是 id 的一部分,不能丢。
132
141
  if (!/^-?\d+$/.test(rawRef)) return fail(`"${rawRef}" 不像消息编号,应该是数字(前面可能带一个负号),比如 265277450 或 -1175570559。`);
@@ -134,39 +143,49 @@ export class ForwardReaderWorld {
134
143
  const maxDepth = clampInt(args.depth ?? cfg.maxDepth, 8, 1, 20);
135
144
  const limit = clampInt(args.maxChars ?? cfg.maxChars, MAX_OUTPUT_CHARS, 1000, 60000);
136
145
 
146
+ let data;
147
+ try {
148
+ data = await callOneBot(cfg, 'get_msg', { message_id: rawRef });
149
+ } catch (err) {
150
+ const why = String(err?.message || err);
151
+ const hint = /not found|不存在|找不到/i.test(why)
152
+ ? '取不到这条,常见原因:消息太老、SnowLuma 那边没缓存了;或者编号抄错——负号是编号的一部分,除了行首的「#」别的字符一个都不能少。'
153
+ : '';
154
+ return fail(`取消息 ${rawRef} 失败:${why}${hint ? '\n' + hint : ''}\n可以先跑 qq_forward_status 看看接口和 token 怎么样。`);
155
+ }
156
+
137
157
  const out = [];
138
158
  const state = { items: 0, levels: 0, truncated: false };
159
+ const segments = data?.message || [];
160
+ const whole = foldMessage(segments, { links: true });
161
+ const who = senderName(data?.sender);
162
+ const when = clockOf(data?.time);
163
+
164
+ if (!whole.nested.length) {
165
+ // 不是合并转发:直接把这条消息本身给人看。
166
+ out.push(`# 消息 ${rawRef}${who !== '?' ? ` · ${who}` : ''}${when ? ` · ${when}` : ''}`);
167
+ out.push('');
168
+ out.push(whole.text || '(没有文字内容)');
169
+ out.push('');
170
+ out.push('这条不是合并转发卡片,所以没有可拆的层。');
171
+ return cap(out.join('\n'), limit);
172
+ }
139
173
 
140
- try {
141
- const data = await callOneBot(cfg, 'get_msg', { message_id: rawRef });
142
- const segments = data?.message || [];
143
- const whole = foldMessage(segments);
144
-
145
- if (!whole.nested.length) {
146
- // 不是合并转发:直接把这条消息本身给人看。
147
- const who = senderName(data?.sender);
148
- const when = clockOf(data?.time);
149
- out.push(`# 消息 ${rawRef}${who !== '?' ? ` · ${who}` : ''}${when ? ` · ${when}` : ''}`);
150
- out.push('');
151
- out.push(whole.text || '(没有文字内容)');
152
- out.push('');
153
- out.push('这条不是合并转发卡片,所以没有可拆的层。');
154
- return cap(out.join('\n'), limit);
155
- }
174
+ out.push(`# 转发拆解 · 消息 ${rawRef}${who !== '?' ? ` · ${who}` : ''}${when ? ` · ${when}` : ''}`);
175
+ out.push('');
156
176
 
157
- const time = clockOf(data?.time);
158
- out.push(`# 转发拆解 · 消息 ${rawRef}${time ? ` · ${time}` : ''}`);
177
+ // 卡片本身带的文字也要留下,不然只列转发内容就把原话吞掉了。
178
+ if (whole.text) {
179
+ out.push(`(这张卡片上还写着)${whole.text}`);
159
180
  out.push('');
181
+ }
160
182
 
161
- for (const seg of whole.nested) {
162
- await this.walk(seg.data?.id, 0, out, state, maxDepth);
163
- }
164
-
165
- out.unshift(`共 ${state.levels} 层 / ${state.items} 条${state.truncated ? '(已达条数上限,后面没再往下翻)' : ''}`);
166
- return cap(out.join('\n'), limit);
167
- } catch (err) {
168
- return fail(`拆不开:${err.message}\n可以先跑 qq_forward_status 看看接口和 token 怎么样。`);
183
+ for (const seg of whole.nested) {
184
+ await this.walk(seg.data?.id, 0, out, state, maxDepth);
169
185
  }
186
+
187
+ out.unshift(`共 ${state.levels} 层 / ${state.items} 条${state.truncated ? '(已达条数上限,后面没再往下翻)' : ''}`);
188
+ return cap(out.join('\n'), limit);
170
189
  }
171
190
 
172
191
  async walk(forwardId, depth, out, state, maxDepth) {
@@ -193,7 +212,7 @@ export class ForwardReaderWorld {
193
212
  }
194
213
  const who = senderName(item?.sender);
195
214
  const when = clockOf(item?.time);
196
- const folded = foldMessage(item?.message);
215
+ const folded = foldMessage(item?.message, { links: true });
197
216
  const head = `${pad(depth)}[${who}${when ? ' ' + when : ''}]`;
198
217
  state.items += 1;
199
218
  out.push(`${head} ${folded.text || '(空)'}`);
package/src/onebot.js CHANGED
@@ -97,15 +97,23 @@ export async function callOneBot(cfg, action, params = {}) {
97
97
  return json.data ?? json;
98
98
  }
99
99
 
100
- /** 一条消息里的某个段 → 一行给人看的小字。 */
101
- export function segmentText(seg) {
100
+ /**
101
+ * 一条消息里的某个段 → 一行给人看的小字。
102
+ * opts.links 为真时,图片后面补上地址(SnowLuma 一般给 url,取不到就退回 file),
103
+ * 这样带图的那条记录也能拿链接去看图,而不是只剩一个「[图片]」。
104
+ */
105
+ export function segmentText(seg, opts = {}) {
102
106
  if (!seg || typeof seg !== 'object') return '';
103
107
  const d = seg.data || {};
104
108
  switch (seg.type) {
105
109
  case 'text':
106
110
  return d.text || '';
107
- case 'image':
108
- return `[图片${d.summary ? ':' + d.summary : ''}]`;
111
+ case 'image': {
112
+ const base = `[图片${d.summary ? ':' + d.summary : ''}]`;
113
+ if (!opts.links) return base;
114
+ const where = d.url || d.path || d.file || '';
115
+ return where ? `${base} ${where}` : base;
116
+ }
109
117
  case 'face':
110
118
  return `[表情${d.id ? ':' + d.id : ''}]`;
111
119
  case 'mface':