feat: add device-targeted android control flow

This commit is contained in:
Codex
2026-03-23 15:03:17 +08:00
parent 0148440ad5
commit c53ff3965b
11 changed files with 621 additions and 128 deletions

View File

@@ -3,6 +3,8 @@ import type {
ApprovalRequest,
AppState,
BossEvent,
DeviceBinding,
ExecutorKind,
Message,
Session,
SessionDetails,
@@ -70,6 +72,7 @@ export class BossEngine {
status: "active",
activeObjective: "",
lastPlannerSummary: "",
activeWorkerId: null,
createdAt: timestamp,
updatedAt: timestamp,
};
@@ -185,11 +188,17 @@ export class BossEngine {
return this.getSession(sessionId);
}
addMessage(sessionId: string, content: string, channel = "web"): SessionDetails {
addMessage(
sessionId: string,
content: string,
channel = "web",
targetWorkerId: string | null = null,
): SessionDetails {
const session = this.getSession(sessionId).session;
if (session.status === "archived") {
throw new Error(`Session ${sessionId} is archived`);
}
const targetWorker = targetWorkerId ? this.getWorker(targetWorkerId) : null;
const message: Message = {
id: createId("msg"),
sessionId,
@@ -210,6 +219,7 @@ export class BossEngine {
}
mutableSession.activeObjective = message.content;
mutableSession.activeWorkerId = targetWorker?.id ?? mutableSession.activeWorkerId ?? null;
mutableSession.updatedAt = message.createdAt;
if (!mutableSession.title || mutableSession.title === "未命名项目") {
mutableSession.title = message.content.slice(0, 32);
@@ -223,11 +233,13 @@ export class BossEngine {
payload: {
channel,
content: message.content,
targetWorkerId: targetWorker?.id ?? null,
targetWorkerName: targetWorker?.name ?? null,
},
});
});
this.applyPlan(session, message.content);
this.applyPlan(session, message.content, targetWorker?.id ?? null);
return this.getSession(sessionId);
}
@@ -239,12 +251,28 @@ export class BossEngine {
const timestamp = now();
const existing = this.getState().workers.find((worker) => worker.name === input.name);
if (existing) {
return this.updateWorker(existing.id, {
const updated = this.updateWorker(existing.id, {
os: input.os,
capabilities: input.capabilities,
status: "idle",
load: 0,
});
this.commit((state) => {
const pendingBinding = state.deviceBindings.find(
(binding) =>
binding.status === "pending" &&
binding.name === updated.name &&
binding.os === updated.os,
);
if (!pendingBinding) {
return;
}
pendingBinding.status = "claimed";
pendingBinding.claimedWorkerId = updated.id;
pendingBinding.claimedAt = timestamp;
pendingBinding.updatedAt = timestamp;
});
return updated;
}
const worker: WorkerNode = {
@@ -262,6 +290,18 @@ export class BossEngine {
this.commit((state, addEvent) => {
state.workers.push(worker);
const pendingBinding = state.deviceBindings.find(
(binding) =>
binding.status === "pending" &&
binding.name === worker.name &&
binding.os === worker.os,
);
if (pendingBinding) {
pendingBinding.status = "claimed";
pendingBinding.claimedWorkerId = worker.id;
pendingBinding.claimedAt = timestamp;
pendingBinding.updatedAt = timestamp;
}
addEvent({
sessionId: null,
taskId: null,
@@ -280,6 +320,71 @@ export class BossEngine {
return worker;
}
createDeviceBinding(input: {
name: string;
os: WorkerNode["os"];
capabilities: string[];
executor: ExecutorKind;
workspaceHint?: string;
}): DeviceBinding {
const timestamp = now();
const binding: DeviceBinding = {
id: createId("binding"),
token: createId("bindtoken"),
name: input.name.trim(),
os: input.os,
capabilities: Array.from(new Set(input.capabilities)).filter(Boolean),
executor: input.executor,
workspaceHint: input.workspaceHint?.trim() ?? "",
status: "pending",
claimedWorkerId: null,
claimedAt: null,
createdAt: timestamp,
updatedAt: timestamp,
};
if (!binding.name) {
throw new Error("Binding name is required.");
}
if (binding.capabilities.length === 0) {
binding.capabilities = ["terminal"];
}
this.commit((state, addEvent) => {
state.deviceBindings = state.deviceBindings.filter(
(item) => !(item.status === "pending" && item.name === binding.name && item.os === binding.os),
);
state.deviceBindings.unshift(binding);
addEvent({
sessionId: null,
taskId: null,
source: "system",
type: "device.binding.created",
payload: {
bindingId: binding.id,
name: binding.name,
os: binding.os,
executor: binding.executor,
},
});
});
return binding;
}
listDeviceBindings(): DeviceBinding[] {
return this.getState().deviceBindings.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
}
getDeviceBindingByToken(token: string): DeviceBinding {
const binding = this.getState().deviceBindings.find((item) => item.token === token);
if (!binding) {
throw new Error(`Device binding not found: ${token}`);
}
return binding;
}
updateWorker(
workerId: string,
input: Partial<Pick<WorkerNode, "os" | "capabilities" | "status" | "load">>,
@@ -696,9 +801,15 @@ export class BossEngine {
return this.getState();
}
private applyPlan(session: Session, content: string): void {
private applyPlan(session: Session, content: string, targetWorkerId: string | null): void {
const sessionDetails = this.getSession(session.id);
const result = createPlan(sessionDetails.session, content, sessionDetails.tasks.filter(isActiveTask));
const targetWorker = targetWorkerId ? this.getWorker(targetWorkerId) : null;
const result = createPlan(
sessionDetails.session,
content,
sessionDetails.tasks.filter(isActiveTask),
targetWorker,
);
const tasks = materializeTasks(session.id, result);
const plannerMessage = buildPlannerMessage(result.summary);
const timestamp = now();
@@ -711,6 +822,7 @@ export class BossEngine {
}
mutableSession.activeObjective = content;
mutableSession.activeWorkerId = targetWorker?.id ?? mutableSession.activeWorkerId ?? null;
mutableSession.lastPlannerSummary = plannerMessage;
mutableSession.updatedAt = timestamp;