@andyqiu/codeforge 0.6.12 → 0.7.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/agents/codeforge.md +3 -3
- package/bin/codeforge.mjs +17 -127
- package/codeforge.config.yaml +0 -1
- package/codeforge.json +1 -1
- package/commands/parallel.md +1 -1
- package/dist/index.js +1716 -2198
- package/install.mjs +2 -15
- package/package.json +1 -12
- package/workflows/feature-dev.yaml +0 -3
package/dist/index.js
CHANGED
|
@@ -34,15 +34,15 @@ __export(exports_worktree_ops, {
|
|
|
34
34
|
commitWorktreeIfDirty: () => commitWorktreeIfDirty
|
|
35
35
|
});
|
|
36
36
|
import { execFile as execFile2 } from "node:child_process";
|
|
37
|
-
import { promises as
|
|
38
|
-
import * as
|
|
37
|
+
import { promises as fs7 } from "node:fs";
|
|
38
|
+
import * as path9 from "node:path";
|
|
39
39
|
async function ensureWorktree(opts) {
|
|
40
|
-
const root =
|
|
41
|
-
const dir = opts.worktrees_dir ??
|
|
42
|
-
await
|
|
43
|
-
const wtPath =
|
|
40
|
+
const root = path9.resolve(opts.root);
|
|
41
|
+
const dir = opts.worktrees_dir ?? path9.join(root, ".git", "codeforge-worktrees");
|
|
42
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
43
|
+
const wtPath = path9.join(dir, sanitizeBranch(opts.branch));
|
|
44
44
|
try {
|
|
45
|
-
const stat = await
|
|
45
|
+
const stat = await fs7.stat(wtPath);
|
|
46
46
|
if (stat.isDirectory()) {
|
|
47
47
|
return { path: wtPath, branch: opts.branch, created: false };
|
|
48
48
|
}
|
|
@@ -76,7 +76,7 @@ function isWorktreeAbsentError(err) {
|
|
|
76
76
|
return /is not a working tree/i.test(msg) || /No such file or directory/i.test(msg) || /not a valid path/i.test(msg);
|
|
77
77
|
}
|
|
78
78
|
async function deleteBranchIfExists(opts) {
|
|
79
|
-
const root =
|
|
79
|
+
const root = path9.resolve(opts.root);
|
|
80
80
|
const timeout = opts.git_timeout_ms ?? 5000;
|
|
81
81
|
try {
|
|
82
82
|
await runGit(root, ["rev-parse", "--verify", `refs/heads/${opts.branch}`], 3000);
|
|
@@ -133,7 +133,7 @@ function sanitizeBranch(name) {
|
|
|
133
133
|
return name.replace(/[/\\:*?"<>|]/g, "-");
|
|
134
134
|
}
|
|
135
135
|
function runGitAllowFail(cwd, args, timeout) {
|
|
136
|
-
return new Promise((
|
|
136
|
+
return new Promise((resolve9, reject) => {
|
|
137
137
|
execFile2("git", args, { cwd, timeout, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
138
138
|
if (err) {
|
|
139
139
|
const errCode = err.code;
|
|
@@ -141,30 +141,30 @@ function runGitAllowFail(cwd, args, timeout) {
|
|
|
141
141
|
reject(new Error(`git ${args.join(" ")} 失败: ${stderr || err.message}`));
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
resolve9({
|
|
145
145
|
code: typeof errCode === "number" ? errCode : 1,
|
|
146
146
|
stdout: stdout ?? "",
|
|
147
147
|
stderr: stderr ?? err.message ?? ""
|
|
148
148
|
});
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
resolve9({ code: 0, stdout, stderr: stderr ?? "" });
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
function runGit(cwd, args, timeout) {
|
|
156
|
-
return new Promise((
|
|
156
|
+
return new Promise((resolve9, reject) => {
|
|
157
157
|
execFile2("git", args, { cwd, timeout, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
158
158
|
if (err) {
|
|
159
159
|
reject(new Error(`git ${args.join(" ")} 失败: ${stderr || err.message}`));
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
resolve9(stdout);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
async function tryMerge(opts) {
|
|
167
|
-
const root =
|
|
167
|
+
const root = path9.resolve(opts.root);
|
|
168
168
|
const timeout = opts.git_timeout_ms ?? 1e4;
|
|
169
169
|
const status = await runGitAllowFail(root, ["status", "--porcelain"], 3000);
|
|
170
170
|
if (status.code !== 0) {
|
|
@@ -210,27 +210,27 @@ async function tryMerge(opts) {
|
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
async function mergeCommit(opts) {
|
|
213
|
-
const root =
|
|
213
|
+
const root = path9.resolve(opts.root);
|
|
214
214
|
await runGit(root, ["commit", "-m", opts.message], opts.git_timeout_ms ?? 1e4);
|
|
215
215
|
}
|
|
216
216
|
async function mergeAbort(opts) {
|
|
217
|
-
const root =
|
|
217
|
+
const root = path9.resolve(opts.root);
|
|
218
218
|
await runGitAllowFail(root, ["merge", "--abort"], opts.git_timeout_ms ?? 5000);
|
|
219
219
|
}
|
|
220
220
|
async function getMergeConflicts(opts) {
|
|
221
|
-
const out = await runGitAllowFail(
|
|
221
|
+
const out = await runGitAllowFail(path9.resolve(opts.root), ["diff", "--name-only", "--diff-filter=U"], opts.git_timeout_ms ?? 3000);
|
|
222
222
|
if (out.code !== 0)
|
|
223
223
|
return [];
|
|
224
224
|
return out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
225
225
|
}
|
|
226
226
|
async function worktreeHasChanges(opts) {
|
|
227
|
-
const out = await runGitAllowFail(
|
|
227
|
+
const out = await runGitAllowFail(path9.resolve(opts.worktree_path), ["status", "--porcelain"], opts.git_timeout_ms ?? 3000);
|
|
228
228
|
if (out.code !== 0)
|
|
229
229
|
return false;
|
|
230
230
|
return out.stdout.trim().length > 0;
|
|
231
231
|
}
|
|
232
232
|
async function commitWorktreeIfDirty(opts) {
|
|
233
|
-
const wt =
|
|
233
|
+
const wt = path9.resolve(opts.worktree_path);
|
|
234
234
|
const timeout = opts.git_timeout_ms ?? 1e4;
|
|
235
235
|
const status = await runGitAllowFail(wt, ["status", "--porcelain"], 3000);
|
|
236
236
|
if (status.code !== 0) {
|
|
@@ -10661,1732 +10661,1755 @@ class ApprovalStore {
|
|
|
10661
10661
|
}
|
|
10662
10662
|
}
|
|
10663
10663
|
|
|
10664
|
-
//
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
" - 本工具 `verdict` 字段属审批层,合法值:APPROVE / APPROVE_WITH_NOTES",
|
|
10670
|
-
" - reviewer 输出的 `## Decision` 节首行属协议层,合法值:APPROVE / REQUEST_CHANGES / BLOCK",
|
|
10671
|
-
" - ⚠️ 严禁把 `APPROVE_WITH_NOTES` 字面量写进 `## Decision` 节首行(容错层会归一,但其他变体如 APPROVE_MINOR 会失败 → merge-loop 误判死循环)",
|
|
10672
|
-
' - ✅ 正确:verdict="APPROVE_WITH_NOTES" + `## Decision\\nAPPROVE`(首行写 APPROVE,详情在审批 notes)',
|
|
10673
|
-
"**pendingIds 格式**:推荐 `session:<sid>` / `plan:<plan_id>` / `decision:<hash>`;旧 `pc-<ts>-NNN` 仍兼容。",
|
|
10674
|
-
"**何时不调**:REQUEST_CHANGES / BLOCK 不调(无 APPROVE = 无审批记录)。",
|
|
10675
|
-
"**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。"
|
|
10676
|
-
].join(`
|
|
10677
|
-
`);
|
|
10678
|
-
var ArgsSchema4 = z4.object({
|
|
10679
|
-
verdict: z4.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
|
|
10680
|
-
pendingIds: z4.array(z4.string().min(1)).min(1, "pendingIds 至少 1 条").describe("本次 APPROVE 覆盖的 id 列表。推荐 session:<sid> / plan:<plan_id> / decision:<hash>;旧 pc-xxx 兼容"),
|
|
10681
|
-
notes: z4.string().min(1, "notes 不能为空").max(2000, "notes 过长(> 2000 字),建议拆条").describe("审阅意见摘要(建议 ≤ 500 字)"),
|
|
10682
|
-
decisionLine: z4.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
|
|
10683
|
-
source: z4.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
|
|
10684
|
-
reviewerAgent: z4.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
|
|
10685
|
-
sessionId: z4.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
|
|
10686
|
-
model: z4.string().optional().describe("审批模型 id(审计用,可选)"),
|
|
10687
|
-
coveredSha: z4.string().optional().describe("approval 写入时 worktree HEAD sha;reviewer 调此工具时传入(git -C <worktreePath> rev-parse HEAD)。pre-check 强绑定核心字段,缺失则 pre-check 不命中。ADR:merge-approval-pre-check"),
|
|
10688
|
-
reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。pre-check 仅放行 startsWith('code') 的值;缺失或其他值均不命中。ADR:merge-approval-pre-check")
|
|
10689
|
-
});
|
|
10690
|
-
var _approvalStore = null;
|
|
10691
|
-
function getApprovalStore() {
|
|
10692
|
-
if (!_approvalStore)
|
|
10693
|
-
_approvalStore = ApprovalStore.forProject(process.cwd());
|
|
10694
|
-
return _approvalStore;
|
|
10695
|
-
}
|
|
10696
|
-
async function execute4(input) {
|
|
10697
|
-
const parsed = ArgsSchema4.safeParse(input);
|
|
10698
|
-
if (!parsed.success) {
|
|
10699
|
-
return {
|
|
10700
|
-
ok: false,
|
|
10701
|
-
error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
10702
|
-
};
|
|
10703
|
-
}
|
|
10704
|
-
const args = parsed.data;
|
|
10705
|
-
const approvals = getApprovalStore();
|
|
10706
|
-
const now = new Date().toISOString();
|
|
10707
|
-
const written = [];
|
|
10708
|
-
for (const id of args.pendingIds) {
|
|
10709
|
-
const meta = {
|
|
10710
|
-
pendingId: id,
|
|
10711
|
-
verdict: args.verdict,
|
|
10712
|
-
reviewer: {
|
|
10713
|
-
agent: args.reviewerAgent ?? (args.source === "codeforge-fallback" ? "codeforge" : "reviewer"),
|
|
10714
|
-
...args.sessionId ? { sessionId: args.sessionId } : {},
|
|
10715
|
-
...args.model ? { model: args.model } : {},
|
|
10716
|
-
source: args.source ?? "reviewer"
|
|
10717
|
-
},
|
|
10718
|
-
targets: [],
|
|
10719
|
-
decisionLine: args.decisionLine ?? args.verdict,
|
|
10720
|
-
notes: args.notes,
|
|
10721
|
-
createdAt: now,
|
|
10722
|
-
...args.coveredSha ? { coveredSha: args.coveredSha } : {},
|
|
10723
|
-
...args.reviewTarget ? { reviewTarget: args.reviewTarget } : {},
|
|
10724
|
-
escapeHatch: null
|
|
10725
|
-
};
|
|
10726
|
-
const file = await approvals.record(meta);
|
|
10727
|
-
written.push({ pendingId: id, file });
|
|
10728
|
-
}
|
|
10729
|
-
return { ok: true, written };
|
|
10730
|
-
}
|
|
10731
|
-
// tools/browser-navigate.ts
|
|
10732
|
-
import { z as z5 } from "zod";
|
|
10664
|
+
// lib/session-worktree.ts
|
|
10665
|
+
init_worktree_ops();
|
|
10666
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
10667
|
+
import { promises as fs9 } from "node:fs";
|
|
10668
|
+
import * as path11 from "node:path";
|
|
10733
10669
|
|
|
10734
|
-
// lib/
|
|
10735
|
-
import
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
function checkUrl(url, cfg = DEFAULT_CONFIG2) {
|
|
10750
|
-
if (typeof url !== "string" || url.trim() === "") {
|
|
10751
|
-
return { ok: false, reason: "empty_url" };
|
|
10752
|
-
}
|
|
10753
|
-
for (const pat of cfg.block) {
|
|
10670
|
+
// lib/file-lock.ts
|
|
10671
|
+
import { promises as fs8 } from "node:fs";
|
|
10672
|
+
import * as crypto3 from "node:crypto";
|
|
10673
|
+
import * as os4 from "node:os";
|
|
10674
|
+
import * as path10 from "node:path";
|
|
10675
|
+
async function withFileLock(lockPath, fn, opts = {}) {
|
|
10676
|
+
const retryMs = opts.retryMs ?? 50;
|
|
10677
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
10678
|
+
const staleMs = opts.staleMs ?? 300000;
|
|
10679
|
+
await fs8.mkdir(path10.dirname(lockPath), { recursive: true });
|
|
10680
|
+
const deadline = Date.now() + timeoutMs;
|
|
10681
|
+
const myHost = os4.hostname();
|
|
10682
|
+
const myToken = crypto3.randomBytes(16).toString("hex");
|
|
10683
|
+
let acquired = false;
|
|
10684
|
+
while (!acquired) {
|
|
10754
10685
|
try {
|
|
10755
|
-
|
|
10756
|
-
|
|
10686
|
+
const handle = await fs8.open(lockPath, "wx");
|
|
10687
|
+
try {
|
|
10688
|
+
const meta = {
|
|
10689
|
+
pid: process.pid,
|
|
10690
|
+
host: myHost,
|
|
10691
|
+
token: myToken,
|
|
10692
|
+
acquired_at: new Date().toISOString()
|
|
10693
|
+
};
|
|
10694
|
+
await handle.writeFile(JSON.stringify(meta), "utf8");
|
|
10695
|
+
await handle.close();
|
|
10696
|
+
} catch (writeErr) {
|
|
10697
|
+
await handle.close().catch(() => {});
|
|
10698
|
+
await fs8.rm(lockPath, { force: true }).catch(() => {});
|
|
10699
|
+
throw writeErr;
|
|
10757
10700
|
}
|
|
10758
|
-
|
|
10701
|
+
acquired = true;
|
|
10702
|
+
} catch (err) {
|
|
10703
|
+
const e = err;
|
|
10704
|
+
if (e.code !== "EEXIST")
|
|
10705
|
+
throw err;
|
|
10706
|
+
const cleaned = await tryCleanStaleLock(lockPath, staleMs, myHost);
|
|
10707
|
+
if (cleaned)
|
|
10708
|
+
continue;
|
|
10709
|
+
if (Date.now() >= deadline) {
|
|
10710
|
+
throw new Error(`withFileLock: 等待 ${lockPath} 超过 ${timeoutMs}ms 仍未拿到锁`);
|
|
10711
|
+
}
|
|
10712
|
+
await sleep(retryMs);
|
|
10713
|
+
}
|
|
10759
10714
|
}
|
|
10760
|
-
|
|
10761
|
-
return
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
if (new RegExp(pat).test(url))
|
|
10765
|
-
return { ok: true };
|
|
10766
|
-
} catch {}
|
|
10715
|
+
try {
|
|
10716
|
+
return await fn();
|
|
10717
|
+
} finally {
|
|
10718
|
+
await releaseLockSafely(lockPath, myToken).catch(() => {});
|
|
10767
10719
|
}
|
|
10768
|
-
return { ok: false, reason: "not_in_allow_list" };
|
|
10769
10720
|
}
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10721
|
+
async function tryCleanStaleLock(lockPath, staleMs, myHost) {
|
|
10722
|
+
let mtimeMs;
|
|
10723
|
+
try {
|
|
10724
|
+
const st = await fs8.stat(lockPath);
|
|
10725
|
+
mtimeMs = st.mtimeMs;
|
|
10726
|
+
} catch {
|
|
10727
|
+
return true;
|
|
10775
10728
|
}
|
|
10776
|
-
|
|
10777
|
-
|
|
10729
|
+
const age = Date.now() - mtimeMs;
|
|
10730
|
+
if (age <= staleMs)
|
|
10731
|
+
return false;
|
|
10732
|
+
let meta = null;
|
|
10733
|
+
try {
|
|
10734
|
+
const raw = await fs8.readFile(lockPath, "utf8");
|
|
10735
|
+
meta = JSON.parse(raw);
|
|
10736
|
+
} catch {
|
|
10737
|
+
meta = null;
|
|
10778
10738
|
}
|
|
10779
|
-
|
|
10780
|
-
|
|
10739
|
+
if (meta && meta.host === myHost && typeof meta.pid === "number" && meta.pid > 0) {
|
|
10740
|
+
try {
|
|
10741
|
+
process.kill(meta.pid, 0);
|
|
10742
|
+
return false;
|
|
10743
|
+
} catch (err) {
|
|
10744
|
+
const e = err;
|
|
10745
|
+
if (e.code === "EPERM")
|
|
10746
|
+
return false;
|
|
10747
|
+
}
|
|
10781
10748
|
}
|
|
10782
|
-
|
|
10783
|
-
|
|
10749
|
+
return await deleteIfMtimeMatch(lockPath, mtimeMs);
|
|
10750
|
+
}
|
|
10751
|
+
async function deleteIfMtimeMatch(lockPath, expectedMtimeMs) {
|
|
10752
|
+
try {
|
|
10753
|
+
const st = await fs8.stat(lockPath);
|
|
10754
|
+
if (st.mtimeMs !== expectedMtimeMs) {
|
|
10755
|
+
return false;
|
|
10756
|
+
}
|
|
10757
|
+
} catch {
|
|
10758
|
+
return true;
|
|
10784
10759
|
}
|
|
10785
|
-
|
|
10786
|
-
|
|
10760
|
+
await fs8.rm(lockPath, { force: true }).catch(() => {});
|
|
10761
|
+
return true;
|
|
10762
|
+
}
|
|
10763
|
+
async function releaseLockSafely(lockPath, myToken) {
|
|
10764
|
+
let raw;
|
|
10765
|
+
try {
|
|
10766
|
+
raw = await fs8.readFile(lockPath, "utf8");
|
|
10767
|
+
} catch {
|
|
10768
|
+
return;
|
|
10787
10769
|
}
|
|
10788
|
-
|
|
10789
|
-
|
|
10770
|
+
let meta;
|
|
10771
|
+
try {
|
|
10772
|
+
meta = JSON.parse(raw);
|
|
10773
|
+
} catch {
|
|
10774
|
+
return;
|
|
10790
10775
|
}
|
|
10791
|
-
|
|
10792
|
-
|
|
10776
|
+
if (meta.token === myToken) {
|
|
10777
|
+
await fs8.rm(lockPath, { force: true });
|
|
10793
10778
|
}
|
|
10794
|
-
async close() {}
|
|
10795
10779
|
}
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10780
|
+
function sleep(ms) {
|
|
10781
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
10782
|
+
}
|
|
10783
|
+
|
|
10784
|
+
// lib/session-worktree.ts
|
|
10785
|
+
var REGISTRY_VERSION = 1;
|
|
10786
|
+
var DEFAULT_WORKTREE_SUBDIR = path11.join(".git", "codeforge-worktrees");
|
|
10787
|
+
function debugLog(msg) {
|
|
10788
|
+
if (process.env["CODEFORGE_DEBUG"]) {
|
|
10789
|
+
console.debug(`[session-worktree] ${msg}`);
|
|
10790
|
+
}
|
|
10791
|
+
}
|
|
10792
|
+
function registryDir(mainRoot) {
|
|
10793
|
+
return path11.join(runtimeDir(path11.resolve(mainRoot), { ensure: false }), "session-worktrees");
|
|
10794
|
+
}
|
|
10795
|
+
function registryPath(mainRoot) {
|
|
10796
|
+
return path11.join(registryDir(mainRoot), "registry.json");
|
|
10797
|
+
}
|
|
10798
|
+
function registryLockPath(mainRoot) {
|
|
10799
|
+
return path11.join(registryDir(mainRoot), "registry.lock");
|
|
10800
|
+
}
|
|
10801
|
+
async function readRegistry(mainRoot) {
|
|
10802
|
+
const file = registryPath(mainRoot);
|
|
10800
10803
|
try {
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
+
const raw = await fs9.readFile(file, "utf8");
|
|
10805
|
+
const parsed = JSON.parse(raw);
|
|
10806
|
+
if (parsed.version !== REGISTRY_VERSION || !Array.isArray(parsed.entries)) {
|
|
10807
|
+
return { version: REGISTRY_VERSION, entries: [] };
|
|
10808
|
+
}
|
|
10809
|
+
return parsed;
|
|
10810
|
+
} catch (err) {
|
|
10811
|
+
const e = err;
|
|
10812
|
+
if (e.code === "ENOENT")
|
|
10813
|
+
return { version: REGISTRY_VERSION, entries: [] };
|
|
10814
|
+
return { version: REGISTRY_VERSION, entries: [] };
|
|
10804
10815
|
}
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
const
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
const
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
});
|
|
10822
|
-
if (consoleBuf.length > cfg.bufferLimit)
|
|
10823
|
-
consoleBuf.splice(0, consoleBuf.length - cfg.bufferLimit);
|
|
10816
|
+
}
|
|
10817
|
+
async function writeRegistry(mainRoot, reg) {
|
|
10818
|
+
const file = registryPath(mainRoot);
|
|
10819
|
+
await fs9.mkdir(path11.dirname(file), { recursive: true });
|
|
10820
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
10821
|
+
await fs9.writeFile(tmp, JSON.stringify(reg, null, 2), "utf8");
|
|
10822
|
+
await fs9.rename(tmp, file);
|
|
10823
|
+
}
|
|
10824
|
+
async function mutateRegistry(mainRoot, fn) {
|
|
10825
|
+
const lockPath = registryLockPath(mainRoot);
|
|
10826
|
+
await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
|
|
10827
|
+
return await withFileLock(lockPath, async () => {
|
|
10828
|
+
const reg = await readRegistry(mainRoot);
|
|
10829
|
+
const result = await fn(reg);
|
|
10830
|
+
await writeRegistry(mainRoot, reg);
|
|
10831
|
+
return result;
|
|
10824
10832
|
});
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10833
|
+
}
|
|
10834
|
+
async function bindSessionWorktree(opts) {
|
|
10835
|
+
if (!opts.sessionId || opts.sessionId.trim() === "") {
|
|
10836
|
+
throw new Error("bindSessionWorktree: sessionId 不能为空");
|
|
10837
|
+
}
|
|
10838
|
+
const mainRoot = path11.resolve(opts.mainRoot);
|
|
10839
|
+
const branch = opts.branchName ?? `codeforge/session-${opts.sessionId}`;
|
|
10840
|
+
const worktreesDir = opts.worktrees_dir ?? path11.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
|
|
10841
|
+
const lockPath = registryLockPath(mainRoot);
|
|
10842
|
+
await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
|
|
10843
|
+
return await withFileLock(lockPath, async () => {
|
|
10844
|
+
const reg = await readRegistry(mainRoot);
|
|
10845
|
+
const existing = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
10846
|
+
if (existing && existing.status === "active")
|
|
10847
|
+
return existing;
|
|
10848
|
+
const baseSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
|
|
10849
|
+
const wt = await ensureWorktree({
|
|
10850
|
+
root: mainRoot,
|
|
10851
|
+
branch,
|
|
10852
|
+
worktrees_dir: worktreesDir
|
|
10831
10853
|
});
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10854
|
+
const now = new Date().toISOString();
|
|
10855
|
+
const entry = {
|
|
10856
|
+
sessionId: opts.sessionId,
|
|
10857
|
+
branch,
|
|
10858
|
+
worktreePath: wt.path,
|
|
10859
|
+
baseSha,
|
|
10860
|
+
...opts.requiredPlanId ? { requiredPlanId: opts.requiredPlanId, planReadOk: false } : {},
|
|
10861
|
+
status: "active",
|
|
10862
|
+
createdAt: now,
|
|
10863
|
+
updatedAt: now
|
|
10864
|
+
};
|
|
10865
|
+
const idx = reg.entries.findIndex((e) => e.sessionId === opts.sessionId);
|
|
10866
|
+
if (idx >= 0)
|
|
10867
|
+
reg.entries[idx] = entry;
|
|
10868
|
+
else
|
|
10869
|
+
reg.entries.push(entry);
|
|
10870
|
+
await writeRegistry(mainRoot, reg);
|
|
10871
|
+
return entry;
|
|
10850
10872
|
});
|
|
10851
|
-
return {
|
|
10852
|
-
async navigate(url) {
|
|
10853
|
-
const c = checkUrl(url, cfg);
|
|
10854
|
-
if (!c.ok)
|
|
10855
|
-
return { ok: false, url, error: c.reason };
|
|
10856
|
-
const start = Date.now();
|
|
10857
|
-
try {
|
|
10858
|
-
const resp = await page.goto(url, { timeout: cfg.actionTimeoutMs });
|
|
10859
|
-
return {
|
|
10860
|
-
ok: true,
|
|
10861
|
-
url,
|
|
10862
|
-
status: resp?.status?.() ?? 200,
|
|
10863
|
-
loaded_in_ms: Date.now() - start
|
|
10864
|
-
};
|
|
10865
|
-
} catch (err) {
|
|
10866
|
-
return { ok: false, url, error: describe3(err) };
|
|
10867
|
-
}
|
|
10868
|
-
},
|
|
10869
|
-
async screenshot(opts) {
|
|
10870
|
-
try {
|
|
10871
|
-
const dir = cfg.screenshotDir && cfg.screenshotDir.trim().length > 0 ? cfg.screenshotDir : defaultScreenshotDir();
|
|
10872
|
-
const path10 = `${dir}/${Date.now()}.png`;
|
|
10873
|
-
if (opts?.selector) {
|
|
10874
|
-
const el = await page.locator(opts.selector).first();
|
|
10875
|
-
await el.screenshot({ path: path10 });
|
|
10876
|
-
} else {
|
|
10877
|
-
await page.screenshot({ path: path10, fullPage: opts?.fullPage });
|
|
10878
|
-
}
|
|
10879
|
-
return { ok: true, path: path10 };
|
|
10880
|
-
} catch (err) {
|
|
10881
|
-
return { ok: false, error: describe3(err) };
|
|
10882
|
-
}
|
|
10883
|
-
},
|
|
10884
|
-
async click(selector) {
|
|
10885
|
-
try {
|
|
10886
|
-
await page.locator(selector).first().click({ timeout: cfg.actionTimeoutMs });
|
|
10887
|
-
return { ok: true, selector, found: true };
|
|
10888
|
-
} catch (err) {
|
|
10889
|
-
return { ok: false, selector, found: false, error: describe3(err) };
|
|
10890
|
-
}
|
|
10891
|
-
},
|
|
10892
|
-
async fill(selector, value) {
|
|
10893
|
-
try {
|
|
10894
|
-
await page.locator(selector).first().fill(value, { timeout: cfg.actionTimeoutMs });
|
|
10895
|
-
return { ok: true, selector, found: true };
|
|
10896
|
-
} catch (err) {
|
|
10897
|
-
return { ok: false, selector, found: false, error: describe3(err) };
|
|
10898
|
-
}
|
|
10899
|
-
},
|
|
10900
|
-
async consoleLogs(opts = {}) {
|
|
10901
|
-
return consoleBuf.filter((e) => (opts.sinceTs === undefined || e.timestamp >= opts.sinceTs) && (opts.level === undefined || e.level === opts.level));
|
|
10902
|
-
},
|
|
10903
|
-
async networkRequests(opts = {}) {
|
|
10904
|
-
return networkBuf.filter((e) => (opts.sinceTs === undefined || e.start_ts >= opts.sinceTs) && (opts.method === undefined || e.method.toUpperCase() === opts.method.toUpperCase()));
|
|
10905
|
-
},
|
|
10906
|
-
async close() {
|
|
10907
|
-
try {
|
|
10908
|
-
await context.close();
|
|
10909
|
-
} catch {}
|
|
10910
|
-
try {
|
|
10911
|
-
await browser.close();
|
|
10912
|
-
} catch {}
|
|
10913
|
-
}
|
|
10914
|
-
};
|
|
10915
10873
|
}
|
|
10916
|
-
async function
|
|
10917
|
-
|
|
10874
|
+
async function getSessionWorktree(sessionId, mainRoot) {
|
|
10875
|
+
const reg = await readRegistry(mainRoot);
|
|
10876
|
+
return reg.entries.find((e) => e.sessionId === sessionId) ?? null;
|
|
10918
10877
|
}
|
|
10919
|
-
function
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
}
|
|
10878
|
+
async function markPlanReadOk(opts) {
|
|
10879
|
+
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
10880
|
+
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
10881
|
+
if (!entry || entry.requiredPlanId !== opts.planId)
|
|
10882
|
+
return false;
|
|
10883
|
+
entry.planReadOk = true;
|
|
10884
|
+
entry.updatedAt = new Date().toISOString();
|
|
10885
|
+
return true;
|
|
10886
|
+
});
|
|
10929
10887
|
}
|
|
10930
|
-
function
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
return
|
|
10937
|
-
}
|
|
10938
|
-
return String(err);
|
|
10939
|
-
}
|
|
10888
|
+
async function touchEntryUpdatedAt(opts) {
|
|
10889
|
+
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
10890
|
+
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
10891
|
+
if (!entry || entry.status !== "active")
|
|
10892
|
+
return false;
|
|
10893
|
+
entry.updatedAt = new Date().toISOString();
|
|
10894
|
+
return true;
|
|
10895
|
+
});
|
|
10940
10896
|
}
|
|
10941
|
-
async function
|
|
10942
|
-
const
|
|
10943
|
-
|
|
10944
|
-
|
|
10897
|
+
async function mergeSessionBack(opts) {
|
|
10898
|
+
const mainRoot = path11.resolve(opts.mainRoot);
|
|
10899
|
+
const entry = await getSessionWorktree(opts.sessionId, mainRoot);
|
|
10900
|
+
if (!entry) {
|
|
10901
|
+
throw new Error(`mergeSessionBack: session ${opts.sessionId} 没有绑定 worktree`);
|
|
10945
10902
|
}
|
|
10946
|
-
if (
|
|
10947
|
-
|
|
10948
|
-
if (c)
|
|
10949
|
-
return c;
|
|
10903
|
+
if (entry.status === "interrupted_dirty") {
|
|
10904
|
+
throw new Error(`mergeSessionBack: session ${opts.sessionId} 处于 interrupted_dirty 状态,` + `请先手动处理 worktree (${entry.worktreePath}) 后再 merge`);
|
|
10950
10905
|
}
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
return playwright;
|
|
10954
|
-
return new NoopBrowserController("browser enabled but no driver available (npm i playwright 后重试)");
|
|
10955
|
-
}
|
|
10956
|
-
|
|
10957
|
-
// tools/browser-navigate.ts
|
|
10958
|
-
var description5 = [
|
|
10959
|
-
"打开指定 URL(受 tools.browser 配置控制;默认禁用)。",
|
|
10960
|
-
"**何时调用**:需要看 web 页面真实渲染、调试前端、复现 bug 报告链接。",
|
|
10961
|
-
"**注意**:仅允许 http/https;file:/data: 等协议被默认安全策略拒绝。"
|
|
10962
|
-
].join(`
|
|
10963
|
-
`);
|
|
10964
|
-
var ArgsSchema5 = z5.object({
|
|
10965
|
-
url: z5.string().min(1).describe("要打开的 URL;必须是 http(s) 协议")
|
|
10966
|
-
});
|
|
10967
|
-
var _controller = null;
|
|
10968
|
-
var _cfg;
|
|
10969
|
-
async function getController() {
|
|
10970
|
-
if (!_controller) {
|
|
10971
|
-
_controller = await createBrowserController({ cfg: _cfg });
|
|
10906
|
+
if (entry.status !== "active") {
|
|
10907
|
+
throw new Error(`mergeSessionBack: session ${opts.sessionId} 状态为 ${entry.status},无法 merge`);
|
|
10972
10908
|
}
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
const
|
|
10977
|
-
if (
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10909
|
+
const wt = entry.worktreePath;
|
|
10910
|
+
const branch = entry.branch;
|
|
10911
|
+
const baseSha = entry.baseSha;
|
|
10912
|
+
const wtStatus = (await runGit2(wt, ["status", "--porcelain"])).trim();
|
|
10913
|
+
if (wtStatus.length > 0) {
|
|
10914
|
+
await runGit2(wt, ["add", "-A"]);
|
|
10915
|
+
await runGit2(wt, ["commit", "-m", `session(${opts.sessionId}): auto-commit before merge`]);
|
|
10916
|
+
}
|
|
10917
|
+
const mainStatus = (await runGit2(mainRoot, ["status", "--porcelain"])).trim();
|
|
10918
|
+
if (mainStatus.length > 0) {
|
|
10919
|
+
throw new Error(`mergeSessionBack: 主仓 ${mainRoot} 有未提交改动,请先 commit/stash 后再 merge`);
|
|
10983
10920
|
}
|
|
10984
10921
|
try {
|
|
10985
|
-
|
|
10986
|
-
return await ctrl.navigate(parsed.data.url);
|
|
10922
|
+
await runGit2(mainRoot, ["merge", "--squash", branch]);
|
|
10987
10923
|
} catch (err) {
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
};
|
|
10993
|
-
}
|
|
10994
|
-
}
|
|
10995
|
-
// tools/browser-click.ts
|
|
10996
|
-
import { z as z6 } from "zod";
|
|
10997
|
-
var description6 = [
|
|
10998
|
-
"点击页面元素。属于「写操作」,semi 模式下应当让用户确认。",
|
|
10999
|
-
"**何时调用**:表单提交、按钮触发、SPA 导航。",
|
|
11000
|
-
"**避坑**:selector 优先用稳定属性(data-testid > role > 文本 > id),尽量不用 xpath。"
|
|
11001
|
-
].join(`
|
|
11002
|
-
`);
|
|
11003
|
-
var ArgsSchema6 = z6.object({
|
|
11004
|
-
selector: z6.string().min(1).describe("CSS / Playwright locator 字符串,必须能唯一定位")
|
|
11005
|
-
});
|
|
11006
|
-
var _controller2 = null;
|
|
11007
|
-
var _cfg2;
|
|
11008
|
-
async function getController2() {
|
|
11009
|
-
if (!_controller2) {
|
|
11010
|
-
_controller2 = await createBrowserController({ cfg: _cfg2 });
|
|
11011
|
-
}
|
|
11012
|
-
return _controller2;
|
|
11013
|
-
}
|
|
11014
|
-
async function execute6(input) {
|
|
11015
|
-
const parsed = ArgsSchema6.safeParse(input);
|
|
11016
|
-
if (!parsed.success) {
|
|
11017
|
-
return {
|
|
11018
|
-
ok: false,
|
|
11019
|
-
selector: typeof input?.selector === "string" ? String(input.selector) : "",
|
|
11020
|
-
found: false,
|
|
11021
|
-
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11022
|
-
};
|
|
10924
|
+
await runGit2(mainRoot, ["reset", "--merge"]).catch(() => {
|
|
10925
|
+
return runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
|
|
10926
|
+
});
|
|
10927
|
+
throw new Error(`mergeSessionBack: squash merge 失败(已 reset 主仓兜底): ${err.message}`);
|
|
11023
10928
|
}
|
|
10929
|
+
const buildScript = await getBuildScript(mainRoot);
|
|
10930
|
+
if (buildScript) {
|
|
10931
|
+
const stagedRaw = await runGit2(mainRoot, [
|
|
10932
|
+
"diff",
|
|
10933
|
+
"--cached",
|
|
10934
|
+
"--name-only",
|
|
10935
|
+
"--diff-filter=ACMR"
|
|
10936
|
+
]);
|
|
10937
|
+
const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
10938
|
+
const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths, wt);
|
|
10939
|
+
if (canSkipDevOnce) {} else {
|
|
10940
|
+
try {
|
|
10941
|
+
await runCmd("npm", ["run", buildScript], mainRoot);
|
|
10942
|
+
} catch (err) {
|
|
10943
|
+
await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
|
|
10944
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10945
|
+
throw new Error(`${buildScript} 失败已 reset 主仓: ${msg}`);
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10948
|
+
} else {}
|
|
10949
|
+
const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
|
|
10950
|
+
const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
10951
|
+
const message = opts.commitMessage ?? buildMergeMessage(opts.sessionId, branch, baseSha, squashedCommits);
|
|
10952
|
+
await runGitWithEnv(mainRoot, ["commit", "-m", message], {
|
|
10953
|
+
SKIP_DEV_SYNC_CHECK: "1"
|
|
10954
|
+
});
|
|
10955
|
+
const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
|
|
11024
10956
|
try {
|
|
11025
|
-
|
|
11026
|
-
return await ctrl.click(parsed.data.selector);
|
|
10957
|
+
await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
|
|
11027
10958
|
} catch (err) {
|
|
11028
|
-
|
|
11029
|
-
ok: false,
|
|
11030
|
-
selector: parsed.data.selector,
|
|
11031
|
-
found: false,
|
|
11032
|
-
error: err instanceof Error ? err.message : String(err)
|
|
11033
|
-
};
|
|
10959
|
+
debugLog(`removeWorktree (merge) 非预期失败 (session=${opts.sessionId}): ${err.message}`);
|
|
11034
10960
|
}
|
|
11035
|
-
}
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
async function getController3() {
|
|
11051
|
-
if (!_controller3) {
|
|
11052
|
-
_controller3 = await createBrowserController({ cfg: _cfg3 });
|
|
10961
|
+
await deleteBranchIfExists({ root: mainRoot, branch }).catch((err) => {
|
|
10962
|
+
debugLog(`deleteBranchIfExists (merge) 非预期失败: ${err.message}`);
|
|
10963
|
+
return { deleted: false };
|
|
10964
|
+
});
|
|
10965
|
+
await mutateRegistry(mainRoot, (reg) => {
|
|
10966
|
+
const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
|
|
10967
|
+
if (e) {
|
|
10968
|
+
e.status = "merged";
|
|
10969
|
+
e.updatedAt = new Date().toISOString();
|
|
10970
|
+
}
|
|
10971
|
+
});
|
|
10972
|
+
if (opts.planStore && entry.requiredPlanId) {
|
|
10973
|
+
await opts.planStore.markMerged(entry.requiredPlanId, opts.sessionId).catch((err) => {
|
|
10974
|
+
console.warn(`[session-worktree] planStore.markMerged(${entry.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
10975
|
+
});
|
|
11053
10976
|
}
|
|
11054
|
-
return
|
|
10977
|
+
return { sha: newSha, squashedCommits };
|
|
11055
10978
|
}
|
|
11056
|
-
async function
|
|
11057
|
-
const
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
};
|
|
10979
|
+
async function discardSession(opts) {
|
|
10980
|
+
const mainRoot = path11.resolve(opts.mainRoot);
|
|
10981
|
+
const entry = await getSessionWorktree(opts.sessionId, mainRoot);
|
|
10982
|
+
if (!entry) {
|
|
10983
|
+
return;
|
|
10984
|
+
}
|
|
10985
|
+
if (entry.status === "merged" || entry.status === "discarded") {
|
|
10986
|
+
return;
|
|
11065
10987
|
}
|
|
11066
10988
|
try {
|
|
11067
|
-
|
|
11068
|
-
|
|
10989
|
+
await removeWorktree({
|
|
10990
|
+
root: mainRoot,
|
|
10991
|
+
worktree_path: entry.worktreePath,
|
|
10992
|
+
force: true
|
|
10993
|
+
});
|
|
11069
10994
|
} catch (err) {
|
|
11070
|
-
|
|
11071
|
-
ok: false,
|
|
11072
|
-
selector: parsed.data.selector,
|
|
11073
|
-
found: false,
|
|
11074
|
-
error: err instanceof Error ? err.message : String(err)
|
|
11075
|
-
};
|
|
10995
|
+
debugLog(`removeWorktree (discard) 非预期失败: ${err.message}`);
|
|
11076
10996
|
}
|
|
11077
|
-
}
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
async function getController4() {
|
|
11093
|
-
if (!_controller4) {
|
|
11094
|
-
_controller4 = await createBrowserController({ cfg: _cfg4 });
|
|
10997
|
+
await deleteBranchIfExists({ root: mainRoot, branch: entry.branch }).catch((err) => {
|
|
10998
|
+
debugLog(`deleteBranchIfExists (discard) 非预期失败: ${err.message}`);
|
|
10999
|
+
return { deleted: false };
|
|
11000
|
+
});
|
|
11001
|
+
await mutateRegistry(mainRoot, (reg) => {
|
|
11002
|
+
const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
|
|
11003
|
+
if (e) {
|
|
11004
|
+
e.status = "discarded";
|
|
11005
|
+
e.updatedAt = new Date().toISOString();
|
|
11006
|
+
}
|
|
11007
|
+
});
|
|
11008
|
+
if (opts.planStore && entry.requiredPlanId) {
|
|
11009
|
+
await opts.planStore.markDiscarded(entry.requiredPlanId, opts.sessionId).catch((err) => {
|
|
11010
|
+
console.warn(`[session-worktree] planStore.markDiscarded(${entry.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
11011
|
+
});
|
|
11095
11012
|
}
|
|
11096
|
-
return _controller4;
|
|
11097
11013
|
}
|
|
11098
|
-
async function
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
} catch (err) {
|
|
11107
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
11108
|
-
}
|
|
11014
|
+
async function markInterruptedDirty(opts) {
|
|
11015
|
+
await mutateRegistry(opts.mainRoot, (reg) => {
|
|
11016
|
+
const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
|
|
11017
|
+
if (e && e.status === "active") {
|
|
11018
|
+
e.status = "interrupted_dirty";
|
|
11019
|
+
e.updatedAt = new Date().toISOString();
|
|
11020
|
+
}
|
|
11021
|
+
});
|
|
11109
11022
|
}
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
"**注意**:缓冲区上限 500 条,超出会丢最早;只能读 navigate 之后的日志。"
|
|
11116
|
-
].join(`
|
|
11117
|
-
`);
|
|
11118
|
-
var ArgsSchema9 = z9.object({
|
|
11119
|
-
level: z9.enum(["log", "info", "warn", "error", "debug"]).optional().describe("只过滤指定级别;缺省返回全部"),
|
|
11120
|
-
sinceTs: z9.number().optional().describe("只返回 timestamp >= sinceTs 的条目")
|
|
11121
|
-
});
|
|
11122
|
-
var _controller5 = null;
|
|
11123
|
-
var _cfg5;
|
|
11124
|
-
async function getController5() {
|
|
11125
|
-
if (!_controller5) {
|
|
11126
|
-
_controller5 = await createBrowserController({ cfg: _cfg5 });
|
|
11023
|
+
async function getCurrentWorktreeHead(worktreePath) {
|
|
11024
|
+
try {
|
|
11025
|
+
return (await runGit2(path11.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
|
|
11026
|
+
} catch {
|
|
11027
|
+
return "";
|
|
11127
11028
|
}
|
|
11128
|
-
return _controller5;
|
|
11129
11029
|
}
|
|
11130
|
-
async function
|
|
11131
|
-
const parsed = ArgsSchema9.safeParse(input);
|
|
11132
|
-
if (!parsed.success) {
|
|
11133
|
-
return {
|
|
11134
|
-
ok: false,
|
|
11135
|
-
entries: [],
|
|
11136
|
-
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11137
|
-
};
|
|
11138
|
-
}
|
|
11030
|
+
async function isWorktreeDirty(worktreePath) {
|
|
11139
11031
|
try {
|
|
11140
|
-
const
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
return {
|
|
11145
|
-
ok: false,
|
|
11146
|
-
entries: [],
|
|
11147
|
-
error: err instanceof Error ? err.message : String(err)
|
|
11148
|
-
};
|
|
11032
|
+
const out = (await runGit2(worktreePath, ["status", "--porcelain"])).trim();
|
|
11033
|
+
return out.length > 0;
|
|
11034
|
+
} catch {
|
|
11035
|
+
return false;
|
|
11149
11036
|
}
|
|
11150
11037
|
}
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
"读取浏览器网络请求缓冲区(含 status / 失败原因 / 耗时)。",
|
|
11155
|
-
"**何时调用**:调 API 调用 4xx/5xx、CORS 报错、查 page 加载耗时分布。",
|
|
11156
|
-
"**注意**:缓冲区上限 500 条;只看 navigate 之后发起的请求。"
|
|
11157
|
-
].join(`
|
|
11158
|
-
`);
|
|
11159
|
-
var ArgsSchema10 = z10.object({
|
|
11160
|
-
method: z10.string().optional().describe("HTTP 方法过滤(GET/POST/...),不区分大小写"),
|
|
11161
|
-
sinceTs: z10.number().optional().describe("只返回 start_ts >= sinceTs 的请求")
|
|
11162
|
-
});
|
|
11163
|
-
var _controller6 = null;
|
|
11164
|
-
var _cfg6;
|
|
11165
|
-
async function getController6() {
|
|
11166
|
-
if (!_controller6) {
|
|
11167
|
-
_controller6 = await createBrowserController({ cfg: _cfg6 });
|
|
11168
|
-
}
|
|
11169
|
-
return _controller6;
|
|
11038
|
+
async function checkpointCommit(opts) {
|
|
11039
|
+
await runGit2(opts.worktreePath, ["add", "-A"]);
|
|
11040
|
+
await runGit2(opts.worktreePath, ["commit", "-m", opts.message]);
|
|
11170
11041
|
}
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
const ctrl = await getController6();
|
|
11182
|
-
const entries = await ctrl.networkRequests(parsed.data);
|
|
11183
|
-
return { ok: true, entries };
|
|
11184
|
-
} catch (err) {
|
|
11185
|
-
return {
|
|
11186
|
-
ok: false,
|
|
11187
|
-
entries: [],
|
|
11188
|
-
error: err instanceof Error ? err.message : String(err)
|
|
11189
|
-
};
|
|
11190
|
-
}
|
|
11191
|
-
}
|
|
11192
|
-
// tools/model-chain.ts
|
|
11193
|
-
import { z as z11 } from "zod";
|
|
11194
|
-
|
|
11195
|
-
// lib/model-config.ts
|
|
11196
|
-
import { promises as fs7 } from "node:fs";
|
|
11197
|
-
import * as path10 from "node:path";
|
|
11198
|
-
|
|
11199
|
-
// lib/model-tier.ts
|
|
11200
|
-
var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
|
|
11201
|
-
|
|
11202
|
-
// lib/model-config.ts
|
|
11203
|
-
var CONFIG_FILE = "codeforge.json";
|
|
11204
|
-
var DEFAULT_RUNTIME_FALLBACK = {
|
|
11205
|
-
enabled: true,
|
|
11206
|
-
max_fallback_attempts: 3,
|
|
11207
|
-
notify_on_fallback: true,
|
|
11208
|
-
trigger_events: ["session.error", "model.unavailable", "model.rate_limited"]
|
|
11209
|
-
};
|
|
11210
|
-
var PROVIDER_MODEL_RE = /^[a-z0-9-]+\/[a-zA-Z0-9._-]+$/;
|
|
11211
|
-
function findConfigFileSync(opts = {}) {
|
|
11212
|
-
const root = opts.root ?? process.cwd();
|
|
11213
|
-
const fsSync = __require("node:fs");
|
|
11214
|
-
const abs = path10.resolve(root, opts.file ?? CONFIG_FILE);
|
|
11215
|
-
return fsSync.existsSync(abs) ? abs : null;
|
|
11042
|
+
function runGit2(cwd, args, timeoutMs = 1e4) {
|
|
11043
|
+
return new Promise((resolve10, reject) => {
|
|
11044
|
+
execFile3("git", args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
11045
|
+
if (err) {
|
|
11046
|
+
reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
|
|
11047
|
+
return;
|
|
11048
|
+
}
|
|
11049
|
+
resolve10(stdout);
|
|
11050
|
+
});
|
|
11051
|
+
});
|
|
11216
11052
|
}
|
|
11217
|
-
function
|
|
11218
|
-
const
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11053
|
+
function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
|
|
11054
|
+
const inheritedEnv = process["env"];
|
|
11055
|
+
return new Promise((resolve10, reject) => {
|
|
11056
|
+
execFile3("git", args, {
|
|
11057
|
+
cwd,
|
|
11058
|
+
timeout: timeoutMs,
|
|
11059
|
+
windowsHide: true,
|
|
11060
|
+
encoding: "utf8",
|
|
11061
|
+
env: Object.assign({}, inheritedEnv, envOverrides)
|
|
11062
|
+
}, (err, stdout, stderr) => {
|
|
11063
|
+
if (err) {
|
|
11064
|
+
reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
|
|
11065
|
+
return;
|
|
11066
|
+
}
|
|
11067
|
+
resolve10(stdout);
|
|
11068
|
+
});
|
|
11069
|
+
});
|
|
11231
11070
|
}
|
|
11232
|
-
async function
|
|
11233
|
-
const
|
|
11234
|
-
const
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
}
|
|
11243
|
-
return { ok: false, path: abs, warnings: [], error: `read_failed: ${e.message}` };
|
|
11244
|
-
}
|
|
11245
|
-
return parseAndValidate(raw, abs);
|
|
11071
|
+
async function getBuildScript(mainRoot) {
|
|
11072
|
+
const cfg = getCodeforgeConfig({ root: mainRoot });
|
|
11073
|
+
const merge = cfg["merge"];
|
|
11074
|
+
if (!merge || typeof merge !== "object" || Array.isArray(merge))
|
|
11075
|
+
return null;
|
|
11076
|
+
const script = merge["postMergeScript"];
|
|
11077
|
+
if (typeof script !== "string")
|
|
11078
|
+
return null;
|
|
11079
|
+
const trimmed = script.trim();
|
|
11080
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
11246
11081
|
}
|
|
11247
|
-
function
|
|
11248
|
-
let
|
|
11082
|
+
async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
|
|
11083
|
+
let distMtimeSec;
|
|
11249
11084
|
try {
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11255
|
-
return { ok: false, path: abs, warnings: [], error: "invalid_root: must be an object" };
|
|
11085
|
+
const st = await fs9.stat(path11.join(mainRoot, "dist/index.js"));
|
|
11086
|
+
distMtimeSec = Math.floor(st.mtimeMs / 1000);
|
|
11087
|
+
} catch {
|
|
11088
|
+
return false;
|
|
11256
11089
|
}
|
|
11257
|
-
const
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11090
|
+
const relevant = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p));
|
|
11091
|
+
if (relevant.length === 0)
|
|
11092
|
+
return true;
|
|
11093
|
+
for (const rel of relevant) {
|
|
11094
|
+
const srcMtimeSec = await statSourceMtime(rel, mainRoot, worktreePath);
|
|
11095
|
+
if (srcMtimeSec === null)
|
|
11096
|
+
return false;
|
|
11097
|
+
if (srcMtimeSec > distMtimeSec)
|
|
11098
|
+
return false;
|
|
11261
11099
|
}
|
|
11262
|
-
|
|
11263
|
-
if (!v.ok)
|
|
11264
|
-
return { ok: false, path: abs, warnings: [], error: v.error };
|
|
11265
|
-
return { ok: true, path: abs, config: v.config, warnings: v.warnings };
|
|
11100
|
+
return true;
|
|
11266
11101
|
}
|
|
11267
|
-
function
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
const agentsRaw = obj.agents;
|
|
11274
|
-
if (!agentsRaw || typeof agentsRaw !== "object" || Array.isArray(agentsRaw)) {
|
|
11275
|
-
return { ok: false, warnings, error: "agents_required_object" };
|
|
11276
|
-
}
|
|
11277
|
-
const agents = {};
|
|
11278
|
-
for (const [name, value] of Object.entries(agentsRaw)) {
|
|
11279
|
-
const r = normalizeAgent(name, value);
|
|
11280
|
-
if (!r.ok)
|
|
11281
|
-
return { ok: false, warnings, error: r.error };
|
|
11282
|
-
agents[name] = r.binding;
|
|
11283
|
-
}
|
|
11284
|
-
if (Object.keys(agents).length === 0) {
|
|
11285
|
-
return { ok: false, warnings, error: "agents_must_have_at_least_one_entry" };
|
|
11286
|
-
}
|
|
11287
|
-
let categories;
|
|
11288
|
-
if (obj.categories !== undefined) {
|
|
11289
|
-
if (!obj.categories || typeof obj.categories !== "object" || Array.isArray(obj.categories)) {
|
|
11290
|
-
return { ok: false, warnings, error: "categories_must_be_object" };
|
|
11291
|
-
}
|
|
11292
|
-
categories = {};
|
|
11293
|
-
for (const [name, value] of Object.entries(obj.categories)) {
|
|
11294
|
-
const r = normalizeCategory(name, value);
|
|
11295
|
-
if (!r.ok)
|
|
11296
|
-
return { ok: false, warnings, error: r.error };
|
|
11297
|
-
categories[name] = r.binding;
|
|
11298
|
-
}
|
|
11299
|
-
}
|
|
11300
|
-
let runtime;
|
|
11301
|
-
if (obj.runtime_fallback !== undefined) {
|
|
11302
|
-
const r = normalizeRuntime(obj.runtime_fallback);
|
|
11303
|
-
if (!r.ok)
|
|
11304
|
-
return { ok: false, warnings, error: r.error };
|
|
11305
|
-
runtime = r.cfg;
|
|
11306
|
-
}
|
|
11307
|
-
let tiers;
|
|
11308
|
-
if (obj.tiers !== undefined) {
|
|
11309
|
-
const r = normalizeTiers(obj.tiers);
|
|
11310
|
-
if (!r.ok)
|
|
11311
|
-
return { ok: false, warnings, error: r.error };
|
|
11312
|
-
tiers = r.cfg;
|
|
11102
|
+
async function statSourceMtime(rel, mainRoot, worktreePath) {
|
|
11103
|
+
if (worktreePath) {
|
|
11104
|
+
try {
|
|
11105
|
+
const st = await fs9.stat(path11.join(worktreePath, rel));
|
|
11106
|
+
return Math.floor(st.mtimeMs / 1000);
|
|
11107
|
+
} catch {}
|
|
11313
11108
|
}
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11109
|
+
try {
|
|
11110
|
+
const st = await fs9.stat(path11.join(mainRoot, rel));
|
|
11111
|
+
return Math.floor(st.mtimeMs / 1000);
|
|
11112
|
+
} catch {
|
|
11113
|
+
return null;
|
|
11318
11114
|
}
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
}
|
|
11115
|
+
}
|
|
11116
|
+
function runCmd(cmd, args, cwd, timeoutMs = 300000) {
|
|
11117
|
+
return new Promise((resolve10, reject) => {
|
|
11118
|
+
execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
11119
|
+
if (err) {
|
|
11120
|
+
reject(new Error(`${cmd} ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
|
|
11121
|
+
return;
|
|
11122
|
+
}
|
|
11123
|
+
resolve10(stdout);
|
|
11124
|
+
});
|
|
11125
|
+
});
|
|
11126
|
+
}
|
|
11127
|
+
function buildMergeMessage(sessionId, branch, baseSha, squashed) {
|
|
11128
|
+
const subject = `session(${sessionId}): merge ${branch}`;
|
|
11129
|
+
const body = squashed.length > 0 ? `
|
|
11130
|
+
|
|
11131
|
+
Squashed commits:
|
|
11132
|
+
${squashed.map((s) => ` - ${s}`).join(`
|
|
11133
|
+
`)}` : "";
|
|
11134
|
+
const footer = `
|
|
11135
|
+
|
|
11136
|
+
Codeforge-Session: ${sessionId}
|
|
11137
|
+
Codeforge-Base: ${baseSha.slice(0, 12)}`;
|
|
11138
|
+
return subject + body + footer;
|
|
11139
|
+
}
|
|
11140
|
+
var ORPHAN_GRACE_MS = 60000;
|
|
11141
|
+
var SEMANTIC_ORPHAN_MIN_AGE_MS = 6 * 60 * 60000;
|
|
11142
|
+
var SEMANTIC_ORPHAN_UNKNOWN_TIMEOUT_MS = 72 * 60 * 60000;
|
|
11143
|
+
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
11144
|
+
const keepRecent = opts.keepRecent ?? 50;
|
|
11145
|
+
if (keepRecent < 0) {
|
|
11146
|
+
throw new Error(`pruneDiscardedRegistryEntries: keepRecent 必须 ≥ 0,收到 ${keepRecent}`);
|
|
11329
11147
|
}
|
|
11330
|
-
return {
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
runtime_fallback: runtime,
|
|
11339
|
-
tiers
|
|
11148
|
+
return await mutateRegistry(path11.resolve(mainRoot), (reg) => {
|
|
11149
|
+
const discarded = [];
|
|
11150
|
+
const others = [];
|
|
11151
|
+
for (const e of reg.entries) {
|
|
11152
|
+
if (e.status === "discarded")
|
|
11153
|
+
discarded.push(e);
|
|
11154
|
+
else
|
|
11155
|
+
others.push(e);
|
|
11340
11156
|
}
|
|
11341
|
-
|
|
11157
|
+
discarded.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0);
|
|
11158
|
+
const kept = discarded.slice(0, keepRecent);
|
|
11159
|
+
const pruned = discarded.length - kept.length;
|
|
11160
|
+
reg.entries = [...others, ...kept];
|
|
11161
|
+
return { pruned, kept: kept.length };
|
|
11162
|
+
});
|
|
11342
11163
|
}
|
|
11343
|
-
function
|
|
11344
|
-
|
|
11345
|
-
|
|
11164
|
+
async function pruneOrphanWorktrees(mainRoot, opts = {}) {
|
|
11165
|
+
const resolved = path11.resolve(mainRoot);
|
|
11166
|
+
const cleaned = [];
|
|
11167
|
+
const failed = [];
|
|
11168
|
+
let skipped = 0;
|
|
11169
|
+
let gitWorktrees = [];
|
|
11170
|
+
try {
|
|
11171
|
+
const mod = await Promise.resolve().then(() => (init_worktree_ops(), exports_worktree_ops));
|
|
11172
|
+
gitWorktrees = await mod.listWorktrees({ root: resolved });
|
|
11173
|
+
} catch {
|
|
11174
|
+
gitWorktrees = [];
|
|
11346
11175
|
}
|
|
11347
|
-
const
|
|
11348
|
-
|
|
11176
|
+
const gitWorktreePaths = new Set(gitWorktrees.map((w) => path11.resolve(w.path)));
|
|
11177
|
+
await mutateRegistry(resolved, async (reg2) => {
|
|
11178
|
+
const now = Date.now();
|
|
11179
|
+
for (const entry of reg2.entries) {
|
|
11180
|
+
if (entry.status !== "active")
|
|
11181
|
+
continue;
|
|
11182
|
+
const wt = entry.worktreePath;
|
|
11183
|
+
let dirExists = true;
|
|
11184
|
+
let dirMtimeMs = 0;
|
|
11185
|
+
try {
|
|
11186
|
+
const st = await fs9.stat(wt);
|
|
11187
|
+
dirExists = st.isDirectory();
|
|
11188
|
+
dirMtimeMs = st.mtimeMs;
|
|
11189
|
+
} catch {
|
|
11190
|
+
dirExists = false;
|
|
11191
|
+
}
|
|
11192
|
+
if (dirExists && now - dirMtimeMs < ORPHAN_GRACE_MS) {
|
|
11193
|
+
skipped++;
|
|
11194
|
+
continue;
|
|
11195
|
+
}
|
|
11196
|
+
let branchExists = true;
|
|
11197
|
+
try {
|
|
11198
|
+
await runGit2(resolved, ["rev-parse", "--verify", `refs/heads/${entry.branch}`]);
|
|
11199
|
+
} catch {
|
|
11200
|
+
branchExists = false;
|
|
11201
|
+
}
|
|
11202
|
+
if (!dirExists || !branchExists) {
|
|
11203
|
+
const reason = !dirExists ? "worktreePath 不存在" : "branch 不存在";
|
|
11204
|
+
entry.status = "discarded";
|
|
11205
|
+
entry.updatedAt = new Date().toISOString();
|
|
11206
|
+
if (dirExists) {
|
|
11207
|
+
try {
|
|
11208
|
+
await removeWorktree({
|
|
11209
|
+
root: resolved,
|
|
11210
|
+
worktree_path: wt,
|
|
11211
|
+
force: true
|
|
11212
|
+
});
|
|
11213
|
+
} catch (err) {
|
|
11214
|
+
failed.push({
|
|
11215
|
+
worktreePath: wt,
|
|
11216
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11217
|
+
});
|
|
11218
|
+
}
|
|
11219
|
+
}
|
|
11220
|
+
cleaned.push({ sessionId: entry.sessionId, worktreePath: wt, reason });
|
|
11221
|
+
}
|
|
11222
|
+
}
|
|
11223
|
+
});
|
|
11224
|
+
if (opts.isSessionAlive) {
|
|
11225
|
+
const minAge = opts.semanticOrphanMinAgeMs ?? SEMANTIC_ORPHAN_MIN_AGE_MS;
|
|
11226
|
+
const unknownTimeout = opts.semanticOrphanUnknownTimeoutMs ?? SEMANTIC_ORPHAN_UNKNOWN_TIMEOUT_MS;
|
|
11227
|
+
const probe = opts.isSessionAlive;
|
|
11228
|
+
await mutateRegistry(resolved, async (reg2) => {
|
|
11229
|
+
const now = Date.now();
|
|
11230
|
+
for (const entry of reg2.entries) {
|
|
11231
|
+
if (entry.status !== "active")
|
|
11232
|
+
continue;
|
|
11233
|
+
const updatedMs = Date.parse(entry.updatedAt);
|
|
11234
|
+
if (!Number.isFinite(updatedMs) || now - updatedMs < minAge) {
|
|
11235
|
+
skipped++;
|
|
11236
|
+
continue;
|
|
11237
|
+
}
|
|
11238
|
+
let aliveResult;
|
|
11239
|
+
try {
|
|
11240
|
+
aliveResult = await probe(entry.sessionId);
|
|
11241
|
+
} catch {
|
|
11242
|
+
skipped++;
|
|
11243
|
+
continue;
|
|
11244
|
+
}
|
|
11245
|
+
if (aliveResult.source === "unknown") {
|
|
11246
|
+
if (now - updatedMs < unknownTimeout) {
|
|
11247
|
+
skipped++;
|
|
11248
|
+
continue;
|
|
11249
|
+
}
|
|
11250
|
+
} else if (aliveResult.alive) {
|
|
11251
|
+
skipped++;
|
|
11252
|
+
continue;
|
|
11253
|
+
}
|
|
11254
|
+
try {
|
|
11255
|
+
await removeWorktree({
|
|
11256
|
+
root: resolved,
|
|
11257
|
+
worktree_path: entry.worktreePath,
|
|
11258
|
+
force: true
|
|
11259
|
+
});
|
|
11260
|
+
} catch (err) {
|
|
11261
|
+
failed.push({
|
|
11262
|
+
worktreePath: entry.worktreePath,
|
|
11263
|
+
error: `D 类 removeWorktree 失败: ${err instanceof Error ? err.message : String(err)}`
|
|
11264
|
+
});
|
|
11265
|
+
continue;
|
|
11266
|
+
}
|
|
11267
|
+
await deleteBranchIfExists({ root: resolved, branch: entry.branch }).catch(() => {});
|
|
11268
|
+
entry.status = "discarded";
|
|
11269
|
+
entry.updatedAt = new Date().toISOString();
|
|
11270
|
+
const reasonSource = aliveResult.source === "unknown" ? `unknown-timeout (registry.updatedAt 已老于 ${unknownTimeout / 3600000}h)` : `opencode session ${aliveResult.source}: dead`;
|
|
11271
|
+
cleaned.push({
|
|
11272
|
+
sessionId: entry.sessionId,
|
|
11273
|
+
worktreePath: entry.worktreePath,
|
|
11274
|
+
reason: `D 类语义孤儿 (${reasonSource})`
|
|
11275
|
+
});
|
|
11276
|
+
}
|
|
11277
|
+
});
|
|
11278
|
+
}
|
|
11279
|
+
const codeforgeWorktreeRoot = path11.resolve(path11.join(resolved, DEFAULT_WORKTREE_SUBDIR));
|
|
11280
|
+
const fsWorktreePaths = [];
|
|
11281
|
+
try {
|
|
11282
|
+
const names = await fs9.readdir(codeforgeWorktreeRoot);
|
|
11283
|
+
for (const name of names) {
|
|
11284
|
+
fsWorktreePaths.push(path11.resolve(path11.join(codeforgeWorktreeRoot, name)));
|
|
11285
|
+
}
|
|
11286
|
+
} catch {}
|
|
11287
|
+
const candidatePaths = new Set([
|
|
11288
|
+
...gitWorktreePaths,
|
|
11289
|
+
...fsWorktreePaths
|
|
11290
|
+
]);
|
|
11291
|
+
const reg = await readRegistry(resolved);
|
|
11292
|
+
const knownPaths = new Set(reg.entries.map((e) => path11.resolve(e.worktreePath)));
|
|
11293
|
+
for (const candidate of candidatePaths) {
|
|
11294
|
+
if (knownPaths.has(candidate))
|
|
11295
|
+
continue;
|
|
11296
|
+
if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path11.sep)) {
|
|
11297
|
+
continue;
|
|
11298
|
+
}
|
|
11299
|
+
let dirExists = true;
|
|
11300
|
+
try {
|
|
11301
|
+
const st = await fs9.stat(candidate);
|
|
11302
|
+
if (Date.now() - st.mtimeMs < ORPHAN_GRACE_MS) {
|
|
11303
|
+
skipped++;
|
|
11304
|
+
continue;
|
|
11305
|
+
}
|
|
11306
|
+
} catch {
|
|
11307
|
+
dirExists = false;
|
|
11308
|
+
}
|
|
11309
|
+
let removed = false;
|
|
11310
|
+
let lastError = null;
|
|
11311
|
+
try {
|
|
11312
|
+
await removeWorktree({
|
|
11313
|
+
root: resolved,
|
|
11314
|
+
worktree_path: candidate,
|
|
11315
|
+
force: true
|
|
11316
|
+
});
|
|
11317
|
+
removed = true;
|
|
11318
|
+
} catch (err) {
|
|
11319
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
11320
|
+
}
|
|
11321
|
+
if (removed) {
|
|
11322
|
+
try {
|
|
11323
|
+
await fs9.stat(candidate);
|
|
11324
|
+
removed = false;
|
|
11325
|
+
lastError = lastError ?? "git worktree remove 返回成功但目录仍存在(C 类 fs-only orphan)";
|
|
11326
|
+
} catch {}
|
|
11327
|
+
}
|
|
11328
|
+
if (!removed && dirExists) {
|
|
11329
|
+
try {
|
|
11330
|
+
await fs9.rm(candidate, { recursive: true, force: true });
|
|
11331
|
+
removed = true;
|
|
11332
|
+
} catch (err) {
|
|
11333
|
+
lastError = `git remove 失败: ${lastError}; fs.rm 也失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
11334
|
+
}
|
|
11335
|
+
}
|
|
11336
|
+
if (removed) {
|
|
11337
|
+
const reason = gitWorktreePaths.has(candidate) ? "fs-only orphan (git 知道但 registry 无 entry)" : "fs-only orphan (git 不知道的纯 fs 残留)";
|
|
11338
|
+
cleaned.push({ worktreePath: candidate, reason });
|
|
11339
|
+
} else {
|
|
11340
|
+
failed.push({ worktreePath: candidate, error: lastError ?? "unknown" });
|
|
11341
|
+
}
|
|
11342
|
+
}
|
|
11343
|
+
let discardedPruned = 0;
|
|
11344
|
+
try {
|
|
11345
|
+
const r = await pruneDiscardedRegistryEntries(resolved);
|
|
11346
|
+
discardedPruned = r.pruned;
|
|
11347
|
+
} catch {}
|
|
11348
|
+
let gitAdminPruned = 0;
|
|
11349
|
+
try {
|
|
11350
|
+
const out = await runGit2(resolved, ["worktree", "prune", "--verbose"]);
|
|
11351
|
+
gitAdminPruned = out.split(`
|
|
11352
|
+
`).filter((l) => /\bRemoving\b/i.test(l)).length;
|
|
11353
|
+
} catch {}
|
|
11354
|
+
return { cleaned, failed, skipped, discardedPruned, gitAdminPruned };
|
|
11355
|
+
}
|
|
11356
|
+
|
|
11357
|
+
// tools/review-approval.ts
|
|
11358
|
+
var description4 = [
|
|
11359
|
+
"reviewer 专用:写入 APPROVE 审批记录。",
|
|
11360
|
+
"**何时调用**:reviewer 给出 `## Decision\\nAPPROVE` 之前必须调本工具。",
|
|
11361
|
+
"**两层语义独立**(ADR:decision-token-vs-approval-verdict-layering):",
|
|
11362
|
+
" - 本工具 `verdict` 字段属审批层,合法值:APPROVE / APPROVE_WITH_NOTES",
|
|
11363
|
+
" - reviewer 输出的 `## Decision` 节首行属协议层,合法值:APPROVE / REQUEST_CHANGES / BLOCK",
|
|
11364
|
+
" - ⚠️ 严禁把 `APPROVE_WITH_NOTES` 字面量写进 `## Decision` 节首行(容错层会归一,但其他变体如 APPROVE_MINOR 会失败 → merge-loop 误判死循环)",
|
|
11365
|
+
' - ✅ 正确:verdict="APPROVE_WITH_NOTES" + `## Decision\\nAPPROVE`(首行写 APPROVE,详情在审批 notes)',
|
|
11366
|
+
"**pendingIds 格式**:推荐 `session:<sid>` / `plan:<plan_id>` / `decision:<hash>`;旧 `pc-<ts>-NNN` 仍兼容。",
|
|
11367
|
+
"**session:<sid> 自动补全**(ADR:review-approval-auto-covered-sha):",
|
|
11368
|
+
" coveredSha 缺失时工具自动从 worktree HEAD 捕获(捕获失败 fail-closed → pre-check 安全 miss → 走完整 review);",
|
|
11369
|
+
" reviewTarget 缺失时默认 'code'([Session Merge Review] 流按合同即 code review)。",
|
|
11370
|
+
" 显式传入的值优先,不覆盖。plan:/decision:/pc- id 不补全。",
|
|
11371
|
+
"**何时不调**:REQUEST_CHANGES / BLOCK 不调(无 APPROVE = 无审批记录)。",
|
|
11372
|
+
"**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。"
|
|
11373
|
+
].join(`
|
|
11374
|
+
`);
|
|
11375
|
+
var ArgsSchema4 = z4.object({
|
|
11376
|
+
verdict: z4.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
|
|
11377
|
+
pendingIds: z4.array(z4.string().min(1)).min(1, "pendingIds 至少 1 条").describe("本次 APPROVE 覆盖的 id 列表。推荐 session:<sid> / plan:<plan_id> / decision:<hash>;旧 pc-xxx 兼容"),
|
|
11378
|
+
notes: z4.string().min(1, "notes 不能为空").max(2000, "notes 过长(> 2000 字),建议拆条").describe("审阅意见摘要(建议 ≤ 500 字)"),
|
|
11379
|
+
decisionLine: z4.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
|
|
11380
|
+
source: z4.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
|
|
11381
|
+
reviewerAgent: z4.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
|
|
11382
|
+
sessionId: z4.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
|
|
11383
|
+
model: z4.string().optional().describe("审批模型 id(审计用,可选)"),
|
|
11384
|
+
coveredSha: z4.string().optional().describe("approval 写入时 worktree HEAD sha;对 session:<sid> id 工具会自动从 worktree HEAD 捕获(缺失时),显式传入值优先。ADR:review-approval-auto-covered-sha / ADR:merge-approval-pre-check"),
|
|
11385
|
+
reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。对 session:<sid> id 缺失时默认 'code',显式传入值优先。pre-check 仅放行 startsWith('code') 的值。ADR:review-approval-auto-covered-sha / ADR:merge-approval-pre-check")
|
|
11386
|
+
});
|
|
11387
|
+
var _approvalStore = null;
|
|
11388
|
+
function getApprovalStore() {
|
|
11389
|
+
if (!_approvalStore)
|
|
11390
|
+
_approvalStore = ApprovalStore.forProject(process.cwd());
|
|
11391
|
+
return _approvalStore;
|
|
11392
|
+
}
|
|
11393
|
+
var _worktreeResolvers = { getSessionWorktree, getCurrentWorktreeHead };
|
|
11394
|
+
async function execute4(input) {
|
|
11395
|
+
const parsed = ArgsSchema4.safeParse(input);
|
|
11396
|
+
if (!parsed.success) {
|
|
11349
11397
|
return {
|
|
11350
11398
|
ok: false,
|
|
11351
|
-
error:
|
|
11399
|
+
error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
11352
11400
|
};
|
|
11353
11401
|
}
|
|
11354
|
-
const
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
const
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
if (
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11402
|
+
const args = parsed.data;
|
|
11403
|
+
const approvals = getApprovalStore();
|
|
11404
|
+
const now = new Date().toISOString();
|
|
11405
|
+
const written = [];
|
|
11406
|
+
for (const id of args.pendingIds) {
|
|
11407
|
+
let coveredSha = args.coveredSha;
|
|
11408
|
+
let reviewTarget = args.reviewTarget;
|
|
11409
|
+
const sessionMatch = /^session:(.+)$/.exec(id);
|
|
11410
|
+
if (sessionMatch) {
|
|
11411
|
+
const sid = sessionMatch[1];
|
|
11412
|
+
if (!coveredSha) {
|
|
11413
|
+
try {
|
|
11414
|
+
const entry = await _worktreeResolvers.getSessionWorktree(sid, process.cwd());
|
|
11415
|
+
if (entry?.worktreePath) {
|
|
11416
|
+
const head = await _worktreeResolvers.getCurrentWorktreeHead(entry.worktreePath);
|
|
11417
|
+
if (head)
|
|
11418
|
+
coveredSha = head;
|
|
11419
|
+
}
|
|
11420
|
+
} catch {}
|
|
11421
|
+
}
|
|
11422
|
+
if (!reviewTarget)
|
|
11423
|
+
reviewTarget = "code";
|
|
11367
11424
|
}
|
|
11368
|
-
|
|
11425
|
+
const meta = {
|
|
11426
|
+
pendingId: id,
|
|
11427
|
+
verdict: args.verdict,
|
|
11428
|
+
reviewer: {
|
|
11429
|
+
agent: args.reviewerAgent ?? (args.source === "codeforge-fallback" ? "codeforge" : "reviewer"),
|
|
11430
|
+
...args.sessionId ? { sessionId: args.sessionId } : {},
|
|
11431
|
+
...args.model ? { model: args.model } : {},
|
|
11432
|
+
source: args.source ?? "reviewer"
|
|
11433
|
+
},
|
|
11434
|
+
targets: [],
|
|
11435
|
+
decisionLine: args.decisionLine ?? args.verdict,
|
|
11436
|
+
notes: args.notes,
|
|
11437
|
+
createdAt: now,
|
|
11438
|
+
...coveredSha ? { coveredSha } : {},
|
|
11439
|
+
...reviewTarget ? { reviewTarget } : {},
|
|
11440
|
+
escapeHatch: null
|
|
11441
|
+
};
|
|
11442
|
+
const file = await approvals.record(meta);
|
|
11443
|
+
written.push({ pendingId: id, file });
|
|
11369
11444
|
}
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11445
|
+
return { ok: true, written };
|
|
11446
|
+
}
|
|
11447
|
+
// tools/browser-navigate.ts
|
|
11448
|
+
import { z as z5 } from "zod";
|
|
11449
|
+
|
|
11450
|
+
// lib/browser-control.ts
|
|
11451
|
+
import * as path12 from "node:path";
|
|
11452
|
+
var DEFAULT_CONFIG2 = {
|
|
11453
|
+
enabled: false,
|
|
11454
|
+
headless: true,
|
|
11455
|
+
allow: ["^https?://"],
|
|
11456
|
+
block: ["^file:", "^data:", "^about:", "^javascript:"],
|
|
11457
|
+
actionTimeoutMs: 15000,
|
|
11458
|
+
idleTimeoutMs: 5 * 60000,
|
|
11459
|
+
screenshotDir: "",
|
|
11460
|
+
bufferLimit: 500
|
|
11461
|
+
};
|
|
11462
|
+
function defaultScreenshotDir(root = process.cwd()) {
|
|
11463
|
+
return path12.join(runtimeDir(root), "browser", "screenshots");
|
|
11464
|
+
}
|
|
11465
|
+
function checkUrl(url, cfg = DEFAULT_CONFIG2) {
|
|
11466
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
11467
|
+
return { ok: false, reason: "empty_url" };
|
|
11468
|
+
}
|
|
11469
|
+
for (const pat of cfg.block) {
|
|
11470
|
+
try {
|
|
11471
|
+
if (new RegExp(pat).test(url)) {
|
|
11472
|
+
return { ok: false, reason: `blocked: matches ${pat}` };
|
|
11473
|
+
}
|
|
11474
|
+
} catch {}
|
|
11475
|
+
}
|
|
11476
|
+
if (cfg.allow.length === 0)
|
|
11477
|
+
return { ok: true };
|
|
11478
|
+
for (const pat of cfg.allow) {
|
|
11479
|
+
try {
|
|
11480
|
+
if (new RegExp(pat).test(url))
|
|
11481
|
+
return { ok: true };
|
|
11482
|
+
} catch {}
|
|
11483
|
+
}
|
|
11484
|
+
return { ok: false, reason: "not_in_allow_list" };
|
|
11485
|
+
}
|
|
11486
|
+
|
|
11487
|
+
class NoopBrowserController {
|
|
11488
|
+
reason;
|
|
11489
|
+
constructor(reason = "browser is disabled (set tools.browser.enabled=true to use)") {
|
|
11490
|
+
this.reason = reason;
|
|
11491
|
+
}
|
|
11492
|
+
async navigate(url) {
|
|
11493
|
+
return { ok: false, url, error: this.reason };
|
|
11494
|
+
}
|
|
11495
|
+
async screenshot() {
|
|
11496
|
+
return { ok: false, error: this.reason };
|
|
11497
|
+
}
|
|
11498
|
+
async click(selector) {
|
|
11499
|
+
return { ok: false, selector, found: false, error: this.reason };
|
|
11500
|
+
}
|
|
11501
|
+
async fill(selector, _value) {
|
|
11502
|
+
return { ok: false, selector, found: false, error: this.reason };
|
|
11503
|
+
}
|
|
11504
|
+
async consoleLogs() {
|
|
11505
|
+
return [];
|
|
11506
|
+
}
|
|
11507
|
+
async networkRequests() {
|
|
11508
|
+
return [];
|
|
11509
|
+
}
|
|
11510
|
+
async close() {}
|
|
11511
|
+
}
|
|
11512
|
+
async function tryCreatePlaywrightController(cfg = DEFAULT_CONFIG2, resolver = defaultPlaywrightResolver) {
|
|
11513
|
+
if (!cfg.enabled)
|
|
11514
|
+
return null;
|
|
11515
|
+
let mod;
|
|
11516
|
+
try {
|
|
11517
|
+
mod = await resolver();
|
|
11518
|
+
} catch {
|
|
11519
|
+
return null;
|
|
11376
11520
|
}
|
|
11521
|
+
if (!mod || typeof mod !== "object")
|
|
11522
|
+
return null;
|
|
11523
|
+
const chromium = mod.chromium;
|
|
11524
|
+
if (!chromium || typeof chromium.launch !== "function")
|
|
11525
|
+
return null;
|
|
11526
|
+
const browser = await chromium.launch({ headless: cfg.headless });
|
|
11527
|
+
const context = await browser.newContext();
|
|
11528
|
+
const page = await context.newPage();
|
|
11529
|
+
const consoleBuf = [];
|
|
11530
|
+
const networkBuf = [];
|
|
11531
|
+
page.on?.("console", (...args) => {
|
|
11532
|
+
const msg = args[0];
|
|
11533
|
+
consoleBuf.push({
|
|
11534
|
+
level: normalizeLevel(msg.type?.()),
|
|
11535
|
+
text: msg.text?.() ?? "",
|
|
11536
|
+
timestamp: Date.now()
|
|
11537
|
+
});
|
|
11538
|
+
if (consoleBuf.length > cfg.bufferLimit)
|
|
11539
|
+
consoleBuf.splice(0, consoleBuf.length - cfg.bufferLimit);
|
|
11540
|
+
});
|
|
11541
|
+
page.on?.("request", (...args) => {
|
|
11542
|
+
const req = args[0];
|
|
11543
|
+
networkBuf.push({
|
|
11544
|
+
url: req.url(),
|
|
11545
|
+
method: req.method(),
|
|
11546
|
+
start_ts: Date.now()
|
|
11547
|
+
});
|
|
11548
|
+
if (networkBuf.length > cfg.bufferLimit)
|
|
11549
|
+
networkBuf.splice(0, networkBuf.length - cfg.bufferLimit);
|
|
11550
|
+
});
|
|
11551
|
+
page.on?.("response", (...args) => {
|
|
11552
|
+
const res = args[0];
|
|
11553
|
+
const item = networkBuf.find((n) => n.url === res.url() && n.end_ts === undefined);
|
|
11554
|
+
if (item) {
|
|
11555
|
+
item.status = res.status();
|
|
11556
|
+
item.end_ts = Date.now();
|
|
11557
|
+
}
|
|
11558
|
+
});
|
|
11559
|
+
page.on?.("requestfailed", (...args) => {
|
|
11560
|
+
const req = args[0];
|
|
11561
|
+
const item = networkBuf.find((n) => n.url === req.url() && n.end_ts === undefined);
|
|
11562
|
+
if (item) {
|
|
11563
|
+
item.failed = req.failure?.()?.errorText ?? "failed";
|
|
11564
|
+
item.end_ts = Date.now();
|
|
11565
|
+
}
|
|
11566
|
+
});
|
|
11377
11567
|
return {
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11568
|
+
async navigate(url) {
|
|
11569
|
+
const c = checkUrl(url, cfg);
|
|
11570
|
+
if (!c.ok)
|
|
11571
|
+
return { ok: false, url, error: c.reason };
|
|
11572
|
+
const start = Date.now();
|
|
11573
|
+
try {
|
|
11574
|
+
const resp = await page.goto(url, { timeout: cfg.actionTimeoutMs });
|
|
11575
|
+
return {
|
|
11576
|
+
ok: true,
|
|
11577
|
+
url,
|
|
11578
|
+
status: resp?.status?.() ?? 200,
|
|
11579
|
+
loaded_in_ms: Date.now() - start
|
|
11580
|
+
};
|
|
11581
|
+
} catch (err) {
|
|
11582
|
+
return { ok: false, url, error: describe3(err) };
|
|
11583
|
+
}
|
|
11584
|
+
},
|
|
11585
|
+
async screenshot(opts) {
|
|
11586
|
+
try {
|
|
11587
|
+
const dir = cfg.screenshotDir && cfg.screenshotDir.trim().length > 0 ? cfg.screenshotDir : defaultScreenshotDir();
|
|
11588
|
+
const path13 = `${dir}/${Date.now()}.png`;
|
|
11589
|
+
if (opts?.selector) {
|
|
11590
|
+
const el = await page.locator(opts.selector).first();
|
|
11591
|
+
await el.screenshot({ path: path13 });
|
|
11592
|
+
} else {
|
|
11593
|
+
await page.screenshot({ path: path13, fullPage: opts?.fullPage });
|
|
11594
|
+
}
|
|
11595
|
+
return { ok: true, path: path13 };
|
|
11596
|
+
} catch (err) {
|
|
11597
|
+
return { ok: false, error: describe3(err) };
|
|
11598
|
+
}
|
|
11599
|
+
},
|
|
11600
|
+
async click(selector) {
|
|
11601
|
+
try {
|
|
11602
|
+
await page.locator(selector).first().click({ timeout: cfg.actionTimeoutMs });
|
|
11603
|
+
return { ok: true, selector, found: true };
|
|
11604
|
+
} catch (err) {
|
|
11605
|
+
return { ok: false, selector, found: false, error: describe3(err) };
|
|
11606
|
+
}
|
|
11607
|
+
},
|
|
11608
|
+
async fill(selector, value) {
|
|
11609
|
+
try {
|
|
11610
|
+
await page.locator(selector).first().fill(value, { timeout: cfg.actionTimeoutMs });
|
|
11611
|
+
return { ok: true, selector, found: true };
|
|
11612
|
+
} catch (err) {
|
|
11613
|
+
return { ok: false, selector, found: false, error: describe3(err) };
|
|
11614
|
+
}
|
|
11615
|
+
},
|
|
11616
|
+
async consoleLogs(opts = {}) {
|
|
11617
|
+
return consoleBuf.filter((e) => (opts.sinceTs === undefined || e.timestamp >= opts.sinceTs) && (opts.level === undefined || e.level === opts.level));
|
|
11618
|
+
},
|
|
11619
|
+
async networkRequests(opts = {}) {
|
|
11620
|
+
return networkBuf.filter((e) => (opts.sinceTs === undefined || e.start_ts >= opts.sinceTs) && (opts.method === undefined || e.method.toUpperCase() === opts.method.toUpperCase()));
|
|
11621
|
+
},
|
|
11622
|
+
async close() {
|
|
11623
|
+
try {
|
|
11624
|
+
await context.close();
|
|
11625
|
+
} catch {}
|
|
11626
|
+
try {
|
|
11627
|
+
await browser.close();
|
|
11628
|
+
} catch {}
|
|
11388
11629
|
}
|
|
11389
11630
|
};
|
|
11390
11631
|
}
|
|
11391
|
-
function
|
|
11392
|
-
|
|
11393
|
-
return { ok: false, error: `category[${name}]: must be object` };
|
|
11394
|
-
}
|
|
11395
|
-
const o = raw;
|
|
11396
|
-
if (typeof o.model !== "string" || !PROVIDER_MODEL_RE.test(o.model)) {
|
|
11397
|
-
return {
|
|
11398
|
-
ok: false,
|
|
11399
|
-
error: `category[${name}].model invalid: "${String(o.model)}"`
|
|
11400
|
-
};
|
|
11401
|
-
}
|
|
11402
|
-
const fallbacks = normalizeFallbackList(`category[${name}].fallback_models`, o.fallback_models);
|
|
11403
|
-
if (!fallbacks.ok)
|
|
11404
|
-
return { ok: false, error: fallbacks.error };
|
|
11405
|
-
const thinking = o.thinking !== undefined ? normalizeThinking(`category[${name}]`, o.thinking) : undefined;
|
|
11406
|
-
if (thinking && !thinking.ok)
|
|
11407
|
-
return { ok: false, error: thinking.error };
|
|
11408
|
-
return {
|
|
11409
|
-
ok: true,
|
|
11410
|
-
binding: {
|
|
11411
|
-
model: o.model,
|
|
11412
|
-
variant: typeof o.variant === "string" ? o.variant : undefined,
|
|
11413
|
-
thinking: thinking?.value,
|
|
11414
|
-
fallback_models: fallbacks.value,
|
|
11415
|
-
_doc: typeof o._doc === "string" ? o._doc : undefined
|
|
11416
|
-
}
|
|
11417
|
-
};
|
|
11632
|
+
async function defaultPlaywrightResolver() {
|
|
11633
|
+
return await import("playwright");
|
|
11418
11634
|
}
|
|
11419
|
-
function
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
return { ok: false, error: `${ctx}: invalid entry "${String(it)}"` };
|
|
11429
|
-
}
|
|
11430
|
-
if (seen.has(it))
|
|
11431
|
-
continue;
|
|
11432
|
-
seen.add(it);
|
|
11433
|
-
result.push(it);
|
|
11635
|
+
function normalizeLevel(t) {
|
|
11636
|
+
switch (t) {
|
|
11637
|
+
case "info":
|
|
11638
|
+
case "warn":
|
|
11639
|
+
case "error":
|
|
11640
|
+
case "debug":
|
|
11641
|
+
return t;
|
|
11642
|
+
default:
|
|
11643
|
+
return "log";
|
|
11434
11644
|
}
|
|
11435
|
-
return { ok: true, value: result };
|
|
11436
11645
|
}
|
|
11437
|
-
function
|
|
11438
|
-
if (
|
|
11439
|
-
return
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
return
|
|
11444
|
-
}
|
|
11445
|
-
|
|
11446
|
-
return { ok: false, error: `${ctx}.thinking.budget_tokens: must be non-negative number` };
|
|
11646
|
+
function describe3(err) {
|
|
11647
|
+
if (err instanceof Error)
|
|
11648
|
+
return err.message;
|
|
11649
|
+
if (typeof err === "string")
|
|
11650
|
+
return err;
|
|
11651
|
+
try {
|
|
11652
|
+
return JSON.stringify(err);
|
|
11653
|
+
} catch {
|
|
11654
|
+
return String(err);
|
|
11447
11655
|
}
|
|
11448
|
-
return { ok: true, value: { ...o, type: o.type } };
|
|
11449
11656
|
}
|
|
11450
|
-
function
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
const o = raw;
|
|
11455
|
-
const cfg = { ...DEFAULT_RUNTIME_FALLBACK };
|
|
11456
|
-
if (o.enabled !== undefined) {
|
|
11457
|
-
if (typeof o.enabled !== "boolean")
|
|
11458
|
-
return { ok: false, error: "runtime_fallback.enabled: boolean" };
|
|
11459
|
-
cfg.enabled = o.enabled;
|
|
11460
|
-
}
|
|
11461
|
-
if (o.max_fallback_attempts !== undefined) {
|
|
11462
|
-
if (typeof o.max_fallback_attempts !== "number" || o.max_fallback_attempts < 1) {
|
|
11463
|
-
return { ok: false, error: "runtime_fallback.max_fallback_attempts: integer >=1" };
|
|
11464
|
-
}
|
|
11465
|
-
cfg.max_fallback_attempts = Math.floor(o.max_fallback_attempts);
|
|
11466
|
-
}
|
|
11467
|
-
if (o.notify_on_fallback !== undefined) {
|
|
11468
|
-
if (typeof o.notify_on_fallback !== "boolean") {
|
|
11469
|
-
return { ok: false, error: "runtime_fallback.notify_on_fallback: boolean" };
|
|
11470
|
-
}
|
|
11471
|
-
cfg.notify_on_fallback = o.notify_on_fallback;
|
|
11657
|
+
async function createBrowserController(opts = {}) {
|
|
11658
|
+
const cfg = { ...DEFAULT_CONFIG2, ...opts.cfg };
|
|
11659
|
+
if (!cfg.enabled) {
|
|
11660
|
+
return new NoopBrowserController("browser disabled in config");
|
|
11472
11661
|
}
|
|
11473
|
-
if (
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
cfg.trigger_events = [...new Set(o.trigger_events)];
|
|
11662
|
+
if (opts.factory) {
|
|
11663
|
+
const c = await opts.factory(cfg);
|
|
11664
|
+
if (c)
|
|
11665
|
+
return c;
|
|
11478
11666
|
}
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11667
|
+
const playwright = await tryCreatePlaywrightController(cfg);
|
|
11668
|
+
if (playwright)
|
|
11669
|
+
return playwright;
|
|
11670
|
+
return new NoopBrowserController("browser enabled but no driver available (npm i playwright 后重试)");
|
|
11482
11671
|
}
|
|
11483
|
-
|
|
11484
|
-
|
|
11672
|
+
|
|
11673
|
+
// tools/browser-navigate.ts
|
|
11674
|
+
var description5 = [
|
|
11675
|
+
"打开指定 URL(受 tools.browser 配置控制;默认禁用)。",
|
|
11676
|
+
"**何时调用**:需要看 web 页面真实渲染、调试前端、复现 bug 报告链接。",
|
|
11677
|
+
"**注意**:仅允许 http/https;file:/data: 等协议被默认安全策略拒绝。"
|
|
11678
|
+
].join(`
|
|
11679
|
+
`);
|
|
11680
|
+
var ArgsSchema5 = z5.object({
|
|
11681
|
+
url: z5.string().min(1).describe("要打开的 URL;必须是 http(s) 协议")
|
|
11682
|
+
});
|
|
11683
|
+
var _controller = null;
|
|
11684
|
+
var _cfg;
|
|
11685
|
+
async function getController() {
|
|
11686
|
+
if (!_controller) {
|
|
11687
|
+
_controller = await createBrowserController({ cfg: _cfg });
|
|
11688
|
+
}
|
|
11689
|
+
return _controller;
|
|
11485
11690
|
}
|
|
11486
|
-
function
|
|
11487
|
-
|
|
11691
|
+
async function execute5(input) {
|
|
11692
|
+
const parsed = ArgsSchema5.safeParse(input);
|
|
11693
|
+
if (!parsed.success) {
|
|
11488
11694
|
return {
|
|
11489
11695
|
ok: false,
|
|
11490
|
-
|
|
11696
|
+
url: typeof input?.url === "string" ? String(input.url) : "",
|
|
11697
|
+
error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
11491
11698
|
};
|
|
11492
11699
|
}
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
}
|
|
11502
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11503
|
-
return {
|
|
11504
|
-
ok: false,
|
|
11505
|
-
error: `agent[${agentName}].tier_overrides.${key}: 必须是 object`
|
|
11506
|
-
};
|
|
11507
|
-
}
|
|
11508
|
-
const ov = value;
|
|
11509
|
-
const partial = {};
|
|
11510
|
-
if (ov.level !== undefined) {
|
|
11511
|
-
if (ov.level !== key) {
|
|
11512
|
-
return {
|
|
11513
|
-
ok: false,
|
|
11514
|
-
error: `agent[${agentName}].tier_overrides.${key}.level="${String(ov.level)}" 与 key "${key}" 错配`
|
|
11515
|
-
};
|
|
11516
|
-
}
|
|
11517
|
-
partial.level = key;
|
|
11518
|
-
}
|
|
11519
|
-
if (ov.model !== undefined) {
|
|
11520
|
-
if (typeof ov.model !== "string" || !PROVIDER_MODEL_RE.test(ov.model)) {
|
|
11521
|
-
return {
|
|
11522
|
-
ok: false,
|
|
11523
|
-
error: `agent[${agentName}].tier_overrides.${key}.model="${String(ov.model)}" 格式非法 (期望 <provider>/<id>)`
|
|
11524
|
-
};
|
|
11525
|
-
}
|
|
11526
|
-
partial.model = ov.model;
|
|
11527
|
-
}
|
|
11528
|
-
if (ov.thinking !== undefined) {
|
|
11529
|
-
const t = normalizeThinking(`agent[${agentName}].tier_overrides.${key}`, ov.thinking);
|
|
11530
|
-
if (!t.ok)
|
|
11531
|
-
return { ok: false, error: t.error };
|
|
11532
|
-
partial.thinking = t.value;
|
|
11533
|
-
}
|
|
11534
|
-
if (ov.fallback_models !== undefined) {
|
|
11535
|
-
const f = normalizeFallbackList(`agent[${agentName}].tier_overrides.${key}.fallback_models`, ov.fallback_models);
|
|
11536
|
-
if (!f.ok)
|
|
11537
|
-
return { ok: false, error: f.error };
|
|
11538
|
-
partial.fallback_models = f.value;
|
|
11539
|
-
}
|
|
11540
|
-
result[key] = partial;
|
|
11541
|
-
}
|
|
11542
|
-
return { ok: true, value: result };
|
|
11543
|
-
}
|
|
11544
|
-
function normalizeTiers(raw) {
|
|
11545
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
11546
|
-
return { ok: false, error: "models.tiers 必须是 object(不是 array / null)" };
|
|
11547
|
-
}
|
|
11548
|
-
const o = raw;
|
|
11549
|
-
const cfg = {};
|
|
11550
|
-
if (o.category_map !== undefined) {
|
|
11551
|
-
if (!o.category_map || typeof o.category_map !== "object" || Array.isArray(o.category_map)) {
|
|
11552
|
-
return { ok: false, error: "models.tiers.category_map 必须是 object" };
|
|
11553
|
-
}
|
|
11554
|
-
const map = {};
|
|
11555
|
-
for (const [key, value] of Object.entries(o.category_map)) {
|
|
11556
|
-
if (!isValidTierLevel(key)) {
|
|
11557
|
-
return {
|
|
11558
|
-
ok: false,
|
|
11559
|
-
error: `models.tiers.category_map.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
|
|
11560
|
-
};
|
|
11561
|
-
}
|
|
11562
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
11563
|
-
return {
|
|
11564
|
-
ok: false,
|
|
11565
|
-
error: `models.tiers.category_map.${key}: 必须是非空字符串(category 名)`
|
|
11566
|
-
};
|
|
11567
|
-
}
|
|
11568
|
-
map[key] = value;
|
|
11569
|
-
}
|
|
11570
|
-
cfg.category_map = map;
|
|
11571
|
-
}
|
|
11572
|
-
if (typeof o._doc === "string")
|
|
11573
|
-
cfg._doc = o._doc;
|
|
11574
|
-
return { ok: true, cfg };
|
|
11575
|
-
}
|
|
11576
|
-
function resolveAgentModel(config, agent) {
|
|
11577
|
-
const a = config.agents[agent];
|
|
11578
|
-
if (!a)
|
|
11579
|
-
return null;
|
|
11580
|
-
const seen = new Set;
|
|
11581
|
-
const chain = [];
|
|
11582
|
-
const push = (m) => {
|
|
11583
|
-
if (!m || seen.has(m))
|
|
11584
|
-
return;
|
|
11585
|
-
seen.add(m);
|
|
11586
|
-
chain.push(m);
|
|
11587
|
-
};
|
|
11588
|
-
push(a.model);
|
|
11589
|
-
for (const m of a.fallback_models ?? [])
|
|
11590
|
-
push(m);
|
|
11591
|
-
let categoryName;
|
|
11592
|
-
let usedCategory = false;
|
|
11593
|
-
if (a.category && config.categories?.[a.category]) {
|
|
11594
|
-
categoryName = a.category;
|
|
11595
|
-
const c = config.categories[a.category];
|
|
11596
|
-
const beforeLen = chain.length;
|
|
11597
|
-
push(c.model);
|
|
11598
|
-
for (const m of c.fallback_models ?? [])
|
|
11599
|
-
push(m);
|
|
11600
|
-
usedCategory = chain.length > beforeLen;
|
|
11700
|
+
try {
|
|
11701
|
+
const ctrl = await getController();
|
|
11702
|
+
return await ctrl.navigate(parsed.data.url);
|
|
11703
|
+
} catch (err) {
|
|
11704
|
+
return {
|
|
11705
|
+
ok: false,
|
|
11706
|
+
url: parsed.data.url,
|
|
11707
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11708
|
+
};
|
|
11601
11709
|
}
|
|
11602
|
-
const source = a.fallback_models && a.fallback_models.length > 0 ? usedCategory ? "merged" : "agent" : usedCategory ? "category" : "agent";
|
|
11603
|
-
return {
|
|
11604
|
-
agent,
|
|
11605
|
-
model: a.model,
|
|
11606
|
-
variant: a.variant,
|
|
11607
|
-
thinking: a.thinking,
|
|
11608
|
-
chain,
|
|
11609
|
-
source,
|
|
11610
|
-
category: categoryName
|
|
11611
|
-
};
|
|
11612
11710
|
}
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11711
|
+
// tools/browser-click.ts
|
|
11712
|
+
import { z as z6 } from "zod";
|
|
11713
|
+
var description6 = [
|
|
11714
|
+
"点击页面元素。属于「写操作」,semi 模式下应当让用户确认。",
|
|
11715
|
+
"**何时调用**:表单提交、按钮触发、SPA 导航。",
|
|
11716
|
+
"**避坑**:selector 优先用稳定属性(data-testid > role > 文本 > id),尽量不用 xpath。"
|
|
11717
|
+
].join(`
|
|
11718
|
+
`);
|
|
11719
|
+
var ArgsSchema6 = z6.object({
|
|
11720
|
+
selector: z6.string().min(1).describe("CSS / Playwright locator 字符串,必须能唯一定位")
|
|
11721
|
+
});
|
|
11722
|
+
var _controller2 = null;
|
|
11723
|
+
var _cfg2;
|
|
11724
|
+
async function getController2() {
|
|
11725
|
+
if (!_controller2) {
|
|
11726
|
+
_controller2 = await createBrowserController({ cfg: _cfg2 });
|
|
11727
|
+
}
|
|
11728
|
+
return _controller2;
|
|
11621
11729
|
}
|
|
11622
|
-
function
|
|
11623
|
-
|
|
11730
|
+
async function execute6(input) {
|
|
11731
|
+
const parsed = ArgsSchema6.safeParse(input);
|
|
11732
|
+
if (!parsed.success) {
|
|
11733
|
+
return {
|
|
11734
|
+
ok: false,
|
|
11735
|
+
selector: typeof input?.selector === "string" ? String(input.selector) : "",
|
|
11736
|
+
found: false,
|
|
11737
|
+
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11738
|
+
};
|
|
11739
|
+
}
|
|
11740
|
+
try {
|
|
11741
|
+
const ctrl = await getController2();
|
|
11742
|
+
return await ctrl.click(parsed.data.selector);
|
|
11743
|
+
} catch (err) {
|
|
11744
|
+
return {
|
|
11745
|
+
ok: false,
|
|
11746
|
+
selector: parsed.data.selector,
|
|
11747
|
+
found: false,
|
|
11748
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11749
|
+
};
|
|
11750
|
+
}
|
|
11624
11751
|
}
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
var
|
|
11628
|
-
"
|
|
11629
|
-
"
|
|
11630
|
-
"
|
|
11631
|
-
"- 当前模型不可用时,决定下一个该试哪个",
|
|
11632
|
-
"- 审计:把整个项目的模型配置一次性列给用户看",
|
|
11633
|
-
"返回 JSON:{ ok, agents: [...], current_chain?, next_fallback? }"
|
|
11752
|
+
// tools/browser-fill.ts
|
|
11753
|
+
import { z as z7 } from "zod";
|
|
11754
|
+
var description7 = [
|
|
11755
|
+
"向 input/textarea/contenteditable 元素写入文本。",
|
|
11756
|
+
"**何时调用**:登录表单、搜索框、富文本初始化。",
|
|
11757
|
+
"**安全**:value 不会被自动 mask;不要把真实凭据塞进来。"
|
|
11634
11758
|
].join(`
|
|
11635
11759
|
`);
|
|
11636
|
-
var
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
root: z11.string().optional().describe("项目根目录,默认 process.cwd()"),
|
|
11640
|
-
config_file: z11.string().optional().describe("配置文件名;默认 codeforge.json")
|
|
11760
|
+
var ArgsSchema7 = z7.object({
|
|
11761
|
+
selector: z7.string().min(1).describe("CSS / Playwright locator"),
|
|
11762
|
+
value: z7.string().describe("要填入的文本;原样写入,不做转义")
|
|
11641
11763
|
});
|
|
11642
|
-
|
|
11643
|
-
|
|
11764
|
+
var _controller3 = null;
|
|
11765
|
+
var _cfg3;
|
|
11766
|
+
async function getController3() {
|
|
11767
|
+
if (!_controller3) {
|
|
11768
|
+
_controller3 = await createBrowserController({ cfg: _cfg3 });
|
|
11769
|
+
}
|
|
11770
|
+
return _controller3;
|
|
11771
|
+
}
|
|
11772
|
+
async function execute7(input) {
|
|
11773
|
+
const parsed = ArgsSchema7.safeParse(input);
|
|
11644
11774
|
if (!parsed.success) {
|
|
11645
11775
|
return {
|
|
11646
11776
|
ok: false,
|
|
11647
|
-
|
|
11777
|
+
selector: typeof input?.selector === "string" ? String(input.selector) : "",
|
|
11778
|
+
found: false,
|
|
11779
|
+
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11648
11780
|
};
|
|
11649
11781
|
}
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
});
|
|
11655
|
-
if (!loadResult.ok || !loadResult.config) {
|
|
11782
|
+
try {
|
|
11783
|
+
const ctrl = await getController3();
|
|
11784
|
+
return await ctrl.fill(parsed.data.selector, parsed.data.value);
|
|
11785
|
+
} catch (err) {
|
|
11656
11786
|
return {
|
|
11657
11787
|
ok: false,
|
|
11658
|
-
|
|
11659
|
-
|
|
11788
|
+
selector: parsed.data.selector,
|
|
11789
|
+
found: false,
|
|
11790
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11660
11791
|
};
|
|
11661
11792
|
}
|
|
11662
|
-
const cfg = loadResult.config;
|
|
11663
|
-
const allChains = listAllAgentChains(cfg).map(toEntry);
|
|
11664
|
-
const out = {
|
|
11665
|
-
ok: true,
|
|
11666
|
-
config_path: loadResult.path ?? "",
|
|
11667
|
-
warnings: loadResult.warnings,
|
|
11668
|
-
agents: allChains
|
|
11669
|
-
};
|
|
11670
|
-
if (args.agent) {
|
|
11671
|
-
const r = resolveAgentModel(cfg, args.agent);
|
|
11672
|
-
if (!r) {
|
|
11673
|
-
return {
|
|
11674
|
-
ok: false,
|
|
11675
|
-
error: `agent_not_found: "${args.agent}" 未在 codeforge.json 配置`,
|
|
11676
|
-
config_path: loadResult.path
|
|
11677
|
-
};
|
|
11678
|
-
}
|
|
11679
|
-
out.current_chain = toEntry(r);
|
|
11680
|
-
if (args.current) {
|
|
11681
|
-
out.next_fallback = nextFallback(cfg, args.agent, args.current);
|
|
11682
|
-
}
|
|
11683
|
-
}
|
|
11684
|
-
return out;
|
|
11685
11793
|
}
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11794
|
+
// tools/browser-screenshot.ts
|
|
11795
|
+
import { z as z8 } from "zod";
|
|
11796
|
+
var description8 = [
|
|
11797
|
+
"截取当前页面(或指定元素)的屏幕快照。",
|
|
11798
|
+
"**何时调用**:前端 dev 调试、回归对比、用户报 bug 没截图时让 agent 自取。",
|
|
11799
|
+
"**注意**:必须先 navigate;输出路径写到 runtime 目录的 browser/screenshots/(XDG 全局位置,详见 lib/runtime-paths.ts)。"
|
|
11800
|
+
].join(`
|
|
11801
|
+
`);
|
|
11802
|
+
var ArgsSchema8 = z8.object({
|
|
11803
|
+
fullPage: z8.boolean().optional().describe("是否截全长页面(默认仅可视区)"),
|
|
11804
|
+
selector: z8.string().optional().describe("CSS 选择器;指定时只截该元素")
|
|
11805
|
+
});
|
|
11806
|
+
var _controller4 = null;
|
|
11807
|
+
var _cfg4;
|
|
11808
|
+
async function getController4() {
|
|
11809
|
+
if (!_controller4) {
|
|
11810
|
+
_controller4 = await createBrowserController({ cfg: _cfg4 });
|
|
11811
|
+
}
|
|
11812
|
+
return _controller4;
|
|
11694
11813
|
}
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
init_worktree_ops();
|
|
11700
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
11701
|
-
import { promises as fs10 } from "node:fs";
|
|
11702
|
-
import * as path13 from "node:path";
|
|
11703
|
-
|
|
11704
|
-
// lib/file-lock.ts
|
|
11705
|
-
import { promises as fs9 } from "node:fs";
|
|
11706
|
-
import * as crypto3 from "node:crypto";
|
|
11707
|
-
import * as os4 from "node:os";
|
|
11708
|
-
import * as path12 from "node:path";
|
|
11709
|
-
async function withFileLock(lockPath, fn, opts = {}) {
|
|
11710
|
-
const retryMs = opts.retryMs ?? 50;
|
|
11711
|
-
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
11712
|
-
const staleMs = opts.staleMs ?? 300000;
|
|
11713
|
-
await fs9.mkdir(path12.dirname(lockPath), { recursive: true });
|
|
11714
|
-
const deadline = Date.now() + timeoutMs;
|
|
11715
|
-
const myHost = os4.hostname();
|
|
11716
|
-
const myToken = crypto3.randomBytes(16).toString("hex");
|
|
11717
|
-
let acquired = false;
|
|
11718
|
-
while (!acquired) {
|
|
11719
|
-
try {
|
|
11720
|
-
const handle = await fs9.open(lockPath, "wx");
|
|
11721
|
-
try {
|
|
11722
|
-
const meta = {
|
|
11723
|
-
pid: process.pid,
|
|
11724
|
-
host: myHost,
|
|
11725
|
-
token: myToken,
|
|
11726
|
-
acquired_at: new Date().toISOString()
|
|
11727
|
-
};
|
|
11728
|
-
await handle.writeFile(JSON.stringify(meta), "utf8");
|
|
11729
|
-
await handle.close();
|
|
11730
|
-
} catch (writeErr) {
|
|
11731
|
-
await handle.close().catch(() => {});
|
|
11732
|
-
await fs9.rm(lockPath, { force: true }).catch(() => {});
|
|
11733
|
-
throw writeErr;
|
|
11734
|
-
}
|
|
11735
|
-
acquired = true;
|
|
11736
|
-
} catch (err) {
|
|
11737
|
-
const e = err;
|
|
11738
|
-
if (e.code !== "EEXIST")
|
|
11739
|
-
throw err;
|
|
11740
|
-
const cleaned = await tryCleanStaleLock(lockPath, staleMs, myHost);
|
|
11741
|
-
if (cleaned)
|
|
11742
|
-
continue;
|
|
11743
|
-
if (Date.now() >= deadline) {
|
|
11744
|
-
throw new Error(`withFileLock: 等待 ${lockPath} 超过 ${timeoutMs}ms 仍未拿到锁`);
|
|
11745
|
-
}
|
|
11746
|
-
await sleep(retryMs);
|
|
11747
|
-
}
|
|
11814
|
+
async function execute8(input) {
|
|
11815
|
+
const parsed = ArgsSchema8.safeParse(input);
|
|
11816
|
+
if (!parsed.success) {
|
|
11817
|
+
return { ok: false, error: parsed.error.issues.map((i) => i.message).join("; ") };
|
|
11748
11818
|
}
|
|
11749
11819
|
try {
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11820
|
+
const ctrl = await getController4();
|
|
11821
|
+
return await ctrl.screenshot(parsed.data);
|
|
11822
|
+
} catch (err) {
|
|
11823
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
11753
11824
|
}
|
|
11754
11825
|
}
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11826
|
+
// tools/browser-console.ts
|
|
11827
|
+
import { z as z9 } from "zod";
|
|
11828
|
+
var description9 = [
|
|
11829
|
+
"读取浏览器控制台缓冲区(log/info/warn/error/debug)。",
|
|
11830
|
+
"**何时调用**:调前端 bug、看页面 runtime error、跟 React/Vue 警告。",
|
|
11831
|
+
"**注意**:缓冲区上限 500 条,超出会丢最早;只能读 navigate 之后的日志。"
|
|
11832
|
+
].join(`
|
|
11833
|
+
`);
|
|
11834
|
+
var ArgsSchema9 = z9.object({
|
|
11835
|
+
level: z9.enum(["log", "info", "warn", "error", "debug"]).optional().describe("只过滤指定级别;缺省返回全部"),
|
|
11836
|
+
sinceTs: z9.number().optional().describe("只返回 timestamp >= sinceTs 的条目")
|
|
11837
|
+
});
|
|
11838
|
+
var _controller5 = null;
|
|
11839
|
+
var _cfg5;
|
|
11840
|
+
async function getController5() {
|
|
11841
|
+
if (!_controller5) {
|
|
11842
|
+
_controller5 = await createBrowserController({ cfg: _cfg5 });
|
|
11843
|
+
}
|
|
11844
|
+
return _controller5;
|
|
11845
|
+
}
|
|
11846
|
+
async function execute9(input) {
|
|
11847
|
+
const parsed = ArgsSchema9.safeParse(input);
|
|
11848
|
+
if (!parsed.success) {
|
|
11849
|
+
return {
|
|
11850
|
+
ok: false,
|
|
11851
|
+
entries: [],
|
|
11852
|
+
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11853
|
+
};
|
|
11762
11854
|
}
|
|
11763
|
-
const age = Date.now() - mtimeMs;
|
|
11764
|
-
if (age <= staleMs)
|
|
11765
|
-
return false;
|
|
11766
|
-
let meta = null;
|
|
11767
11855
|
try {
|
|
11768
|
-
const
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
} catch (err) {
|
|
11778
|
-
const e = err;
|
|
11779
|
-
if (e.code === "EPERM")
|
|
11780
|
-
return false;
|
|
11781
|
-
}
|
|
11856
|
+
const ctrl = await getController5();
|
|
11857
|
+
const entries = await ctrl.consoleLogs(parsed.data);
|
|
11858
|
+
return { ok: true, entries };
|
|
11859
|
+
} catch (err) {
|
|
11860
|
+
return {
|
|
11861
|
+
ok: false,
|
|
11862
|
+
entries: [],
|
|
11863
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11864
|
+
};
|
|
11782
11865
|
}
|
|
11783
|
-
return await deleteIfMtimeMatch(lockPath, mtimeMs);
|
|
11784
11866
|
}
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11867
|
+
// tools/browser-network.ts
|
|
11868
|
+
import { z as z10 } from "zod";
|
|
11869
|
+
var description10 = [
|
|
11870
|
+
"读取浏览器网络请求缓冲区(含 status / 失败原因 / 耗时)。",
|
|
11871
|
+
"**何时调用**:调 API 调用 4xx/5xx、CORS 报错、查 page 加载耗时分布。",
|
|
11872
|
+
"**注意**:缓冲区上限 500 条;只看 navigate 之后发起的请求。"
|
|
11873
|
+
].join(`
|
|
11874
|
+
`);
|
|
11875
|
+
var ArgsSchema10 = z10.object({
|
|
11876
|
+
method: z10.string().optional().describe("HTTP 方法过滤(GET/POST/...),不区分大小写"),
|
|
11877
|
+
sinceTs: z10.number().optional().describe("只返回 start_ts >= sinceTs 的请求")
|
|
11878
|
+
});
|
|
11879
|
+
var _controller6 = null;
|
|
11880
|
+
var _cfg6;
|
|
11881
|
+
async function getController6() {
|
|
11882
|
+
if (!_controller6) {
|
|
11883
|
+
_controller6 = await createBrowserController({ cfg: _cfg6 });
|
|
11793
11884
|
}
|
|
11794
|
-
|
|
11795
|
-
return true;
|
|
11885
|
+
return _controller6;
|
|
11796
11886
|
}
|
|
11797
|
-
async function
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11887
|
+
async function execute10(input) {
|
|
11888
|
+
const parsed = ArgsSchema10.safeParse(input);
|
|
11889
|
+
if (!parsed.success) {
|
|
11890
|
+
return {
|
|
11891
|
+
ok: false,
|
|
11892
|
+
entries: [],
|
|
11893
|
+
error: parsed.error.issues.map((i) => i.message).join("; ")
|
|
11894
|
+
};
|
|
11803
11895
|
}
|
|
11804
|
-
let meta;
|
|
11805
11896
|
try {
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
return;
|
|
11809
|
-
}
|
|
11810
|
-
|
|
11811
|
-
|
|
11897
|
+
const ctrl = await getController6();
|
|
11898
|
+
const entries = await ctrl.networkRequests(parsed.data);
|
|
11899
|
+
return { ok: true, entries };
|
|
11900
|
+
} catch (err) {
|
|
11901
|
+
return {
|
|
11902
|
+
ok: false,
|
|
11903
|
+
entries: [],
|
|
11904
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11905
|
+
};
|
|
11812
11906
|
}
|
|
11813
11907
|
}
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
}
|
|
11908
|
+
// tools/model-chain.ts
|
|
11909
|
+
import { z as z11 } from "zod";
|
|
11817
11910
|
|
|
11818
|
-
// lib/
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11911
|
+
// lib/model-config.ts
|
|
11912
|
+
import { promises as fs10 } from "node:fs";
|
|
11913
|
+
import * as path13 from "node:path";
|
|
11914
|
+
|
|
11915
|
+
// lib/model-tier.ts
|
|
11916
|
+
var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
|
|
11917
|
+
|
|
11918
|
+
// lib/model-config.ts
|
|
11919
|
+
var CONFIG_FILE = "codeforge.json";
|
|
11920
|
+
var DEFAULT_RUNTIME_FALLBACK = {
|
|
11921
|
+
enabled: true,
|
|
11922
|
+
max_fallback_attempts: 3,
|
|
11923
|
+
notify_on_fallback: true,
|
|
11924
|
+
trigger_events: ["session.error", "model.unavailable", "model.rate_limited"]
|
|
11925
|
+
};
|
|
11926
|
+
var PROVIDER_MODEL_RE = /^[a-z0-9-]+\/[a-zA-Z0-9._-]+$/;
|
|
11927
|
+
function findConfigFileSync(opts = {}) {
|
|
11928
|
+
const root = opts.root ?? process.cwd();
|
|
11929
|
+
const fsSync = __require("node:fs");
|
|
11930
|
+
const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
|
|
11931
|
+
return fsSync.existsSync(abs) ? abs : null;
|
|
11831
11932
|
}
|
|
11832
|
-
function
|
|
11833
|
-
|
|
11933
|
+
function loadModelConfigSync(opts = {}) {
|
|
11934
|
+
const root = opts.root ?? process.cwd();
|
|
11935
|
+
const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
|
|
11936
|
+
const fsSync = __require("node:fs");
|
|
11937
|
+
if (!fsSync.existsSync(abs)) {
|
|
11938
|
+
return { ok: false, warnings: [], error: `config_not_found: ${abs}` };
|
|
11939
|
+
}
|
|
11940
|
+
let raw;
|
|
11941
|
+
try {
|
|
11942
|
+
raw = fsSync.readFileSync(abs, "utf8");
|
|
11943
|
+
} catch (e) {
|
|
11944
|
+
return { ok: false, path: abs, warnings: [], error: `read_failed: ${e.message}` };
|
|
11945
|
+
}
|
|
11946
|
+
return parseAndValidate(raw, abs);
|
|
11834
11947
|
}
|
|
11835
|
-
async function
|
|
11836
|
-
const
|
|
11948
|
+
async function loadModelConfig(opts = {}) {
|
|
11949
|
+
const root = opts.root ?? process.cwd();
|
|
11950
|
+
const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
|
|
11951
|
+
let raw;
|
|
11837
11952
|
try {
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11953
|
+
raw = await fs10.readFile(abs, "utf8");
|
|
11954
|
+
} catch (e) {
|
|
11955
|
+
const code = e.code;
|
|
11956
|
+
if (code === "ENOENT") {
|
|
11957
|
+
return { ok: false, path: abs, warnings: [], error: `config_not_found: ${abs}` };
|
|
11842
11958
|
}
|
|
11843
|
-
return
|
|
11844
|
-
} catch (err) {
|
|
11845
|
-
const e = err;
|
|
11846
|
-
if (e.code === "ENOENT")
|
|
11847
|
-
return { version: REGISTRY_VERSION, entries: [] };
|
|
11848
|
-
return { version: REGISTRY_VERSION, entries: [] };
|
|
11959
|
+
return { ok: false, path: abs, warnings: [], error: `read_failed: ${e.message}` };
|
|
11849
11960
|
}
|
|
11961
|
+
return parseAndValidate(raw, abs);
|
|
11850
11962
|
}
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
}
|
|
11858
|
-
async function mutateRegistry(mainRoot, fn) {
|
|
11859
|
-
const lockPath = registryLockPath(mainRoot);
|
|
11860
|
-
await fs10.mkdir(path13.dirname(lockPath), { recursive: true });
|
|
11861
|
-
return await withFileLock(lockPath, async () => {
|
|
11862
|
-
const reg = await readRegistry(mainRoot);
|
|
11863
|
-
const result = await fn(reg);
|
|
11864
|
-
await writeRegistry(mainRoot, reg);
|
|
11865
|
-
return result;
|
|
11866
|
-
});
|
|
11867
|
-
}
|
|
11868
|
-
async function bindSessionWorktree(opts) {
|
|
11869
|
-
if (!opts.sessionId || opts.sessionId.trim() === "") {
|
|
11870
|
-
throw new Error("bindSessionWorktree: sessionId 不能为空");
|
|
11963
|
+
function parseAndValidate(raw, abs) {
|
|
11964
|
+
let parsed;
|
|
11965
|
+
try {
|
|
11966
|
+
parsed = JSON.parse(raw);
|
|
11967
|
+
} catch (e) {
|
|
11968
|
+
return { ok: false, path: abs, warnings: [], error: `invalid_json: ${e.message}` };
|
|
11871
11969
|
}
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
const worktreesDir = opts.worktrees_dir ?? path13.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
|
|
11875
|
-
const lockPath = registryLockPath(mainRoot);
|
|
11876
|
-
await fs10.mkdir(path13.dirname(lockPath), { recursive: true });
|
|
11877
|
-
return await withFileLock(lockPath, async () => {
|
|
11878
|
-
const reg = await readRegistry(mainRoot);
|
|
11879
|
-
const existing = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
11880
|
-
if (existing && existing.status === "active")
|
|
11881
|
-
return existing;
|
|
11882
|
-
const baseSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
|
|
11883
|
-
const wt = await ensureWorktree({
|
|
11884
|
-
root: mainRoot,
|
|
11885
|
-
branch,
|
|
11886
|
-
worktrees_dir: worktreesDir
|
|
11887
|
-
});
|
|
11888
|
-
const now = new Date().toISOString();
|
|
11889
|
-
const entry = {
|
|
11890
|
-
sessionId: opts.sessionId,
|
|
11891
|
-
branch,
|
|
11892
|
-
worktreePath: wt.path,
|
|
11893
|
-
baseSha,
|
|
11894
|
-
...opts.requiredPlanId ? { requiredPlanId: opts.requiredPlanId, planReadOk: false } : {},
|
|
11895
|
-
status: "active",
|
|
11896
|
-
createdAt: now,
|
|
11897
|
-
updatedAt: now
|
|
11898
|
-
};
|
|
11899
|
-
const idx = reg.entries.findIndex((e) => e.sessionId === opts.sessionId);
|
|
11900
|
-
if (idx >= 0)
|
|
11901
|
-
reg.entries[idx] = entry;
|
|
11902
|
-
else
|
|
11903
|
-
reg.entries.push(entry);
|
|
11904
|
-
await writeRegistry(mainRoot, reg);
|
|
11905
|
-
return entry;
|
|
11906
|
-
});
|
|
11907
|
-
}
|
|
11908
|
-
async function getSessionWorktree(sessionId, mainRoot) {
|
|
11909
|
-
const reg = await readRegistry(mainRoot);
|
|
11910
|
-
return reg.entries.find((e) => e.sessionId === sessionId) ?? null;
|
|
11911
|
-
}
|
|
11912
|
-
async function markPlanReadOk(opts) {
|
|
11913
|
-
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
11914
|
-
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
11915
|
-
if (!entry || entry.requiredPlanId !== opts.planId)
|
|
11916
|
-
return false;
|
|
11917
|
-
entry.planReadOk = true;
|
|
11918
|
-
entry.updatedAt = new Date().toISOString();
|
|
11919
|
-
return true;
|
|
11920
|
-
});
|
|
11921
|
-
}
|
|
11922
|
-
async function touchEntryUpdatedAt(opts) {
|
|
11923
|
-
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
11924
|
-
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
11925
|
-
if (!entry || entry.status !== "active")
|
|
11926
|
-
return false;
|
|
11927
|
-
entry.updatedAt = new Date().toISOString();
|
|
11928
|
-
return true;
|
|
11929
|
-
});
|
|
11930
|
-
}
|
|
11931
|
-
async function mergeSessionBack(opts) {
|
|
11932
|
-
const mainRoot = path13.resolve(opts.mainRoot);
|
|
11933
|
-
const entry = await getSessionWorktree(opts.sessionId, mainRoot);
|
|
11934
|
-
if (!entry) {
|
|
11935
|
-
throw new Error(`mergeSessionBack: session ${opts.sessionId} 没有绑定 worktree`);
|
|
11970
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11971
|
+
return { ok: false, path: abs, warnings: [], error: "invalid_root: must be an object" };
|
|
11936
11972
|
}
|
|
11937
|
-
|
|
11938
|
-
|
|
11973
|
+
const root = parsed;
|
|
11974
|
+
const modelsNode = root.models;
|
|
11975
|
+
if (!modelsNode || typeof modelsNode !== "object" || Array.isArray(modelsNode)) {
|
|
11976
|
+
return { ok: false, path: abs, warnings: [], error: "missing_models: codeforge.json must have a top-level `models` object" };
|
|
11939
11977
|
}
|
|
11940
|
-
|
|
11941
|
-
|
|
11978
|
+
const v = validateConfig(modelsNode);
|
|
11979
|
+
if (!v.ok)
|
|
11980
|
+
return { ok: false, path: abs, warnings: [], error: v.error };
|
|
11981
|
+
return { ok: true, path: abs, config: v.config, warnings: v.warnings };
|
|
11982
|
+
}
|
|
11983
|
+
function validateConfig(input) {
|
|
11984
|
+
const warnings = [];
|
|
11985
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
11986
|
+
return { ok: false, warnings, error: "config_root_must_be_object" };
|
|
11942
11987
|
}
|
|
11943
|
-
const
|
|
11944
|
-
const
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
if (wtStatus.length > 0) {
|
|
11948
|
-
await runGit2(wt, ["add", "-A"]);
|
|
11949
|
-
await runGit2(wt, ["commit", "-m", `session(${opts.sessionId}): auto-commit before merge`]);
|
|
11988
|
+
const obj = input;
|
|
11989
|
+
const agentsRaw = obj.agents;
|
|
11990
|
+
if (!agentsRaw || typeof agentsRaw !== "object" || Array.isArray(agentsRaw)) {
|
|
11991
|
+
return { ok: false, warnings, error: "agents_required_object" };
|
|
11950
11992
|
}
|
|
11951
|
-
const
|
|
11952
|
-
|
|
11953
|
-
|
|
11993
|
+
const agents = {};
|
|
11994
|
+
for (const [name, value] of Object.entries(agentsRaw)) {
|
|
11995
|
+
const r = normalizeAgent(name, value);
|
|
11996
|
+
if (!r.ok)
|
|
11997
|
+
return { ok: false, warnings, error: r.error };
|
|
11998
|
+
agents[name] = r.binding;
|
|
11954
11999
|
}
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
} catch (err) {
|
|
11958
|
-
await runGit2(mainRoot, ["reset", "--merge"]).catch(() => {
|
|
11959
|
-
return runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
|
|
11960
|
-
});
|
|
11961
|
-
throw new Error(`mergeSessionBack: squash merge 失败(已 reset 主仓兜底): ${err.message}`);
|
|
12000
|
+
if (Object.keys(agents).length === 0) {
|
|
12001
|
+
return { ok: false, warnings, error: "agents_must_have_at_least_one_entry" };
|
|
11962
12002
|
}
|
|
11963
|
-
|
|
11964
|
-
if (
|
|
11965
|
-
|
|
11966
|
-
"
|
|
11967
|
-
"--cached",
|
|
11968
|
-
"--name-only",
|
|
11969
|
-
"--diff-filter=ACMR"
|
|
11970
|
-
]);
|
|
11971
|
-
const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
11972
|
-
const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths, wt);
|
|
11973
|
-
if (canSkipDevOnce) {} else {
|
|
11974
|
-
try {
|
|
11975
|
-
await runCmd("npm", ["run", buildScript], mainRoot);
|
|
11976
|
-
} catch (err) {
|
|
11977
|
-
await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
|
|
11978
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
11979
|
-
throw new Error(`${buildScript} 失败已 reset 主仓: ${msg}`);
|
|
11980
|
-
}
|
|
12003
|
+
let categories;
|
|
12004
|
+
if (obj.categories !== undefined) {
|
|
12005
|
+
if (!obj.categories || typeof obj.categories !== "object" || Array.isArray(obj.categories)) {
|
|
12006
|
+
return { ok: false, warnings, error: "categories_must_be_object" };
|
|
11981
12007
|
}
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
});
|
|
11989
|
-
const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
|
|
11990
|
-
try {
|
|
11991
|
-
await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
|
|
11992
|
-
} catch (err) {
|
|
11993
|
-
debugLog(`removeWorktree (merge) 非预期失败 (session=${opts.sessionId}): ${err.message}`);
|
|
11994
|
-
}
|
|
11995
|
-
await deleteBranchIfExists({ root: mainRoot, branch }).catch((err) => {
|
|
11996
|
-
debugLog(`deleteBranchIfExists (merge) 非预期失败: ${err.message}`);
|
|
11997
|
-
return { deleted: false };
|
|
11998
|
-
});
|
|
11999
|
-
await mutateRegistry(mainRoot, (reg) => {
|
|
12000
|
-
const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
|
|
12001
|
-
if (e) {
|
|
12002
|
-
e.status = "merged";
|
|
12003
|
-
e.updatedAt = new Date().toISOString();
|
|
12008
|
+
categories = {};
|
|
12009
|
+
for (const [name, value] of Object.entries(obj.categories)) {
|
|
12010
|
+
const r = normalizeCategory(name, value);
|
|
12011
|
+
if (!r.ok)
|
|
12012
|
+
return { ok: false, warnings, error: r.error };
|
|
12013
|
+
categories[name] = r.binding;
|
|
12004
12014
|
}
|
|
12005
|
-
});
|
|
12006
|
-
if (opts.planStore && entry.requiredPlanId) {
|
|
12007
|
-
await opts.planStore.markMerged(entry.requiredPlanId, opts.sessionId).catch((err) => {
|
|
12008
|
-
console.warn(`[session-worktree] planStore.markMerged(${entry.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
12009
|
-
});
|
|
12010
12015
|
}
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
return;
|
|
12016
|
+
let runtime;
|
|
12017
|
+
if (obj.runtime_fallback !== undefined) {
|
|
12018
|
+
const r = normalizeRuntime(obj.runtime_fallback);
|
|
12019
|
+
if (!r.ok)
|
|
12020
|
+
return { ok: false, warnings, error: r.error };
|
|
12021
|
+
runtime = r.cfg;
|
|
12018
12022
|
}
|
|
12019
|
-
|
|
12020
|
-
|
|
12023
|
+
let tiers;
|
|
12024
|
+
if (obj.tiers !== undefined) {
|
|
12025
|
+
const r = normalizeTiers(obj.tiers);
|
|
12026
|
+
if (!r.ok)
|
|
12027
|
+
return { ok: false, warnings, error: r.error };
|
|
12028
|
+
tiers = r.cfg;
|
|
12021
12029
|
}
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
force: true
|
|
12027
|
-
});
|
|
12028
|
-
} catch (err) {
|
|
12029
|
-
debugLog(`removeWorktree (discard) 非预期失败: ${err.message}`);
|
|
12030
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
12031
|
+
if (agent.category && !categories?.[agent.category]) {
|
|
12032
|
+
warnings.push(`agent[${name}].category="${agent.category}" 未在 categories 定义,将忽略 category 链`);
|
|
12033
|
+
}
|
|
12030
12034
|
}
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12035
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
12036
|
+
if (!agent.tier)
|
|
12037
|
+
continue;
|
|
12038
|
+
const mappedCat = tiers?.category_map?.[agent.tier];
|
|
12039
|
+
const hasOverride = agent.tier_overrides?.[agent.tier] !== undefined;
|
|
12040
|
+
if (!mappedCat && !hasOverride) {
|
|
12041
|
+
warnings.push(`agent[${name}].tier="${agent.tier}" 既无 models.tiers.category_map["${agent.tier}"] 映射,` + `也无 tier_overrides["${agent.tier}"];该 agent 将不参与 tier 体系(adapter 返 null)。`);
|
|
12042
|
+
} else if (mappedCat && !categories?.[mappedCat] && !hasOverride) {
|
|
12043
|
+
warnings.push(`agent[${name}].tier="${agent.tier}" 通过 category_map 映射到 "${mappedCat}",` + `但 categories.${mappedCat} 不存在;该 agent 将不参与 tier 体系。`);
|
|
12040
12044
|
}
|
|
12041
|
-
});
|
|
12042
|
-
if (opts.planStore && entry.requiredPlanId) {
|
|
12043
|
-
await opts.planStore.markDiscarded(entry.requiredPlanId, opts.sessionId).catch((err) => {
|
|
12044
|
-
console.warn(`[session-worktree] planStore.markDiscarded(${entry.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
12045
|
-
});
|
|
12046
12045
|
}
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12046
|
+
return {
|
|
12047
|
+
ok: true,
|
|
12048
|
+
warnings,
|
|
12049
|
+
config: {
|
|
12050
|
+
$schema: typeof obj.$schema === "string" ? obj.$schema : undefined,
|
|
12051
|
+
_doc: typeof obj._doc === "string" ? obj._doc : undefined,
|
|
12052
|
+
agents,
|
|
12053
|
+
categories,
|
|
12054
|
+
runtime_fallback: runtime,
|
|
12055
|
+
tiers
|
|
12054
12056
|
}
|
|
12055
|
-
}
|
|
12057
|
+
};
|
|
12056
12058
|
}
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
return
|
|
12060
|
-
} catch {
|
|
12061
|
-
return "";
|
|
12059
|
+
function normalizeAgent(name, raw) {
|
|
12060
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12061
|
+
return { ok: false, error: `agent[${name}]: must be object` };
|
|
12062
12062
|
}
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
return false;
|
|
12063
|
+
const o = raw;
|
|
12064
|
+
if (typeof o.model !== "string" || !PROVIDER_MODEL_RE.test(o.model)) {
|
|
12065
|
+
return {
|
|
12066
|
+
ok: false,
|
|
12067
|
+
error: `agent[${name}].model invalid: "${String(o.model)}" (expect <provider>/<id>)`
|
|
12068
|
+
};
|
|
12070
12069
|
}
|
|
12071
|
-
}
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
}
|
|
12085
|
-
|
|
12086
|
-
}
|
|
12087
|
-
function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
|
|
12088
|
-
const inheritedEnv = process["env"];
|
|
12089
|
-
return new Promise((resolve11, reject) => {
|
|
12090
|
-
execFile3("git", args, {
|
|
12091
|
-
cwd,
|
|
12092
|
-
timeout: timeoutMs,
|
|
12093
|
-
windowsHide: true,
|
|
12094
|
-
encoding: "utf8",
|
|
12095
|
-
env: Object.assign({}, inheritedEnv, envOverrides)
|
|
12096
|
-
}, (err, stdout, stderr) => {
|
|
12097
|
-
if (err) {
|
|
12098
|
-
reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
|
|
12099
|
-
return;
|
|
12100
|
-
}
|
|
12101
|
-
resolve11(stdout);
|
|
12102
|
-
});
|
|
12103
|
-
});
|
|
12104
|
-
}
|
|
12105
|
-
async function getBuildScript(mainRoot) {
|
|
12106
|
-
const cfg = getCodeforgeConfig({ root: mainRoot });
|
|
12107
|
-
const merge = cfg["merge"];
|
|
12108
|
-
if (!merge || typeof merge !== "object" || Array.isArray(merge))
|
|
12109
|
-
return null;
|
|
12110
|
-
const script = merge["postMergeScript"];
|
|
12111
|
-
if (typeof script !== "string")
|
|
12112
|
-
return null;
|
|
12113
|
-
const trimmed = script.trim();
|
|
12114
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
12115
|
-
}
|
|
12116
|
-
async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
|
|
12117
|
-
let distMtimeSec;
|
|
12118
|
-
try {
|
|
12119
|
-
const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
|
|
12120
|
-
distMtimeSec = Math.floor(st.mtimeMs / 1000);
|
|
12121
|
-
} catch {
|
|
12122
|
-
return false;
|
|
12070
|
+
const fallbacks = normalizeFallbackList(`agent[${name}].fallback_models`, o.fallback_models);
|
|
12071
|
+
if (!fallbacks.ok)
|
|
12072
|
+
return { ok: false, error: fallbacks.error };
|
|
12073
|
+
const thinking = o.thinking !== undefined ? normalizeThinking(`agent[${name}]`, o.thinking) : undefined;
|
|
12074
|
+
if (thinking && !thinking.ok)
|
|
12075
|
+
return { ok: false, error: thinking.error };
|
|
12076
|
+
let tier;
|
|
12077
|
+
if (o.tier !== undefined) {
|
|
12078
|
+
if (typeof o.tier !== "string" || !isValidTierLevel(o.tier)) {
|
|
12079
|
+
return {
|
|
12080
|
+
ok: false,
|
|
12081
|
+
error: `agent[${name}].tier="${String(o.tier)}" 不是合法 TierLevel (期望 ${TIER_ORDER.join("/")})`
|
|
12082
|
+
};
|
|
12083
|
+
}
|
|
12084
|
+
tier = o.tier;
|
|
12123
12085
|
}
|
|
12124
|
-
|
|
12125
|
-
if (
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
return false;
|
|
12131
|
-
if (srcMtimeSec > distMtimeSec)
|
|
12132
|
-
return false;
|
|
12086
|
+
let tierOverrides;
|
|
12087
|
+
if (o.tier_overrides !== undefined) {
|
|
12088
|
+
const r = normalizeTierOverrides(name, o.tier_overrides);
|
|
12089
|
+
if (!r.ok)
|
|
12090
|
+
return { ok: false, error: r.error };
|
|
12091
|
+
tierOverrides = r.value;
|
|
12133
12092
|
}
|
|
12134
|
-
return
|
|
12093
|
+
return {
|
|
12094
|
+
ok: true,
|
|
12095
|
+
binding: {
|
|
12096
|
+
model: o.model,
|
|
12097
|
+
variant: typeof o.variant === "string" ? o.variant : undefined,
|
|
12098
|
+
category: typeof o.category === "string" ? o.category : undefined,
|
|
12099
|
+
thinking: thinking?.value,
|
|
12100
|
+
fallback_models: fallbacks.value,
|
|
12101
|
+
tier,
|
|
12102
|
+
tier_overrides: tierOverrides,
|
|
12103
|
+
_doc: typeof o._doc === "string" ? o._doc : undefined
|
|
12104
|
+
}
|
|
12105
|
+
};
|
|
12135
12106
|
}
|
|
12136
|
-
|
|
12137
|
-
if (
|
|
12138
|
-
|
|
12139
|
-
const st = await fs10.stat(path13.join(worktreePath, rel));
|
|
12140
|
-
return Math.floor(st.mtimeMs / 1000);
|
|
12141
|
-
} catch {}
|
|
12107
|
+
function normalizeCategory(name, raw) {
|
|
12108
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12109
|
+
return { ok: false, error: `category[${name}]: must be object` };
|
|
12142
12110
|
}
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
return
|
|
12146
|
-
|
|
12147
|
-
|
|
12111
|
+
const o = raw;
|
|
12112
|
+
if (typeof o.model !== "string" || !PROVIDER_MODEL_RE.test(o.model)) {
|
|
12113
|
+
return {
|
|
12114
|
+
ok: false,
|
|
12115
|
+
error: `category[${name}].model invalid: "${String(o.model)}"`
|
|
12116
|
+
};
|
|
12148
12117
|
}
|
|
12118
|
+
const fallbacks = normalizeFallbackList(`category[${name}].fallback_models`, o.fallback_models);
|
|
12119
|
+
if (!fallbacks.ok)
|
|
12120
|
+
return { ok: false, error: fallbacks.error };
|
|
12121
|
+
const thinking = o.thinking !== undefined ? normalizeThinking(`category[${name}]`, o.thinking) : undefined;
|
|
12122
|
+
if (thinking && !thinking.ok)
|
|
12123
|
+
return { ok: false, error: thinking.error };
|
|
12124
|
+
return {
|
|
12125
|
+
ok: true,
|
|
12126
|
+
binding: {
|
|
12127
|
+
model: o.model,
|
|
12128
|
+
variant: typeof o.variant === "string" ? o.variant : undefined,
|
|
12129
|
+
thinking: thinking?.value,
|
|
12130
|
+
fallback_models: fallbacks.value,
|
|
12131
|
+
_doc: typeof o._doc === "string" ? o._doc : undefined
|
|
12132
|
+
}
|
|
12133
|
+
};
|
|
12149
12134
|
}
|
|
12150
|
-
function
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12135
|
+
function normalizeFallbackList(ctx, raw) {
|
|
12136
|
+
if (raw === undefined)
|
|
12137
|
+
return { ok: true, value: [] };
|
|
12138
|
+
if (!Array.isArray(raw))
|
|
12139
|
+
return { ok: false, error: `${ctx}: must be array` };
|
|
12140
|
+
const seen = new Set;
|
|
12141
|
+
const result = [];
|
|
12142
|
+
for (const it of raw) {
|
|
12143
|
+
if (typeof it !== "string" || !PROVIDER_MODEL_RE.test(it)) {
|
|
12144
|
+
return { ok: false, error: `${ctx}: invalid entry "${String(it)}"` };
|
|
12145
|
+
}
|
|
12146
|
+
if (seen.has(it))
|
|
12147
|
+
continue;
|
|
12148
|
+
seen.add(it);
|
|
12149
|
+
result.push(it);
|
|
12150
|
+
}
|
|
12151
|
+
return { ok: true, value: result };
|
|
12160
12152
|
}
|
|
12161
|
-
function
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
return
|
|
12153
|
+
function normalizeThinking(ctx, raw) {
|
|
12154
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12155
|
+
return { ok: false, error: `${ctx}.thinking: must be object` };
|
|
12156
|
+
}
|
|
12157
|
+
const o = raw;
|
|
12158
|
+
if (o.type !== "enabled" && o.type !== "disabled") {
|
|
12159
|
+
return { ok: false, error: `${ctx}.thinking.type: must be "enabled" or "disabled"` };
|
|
12160
|
+
}
|
|
12161
|
+
if (o.budget_tokens !== undefined && (typeof o.budget_tokens !== "number" || o.budget_tokens < 0)) {
|
|
12162
|
+
return { ok: false, error: `${ctx}.thinking.budget_tokens: must be non-negative number` };
|
|
12163
|
+
}
|
|
12164
|
+
return { ok: true, value: { ...o, type: o.type } };
|
|
12173
12165
|
}
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
12178
|
-
const keepRecent = opts.keepRecent ?? 50;
|
|
12179
|
-
if (keepRecent < 0) {
|
|
12180
|
-
throw new Error(`pruneDiscardedRegistryEntries: keepRecent 必须 ≥ 0,收到 ${keepRecent}`);
|
|
12166
|
+
function normalizeRuntime(raw) {
|
|
12167
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12168
|
+
return { ok: false, error: "runtime_fallback: must be object" };
|
|
12181
12169
|
}
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12170
|
+
const o = raw;
|
|
12171
|
+
const cfg = { ...DEFAULT_RUNTIME_FALLBACK };
|
|
12172
|
+
if (o.enabled !== undefined) {
|
|
12173
|
+
if (typeof o.enabled !== "boolean")
|
|
12174
|
+
return { ok: false, error: "runtime_fallback.enabled: boolean" };
|
|
12175
|
+
cfg.enabled = o.enabled;
|
|
12176
|
+
}
|
|
12177
|
+
if (o.max_fallback_attempts !== undefined) {
|
|
12178
|
+
if (typeof o.max_fallback_attempts !== "number" || o.max_fallback_attempts < 1) {
|
|
12179
|
+
return { ok: false, error: "runtime_fallback.max_fallback_attempts: integer >=1" };
|
|
12190
12180
|
}
|
|
12191
|
-
|
|
12192
|
-
const kept = discarded.slice(0, keepRecent);
|
|
12193
|
-
const pruned = discarded.length - kept.length;
|
|
12194
|
-
reg.entries = [...others, ...kept];
|
|
12195
|
-
return { pruned, kept: kept.length };
|
|
12196
|
-
});
|
|
12197
|
-
}
|
|
12198
|
-
async function pruneOrphanWorktrees(mainRoot, opts = {}) {
|
|
12199
|
-
const resolved = path13.resolve(mainRoot);
|
|
12200
|
-
const cleaned = [];
|
|
12201
|
-
const failed = [];
|
|
12202
|
-
let skipped = 0;
|
|
12203
|
-
let gitWorktrees = [];
|
|
12204
|
-
try {
|
|
12205
|
-
const mod = await Promise.resolve().then(() => (init_worktree_ops(), exports_worktree_ops));
|
|
12206
|
-
gitWorktrees = await mod.listWorktrees({ root: resolved });
|
|
12207
|
-
} catch {
|
|
12208
|
-
gitWorktrees = [];
|
|
12181
|
+
cfg.max_fallback_attempts = Math.floor(o.max_fallback_attempts);
|
|
12209
12182
|
}
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
for (const entry of reg2.entries) {
|
|
12214
|
-
if (entry.status !== "active")
|
|
12215
|
-
continue;
|
|
12216
|
-
const wt = entry.worktreePath;
|
|
12217
|
-
let dirExists = true;
|
|
12218
|
-
let dirMtimeMs = 0;
|
|
12219
|
-
try {
|
|
12220
|
-
const st = await fs10.stat(wt);
|
|
12221
|
-
dirExists = st.isDirectory();
|
|
12222
|
-
dirMtimeMs = st.mtimeMs;
|
|
12223
|
-
} catch {
|
|
12224
|
-
dirExists = false;
|
|
12225
|
-
}
|
|
12226
|
-
if (dirExists && now - dirMtimeMs < ORPHAN_GRACE_MS) {
|
|
12227
|
-
skipped++;
|
|
12228
|
-
continue;
|
|
12229
|
-
}
|
|
12230
|
-
let branchExists = true;
|
|
12231
|
-
try {
|
|
12232
|
-
await runGit2(resolved, ["rev-parse", "--verify", `refs/heads/${entry.branch}`]);
|
|
12233
|
-
} catch {
|
|
12234
|
-
branchExists = false;
|
|
12235
|
-
}
|
|
12236
|
-
if (!dirExists || !branchExists) {
|
|
12237
|
-
const reason = !dirExists ? "worktreePath 不存在" : "branch 不存在";
|
|
12238
|
-
entry.status = "discarded";
|
|
12239
|
-
entry.updatedAt = new Date().toISOString();
|
|
12240
|
-
if (dirExists) {
|
|
12241
|
-
try {
|
|
12242
|
-
await removeWorktree({
|
|
12243
|
-
root: resolved,
|
|
12244
|
-
worktree_path: wt,
|
|
12245
|
-
force: true
|
|
12246
|
-
});
|
|
12247
|
-
} catch (err) {
|
|
12248
|
-
failed.push({
|
|
12249
|
-
worktreePath: wt,
|
|
12250
|
-
error: err instanceof Error ? err.message : String(err)
|
|
12251
|
-
});
|
|
12252
|
-
}
|
|
12253
|
-
}
|
|
12254
|
-
cleaned.push({ sessionId: entry.sessionId, worktreePath: wt, reason });
|
|
12255
|
-
}
|
|
12183
|
+
if (o.notify_on_fallback !== undefined) {
|
|
12184
|
+
if (typeof o.notify_on_fallback !== "boolean") {
|
|
12185
|
+
return { ok: false, error: "runtime_fallback.notify_on_fallback: boolean" };
|
|
12256
12186
|
}
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
}
|
|
12279
|
-
if (aliveResult.source === "unknown") {
|
|
12280
|
-
if (now - updatedMs < unknownTimeout) {
|
|
12281
|
-
skipped++;
|
|
12282
|
-
continue;
|
|
12283
|
-
}
|
|
12284
|
-
} else if (aliveResult.alive) {
|
|
12285
|
-
skipped++;
|
|
12286
|
-
continue;
|
|
12287
|
-
}
|
|
12288
|
-
try {
|
|
12289
|
-
await removeWorktree({
|
|
12290
|
-
root: resolved,
|
|
12291
|
-
worktree_path: entry.worktreePath,
|
|
12292
|
-
force: true
|
|
12293
|
-
});
|
|
12294
|
-
} catch (err) {
|
|
12295
|
-
failed.push({
|
|
12296
|
-
worktreePath: entry.worktreePath,
|
|
12297
|
-
error: `D 类 removeWorktree 失败: ${err instanceof Error ? err.message : String(err)}`
|
|
12298
|
-
});
|
|
12299
|
-
continue;
|
|
12300
|
-
}
|
|
12301
|
-
await deleteBranchIfExists({ root: resolved, branch: entry.branch }).catch(() => {});
|
|
12302
|
-
entry.status = "discarded";
|
|
12303
|
-
entry.updatedAt = new Date().toISOString();
|
|
12304
|
-
const reasonSource = aliveResult.source === "unknown" ? `unknown-timeout (registry.updatedAt 已老于 ${unknownTimeout / 3600000}h)` : `opencode session ${aliveResult.source}: dead`;
|
|
12305
|
-
cleaned.push({
|
|
12306
|
-
sessionId: entry.sessionId,
|
|
12307
|
-
worktreePath: entry.worktreePath,
|
|
12308
|
-
reason: `D 类语义孤儿 (${reasonSource})`
|
|
12309
|
-
});
|
|
12310
|
-
}
|
|
12311
|
-
});
|
|
12187
|
+
cfg.notify_on_fallback = o.notify_on_fallback;
|
|
12188
|
+
}
|
|
12189
|
+
if (o.trigger_events !== undefined) {
|
|
12190
|
+
if (!Array.isArray(o.trigger_events) || o.trigger_events.some((x) => typeof x !== "string")) {
|
|
12191
|
+
return { ok: false, error: "runtime_fallback.trigger_events: string[]" };
|
|
12192
|
+
}
|
|
12193
|
+
cfg.trigger_events = [...new Set(o.trigger_events)];
|
|
12194
|
+
}
|
|
12195
|
+
if (typeof o._doc === "string")
|
|
12196
|
+
cfg._doc = o._doc;
|
|
12197
|
+
return { ok: true, cfg };
|
|
12198
|
+
}
|
|
12199
|
+
function isValidTierLevel(x) {
|
|
12200
|
+
return typeof x === "string" && TIER_ORDER.includes(x);
|
|
12201
|
+
}
|
|
12202
|
+
function normalizeTierOverrides(agentName, raw) {
|
|
12203
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12204
|
+
return {
|
|
12205
|
+
ok: false,
|
|
12206
|
+
error: `agent[${agentName}].tier_overrides 必须是 object(不是 array / null / 其他类型)`
|
|
12207
|
+
};
|
|
12312
12208
|
}
|
|
12313
|
-
const
|
|
12314
|
-
const
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12209
|
+
const o = raw;
|
|
12210
|
+
const result = {};
|
|
12211
|
+
for (const [key, value] of Object.entries(o)) {
|
|
12212
|
+
if (!isValidTierLevel(key)) {
|
|
12213
|
+
return {
|
|
12214
|
+
ok: false,
|
|
12215
|
+
error: `agent[${agentName}].tier_overrides.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
|
|
12216
|
+
};
|
|
12319
12217
|
}
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
const reg = await readRegistry(resolved);
|
|
12326
|
-
const knownPaths = new Set(reg.entries.map((e) => path13.resolve(e.worktreePath)));
|
|
12327
|
-
for (const candidate of candidatePaths) {
|
|
12328
|
-
if (knownPaths.has(candidate))
|
|
12329
|
-
continue;
|
|
12330
|
-
if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path13.sep)) {
|
|
12331
|
-
continue;
|
|
12218
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12219
|
+
return {
|
|
12220
|
+
ok: false,
|
|
12221
|
+
error: `agent[${agentName}].tier_overrides.${key}: 必须是 object`
|
|
12222
|
+
};
|
|
12332
12223
|
}
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
if (
|
|
12337
|
-
|
|
12338
|
-
|
|
12224
|
+
const ov = value;
|
|
12225
|
+
const partial = {};
|
|
12226
|
+
if (ov.level !== undefined) {
|
|
12227
|
+
if (ov.level !== key) {
|
|
12228
|
+
return {
|
|
12229
|
+
ok: false,
|
|
12230
|
+
error: `agent[${agentName}].tier_overrides.${key}.level="${String(ov.level)}" 与 key "${key}" 错配`
|
|
12231
|
+
};
|
|
12339
12232
|
}
|
|
12340
|
-
|
|
12341
|
-
dirExists = false;
|
|
12233
|
+
partial.level = key;
|
|
12342
12234
|
}
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
removed = true;
|
|
12352
|
-
} catch (err) {
|
|
12353
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
12235
|
+
if (ov.model !== undefined) {
|
|
12236
|
+
if (typeof ov.model !== "string" || !PROVIDER_MODEL_RE.test(ov.model)) {
|
|
12237
|
+
return {
|
|
12238
|
+
ok: false,
|
|
12239
|
+
error: `agent[${agentName}].tier_overrides.${key}.model="${String(ov.model)}" 格式非法 (期望 <provider>/<id>)`
|
|
12240
|
+
};
|
|
12241
|
+
}
|
|
12242
|
+
partial.model = ov.model;
|
|
12354
12243
|
}
|
|
12355
|
-
if (
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
} catch {}
|
|
12244
|
+
if (ov.thinking !== undefined) {
|
|
12245
|
+
const t = normalizeThinking(`agent[${agentName}].tier_overrides.${key}`, ov.thinking);
|
|
12246
|
+
if (!t.ok)
|
|
12247
|
+
return { ok: false, error: t.error };
|
|
12248
|
+
partial.thinking = t.value;
|
|
12361
12249
|
}
|
|
12362
|
-
if (
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12250
|
+
if (ov.fallback_models !== undefined) {
|
|
12251
|
+
const f = normalizeFallbackList(`agent[${agentName}].tier_overrides.${key}.fallback_models`, ov.fallback_models);
|
|
12252
|
+
if (!f.ok)
|
|
12253
|
+
return { ok: false, error: f.error };
|
|
12254
|
+
partial.fallback_models = f.value;
|
|
12255
|
+
}
|
|
12256
|
+
result[key] = partial;
|
|
12257
|
+
}
|
|
12258
|
+
return { ok: true, value: result };
|
|
12259
|
+
}
|
|
12260
|
+
function normalizeTiers(raw) {
|
|
12261
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12262
|
+
return { ok: false, error: "models.tiers 必须是 object(不是 array / null)" };
|
|
12263
|
+
}
|
|
12264
|
+
const o = raw;
|
|
12265
|
+
const cfg = {};
|
|
12266
|
+
if (o.category_map !== undefined) {
|
|
12267
|
+
if (!o.category_map || typeof o.category_map !== "object" || Array.isArray(o.category_map)) {
|
|
12268
|
+
return { ok: false, error: "models.tiers.category_map 必须是 object" };
|
|
12269
|
+
}
|
|
12270
|
+
const map = {};
|
|
12271
|
+
for (const [key, value] of Object.entries(o.category_map)) {
|
|
12272
|
+
if (!isValidTierLevel(key)) {
|
|
12273
|
+
return {
|
|
12274
|
+
ok: false,
|
|
12275
|
+
error: `models.tiers.category_map.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
|
|
12276
|
+
};
|
|
12277
|
+
}
|
|
12278
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
12279
|
+
return {
|
|
12280
|
+
ok: false,
|
|
12281
|
+
error: `models.tiers.category_map.${key}: 必须是非空字符串(category 名)`
|
|
12282
|
+
};
|
|
12368
12283
|
}
|
|
12284
|
+
map[key] = value;
|
|
12369
12285
|
}
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12286
|
+
cfg.category_map = map;
|
|
12287
|
+
}
|
|
12288
|
+
if (typeof o._doc === "string")
|
|
12289
|
+
cfg._doc = o._doc;
|
|
12290
|
+
return { ok: true, cfg };
|
|
12291
|
+
}
|
|
12292
|
+
function resolveAgentModel(config, agent) {
|
|
12293
|
+
const a = config.agents[agent];
|
|
12294
|
+
if (!a)
|
|
12295
|
+
return null;
|
|
12296
|
+
const seen = new Set;
|
|
12297
|
+
const chain = [];
|
|
12298
|
+
const push = (m) => {
|
|
12299
|
+
if (!m || seen.has(m))
|
|
12300
|
+
return;
|
|
12301
|
+
seen.add(m);
|
|
12302
|
+
chain.push(m);
|
|
12303
|
+
};
|
|
12304
|
+
push(a.model);
|
|
12305
|
+
for (const m of a.fallback_models ?? [])
|
|
12306
|
+
push(m);
|
|
12307
|
+
let categoryName;
|
|
12308
|
+
let usedCategory = false;
|
|
12309
|
+
if (a.category && config.categories?.[a.category]) {
|
|
12310
|
+
categoryName = a.category;
|
|
12311
|
+
const c = config.categories[a.category];
|
|
12312
|
+
const beforeLen = chain.length;
|
|
12313
|
+
push(c.model);
|
|
12314
|
+
for (const m of c.fallback_models ?? [])
|
|
12315
|
+
push(m);
|
|
12316
|
+
usedCategory = chain.length > beforeLen;
|
|
12317
|
+
}
|
|
12318
|
+
const source = a.fallback_models && a.fallback_models.length > 0 ? usedCategory ? "merged" : "agent" : usedCategory ? "category" : "agent";
|
|
12319
|
+
return {
|
|
12320
|
+
agent,
|
|
12321
|
+
model: a.model,
|
|
12322
|
+
variant: a.variant,
|
|
12323
|
+
thinking: a.thinking,
|
|
12324
|
+
chain,
|
|
12325
|
+
source,
|
|
12326
|
+
category: categoryName
|
|
12327
|
+
};
|
|
12328
|
+
}
|
|
12329
|
+
function nextFallback(config, agent, current) {
|
|
12330
|
+
const r = resolveAgentModel(config, agent);
|
|
12331
|
+
if (!r)
|
|
12332
|
+
return null;
|
|
12333
|
+
const idx = r.chain.indexOf(current);
|
|
12334
|
+
if (idx < 0)
|
|
12335
|
+
return r.chain[0] ?? null;
|
|
12336
|
+
return r.chain[idx + 1] ?? null;
|
|
12337
|
+
}
|
|
12338
|
+
function listAllAgentChains(config) {
|
|
12339
|
+
return Object.keys(config.agents).sort().map((name) => resolveAgentModel(config, name)).filter((x) => x !== null);
|
|
12340
|
+
}
|
|
12341
|
+
|
|
12342
|
+
// tools/model-chain.ts
|
|
12343
|
+
var description11 = [
|
|
12344
|
+
"查询 CodeForge 配置文件 codeforge.json:每个 agent 的主模型 + fallback 链。",
|
|
12345
|
+
"**何时调用**:",
|
|
12346
|
+
"- 用户问「coder 用什么模型 / 备用是什么」",
|
|
12347
|
+
"- 当前模型不可用时,决定下一个该试哪个",
|
|
12348
|
+
"- 审计:把整个项目的模型配置一次性列给用户看",
|
|
12349
|
+
"返回 JSON:{ ok, agents: [...], current_chain?, next_fallback? }"
|
|
12350
|
+
].join(`
|
|
12351
|
+
`);
|
|
12352
|
+
var ArgsSchema11 = z11.object({
|
|
12353
|
+
agent: z11.string().optional().describe("查指定 agent;不传 → 列出全部"),
|
|
12354
|
+
current: z11.string().optional().describe("当前已用过的模型(<provider>/<id>),用于算下一档"),
|
|
12355
|
+
root: z11.string().optional().describe("项目根目录,默认 process.cwd()"),
|
|
12356
|
+
config_file: z11.string().optional().describe("配置文件名;默认 codeforge.json")
|
|
12357
|
+
});
|
|
12358
|
+
async function execute11(rawArgs) {
|
|
12359
|
+
const parsed = ArgsSchema11.safeParse(rawArgs ?? {});
|
|
12360
|
+
if (!parsed.success) {
|
|
12361
|
+
return {
|
|
12362
|
+
ok: false,
|
|
12363
|
+
error: "invalid_args: " + parsed.error.issues.map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join("; ")
|
|
12364
|
+
};
|
|
12365
|
+
}
|
|
12366
|
+
const args = parsed.data;
|
|
12367
|
+
const loadResult = await loadModelConfig({
|
|
12368
|
+
root: args.root,
|
|
12369
|
+
file: args.config_file
|
|
12370
|
+
});
|
|
12371
|
+
if (!loadResult.ok || !loadResult.config) {
|
|
12372
|
+
return {
|
|
12373
|
+
ok: false,
|
|
12374
|
+
error: loadResult.error ?? "load_failed",
|
|
12375
|
+
config_path: loadResult.path
|
|
12376
|
+
};
|
|
12377
|
+
}
|
|
12378
|
+
const cfg = loadResult.config;
|
|
12379
|
+
const allChains = listAllAgentChains(cfg).map(toEntry);
|
|
12380
|
+
const out = {
|
|
12381
|
+
ok: true,
|
|
12382
|
+
config_path: loadResult.path ?? "",
|
|
12383
|
+
warnings: loadResult.warnings,
|
|
12384
|
+
agents: allChains
|
|
12385
|
+
};
|
|
12386
|
+
if (args.agent) {
|
|
12387
|
+
const r = resolveAgentModel(cfg, args.agent);
|
|
12388
|
+
if (!r) {
|
|
12389
|
+
return {
|
|
12390
|
+
ok: false,
|
|
12391
|
+
error: `agent_not_found: "${args.agent}" 未在 codeforge.json 配置`,
|
|
12392
|
+
config_path: loadResult.path
|
|
12393
|
+
};
|
|
12394
|
+
}
|
|
12395
|
+
out.current_chain = toEntry(r);
|
|
12396
|
+
if (args.current) {
|
|
12397
|
+
out.next_fallback = nextFallback(cfg, args.agent, args.current);
|
|
12375
12398
|
}
|
|
12376
12399
|
}
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
} catch {}
|
|
12388
|
-
return { cleaned, failed, skipped, discardedPruned, gitAdminPruned };
|
|
12400
|
+
return out;
|
|
12401
|
+
}
|
|
12402
|
+
function toEntry(r) {
|
|
12403
|
+
return {
|
|
12404
|
+
agent: r.agent,
|
|
12405
|
+
model: r.model,
|
|
12406
|
+
category: r.category,
|
|
12407
|
+
source: r.source,
|
|
12408
|
+
chain: r.chain
|
|
12409
|
+
};
|
|
12389
12410
|
}
|
|
12411
|
+
// tools/session-merge.ts
|
|
12412
|
+
import { z as z12 } from "zod";
|
|
12390
12413
|
|
|
12391
12414
|
// lib/merge-gate.ts
|
|
12392
12415
|
import { promises as fs11 } from "node:fs";
|
|
@@ -14545,7 +14568,7 @@ function buildReviewerPrompt(args) {
|
|
|
14545
14568
|
];
|
|
14546
14569
|
if (args.planId)
|
|
14547
14570
|
lines.push(`plan_id: ${args.planId}`);
|
|
14548
|
-
lines.push(`round: ${args.round}/${args.maxRounds}`, "", "请按 reviewer.md 「worktree-session 审阅」模式执行:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 拿方案", "2. 跑 `git -C <worktree_path> diff <base_sha>..HEAD` 看改动", '3. APPROVE 前必须先调 review_approval(verdict=APPROVE, pendingIds=["session:<session_id>"]) 写审批', "4. 输出 ## Decision 节,首行 APPROVE / REQUEST_CHANGES / BLOCK 之一");
|
|
14571
|
+
lines.push(`round: ${args.round}/${args.maxRounds}`, "", "请按 reviewer.md 「worktree-session 审阅」模式执行:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 拿方案", "2. 跑 `git -C <worktree_path> diff <base_sha>..HEAD` 看改动", '3. APPROVE 前必须先调 review_approval(verdict=APPROVE, pendingIds=["session:<session_id>"], reviewTarget="code") 写审批', " (coveredSha 工具会自动从 worktree HEAD 捕获,无需手传)", "4. 输出 ## Decision 节,首行 APPROVE / REQUEST_CHANGES / BLOCK 之一");
|
|
14549
14572
|
if (args.prevSummary) {
|
|
14550
14573
|
lines.push("", "## 上一轮 reviewer 意见", "", args.prevSummary, "", "请确认 coder 是否已按意见修复");
|
|
14551
14574
|
}
|
|
@@ -20143,35 +20166,21 @@ var toolPolicyServer = async (ctx) => {
|
|
|
20143
20166
|
var handler19 = toolPolicyServer;
|
|
20144
20167
|
|
|
20145
20168
|
// plugins/update-checker.ts
|
|
20146
|
-
import { existsSync as
|
|
20147
|
-
import { homedir as
|
|
20148
|
-
import { join as join25 } from "node:path";
|
|
20169
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20170
|
+
import { homedir as homedir8 } from "node:os";
|
|
20171
|
+
import { dirname as dirname15, join as join25 } from "node:path";
|
|
20149
20172
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
20150
20173
|
|
|
20151
20174
|
// lib/update-checker-impl.ts
|
|
20152
|
-
import {
|
|
20153
|
-
import {
|
|
20154
|
-
copyFileSync,
|
|
20155
|
-
existsSync as existsSync5,
|
|
20156
|
-
mkdirSync as mkdirSync3,
|
|
20157
|
-
mkdtempSync,
|
|
20158
|
-
readFileSync as readFileSync5,
|
|
20159
|
-
readdirSync as readdirSync3,
|
|
20160
|
-
renameSync,
|
|
20161
|
-
statSync as statSync4,
|
|
20162
|
-
unlinkSync,
|
|
20163
|
-
writeFileSync as writeFileSync2
|
|
20164
|
-
} from "node:fs";
|
|
20165
|
-
import { homedir as homedir8, tmpdir } from "node:os";
|
|
20175
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
20166
20176
|
import { dirname as dirname14, join as join24 } from "node:path";
|
|
20167
20177
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
20168
20178
|
import * as https from "node:https";
|
|
20169
|
-
import * as zlib from "node:zlib";
|
|
20170
20179
|
|
|
20171
20180
|
// lib/version-injected.ts
|
|
20172
20181
|
function getInjectedVersion() {
|
|
20173
20182
|
try {
|
|
20174
|
-
const v = "0.
|
|
20183
|
+
const v = "0.7.0";
|
|
20175
20184
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20176
20185
|
return v;
|
|
20177
20186
|
}
|
|
@@ -20182,48 +20191,6 @@ function getInjectedVersion() {
|
|
|
20182
20191
|
}
|
|
20183
20192
|
|
|
20184
20193
|
// lib/update-checker-impl.ts
|
|
20185
|
-
async function checkUpdateOnce(opts) {
|
|
20186
|
-
const local = opts.localVersion ?? readLocalVersion();
|
|
20187
|
-
const cacheFile = opts.cacheFile ?? defaultCacheFile();
|
|
20188
|
-
const now = (opts.now ?? Date.now)();
|
|
20189
|
-
if (!opts.forceFresh) {
|
|
20190
|
-
const cached = readCache(cacheFile);
|
|
20191
|
-
if (cached && cached.repo === opts.repo && now - cached.checkedAt < opts.intervalMs) {
|
|
20192
|
-
return {
|
|
20193
|
-
hasUpdate: cmpVersion(local, cached.remote) < 0,
|
|
20194
|
-
local,
|
|
20195
|
-
remote: cached.remote,
|
|
20196
|
-
fromCache: true,
|
|
20197
|
-
cacheAgeMs: now - cached.checkedAt
|
|
20198
|
-
};
|
|
20199
|
-
}
|
|
20200
|
-
}
|
|
20201
|
-
const fetcher = opts.fetcher ?? fetchLatestTagFromGitHub;
|
|
20202
|
-
let remote = null;
|
|
20203
|
-
let fetchErr;
|
|
20204
|
-
try {
|
|
20205
|
-
remote = await fetcher(opts.repo);
|
|
20206
|
-
} catch (e) {
|
|
20207
|
-
fetchErr = e instanceof Error ? e.message : String(e);
|
|
20208
|
-
}
|
|
20209
|
-
if (remote === null) {
|
|
20210
|
-
writeCache(cacheFile, { checkedAt: now, remote: local, repo: opts.repo });
|
|
20211
|
-
return {
|
|
20212
|
-
hasUpdate: false,
|
|
20213
|
-
local,
|
|
20214
|
-
remote: "(no release)",
|
|
20215
|
-
fromCache: false,
|
|
20216
|
-
error: fetchErr ?? "no_release"
|
|
20217
|
-
};
|
|
20218
|
-
}
|
|
20219
|
-
writeCache(cacheFile, { checkedAt: now, remote, repo: opts.repo });
|
|
20220
|
-
return {
|
|
20221
|
-
hasUpdate: cmpVersion(local, remote) < 0,
|
|
20222
|
-
local,
|
|
20223
|
-
remote,
|
|
20224
|
-
fromCache: false
|
|
20225
|
-
};
|
|
20226
|
-
}
|
|
20227
20194
|
function cmpVersion(a, b) {
|
|
20228
20195
|
const pa = parseSemver(a);
|
|
20229
20196
|
const pb = parseSemver(b);
|
|
@@ -20267,97 +20234,6 @@ function readLocalVersion() {
|
|
|
20267
20234
|
return "0.0.0";
|
|
20268
20235
|
}
|
|
20269
20236
|
}
|
|
20270
|
-
function defaultCacheDir() {
|
|
20271
|
-
return process.env["CODEFORGE_CACHE_DIR"] ?? join24(homedir8(), ".cache", "codeforge");
|
|
20272
|
-
}
|
|
20273
|
-
function defaultCacheFile() {
|
|
20274
|
-
return join24(defaultCacheDir(), "update-check.json");
|
|
20275
|
-
}
|
|
20276
|
-
function readCache(file) {
|
|
20277
|
-
try {
|
|
20278
|
-
if (!existsSync5(file))
|
|
20279
|
-
return null;
|
|
20280
|
-
const raw = readFileSync5(file, "utf8");
|
|
20281
|
-
const obj = JSON.parse(raw);
|
|
20282
|
-
if (obj && typeof obj === "object" && typeof obj.checkedAt === "number" && typeof obj.remote === "string" && typeof obj.repo === "string") {
|
|
20283
|
-
return obj;
|
|
20284
|
-
}
|
|
20285
|
-
return null;
|
|
20286
|
-
} catch {
|
|
20287
|
-
return null;
|
|
20288
|
-
}
|
|
20289
|
-
}
|
|
20290
|
-
function writeCache(file, entry) {
|
|
20291
|
-
try {
|
|
20292
|
-
mkdirSync3(dirname14(file), { recursive: true });
|
|
20293
|
-
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
20294
|
-
} catch {}
|
|
20295
|
-
}
|
|
20296
|
-
var GITHUB_API_HOST = "api.github.com";
|
|
20297
|
-
var MAX_REDIRECTS = 5;
|
|
20298
|
-
function fetchLatestTagFromGitHub(repo) {
|
|
20299
|
-
return getJsonWithRedirect(`https://${GITHUB_API_HOST}/repos/${repo}/releases/latest`, MAX_REDIRECTS).then((body) => {
|
|
20300
|
-
if (body === null)
|
|
20301
|
-
return null;
|
|
20302
|
-
try {
|
|
20303
|
-
const json = JSON.parse(body);
|
|
20304
|
-
return typeof json.tag_name === "string" && json.tag_name.length > 0 ? json.tag_name : null;
|
|
20305
|
-
} catch (e) {
|
|
20306
|
-
throw e instanceof Error ? e : new Error(String(e));
|
|
20307
|
-
}
|
|
20308
|
-
});
|
|
20309
|
-
}
|
|
20310
|
-
function getJsonWithRedirect(url2, hopsLeft) {
|
|
20311
|
-
return new Promise((resolve17, reject) => {
|
|
20312
|
-
const u = new URL(url2);
|
|
20313
|
-
const headers = {
|
|
20314
|
-
"User-Agent": "codeforge-update-checker",
|
|
20315
|
-
Accept: "application/vnd.github+json"
|
|
20316
|
-
};
|
|
20317
|
-
if (process.env["GITHUB_TOKEN"]) {
|
|
20318
|
-
headers["Authorization"] = `Bearer ${process.env["GITHUB_TOKEN"]}`;
|
|
20319
|
-
}
|
|
20320
|
-
const req = https.request({
|
|
20321
|
-
host: u.hostname,
|
|
20322
|
-
path: u.pathname + (u.search ?? ""),
|
|
20323
|
-
method: "GET",
|
|
20324
|
-
headers,
|
|
20325
|
-
timeout: 5000
|
|
20326
|
-
}, (res) => {
|
|
20327
|
-
const status = res.statusCode ?? 0;
|
|
20328
|
-
if (status >= 300 && status < 400 && res.headers.location) {
|
|
20329
|
-
res.resume();
|
|
20330
|
-
if (hopsLeft <= 0) {
|
|
20331
|
-
reject(new Error("too_many_redirects"));
|
|
20332
|
-
return;
|
|
20333
|
-
}
|
|
20334
|
-
const next = new URL(res.headers.location, url2).toString();
|
|
20335
|
-
getJsonWithRedirect(next, hopsLeft - 1).then(resolve17, reject);
|
|
20336
|
-
return;
|
|
20337
|
-
}
|
|
20338
|
-
if (status === 404) {
|
|
20339
|
-
res.resume();
|
|
20340
|
-
resolve17(null);
|
|
20341
|
-
return;
|
|
20342
|
-
}
|
|
20343
|
-
if (status >= 400) {
|
|
20344
|
-
res.resume();
|
|
20345
|
-
reject(new Error(`http_${status}`));
|
|
20346
|
-
return;
|
|
20347
|
-
}
|
|
20348
|
-
let body = "";
|
|
20349
|
-
res.setEncoding("utf8");
|
|
20350
|
-
res.on("data", (chunk) => body += chunk);
|
|
20351
|
-
res.on("end", () => resolve17(body));
|
|
20352
|
-
});
|
|
20353
|
-
req.on("timeout", () => {
|
|
20354
|
-
req.destroy();
|
|
20355
|
-
reject(new Error("timeout"));
|
|
20356
|
-
});
|
|
20357
|
-
req.on("error", reject);
|
|
20358
|
-
req.end();
|
|
20359
|
-
});
|
|
20360
|
-
}
|
|
20361
20237
|
async function fetchLatestFromNpm(opts) {
|
|
20362
20238
|
const registry = (opts.registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
20363
20239
|
const channel = opts.channel ?? "latest";
|
|
@@ -20426,256 +20302,75 @@ function defaultHttpFetcher(url2, timeoutMs) {
|
|
|
20426
20302
|
req.end();
|
|
20427
20303
|
});
|
|
20428
20304
|
}
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
extractTarToDir(tarBuf, tmpRoot);
|
|
20437
|
-
const bundlePath = join24(tmpRoot, "package", "dist", "index.js");
|
|
20438
|
-
if (!existsSync5(bundlePath)) {
|
|
20439
|
-
throw new Error(`bundle_not_found: ${bundlePath}`);
|
|
20440
|
-
}
|
|
20441
|
-
return { bundlePath, extractDir: tmpRoot };
|
|
20442
|
-
}
|
|
20443
|
-
function verifyIntegrity(buf, expected) {
|
|
20444
|
-
const m = /^([a-z0-9]+)-(.+)$/i.exec(expected.trim());
|
|
20445
|
-
if (!m) {
|
|
20446
|
-
throw new Error(`integrity_format_invalid: ${expected}`);
|
|
20447
|
-
}
|
|
20448
|
-
const algo = m[1].toLowerCase();
|
|
20449
|
-
const expectedB64 = m[2];
|
|
20450
|
-
if (algo !== "sha512" && algo !== "sha256" && algo !== "sha384") {
|
|
20451
|
-
throw new Error(`integrity_algo_unsupported: ${algo}`);
|
|
20452
|
-
}
|
|
20453
|
-
const actualB64 = createHash4(algo).update(buf).digest("base64");
|
|
20454
|
-
if (actualB64 !== expectedB64) {
|
|
20455
|
-
throw new Error(`integrity_mismatch: expected ${algo}=${expectedB64.slice(0, 16)}... got ${actualB64.slice(0, 16)}...`);
|
|
20456
|
-
}
|
|
20457
|
-
}
|
|
20458
|
-
function extractTarToDir(tarBuf, destRoot) {
|
|
20459
|
-
let offset = 0;
|
|
20460
|
-
while (offset + 512 <= tarBuf.length) {
|
|
20461
|
-
const header = tarBuf.subarray(offset, offset + 512);
|
|
20462
|
-
if (header.every((b) => b === 0))
|
|
20463
|
-
break;
|
|
20464
|
-
const nameRaw = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
20465
|
-
if (!nameRaw) {
|
|
20466
|
-
offset += 512;
|
|
20467
|
-
continue;
|
|
20468
|
-
}
|
|
20469
|
-
const sizeOctal = header.subarray(124, 124 + 12).toString("ascii").replace(/\0.*$/, "").trim();
|
|
20470
|
-
const size = sizeOctal ? parseInt(sizeOctal, 8) : 0;
|
|
20471
|
-
const typeFlag = header.subarray(156, 157).toString("ascii");
|
|
20472
|
-
const prefixRaw = header.subarray(345, 345 + 155).toString("utf8").replace(/\0.*$/, "");
|
|
20473
|
-
const fullName = prefixRaw ? `${prefixRaw}/${nameRaw}` : nameRaw;
|
|
20474
|
-
offset += 512;
|
|
20475
|
-
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
20476
|
-
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
20477
|
-
const dest = join24(destRoot, fullName);
|
|
20478
|
-
mkdirSync3(dirname14(dest), { recursive: true });
|
|
20479
|
-
writeFileSync2(dest, fileBuf);
|
|
20480
|
-
} else if (typeFlag === "5") {
|
|
20481
|
-
mkdirSync3(join24(destRoot, fullName), { recursive: true });
|
|
20482
|
-
}
|
|
20483
|
-
offset += Math.ceil(size / 512) * 512;
|
|
20484
|
-
}
|
|
20485
|
-
}
|
|
20486
|
-
function defaultBinaryFetcher(url2) {
|
|
20487
|
-
return downloadBinary(url2, 3);
|
|
20488
|
-
}
|
|
20489
|
-
function downloadBinary(url2, hopsLeft) {
|
|
20490
|
-
return new Promise((resolve17, reject) => {
|
|
20491
|
-
const u = new URL(url2);
|
|
20492
|
-
const req = https.request({
|
|
20493
|
-
host: u.hostname,
|
|
20494
|
-
port: u.port || undefined,
|
|
20495
|
-
path: u.pathname + (u.search ?? ""),
|
|
20496
|
-
method: "GET",
|
|
20497
|
-
headers: { "User-Agent": "codeforge-update-checker" },
|
|
20498
|
-
timeout: 30000
|
|
20499
|
-
}, (res) => {
|
|
20500
|
-
const status = res.statusCode ?? 0;
|
|
20501
|
-
if (status >= 300 && status < 400 && res.headers.location) {
|
|
20502
|
-
res.resume();
|
|
20503
|
-
if (hopsLeft <= 0) {
|
|
20504
|
-
reject(new Error("too_many_redirects"));
|
|
20505
|
-
return;
|
|
20506
|
-
}
|
|
20507
|
-
const next = new URL(res.headers.location, url2).toString();
|
|
20508
|
-
downloadBinary(next, hopsLeft - 1).then(resolve17, reject);
|
|
20509
|
-
return;
|
|
20510
|
-
}
|
|
20511
|
-
if (status >= 400) {
|
|
20512
|
-
res.resume();
|
|
20513
|
-
reject(new Error(`http_${status}`));
|
|
20514
|
-
return;
|
|
20515
|
-
}
|
|
20516
|
-
const chunks = [];
|
|
20517
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
20518
|
-
res.on("end", () => resolve17(Buffer.concat(chunks)));
|
|
20519
|
-
});
|
|
20520
|
-
req.on("timeout", () => {
|
|
20521
|
-
req.destroy();
|
|
20522
|
-
reject(new Error("timeout"));
|
|
20523
|
-
});
|
|
20524
|
-
req.on("error", reject);
|
|
20525
|
-
req.end();
|
|
20526
|
-
});
|
|
20305
|
+
|
|
20306
|
+
// plugins/update-checker.ts
|
|
20307
|
+
var PLUGIN_NAME20 = "update-checker";
|
|
20308
|
+
var PLUGIN_VERSION = "3.0.0";
|
|
20309
|
+
var _updateCheckStarted = false;
|
|
20310
|
+
function getCacheFile() {
|
|
20311
|
+
return join25(process.env["CODEFORGE_CACHE_DIR"] ?? join25(homedir8(), ".cache", "codeforge"), "update-check.json");
|
|
20527
20312
|
}
|
|
20528
|
-
function
|
|
20529
|
-
const { source, target, oldVersion } = opts;
|
|
20530
|
-
const keep = opts.keepBackups ?? 3;
|
|
20531
|
-
if (!existsSync5(source)) {
|
|
20532
|
-
throw new Error(`atomic_source_missing: ${source}`);
|
|
20533
|
-
}
|
|
20534
|
-
mkdirSync3(dirname14(target), { recursive: true });
|
|
20535
|
-
const newPath = `${target}.new`;
|
|
20536
|
-
const backupPath = `${target}.bak.${oldVersion}`;
|
|
20537
|
-
let strategy = "rename";
|
|
20313
|
+
function readLastInstalledVersion() {
|
|
20538
20314
|
try {
|
|
20539
|
-
|
|
20540
|
-
if (existsSync5(
|
|
20541
|
-
|
|
20542
|
-
|
|
20543
|
-
|
|
20544
|
-
|
|
20545
|
-
|
|
20546
|
-
copyFileSync(target, backupPath);
|
|
20547
|
-
copyFileSync(newPath, target);
|
|
20548
|
-
try {
|
|
20549
|
-
unlinkSync(newPath);
|
|
20550
|
-
} catch {}
|
|
20551
|
-
strategy = "copy_fallback";
|
|
20552
|
-
cleanupOldBackups(target, keep);
|
|
20553
|
-
return { backupPath, strategy };
|
|
20554
|
-
}
|
|
20555
|
-
throw e;
|
|
20556
|
-
}
|
|
20557
|
-
}
|
|
20558
|
-
try {
|
|
20559
|
-
renameSync(newPath, target);
|
|
20560
|
-
} catch (e) {
|
|
20561
|
-
const code = e.code;
|
|
20562
|
-
if (code === "EBUSY" || code === "EPERM" || code === "EACCES") {
|
|
20563
|
-
copyFileSync(newPath, target);
|
|
20564
|
-
try {
|
|
20565
|
-
unlinkSync(newPath);
|
|
20566
|
-
} catch {}
|
|
20567
|
-
strategy = "copy_fallback";
|
|
20568
|
-
} else {
|
|
20569
|
-
throw e;
|
|
20570
|
-
}
|
|
20571
|
-
}
|
|
20572
|
-
cleanupOldBackups(target, keep);
|
|
20573
|
-
return { backupPath, strategy };
|
|
20574
|
-
} catch (e) {
|
|
20575
|
-
try {
|
|
20576
|
-
if (existsSync5(newPath))
|
|
20577
|
-
unlinkSync(newPath);
|
|
20578
|
-
} catch {}
|
|
20579
|
-
throw e;
|
|
20315
|
+
const f = getCacheFile();
|
|
20316
|
+
if (!existsSync5(f))
|
|
20317
|
+
return null;
|
|
20318
|
+
const o = JSON.parse(readFileSync6(f, "utf8"));
|
|
20319
|
+
return typeof o.installedVersion === "string" ? o.installedVersion : null;
|
|
20320
|
+
} catch {
|
|
20321
|
+
return null;
|
|
20580
20322
|
}
|
|
20581
20323
|
}
|
|
20582
|
-
function
|
|
20583
|
-
if (keep <= 0)
|
|
20584
|
-
return;
|
|
20324
|
+
function writeLastInstalledVersion(v) {
|
|
20585
20325
|
try {
|
|
20586
|
-
const
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
20590
|
-
const full = join24(dir, f);
|
|
20591
|
-
let mtimeMs = 0;
|
|
20592
|
-
try {
|
|
20593
|
-
mtimeMs = statSync4(full).mtimeMs;
|
|
20594
|
-
} catch {}
|
|
20595
|
-
return { full, mtimeMs };
|
|
20596
|
-
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
20597
|
-
const toRemove = all.slice(keep);
|
|
20598
|
-
for (const item of toRemove) {
|
|
20599
|
-
try {
|
|
20600
|
-
unlinkSync(item.full);
|
|
20601
|
-
} catch {}
|
|
20602
|
-
}
|
|
20326
|
+
const f = getCacheFile();
|
|
20327
|
+
mkdirSync3(dirname15(f), { recursive: true });
|
|
20328
|
+
writeFileSync2(f, JSON.stringify({ installedVersion: v }, null, 2), "utf8");
|
|
20603
20329
|
} catch {}
|
|
20604
20330
|
}
|
|
20605
|
-
function
|
|
20331
|
+
function resolveNodeBin() {
|
|
20332
|
+
const w = process.platform === "win32";
|
|
20606
20333
|
try {
|
|
20607
|
-
|
|
20608
|
-
if (
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
|
|
20613
|
-
|
|
20614
|
-
|
|
20615
|
-
|
|
20616
|
-
const raw = readFileSync5(file, "utf8");
|
|
20617
|
-
const obj = JSON.parse(raw);
|
|
20618
|
-
if (!obj || typeof obj !== "object")
|
|
20619
|
-
return null;
|
|
20620
|
-
const oc = obj["opencode"];
|
|
20621
|
-
if (!oc || typeof oc !== "object")
|
|
20622
|
-
return null;
|
|
20623
|
-
const o = oc;
|
|
20624
|
-
const min_version = typeof o["min_version"] === "string" ? o["min_version"] : "";
|
|
20625
|
-
const max_version = typeof o["max_version"] === "string" ? o["max_version"] : null;
|
|
20626
|
-
const tested_versions = Array.isArray(o["tested_versions"]) ? o["tested_versions"].filter((v) => typeof v === "string") : [];
|
|
20627
|
-
if (!min_version)
|
|
20628
|
-
return null;
|
|
20629
|
-
return { min_version, max_version, tested_versions };
|
|
20630
|
-
} catch {
|
|
20631
|
-
return null;
|
|
20334
|
+
const r = spawnSync2(w ? "where" : "which", ["node"], { encoding: "utf8", stdio: "pipe" });
|
|
20335
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20336
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20337
|
+
} catch {}
|
|
20338
|
+
for (const c of w ? ["C:\\Program Files\\nodejs\\node.exe"] : ["/usr/local/bin/node", "/usr/bin/node", "/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node"]) {
|
|
20339
|
+
try {
|
|
20340
|
+
if (spawnSync2(c, ["--version"], { stdio: "pipe", timeout: 2000 }).status === 0)
|
|
20341
|
+
return c;
|
|
20342
|
+
} catch {}
|
|
20632
20343
|
}
|
|
20344
|
+
return process.execPath;
|
|
20633
20345
|
}
|
|
20634
|
-
function
|
|
20346
|
+
function resolveNpmBin() {
|
|
20347
|
+
const w = process.platform === "win32";
|
|
20635
20348
|
try {
|
|
20636
|
-
const
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
|
|
20349
|
+
const r = spawnSync2(w ? "where" : "which", ["npm"], { encoding: "utf8", stdio: "pipe" });
|
|
20350
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20351
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20352
|
+
} catch {}
|
|
20353
|
+
for (const c of w ? ["C:\\Program Files\\nodejs\\npm.cmd"] : ["/usr/local/bin/npm", "/usr/bin/npm", "/opt/homebrew/bin/npm"]) {
|
|
20354
|
+
try {
|
|
20355
|
+
if (spawnSync2(c, ["--version"], { stdio: "pipe", timeout: 2000 }).status === 0)
|
|
20356
|
+
return c;
|
|
20357
|
+
} catch {}
|
|
20640
20358
|
}
|
|
20359
|
+
return "npm";
|
|
20641
20360
|
}
|
|
20642
|
-
function
|
|
20643
|
-
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20647
|
-
|
|
20648
|
-
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
message: `opencode ${cur} 在 CodeForge 已测试版本列表内`
|
|
20655
|
-
};
|
|
20656
|
-
}
|
|
20657
|
-
if (cmpVersion(cur, compat.min_version) < 0) {
|
|
20658
|
-
return {
|
|
20659
|
-
status: "below_min",
|
|
20660
|
-
message: `opencode ${cur} 低于 CodeForge 要求的最低版本 ${compat.min_version}`
|
|
20661
|
-
};
|
|
20662
|
-
}
|
|
20663
|
-
if (compat.max_version && cmpVersion(cur, compat.max_version) > 0) {
|
|
20664
|
-
return {
|
|
20665
|
-
status: "above_max",
|
|
20666
|
-
message: `opencode ${cur} 高于 CodeForge 已知兼容上限 ${compat.max_version}`
|
|
20667
|
-
};
|
|
20668
|
-
}
|
|
20669
|
-
const testedDisplay = compat.tested_versions.length > 0 ? compat.tested_versions.join(", ") : "(空)";
|
|
20670
|
-
return {
|
|
20671
|
-
status: "untested",
|
|
20672
|
-
message: `opencode ${cur} 未在 CodeForge 已测试版本列表中(已测:${testedDisplay})`
|
|
20673
|
-
};
|
|
20361
|
+
function getNpmGlobalRoot(npmBin) {
|
|
20362
|
+
try {
|
|
20363
|
+
const r = spawnSync2(npmBin, ["root", "-g"], {
|
|
20364
|
+
encoding: "utf8",
|
|
20365
|
+
stdio: "pipe",
|
|
20366
|
+
timeout: 1e4,
|
|
20367
|
+
shell: process.platform === "win32"
|
|
20368
|
+
});
|
|
20369
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20370
|
+
return r.stdout.trim();
|
|
20371
|
+
} catch {}
|
|
20372
|
+
return null;
|
|
20674
20373
|
}
|
|
20675
|
-
|
|
20676
|
-
// plugins/update-checker.ts
|
|
20677
|
-
var PLUGIN_NAME20 = "update-checker";
|
|
20678
|
-
var PLUGIN_VERSION = "2.0.0";
|
|
20679
20374
|
logLifecycle(PLUGIN_NAME20, "import", { version: PLUGIN_VERSION });
|
|
20680
20375
|
var updateCheckerServer = async (ctx) => {
|
|
20681
20376
|
const yieldResult = shouldYieldToLocalPlugin({ directory: ctx.directory });
|
|
@@ -20699,45 +20394,20 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20699
20394
|
logLifecycle(PLUGIN_NAME20, "activate", {
|
|
20700
20395
|
version: PLUGIN_VERSION,
|
|
20701
20396
|
auto_check_enabled: u.auto_check_enabled,
|
|
20702
|
-
interval_hours: u.interval_hours,
|
|
20703
20397
|
package: u.package,
|
|
20704
20398
|
registry: u.registry,
|
|
20705
20399
|
channel: u.channel,
|
|
20706
|
-
auto_install: u.auto_install
|
|
20707
|
-
backup_keep: u.backup_keep,
|
|
20708
|
-
repo_fallback: u.repo,
|
|
20709
|
-
config_source: "codeforge.json"
|
|
20710
|
-
});
|
|
20711
|
-
await safeAsync(PLUGIN_NAME20, "opencode_version_check", async () => {
|
|
20712
|
-
const compat = loadCompatibility();
|
|
20713
|
-
const opencodeVer = detectOpencodeVersion();
|
|
20714
|
-
const verdict = compareOpencodeVersion({
|
|
20715
|
-
currentOpencodeVer: opencodeVer,
|
|
20716
|
-
compat
|
|
20717
|
-
});
|
|
20718
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20719
|
-
level: "info",
|
|
20720
|
-
msg: "opencode_version_check",
|
|
20721
|
-
opencodeVer,
|
|
20722
|
-
status: verdict.status,
|
|
20723
|
-
verdict_message: verdict.message
|
|
20724
|
-
});
|
|
20725
|
-
if (verdict.status === "untested" || verdict.status === "above_max") {
|
|
20726
|
-
await postToast(ctx, `[CodeForge] ⚠ ${verdict.message}
|
|
20727
|
-
遇到问题请在 issue 中附带本提示`);
|
|
20728
|
-
} else if (verdict.status === "below_min") {
|
|
20729
|
-
const minVer = compat?.min_version ?? "(unknown)";
|
|
20730
|
-
await postToast(ctx, `[CodeForge] ⚠ ${verdict.message}
|
|
20731
|
-
请升级 opencode 到 ${minVer}+`);
|
|
20732
|
-
}
|
|
20400
|
+
auto_install: u.auto_install
|
|
20733
20401
|
});
|
|
20734
20402
|
if (!u.auto_check_enabled) {
|
|
20735
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20736
|
-
|
|
20737
|
-
|
|
20738
|
-
|
|
20403
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_check_disabled" });
|
|
20404
|
+
return {};
|
|
20405
|
+
}
|
|
20406
|
+
if (_updateCheckStarted) {
|
|
20407
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "check_skipped_already_running" });
|
|
20739
20408
|
return {};
|
|
20740
20409
|
}
|
|
20410
|
+
_updateCheckStarted = true;
|
|
20741
20411
|
setImmediate(() => {
|
|
20742
20412
|
safeAsync(PLUGIN_NAME20, "checkAndMaybeUpdate", async () => {
|
|
20743
20413
|
const local = readLocalVersion();
|
|
@@ -20750,224 +20420,72 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20750
20420
|
timeoutMs: 5000
|
|
20751
20421
|
});
|
|
20752
20422
|
} catch (e) {
|
|
20753
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20754
|
-
level: "warn",
|
|
20755
|
-
msg: "npm_fetch_failed",
|
|
20756
|
-
error: e.message
|
|
20757
|
-
});
|
|
20758
|
-
await fallbackToGitHubReleases(ctx, u);
|
|
20423
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "npm_fetch_failed", error: e.message });
|
|
20759
20424
|
return;
|
|
20760
20425
|
}
|
|
20761
|
-
if (!npmResult)
|
|
20762
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20763
|
-
level: "info",
|
|
20764
|
-
msg: "npm_no_release",
|
|
20765
|
-
package: u.package,
|
|
20766
|
-
channel: u.channel
|
|
20767
|
-
});
|
|
20426
|
+
if (!npmResult)
|
|
20768
20427
|
return;
|
|
20769
|
-
|
|
20770
|
-
const hasUpdate = cmpVersion(local,
|
|
20771
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20772
|
-
level: "info",
|
|
20773
|
-
msg: "npm_check_result",
|
|
20774
|
-
local,
|
|
20775
|
-
remote: npmResult.version,
|
|
20776
|
-
hasUpdate,
|
|
20777
|
-
tarballUrl: npmResult.tarballUrl
|
|
20778
|
-
});
|
|
20428
|
+
const remote = npmResult.version;
|
|
20429
|
+
const hasUpdate = cmpVersion(local, remote) < 0;
|
|
20430
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "npm_check_result", local, remote, hasUpdate });
|
|
20779
20431
|
if (!hasUpdate)
|
|
20780
20432
|
return;
|
|
20433
|
+
const lastInstalled = readLastInstalledVersion();
|
|
20434
|
+
if (lastInstalled === remote) {
|
|
20435
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "install_skipped_already_installed", version: remote });
|
|
20436
|
+
return;
|
|
20437
|
+
}
|
|
20781
20438
|
if (!u.auto_install) {
|
|
20782
|
-
await postToast(ctx, `[
|
|
20783
|
-
|
|
20439
|
+
await postToast(ctx, `[codeforge] 有新版本 ${local} → ${remote}
|
|
20440
|
+
运行:codeforge upgrade`);
|
|
20784
20441
|
return;
|
|
20785
20442
|
}
|
|
20786
|
-
await safeAsync(PLUGIN_NAME20, "
|
|
20787
|
-
const
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
|
|
20443
|
+
await safeAsync(PLUGIN_NAME20, "auto_install", async () => {
|
|
20444
|
+
const nodeBin = resolveNodeBin();
|
|
20445
|
+
const npmBin = resolveNpmBin();
|
|
20446
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_install_start", local, remote, nodeBin, npmBin });
|
|
20447
|
+
const r1 = spawnSync2(npmBin, ["install", "-g", `${u.package}@${remote}`], {
|
|
20448
|
+
stdio: "pipe",
|
|
20449
|
+
encoding: "utf8",
|
|
20450
|
+
timeout: 120000,
|
|
20451
|
+
shell: process.platform === "win32"
|
|
20452
|
+
});
|
|
20453
|
+
if (r1.status !== 0) {
|
|
20454
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "npm_install_failed", status: r1.status, stderr: (r1.stderr ?? "").slice(0, 300) });
|
|
20455
|
+
await postToast(ctx, `[codeforge] 自动升级失败(${local} → ${remote}),请手动运行:codeforge upgrade`);
|
|
20456
|
+
return;
|
|
20457
|
+
}
|
|
20458
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "npm_install_success", remote });
|
|
20459
|
+
const npmRoot = getNpmGlobalRoot(npmBin);
|
|
20460
|
+
const codeForgeBin = npmRoot ? join25(npmRoot, "@andyqiu", "codeforge", "bin", "codeforge.mjs") : null;
|
|
20461
|
+
if (!codeForgeBin || !existsSync5(codeForgeBin)) {
|
|
20462
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "codeforge_bin_not_found", path: codeForgeBin ?? "null" });
|
|
20463
|
+
await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署未完成。下次启动将重试,或手动运行:codeforge upgrade`);
|
|
20796
20464
|
return;
|
|
20797
20465
|
}
|
|
20798
|
-
const
|
|
20799
|
-
|
|
20800
|
-
|
|
20466
|
+
const r2 = spawnSync2(nodeBin, [codeForgeBin, "install", "--global", "--skip-build"], {
|
|
20467
|
+
stdio: "pipe",
|
|
20468
|
+
encoding: "utf8",
|
|
20469
|
+
timeout: 60000,
|
|
20470
|
+
shell: false
|
|
20801
20471
|
});
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
const r = spawnSync2(nodeBin, [installMjs, "--global", "--skip-build"], {
|
|
20807
|
-
cwd: join25(extractDir, "package"),
|
|
20808
|
-
stdio: "pipe",
|
|
20809
|
-
encoding: "utf8"
|
|
20810
|
-
});
|
|
20811
|
-
if (r.status === 0) {
|
|
20812
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20813
|
-
level: "info",
|
|
20814
|
-
msg: "auto_install_full_success",
|
|
20815
|
-
local,
|
|
20816
|
-
remote: npmResult.version,
|
|
20817
|
-
strategy: "install.mjs"
|
|
20818
|
-
});
|
|
20819
|
-
await postToast(ctx, `[CodeForge] ✅ 已全量更新 ${local} → ${npmResult.version}(bundle + agents/skills/commands/workflows,重启 opencode 生效)
|
|
20820
|
-
回滚:npx ${u.package} rollback`);
|
|
20821
|
-
return;
|
|
20822
|
-
}
|
|
20823
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20824
|
-
level: "warn",
|
|
20825
|
-
msg: "install_mjs_failed",
|
|
20826
|
-
nodeBin,
|
|
20827
|
-
status: r.status,
|
|
20828
|
-
stderr: r.stderr?.slice(0, 500),
|
|
20829
|
-
stdout: r.stdout?.slice(0, 500)
|
|
20830
|
-
});
|
|
20831
|
-
} else {
|
|
20832
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20833
|
-
level: "info",
|
|
20834
|
-
msg: "install_mjs_absent_fallback_bundle_only",
|
|
20835
|
-
installMjs
|
|
20836
|
-
});
|
|
20837
|
-
}
|
|
20838
|
-
const { backupPath, strategy } = atomicReplaceBundle({
|
|
20839
|
-
source: bundlePath,
|
|
20840
|
-
target,
|
|
20841
|
-
oldVersion: local,
|
|
20842
|
-
keepBackups: u.backup_keep
|
|
20843
|
-
});
|
|
20844
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20845
|
-
level: "info",
|
|
20846
|
-
msg: "auto_install_bundle_only_success",
|
|
20847
|
-
local,
|
|
20848
|
-
remote: npmResult.version,
|
|
20849
|
-
target,
|
|
20850
|
-
backupPath,
|
|
20851
|
-
strategy
|
|
20852
|
-
});
|
|
20853
|
-
await postToast(ctx, `[CodeForge] ✅ 已更新 bundle ${local} → ${npmResult.version}(仅 bundle,agents/skills 未变;重启生效)
|
|
20854
|
-
回滚:npx ${u.package} rollback`);
|
|
20855
|
-
} finally {
|
|
20856
|
-
try {
|
|
20857
|
-
rmSync(extractDir, { recursive: true, force: true });
|
|
20858
|
-
} catch {}
|
|
20472
|
+
if (r2.status !== 0) {
|
|
20473
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "codeforge_install_failed", status: r2.status, stderr: (r2.stderr ?? "").slice(0, 300) });
|
|
20474
|
+
await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署失败。下次启动将重试,或手动运行:codeforge upgrade`);
|
|
20475
|
+
return;
|
|
20859
20476
|
}
|
|
20477
|
+
writeLastInstalledVersion(remote);
|
|
20478
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_install_full_success", local, remote, nodeBin, npmBin });
|
|
20479
|
+
await postToast(ctx, `[codeforge] ✅ 已升级 ${local} → ${remote}(重启 opencode 生效)
|
|
20480
|
+
回滚:npm install -g ${u.package}@${local}`);
|
|
20860
20481
|
});
|
|
20861
20482
|
});
|
|
20862
20483
|
});
|
|
20863
20484
|
return {};
|
|
20864
20485
|
};
|
|
20865
|
-
function resolveNodeBin() {
|
|
20866
|
-
const IS_WIN = process.platform === "win32";
|
|
20867
|
-
try {
|
|
20868
|
-
const r = spawnSync2(IS_WIN ? "where" : "which", ["node"], {
|
|
20869
|
-
encoding: "utf8",
|
|
20870
|
-
stdio: "pipe"
|
|
20871
|
-
});
|
|
20872
|
-
if (r.status === 0 && r.stdout.trim()) {
|
|
20873
|
-
const first = r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20874
|
-
if (first)
|
|
20875
|
-
return first;
|
|
20876
|
-
}
|
|
20877
|
-
} catch {}
|
|
20878
|
-
const candidates = IS_WIN ? [
|
|
20879
|
-
"C:\\Program Files\\nodejs\\node.exe",
|
|
20880
|
-
"C:\\Program Files (x86)\\nodejs\\node.exe"
|
|
20881
|
-
] : [
|
|
20882
|
-
"/usr/local/bin/node",
|
|
20883
|
-
"/usr/bin/node",
|
|
20884
|
-
"/opt/homebrew/bin/node",
|
|
20885
|
-
"/opt/homebrew/opt/node/bin/node"
|
|
20886
|
-
];
|
|
20887
|
-
for (const c of candidates) {
|
|
20888
|
-
try {
|
|
20889
|
-
const t = spawnSync2(c, ["--version"], {
|
|
20890
|
-
encoding: "utf8",
|
|
20891
|
-
stdio: "pipe",
|
|
20892
|
-
timeout: 2000
|
|
20893
|
-
});
|
|
20894
|
-
if (t.status === 0)
|
|
20895
|
-
return c;
|
|
20896
|
-
} catch {}
|
|
20897
|
-
}
|
|
20898
|
-
return process.execPath;
|
|
20899
|
-
}
|
|
20900
|
-
function detectOpencodeVersion() {
|
|
20901
|
-
const env = process.env["OPENCODE_VERSION"];
|
|
20902
|
-
if (env && env.trim().length > 0)
|
|
20903
|
-
return env.trim();
|
|
20904
|
-
return "unknown";
|
|
20905
|
-
}
|
|
20906
|
-
function getOpencodeBundlePath() {
|
|
20907
|
-
const candidates = [];
|
|
20908
|
-
candidates.push(join25(homedir9(), ".config", "opencode", "codeforge", "index.js"));
|
|
20909
|
-
if (process.platform === "win32") {
|
|
20910
|
-
const appData = process.env["APPDATA"];
|
|
20911
|
-
if (appData)
|
|
20912
|
-
candidates.push(join25(appData, "opencode", "codeforge", "index.js"));
|
|
20913
|
-
const localAppData = process.env["LOCALAPPDATA"];
|
|
20914
|
-
if (localAppData)
|
|
20915
|
-
candidates.push(join25(localAppData, "opencode", "codeforge", "index.js"));
|
|
20916
|
-
}
|
|
20917
|
-
for (const c of candidates) {
|
|
20918
|
-
if (existsSync6(c))
|
|
20919
|
-
return c;
|
|
20920
|
-
}
|
|
20921
|
-
return candidates[0] ?? null;
|
|
20922
|
-
}
|
|
20923
|
-
async function fallbackToGitHubReleases(ctx, u) {
|
|
20924
|
-
await safeAsync(PLUGIN_NAME20, "github_fallback", async () => {
|
|
20925
|
-
const result = await checkUpdateOnce({
|
|
20926
|
-
repo: u.repo,
|
|
20927
|
-
intervalMs: u.interval_hours * 3600 * 1000
|
|
20928
|
-
});
|
|
20929
|
-
await reportLegacyResult(ctx, result, u.repo);
|
|
20930
|
-
});
|
|
20931
|
-
}
|
|
20932
|
-
async function reportLegacyResult(ctx, result, repo) {
|
|
20933
|
-
if (result.error && !result.fromCache) {
|
|
20934
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20935
|
-
level: "warn",
|
|
20936
|
-
msg: `update check failed: ${result.error}`,
|
|
20937
|
-
...result
|
|
20938
|
-
});
|
|
20939
|
-
return;
|
|
20940
|
-
}
|
|
20941
|
-
if (!result.hasUpdate) {
|
|
20942
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20943
|
-
level: "info",
|
|
20944
|
-
msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
|
|
20945
|
-
...result
|
|
20946
|
-
});
|
|
20947
|
-
return;
|
|
20948
|
-
}
|
|
20949
|
-
const updateCmd = `bunx --bun github:${repo} install`;
|
|
20950
|
-
const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
|
|
20951
|
-
更新命令:${updateCmd}`;
|
|
20952
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20953
|
-
level: "info",
|
|
20954
|
-
msg: "new_version_available_github_fallback",
|
|
20955
|
-
local: result.local,
|
|
20956
|
-
remote: result.remote,
|
|
20957
|
-
update_command: updateCmd,
|
|
20958
|
-
fromCache: result.fromCache
|
|
20959
|
-
});
|
|
20960
|
-
await postToast(ctx, toast);
|
|
20961
|
-
}
|
|
20962
20486
|
async function postToast(ctx, message) {
|
|
20963
20487
|
await safeAsync(PLUGIN_NAME20, "client.app.log", async () => {
|
|
20964
|
-
await ctx.client.app.log({
|
|
20965
|
-
body: {
|
|
20966
|
-
service: PLUGIN_NAME20,
|
|
20967
|
-
level: "info",
|
|
20968
|
-
message
|
|
20969
|
-
}
|
|
20970
|
-
});
|
|
20488
|
+
await ctx.client.app.log({ body: { service: PLUGIN_NAME20, level: "info", message } });
|
|
20971
20489
|
});
|
|
20972
20490
|
}
|
|
20973
20491
|
var handler20 = updateCheckerServer;
|