fix: keep imported thread candidates distinct

This commit is contained in:
kris
2026-04-03 05:02:18 +08:00
parent 42cf489450
commit ad7dd94d95
2 changed files with 150 additions and 3 deletions

View File

@@ -2021,7 +2021,7 @@ function buildDeviceImportCandidateId(input: {
]
.filter(Boolean)
.join("-");
return `import-${slugify(signature)}`;
return `import-${slugifyWithHash(signature)}`;
}
function normalizeDeviceImportCandidate(
@@ -2316,6 +2316,12 @@ function slugify(value: string) {
.slice(0, 48) || `item-${randomBytes(2).toString("hex")}`;
}
function slugifyWithHash(value: string) {
const base = slugify(value).slice(0, 40);
const hash = createHash("sha1").update(value).digest("hex").slice(0, 8);
return `${base || "item"}-${hash}`;
}
const aiRolePriority: Record<AiAccountRole, number> = {
primary: 0,
backup: 1,
@@ -7144,8 +7150,8 @@ function parseDeviceImportResolutionReply(
function buildImportedThreadProject(device: Device, candidate: DeviceImportCandidate) {
const projectId =
candidate.codexThreadRef?.trim() && candidate.codexFolderRef?.trim()
? slugify(`${device.id}-${candidate.codexFolderRef}-${candidate.codexThreadRef}`)
: slugify(`${device.id}-${candidate.folderName}-${candidate.threadId}`);
? slugifyWithHash(`${device.id}-${candidate.codexFolderRef}-${candidate.codexThreadRef}`)
: slugifyWithHash(`${device.id}-${candidate.folderName}-${candidate.threadId}`);
const now = nowIso();
return normalizeProject({
id: projectId,

View File

@@ -0,0 +1,141 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdtemp, rm } from "node:fs/promises";
import { NextRequest } from "next/server";
let runtimeRoot = "";
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
let AUTH_SESSION_COOKIE = "";
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-import-candidate-id-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [heartbeatModule, data, auth] = await Promise.all([
import("../src/app/api/device-heartbeat/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
deviceHeartbeatRoute = heartbeatModule.POST;
createAuthSession = data.createAuthSession;
readState = data.readState;
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
async function createAuthedSessionCookie() {
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return `${AUTH_SESSION_COOKIE}=${session.sessionToken}`;
}
test("auto-sync import keeps long-prefix project candidates distinct", async () => {
await setup();
const cookie = await createAuthedSessionCookie();
const state = await readState();
state.devices = [
{
id: "mac-studio",
name: "Mac Studio",
avatar: "M",
account: "17600003315",
source: "production",
status: "online",
projects: [],
quota5h: 68,
quota7d: 81,
lastSeenAt: "2026-04-03T10:00:00.000Z",
endpoint: "mac://kris.local",
token: "boss-mac-studio-token",
note: "本机 Codex 主节点",
},
];
const response = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: {
"content-type": "application/json",
cookie,
},
body: JSON.stringify({
deviceId: "mac-studio",
token: "boss-mac-studio-token",
name: "Mac Studio",
avatar: "M",
account: "17600003315",
status: "online",
quota5h: 68,
quota7d: 81,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "AItoukui",
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/AItoukui",
threadId: "019d4efb-3485-7b21-86dc-b70d3f4adf68",
threadDisplayName: "拉取代码并验证交接文档和本地启动状态",
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/AItoukui",
codexThreadRef: "019d4efb-3485-7b21-86dc-b70d3f4adf68",
lastActiveAt: "2026-04-02T20:50:36.000Z",
suggestedImport: true,
},
{
folderName: "500Gcode",
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode",
threadId: "019d4e19-1d27-77d3-bc54-2a1c80c7431b",
threadDisplayName: "本机运行3.5B模型并搭建Web演示",
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode",
codexThreadRef: "019d4e19-1d27-77d3-bc54-2a1c80c7431b",
lastActiveAt: "2026-04-02T20:50:36.000Z",
suggestedImport: true,
},
{
folderName: "figma",
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/figma",
threadId: "019d4e1d-03b8-7610-b427-c4ffd234aed4",
threadDisplayName: "配置Figma账号并启用UI制作Skill",
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/figma",
codexThreadRef: "019d4e1d-03b8-7610-b427-c4ffd234aed4",
lastActiveAt: "2026-04-02T20:50:36.000Z",
suggestedImport: true,
},
],
}),
}),
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
importDraft?: { candidates: Array<{ candidateId: string }>; appliedProjectNames: string[] };
};
const candidateIds = payload.importDraft?.candidates.map((candidate) => candidate.candidateId) ?? [];
assert.equal(new Set(candidateIds).size, 3);
const nextState = await readState();
const importedProjects = nextState.projects.filter((project) =>
["AItoukui", "500Gcode", "figma"].includes(project.threadMeta.folderName),
);
assert.equal(importedProjects.length, 3);
assert.deepEqual(
importedProjects.map((project) => project.threadMeta.folderName).sort(),
["500Gcode", "AItoukui", "figma"],
);
});