pi-skill-evolve 0.0.0-stage → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +81 -2
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
- package/extensions/skill-evolve/index.ts +300 -0
- package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
- package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
- package/extensions/skill-evolve/lib/apply.ts +141 -0
- package/extensions/skill-evolve/lib/attribution.ts +138 -0
- package/extensions/skill-evolve/lib/audit.ts +221 -0
- package/extensions/skill-evolve/lib/baseline.ts +176 -0
- package/extensions/skill-evolve/lib/child-env.ts +10 -0
- package/extensions/skill-evolve/lib/classify-action.ts +161 -0
- package/extensions/skill-evolve/lib/command.ts +100 -0
- package/extensions/skill-evolve/lib/config-command.ts +38 -0
- package/extensions/skill-evolve/lib/config.ts +194 -0
- package/extensions/skill-evolve/lib/detect.ts +62 -0
- package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
- package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
- package/extensions/skill-evolve/lib/dispatch.ts +300 -0
- package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
- package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
- package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
- package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
- package/extensions/skill-evolve/lib/evolution.ts +289 -0
- package/extensions/skill-evolve/lib/flags.ts +44 -0
- package/extensions/skill-evolve/lib/gate0.ts +78 -0
- package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
- package/extensions/skill-evolve/lib/lock.ts +215 -0
- package/extensions/skill-evolve/lib/logfile.ts +52 -0
- package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
- package/extensions/skill-evolve/lib/mechanical.ts +136 -0
- package/extensions/skill-evolve/lib/metrics.ts +172 -0
- package/extensions/skill-evolve/lib/model-config.ts +95 -0
- package/extensions/skill-evolve/lib/notify.ts +132 -0
- package/extensions/skill-evolve/lib/panel.ts +174 -0
- package/extensions/skill-evolve/lib/pending.ts +34 -0
- package/extensions/skill-evolve/lib/proc.ts +65 -0
- package/extensions/skill-evolve/lib/prompt.ts +80 -0
- package/extensions/skill-evolve/lib/proposal.ts +227 -0
- package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
- package/extensions/skill-evolve/lib/queue.ts +136 -0
- package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
- package/extensions/skill-evolve/lib/replay.ts +246 -0
- package/extensions/skill-evolve/lib/report.ts +181 -0
- package/extensions/skill-evolve/lib/review-post.ts +101 -0
- package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
- package/extensions/skill-evolve/lib/review-record.ts +63 -0
- package/extensions/skill-evolve/lib/review-run.ts +131 -0
- package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
- package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
- package/extensions/skill-evolve/lib/round.ts +116 -0
- package/extensions/skill-evolve/lib/run-watch.ts +34 -0
- package/extensions/skill-evolve/lib/runs.ts +208 -0
- package/extensions/skill-evolve/lib/selftest.ts +201 -0
- package/extensions/skill-evolve/lib/session-index.ts +145 -0
- package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
- package/extensions/skill-evolve/lib/single-flight.ts +111 -0
- package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
- package/extensions/skill-evolve/lib/stale.ts +217 -0
- package/extensions/skill-evolve/lib/telemetry.ts +205 -0
- package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
- package/extensions/skill-evolve/lib/web/actions.ts +299 -0
- package/extensions/skill-evolve/lib/web/activity.ts +57 -0
- package/extensions/skill-evolve/lib/web/applied.ts +64 -0
- package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
- package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
- package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
- package/extensions/skill-evolve/lib/web/data.ts +298 -0
- package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
- package/extensions/skill-evolve/lib/web/filter.ts +203 -0
- package/extensions/skill-evolve/lib/web/http.ts +68 -0
- package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
- package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
- package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
- package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
- package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
- package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
- package/extensions/skill-evolve/lib/web/render.ts +136 -0
- package/extensions/skill-evolve/lib/web/reports.ts +160 -0
- package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
- package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
- package/extensions/skill-evolve/lib/web/server.ts +268 -0
- package/extensions/skill-evolve/lib/web/shell.ts +87 -0
- package/extensions/skill-evolve/lib/web/skills.ts +202 -0
- package/extensions/skill-evolve/lib/web/style.ts +107 -0
- package/extensions/skill-evolve/lib/web/text.ts +52 -0
- package/extensions/skill-evolve/lib/web/trash.ts +36 -0
- package/extensions/skill-evolve/lib/weekly.ts +285 -0
- package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
- package/extensions/skill-evolve/scripts/replay.ts +93 -0
- package/extensions/skill-evolve/test/apply.test.ts +278 -0
- package/extensions/skill-evolve/test/approve.test.ts +248 -0
- package/extensions/skill-evolve/test/attribution.test.ts +133 -0
- package/extensions/skill-evolve/test/audit.test.ts +151 -0
- package/extensions/skill-evolve/test/baseline.test.ts +132 -0
- package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
- package/extensions/skill-evolve/test/command.test.ts +267 -0
- package/extensions/skill-evolve/test/config-command.test.ts +73 -0
- package/extensions/skill-evolve/test/config.test.ts +154 -0
- package/extensions/skill-evolve/test/detect.test.ts +71 -0
- package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
- package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
- package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
- package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
- package/extensions/skill-evolve/test/evolution.test.ts +378 -0
- package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
- package/extensions/skill-evolve/test/hard-rules.test.ts +116 -0
- package/extensions/skill-evolve/test/lock.test.ts +138 -0
- package/extensions/skill-evolve/test/logfile.test.ts +85 -0
- package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
- package/extensions/skill-evolve/test/metrics.test.ts +129 -0
- package/extensions/skill-evolve/test/model-config.test.ts +84 -0
- package/extensions/skill-evolve/test/notify.test.ts +133 -0
- package/extensions/skill-evolve/test/observe.test.ts +100 -0
- package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
- package/extensions/skill-evolve/test/panel.test.ts +115 -0
- package/extensions/skill-evolve/test/proposal.test.ts +159 -0
- package/extensions/skill-evolve/test/queue.test.ts +110 -0
- package/extensions/skill-evolve/test/reject.test.ts +134 -0
- package/extensions/skill-evolve/test/replay.test.ts +111 -0
- package/extensions/skill-evolve/test/report.test.ts +66 -0
- package/extensions/skill-evolve/test/review-post.test.ts +92 -0
- package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
- package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
- package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
- package/extensions/skill-evolve/test/selftest.test.ts +113 -0
- package/extensions/skill-evolve/test/session-index.test.ts +128 -0
- package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
- package/extensions/skill-evolve/test/stale.test.ts +272 -0
- package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
- package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
- package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
- package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
- package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
- package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
- package/extensions/skill-evolve/test/web-data.test.ts +420 -0
- package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
- package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
- package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
- package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
- package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
- package/extensions/skill-evolve/test/web-render.test.ts +300 -0
- package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
- package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
- package/extensions/skill-evolve/test/web-server.test.ts +465 -0
- package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
- package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
- package/extensions/skill-evolve/test/weekly.test.ts +123 -0
- package/package.json +34 -4
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能管理页(`?view=skills`)与技能详情页(`?view=skill&name=...`)的数据聚合。
|
|
3
|
+
*
|
|
4
|
+
* 「技能改进」这件事有两个真相来源,这里把它们并为一行:
|
|
5
|
+
* - `.pi/skills/*\/SKILL.md` 目录 = **有哪些技能**(目录是权威,不是遥测里出现过的名字);
|
|
6
|
+
* - `.pi/evolution/telemetry/*.jsonl` = **曝光与加载**(advertised / loaded 事件,默认看最近 30 天)。
|
|
7
|
+
*
|
|
8
|
+
* 三个状态标签**不合并**(用户明确要求):有待批提案 / 存量违规 / 缺 .cursor stub。
|
|
9
|
+
* 合成就得二选一显示,而这三件事的处置动作完全不同(去批 / 去改 / 去补 stub)。
|
|
10
|
+
*
|
|
11
|
+
* 只读:这里只读目录与 jsonl,不写任何文件。
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { baselinePath, readBaseline } from "../baseline.ts";
|
|
16
|
+
import { listSkillDirs } from "../hard-rules.ts";
|
|
17
|
+
import { readQueue } from "../queue.ts";
|
|
18
|
+
import { listRuns } from "../runs.ts";
|
|
19
|
+
import { readEvents, ratio, type TelemetryEvent } from "../telemetry.ts";
|
|
20
|
+
import { asRecord, asText } from "./json-read.ts";
|
|
21
|
+
|
|
22
|
+
/** 允许的统计窗口(白名单):URL 上的 `?days=` 只认这三个值,其余回落默认 */
|
|
23
|
+
export const SKILL_DAYS = [7, 30, 90] as const;
|
|
24
|
+
export const DEFAULT_SKILL_DAYS = 30;
|
|
25
|
+
/** 详情页要列历史提案,扫的 run 数比列表页多:一个技能一年可能被改几十次 */
|
|
26
|
+
const RUN_SCAN_LIMIT = 200;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 允许没有 `.cursor/skills/<名>/SKILL.md` 的路由 stub 的技能。
|
|
30
|
+
* 依据 AGENTS.md 明文例外:描述 pi 自身机制的技能不加 Cursor 路由(Cursor 没有对应物,加了是误导)。
|
|
31
|
+
*/
|
|
32
|
+
const CURSOR_STUB_EXEMPT = new Set(["nested-agents-md", "pi-deepseek-search"]);
|
|
33
|
+
|
|
34
|
+
export interface SkillRow {
|
|
35
|
+
name: string;
|
|
36
|
+
/** 曝光:最近 N 天里「含这个技能的 advertised 轮数」(同一轮重复只算一次,disabled 不计) */
|
|
37
|
+
exposed: number;
|
|
38
|
+
/** 加载:loaded 事件数(`ok === false` 不计) */
|
|
39
|
+
loaded: number;
|
|
40
|
+
/** 加载率:loaded / exposed;exposed 为 0 时 null(页面写「—」,不能写 0%) */
|
|
41
|
+
rate: number | null;
|
|
42
|
+
/** 最近一次相关 run(proposal.skill === name 的最新一条);没有就是 null */
|
|
43
|
+
lastRunId: string | null;
|
|
44
|
+
lastRunAt: number | null;
|
|
45
|
+
/** 状态标签一:队列里有 pending 且是它的提案 */
|
|
46
|
+
hasPendingProposal: boolean;
|
|
47
|
+
/** 状态标签二:baseline.json 里有它一条违规 */
|
|
48
|
+
hasBaseline: boolean;
|
|
49
|
+
/** 状态标签三:`.cursor/skills/<名>/SKILL.md` 不存在(豁免名单除外) */
|
|
50
|
+
missingCursorStub: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SkillsView {
|
|
54
|
+
days: number;
|
|
55
|
+
rows: SkillRow[];
|
|
56
|
+
/** 采集开关:关着时页面要写「没有遥测数据(采集关着)」而不是一屏 0 */
|
|
57
|
+
telemetryOn: boolean;
|
|
58
|
+
/** 窗口内遥测事件总数(0 = 采集开着但没数据,与「关着」是两种空态) */
|
|
59
|
+
events: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SkillRunRow {
|
|
63
|
+
runId: string;
|
|
64
|
+
status: string;
|
|
65
|
+
at: number | null;
|
|
66
|
+
type: string;
|
|
67
|
+
added: number;
|
|
68
|
+
removed: number;
|
|
69
|
+
verdict: string;
|
|
70
|
+
action: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SkillDetail {
|
|
74
|
+
name: string;
|
|
75
|
+
days: number;
|
|
76
|
+
row: SkillRow | null;
|
|
77
|
+
runs: SkillRunRow[];
|
|
78
|
+
/** 采集总开关与窗口内事件数:关着且没事件时,曝光/加载是「不知道」而不是 0 */
|
|
79
|
+
telemetryOn: boolean;
|
|
80
|
+
events: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 工作区根:约定 evolutionDir 恒为 `<cwd>/.pi/evolution`,所以往上两层就是仓库根 */
|
|
84
|
+
export function workspaceRoot(evolutionDir: string): string {
|
|
85
|
+
return path.dirname(path.dirname(path.resolve(evolutionDir)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasFile(file: string): boolean {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(file).isFile();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 曝光按「轮」算:同一轮 advertised 里同名的技能只计一次(disabled 的不算曝光) */
|
|
97
|
+
function countExposure(events: readonly TelemetryEvent[]): Map<string, number> {
|
|
98
|
+
const exposure = new Map<string, number>();
|
|
99
|
+
for (const event of events) {
|
|
100
|
+
if (asText(event.kind) !== "advertised") continue;
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
for (const item of Array.isArray(event.skills) ? event.skills : []) {
|
|
103
|
+
const record = asRecord(item);
|
|
104
|
+
if (!record || record.disabled === true) continue;
|
|
105
|
+
const name = asText(record.name);
|
|
106
|
+
if (name.length > 0) seen.add(name);
|
|
107
|
+
}
|
|
108
|
+
for (const name of seen) exposure.set(name, (exposure.get(name) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
return exposure;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function countLoaded(events: readonly TelemetryEvent[]): Map<string, number> {
|
|
114
|
+
const loaded = new Map<string, number>();
|
|
115
|
+
for (const event of events) {
|
|
116
|
+
if (asText(event.kind) !== "loaded") continue;
|
|
117
|
+
if (event.ok === false) continue; // 读失败不算加载
|
|
118
|
+
const name = asText(event.skill);
|
|
119
|
+
if (name.length > 0) loaded.set(name, (loaded.get(name) ?? 0) + 1);
|
|
120
|
+
}
|
|
121
|
+
return loaded;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SkillsOptions {
|
|
125
|
+
/** 统计窗口天数,只接受 SKILL_DAYS 里的值 */
|
|
126
|
+
days?: number;
|
|
127
|
+
now?: number;
|
|
128
|
+
telemetryOn?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 归一窗口:非法值(含 `?days=9999`)回落 30,避免有人用 URL 拉全量历史 */
|
|
132
|
+
export function parseDays(raw: string | null): number {
|
|
133
|
+
const value = Number(raw);
|
|
134
|
+
return (SKILL_DAYS as readonly number[]).includes(value) ? value : DEFAULT_SKILL_DAYS;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 技能列表:目录为准,遥测补曝光/加载,队列/baseline/cursor 补三个标签 */
|
|
138
|
+
export function readSkills(evolutionDir: string, options: SkillsOptions = {}): SkillsView {
|
|
139
|
+
const days = options.days ?? DEFAULT_SKILL_DAYS;
|
|
140
|
+
const now = options.now ?? Date.now();
|
|
141
|
+
const root = workspaceRoot(evolutionDir);
|
|
142
|
+
const events = readEvents(path.join(evolutionDir, "telemetry"), days, now);
|
|
143
|
+
const exposure = countExposure(events);
|
|
144
|
+
const loaded = countLoaded(events);
|
|
145
|
+
const pending = new Set(readQueue(evolutionDir).filter((item) => item.status === "pending").map((item) => item.skill));
|
|
146
|
+
const baseline = readBaseline(baselinePath(evolutionDir));
|
|
147
|
+
const offending = new Set((baseline?.violations ?? []).map((item) => item.skill));
|
|
148
|
+
// 最近一次相关 run:listRuns 已按派生时间倒序,第一个命中的就是最新的
|
|
149
|
+
const lastRun = new Map<string, { runId: string; at: number | null }>();
|
|
150
|
+
for (const run of listRuns(evolutionDir, RUN_SCAN_LIMIT)) {
|
|
151
|
+
const skill = run.proposal?.skill;
|
|
152
|
+
if (!skill || lastRun.has(skill)) continue;
|
|
153
|
+
lastRun.set(skill, { runId: run.runId, at: run.dispatchedAt });
|
|
154
|
+
}
|
|
155
|
+
const rows: SkillRow[] = listSkillDirs(path.join(root, ".pi", "skills")).map((name) => {
|
|
156
|
+
const exposed = exposure.get(name) ?? 0;
|
|
157
|
+
const loadCount = loaded.get(name) ?? 0;
|
|
158
|
+
return {
|
|
159
|
+
name,
|
|
160
|
+
exposed,
|
|
161
|
+
loaded: loadCount,
|
|
162
|
+
rate: exposed === 0 ? null : ratio(loadCount, exposed),
|
|
163
|
+
lastRunId: lastRun.get(name)?.runId ?? null,
|
|
164
|
+
lastRunAt: lastRun.get(name)?.at ?? null,
|
|
165
|
+
hasPendingProposal: pending.has(name),
|
|
166
|
+
hasBaseline: offending.has(name),
|
|
167
|
+
missingCursorStub: !CURSOR_STUB_EXEMPT.has(name) && !hasFile(path.join(root, ".cursor", "skills", name, "SKILL.md")),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
return { days, rows, telemetryOn: options.telemetryOn === true, events: events.length };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 技能详情:这一行的统计 + 它所有历史提案(每个 run 一行,带 verdict 与 diff 链接) */
|
|
174
|
+
export function readSkillDetail(
|
|
175
|
+
evolutionDir: string,
|
|
176
|
+
name: string,
|
|
177
|
+
options: SkillsOptions = {},
|
|
178
|
+
): SkillDetail {
|
|
179
|
+
const view = readSkills(evolutionDir, options);
|
|
180
|
+
const runs: SkillRunRow[] = [];
|
|
181
|
+
for (const run of listRuns(evolutionDir, RUN_SCAN_LIMIT)) {
|
|
182
|
+
if (run.proposal?.skill !== name) continue;
|
|
183
|
+
runs.push({
|
|
184
|
+
runId: run.runId,
|
|
185
|
+
status: run.status,
|
|
186
|
+
at: run.dispatchedAt,
|
|
187
|
+
type: run.proposal.type,
|
|
188
|
+
added: run.proposal.added,
|
|
189
|
+
removed: run.proposal.removed,
|
|
190
|
+
verdict: run.review?.verdict ?? "—",
|
|
191
|
+
action: run.review?.action ?? "—",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
name,
|
|
196
|
+
days: view.days,
|
|
197
|
+
row: view.rows.find((row) => row.name === name) ?? null,
|
|
198
|
+
runs,
|
|
199
|
+
telemetryOn: view.telemetryOn,
|
|
200
|
+
events: view.events,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板的内联样式:单文件放着是因为只有一处消费(render.ts),拆出来只是为了把 render.ts 压在 300 行内。
|
|
3
|
+
* 不引外部 CSS:面板必须离线可用,一条网络请求都不发。
|
|
4
|
+
*/
|
|
5
|
+
export const STYLE = `
|
|
6
|
+
:root { color-scheme: dark }
|
|
7
|
+
* { box-sizing: border-box }
|
|
8
|
+
body { margin: 0; padding: 24px; background: #0f1115; color: #d7dce5;
|
|
9
|
+
font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace }
|
|
10
|
+
h1 { margin: 0 0 4px; font-size: 18px; font-weight: 600 }
|
|
11
|
+
.bar { margin: 0 0 16px; color: #8d99ae }
|
|
12
|
+
.bar a { color: #7cc4ff; text-decoration: none }
|
|
13
|
+
.notice { margin: 0 0 12px; padding: 8px 12px; border-left: 2px solid #3f9d5a; background: #131a17; color: #b6e2c4 }
|
|
14
|
+
.notice.err { border-left-color: #c05c5c; background: #1a1313; color: #f0bdbd }
|
|
15
|
+
nav { display: flex; gap: 12px; margin-bottom: 16px; border-bottom: 1px solid #232733; padding-bottom: 8px }
|
|
16
|
+
nav a { color: #8d99ae; text-decoration: none; padding: 2px 8px; border-radius: 4px }
|
|
17
|
+
nav a:hover { background: #1a1f29 }
|
|
18
|
+
nav a.active { color: #0f1115; background: #7cc4ff }
|
|
19
|
+
ul { list-style: none; margin: 0; padding: 0 }
|
|
20
|
+
li.row { border-bottom: 1px solid #1c202a; padding: 8px 0 }
|
|
21
|
+
summary.head { cursor: pointer; color: #cfe3ff }
|
|
22
|
+
span.head { color: #cfe3ff }
|
|
23
|
+
pre { margin: 8px 0 0; padding: 12px; background: #141821; border-left: 2px solid #2b3342;
|
|
24
|
+
white-space: pre-wrap; word-break: break-word; color: #aeb7c4 }
|
|
25
|
+
.empty { color: #6f7a8c; padding: 24px 0 }
|
|
26
|
+
.acts { display: flex; gap: 8px; margin-top: 8px }
|
|
27
|
+
.acts form { display: inline; margin: 0 }
|
|
28
|
+
button { font: inherit; color: #0f1115; background: #7cc4ff; border: 0; border-radius: 4px; padding: 2px 12px; cursor: pointer }
|
|
29
|
+
button.reject { background: #6f7a8c; color: #0f1115 }
|
|
30
|
+
button.danger { background: #c05c5c; color: #fff }
|
|
31
|
+
button:hover { filter: brightness(1.1) }
|
|
32
|
+
/* 顶部控制条:开关状态、切换/立即复盘按钮、最后活动 */
|
|
33
|
+
.ctl { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
34
|
+
margin: 0 0 16px; padding: 8px 12px; background: #141821; border: 1px solid #232733; border-radius: 6px }
|
|
35
|
+
.ctl form { display: inline; margin: 0 }
|
|
36
|
+
.ctl .state { padding: 1px 8px; border-radius: 4px }
|
|
37
|
+
.ctl .state.on { background: #16301f; color: #b6e2c4 }
|
|
38
|
+
.ctl .state.off { background: #2a1f1f; color: #e6b0b0 }
|
|
39
|
+
.ctl .act { color: #8d99ae }
|
|
40
|
+
/* 单个开关:状态 + 后果说明 + 按钮竖排,两个开关之间靠 .ctl 的 gap 分隔 */
|
|
41
|
+
.ctl .sw { display: inline-flex; align-items: center; gap: 6px }
|
|
42
|
+
.ctl .hint { color: #8d99ae; font-size: 12px }
|
|
43
|
+
.warn { margin: 6px 0 0; padding: 6px 10px; border-left: 3px solid #b58900;
|
|
44
|
+
background: #2a2418; color: #e8d9a8 }
|
|
45
|
+
/* 筛选表单与翻页:零 JS,样式也要看得出它们是一组 */
|
|
46
|
+
.filter { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
47
|
+
margin: 0 0 12px; padding: 8px 12px; background: #141821; border: 1px solid #232733; border-radius: 6px }
|
|
48
|
+
.filter label { color: #8d99ae }
|
|
49
|
+
.filter input, .filter select { font: inherit; color: #d7dce5; background: #0f1115;
|
|
50
|
+
border: 1px solid #2b3342; border-radius: 4px; padding: 1px 6px }
|
|
51
|
+
.page { display: flex; gap: 12px; align-items: center; margin: 12px 0 0; color: #8d99ae }
|
|
52
|
+
.page a { color: #7cc4ff; text-decoration: none }
|
|
53
|
+
.page .count { color: #6f7a8c }
|
|
54
|
+
.off { color: #3f4654 }
|
|
55
|
+
a.btn { color: #7cc4ff; text-decoration: none; padding: 2px 10px; border: 1px solid #2b3342; border-radius: 4px }
|
|
56
|
+
a.btn:hover { background: #1a1f29 }
|
|
57
|
+
/* 最近操作:结果分三色,ok / partial / error 扫一眼就能分开 */
|
|
58
|
+
.audit { margin-top: 24px; padding-top: 12px; border-top: 1px solid #232733 }
|
|
59
|
+
.audit h2 { margin: 0 0 8px; font-size: 14px; font-weight: 600; color: #8d99ae }
|
|
60
|
+
.audit li { padding: 2px 0; color: #aeb7c4 }
|
|
61
|
+
.audit .when { color: #6f7a8c }
|
|
62
|
+
.audit code { font: inherit; color: #cfe3ff }
|
|
63
|
+
.ok { color: #7fc78f }
|
|
64
|
+
.partial { color: #d6b25e }
|
|
65
|
+
.fail { color: #e08585 }
|
|
66
|
+
/* 表格:评审 blocking / 五闸 / 发现 / 技能管理都用同一套,避免每处各写一份颜色 */
|
|
67
|
+
.tbl { border-collapse: collapse; width: 100%; margin: 8px 0 0; font-size: 13px }
|
|
68
|
+
.tbl th, .tbl td { border-bottom: 1px solid #1c202a; padding: 4px 8px; text-align: left; vertical-align: top }
|
|
69
|
+
.tbl th { color: #8d99ae; font-weight: 600 }
|
|
70
|
+
.tbl a { color: #7cc4ff; text-decoration: none }
|
|
71
|
+
/* 状态标签:三个标签三色,合成一个就分不出该去批、去改还是去补 stub */
|
|
72
|
+
.tag { display: inline-block; padding: 0 6px; border-radius: 4px; background: #1a1f29; color: #aeb7c4; font-size: 12px }
|
|
73
|
+
.tag.warn { background: #2a2418; color: #e8d9a8 }
|
|
74
|
+
.tag.fail { background: #2a1616; color: #f0bdbd }
|
|
75
|
+
.hint { color: #8d99ae; font-size: 12px }
|
|
76
|
+
.days { margin: 0 0 8px; color: #8d99ae }
|
|
77
|
+
.days a { color: #7cc4ff; text-decoration: none }
|
|
78
|
+
.days .on { color: #d7dce5; padding: 0 6px; background: #1a1f29; border-radius: 4px }
|
|
79
|
+
/* run 详情页的概要:左标签右值,两列对齐才好扫 */
|
|
80
|
+
.kv { display: grid; grid-template-columns: 6em 1fr; gap: 2px 12px; margin: 8px 0 0 }
|
|
81
|
+
.kv-row { display: contents }
|
|
82
|
+
.kv .k { color: #8d99ae }
|
|
83
|
+
.kv .v { color: #d7dce5 }
|
|
84
|
+
.kv-line { margin: 4px 0 }
|
|
85
|
+
.kv .v code { font: inherit; color: #cfe3ff }
|
|
86
|
+
a.run { color: #cfe3ff; text-decoration: none; border-bottom: 1px dashed #3f4654 }
|
|
87
|
+
a.run:hover { border-bottom-color: #7cc4ff }
|
|
88
|
+
h2 { margin: 20px 0 6px; font-size: 15px; font-weight: 600; color: #cfe3ff }
|
|
89
|
+
/* diff:文件头灰底、hunk 灰字、+ 绿底、- 红底;行号两列宽固定,不随内容抹动 */
|
|
90
|
+
.diff { margin: 8px 0 0; padding: 8px 0; background: #0c0e12; border: 1px solid #232733; border-radius: 6px;
|
|
91
|
+
overflow-x: auto; white-space: pre; font-size: 12.5px; line-height: 1.5 }
|
|
92
|
+
.diff .dl { display: block; padding: 0 8px }
|
|
93
|
+
/* 长行必须横向滚动而不是被裁掉:技能 description 动辄两三百字,截断等于看不见改了什么 */
|
|
94
|
+
.diff { overflow-x: auto }
|
|
95
|
+
.diff .dl { min-width: max-content }
|
|
96
|
+
.diff .ln { display: inline-block; width: 4.5em; color: #4a5364; text-align: right; padding-right: 8px; user-select: none }
|
|
97
|
+
.diff .tx { white-space: pre }
|
|
98
|
+
.diff .file { background: #22262f; color: #b8c1d1 }
|
|
99
|
+
.diff .hunk { color: #8d99ae }
|
|
100
|
+
.diff .add { background: #12281a; color: #b7e7c5 }
|
|
101
|
+
.diff .del { background: #2a1616; color: #f0bdbd }
|
|
102
|
+
.diff .meta { color: #6f7a8c }
|
|
103
|
+
/* 改后全文:长文件限高内部滚动,别把后面的评审/五闸顶到屏幕外 */
|
|
104
|
+
.candidate { max-height: 480px; overflow: auto }
|
|
105
|
+
footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid #232733; color: #6f7a8c }
|
|
106
|
+
footer a { color: #7cc4ff }
|
|
107
|
+
`.trim();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板上的文本格式化:转义、时间戳、短 runId。
|
|
3
|
+
*
|
|
4
|
+
* 单独一份是因为渲染层的三个文件(骨架 / 列表行 / 控制条)都要用它们,
|
|
5
|
+
* 放在其中一个里会变成「A 引 B、B 又引 A」的循环。
|
|
6
|
+
*
|
|
7
|
+
* 为什么全转义:页面里的每个字都来自 run 产物与技能文件,而复盘内容本来就是「模型写的自由文本」
|
|
8
|
+
* —— 一条 findings 里出现 `<script>` 完全可能。只要漏一个 `escapeHtml`,面板就成了本地 XSS 的跳板
|
|
9
|
+
* (面板虽只绑 127.0.0.1,但同一台机器上的任意进程/浏览器标签都能打到它)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 五个字符全部要转:`'` 与 `"` 用于属性值,`&` 必须先转,否则实体会被二次解码 */
|
|
13
|
+
export function escapeHtml(text: string): string {
|
|
14
|
+
return text.replace(/[&<>"']/g, (char) => {
|
|
15
|
+
switch (char) {
|
|
16
|
+
case "&":
|
|
17
|
+
return "&";
|
|
18
|
+
case "<":
|
|
19
|
+
return "<";
|
|
20
|
+
case ">":
|
|
21
|
+
return ">";
|
|
22
|
+
case '"':
|
|
23
|
+
return """;
|
|
24
|
+
default:
|
|
25
|
+
return "'";
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 本地时间 `YYYY-MM-DD HH:mm`:面板上全是同一时区的人在看,不折腾 ISO */
|
|
31
|
+
export function formatStamp(ts: number | null): string {
|
|
32
|
+
if (ts === null || !Number.isFinite(ts)) return "—";
|
|
33
|
+
const date = new Date(ts);
|
|
34
|
+
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
35
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 列表里的短 runId:同一会话的 runId 都是 `<sessionHash8>-<时间戳>`,前缀一模一样,
|
|
40
|
+
* 全量打印会变成一列看不出区别的字符串。保留前 8 + 后 4,完整值放详情里。
|
|
41
|
+
*/
|
|
42
|
+
export function shortRunId(runId: string): string {
|
|
43
|
+
return runId.length > 16 && runId.includes("-") ? `${runId.slice(0, 8)}…${runId.slice(-4)}` : runId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 详情第一行永远是完整 runId:列表上显示的是缩写,复制要靠这一行。
|
|
48
|
+
* 文案里带 `runId:` 前缀是因为详情区是纯文本,没有别的办法标出这一行是什么。
|
|
49
|
+
*/
|
|
50
|
+
export function withId(runId: string, detail: string): string {
|
|
51
|
+
return detail.length > 0 ? `runId:${runId}\n${detail}` : `runId:${runId}`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「清理记录」的落地:把 `runs/<runId>/` 移到 `runs/.trash/<runId>-<时间戳>/`。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是移动而不是删除:清理只是把卡死的 run 从列表里挪走,数据必须还能捞回来 ——
|
|
5
|
+
* 复盘目录里有会话原文、findings、候选稿,删掉就真没了。`.trash/` 已在 .gitignore 里,
|
|
6
|
+
* 要腾空间时由人手动 rm,不由一次网页点击决定。
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { resolveRunDir } from "./run-path.ts";
|
|
11
|
+
|
|
12
|
+
export const TRASH_DIR = ".trash";
|
|
13
|
+
|
|
14
|
+
/** 目录名里的时间戳:人读得懂,且同一 run 多次清理不会撞名 */
|
|
15
|
+
function stamp(ts: number): string {
|
|
16
|
+
const date = new Date(ts);
|
|
17
|
+
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
18
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 移动成功返回一句人话(带相对路径,方便人去捞)。
|
|
23
|
+
* 失败抛 Error:所有「不该发生」的分支都在这里挡住,绝不静默跳过 —— 静默跳过会让人以为清掉了,其实还在列表里。
|
|
24
|
+
*/
|
|
25
|
+
export function moveRunToTrash(evolutionDir: string, runId: string, now = Date.now()): string {
|
|
26
|
+
const runDir = resolveRunDir(evolutionDir, runId);
|
|
27
|
+
if (runDir === null) throw new Error(`runId 不合法:${runId}`);
|
|
28
|
+
if (!fs.existsSync(runDir)) throw new Error(`找不到 run 目录:runs/${runId}`);
|
|
29
|
+
const trashRoot = path.join(evolutionDir, "runs", TRASH_DIR);
|
|
30
|
+
fs.mkdirSync(trashRoot, { recursive: true });
|
|
31
|
+
const target = path.join(trashRoot, `${runId}-${stamp(now)}`);
|
|
32
|
+
// rename 到已存在的目录会在部分平台上静默合并,所以先显式拒绝(同一秒内点两次就会碰到)
|
|
33
|
+
if (fs.existsSync(target)) throw new Error(`目标已存在,未移动:runs/${TRASH_DIR}/${path.basename(target)}`);
|
|
34
|
+
fs.renameSync(runDir, target);
|
|
35
|
+
return `已清理 ${runId}:移动到 runs/${TRASH_DIR}/${path.basename(target)}(可逆,没有删数据)`;
|
|
36
|
+
}
|