v2er-insight 1.0.0

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 (205) hide show
  1. package/README.md +215 -0
  2. package/dist/cli/commands/ai.d.ts +13 -0
  3. package/dist/cli/commands/ai.js +153 -0
  4. package/dist/cli/commands/analyze.d.ts +13 -0
  5. package/dist/cli/commands/analyze.js +80 -0
  6. package/dist/cli/commands/config.d.ts +43 -0
  7. package/dist/cli/commands/config.js +267 -0
  8. package/dist/cli/commands/fetch.d.ts +13 -0
  9. package/dist/cli/commands/fetch.js +150 -0
  10. package/dist/cli/commands/index.d.ts +10 -0
  11. package/dist/cli/commands/index.js +22 -0
  12. package/dist/cli/commands/run.d.ts +23 -0
  13. package/dist/cli/commands/run.js +52 -0
  14. package/dist/cli/commands/show.d.ts +13 -0
  15. package/dist/cli/commands/show.js +154 -0
  16. package/dist/cli/index.d.ts +6 -0
  17. package/dist/cli/index.js +107 -0
  18. package/dist/cli/types.d.ts +58 -0
  19. package/dist/cli/types.js +6 -0
  20. package/dist/cli/utils/error.d.ts +6 -0
  21. package/dist/cli/utils/error.js +18 -0
  22. package/dist/cli/utils.d.ts +20 -0
  23. package/dist/cli/utils.js +48 -0
  24. package/dist/cli/workflow/orchestrator.d.ts +15 -0
  25. package/dist/cli/workflow/orchestrator.js +144 -0
  26. package/dist/cli/workflow/recovery.d.ts +10 -0
  27. package/dist/cli/workflow/recovery.js +134 -0
  28. package/dist/cli/workflow/state.d.ts +19 -0
  29. package/dist/cli/workflow/state.js +45 -0
  30. package/dist/cli/workflow/types.d.ts +60 -0
  31. package/dist/cli/workflow/types.js +3 -0
  32. package/dist/config/defaults.d.ts +48 -0
  33. package/dist/config/defaults.js +42 -0
  34. package/dist/config/index.d.ts +16 -0
  35. package/dist/config/index.js +21 -0
  36. package/dist/config/path.d.ts +11 -0
  37. package/dist/config/path.js +28 -0
  38. package/dist/config/proxy.d.ts +16 -0
  39. package/dist/config/proxy.js +39 -0
  40. package/dist/config/storage.d.ts +23 -0
  41. package/dist/config/storage.js +85 -0
  42. package/dist/config/types/ai.d.ts +31 -0
  43. package/dist/config/types/ai.js +13 -0
  44. package/dist/config/types/analyzer.d.ts +15 -0
  45. package/dist/config/types/analyzer.js +6 -0
  46. package/dist/config/types/data.d.ts +20 -0
  47. package/dist/config/types/data.js +6 -0
  48. package/dist/config/types/fetch.d.ts +9 -0
  49. package/dist/config/types/fetch.js +6 -0
  50. package/dist/config/types/index.d.ts +32 -0
  51. package/dist/config/types/index.js +11 -0
  52. package/dist/config/types/log.d.ts +11 -0
  53. package/dist/config/types/log.js +6 -0
  54. package/dist/core/ai/index.d.ts +11 -0
  55. package/dist/core/ai/index.js +18 -0
  56. package/dist/core/ai/parser/index.d.ts +12 -0
  57. package/dist/core/ai/parser/index.js +44 -0
  58. package/dist/core/ai/parser/validator.d.ts +18 -0
  59. package/dist/core/ai/parser/validator.js +179 -0
  60. package/dist/core/ai/prompt/index.d.ts +20 -0
  61. package/dist/core/ai/prompt/index.js +75 -0
  62. package/dist/core/ai/prompt/system-prompt.md +210 -0
  63. package/dist/core/ai/providers/gemini.d.ts +25 -0
  64. package/dist/core/ai/providers/gemini.js +74 -0
  65. package/dist/core/ai/providers/index.d.ts +6 -0
  66. package/dist/core/ai/providers/index.js +9 -0
  67. package/dist/core/ai/types/index.d.ts +7 -0
  68. package/dist/core/ai/types/index.js +6 -0
  69. package/dist/core/ai/types/options.d.ts +14 -0
  70. package/dist/core/ai/types/options.js +6 -0
  71. package/dist/core/ai/types/provider.d.ts +19 -0
  72. package/dist/core/ai/types/provider.js +6 -0
  73. package/dist/core/ai/types/result.d.ts +64 -0
  74. package/dist/core/ai/types/result.js +6 -0
  75. package/dist/core/ai/utils/api-key.d.ts +15 -0
  76. package/dist/core/ai/utils/api-key.js +36 -0
  77. package/dist/core/ai/utils/index.d.ts +6 -0
  78. package/dist/core/ai/utils/index.js +11 -0
  79. package/dist/core/ai/utils/retry.d.ts +15 -0
  80. package/dist/core/ai/utils/retry.js +37 -0
  81. package/dist/core/analyzer/builder.d.ts +23 -0
  82. package/dist/core/analyzer/builder.js +113 -0
  83. package/dist/core/analyzer/content/chunker.d.ts +18 -0
  84. package/dist/core/analyzer/content/chunker.js +74 -0
  85. package/dist/core/analyzer/content/index.d.ts +7 -0
  86. package/dist/core/analyzer/content/index.js +13 -0
  87. package/dist/core/analyzer/content/transformer.d.ts +19 -0
  88. package/dist/core/analyzer/content/transformer.js +33 -0
  89. package/dist/core/analyzer/index.d.ts +17 -0
  90. package/dist/core/analyzer/index.js +21 -0
  91. package/dist/core/analyzer/periods/detector.d.ts +17 -0
  92. package/dist/core/analyzer/periods/detector.js +36 -0
  93. package/dist/core/analyzer/periods/index.d.ts +6 -0
  94. package/dist/core/analyzer/periods/index.js +11 -0
  95. package/dist/core/analyzer/periods/splitter.d.ts +11 -0
  96. package/dist/core/analyzer/periods/splitter.js +35 -0
  97. package/dist/core/analyzer/stats/index.d.ts +7 -0
  98. package/dist/core/analyzer/stats/index.js +13 -0
  99. package/dist/core/analyzer/stats/reply-stats.d.ts +15 -0
  100. package/dist/core/analyzer/stats/reply-stats.js +45 -0
  101. package/dist/core/analyzer/stats/topic-stats.d.ts +16 -0
  102. package/dist/core/analyzer/stats/topic-stats.js +51 -0
  103. package/dist/core/analyzer/stats/user-overview.d.ts +9 -0
  104. package/dist/core/analyzer/stats/user-overview.js +52 -0
  105. package/dist/core/analyzer/types/index.d.ts +7 -0
  106. package/dist/core/analyzer/types/index.js +6 -0
  107. package/dist/core/analyzer/types/input.d.ts +13 -0
  108. package/dist/core/analyzer/types/input.js +6 -0
  109. package/dist/core/analyzer/types/internal.d.ts +28 -0
  110. package/dist/core/analyzer/types/internal.js +6 -0
  111. package/dist/core/analyzer/types/output.d.ts +68 -0
  112. package/dist/core/analyzer/types/output.js +6 -0
  113. package/dist/core/analyzer/utils/date-parser.d.ts +41 -0
  114. package/dist/core/analyzer/utils/date-parser.js +118 -0
  115. package/dist/core/analyzer/utils/index.d.ts +6 -0
  116. package/dist/core/analyzer/utils/index.js +18 -0
  117. package/dist/core/analyzer/utils/stats.d.ts +12 -0
  118. package/dist/core/analyzer/utils/stats.js +64 -0
  119. package/dist/core/v2ex/index.d.ts +10 -0
  120. package/dist/core/v2ex/index.js +27 -0
  121. package/dist/core/v2ex/parsers/index.d.ts +8 -0
  122. package/dist/core/v2ex/parsers/index.js +15 -0
  123. package/dist/core/v2ex/parsers/replies-page.d.ts +11 -0
  124. package/dist/core/v2ex/parsers/replies-page.js +114 -0
  125. package/dist/core/v2ex/parsers/selectors/index.d.ts +10 -0
  126. package/dist/core/v2ex/parsers/selectors/index.js +18 -0
  127. package/dist/core/v2ex/parsers/selectors/pagination.d.ts +11 -0
  128. package/dist/core/v2ex/parsers/selectors/pagination.js +14 -0
  129. package/dist/core/v2ex/parsers/selectors/replies-page.d.ts +21 -0
  130. package/dist/core/v2ex/parsers/selectors/replies-page.js +24 -0
  131. package/dist/core/v2ex/parsers/selectors/topic-detail.d.ts +19 -0
  132. package/dist/core/v2ex/parsers/selectors/topic-detail.js +22 -0
  133. package/dist/core/v2ex/parsers/selectors/topics-list-page.d.ts +11 -0
  134. package/dist/core/v2ex/parsers/selectors/topics-list-page.js +14 -0
  135. package/dist/core/v2ex/parsers/selectors/user-profile.d.ts +11 -0
  136. package/dist/core/v2ex/parsers/selectors/user-profile.js +14 -0
  137. package/dist/core/v2ex/parsers/topic-detail.d.ts +11 -0
  138. package/dist/core/v2ex/parsers/topic-detail.js +94 -0
  139. package/dist/core/v2ex/parsers/topics-list-page.d.ts +11 -0
  140. package/dist/core/v2ex/parsers/topics-list-page.js +90 -0
  141. package/dist/core/v2ex/parsers/user-profile.d.ts +11 -0
  142. package/dist/core/v2ex/parsers/user-profile.js +70 -0
  143. package/dist/core/v2ex/parsers/utils/index.d.ts +6 -0
  144. package/dist/core/v2ex/parsers/utils/index.js +9 -0
  145. package/dist/core/v2ex/parsers/utils/pagination.d.ts +19 -0
  146. package/dist/core/v2ex/parsers/utils/pagination.js +29 -0
  147. package/dist/core/v2ex/types/entities.d.ts +45 -0
  148. package/dist/core/v2ex/types/entities.js +7 -0
  149. package/dist/core/v2ex/types/index.d.ts +6 -0
  150. package/dist/core/v2ex/types/index.js +6 -0
  151. package/dist/core/v2ex/types/parse-result.d.ts +64 -0
  152. package/dist/core/v2ex/types/parse-result.js +7 -0
  153. package/dist/core/v2ex/urls/constants.d.ts +5 -0
  154. package/dist/core/v2ex/urls/constants.js +8 -0
  155. package/dist/core/v2ex/urls/index.d.ts +7 -0
  156. package/dist/core/v2ex/urls/index.js +16 -0
  157. package/dist/core/v2ex/urls/topic-urls.d.ts +19 -0
  158. package/dist/core/v2ex/urls/topic-urls.js +48 -0
  159. package/dist/core/v2ex/urls/user-urls.d.ts +24 -0
  160. package/dist/core/v2ex/urls/user-urls.js +36 -0
  161. package/dist/core/v2ex/use-cases/index.d.ts +8 -0
  162. package/dist/core/v2ex/use-cases/index.js +14 -0
  163. package/dist/core/v2ex/use-cases/types.d.ts +31 -0
  164. package/dist/core/v2ex/use-cases/types.js +7 -0
  165. package/dist/core/v2ex/use-cases/user/index.d.ts +10 -0
  166. package/dist/core/v2ex/use-cases/user/index.js +16 -0
  167. package/dist/core/v2ex/use-cases/user/profile.d.ts +14 -0
  168. package/dist/core/v2ex/use-cases/user/profile.js +51 -0
  169. package/dist/core/v2ex/use-cases/user/replies.d.ts +14 -0
  170. package/dist/core/v2ex/use-cases/user/replies.js +20 -0
  171. package/dist/core/v2ex/use-cases/user/topic-urls.d.ts +21 -0
  172. package/dist/core/v2ex/use-cases/user/topic-urls.js +29 -0
  173. package/dist/core/v2ex/use-cases/user/topics-detail.d.ts +30 -0
  174. package/dist/core/v2ex/use-cases/user/topics-detail.js +62 -0
  175. package/dist/core/v2ex/use-cases/utils/index.d.ts +6 -0
  176. package/dist/core/v2ex/use-cases/utils/index.js +9 -0
  177. package/dist/core/v2ex/use-cases/utils/page-orchestrator.d.ts +24 -0
  178. package/dist/core/v2ex/use-cases/utils/page-orchestrator.js +93 -0
  179. package/dist/infra/fetcher/agent.d.ts +10 -0
  180. package/dist/infra/fetcher/agent.js +17 -0
  181. package/dist/infra/fetcher/fetcher.d.ts +10 -0
  182. package/dist/infra/fetcher/fetcher.js +81 -0
  183. package/dist/infra/fetcher/index.d.ts +3 -0
  184. package/dist/infra/fetcher/index.js +19 -0
  185. package/dist/infra/fetcher/types.d.ts +29 -0
  186. package/dist/infra/fetcher/types.js +6 -0
  187. package/dist/infra/logger/colors.d.ts +15 -0
  188. package/dist/infra/logger/colors.js +18 -0
  189. package/dist/infra/logger/index.d.ts +16 -0
  190. package/dist/infra/logger/index.js +19 -0
  191. package/dist/infra/logger/logger.d.ts +34 -0
  192. package/dist/infra/logger/logger.js +101 -0
  193. package/dist/infra/storage/cleaner.d.ts +24 -0
  194. package/dist/infra/storage/cleaner.js +73 -0
  195. package/dist/infra/storage/index.d.ts +7 -0
  196. package/dist/infra/storage/index.js +15 -0
  197. package/dist/infra/storage/paths.d.ts +26 -0
  198. package/dist/infra/storage/paths.js +53 -0
  199. package/dist/infra/storage/reader.d.ts +15 -0
  200. package/dist/infra/storage/reader.js +34 -0
  201. package/dist/infra/storage/types.d.ts +21 -0
  202. package/dist/infra/storage/types.js +18 -0
  203. package/dist/infra/storage/writer.d.ts +16 -0
  204. package/dist/infra/storage/writer.js +31 -0
  205. package/package.json +89 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Analyzer 类型入口
