@jackwener/opencli 1.5.5 → 1.5.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.
Files changed (231) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1229 -67
  7. package/dist/clis/band/bands.d.ts +1 -0
  8. package/dist/clis/band/bands.js +72 -0
  9. package/dist/clis/band/mentions.d.ts +1 -0
  10. package/dist/clis/band/mentions.js +127 -0
  11. package/dist/clis/band/post.d.ts +1 -0
  12. package/dist/clis/band/post.js +175 -0
  13. package/dist/clis/band/posts.d.ts +1 -0
  14. package/dist/clis/band/posts.js +94 -0
  15. package/dist/clis/doubao/detail.d.ts +1 -0
  16. package/dist/clis/doubao/detail.js +33 -0
  17. package/dist/clis/doubao/detail.test.d.ts +1 -0
  18. package/dist/clis/doubao/detail.test.js +42 -0
  19. package/dist/clis/doubao/history.d.ts +1 -0
  20. package/dist/clis/doubao/history.js +28 -0
  21. package/dist/clis/doubao/history.test.d.ts +1 -0
  22. package/dist/clis/doubao/history.test.js +37 -0
  23. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  24. package/dist/clis/doubao/meeting-summary.js +39 -0
  25. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-transcript.js +36 -0
  27. package/dist/clis/doubao/utils.d.ts +27 -0
  28. package/dist/clis/doubao/utils.js +317 -0
  29. package/dist/clis/doubao/utils.test.d.ts +1 -0
  30. package/dist/clis/doubao/utils.test.js +24 -0
  31. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  32. package/dist/clis/douyin/_shared/public-api.js +29 -0
  33. package/dist/clis/douyin/user-videos.d.ts +5 -0
  34. package/dist/clis/douyin/user-videos.js +74 -0
  35. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  36. package/dist/clis/douyin/user-videos.test.js +108 -0
  37. package/dist/clis/ones/common.d.ts +32 -0
  38. package/dist/clis/ones/common.js +144 -0
  39. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  40. package/dist/clis/ones/enrich-tasks.js +37 -0
  41. package/dist/clis/ones/login.d.ts +1 -0
  42. package/dist/clis/ones/login.js +80 -0
  43. package/dist/clis/ones/logout.d.ts +1 -0
  44. package/dist/clis/ones/logout.js +17 -0
  45. package/dist/clis/ones/me.d.ts +1 -0
  46. package/dist/clis/ones/me.js +30 -0
  47. package/dist/clis/ones/my-tasks.d.ts +1 -0
  48. package/dist/clis/ones/my-tasks.js +120 -0
  49. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  50. package/dist/clis/ones/resolve-labels.js +64 -0
  51. package/dist/clis/ones/task-helpers.d.ts +29 -0
  52. package/dist/clis/ones/task-helpers.js +212 -0
  53. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  54. package/dist/clis/ones/task-helpers.test.js +12 -0
  55. package/dist/clis/ones/task.d.ts +1 -0
  56. package/dist/clis/ones/task.js +66 -0
  57. package/dist/clis/ones/tasks.d.ts +1 -0
  58. package/dist/clis/ones/tasks.js +79 -0
  59. package/dist/clis/ones/token-info.d.ts +1 -0
  60. package/dist/clis/ones/token-info.js +42 -0
  61. package/dist/clis/ones/worklog.d.ts +11 -0
  62. package/dist/clis/ones/worklog.js +267 -0
  63. package/dist/clis/ones/worklog.test.d.ts +1 -0
  64. package/dist/clis/ones/worklog.test.js +20 -0
  65. package/dist/clis/spotify/spotify.d.ts +1 -0
  66. package/dist/clis/spotify/spotify.js +316 -0
  67. package/dist/clis/spotify/utils.d.ts +21 -0
  68. package/dist/clis/spotify/utils.js +66 -0
  69. package/dist/clis/spotify/utils.test.d.ts +1 -0
  70. package/dist/clis/spotify/utils.test.js +67 -0
  71. package/dist/clis/tieba/commands.test.d.ts +4 -0
  72. package/dist/clis/tieba/commands.test.js +79 -0
  73. package/dist/clis/tieba/hot.d.ts +1 -0
  74. package/dist/clis/tieba/hot.js +48 -0
  75. package/dist/clis/tieba/posts.d.ts +1 -0
  76. package/dist/clis/tieba/posts.js +85 -0
  77. package/dist/clis/tieba/read.d.ts +1 -0
  78. package/dist/clis/tieba/read.js +140 -0
  79. package/dist/clis/tieba/search.d.ts +1 -0
  80. package/dist/clis/tieba/search.js +108 -0
  81. package/dist/clis/tieba/utils.d.ts +101 -0
  82. package/dist/clis/tieba/utils.js +240 -0
  83. package/dist/clis/tieba/utils.test.d.ts +1 -0
  84. package/dist/clis/tieba/utils.test.js +290 -0
  85. package/dist/clis/weread/book.js +100 -13
  86. package/dist/clis/weread/commands.test.js +221 -0
  87. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  88. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  89. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  90. package/dist/clis/weread/search-regression.test.js +407 -0
  91. package/dist/clis/weread/search.js +143 -7
  92. package/dist/clis/weread/shelf.js +13 -95
  93. package/dist/clis/weread/utils.d.ts +46 -0
  94. package/dist/clis/weread/utils.js +214 -7
  95. package/dist/clis/weread/utils.test.js +71 -1
  96. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  97. package/dist/clis/xiaohongshu/publish.js +78 -31
  98. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  99. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  100. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  101. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  102. package/dist/clis/xueqiu/comments.d.ts +118 -0
  103. package/dist/clis/xueqiu/comments.js +354 -0
  104. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  105. package/dist/clis/xueqiu/comments.test.js +696 -0
  106. package/dist/clis/youtube/transcript.js +2 -4
  107. package/dist/clis/youtube/utils.d.ts +9 -0
  108. package/dist/clis/youtube/utils.js +67 -3
  109. package/dist/clis/youtube/utils.test.d.ts +1 -0
  110. package/dist/clis/youtube/utils.test.js +37 -0
  111. package/dist/clis/youtube/video.js +16 -15
  112. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  113. package/dist/clis/zsxq/dynamics.js +47 -0
  114. package/dist/clis/zsxq/groups.d.ts +1 -0
  115. package/dist/clis/zsxq/groups.js +32 -0
  116. package/dist/clis/zsxq/search.d.ts +1 -0
  117. package/dist/clis/zsxq/search.js +43 -0
  118. package/dist/clis/zsxq/search.test.d.ts +1 -0
  119. package/dist/clis/zsxq/search.test.js +24 -0
  120. package/dist/clis/zsxq/topic.d.ts +1 -0
  121. package/dist/clis/zsxq/topic.js +47 -0
  122. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  123. package/dist/clis/zsxq/topic.test.js +29 -0
  124. package/dist/clis/zsxq/topics.d.ts +1 -0
  125. package/dist/clis/zsxq/topics.js +25 -0
  126. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  127. package/dist/clis/zsxq/topics.test.js +24 -0
  128. package/dist/clis/zsxq/utils.d.ts +97 -0
  129. package/dist/clis/zsxq/utils.js +230 -0
  130. package/dist/commanderAdapter.js +1 -1
  131. package/dist/commanderAdapter.test.js +39 -0
  132. package/dist/external-clis.yaml +17 -0
  133. package/dist/types.d.ts +5 -0
  134. package/docs/.vitepress/config.mts +3 -0
  135. package/docs/adapters/browser/band.md +63 -0
  136. package/docs/adapters/browser/ones.md +59 -0
  137. package/docs/adapters/browser/spotify.md +62 -0
  138. package/docs/adapters/browser/tieba.md +45 -0
  139. package/docs/adapters/browser/xueqiu.md +5 -0
  140. package/docs/adapters/browser/zsxq.md +49 -0
  141. package/docs/adapters/index.md +5 -2
  142. package/docs/adapters-doc/ones.md +32 -0
  143. package/extension/src/background.ts +15 -0
  144. package/extension/src/cdp.ts +42 -0
  145. package/extension/src/protocol.ts +5 -1
  146. package/package.json +1 -1
  147. package/scripts/postinstall.js +16 -0
  148. package/src/browser/daemon-client.ts +5 -1
  149. package/src/browser/page.ts +16 -0
  150. package/src/clis/band/bands.ts +76 -0
  151. package/src/clis/band/mentions.ts +134 -0
  152. package/src/clis/band/post.ts +187 -0
  153. package/src/clis/band/posts.ts +106 -0
  154. package/src/clis/doubao/detail.test.ts +53 -0
  155. package/src/clis/doubao/detail.ts +41 -0
  156. package/src/clis/doubao/history.test.ts +45 -0
  157. package/src/clis/doubao/history.ts +32 -0
  158. package/src/clis/doubao/meeting-summary.ts +53 -0
  159. package/src/clis/doubao/meeting-transcript.ts +48 -0
  160. package/src/clis/doubao/utils.test.ts +45 -0
  161. package/src/clis/doubao/utils.ts +371 -0
  162. package/src/clis/douyin/_shared/public-api.ts +84 -0
  163. package/src/clis/douyin/user-videos.test.ts +122 -0
  164. package/src/clis/douyin/user-videos.ts +101 -0
  165. package/src/clis/ones/common.ts +187 -0
  166. package/src/clis/ones/enrich-tasks.ts +47 -0
  167. package/src/clis/ones/login.ts +103 -0
  168. package/src/clis/ones/logout.ts +19 -0
  169. package/src/clis/ones/me.ts +34 -0
  170. package/src/clis/ones/my-tasks.ts +148 -0
  171. package/src/clis/ones/resolve-labels.ts +80 -0
  172. package/src/clis/ones/task-helpers.test.ts +14 -0
  173. package/src/clis/ones/task-helpers.ts +214 -0
  174. package/src/clis/ones/task.ts +79 -0
  175. package/src/clis/ones/tasks.ts +92 -0
  176. package/src/clis/ones/token-info.ts +46 -0
  177. package/src/clis/ones/worklog.test.ts +24 -0
  178. package/src/clis/ones/worklog.ts +306 -0
  179. package/src/clis/spotify/spotify.ts +328 -0
  180. package/src/clis/spotify/utils.test.ts +87 -0
  181. package/src/clis/spotify/utils.ts +92 -0
  182. package/src/clis/tieba/commands.test.ts +86 -0
  183. package/src/clis/tieba/hot.ts +52 -0
  184. package/src/clis/tieba/posts.ts +108 -0
  185. package/src/clis/tieba/read.ts +158 -0
  186. package/src/clis/tieba/search.ts +119 -0
  187. package/src/clis/tieba/utils.test.ts +322 -0
  188. package/src/clis/tieba/utils.ts +348 -0
  189. package/src/clis/weread/book.ts +116 -13
  190. package/src/clis/weread/commands.test.ts +249 -0
  191. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  192. package/src/clis/weread/search-regression.test.ts +440 -0
  193. package/src/clis/weread/search.ts +189 -9
  194. package/src/clis/weread/shelf.ts +20 -122
  195. package/src/clis/weread/utils.test.ts +81 -1
  196. package/src/clis/weread/utils.ts +264 -7
  197. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  198. package/src/clis/xiaohongshu/publish.ts +84 -30
  199. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  200. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  201. package/src/clis/xueqiu/comments.test.ts +823 -0
  202. package/src/clis/xueqiu/comments.ts +461 -0
  203. package/src/clis/youtube/transcript.ts +2 -4
  204. package/src/clis/youtube/utils.test.ts +43 -0
  205. package/src/clis/youtube/utils.ts +69 -0
  206. package/src/clis/youtube/video.ts +16 -15
  207. package/src/clis/zsxq/dynamics.ts +60 -0
  208. package/src/clis/zsxq/groups.ts +41 -0
  209. package/src/clis/zsxq/search.test.ts +29 -0
  210. package/src/clis/zsxq/search.ts +54 -0
  211. package/src/clis/zsxq/topic.test.ts +34 -0
  212. package/src/clis/zsxq/topic.ts +68 -0
  213. package/src/clis/zsxq/topics.test.ts +29 -0
  214. package/src/clis/zsxq/topics.ts +36 -0
  215. package/src/clis/zsxq/utils.ts +351 -0
  216. package/src/commanderAdapter.test.ts +47 -0
  217. package/src/commanderAdapter.ts +1 -1
  218. package/src/external-clis.yaml +17 -0
  219. package/src/types.ts +5 -0
  220. package/tests/e2e/band-auth.test.ts +20 -0
  221. package/tests/e2e/browser-auth-helpers.ts +18 -0
  222. package/tests/e2e/browser-auth.test.ts +35 -47
  223. package/tests/e2e/browser-public.test.ts +288 -0
  224. package/tests/e2e/management.test.ts +1 -1
  225. package/tests/e2e/plugin-management.test.ts +1 -1
  226. package/vitest.config.ts +1 -0
  227. package/SKILL.md +0 -879
  228. package/dist/weread-private-api-regression.test.d.ts +0 -1
  229. package/dist/weread-search-regression.test.d.ts +0 -1
  230. package/dist/weread-search-regression.test.js +0 -39
  231. package/src/weread-search-regression.test.ts +0 -44
