@sovovs/bycli 2.1.29 → 2.1.31

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.
@@ -26,6 +26,27 @@ const PAGE_SIZE = 20;
26
26
  const MAX_LIMIT = 200;
27
27
  const MAX_PAGES = 30;
28
28
 
29
+ // 掘金对**非 0 的时间窗**(search_type=1/2/3)会间歇性返回空首页:`data: []`
30
+ // 但 err_no 仍是 0 / err_msg "success",整个响应体除了 cursor 里的实例标识
31
+ // 之外和有数据时完全一致,只有 has_more 跟着变 false——像是部分后端分片对时间
32
+ // 窗查询答不出来。
33
+ //
34
+ // 单次请求的空概率实测 **76%**(query=rust/period=day,25 次里 19 次空),
35
+ // 且与请求间隔无关(0ms 9/12、400ms 10/12、1500ms 11/12——加延迟只会更差),
36
+ // 所以各次尝试相互独立、退避没有意义。改过 8 种参数组合(去 cursor / 去 limit /
37
+ // 去 spider / 补 version / 换 sort_type / 换 id_type / 最小参数集)空率都在
38
+ // 6-9/10,没有能让它确定化的参数。
39
+ //
40
+ // 首次成功所需次数实测分布(12 轮):中位数 3、p90 5、最大 7。取 12 次把
41
+ // 独立失败概率压到 0.76^12 ≈ 3%(4 次时是 33%,实测正好对应 4/6 的命中率)。
42
+ //
43
+ // 真实无结果(query="鿃鿄鿅鿆" / "zzzqqqxxx...")连打 12-14 次稳定是 0,所以
44
+ // 重试只会救回抖动,不会把空态变成假数据。注意掘金对**无匹配**的查询会退化成
45
+ // 返回无关的推荐文章,但那属于「有数据」分支,和这里的空首页是两回事。
46
+ //
47
+ // 只重试**首页且一行都没拿到**的情形:翻页途中的空页本来就是正常的终止信号。
48
+ const EMPTY_RETRY_ATTEMPTS = 12;
49
+
29
50
  const TYPES = {
30
51
  all: 0,
31
52
  article: 2,
@@ -308,9 +329,18 @@ cli({
308
329
 
309
330
  while (rows.length < limit && pages < MAX_PAGES) {
310
331
  const url = buildUrl({ query, idType, sortType, period, cursor });
311
- const payload = await fetchPage(url);
332
+ let payload = await fetchPage(url);
312
333
  pages += 1;
313
334
 
335
+ // 首页空 → 可能是上面说的分片抖动,有界重试;真实空态重试后仍是空。
336
+ // 不加退避:实测各次尝试独立,延迟只会拖慢且略微更差。
337
+ if (rows.length === 0 && payload.data.length === 0) {
338
+ for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS; attempt += 1) {
339
+ payload = await fetchPage(url);
340
+ if (payload.data.length > 0) break;
341
+ }
342
+ }
343
+
314
344
  for (const entry of payload.data) {
315
345
  const normalized = normalizeEntry(entry);
316
346
  if (!normalized) continue;
@@ -357,6 +387,7 @@ export const __test__ = {
357
387
  PAGE_SIZE,
358
388
  MAX_LIMIT,
359
389
  MAX_PAGES,
390
+ EMPTY_RETRY_ATTEMPTS,
360
391
  requireQuery,
361
392
  requireChoice,
362
393
  requireLimit,
@@ -10,6 +10,26 @@ function normalizeRows(data) {
10
10
  return [data];
11
11
  return [{ value: data }];
12
12
  }
13
+ /**
14
+ * Render one cell for the flat text formats (table / plain / md / csv).
15
+ *
16
+ * Adapters occasionally expose a structured column (e.g. juejin/search's
17
+ * per-kind `extra`), which `String(v)` would turn into "[object Object]".
18
+ * JSON is lossless and still one line, so it degrades gracefully.
19
+ */
20
+ function formatCell(v) {
21
+ if (v === null || v === undefined)
22
+ return '';
23
+ if (typeof v === 'object') {
24
+ try {
25
+ return JSON.stringify(v);
26
+ }
27
+ catch {
28
+ return String(v);
29
+ }
30
+ }
31
+ return String(v);
32
+ }
13
33
  function resolveColumns(rows, opts) {
14
34
  return opts.columns ?? Object.keys(rows[0] ?? {});
15
35
  }
@@ -63,8 +83,7 @@ function renderTable(data, opts) {
63
83
  });
64
84
  for (const row of rows) {
65
85
  table.push(columns.map(c => {
66
- const v = row[c];
67
- return v === null || v === undefined ? '' : String(v);
86
+ return formatCell(row[c]);
68
87
  }));
69
88
  }
70
89
  console.log();
@@ -101,7 +120,9 @@ function renderPlain(data, opts) {
101
120
  }
102
121
  }
103
122
  rows.forEach((row, index) => {
104
- const entries = Object.entries(row).filter(([, value]) => value !== undefined && value !== null && String(value) !== '');
123
+ const entries = Object.entries(row)
124
+ .map(([key, value]) => [key, formatCell(value)])
125
+ .filter(([, value]) => value !== '');
105
126
  entries.forEach(([key, value]) => {
106
127
  console.log(`${key}: ${value}`);
107
128
  });
@@ -127,7 +148,7 @@ function renderMarkdown(data, opts) {
127
148
  console.log('| ' + columns.join(' | ') + ' |');
128
149
  console.log('| ' + columns.map(() => '---').join(' | ') + ' |');
129
150
  for (const row of rows) {
130
- console.log('| ' + columns.map(c => String(row[c] ?? '')).join(' | ') + ' |');
151
+ console.log('| ' + columns.map(c => formatCell(row[c])).join(' | ') + ' |');
131
152
  }
132
153
  }
133
154
  function renderCsv(data, opts) {
@@ -138,7 +159,7 @@ function renderCsv(data, opts) {
138
159
  console.log(columns.join(','));
139
160
  for (const row of rows) {
140
161
  console.log(columns.map(c => {
141
- const v = String(row[c] ?? '');
162
+ const v = formatCell(row[c]);
142
163
  return v.includes(',') || v.includes('"') || v.includes('\n') || v.includes('\r')
143
164
  ? `"${v.replace(/"/g, '""')}"` : v;
144
165
  }).join(','));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.29",
3
+ "version": "2.1.31",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },