feat: add device-targeted android control flow
This commit is contained in:
122
src/engine.ts
122
src/engine.ts
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user