@bolloon/bolloon-agent 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +668 -204
- package/dist/web/index.html +24 -4
- package/dist/web/server.js +531 -10
- package/lefthook.yml +29 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +668 -204
- package/src/web/index.html +24 -4
- package/src/web/server.ts +596 -10
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* weekly-report.ts — Bolloon 每周表现报告 (阶段 B)
|
|
4
|
+
*
|
|
5
|
+
* 输入:
|
|
6
|
+
* ~/.bolloon/human-values/evolution.jsonl (接受/拒绝/回滚事件)
|
|
7
|
+
* ~/.bolloon/human-values/usage.jsonl (judgment 使用记录)
|
|
8
|
+
* ~/.bolloon/human-values/counterfactual-audit.jsonl (反事实审计)
|
|
9
|
+
* ~/.bolloon/human-values/judgments.json (当前 judgment 库)
|
|
10
|
+
* ~/.bolloon/self-improve-audit.log (改源码尝试的审计, 即使被拒)
|
|
11
|
+
*
|
|
12
|
+
* 输出:
|
|
13
|
+
* ~/.bolloon/reports/2026-W24.md (markdown 报告)
|
|
14
|
+
*
|
|
15
|
+
* 设计原则:
|
|
16
|
+
* - 纯本地计算 + 纯函数式分析 (不调 LLM, 避免幻觉)
|
|
17
|
+
* - 周范围默认 ISO 周(周一为起点), 可 --week 2026-W24 指定
|
|
18
|
+
* - 不写 judgments.json, 不动 persona, 仅追加 reports/*.md
|
|
19
|
+
* - 失败静默 + 退出码 != 0 让 cron 知道坏了
|
|
20
|
+
*
|
|
21
|
+
* 用法:
|
|
22
|
+
* tsx scripts/weekly-report.ts # 生成上周
|
|
23
|
+
* tsx scripts/weekly-report.ts --week 2026-W24 # 指定周
|
|
24
|
+
* tsx scripts/weekly-report.ts --week 2026-W24 --dry-run
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'fs/promises';
|
|
28
|
+
import * as os from 'os';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
|
|
31
|
+
const HOME = () => os.homedir() || process.env.HOME || '/tmp';
|
|
32
|
+
const ROOT = () => HOME() + '/.bolloon';
|
|
33
|
+
const REPORTS_DIR = () => ROOT() + '/reports';
|
|
34
|
+
|
|
35
|
+
const FILES = {
|
|
36
|
+
evolution: () => ROOT() + '/human-values/evolution.jsonl',
|
|
37
|
+
usage: () => ROOT() + '/human-values/usage.jsonl',
|
|
38
|
+
counterfactual: () => ROOT() + '/human-values/counterfactual-audit.jsonl',
|
|
39
|
+
judgments: () => ROOT() + '/human-values/judgments.json',
|
|
40
|
+
selfImproveAudit: () => ROOT() + '/self-improve-audit.log',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface EvolutionEntry {
|
|
44
|
+
ts: string;
|
|
45
|
+
action: 'accept' | 'reject' | 'revert';
|
|
46
|
+
suggestion: { kind: string; judgmentId: string; action: string };
|
|
47
|
+
appliedPatch?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
interface UsageEntry {
|
|
50
|
+
ts: string;
|
|
51
|
+
channelId: string | null;
|
|
52
|
+
userInputPreview: string;
|
|
53
|
+
usedIds: string[];
|
|
54
|
+
}
|
|
55
|
+
interface CounterfactualEntry {
|
|
56
|
+
ts: string;
|
|
57
|
+
trigger: { userInput: string; aiReply: string; violatedPrinciples: unknown[] };
|
|
58
|
+
verdict: string;
|
|
59
|
+
recomendaciones?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readJsonl<T>(p: string): Promise<T[]> {
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(p, 'utf-8');
|
|
65
|
+
return content
|
|
66
|
+
.trim()
|
|
67
|
+
.split('\n')
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map((l) => {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(l) as T;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.filter((e): e is T => Boolean(e));
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readJson<T>(p: string): Promise<T | null> {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(await fs.readFile(p, 'utf-8')) as T;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isoWeekString(d: Date): string {
|
|
91
|
+
// ISO week: 1 = 包含 1 月 4 日的那周
|
|
92
|
+
const target = new Date(d.valueOf());
|
|
93
|
+
const dayNr = (d.getUTCDay() + 6) % 7; // 周一=0
|
|
94
|
+
target.setUTCDate(target.getUTCDate() - dayNr + 3);
|
|
95
|
+
const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
|
|
96
|
+
const week =
|
|
97
|
+
1 +
|
|
98
|
+
Math.round(
|
|
99
|
+
((target.valueOf() - firstThursday.valueOf()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7
|
|
100
|
+
);
|
|
101
|
+
return `${target.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function weekRange(iso: string): { start: Date; end: Date } {
|
|
105
|
+
const m = /^(\d{4})-W(\d{1,2})$/.exec(iso);
|
|
106
|
+
if (!m) throw new Error(`bad ISO week: ${iso}`);
|
|
107
|
+
const year = Number(m[1]);
|
|
108
|
+
const week = Number(m[2]);
|
|
109
|
+
// ISO 周 1 是包含 1 月 4 日的那周
|
|
110
|
+
const jan4 = new Date(Date.UTC(year, 0, 4));
|
|
111
|
+
const jan4Day = (jan4.getUTCDay() + 6) % 7; // 周一=0
|
|
112
|
+
const week1Mon = new Date(jan4);
|
|
113
|
+
week1Mon.setUTCDate(jan4.getUTCDate() - jan4Day);
|
|
114
|
+
const start = new Date(week1Mon);
|
|
115
|
+
start.setUTCDate(week1Mon.getUTCDate() + (week - 1) * 7);
|
|
116
|
+
const end = new Date(start);
|
|
117
|
+
end.setUTCDate(start.getUTCDate() + 7);
|
|
118
|
+
return { start, end };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function inWeek(ts: string, start: Date, end: Date): boolean {
|
|
122
|
+
const t = new Date(ts).getTime();
|
|
123
|
+
return t >= start.getTime() && t < end.getTime();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pct(n: number, d: number): string {
|
|
127
|
+
if (d === 0) return 'n/a';
|
|
128
|
+
return `${((n / d) * 100).toFixed(1)}%`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function topN<T>(arr: T[], key: (x: T) => string, n: number): Array<[string, number]> {
|
|
132
|
+
const m = new Map<string, number>();
|
|
133
|
+
for (const x of arr) {
|
|
134
|
+
const k = key(x);
|
|
135
|
+
m.set(k, (m.get(k) || 0) + 1);
|
|
136
|
+
}
|
|
137
|
+
return Array.from(m.entries())
|
|
138
|
+
.sort((a, b) => b[1] - a[1])
|
|
139
|
+
.slice(0, n);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface Report {
|
|
143
|
+
week: string;
|
|
144
|
+
range: { start: string; end: string };
|
|
145
|
+
generatedAt: string;
|
|
146
|
+
summary: {
|
|
147
|
+
totalUsage: number;
|
|
148
|
+
uniqueChannels: number;
|
|
149
|
+
uniqueJudgments: number;
|
|
150
|
+
evolutionEvents: number;
|
|
151
|
+
acceptRate: number;
|
|
152
|
+
rejected: number;
|
|
153
|
+
reverted: number;
|
|
154
|
+
counterfactualScans: number;
|
|
155
|
+
};
|
|
156
|
+
topJudgments: Array<[string, number]>;
|
|
157
|
+
topKinds: Array<[string, number]>;
|
|
158
|
+
policyAudit: {
|
|
159
|
+
selfImproveAttempts: number;
|
|
160
|
+
blockedByPolicy: number;
|
|
161
|
+
note: string;
|
|
162
|
+
};
|
|
163
|
+
openQuestions: string[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function buildReport(weekIso: string): Promise<Report> {
|
|
167
|
+
const { start, end } = weekRange(weekIso);
|
|
168
|
+
|
|
169
|
+
const [evolution, usage, counterfactual, judgments, audit] = await Promise.all([
|
|
170
|
+
readJsonl<EvolutionEntry>(FILES.evolution()),
|
|
171
|
+
readJsonl<UsageEntry>(FILES.usage()),
|
|
172
|
+
readJsonl<CounterfactualEntry>(FILES.counterfactual()),
|
|
173
|
+
readJson<unknown[]>(FILES.judgments()),
|
|
174
|
+
(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const txt = await fs.readFile(FILES.selfImproveAudit(), 'utf-8');
|
|
177
|
+
return txt.split('\n').filter(Boolean);
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
})(),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const evoInWeek = evolution.filter((e) => inWeek(e.ts, start, end));
|
|
185
|
+
const useInWeek = usage.filter((u) => inWeek(u.ts, start, end));
|
|
186
|
+
const cfInWeek = counterfactual.filter((c) => inWeek(c.ts, start, end));
|
|
187
|
+
const auditInWeek = audit.filter((line) => {
|
|
188
|
+
// audit 是日志行, 格式不一定, 简单按日期前缀过滤
|
|
189
|
+
const d = line.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
190
|
+
if (!d) return false;
|
|
191
|
+
const ts = new Date(d[1] + 'T00:00:00Z').getTime();
|
|
192
|
+
return ts >= start.getTime() && ts < end.getTime();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const totalUsage = useInWeek.length;
|
|
196
|
+
const channels = new Set(useInWeek.map((u) => u.channelId || 'null'));
|
|
197
|
+
const ids = new Set<string>();
|
|
198
|
+
for (const u of useInWeek) for (const id of u.usedIds) ids.add(id);
|
|
199
|
+
|
|
200
|
+
const accepts = evoInWeek.filter((e) => e.action === 'accept').length;
|
|
201
|
+
const rejects = evoInWeek.filter((e) => e.action === 'reject').length;
|
|
202
|
+
const reverts = evoInWeek.filter((e) => e.action === 'revert').length;
|
|
203
|
+
const acceptRate = accepts + rejects === 0 ? 0 : accepts / (accepts + rejects);
|
|
204
|
+
|
|
205
|
+
// 自改审计 (路径白/黑名单拦截)
|
|
206
|
+
const selfImproveAttempts = auditInWeek.filter((l) => /attempt|尝试/i.test(l)).length;
|
|
207
|
+
const blockedByPolicy = auditInWeek.filter((l) => /block|deny|拒绝|denylist/i.test(l)).length;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
week: weekIso,
|
|
211
|
+
range: { start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) },
|
|
212
|
+
generatedAt: new Date().toISOString(),
|
|
213
|
+
summary: {
|
|
214
|
+
totalUsage,
|
|
215
|
+
uniqueChannels: channels.size,
|
|
216
|
+
uniqueJudgments: ids.size,
|
|
217
|
+
evolutionEvents: evoInWeek.length,
|
|
218
|
+
acceptRate,
|
|
219
|
+
rejected: rejects,
|
|
220
|
+
reverted: reverts,
|
|
221
|
+
counterfactualScans: cfInWeek.length,
|
|
222
|
+
},
|
|
223
|
+
topJudgments: topN(useInWeek.flatMap((u) => u.usedIds.map((id) => ({ id }))), (x) => x.id, 10),
|
|
224
|
+
topKinds: topN(evoInWeek, (e) => e.suggestion.kind, 5),
|
|
225
|
+
policyAudit: {
|
|
226
|
+
selfImproveAttempts,
|
|
227
|
+
blockedByPolicy,
|
|
228
|
+
note: selfImproveAttempts === 0 ? '本周无源码自改尝试(护栏未触发)' : '见 self-improve-audit.log',
|
|
229
|
+
},
|
|
230
|
+
openQuestions: openQuestions(evoInWeek, cfInWeek, useInWeek),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function openQuestions(evo: EvolutionEntry[], cf: CounterfactualEntry[], use: UsageEntry[]): string[] {
|
|
235
|
+
const out: string[] = [];
|
|
236
|
+
if (evo.length === 0 && use.length > 0) {
|
|
237
|
+
out.push('本周有使用但无自适应建议 → 可能 judgment 库太稳定, 或扫描器未触发');
|
|
238
|
+
}
|
|
239
|
+
if (use.length === 0) {
|
|
240
|
+
out.push('本周无 judgment 使用记录 → 检查 usage.jsonl 是否在写, 或渠道是否活跃');
|
|
241
|
+
}
|
|
242
|
+
if (cf.length > 0) {
|
|
243
|
+
const conflictCount = cf.filter((c) => /冲突|conflict|不合理/i.test(c.verdict)).length;
|
|
244
|
+
if (conflictCount > 0) {
|
|
245
|
+
out.push(`反事实审计发现 ${conflictCount} 条潜在冲突 → 看 counterfactual-audit.jsonl`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const reverts = evo.filter((e) => e.action === 'revert').length;
|
|
249
|
+
if (reverts >= 2) {
|
|
250
|
+
out.push(`本周回滚 ${reverts} 次 → 类 B 建议可能过激, 考虑收紧阈值`);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function toMarkdown(r: Report, totalJudgments: number): string {
|
|
256
|
+
const lines: string[] = [];
|
|
257
|
+
lines.push(`# 📊 Bolloon 周报 — ${r.week}`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push(`> 范围: ${r.range.start} → ${r.range.end} · 生成于 ${r.generatedAt}`);
|
|
260
|
+
lines.push('');
|
|
261
|
+
lines.push('## 核心数字');
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push('| 指标 | 本周 |');
|
|
264
|
+
lines.push('|------|------|');
|
|
265
|
+
lines.push(`| judgment 调用次数 | ${r.summary.totalUsage} |`);
|
|
266
|
+
lines.push(`| 触达渠道数 | ${r.summary.uniqueChannels} |`);
|
|
267
|
+
lines.push(`| 用到的不同 judgment 数 | ${r.summary.uniqueJudgments} |`);
|
|
268
|
+
lines.push(`| 自适应事件数 | ${r.summary.evolutionEvents} |`);
|
|
269
|
+
lines.push(`| 接受率 | ${pct(r.summary.acceptRate * 100, 100)} (${Math.round(r.summary.acceptRate * 100)}%) |`);
|
|
270
|
+
lines.push(`| 拒绝数 | ${r.summary.rejected} |`);
|
|
271
|
+
lines.push(`| 回滚数 | ${r.summary.reverted} |`);
|
|
272
|
+
lines.push(`| 反事实审计次数 | ${r.summary.counterfactualScans} |`);
|
|
273
|
+
lines.push(`| 当前 judgment 库总条数 | ${totalJudgments} |`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('## 最常被引用的 judgment');
|
|
276
|
+
lines.push('');
|
|
277
|
+
if (r.topJudgments.length === 0) {
|
|
278
|
+
lines.push('_(本周无引用)_');
|
|
279
|
+
} else {
|
|
280
|
+
for (const [id, n] of r.topJudgments) {
|
|
281
|
+
lines.push(`- \`${id}\` × ${n}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push('## 自适应建议类型分布');
|
|
286
|
+
lines.push('');
|
|
287
|
+
if (r.topKinds.length === 0) {
|
|
288
|
+
lines.push('_(本周无自适应事件)_');
|
|
289
|
+
} else {
|
|
290
|
+
for (const [kind, n] of r.topKinds) {
|
|
291
|
+
lines.push(`- **${kind}** × ${n}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push('## 护栏审计');
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push(`- 源码自改尝试: **${r.policyAudit.selfImproveAttempts}**`);
|
|
298
|
+
lines.push(`- 被策略拦截: **${r.policyAudit.blockedByPolicy}**`);
|
|
299
|
+
lines.push(`- ${r.policyAudit.note}`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push('## 关注事项');
|
|
302
|
+
lines.push('');
|
|
303
|
+
if (r.openQuestions.length === 0) {
|
|
304
|
+
lines.push('_(本周一切正常, 无特别关注)_');
|
|
305
|
+
} else {
|
|
306
|
+
for (const q of r.openQuestions) {
|
|
307
|
+
lines.push(`- ${q}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
lines.push('');
|
|
311
|
+
lines.push('---');
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push('> 本报告由 `scripts/weekly-report.ts` 纯本地生成, 不调 LLM. 数据源见 ~/.bolloon/human-values/');
|
|
314
|
+
lines.push('');
|
|
315
|
+
return lines.join('\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseArgs(argv: string[]): { week?: string; dryRun: boolean } {
|
|
319
|
+
const out: { week?: string; dryRun: boolean } = { dryRun: false };
|
|
320
|
+
for (let i = 0; i < argv.length; i++) {
|
|
321
|
+
if (argv[i] === '--week' && argv[i + 1]) {
|
|
322
|
+
out.week = argv[++i];
|
|
323
|
+
} else if (argv[i] === '--dry-run') {
|
|
324
|
+
out.dryRun = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main() {
|
|
331
|
+
const args = parseArgs(process.argv.slice(2));
|
|
332
|
+
// 默认: 上周(更合理 — 周末才回顾)
|
|
333
|
+
const now = new Date();
|
|
334
|
+
const lastWeek = new Date(now);
|
|
335
|
+
lastWeek.setUTCDate(now.getUTCDate() - 7);
|
|
336
|
+
const week = args.week || isoWeekString(lastWeek);
|
|
337
|
+
const { start, end } = weekRange(week);
|
|
338
|
+
|
|
339
|
+
console.log(`[weekly-report] 生成 ${week} (${start.toISOString().slice(0, 10)} → ${end.toISOString().slice(0, 10)})`);
|
|
340
|
+
|
|
341
|
+
const r = await buildReport(week);
|
|
342
|
+
const judgments = (await readJson<unknown[]>(FILES.judgments())) || [];
|
|
343
|
+
const md = toMarkdown(r, judgments.length);
|
|
344
|
+
|
|
345
|
+
const outDir = REPORTS_DIR();
|
|
346
|
+
const outPath = path.join(outDir, `${week}.md`);
|
|
347
|
+
|
|
348
|
+
if (args.dryRun) {
|
|
349
|
+
console.log(`[weekly-report] DRY-RUN, 不会写盘. 输出预览:`);
|
|
350
|
+
console.log('---');
|
|
351
|
+
console.log(md);
|
|
352
|
+
console.log('---');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
357
|
+
await fs.writeFile(outPath, md, 'utf-8');
|
|
358
|
+
console.log(`[weekly-report] ✅ 写入 ${outPath} (${md.length} bytes)`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
main().catch((err) => {
|
|
362
|
+
console.error('[weekly-report] ❌ 失败:', err);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
});
|