feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -12,6 +12,7 @@ let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSessio
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let AUTH_SESSION_COOKIE = "";
const TEST_ACCOUNT = "17600003315";
async function setup() {
if (runtimeRoot) {
@@ -45,7 +46,7 @@ test.after(async () => {
async function createAuthedRequest(url: string, method: "POST", body: unknown) {
const session = await createAuthSession({
account: "17600003315",
account: TEST_ACCOUNT,
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
@@ -120,10 +121,41 @@ async function ensureSingleThreadProject() {
return findSingleThreadProject(nextState);
}
async function setProjectTakeover(projectId: string, enabled: boolean) {
const state = await readState();
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
state.userProjectAgentControls.unshift({
account: TEST_ACCOUNT,
projectId,
controls: {
takeoverEnabled: enabled,
updatedAt: enabled ? "2026-04-17T10:00:00.000Z" : "2026-04-17T10:10:00.000Z",
},
});
await writeState(state);
}
async function resetThreadExecutionState(projectId: string) {
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
const targetDevice = project ? state.devices.find((device) => device.id === project.deviceIds[0]) : null;
if (targetDevice) {
targetDevice.preferredExecutionMode = "cli";
}
state.projectExecutionPolicies = state.projectExecutionPolicies.filter((policy) => policy.projectId !== projectId);
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
await writeState(state);
}
test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task for single-thread projects", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const response = await postMessageRoute(
await createAuthedRequest(
@@ -157,17 +189,146 @@ test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task fo
assert.ok(task, "expected a queued conversation_reply task for the single-thread project");
assert.equal(task?.targetProjectId, singleProject.id);
assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId);
assert.equal(task?.targetCodexThreadRef, singleProject.threadMeta.codexThreadRef);
assert.equal(task?.targetCodexFolderRef, singleProject.threadMeta.codexFolderRef);
assert.ok(task?.executionPrompt?.includes("请同步一下当前阻塞情况"));
assert.ok(task?.executionPrompt?.includes(singleProject.threadMeta.threadDisplayName));
assert.ok(!task?.executionPrompt?.includes("threadProjectId:"), "thread prompt should not include project id labels");
assert.ok(!task?.executionPrompt?.includes("folderName:"), "thread prompt should not include folder labels");
assert.ok(!task?.executionPrompt?.includes("deviceIds:"), "thread prompt should not include device id labels");
assert.equal(task?.relayViaMasterAgent, undefined);
});
test("POST /api/v1/projects/[projectId]/messages routes takeover mode to master-agent conversation first", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请继续同步当前线程进展" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "请继续同步当前线程进展",
);
assert.ok(task, "expected a queued conversation_reply task for takeover mode");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, undefined);
assert.equal(task?.targetThreadId, undefined);
assert.equal(task?.targetCodexThreadRef, undefined);
assert.equal(task?.targetCodexFolderRef, undefined);
assert.ok(task?.executionPrompt?.includes("主 Agent"));
assert.ok(task?.executionPrompt?.includes("协同接管"));
assert.ok(task?.executionPrompt?.includes("先准确理解并确认用户意图"));
});
test("takeover prompt asks master agent to sync verified goals and version records when user requests review", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "核对一下项目目标和版本记录,确认后同步到顶部入口" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected a queued takeover task");
assert.equal(task?.relayViaMasterAgent, true);
assert.match(task!.executionPrompt, /用户要求核对或更新项目目标、版本记录时/);
assert.match(task!.executionPrompt, /先让当前线程基于本地开发文档和实际代码重新汇总/);
assert.match(task!.executionPrompt, /自动同步到当前会话顶部的“项目目标”和“版本记录”入口/);
const understandingTask = nextState.masterAgentTasks.find(
(item) =>
item.projectId === "master-agent" &&
item.projectUnderstandingTargetProjectId === singleProject.id &&
item.status === "queued",
);
assert.ok(understandingTask, "expected a hidden project understanding sync task");
assert.match(understandingTask!.executionPrompt, /先基于当前项目本地可见的开发文档和实际代码进行汇总/);
});
test("POST /api/v1/projects/[projectId]/messages still lets takeover mode talk to master agent during gui conflict", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "我先和你确认一下接下来怎么推进" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected takeover mode to queue a master-agent task");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, undefined);
});
test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when the target device prefers gui mode", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
@@ -227,6 +388,7 @@ test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
@@ -289,12 +451,139 @@ test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
});
test("POST /api/v1/projects/[projectId]/messages blocks before queueing when recent codex activity exists without a stored policy", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const recentExternalActivityAt = new Date(Date.now() - 60_000).toISOString();
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === singleProject.id
? {
...project,
threadMeta: {
...project.threadMeta,
lastObservedCodexActivityAt: recentExternalActivityAt,
},
}
: project,
),
projectExecutionPolicies: state.projectExecutionPolicies.filter(
(policy) => policy.projectId !== singleProject.id,
),
});
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请看一下这个项目现在卡在哪里" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 409);
const payload = (await response.json()) as {
ok: boolean;
code?: string;
executionConflict?: {
projectId: string;
preferredExecutionMode: "gui" | "cli";
allowPolicy: "forbid" | "allow_once" | "allow_always";
conflictState: "none" | "warning" | "blocked";
reason: string;
};
};
assert.equal(payload.ok, false);
assert.equal(payload.code, "THREAD_EXECUTION_CONFLICT");
assert.equal(payload.executionConflict?.projectId, singleProject.id);
assert.equal(payload.executionConflict?.preferredExecutionMode, "cli");
assert.equal(payload.executionConflict?.allowPolicy, "forbid");
assert.equal(payload.executionConflict?.conflictState, "blocked");
assert.equal(payload.executionConflict?.reason, "project_conflict_forbid");
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const blockedMessage = updatedProject?.messages.find((message) =>
message.body.includes("请看一下这个项目现在卡在哪里"),
);
assert.equal(blockedMessage, undefined, "blocked send should not append a local chat message");
const queuedTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "请看一下这个项目现在卡在哪里",
);
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
});
test("POST /api/v1/projects/[projectId]/messages ignores stale scoped conflict policies", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const staleExternalActivityAt = new Date(Date.now() - 30 * 60_000).toISOString();
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === singleProject.id
? {
...project,
threadMeta: {
...project.threadMeta,
lastObservedCodexActivityAt: staleExternalActivityAt,
},
}
: project,
),
projectExecutionPolicies: [
...state.projectExecutionPolicies.filter((policy) => policy.projectId !== singleProject.id),
{
deviceId: singleProject.deviceIds[0],
folderKey: buildProjectFolderKey(singleProject),
projectId: singleProject.id,
allowPolicy: "forbid" as const,
conflictState: "blocked" as const,
recentExternalActivityAt: staleExternalActivityAt,
updatedAt: staleExternalActivityAt,
},
],
});
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "继续同步这个线程" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.task?.taskType, "conversation_reply");
assert.equal(payload.task?.status, "queued");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread reply back to the single-thread project", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
await postMessageRoute(
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
@@ -302,13 +591,13 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread re
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as {
task?: { taskId: string };
};
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.targetProjectId === singleProject.id,
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
@@ -337,10 +626,66 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread re
assert.equal(mirroredReply?.sender, "device");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes takeover master replies to the current project", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "托管后请帮我问一下当前阻塞" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as {
task?: { taskId: string };
};
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, undefined);
assert.equal(task?.targetThreadId, undefined);
await setProjectTakeover(singleProject.id, false);
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
replyBody: "我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?",
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const relayedReply = updatedProject?.messages.find((message) =>
message.body.includes("我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?"),
);
assert.ok(relayedReply, "expected a master reply to be written back to the current project");
assert.equal(relayedReply?.sender, "master");
assert.match(relayedReply?.senderLabel ?? "", /主 Agent/);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete blocks leaked thread environment diagnostics from the chat transcript", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
await postMessageRoute(
await createAuthedRequest(