@@ -0,0 +1,348 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * Shared Tieba parsing helpers used by the browser adapters.
5
+ */
6
+
7
+ export const MAX_TIEBA_LIMIT = 20;
8
+ const TIEBA_PC_SIGN_SALT = '36770b1f34c9bbf2e7d1a99d2b82fa9e';
9
+ const TIEBA_TIME_ZONE = 'Asia/Shanghai';
10
+
11
+ export interface RawTiebaPostCard {
12
+ title?: string;
13
+ author?: string;
14
+ descInfo?: string;
15
+ actionTexts?: string[];
16
+ commentCount?: unknown;
17
+ threadId?: unknown;
18
+ url?: unknown;
19
+ }
20
+
21
+ export interface RawTiebaPagePcFeedEntry {
22
+ layout?: string;
23
+ feed?: {
24
+ schema?: unknown;
25
+ log_param?: Array<{ key?: unknown; value?: unknown }>;
26
+ business_info_map?: Record<string, unknown>;
27
+ components?: Array<Record<string, unknown>>;
28
+ };
29
+ }
30
+
31
+ export interface TiebaPostItem {
32
+ rank: number;
33
+ title: string;
34
+ author: string;
35
+ replies: number;
36
+ last_reply: string;
37
+ id: string;
38
+ url: string;
39
+ }
40
+
41
+ export interface RawTiebaSearchItem {
42
+ title?: string;
43
+ forum?: string;
44
+ author?: string;
45
+ time?: string;
46
+ snippet?: string;
47
+ id?: string;
48
+ url?: string;
49
+ }
50
+
51
+ export interface TiebaSearchItem {
52
+ rank: number;
53
+ title: string;
54
+ forum: string;
55
+ author: string;
56
+ time: string;
57
+ snippet: string;
58
+ id: string;
59
+ url: string;
60
+ }
61
+
62
+ export interface RawTiebaMainPost {
63
+ title?: string;
64
+ author?: string;
65
+ fallbackAuthor?: string;
66
+ contentText?: string;
67
+ structuredText?: string;
68
+ visibleTime?: string;
69
+ structuredTime?: unknown;
70
+ hasMedia?: boolean;
71
+ }
72
+
73
+ export interface RawTiebaReply {
74
+ floor?: unknown;
75
+ author?: string;
76
+ content?: string;
77
+ time?: string;
78
+ }
79
+
80
+ export interface RawTiebaReadPayload {
81
+ mainPost?: RawTiebaMainPost | null;
82
+ replies?: RawTiebaReply[];
83
+ }
84
+
85
+ export interface TiebaReadItem {
86
+ floor: number;
87
+ author: string;
88
+ content: string;
89
+ time: string;
90
+ }
91
+
92
+ export interface TiebaReadBuildOptions {
93
+ limit?: unknown;
94
+ includeMainPost?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Keep the public CLI limit contract aligned with the real implementation.
99
+ */
100
+ export function normalizeTiebaLimit(value: unknown, fallback: number = MAX_TIEBA_LIMIT): number {
101
+ const parsed = Number(value ?? fallback);
102
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
103
+ return Math.min(Math.trunc(parsed), MAX_TIEBA_LIMIT);
104
+ }
105
+
106
+ export function normalizeText(value: unknown): string {
107
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
108
+ }
109
+
110
+ /**
111
+ * Match Tieba PC's signed request contract so forum list fetching stays stable.
112
+ */
113
+ export function signTiebaPcParams(params: Record<string, string>): string {
114
+ const payload = Object.keys(params)
115
+ .sort((left, right) => left.localeCompare(right))
116
+ .map((key) => `${key}=${params[key]}`)
117
+ .join('') + TIEBA_PC_SIGN_SALT;
118
+ return createHash('md5').update(payload).digest('hex');
119
+ }
120
+
121
+ export function parseTiebaCount(text: string): number {
122
+ const value = normalizeText(text).toUpperCase();
123
+ if (!value) return 0;
124
+ const compact = value.replace(/[^\d.W万]/g, '');
125
+ if (compact.endsWith('万')) {
126
+ return Math.round(parseFloat(compact.slice(0, -1)) * 10000);
127
+ }
128
+ if (compact.endsWith('W')) {
129
+ return Math.round(parseFloat(compact.slice(0, -1)) * 10000);
130
+ }
131
+ return parseInt(compact.replace(/[^\d]/g, ''), 10) || 0;
132
+ }
133
+
134
+ export function parseTiebaLastReply(text: string): string {
135
+ const normalized = normalizeText(text).replace(/^回复于/, '').trim();
136
+ const match = normalized.match(/(刚刚|\d+\s*(?:分钟|小时|天)前|\d{2}-\d{2}(?:\s+\d{2}:\d{2})?|\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)/);
137
+ return match ? match[1].trim() : normalized;
138
+ }
139
+
140
+ function buildTiebaThreadUrl(id: string, rawUrl?: unknown): string {
141
+ const explicitUrl = normalizeText(rawUrl);
142
+ if (explicitUrl) return explicitUrl;
143
+ return id ? `https://tieba.baidu.com/p/${id}` : '';
144
+ }
145
+
146
+ function resolveTiebaThreadId(raw: RawTiebaPostCard): string {
147
+ const direct = normalizeText(raw.threadId);
148
+ if (direct) return direct;
149
+
150
+ const fromUrl = normalizeText(raw.url).match(/\/p\/(\d+)/);
151
+ return fromUrl ? fromUrl[1] : '';
152
+ }
153
+
154
+ function getTiebaFeedComponent(feed: RawTiebaPagePcFeedEntry['feed'], name: string): Record<string, unknown> {
155
+ const components = Array.isArray(feed?.components) ? feed.components : [];
156
+ const match = components.find((entry) => normalizeText((entry as Record<string, unknown>).component) === name);
157
+ if (!match) return {};
158
+ const payload = (match as Record<string, unknown>)[name];
159
+ return payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
160
+ }
161
+
162
+ function extractTiebaFeedAuthor(feed: RawTiebaPagePcFeedEntry['feed']): string {
163
+ const head = getTiebaFeedComponent(feed, 'feed_head');
164
+ const mainData = Array.isArray(head.main_data) ? head.main_data : [];
165
+ for (const item of mainData) {
166
+ const textRecord = (item as Record<string, unknown>).text as Record<string, unknown> | undefined;
167
+ const author = normalizeText(textRecord?.text);
168
+ if (author) return author;
169
+ }
170
+ return '';
171
+ }
172
+
173
+ function extractTiebaFeedTitle(feed: RawTiebaPagePcFeedEntry['feed']): string {
174
+ const title = getTiebaFeedComponent(feed, 'feed_title');
175
+ const titleData = Array.isArray(title.data) ? title.data : [];
176
+ const firstTitle = titleData[0] as Record<string, unknown> | undefined;
177
+ const textInfo = firstTitle?.text_info as Record<string, unknown> | undefined;
178
+ return normalizeText(textInfo?.text) || normalizeText(feed?.business_info_map?.title);
179
+ }
180
+
181
+ function extractTiebaFeedCommentCount(feed: RawTiebaPagePcFeedEntry['feed']): number {
182
+ const social = getTiebaFeedComponent(feed, 'feed_social');
183
+ const commentCount = Number(social.comment_num ?? feed?.business_info_map?.comment_num ?? 0);
184
+ return Number.isFinite(commentCount) ? commentCount : 0;
185
+ }
186
+
187
+ function extractTiebaFeedThreadId(feed: RawTiebaPagePcFeedEntry['feed']): string {
188
+ const direct = normalizeText(feed?.business_info_map?.thread_id);
189
+ if (direct) return direct;
190
+
191
+ const logParams = Array.isArray(feed?.log_param) ? feed.log_param : [];
192
+ const fromLog = normalizeText(logParams.find((item) => normalizeText(item?.key) === 'tid')?.value);
193
+ if (fromLog) return fromLog;
194
+
195
+ const fromSchema = normalizeText(feed?.schema).match(/[?&]tid=(\d+)/);
196
+ return fromSchema ? fromSchema[1] : '';
197
+ }
198
+
199
+ function extractTiebaFeedLastReply(feed: RawTiebaPagePcFeedEntry['feed']): string {
200
+ const head = getTiebaFeedComponent(feed, 'feed_head');
201
+ const extraData = Array.isArray(head.extra_data) ? head.extra_data : [];
202
+ const first = extraData[0] as Record<string, unknown> | undefined;
203
+ const prefix = normalizeText((first?.business_info_map as Record<string, unknown> | undefined)?.time_prefix);
204
+ const textRecord = first?.text as Record<string, unknown> | undefined;
205
+ const rawTime = normalizeText(textRecord?.text);
206
+ const formattedTime = /^\d+$/.test(rawTime) ? formatTiebaUnixTime(rawTime) : rawTime;
207
+ return [prefix, formattedTime].filter(Boolean).join('');
208
+ }
209
+
210
+ /**
211
+ * Convert Tieba's signed `page_pc` feed entries into the stable card shape used by the CLI.
212
+ */
213
+ export function buildTiebaPostCardsFromPagePc(rawFeeds: RawTiebaPagePcFeedEntry[]): RawTiebaPostCard[] {
214
+ return rawFeeds
215
+ .filter((entry) => normalizeText(entry.layout) === 'feed' && entry.feed)
216
+ .map((entry) => {
217
+ const feed = entry.feed;
218
+ const threadId = extractTiebaFeedThreadId(feed);
219
+ return {
220
+ title: extractTiebaFeedTitle(feed),
221
+ author: extractTiebaFeedAuthor(feed),
222
+ descInfo: extractTiebaFeedLastReply(feed),
223
+ commentCount: extractTiebaFeedCommentCount(feed),
224
+ actionTexts: [],
225
+ threadId,
226
+ url: buildTiebaThreadUrl(threadId),
227
+ };
228
+ })
229
+ .filter((entry) => normalizeText(entry.title));
230
+ }
231
+
232
+ export function buildTiebaPostItems(rawCards: RawTiebaPostCard[], requestedLimit: unknown): TiebaPostItem[] {
233
+ const limit = normalizeTiebaLimit(requestedLimit);
234
+
235
+ return rawCards
236
+ .map((raw) => {
237
+ const title = normalizeText(raw.title);
238
+ const id = resolveTiebaThreadId(raw);
239
+ const actionTexts = Array.isArray(raw.actionTexts) ? raw.actionTexts.map(normalizeText).filter(Boolean) : [];
240
+ const commentText = actionTexts.find((text) => /评论/.test(text)) || actionTexts[actionTexts.length - 1] || '';
241
+
242
+ return {
243
+ title,
244
+ author: normalizeText(raw.author),
245
+ replies: Number.isFinite(Number(raw.commentCount))
246
+ ? Number(raw.commentCount)
247
+ : parseTiebaCount(commentText),
248
+ last_reply: parseTiebaLastReply(String(raw.descInfo ?? '')),
249
+ id,
250
+ url: buildTiebaThreadUrl(id, raw.url),
251
+ };
252
+ })
253
+ .filter((item) => item.title)
254
+ .slice(0, limit)
255
+ .map((item, index) => ({ rank: index + 1, ...item }));
256
+ }
257
+
258
+ export function buildTiebaSearchItems(rawItems: RawTiebaSearchItem[], requestedLimit: unknown): TiebaSearchItem[] {
259
+ const limit = normalizeTiebaLimit(requestedLimit);
260
+
261
+ return rawItems
262
+ .map((raw) => {
263
+ const url = normalizeText(raw.url);
264
+ const directId = normalizeText(raw.id);
265
+ const idFromUrl = url.match(/\/p\/(\d+)/)?.[1] || '';
266
+
267
+ return {
268
+ title: normalizeText(raw.title),
269
+ forum: normalizeText(raw.forum),
270
+ author: normalizeText(raw.author),
271
+ time: normalizeText(raw.time),
272
+ snippet: normalizeText(raw.snippet).slice(0, 200),
273
+ id: directId || idFromUrl,
274
+ url,
275
+ };
276
+ })
277
+ .filter((item) => item.title)
278
+ .slice(0, limit)
279
+ .map((item, index) => ({ rank: index + 1, ...item }));
280
+ }
281
+
282
+ function formatTiebaUnixTime(value: unknown): string {
283
+ const ts = Number(value || 0);
284
+ if (!Number.isFinite(ts) || ts <= 0) return '';
285
+ const parts = new Intl.DateTimeFormat('sv-SE', {
286
+ timeZone: TIEBA_TIME_ZONE,
287
+ year: 'numeric',
288
+ month: '2-digit',
289
+ day: '2-digit',
290
+ hour: '2-digit',
291
+ minute: '2-digit',
292
+ hour12: false,
293
+ }).formatToParts(new Date(ts * 1000));
294
+ const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
295
+ return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}`;
296
+ }
297
+
298
+ function parseTiebaReplyTime(text: string): string {
299
+ const normalized = normalizeText(text);
300
+ const withoutFloor = normalized.replace(/^第\d+楼\s+/, '').trim();
301
+ const match = withoutFloor.match(/^(刚刚|昨天|前天|\d+\s*(?:分钟|小时|天)前|\d{2}-\d{2}(?:\s+\d{2}:\d{2})?|\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)/);
302
+ return match ? match[1].trim() : withoutFloor;
303
+ }
304
+
305
+ function buildMainPostItem(mainPost?: RawTiebaMainPost | null): TiebaReadItem | null {
306
+ if (!mainPost) return null;
307
+
308
+ const title = normalizeText(mainPost.title);
309
+ const author = normalizeText(mainPost.author) || normalizeText(mainPost.fallbackAuthor);
310
+ const body = normalizeText(mainPost.contentText) || normalizeText(mainPost.structuredText);
311
+ const hasMedia = Boolean(mainPost.hasMedia);
312
+ const content = [title, body || (hasMedia ? '[media]' : '')].filter(Boolean).join(' ').trim();
313
+
314
+ if (!content) return null;
315
+
316
+ return {
317
+ floor: 1,
318
+ author,
319
+ content,
320
+ time: normalizeText(mainPost.visibleTime) || formatTiebaUnixTime(mainPost.structuredTime),
321
+ };
322
+ }
323
+
324
+ export function buildTiebaReadItems(payload: RawTiebaReadPayload, options: TiebaReadBuildOptions = {}): TiebaReadItem[] {
325
+ const fallback = Number.isFinite(Number(options.limit)) ? Number(options.limit) : 30;
326
+ const limit = Math.max(1, Math.trunc(fallback));
327
+ const includeMainPost = options.includeMainPost !== false;
328
+ const items: TiebaReadItem[] = [];
329
+ const mainPost = buildMainPostItem(payload.mainPost);
330
+
331
+ if (includeMainPost && mainPost) items.push(mainPost);
332
+
333
+ const replies = Array.isArray(payload.replies) ? payload.replies : [];
334
+ const replyItems: TiebaReadItem[] = [];
335
+ for (const reply of replies) {
336
+ const floor = Number(reply.floor || 0);
337
+ const content = normalizeText(reply.content);
338
+ if (!Number.isFinite(floor) || floor < 1 || !content) continue;
339
+ replyItems.push({
340
+ floor,
341
+ author: normalizeText(reply.author),
342
+ content,
343
+ time: parseTiebaReplyTime(String(reply.time ?? '')),
344
+ });
345
+ }
346
+
347
+ return items.concat(replyItems.slice(0, limit));
348
+ }
@@ -1,6 +1,82 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
2
3
  import type { IPage } from '../../types.js';
3
- import { fetchPrivateApi } from './utils.js';
4
+ import { fetchPrivateApi, resolveShelfReaderUrl } from './utils.js';
5
+
6
+ interface ReaderFallbackResult {
7
+ title: string;
8
+ author: string;
9
+ publisher: string;
10
+ intro: string;
11
+ category: string;
12
+ rating: string;
13
+ metadataReady: boolean;
14
+ }
15
+
16
+ /**
17
+ * Read visible book metadata from the web reader cover/flyleaf page.
18
+ * This path is used as a fallback when the private API session has expired.
19
+ */
20
+ async function loadReaderFallbackResult(page: IPage, readerUrl: string): Promise<ReaderFallbackResult> {
21
+ await page.goto(readerUrl);
22
+ await page.wait({ selector: '.horizontalReaderCoverPage_content_bookTitle, .wr_flyleaf_page_bookInfo_bookTitle', timeout: 10 });
23
+
24
+ const result = await page.evaluate(`
25
+ (() => {
26
+ const text = (node) => node?.textContent?.trim() || '';
27
+ const bodyText = document.body?.innerText?.replace(/\\s+/g, ' ').trim() || '';
28
+ const titleSelector = '.horizontalReaderCoverPage_content_bookTitle, .wr_flyleaf_page_bookInfo_bookTitle';
29
+ const authorSelector = '.horizontalReaderCoverPage_content_author, .wr_flyleaf_page_bookInfo_author';
30
+ const extractRating = () => {
31
+ const match = bodyText.match(/微信读书推荐值\\s*([0-9.]+%)/);
32
+ return match ? match[1] : '';
33
+ };
34
+ const extractPublisher = () => {
35
+ const direct = text(document.querySelector('.introDialog_content_pub_line'));
36
+ return direct.startsWith('出版社') ? direct.replace(/^出版社\\s*/, '').trim() : '';
37
+ };
38
+ const extractIntro = () => {
39
+ const selectors = [
40
+ '.horizontalReaderCoverPage_content_bookInfo_intro',
41
+ '.wr_flyleaf_page_bookIntro_content',
42
+ '.introDialog_content_intro_para',
43
+ ];
44
+ for (const selector of selectors) {
45
+ const value = text(document.querySelector(selector));
46
+ if (value) return value;
47
+ }
48
+ return '';
49
+ };
50
+
51
+ const categorySource = Array.from(document.scripts)
52
+ .map((script) => script.textContent || '')
53
+ .find((scriptText) => scriptText.includes('"category"')) || '';
54
+ const categoryMatch = categorySource.match(/"category"\\s*:\\s*"([^"]+)"/);
55
+ const title = text(document.querySelector(titleSelector));
56
+ const author = text(document.querySelector(authorSelector));
57
+
58
+ return {
59
+ title,
60
+ author,
61
+ publisher: extractPublisher(),
62
+ intro: extractIntro(),
63
+ category: categoryMatch ? categoryMatch[1].trim() : '',
64
+ rating: extractRating(),
65
+ metadataReady: Boolean(title || author),
66
+ };
67
+ })()
68
+ `) as Partial<ReaderFallbackResult>;
69
+
70
+ return {
71
+ title: String(result?.title || '').trim(),
72
+ author: String(result?.author || '').trim(),
73
+ publisher: String(result?.publisher || '').trim(),
74
+ intro: String(result?.intro || '').trim(),
75
+ category: String(result?.category || '').trim(),
76
+ rating: String(result?.rating || '').trim(),
77
+ metadataReady: result?.metadataReady === true,
78
+ };
79
+ }
4
80
 
5
81
  cli({
6
82
  site: 'weread',
@@ -9,20 +85,47 @@ cli({
9
85
  domain: 'weread.qq.com',
10
86
  strategy: Strategy.COOKIE,
11
87
  args: [
12
- { name: 'book-id', positional: true, required: true, help: 'Book ID (numeric, from search or shelf results)' },
88
+ { name: 'book-id', positional: true, required: true, help: 'Book ID from search or shelf results' },
13
89
  ],
14
90
  columns: ['title', 'author', 'publisher', 'intro', 'category', 'rating'],
15
91
  func: async (page: IPage, args) => {
16
- const data = await fetchPrivateApi(page, '/book/info', { bookId: args['book-id'] });
17
- // newRating is 0-1000 scale per community docs; needs runtime verification
18
- const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
19
- return [{
20
- title: data.title ?? '',
21
- author: data.author ?? '',
22
- publisher: data.publisher ?? '',
23
- intro: data.intro ?? '',
24
- category: data.category ?? '',
25
- rating,
26
- }];
92
+ const bookId = String(args['book-id'] || '').trim();
93
+
94
+ try {
95
+ const data = await fetchPrivateApi(page, '/book/info', { bookId });
96
+ // newRating is 0-1000 scale per community docs; needs runtime verification
97
+ const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
98
+ return [{
99
+ title: data.title ?? '',
100
+ author: data.author ?? '',
101
+ publisher: data.publisher ?? '',
102
+ intro: data.intro ?? '',
103
+ category: data.category ?? '',
104
+ rating,
105
+ }];
106
+ } catch (error) {
107
+ if (!(error instanceof CliError) || error.code !== 'AUTH_REQUIRED') {
108
+ throw error;
109
+ }
110
+
111
+ const readerUrl = await resolveShelfReaderUrl(page, bookId);
112
+ if (!readerUrl) {
113
+ throw error;
114
+ }
115
+
116
+ const data = await loadReaderFallbackResult(page, readerUrl);
117
+ if (!data.metadataReady || !data.title) {
118
+ throw error;
119
+ }
120
+
121
+ return [{
122
+ title: data.title,
123
+ author: data.author,
124
+ publisher: data.publisher,
125
+ intro: data.intro,
126
+ category: data.category,
127
+ rating: data.rating,
128
+ }];
129
+ }
27
130
  },
28
131
  });