codeksei 0.1.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 (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. package/templates/weixin-operations.md +69 -0
@@ -0,0 +1,1081 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { maybeGenerateSemanticReview } = require("./review-semantic");
4
+ const {
5
+ PRIMARY_REVIEW_MARKER_PREFIX,
6
+ LEGACY_REVIEW_MARKER_PREFIX,
7
+ } = require("./branding");
8
+
9
+ const REVIEW_MARKER_PREFIX = PRIMARY_REVIEW_MARKER_PREFIX;
10
+ const REVIEW_MARKER_PREFIXES = [PRIMARY_REVIEW_MARKER_PREFIX, LEGACY_REVIEW_MARKER_PREFIX];
11
+
12
+ const DEFAULT_REVIEW_MODELS = {
13
+ nightly: {
14
+ cadenceLabel: "夜",
15
+ titleSuffix: "睡前收口",
16
+ carryLabel: "明天第一步",
17
+ intro: "这是一份 Codeksei 睡前收口,不是学习项目模板。它只收今天真实推进了什么、现在还挂着什么、明天从哪里更容易接上,好把周/月复盘的原料先压成一层低摩擦摘要。",
18
+ tags: ["codeksei", "cyberboss", "life-assistant", "review", "nightly"],
19
+ },
20
+ weekly: {
21
+ cadenceLabel: "周",
22
+ titleSuffix: "周复盘",
23
+ carryLabel: "下周第一步",
24
+ intro: "这是一份 Codeksei 生活助理周复盘,不是学习项目模板。它只关心这周真实推进了什么、摩擦在哪里、线头还挂着什么,以及下周如何更容易重新接上。",
25
+ tags: ["codeksei", "cyberboss", "life-assistant", "review", "weekly"],
26
+ },
27
+ monthly: {
28
+ cadenceLabel: "月",
29
+ titleSuffix: "月复盘",
30
+ carryLabel: "下月第一步",
31
+ intro: "这是一份 Codeksei 生活助理月复盘,不是学习项目模板。它优先收口这个月真实推进的线、反复出现的摩擦、仍未解决的线头,以及下个月应该从哪里接上。",
32
+ tags: ["codeksei", "cyberboss", "life-assistant", "review", "monthly"],
33
+ },
34
+ };
35
+
36
+ function loadReviewSchemaConfig(config = {}) {
37
+ const filePath = normalizeText(config.reviewSchemaConfigFile);
38
+ if (!filePath) {
39
+ return {};
40
+ }
41
+ try {
42
+ const raw = fs.readFileSync(filePath, "utf8");
43
+ const parsed = JSON.parse(raw);
44
+ return parsed && typeof parsed === "object" ? parsed : {};
45
+ } catch (error) {
46
+ if (error && error.code === "ENOENT") {
47
+ return {};
48
+ }
49
+ throw new Error(`review schema 不是合法 JSON: ${filePath} (${formatErrorMessage(error)})`);
50
+ }
51
+ }
52
+
53
+ function resolveReviewProfile(config = {}, kind, options = {}) {
54
+ const normalizedKind = normalizeReviewKind(kind);
55
+ const defaults = DEFAULT_REVIEW_MODELS[normalizedKind];
56
+ const required = options.required !== false;
57
+ const workspaceRoot = normalizeDisplayPath(path.resolve(String(config.workspaceRoot || process.cwd())));
58
+ const schemaConfig = loadReviewSchemaConfig(config);
59
+ const workspaceProfile = selectWorkspaceProfile(schemaConfig.workspaces, workspaceRoot);
60
+ const reviews = workspaceProfile?.reviews && typeof workspaceProfile.reviews === "object"
61
+ ? workspaceProfile.reviews
62
+ : {};
63
+ const rawProfile = reviews[normalizedKind];
64
+
65
+ if ((!rawProfile || typeof rawProfile !== "object") && !required) {
66
+ return null;
67
+ }
68
+ if (!rawProfile || typeof rawProfile !== "object") {
69
+ throw new Error(`当前 workspace 没有 ${normalizedKind} review 配置: ${workspaceRoot}`);
70
+ }
71
+
72
+ const folder = normalizeRelativeOrAbsolutePath(rawProfile.folder);
73
+ if (!folder) {
74
+ throw new Error(`${normalizedKind} review 缺少 folder 配置`);
75
+ }
76
+
77
+ const carryLabel = normalizeText(rawProfile.carryLabel) || defaults.carryLabel;
78
+ return {
79
+ kind: normalizedKind,
80
+ workspaceRoot,
81
+ folderPath: resolveWorkspacePath(workspaceRoot, folder),
82
+ intro: normalizeText(rawProfile.intro) || defaults.intro,
83
+ cadenceLabel: normalizeText(rawProfile.cadenceLabel) || defaults.cadenceLabel,
84
+ titleSuffix: normalizeText(rawProfile.titleSuffix) || defaults.titleSuffix,
85
+ carryLabel,
86
+ tags: normalizeTags(rawProfile.tags, defaults.tags),
87
+ sections: buildReviewSections(normalizedKind, carryLabel),
88
+ };
89
+ }
90
+
91
+ async function buildReview(config = {}, kind, options = {}) {
92
+ const profile = resolveReviewProfile(config, kind);
93
+ const window = resolveReviewWindow(profile.kind, options);
94
+ const diaryEntries = collectDiaryEntries(config.diaryDir, window.startDate, window.endDate);
95
+ const nightlyEntries = profile.kind === "nightly"
96
+ ? []
97
+ : collectNightlyEntries(config, window);
98
+ const deterministicDraft = buildReviewDraft(profile, window, diaryEntries, nightlyEntries);
99
+ // Review v2 keeps routing, windowing, and managed-block writes deterministic.
100
+ // The semantic pass may upgrade the human-facing bullets, but it must never
101
+ // become a hard dependency for file generation.
102
+ const semantic = await maybeGenerateSemanticReview(config, {
103
+ profile,
104
+ window,
105
+ diaryEntries,
106
+ nightlyEntries,
107
+ deterministicDraft,
108
+ options,
109
+ });
110
+ const draft = mergeReviewDraft(profile.kind, deterministicDraft, semantic.data);
111
+ const notePath = normalizeDisplayPath(path.join(profile.folderPath, `${draft.periodLabel}.md`));
112
+ return {
113
+ profile,
114
+ window,
115
+ diaryEntries,
116
+ nightlyEntries,
117
+ semantic,
118
+ draft,
119
+ notePath,
120
+ };
121
+ }
122
+
123
+ async function writeReview(config = {}, kind, options = {}) {
124
+ const review = await buildReview(config, kind, options);
125
+ fs.mkdirSync(path.dirname(review.notePath), { recursive: true });
126
+ const now = new Date();
127
+ const current = fs.existsSync(review.notePath)
128
+ ? normalizeLineEnding(fs.readFileSync(review.notePath, "utf8"))
129
+ : buildReviewFileSkeleton(review, now);
130
+ const next = syncReviewContent(current, review, now);
131
+ const changed = ensureTrailingNewline(current) !== ensureTrailingNewline(next);
132
+ if (changed) {
133
+ fs.writeFileSync(review.notePath, ensureTrailingNewline(next), "utf8");
134
+ }
135
+ return {
136
+ changed,
137
+ filePath: review.notePath,
138
+ periodLabel: review.draft.periodLabel,
139
+ diaryCount: review.diaryEntries.length,
140
+ nightlyCount: review.nightlyEntries.length,
141
+ semanticUsed: !!review.semantic?.used,
142
+ semanticReason: review.semantic?.reason || "",
143
+ };
144
+ }
145
+
146
+ function buildReviewFileSkeleton(review, now = new Date()) {
147
+ const createdAt = formatDateTime(now);
148
+ const updated = formatDate(now);
149
+ const frontmatter = [
150
+ "---",
151
+ `created: ${createdAt}`,
152
+ `updated: ${updated}`,
153
+ "type: review",
154
+ `review_period: ${review.profile.kind}`,
155
+ `period_label: ${review.draft.periodLabel}`,
156
+ `period_start: ${review.window.startDate}`,
157
+ `period_end: ${review.window.endDate}`,
158
+ `source_diary_days: ${review.draft.sourceDiaryDays}`,
159
+ ];
160
+
161
+ if (review.profile.kind !== "nightly") {
162
+ frontmatter.push(`source_nightly_days: ${review.draft.sourceNightlyDays}`);
163
+ }
164
+
165
+ frontmatter.push(
166
+ "status: working",
167
+ "tags:",
168
+ ...review.profile.tags.map((tag) => ` - ${tag}`),
169
+ "---",
170
+ `# ${review.draft.periodTitle}`,
171
+ "",
172
+ `> ${review.profile.intro} `,
173
+ ""
174
+ );
175
+
176
+ for (const section of review.profile.sections) {
177
+ frontmatter.push(section.heading);
178
+ if (section.slot) {
179
+ frontmatter.push(buildManagedBlock(section.slot, ""));
180
+ } else {
181
+ frontmatter.push(section.staticBody || "");
182
+ }
183
+ frontmatter.push("");
184
+ }
185
+
186
+ return frontmatter.join("\n");
187
+ }
188
+
189
+ function syncReviewContent(content, review, now = new Date()) {
190
+ let next = ensureReviewSections(normalizeLineEnding(content), review);
191
+ next = updateFrontmatterValue(next, "updated", formatDate(now));
192
+ next = updateFrontmatterValue(next, "period_label", review.draft.periodLabel);
193
+ next = updateFrontmatterValue(next, "period_start", review.window.startDate);
194
+ next = updateFrontmatterValue(next, "period_end", review.window.endDate);
195
+ next = updateFrontmatterValue(next, "source_diary_days", String(review.diaryEntries.length));
196
+ if (review.profile.kind !== "nightly") {
197
+ next = updateFrontmatterValue(next, "source_nightly_days", String(review.nightlyEntries.length));
198
+ }
199
+
200
+ next = replaceHeading(next, 1, review.draft.periodTitle);
201
+ for (const [slot, body] of Object.entries(review.draft.content)) {
202
+ next = upsertManagedBlock(next, slot, body);
203
+ }
204
+ return ensureTrailingNewline(next);
205
+ }
206
+
207
+ function ensureReviewSections(content, review) {
208
+ let next = content;
209
+ for (const section of review.profile.sections) {
210
+ const headingPattern = new RegExp(`^${escapeRegExp(section.heading)}\\s*$`, "m");
211
+ const hasHeading = headingPattern.test(next);
212
+ const hasBlock = !section.slot || hasManagedBlock(next, section.slot);
213
+ if (hasHeading && hasBlock) {
214
+ continue;
215
+ }
216
+
217
+ const sectionBody = section.slot
218
+ ? buildManagedBlock(section.slot, "")
219
+ : (section.staticBody || "");
220
+
221
+ if (!hasHeading) {
222
+ next = `${next.replace(/\s*$/u, "")}\n\n${section.heading}\n${sectionBody}\n`;
223
+ continue;
224
+ }
225
+
226
+ if (section.slot && !hasBlock) {
227
+ next = next.replace(
228
+ headingPattern,
229
+ `${section.heading}\n${buildManagedBlock(section.slot, "")}`
230
+ );
231
+ }
232
+ }
233
+ return next;
234
+ }
235
+
236
+ function buildReviewDraft(profile, window, diaryEntries, nightlyEntries = []) {
237
+ if (profile.kind === "nightly") {
238
+ return buildNightlyDraft(profile, window, diaryEntries);
239
+ }
240
+ return buildPeriodicReviewDraft(profile, window, diaryEntries, nightlyEntries);
241
+ }
242
+
243
+ function buildNightlyDraft(profile, window, diaryEntries) {
244
+ const entry = diaryEntries[0] || null;
245
+ const openTodos = entry ? entry.todo.open.length : 0;
246
+ const doneTodos = entry ? entry.todo.done.length : 0;
247
+ const timelineCount = entry ? entry.timeline.length : 0;
248
+ const supplementCount = entry ? entry.supplement.length : 0;
249
+
250
+ const progress = dedupeStatements(selectProgressFromDiary(entry)).slice(0, 6);
251
+ const friction = dedupeStatements(selectFrictionFromDiary(entry)).slice(0, 6);
252
+ const openLoops = dedupeStatements(entry ? entry.todo.open : []).slice(0, 8);
253
+ const carryForward = dedupeStatements([
254
+ ...(entry ? entry.summary.filter(looksLikeCarryForward) : []),
255
+ ...openLoops,
256
+ ]).slice(0, 5);
257
+ const closeout = dedupeStatements(
258
+ entry && entry.summary.length ? entry.summary : progress
259
+ ).slice(0, 6);
260
+ const signals = dedupeStatements([
261
+ ...(entry ? entry.fragment : []),
262
+ ...selectSignalFromSupplements(entry ? entry.supplement : []),
263
+ ...carryForward,
264
+ ]).slice(0, 6);
265
+ const windowFacts = [
266
+ `日期:${window.startDate}`,
267
+ `覆盖日记:${diaryEntries.length} 天`,
268
+ `Todo 完成 / 未完成:${doneTodos} / ${openTodos}`,
269
+ `时间线事实条数:${timelineCount}`,
270
+ `补充记录条数:${supplementCount}`,
271
+ ];
272
+
273
+ return {
274
+ periodLabel: window.label,
275
+ periodTitle: `${window.label} ${profile.titleSuffix}`,
276
+ sourceDiaryDays: diaryEntries.length,
277
+ sourceNightlyDays: 0,
278
+ windowFacts,
279
+ insights: {
280
+ progress,
281
+ friction,
282
+ openLoops,
283
+ carryForward,
284
+ closeout,
285
+ signals,
286
+ },
287
+ content: {
288
+ window: renderBulletList(windowFacts, "今天还没有可用的日记事实。"),
289
+ progress: renderBulletList(progress, "今天还没有收出可用的推进摘要。"),
290
+ friction: renderBulletList(friction, "今天还没有明显的摩擦摘要。"),
291
+ "open-loops": renderBulletList(openLoops, "今晚没有明显还开着的线头。"),
292
+ "carry-forward": renderBulletList(carryForward, "明天先从最小动作重新接上。"),
293
+ closeout: renderBulletList(closeout, "今天的睡前收口还没有写出来。"),
294
+ signals: renderBulletList(signals, "今天还没有稳定到值得带走的信号。"),
295
+ },
296
+ };
297
+ }
298
+
299
+ function buildPeriodicReviewDraft(profile, window, diaryEntries, nightlyEntries = []) {
300
+ const latestEntry = diaryEntries[diaryEntries.length - 1] || null;
301
+ const nightlyByDate = new Map(
302
+ nightlyEntries.map((entry) => [entry.date, entry])
303
+ );
304
+ const totalOpenTodos = diaryEntries.reduce((sum, entry) => sum + entry.todo.open.length, 0);
305
+ const totalDoneTodos = diaryEntries.reduce((sum, entry) => sum + entry.todo.done.length, 0);
306
+ const totalTimelineFacts = diaryEntries.reduce((sum, entry) => sum + entry.timeline.length, 0);
307
+
308
+ const progress = dedupeStatements(
309
+ diaryEntries.flatMap((entry) => selectPeriodicProgress(entry, nightlyByDate.get(entry.date)))
310
+ ).slice(0, 8);
311
+
312
+ const friction = dedupeStatements(
313
+ diaryEntries.flatMap((entry) => selectPeriodicFriction(entry, nightlyByDate.get(entry.date)))
314
+ ).slice(0, 8);
315
+
316
+ const latestNightly = latestEntry ? nightlyByDate.get(latestEntry.date) || null : null;
317
+ const openLoops = dedupeStatements(
318
+ latestNightly?.openLoops?.length
319
+ ? latestNightly.openLoops
320
+ : (latestEntry ? latestEntry.todo.open : [])
321
+ ).slice(0, 8);
322
+
323
+ const carryForward = dedupeStatements([
324
+ ...(latestNightly?.carryForward || []),
325
+ ...(latestEntry ? latestEntry.summary.filter(looksLikeCarryForward) : []),
326
+ ...openLoops,
327
+ ]).slice(0, 5);
328
+
329
+ const dailySummaries = diaryEntries
330
+ .map((entry) => ({
331
+ date: entry.date,
332
+ lines: dedupeStatements(
333
+ selectPeriodicCloseout(entry, nightlyByDate.get(entry.date))
334
+ ).slice(0, 6),
335
+ }))
336
+ .filter((entry) => entry.lines.length);
337
+
338
+ const supplements = diaryEntries
339
+ .flatMap((entry) => selectPeriodicSupplementGroups(entry, nightlyByDate.get(entry.date)))
340
+ .slice(-8);
341
+ const windowFacts = [
342
+ `时间范围:${window.startDate} ~ ${window.endDate}`,
343
+ `覆盖日记:${diaryEntries.length} 天`,
344
+ `夜间收口:${nightlyEntries.length} 天`,
345
+ `Todo 完成 / 未完成:${totalDoneTodos} / ${totalOpenTodos}`,
346
+ `时间线事实条数:${totalTimelineFacts}`,
347
+ `周期末尾仍开着的线头:${openLoops.length}`,
348
+ ];
349
+
350
+ return {
351
+ periodLabel: window.label,
352
+ periodTitle: `${window.label} ${profile.titleSuffix}`,
353
+ sourceDiaryDays: diaryEntries.length,
354
+ sourceNightlyDays: nightlyEntries.length,
355
+ windowFacts,
356
+ insights: {
357
+ progress,
358
+ friction,
359
+ openLoops,
360
+ carryForward,
361
+ dailySummaries,
362
+ supplements,
363
+ },
364
+ content: {
365
+ window: renderBulletList(windowFacts, "这一段时间还没有可用日记事实。"),
366
+ progress: renderBulletList(progress, "这段时间还没有收出可用的推进摘要。"),
367
+ friction: renderBulletList(friction, "这段时间还没有明显的摩擦摘要。"),
368
+ "open-loops": renderBulletList(openLoops, "这一周期末尾没有明显还开着的线头。"),
369
+ "carry-forward": renderBulletList(carryForward, "下一次先从最小动作重新接上。"),
370
+ "daily-summaries": renderDatedGroups(dailySummaries, "这段时间没有可引用的每日总结。"),
371
+ supplements: renderSupplementGroups(supplements, "这段时间没有值得回看的补充记录。"),
372
+ },
373
+ };
374
+ }
375
+
376
+ function mergeReviewDraft(kind, deterministicDraft, semanticData) {
377
+ if (!semanticData || typeof semanticData !== "object") {
378
+ return deterministicDraft;
379
+ }
380
+
381
+ const mergedInsights = {
382
+ ...deterministicDraft.insights,
383
+ };
384
+ for (const [key, value] of Object.entries(semanticData)) {
385
+ if (Array.isArray(value) && value.length) {
386
+ mergedInsights[key] = value;
387
+ }
388
+ }
389
+
390
+ const mergedDraft = {
391
+ ...deterministicDraft,
392
+ insights: mergedInsights,
393
+ content: {
394
+ ...deterministicDraft.content,
395
+ },
396
+ };
397
+
398
+ if (kind === "nightly") {
399
+ mergedDraft.content.progress = renderBulletList(mergedInsights.progress, "今天还没有收出可用的推进摘要。");
400
+ mergedDraft.content.friction = renderBulletList(mergedInsights.friction, "今天还没有明显的摩擦摘要。");
401
+ mergedDraft.content["open-loops"] = renderBulletList(mergedInsights.openLoops, "今晚没有明显还开着的线头。");
402
+ mergedDraft.content["carry-forward"] = renderBulletList(mergedInsights.carryForward, "明天先从最小动作重新接上。");
403
+ mergedDraft.content.closeout = renderBulletList(mergedInsights.closeout, "今天的睡前收口还没有写出来。");
404
+ mergedDraft.content.signals = renderBulletList(mergedInsights.signals, "今天还没有稳定到值得带走的信号。");
405
+ return mergedDraft;
406
+ }
407
+
408
+ mergedDraft.content.progress = renderBulletList(mergedInsights.progress, "这段时间还没有收出可用的推进摘要。");
409
+ mergedDraft.content.friction = renderBulletList(mergedInsights.friction, "这段时间还没有明显的摩擦摘要。");
410
+ mergedDraft.content["open-loops"] = renderBulletList(mergedInsights.openLoops, "这一周期末尾没有明显还开着的线头。");
411
+ mergedDraft.content["carry-forward"] = renderBulletList(mergedInsights.carryForward, "下一次先从最小动作重新接上。");
412
+ mergedDraft.content["daily-summaries"] = renderDatedGroups(mergedInsights.dailySummaries, "这段时间没有可引用的每日总结。");
413
+ mergedDraft.content.supplements = renderSupplementGroups(mergedInsights.supplements, "这段时间没有值得回看的补充记录。");
414
+ return mergedDraft;
415
+ }
416
+
417
+ function collectDiaryEntries(diaryDir, startDate, endDate) {
418
+ const normalizedDiaryDir = normalizeText(diaryDir);
419
+ if (!normalizedDiaryDir || !fs.existsSync(normalizedDiaryDir)) {
420
+ return [];
421
+ }
422
+ return fs.readdirSync(normalizedDiaryDir)
423
+ .filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/u.test(name))
424
+ .map((name) => name.replace(/\.md$/u, ""))
425
+ .filter((date) => date >= startDate && date <= endDate)
426
+ .sort()
427
+ .map((date) => parseDiaryFile(path.join(normalizedDiaryDir, `${date}.md`), date))
428
+ .filter(Boolean);
429
+ }
430
+
431
+ function collectNightlyEntries(config, window) {
432
+ const nightlyProfile = resolveReviewProfile(config, "nightly", { required: false });
433
+ if (!nightlyProfile || !fs.existsSync(nightlyProfile.folderPath)) {
434
+ return [];
435
+ }
436
+ return fs.readdirSync(nightlyProfile.folderPath)
437
+ .filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/u.test(name))
438
+ .map((name) => name.replace(/\.md$/u, ""))
439
+ .filter((date) => date >= window.startDate && date <= window.endDate)
440
+ .sort()
441
+ .map((date) => parseNightlyReviewFile(path.join(nightlyProfile.folderPath, `${date}.md`), date))
442
+ .filter(Boolean);
443
+ }
444
+
445
+ function parseDiaryFile(filePath, date) {
446
+ const content = normalizeLineEnding(fs.readFileSync(filePath, "utf8"));
447
+ return {
448
+ date,
449
+ filePath: normalizeDisplayPath(filePath),
450
+ todo: parseTodoSection(readSectionBody(content, "Todo")),
451
+ timeline: parseBulletSection(readSectionBody(content, "时间线事实")),
452
+ fragment: parseBulletSection(readSectionBody(content, "今日碎片")),
453
+ supplement: parseSupplementSection(readSectionBody(content, "补充记录")),
454
+ summary: parseSummarySection(readSectionBody(content, "总结")),
455
+ };
456
+ }
457
+
458
+ function parseNightlyReviewFile(filePath, date) {
459
+ const content = normalizeLineEnding(fs.readFileSync(filePath, "utf8"));
460
+ return {
461
+ date,
462
+ filePath: normalizeDisplayPath(filePath),
463
+ progress: parseManagedBulletList(content, "progress"),
464
+ friction: parseManagedBulletList(content, "friction"),
465
+ openLoops: parseManagedBulletList(content, "open-loops"),
466
+ carryForward: parseManagedBulletList(content, "carry-forward"),
467
+ closeout: parseManagedBulletList(content, "closeout"),
468
+ signals: parseManagedBulletList(content, "signals"),
469
+ };
470
+ }
471
+
472
+ function readSectionBody(content, headingText) {
473
+ const startPattern = new RegExp(`^##\\s+${escapeRegExp(headingText)}\\s*$`, "m");
474
+ const startMatch = startPattern.exec(content);
475
+ if (!startMatch) {
476
+ return "";
477
+ }
478
+ const headingEnd = content.indexOf("\n", startMatch.index);
479
+ const contentStart = headingEnd >= 0 ? headingEnd + 1 : content.length;
480
+ const rest = content.slice(contentStart);
481
+ const nextHeading = /^\##\s+/m.exec(rest);
482
+ if (!nextHeading) {
483
+ return rest.trim();
484
+ }
485
+ return rest.slice(0, nextHeading.index).trim();
486
+ }
487
+
488
+ function parseTodoSection(body) {
489
+ const open = [];
490
+ const done = [];
491
+ for (const line of splitLines(body)) {
492
+ const match = /^- \[( |x|X)\] (.*)$/u.exec(line);
493
+ if (!match) {
494
+ continue;
495
+ }
496
+ const text = normalizeLineItem(match[2]);
497
+ if (!text) {
498
+ continue;
499
+ }
500
+ if (match[1].toLowerCase() === "x") {
501
+ done.push(text);
502
+ } else {
503
+ open.push(text);
504
+ }
505
+ }
506
+ return { open, done };
507
+ }
508
+
509
+ function parseBulletSection(body) {
510
+ return splitLines(body)
511
+ .map((line) => {
512
+ const match = /^-\s+(.*)$/u.exec(line);
513
+ return normalizeLineItem(match ? match[1] : "");
514
+ })
515
+ .filter(Boolean);
516
+ }
517
+
518
+ function parseSummarySection(body) {
519
+ return splitLines(body)
520
+ .map((line) => {
521
+ if (/^-\s+/u.test(line)) {
522
+ return normalizeLineItem(line.replace(/^-\s+/u, ""));
523
+ }
524
+ return normalizeLineItem(line);
525
+ })
526
+ .filter((line) => line && !isReviewLeadIn(line));
527
+ }
528
+
529
+ function parseSupplementSection(body) {
530
+ const normalized = normalizeLineEnding(body).trim();
531
+ if (!normalized) {
532
+ return [];
533
+ }
534
+ return normalized
535
+ .split(/\n(?=###\s)/u)
536
+ .map((block) => {
537
+ const lines = block.split("\n");
538
+ const heading = normalizeLineItem(lines.shift());
539
+ const match = /^###\s+(\d{2}:\d{2})(?:\s+(.*))?$/u.exec(heading);
540
+ return {
541
+ time: match?.[1] || "",
542
+ title: normalizeLineItem(match?.[2] || ""),
543
+ body: normalizeBody(lines.join("\n")),
544
+ };
545
+ })
546
+ .filter((item) => item.title || item.body);
547
+ }
548
+
549
+ function selectProgressFromDiary(entry) {
550
+ if (!entry) {
551
+ return [];
552
+ }
553
+ const progressFromSummary = entry.summary.filter(
554
+ (line) => !looksLikeCarryForward(line) && !hasFrictionSignal(line)
555
+ );
556
+ return progressFromSummary.length ? progressFromSummary : entry.timeline;
557
+ }
558
+
559
+ function selectFrictionFromDiary(entry) {
560
+ if (!entry) {
561
+ return [];
562
+ }
563
+ return [
564
+ ...entry.fragment.filter(hasFrictionSignal),
565
+ ...selectFrictionFromSupplements(entry.supplement),
566
+ ...entry.summary.filter((line) => hasFrictionSignal(line)),
567
+ ];
568
+ }
569
+
570
+ function selectPeriodicProgress(entry, nightlyEntry) {
571
+ if (nightlyEntry?.progress?.length) {
572
+ return nightlyEntry.progress;
573
+ }
574
+ return selectProgressFromDiary(entry);
575
+ }
576
+
577
+ function selectPeriodicFriction(entry, nightlyEntry) {
578
+ if (nightlyEntry?.friction?.length) {
579
+ return nightlyEntry.friction;
580
+ }
581
+ return selectFrictionFromDiary(entry);
582
+ }
583
+
584
+ function selectPeriodicCloseout(entry, nightlyEntry) {
585
+ if (nightlyEntry?.closeout?.length) {
586
+ return nightlyEntry.closeout;
587
+ }
588
+ if (entry?.summary?.length) {
589
+ return entry.summary;
590
+ }
591
+ return selectProgressFromDiary(entry);
592
+ }
593
+
594
+ function selectPeriodicSupplementGroups(entry, nightlyEntry) {
595
+ if (nightlyEntry?.signals?.length) {
596
+ return [{
597
+ date: entry.date,
598
+ title: "夜间收口提炼",
599
+ body: nightlyEntry.signals.map((line) => `- ${normalizeLineItem(line)}`).join("\n"),
600
+ }];
601
+ }
602
+ return entry.supplement
603
+ .map((item) => ({
604
+ date: entry.date,
605
+ title: item.title,
606
+ body: toCompactSentence(item.body),
607
+ }))
608
+ .filter((item) => item.body);
609
+ }
610
+
611
+ function selectFrictionFromSupplements(items) {
612
+ return (Array.isArray(items) ? items : [])
613
+ .map((item) => {
614
+ const seed = item.title || toCompactSentence(item.body);
615
+ if (!hasFrictionSignal(seed) && !hasFrictionSignal(item.body)) {
616
+ return "";
617
+ }
618
+ const body = toCompactSentence(item.body);
619
+ if (item.title && body) {
620
+ return `${item.title}:${body}`;
621
+ }
622
+ return item.title || body;
623
+ })
624
+ .filter(Boolean);
625
+ }
626
+
627
+ function selectSignalFromSupplements(items) {
628
+ return (Array.isArray(items) ? items : [])
629
+ .map((item) => {
630
+ const title = normalizeLineItem(item.title);
631
+ if (title) {
632
+ return title;
633
+ }
634
+ return truncateSentence(toCompactSentence(item.body), 140);
635
+ })
636
+ .filter(Boolean);
637
+ }
638
+
639
+ function renderBulletList(items, fallbackText) {
640
+ const lines = Array.isArray(items) && items.length
641
+ ? items.map((item) => `- ${normalizeLineItem(item)}`)
642
+ : [`- ${fallbackText}`];
643
+ return lines.join("\n");
644
+ }
645
+
646
+ function renderDatedGroups(groups, fallbackText) {
647
+ if (!Array.isArray(groups) || !groups.length) {
648
+ return `- ${fallbackText}`;
649
+ }
650
+ const parts = [];
651
+ for (const group of groups) {
652
+ parts.push(`### ${group.date}`);
653
+ for (const line of group.lines) {
654
+ parts.push(`- ${normalizeLineItem(line)}`);
655
+ }
656
+ }
657
+ return parts.join("\n");
658
+ }
659
+
660
+ function renderSupplementGroups(items, fallbackText) {
661
+ if (!Array.isArray(items) || !items.length) {
662
+ return `- ${fallbackText}`;
663
+ }
664
+ const parts = [];
665
+ for (const item of items) {
666
+ const heading = item.title
667
+ ? `### ${item.date} ${item.title}`
668
+ : `### ${item.date}`;
669
+ parts.push(heading);
670
+ parts.push(item.body || "-");
671
+ }
672
+ return parts.join("\n\n");
673
+ }
674
+
675
+ function upsertManagedBlock(content, slot, body) {
676
+ const block = buildManagedBlock(slot, body);
677
+ const pattern = buildManagedBlockPattern(slot);
678
+ if (pattern.test(content)) {
679
+ return content.replace(pattern, block);
680
+ }
681
+ return `${content.replace(/\s*$/u, "")}\n\n${block}\n`;
682
+ }
683
+
684
+ function buildManagedBlock(slot, body) {
685
+ return [
686
+ `<!-- ${REVIEW_MARKER_PREFIX}:${slot}:start -->`,
687
+ String(body || "").trim(),
688
+ `<!-- ${REVIEW_MARKER_PREFIX}:${slot}:end -->`,
689
+ ].join("\n");
690
+ }
691
+
692
+ function hasManagedBlock(content, slot) {
693
+ return REVIEW_MARKER_PREFIXES.some((prefix) => {
694
+ const markerStart = `<!-- ${prefix}:${slot}:start -->`;
695
+ const markerEnd = `<!-- ${prefix}:${slot}:end -->`;
696
+ return content.includes(markerStart) && content.includes(markerEnd);
697
+ });
698
+ }
699
+
700
+ function readManagedBlock(content, slot) {
701
+ const pattern = buildManagedBlockPattern(slot, true);
702
+ const match = pattern.exec(content);
703
+ return match?.[1] || "";
704
+ }
705
+
706
+ function buildManagedBlockPattern(slot, captureBody = false) {
707
+ const prefixPattern = REVIEW_MARKER_PREFIXES.map(escapeRegExp).join("|");
708
+ const normalizedSlot = escapeRegExp(slot);
709
+ const bodyPattern = captureBody ? "([\\s\\S]*?)" : "[\\s\\S]*?";
710
+ return new RegExp(
711
+ `<!--\\s*(?:${prefixPattern}):${normalizedSlot}:start\\s*-->\\n?${bodyPattern}\\n?<!--\\s*(?:${prefixPattern}):${normalizedSlot}:end\\s*-->`,
712
+ "u"
713
+ );
714
+ }
715
+
716
+ function parseManagedBulletList(content, slot) {
717
+ return splitLines(readManagedBlock(content, slot))
718
+ .map((line) => {
719
+ if (/^###\s+/u.test(line)) {
720
+ return "";
721
+ }
722
+ if (/^-\s+/u.test(line)) {
723
+ return normalizeLineItem(line.replace(/^-\s+/u, ""));
724
+ }
725
+ return normalizeLineItem(line);
726
+ })
727
+ .filter((line) => line && !isGeneratedFallbackLine(line));
728
+ }
729
+
730
+ function replaceHeading(content, level, title) {
731
+ const pattern = new RegExp(`^${"#".repeat(level)}\\s+.*$`, "m");
732
+ if (!pattern.test(content)) {
733
+ return content;
734
+ }
735
+ return content.replace(pattern, `${"#".repeat(level)} ${title}`);
736
+ }
737
+
738
+ function updateFrontmatterValue(content, key, value) {
739
+ if (!content.startsWith("---\n")) {
740
+ return content;
741
+ }
742
+ const end = content.indexOf("\n---\n", 4);
743
+ if (end < 0) {
744
+ return content;
745
+ }
746
+ const frontmatter = content.slice(4, end);
747
+ const rest = content.slice(end + 5);
748
+ const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*.*$`, "m");
749
+ const nextFrontmatter = pattern.test(frontmatter)
750
+ ? frontmatter.replace(pattern, `${key}: ${value}`)
751
+ : `${frontmatter}\n${key}: ${value}`;
752
+ return `---\n${nextFrontmatter}\n---\n${rest.replace(/^\n*/u, "")}`;
753
+ }
754
+
755
+ function resolveReviewWindow(kind, options = {}) {
756
+ const normalizedKind = normalizeReviewKind(kind);
757
+ if (normalizedKind === "nightly") {
758
+ return resolveNightlyWindow(options);
759
+ }
760
+ if (normalizedKind === "weekly") {
761
+ return resolveWeeklyWindow(options);
762
+ }
763
+ return resolveMonthlyWindow(options);
764
+ }
765
+
766
+ function resolveNightlyWindow(options = {}) {
767
+ const baseDate = normalizeText(options.date)
768
+ ? parseDateString(normalizeText(options.date))
769
+ : getCurrentUtcDateInShanghai();
770
+ const label = formatUtcDate(baseDate);
771
+ return {
772
+ label,
773
+ startDate: label,
774
+ endDate: label,
775
+ };
776
+ }
777
+
778
+ function resolveWeeklyWindow(options = {}) {
779
+ if (normalizeText(options.week)) {
780
+ const match = /^(\d{4})-W(\d{2})$/u.exec(normalizeText(options.week));
781
+ if (!match) {
782
+ throw new Error(`--week 格式应为 YYYY-Www: ${options.week}`);
783
+ }
784
+ const year = Number.parseInt(match[1], 10);
785
+ const week = Number.parseInt(match[2], 10);
786
+ const weekOneStart = startOfIsoWeek(createUtcDate(year, 1, 4));
787
+ const start = addDays(weekOneStart, (week - 1) * 7);
788
+ const end = addDays(start, 6);
789
+ return {
790
+ label: `${year}-W${String(week).padStart(2, "0")}`,
791
+ startDate: formatUtcDate(start),
792
+ endDate: formatUtcDate(end),
793
+ };
794
+ }
795
+
796
+ const baseDate = normalizeText(options.date)
797
+ ? parseDateString(normalizeText(options.date))
798
+ : getCurrentUtcDateInShanghai();
799
+ const start = startOfIsoWeek(baseDate);
800
+ const end = addDays(start, 6);
801
+ const week = isoWeekNumber(baseDate);
802
+ return {
803
+ label: `${baseDate.getUTCFullYear()}-W${String(week).padStart(2, "0")}`,
804
+ startDate: formatUtcDate(start),
805
+ endDate: formatUtcDate(end),
806
+ };
807
+ }
808
+
809
+ function resolveMonthlyWindow(options = {}) {
810
+ let year = 0;
811
+ let month = 0;
812
+ if (normalizeText(options.month)) {
813
+ const match = /^(\d{4})-(\d{2})$/u.exec(normalizeText(options.month));
814
+ if (!match) {
815
+ throw new Error(`--month 格式应为 YYYY-MM: ${options.month}`);
816
+ }
817
+ year = Number.parseInt(match[1], 10);
818
+ month = Number.parseInt(match[2], 10);
819
+ } else {
820
+ const baseDate = normalizeText(options.date)
821
+ ? parseDateString(normalizeText(options.date))
822
+ : getCurrentUtcDateInShanghai();
823
+ year = baseDate.getUTCFullYear();
824
+ month = baseDate.getUTCMonth() + 1;
825
+ }
826
+ const start = createUtcDate(year, month, 1);
827
+ const end = addDays(createUtcDate(year, month + 1, 1), -1);
828
+ return {
829
+ label: `${year}-${String(month).padStart(2, "0")}`,
830
+ startDate: formatUtcDate(start),
831
+ endDate: formatUtcDate(end),
832
+ };
833
+ }
834
+
835
+ function buildReviewSections(kind, carryLabel) {
836
+ if (kind === "nightly") {
837
+ return [
838
+ { heading: "## 今晚窗口", slot: "window" },
839
+ { heading: "## 今天最真实的推进", slot: "progress" },
840
+ { heading: "## 今天的消耗与摩擦", slot: "friction" },
841
+ { heading: "## 今晚还开着的线头", slot: "open-loops" },
842
+ { heading: `## ${carryLabel}`, slot: "carry-forward" },
843
+ { heading: "## 睡前收口摘录", slot: "closeout" },
844
+ { heading: "## 值得带走的信号", slot: "signals" },
845
+ { heading: "## Agent 判断", staticBody: "- " },
846
+ ];
847
+ }
848
+ return [
849
+ { heading: "## 本周期窗口", slot: "window" },
850
+ { heading: "## 这段时间最真实的推进", slot: "progress" },
851
+ { heading: "## 消耗与摩擦", slot: "friction" },
852
+ { heading: "## 还开着的线头", slot: "open-loops" },
853
+ { heading: `## ${carryLabel}`, slot: "carry-forward" },
854
+ { heading: "## 每天收口摘录", slot: "daily-summaries" },
855
+ { heading: "## 值得回看的补充记录", slot: "supplements" },
856
+ { heading: "## Agent 判断", staticBody: "- " },
857
+ ];
858
+ }
859
+
860
+ function normalizeReviewKind(value) {
861
+ const normalized = normalizeText(value).toLowerCase();
862
+ if (normalized === "nightly" || normalized === "weekly" || normalized === "monthly") {
863
+ return normalized;
864
+ }
865
+ throw new Error(`不支持的 review kind: ${value}`);
866
+ }
867
+
868
+ function normalizeTags(value, fallback) {
869
+ const tags = Array.isArray(value) ? value : fallback;
870
+ return tags
871
+ .map((tag) => normalizeText(tag))
872
+ .filter(Boolean);
873
+ }
874
+
875
+ function hasFrictionSignal(value) {
876
+ const normalized = normalizeText(value);
877
+ if (!normalized) {
878
+ return false;
879
+ }
880
+ return /(偏重|头痛|忘|烦|卡|累|耗|断开|岔开|拖|收不住|重复发送|截断|低电量|羞耻|分心)/u.test(normalized);
881
+ }
882
+
883
+ function looksLikeCarryForward(value) {
884
+ const normalized = normalizeText(value);
885
+ if (!normalized) {
886
+ return false;
887
+ }
888
+ return /^(明天|下周|下个月|后面|下一步|后续)/u.test(normalized);
889
+ }
890
+
891
+ function isReviewLeadIn(value) {
892
+ const normalized = normalizeText(value);
893
+ if (!normalized) {
894
+ return true;
895
+ }
896
+ return /^今天有\d+条主线[::]?$/u.test(normalized)
897
+ || /^[^::]{1,20}[::]$/u.test(normalized);
898
+ }
899
+
900
+ function isGeneratedFallbackLine(value) {
901
+ const normalized = normalizeText(value);
902
+ if (!normalized) {
903
+ return true;
904
+ }
905
+ return /还没有可用|还没有明显|没有明显还开着|没有明显的摩擦|下一次先从最小动作|明天先从最小动作|没有可引用的每日总结|没有值得回看的补充记录|睡前收口还没有写出来|还没有稳定到值得带走的信号/u.test(normalized);
906
+ }
907
+
908
+ function dedupeStatements(items) {
909
+ const seen = new Set();
910
+ const result = [];
911
+ for (const rawItem of Array.isArray(items) ? items : []) {
912
+ const item = normalizeLineItem(rawItem);
913
+ if (!item) {
914
+ continue;
915
+ }
916
+ const comparable = item.toLowerCase();
917
+ if (seen.has(comparable)) {
918
+ continue;
919
+ }
920
+ seen.add(comparable);
921
+ result.push(item);
922
+ }
923
+ return result;
924
+ }
925
+
926
+ function splitLines(body) {
927
+ return normalizeLineEnding(body)
928
+ .split("\n")
929
+ .map((line) => String(line || "").trim())
930
+ .filter(Boolean);
931
+ }
932
+
933
+ function truncateSentence(value, maxLength) {
934
+ const normalized = normalizeText(value);
935
+ if (!normalized || normalized.length <= maxLength) {
936
+ return normalized;
937
+ }
938
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).replace(/[,。;,;:\s]+$/u, "")}…`;
939
+ }
940
+
941
+ function toCompactSentence(value) {
942
+ return normalizeBody(value).replace(/\s*\n+\s*/gu, " ").replace(/\s{2,}/gu, " ").trim();
943
+ }
944
+
945
+ function selectWorkspaceProfile(workspaces, workspaceRoot) {
946
+ if (!workspaces || typeof workspaces !== "object") {
947
+ return {};
948
+ }
949
+ const normalizedWorkspaceRoot = normalizeDisplayPath(workspaceRoot);
950
+ for (const [candidateRoot, profile] of Object.entries(workspaces)) {
951
+ if (normalizeDisplayPath(candidateRoot) === normalizedWorkspaceRoot) {
952
+ return profile && typeof profile === "object" ? profile : {};
953
+ }
954
+ }
955
+ return {};
956
+ }
957
+
958
+ function resolveWorkspacePath(workspaceRoot, targetPath) {
959
+ if (path.isAbsolute(targetPath)) {
960
+ return normalizeDisplayPath(path.resolve(targetPath));
961
+ }
962
+ return normalizeDisplayPath(path.resolve(workspaceRoot, ...String(targetPath || "").split("/")));
963
+ }
964
+
965
+ function normalizeRelativeOrAbsolutePath(value) {
966
+ return normalizeText(value).replace(/\\/g, "/");
967
+ }
968
+
969
+ function createUtcDate(year, month, day) {
970
+ return new Date(Date.UTC(year, month - 1, day));
971
+ }
972
+
973
+ function addDays(date, offset) {
974
+ const next = new Date(date.getTime());
975
+ next.setUTCDate(next.getUTCDate() + offset);
976
+ return next;
977
+ }
978
+
979
+ function parseDateString(value) {
980
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(value);
981
+ if (!match) {
982
+ throw new Error(`日期格式应为 YYYY-MM-DD: ${value}`);
983
+ }
984
+ return createUtcDate(
985
+ Number.parseInt(match[1], 10),
986
+ Number.parseInt(match[2], 10),
987
+ Number.parseInt(match[3], 10)
988
+ );
989
+ }
990
+
991
+ function startOfIsoWeek(date) {
992
+ const day = date.getUTCDay() || 7;
993
+ return addDays(date, 1 - day);
994
+ }
995
+
996
+ function isoWeekNumber(date) {
997
+ const thursday = addDays(startOfIsoWeek(date), 3);
998
+ const firstThursday = addDays(startOfIsoWeek(createUtcDate(thursday.getUTCFullYear(), 1, 4)), 3);
999
+ return Math.round((thursday - firstThursday) / 604800000) + 1;
1000
+ }
1001
+
1002
+ function formatUtcDate(date) {
1003
+ const year = date.getUTCFullYear();
1004
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1005
+ const day = String(date.getUTCDate()).padStart(2, "0");
1006
+ return `${year}-${month}-${day}`;
1007
+ }
1008
+
1009
+ function getCurrentUtcDateInShanghai() {
1010
+ const now = new Date();
1011
+ const shanghai = new Intl.DateTimeFormat("en-CA", {
1012
+ timeZone: "Asia/Shanghai",
1013
+ year: "numeric",
1014
+ month: "2-digit",
1015
+ day: "2-digit",
1016
+ }).format(now);
1017
+ return parseDateString(shanghai);
1018
+ }
1019
+
1020
+ function formatDate(date) {
1021
+ return new Intl.DateTimeFormat("en-CA", {
1022
+ timeZone: "Asia/Shanghai",
1023
+ year: "numeric",
1024
+ month: "2-digit",
1025
+ day: "2-digit",
1026
+ }).format(date);
1027
+ }
1028
+
1029
+ function formatDateTime(date) {
1030
+ const formatter = new Intl.DateTimeFormat("sv-SE", {
1031
+ timeZone: "Asia/Shanghai",
1032
+ year: "numeric",
1033
+ month: "2-digit",
1034
+ day: "2-digit",
1035
+ hour: "2-digit",
1036
+ minute: "2-digit",
1037
+ hour12: false,
1038
+ });
1039
+ return formatter.format(date).replace(" ", "T");
1040
+ }
1041
+
1042
+ function normalizeBody(value) {
1043
+ return normalizeLineEnding(value).trim();
1044
+ }
1045
+
1046
+ function normalizeLineItem(value) {
1047
+ return normalizeBody(value).replace(/\s*\n+\s*/gu, " ").replace(/\s{2,}/gu, " ").trim();
1048
+ }
1049
+
1050
+ function normalizeLineEnding(value) {
1051
+ return String(value || "").replace(/\r\n/g, "\n");
1052
+ }
1053
+
1054
+ function normalizeText(value) {
1055
+ return typeof value === "string" ? value.trim() : "";
1056
+ }
1057
+
1058
+ function ensureTrailingNewline(value) {
1059
+ const normalized = normalizeLineEnding(value);
1060
+ return normalized.endsWith("\n") ? normalized : `${normalized}\n`;
1061
+ }
1062
+
1063
+ function normalizeDisplayPath(targetPath) {
1064
+ return normalizeText(targetPath).replace(/\\/g, "/");
1065
+ }
1066
+
1067
+ function escapeRegExp(value) {
1068
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1069
+ }
1070
+
1071
+ function formatErrorMessage(error) {
1072
+ return error instanceof Error ? error.message : String(error || "unknown error");
1073
+ }
1074
+
1075
+ module.exports = {
1076
+ buildReview,
1077
+ loadReviewSchemaConfig,
1078
+ resolveReviewProfile,
1079
+ resolveReviewWindow,
1080
+ writeReview,
1081
+ };