3
+ */
4
+ export type { RawUserData } from './input';
5
+ export type { ActivePeriod, TopicWithDate, ReplyWithDate, PeriodBoundary } from './internal';
6
+ export type { UserOverview, SinglePeriodStats, PeriodsSummary, ContentTopic, ContentReply, PeriodContent, PeriodContentChunk, AnalyzerOutput, } from './output';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Analyzer 类型入口
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Analyzer 输入类型
3
+ */
4
+ import type { V2exReply, V2exTopicDetail } from '../../../core/v2ex/types/entities';
5
+ import type { UserProfileParseResult } from '../../../core/v2ex/types/parse-result';
6
+ /** V2EX 抓取的原始用户数据 */
7
+ export interface RawUserData {
8
+ profile: UserProfileParseResult;
9
+ topics: V2exTopicDetail[];
10
+ replies: V2exReply[];
11
+ isTopicsHidden: boolean;
12
+ }
13
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Analyzer 输入类型
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Analyzer 内部类型
3
+ */
4
+ import type { V2exReply, V2exTopicDetail } from '../../../core/v2ex/types/entities';
5
+ /** 活跃期:两个暂停期之间的连续活动时间段 */
6
+ export interface ActivePeriod {
7
+ index: number;
8
+ startDate: Date;
9
+ endDate: Date;
10
+ topics: V2exTopicDetail[];
11
+ replies: V2exReply[];
12
+ }
13
+ /** 带解析日期的帖子 */
14
+ export interface TopicWithDate {
15
+ topic: V2exTopicDetail;
16
+ createdDate: Date;
17
+ }
18
+ /** 带解析日期的回复 */
19
+ export interface ReplyWithDate {
20
+ reply: V2exReply;
21
+ replyDate: Date;
22
+ }
23
+ /** 活跃期边界 */
24
+ export interface PeriodBoundary {
25
+ startDate: Date;
26
+ endDate: Date;
27
+ }
28
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Analyzer 内部类型
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Analyzer 输出类型
3
+ */
4
+ /** 用户总览 */
5
+ export interface UserOverview {
6
+ joinDate: string;
7
+ lastActiveTime: string;
8
+ topicReplyRatio: number | null;
9
+ totalTopics: number | null;
10
+ totalReplies: number;
11
+ isTopicsHidden: boolean;
12
+ dailyRanking: number | null;
13
+ }
14
+ /** 单个活跃期统计 */
15
+ export interface SinglePeriodStats {
16
+ timeRange: string;
17
+ topicCount: number;
18
+ avgTopicReplyCount: number;
19
+ avgTopicClickCount: number;
20
+ avgTopicLifecycleDays: number;
21
+ topicInteractionRatio: number;
22
+ topicHourDistribution: Record<number, number>;
23
+ topicNodeDistribution: Record<string, number>;
24
+ replyCount: number;
25
+ avgReplyLength: number;
26
+ directReplyRatio: number;
27
+ avgRepliedTopicHeat: number;
28
+ replyWeekdayDistribution: Record<string, number> | null;
29
+ replyNodeDistribution: Record<string, number>;
30
+ }
31
+ /** 活跃期统计汇总 */
32
+ export interface PeriodsSummary {
33
+ totalPeriods: number;
34
+ periods: SinglePeriodStats[];
35
+ }
36
+ /** 发送给 AI 的帖子 */
37
+ export interface ContentTopic {
38
+ title: string;
39
+ nodeName: string;
40
+ content: string;
41
+ }
42
+ /** 发送给 AI 的回复 */
43
+ export interface ContentReply {
44
+ topicTitle: string;
45
+ nodeName: string;
46
+ content: string;
47
+ }
48
+ /** 活跃期内容(完整版) */
49
+ export interface PeriodContent {
50
+ periodIndex: number;
51
+ topics: ContentTopic[];
52
+ replies: ContentReply[];
53
+ }
54
+ /** 活跃期内容分片 */
55
+ export interface PeriodContentChunk {
56
+ periodIndex: number;
57
+ chunkIndex: number;
58
+ totalChunksInPeriod: number;
59
+ topics: ContentTopic[];
60
+ replies: ContentReply[];
61
+ }
62
+ /** Analyzer 最终输出 */
63
+ export interface AnalyzerOutput {
64
+ userOverview: UserOverview;
65
+ summary: PeriodsSummary;
66
+ contents: Array<PeriodContent | PeriodContentChunk>;
67
+ }
68
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Analyzer 输出类型
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 日期解析工具
3
+ */
4
+ /** 解析结果 */
5
+ export interface ParsedDate {
6
+ date: Date;
7
+ hasTime: boolean;
8
+ }
9
+ /**
10
+ * 解析绝对时间
11
+ * 格式: "2024-01-16 10:00:00 +08:00"
12
+ */
13
+ export declare function parseAbsoluteDate(dateStr: string): ParsedDate | null;
14
+ /**
15
+ * 解析相对时间
16
+ * 格式: "3 分钟前", "2 小时前", "5 天前"
17
+ * @param relativeStr 相对时间字符串
18
+ * @param referenceDate 参考时间,默认为当前时间
19
+ */
20
+ export declare function parseRelativeTime(relativeStr: string, referenceDate?: Date): ParsedDate | null;
21
+ /**
22
+ * 解析中文日期格式
23
+ * 格式: "2025 年 4 月 21 日", "4 月 21 日", "1 月 7 日"
24
+ * @param dateStr 日期字符串
25
+ * @param referenceDate 参考时间,用于补充年份
26
+ */
27
+ export declare function parseChineseDate(dateStr: string, referenceDate?: Date): ParsedDate | null;
28
+ /**
29
+ * 格式化时间范围
30
+ * 输出: "2015-04-01 to 2017-08-15"
31
+ */
32
+ export declare function formatTimeRange(start: Date, end: Date): string;
33
+ /**
34
+ * 获取星期几
35
+ */
36
+ export declare function getWeekday(date: Date): string;
37
+ /**
38
+ * 获取小时 (0-23)
39
+ */
40
+ export declare function getHour(date: Date): number;
41
+ //# sourceMappingURL=date-parser.d.ts.map
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * 日期解析工具
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseAbsoluteDate = parseAbsoluteDate;
7
+ exports.parseRelativeTime = parseRelativeTime;
8
+ exports.parseChineseDate = parseChineseDate;
9
+ exports.formatTimeRange = formatTimeRange;
10
+ exports.getWeekday = getWeekday;
11
+ exports.getHour = getHour;
12
+ /**
13
+ * 解析绝对时间
14
+ * 格式: "2024-01-16 10:00:00 +08:00"
15
+ */
16
+ function parseAbsoluteDate(dateStr) {
17
+ const date = new Date(dateStr);
18
+ if (isNaN(date.getTime())) {
19
+ return null;
20
+ }
21
+ return { date, hasTime: true };
22
+ }
23
+ /**
24
+ * 解析相对时间
25
+ * 格式: "3 分钟前", "2 小时前", "5 天前"
26
+ * @param relativeStr 相对时间字符串
27
+ * @param referenceDate 参考时间,默认为当前时间
28
+ */
29
+ function parseRelativeTime(relativeStr, referenceDate = new Date()) {
30
+ const patterns = [
31
+ { regex: /(\d+)\s*分钟前/, unit: 'minute', hasTime: true },
32
+ { regex: /(\d+)\s*小时前/, unit: 'hour', hasTime: true },
33
+ { regex: /(\d+)\s*天前/, unit: 'day', hasTime: false },
34
+ ];
35
+ for (const { regex, unit, hasTime } of patterns) {
36
+ const match = relativeStr.match(regex);
37
+ if (match?.[1]) {
38
+ const value = parseInt(match[1], 10);
39
+ const date = new Date(referenceDate);
40
+ switch (unit) {
41
+ case 'minute':
42
+ date.setMinutes(date.getMinutes() - value);
43
+ break;
44
+ case 'hour':
45
+ date.setHours(date.getHours() - value);
46
+ break;
47
+ case 'day':
48
+ date.setDate(date.getDate() - value);
49
+ break;
50
+ }
51
+ return { date, hasTime };
52
+ }
53
+ }
54
+ // 尝试解析中文日期格式
55
+ const chineseDateResult = parseChineseDate(relativeStr, referenceDate);
56
+ if (chineseDateResult) {
57
+ return chineseDateResult;
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * 解析中文日期格式
63
+ * 格式: "2025 年 4 月 21 日", "4 月 21 日", "1 月 7 日"
64
+ * @param dateStr 日期字符串
65
+ * @param referenceDate 参考时间,用于补充年份
66
+ */
67
+ function parseChineseDate(dateStr, referenceDate = new Date()) {
68
+ // "2025 年 4 月 21 日" 格式
69
+ const fullMatch = dateStr.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/);
70
+ if (fullMatch) {
71
+ const year = parseInt(fullMatch[1], 10);
72
+ const month = parseInt(fullMatch[2], 10) - 1; // JS 月份从 0 开始
73
+ const day = parseInt(fullMatch[3], 10);
74
+ return { date: new Date(year, month, day), hasTime: false };
75
+ }
76
+ // "4 月 21 日" 格式(使用参考年份)
77
+ const shortMatch = dateStr.match(/(\d{1,2})\s*月\s*(\d{1,2})\s*日/);
78
+ if (shortMatch) {
79
+ const month = parseInt(shortMatch[1], 10) - 1;
80
+ const day = parseInt(shortMatch[2], 10);
81
+ const year = referenceDate.getFullYear();
82
+ const date = new Date(year, month, day);
83
+ // 如果日期在未来,则为去年
84
+ if (date > referenceDate) {
85
+ date.setFullYear(year - 1);
86
+ }
87
+ return { date, hasTime: false };
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * 格式化时间范围
93
+ * 输出: "2015-04-01 to 2017-08-15"
94
+ */
95
+ function formatTimeRange(start, end) {
96
+ const format = (d) => {
97
+ const year = d.getFullYear();
98
+ const month = String(d.getMonth() + 1).padStart(2, '0');
99
+ const day = String(d.getDate()).padStart(2, '0');
100
+ return `${year}-${month}-${day}`;
101
+ };
102
+ return `${format(start)} to ${format(end)}`;
103
+ }
104
+ /** 星期几名称 */
105
+ const WEEKDAY_NAMES = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
106
+ /**
107
+ * 获取星期几
108
+ */
109
+ function getWeekday(date) {
110
+ return WEEKDAY_NAMES[date.getDay()] ?? '未知';
111
+ }
112
+ /**
113
+ * 获取小时 (0-23)
114
+ */
115
+ function getHour(date) {
116
+ return date.getHours();
117
+ }
118
+ //# sourceMappingURL=date-parser.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utils 入口
3
+ */
4
+ export { parseAbsoluteDate, parseRelativeTime, formatTimeRange, getWeekday, getHour, type ParsedDate, } from './date-parser';
5
+ export { average, topN, hourDistribution, weekdayDistribution } from './stats';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /**
3
+ * Utils 入口
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.weekdayDistribution = exports.hourDistribution = exports.topN = exports.average = exports.getHour = exports.getWeekday = exports.formatTimeRange = exports.parseRelativeTime = exports.parseAbsoluteDate = void 0;
7
+ var date_parser_1 = require("./date-parser");
8
+ Object.defineProperty(exports, "parseAbsoluteDate", { enumerable: true, get: function () { return date_parser_1.parseAbsoluteDate; } });
9
+ Object.defineProperty(exports, "parseRelativeTime", { enumerable: true, get: function () { return date_parser_1.parseRelativeTime; } });
10
+ Object.defineProperty(exports, "formatTimeRange", { enumerable: true, get: function () { return date_parser_1.formatTimeRange; } });
11
+ Object.defineProperty(exports, "getWeekday", { enumerable: true, get: function () { return date_parser_1.getWeekday; } });
12
+ Object.defineProperty(exports, "getHour", { enumerable: true, get: function () { return date_parser_1.getHour; } });
13
+ var stats_1 = require("./stats");
14
+ Object.defineProperty(exports, "average", { enumerable: true, get: function () { return stats_1.average; } });
15
+ Object.defineProperty(exports, "topN", { enumerable: true, get: function () { return stats_1.topN; } });
16
+ Object.defineProperty(exports, "hourDistribution", { enumerable: true, get: function () { return stats_1.hourDistribution; } });
17
+ Object.defineProperty(exports, "weekdayDistribution", { enumerable: true, get: function () { return stats_1.weekdayDistribution; } });
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 统计工具
3
+ */
4
+ /** 计算平均值 */
5
+ export declare function average(values: number[]): number;
6
+ /** 统计分布并返回 Top N */
7
+ export declare function topN<T>(items: T[], getKey: (item: T) => string, n: number): Record<string, number>;
8
+ /** 计算小时分布 (0-23) */
9
+ export declare function hourDistribution(dates: Date[]): Record<number, number>;
10
+ /** 计算星期分布(百分比) */
11
+ export declare function weekdayDistribution(dates: Date[]): Record<string, number>;
12
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * 统计工具
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.average = average;
7
+ exports.topN = topN;
8
+ exports.hourDistribution = hourDistribution;
9
+ exports.weekdayDistribution = weekdayDistribution;
10
+ /** 计算平均值 */
11
+ function average(values) {
12
+ if (values.length === 0)
13
+ return 0;
14
+ const sum = values.reduce((acc, val) => acc + val, 0);
15
+ return sum / values.length;
16
+ }
17
+ /** 统计分布并返回 Top N */
18
+ function topN(items, getKey, n) {
19
+ const counts = {};
20
+ for (const item of items) {
21
+ const key = getKey(item);
22
+ counts[key] = (counts[key] ?? 0) + 1;
23
+ }
24
+ // 按数量降序排序,取前 N
25
+ const sorted = Object.entries(counts)
26
+ .sort((a, b) => b[1] - a[1])
27
+ .slice(0, n);
28
+ return Object.fromEntries(sorted);
29
+ }
30
+ /** 计算小时分布 (0-23) */
31
+ function hourDistribution(dates) {
32
+ const dist = {};
33
+ for (const date of dates) {
34
+ const hour = date.getHours();
35
+ dist[hour] = (dist[hour] ?? 0) + 1;
36
+ }
37
+ return dist;
38
+ }
39
+ /** 计算星期分布(百分比) */
40
+ function weekdayDistribution(dates) {
41
+ if (dates.length === 0)
42
+ return {};
43
+ const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
44
+ const counts = {};
45
+ // 初始化所有天数为 0
46
+ weekdays.forEach((day) => {
47
+ counts[day] = 0;
48
+ });
49
+ for (const date of dates) {
50
+ const day = weekdays[date.getDay()] ?? '未知';
51
+ counts[day] = (counts[day] ?? 0) + 1;
52
+ }
53
+ // 转换为百分比
54
+ const total = dates.length;
55
+ const result = {};
56
+ for (const day of weekdays) {
57
+ // 即使无活动也要返回 0
58
+ result[day] = total === 0 ? 0 : (counts[day] ?? 0) / total;
59
+ }
60
+ // 按占比降序排序
61
+ const sortedEntries = Object.entries(result).sort(([, a], [, b]) => b - a);
62
+ return Object.fromEntries(sortedEntries);
63
+ }
64
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * V2EX 模块公共导出
3
+ * 提供 V2EX 业务相关的类型、URL 生成和 HTML 解析功能
4
+ */
5
+ export type { V2exReply, V2exTopicDetail, UserProfileParseResult, RepliesPageParseResult, TopicsPageParseResult, TopicDetailParseResult, } from './types';
6
+ export { getUserProfileUrl, getUserRepliesUrl, getUserTopicsUrl, getTopicUrl, extractTopicIdFromPath, } from './urls';
7
+ export { parseUserProfile, parseRepliesPage, parseTopicsListPage, parseTopicDetail, } from './parsers';
8
+ export { getUserProfile, getAllUserReplies, getAllUserTopicUrls, getAllUserTopicsDetail, } from './use-cases';
9
+ export type { ServiceOptions, PagedResult, UserTopicUrlsResult, UserTopicsDetailResult, } from './use-cases';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * V2EX 模块公共导出
4
+ * 提供 V2EX 业务相关的类型、URL 生成和 HTML 解析功能
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.getAllUserTopicsDetail = exports.getAllUserTopicUrls = exports.getAllUserReplies = exports.getUserProfile = exports.parseTopicDetail = exports.parseTopicsListPage = exports.parseRepliesPage = exports.parseUserProfile = exports.extractTopicIdFromPath = exports.getTopicUrl = exports.getUserTopicsUrl = exports.getUserRepliesUrl = exports.getUserProfileUrl = void 0;
8
+ // URL 生成器导出
9
+ var urls_1 = require("./urls");
10
+ Object.defineProperty(exports, "getUserProfileUrl", { enumerable: true, get: function () { return urls_1.getUserProfileUrl; } });
11
+ Object.defineProperty(exports, "getUserRepliesUrl", { enumerable: true, get: function () { return urls_1.getUserRepliesUrl; } });
12
+ Object.defineProperty(exports, "getUserTopicsUrl", { enumerable: true, get: function () { return urls_1.getUserTopicsUrl; } });
13
+ Object.defineProperty(exports, "getTopicUrl", { enumerable: true, get: function () { return urls_1.getTopicUrl; } });
14
+ Object.defineProperty(exports, "extractTopicIdFromPath", { enumerable: true, get: function () { return urls_1.extractTopicIdFromPath; } });
15
+ // 解析器导出
16
+ var parsers_1 = require("./parsers");
17
+ Object.defineProperty(exports, "parseUserProfile", { enumerable: true, get: function () { return parsers_1.parseUserProfile; } });
18
+ Object.defineProperty(exports, "parseRepliesPage", { enumerable: true, get: function () { return parsers_1.parseRepliesPage; } });
19
+ Object.defineProperty(exports, "parseTopicsListPage", { enumerable: true, get: function () { return parsers_1.parseTopicsListPage; } });
20
+ Object.defineProperty(exports, "parseTopicDetail", { enumerable: true, get: function () { return parsers_1.parseTopicDetail; } });
21
+ // 服务层导出
22
+ var use_cases_1 = require("./use-cases");
23
+ Object.defineProperty(exports, "getUserProfile", { enumerable: true, get: function () { return use_cases_1.getUserProfile; } });
24
+ Object.defineProperty(exports, "getAllUserReplies", { enumerable: true, get: function () { return use_cases_1.getAllUserReplies; } });
25
+ Object.defineProperty(exports, "getAllUserTopicUrls", { enumerable: true, get: function () { return use_cases_1.getAllUserTopicUrls; } });
26
+ Object.defineProperty(exports, "getAllUserTopicsDetail", { enumerable: true, get: function () { return use_cases_1.getAllUserTopicsDetail; } });
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * V2EX 解析器导出
3
+ */
4
+ export { parseUserProfile } from './user-profile';
5
+ export { parseRepliesPage } from './replies-page';
6
+ export { parseTopicsListPage } from './topics-list-page';
7
+ export { parseTopicDetail } from './topic-detail';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /**
3
+ * V2EX 解析器导出
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseTopicDetail = exports.parseTopicsListPage = exports.parseRepliesPage = exports.parseUserProfile = void 0;
7
+ var user_profile_1 = require("./user-profile");
8
+ Object.defineProperty(exports, "parseUserProfile", { enumerable: true, get: function () { return user_profile_1.parseUserProfile; } });
9
+ var replies_page_1 = require("./replies-page");
10
+ Object.defineProperty(exports, "parseRepliesPage", { enumerable: true, get: function () { return replies_page_1.parseRepliesPage; } });
11
+ var topics_list_page_1 = require("./topics-list-page");
12
+ Object.defineProperty(exports, "parseTopicsListPage", { enumerable: true, get: function () { return topics_list_page_1.parseTopicsListPage; } });
13
+ var topic_detail_1 = require("./topic-detail");
14
+ Object.defineProperty(exports, "parseTopicDetail", { enumerable: true, get: function () { return topic_detail_1.parseTopicDetail; } });
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 回复列表页解析器
3
+ */
4
+ import type { RepliesPageParseResult } from '../types/parse-result';
5
+ /**
6
+ * 解析回复列表页
7
+ * @param html - 页面 HTML
8
+ * @returns 回复列表解析结果
9
+ */
10
+ export declare function parseRepliesPage(html: string): RepliesPageParseResult;
11
+ //# sourceMappingURL=replies-page.d.ts.map
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * 回复列表页解析器
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.parseRepliesPage = parseRepliesPage;
40
+ const cheerio = __importStar(require("cheerio"));
41
+ const utils_1 = require("./utils");
42
+ const selectors_1 = require("./selectors");
43
+ const { totalRepliesContainer: TOTAL_CONTAINER, replyItem: REPLY_ITEM, replyContent: REPLY_CONTENT, replyTime: REPLY_TIME, topicLink: TOPIC_LINK, nodeLink: NODE_LINK, memberLink: MEMBER_LINK, } = selectors_1.REPLIES_PAGE_SELECTORS;
44
+ /**
45
+ * 解析回复列表页
46
+ * @param html - 页面 HTML
47
+ * @returns 回复列表解析结果
48
+ */
49
+ function parseRepliesPage(html) {
50
+ const $ = cheerio.load(html);
51
+ const replies = [];
52
+ // 获取用户回复总数
53
+ let totalReplies = 0;
54
+ const headerText = $(TOTAL_CONTAINER).text();
55
+ const totalMatch = headerText.match(/回复总数\s+(\d+)/);
56
+ if (totalMatch?.[1]) {
57
+ totalReplies = parseInt(totalMatch[1], 10);
58
+ }
59
+ // 分页信息
60
+ const { currentPage, totalPages } = (0, utils_1.parsePagination)($);
61
+ // 解析每条回复
62
+ $(REPLY_ITEM).each((_, dockEl) => {
63
+ const dockArea = $(dockEl);
64
+ // .reply_content 在 .dock_area 的下一个兄弟元素 (.inner 或 .cell) 内部
65
+ const replyContentWrapper = dockArea.next();
66
+ const replyContent = replyContentWrapper.find(REPLY_CONTENT);
67
+ // 主题标题和回复总数
68
+ const topicLink = dockArea.find(TOPIC_LINK);
69
+ const topicTitle = topicLink.text().trim();
70
+ const topicHref = topicLink.attr('href') ?? '';
71
+ const replyCountMatch = topicHref.match(/#reply(\d+)/);
72
+ const topicReplyCount = replyCountMatch?.[1] ? parseInt(replyCountMatch[1], 10) : 0;
73
+ // 节点名称
74
+ const nodeLink = dockArea.find(NODE_LINK);
75
+ const nodeName = nodeLink.text().trim();
76
+ // 回复时间
77
+ const timeSpan = dockArea.find(REPLY_TIME);
78
+ const replyTime = timeSpan.text().trim();
79
+ // 回复内容处理
80
+ const contentHtml = replyContent.html() ?? '';
81
+ let content = replyContent.text().trim();
82
+ let isDirectReply = true;
83
+ let replyTo = null;
84
+ // 检查是否以 @ 开头(回复他人)
85
+ if (contentHtml.trim().startsWith('@')) {
86
+ isDirectReply = false;
87
+ const memberLink = replyContent.find(MEMBER_LINK).first();
88
+ if (memberLink.length > 0) {
89
+ replyTo = memberLink.text().trim();
90
+ // 移除 @用户名 部分(使用字符串方法避免 ReDoS)
91
+ const atPrefix = `@${replyTo}`;
92
+ if (content.startsWith(atPrefix)) {
93
+ content = content.slice(atPrefix.length).trim();
94
+ }
95
+ }
96
+ }
97
+ replies.push({
98
+ topicTitle,
99
+ topicReplyCount,
100
+ nodeName,
101
+ replyTime,
102
+ content,
103
+ isDirectReply,
104
+ replyTo,
105
+ });
106
+ });
107
+ return {
108
+ totalReplies,
109
+ replies,
110
+ currentPage,
111
+ totalPages,
112
+ };
113
+ }
114
+ //# sourceMappingURL=replies-page.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * V2EX 选择器导出
3
+ * 集中管理所有页面的 DOM 选择器
4
+ */
5
+ export { USER_PROFILE_SELECTORS } from './user-profile';
6
+ export { REPLIES_PAGE_SELECTORS } from './replies-page';
7
+ export { TOPICS_LIST_PAGE_SELECTORS } from './topics-list-page';
8
+ export { TOPIC_DETAIL_SELECTORS } from './topic-detail';
9
+ export { PAGINATION_SELECTORS } from './pagination';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /**
3
+ * V2EX 选择器导出
4
+ * 集中管理所有页面的 DOM 选择器
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.PAGINATION_SELECTORS = exports.TOPIC_DETAIL_SELECTORS = exports.TOPICS_LIST_PAGE_SELECTORS = exports.REPLIES_PAGE_SELECTORS = exports.USER_PROFILE_SELECTORS = void 0;
8
+ var user_profile_1 = require("./user-profile");
9
+ Object.defineProperty(exports, "USER_PROFILE_SELECTORS", { enumerable: true, get: function () { return user_profile_1.USER_PROFILE_SELECTORS; } });
10
+ var replies_page_1 = require("./replies-page");
11
+ Object.defineProperty(exports, "REPLIES_PAGE_SELECTORS", { enumerable: true, get: function () { return replies_page_1.REPLIES_PAGE_SELECTORS; } });
12
+ var topics_list_page_1 = require("./topics-list-page");
13
+ Object.defineProperty(exports, "TOPICS_LIST_PAGE_SELECTORS", { enumerable: true, get: function () { return topics_list_page_1.TOPICS_LIST_PAGE_SELECTORS; } });
14
+ var topic_detail_1 = require("./topic-detail");
15
+ Object.defineProperty(exports, "TOPIC_DETAIL_SELECTORS", { enumerable: true, get: function () { return topic_detail_1.TOPIC_DETAIL_SELECTORS; } });
16
+ var pagination_1 = require("./pagination");
17
+ Object.defineProperty(exports, "PAGINATION_SELECTORS", { enumerable: true, get: function () { return pagination_1.PAGINATION_SELECTORS; } });
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 分页选择器
3
+ * 用于解析分页信息
4
+ */
5
+ export declare const PAGINATION_SELECTORS: {
6
+ /** 当前页码 */
7
+ readonly currentPage: "a.page_current";
8
+ /** 其他页码链接 */
9
+ readonly pageLinks: "a.page_normal";
10
+ };
11
+ //# sourceMappingURL=pagination.d.ts.map