dnd-kit derives the id of its accessibility description element from an internal counter that starts fresh on the server and is already advanced on the client. Every sortable card rendered aria-describedby="DndDescribedBy-0" server-side and "DndDescribedBy-3" client-side, and React reported a hydration mismatch across the whole board. A stable id makes both sides agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* One-off: summarise a project's commits from the command line.
|
|
*
|
|
* Imports the same PURE modules the app uses — batch.ts, preamble.ts,
|
|
* prompt.ts — so the prompts, the batching and the index-matched parsing are
|
|
* byte-identical to what the "Summarise" button does. Only the surrounding
|
|
* plumbing (which carries `server-only`) is reimplemented here.
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
|
|
import { BATCH_SIZE, buildBatchPrompt, chunk, parseBatchResponse } from "./src/lib/ai/batch";
|
|
import { PROMPT_VERSION } from "./src/lib/ai/preamble";
|
|
import { buildSystemPrompt } from "./src/lib/ai/prompt";
|
|
import { stripThinking } from "./src/lib/ai/think-strip";
|
|
|
|
async function main() {
|
|
const pb = new PocketBase(process.env.NEXT_PUBLIC_PB_URL!);
|
|
pb.autoCancellation(false);
|
|
await pb
|
|
.collection("_superusers")
|
|
.authWithPassword(process.env.PB_SUPERUSER_EMAIL!, process.env.PB_SUPERUSER_PASSWORD!);
|
|
|
|
const settings = await pb.collection("settings").getFirstListItem("id != ''");
|
|
const key: string = settings.ai_api_key ?? "";
|
|
const model: string = settings.ai_model || "MiniMax-M2";
|
|
if (!key) throw new Error("No API key configured — paste it in Settings first.");
|
|
|
|
const system = buildSystemPrompt(settings.ai_system_prompt_extra ?? "");
|
|
|
|
const pending = await pb.collection("commits").getFullList({
|
|
filter: [
|
|
"is_merge = false",
|
|
`(summary = "" || summary_version < ${PROMPT_VERSION})`,
|
|
"summary_attempts < 3",
|
|
].join(" && "),
|
|
sort: "-authored_at",
|
|
fields: "id,subject,type,scope,summary_attempts",
|
|
});
|
|
|
|
console.log(`provider=${settings.ai_provider} model=${model}`);
|
|
console.log(`pending commits: ${pending.length}`);
|
|
if (pending.length === 0) return;
|
|
|
|
const batches = chunk(
|
|
pending.map((c) => ({
|
|
id: c.id,
|
|
subject: (c.subject as string) || "",
|
|
type: (c.type as string) || "other",
|
|
scope: (c.scope as string) || "",
|
|
})),
|
|
BATCH_SIZE
|
|
);
|
|
|
|
let summarised = 0;
|
|
let missing = 0;
|
|
let inTok = 0;
|
|
let outTok = 0;
|
|
|
|
for (const [index, batch] of batches.entries()) {
|
|
process.stdout.write(` batch ${index + 1}/${batches.length} … `);
|
|
|
|
let text = "";
|
|
try {
|
|
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
|
|
body: JSON.stringify({
|
|
model,
|
|
max_tokens: 4096,
|
|
n: 1,
|
|
messages: [
|
|
{ role: "system", content: system },
|
|
{ role: "user", content: buildBatchPrompt(batch) },
|
|
],
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.log(`HTTP ${res.status} — ${(await res.text()).slice(0, 120)}`);
|
|
continue;
|
|
}
|
|
|
|
const json = await res.json();
|
|
text = stripThinking(json.choices?.[0]?.message?.content ?? "");
|
|
inTok += json.usage?.prompt_tokens ?? 0;
|
|
outTok += json.usage?.completion_tokens ?? 0;
|
|
} catch (error) {
|
|
console.log(`failed — ${(error as Error).message}`);
|
|
continue;
|
|
}
|
|
|
|
const parsed = parseBatchResponse(text, batch);
|
|
|
|
for (const commit of batch) {
|
|
const summary = parsed.get(commit.id);
|
|
if (!summary) {
|
|
missing += 1;
|
|
continue;
|
|
}
|
|
await pb.collection("commits").update(commit.id, {
|
|
summary,
|
|
summary_version: PROMPT_VERSION,
|
|
summary_attempts: 0,
|
|
});
|
|
summarised += 1;
|
|
}
|
|
|
|
console.log(`${parsed.size}/${batch.length}`);
|
|
}
|
|
|
|
console.log(`\nsummarised: ${summarised}`);
|
|
console.log(`not returned by the model: ${missing}`);
|
|
console.log(`tokens: ${inTok} in, ${outTok} out`);
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error("FAILED:", e?.message ?? e);
|
|
process.exit(1);
|
|
});
|