From 40d93c05d1423195004d8d2850080834d6bec5a3 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 30 Mar 2026 01:13:45 +0800 Subject: [PATCH] feat: add orchestration dispatch state --- src/lib/boss-data.ts | 237 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 470f0b5..d0778a2 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -131,6 +131,12 @@ export type AiAccountRole = "primary" | "backup" | "api_fallback"; export type AiAccountStatus = "ready" | "needs_login" | "needs_api_key" | "degraded" | "disabled"; export type MasterAgentTaskStatus = "queued" | "running" | "completed" | "failed"; export type MasterAgentTaskType = "conversation_reply" | "attachment_analysis"; +export type DispatchPlanStatus = + | "pending_user_confirmation" + | "approved" + | "rejected" + | "dispatched"; +export type DispatchExecutionStatus = "queued" | "running" | "completed" | "failed"; export interface UserSettings { liveUpdates: boolean; @@ -263,6 +269,42 @@ export interface Project { versions: VersionEntry[]; } +export interface DispatchPlanTarget { + deviceId: string; + projectId: string; + threadId: string; + threadDisplayName: string; + folderName: string; + codexFolderRef?: string; + codexThreadRef?: string; + reason: string; +} + +export interface DispatchPlan { + planId: string; + groupProjectId: string; + requestMessageId: string; + requestedBy: string; + status: DispatchPlanStatus; + targets: DispatchPlanTarget[]; + summary: string; + createdAt: string; + confirmedAt?: string; +} + +export interface DispatchExecution { + executionId: string; + planId: string; + groupProjectId: string; + targetProjectId: string; + targetThreadId: string; + deviceId: string; + status: DispatchExecutionStatus; + createdAt: string; + completedAt?: string; + resultMessageId?: string; +} + export interface VerificationCode { id: string; account: string; @@ -667,6 +709,8 @@ export interface BossState { aiAccounts: AiAccount[]; aiAccountSwitchHistory: AiAccountSwitchRecord[]; masterAgentTasks: MasterAgentTask[]; + dispatchPlans: DispatchPlan[]; + dispatchExecutions: DispatchExecution[]; otaUpdates: OtaUpdate[]; otaUpdateLogs: OtaUpdateLog[]; deviceSkills: DeviceSkill[]; @@ -1087,6 +1131,8 @@ const initialState: BossState = { }, ], masterAgentTasks: [], + dispatchPlans: [], + dispatchExecutions: [], otaUpdates: [ { releaseId: "ota_140_to_141", @@ -1484,6 +1530,58 @@ function normalizeGroupMember( }; } +function normalizeDispatchPlanTarget( + raw: Partial, + fallback?: DispatchPlanTarget, +): DispatchPlanTarget { + return { + deviceId: raw.deviceId ?? fallback?.deviceId ?? "", + projectId: raw.projectId ?? fallback?.projectId ?? "", + threadId: raw.threadId ?? fallback?.threadId ?? "", + threadDisplayName: raw.threadDisplayName ?? fallback?.threadDisplayName ?? "", + folderName: raw.folderName ?? fallback?.folderName ?? "", + codexFolderRef: raw.codexFolderRef ?? fallback?.codexFolderRef, + codexThreadRef: raw.codexThreadRef ?? fallback?.codexThreadRef, + reason: raw.reason ?? fallback?.reason ?? "", + }; +} + +function normalizeDispatchPlan(raw: Partial, fallback?: DispatchPlan): DispatchPlan { + const fallbackTargets = fallback?.targets ?? []; + return { + planId: raw.planId ?? fallback?.planId ?? randomToken("dispatch-plan"), + groupProjectId: raw.groupProjectId ?? fallback?.groupProjectId ?? "", + requestMessageId: raw.requestMessageId ?? fallback?.requestMessageId ?? "", + requestedBy: raw.requestedBy ?? fallback?.requestedBy ?? "", + status: raw.status ?? fallback?.status ?? "pending_user_confirmation", + targets: ensureArray(raw.targets as Partial[] | undefined, fallbackTargets).map( + (target, index) => + normalizeDispatchPlanTarget(target, fallbackTargets[index % Math.max(1, fallbackTargets.length)]), + ), + summary: raw.summary ?? fallback?.summary ?? "", + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + confirmedAt: raw.confirmedAt ?? fallback?.confirmedAt, + }; +} + +function normalizeDispatchExecution( + raw: Partial, + fallback?: DispatchExecution, +): DispatchExecution { + return { + executionId: raw.executionId ?? fallback?.executionId ?? randomToken("dispatch-exec"), + planId: raw.planId ?? fallback?.planId ?? "", + groupProjectId: raw.groupProjectId ?? fallback?.groupProjectId ?? "", + targetProjectId: raw.targetProjectId ?? fallback?.targetProjectId ?? "", + targetThreadId: raw.targetThreadId ?? fallback?.targetThreadId ?? "", + deviceId: raw.deviceId ?? fallback?.deviceId ?? "", + status: raw.status ?? fallback?.status ?? "queued", + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + completedAt: raw.completedAt ?? fallback?.completedAt, + resultMessageId: raw.resultMessageId ?? fallback?.resultMessageId, + }; +} + function dedupeStrings(values: string[]) { return [...new Set(values.filter((value) => Boolean(value)))]; } @@ -2156,6 +2254,15 @@ function normalizeState(raw: Partial | undefined): BossState { errorMessage: task.errorMessage, requestId: task.requestId, })), + dispatchPlans: ensureArray(raw.dispatchPlans, base.dispatchPlans).map((plan, index) => + normalizeDispatchPlan(plan, base.dispatchPlans[index % Math.max(1, base.dispatchPlans.length)]), + ), + dispatchExecutions: ensureArray(raw.dispatchExecutions, base.dispatchExecutions).map((execution, index) => + normalizeDispatchExecution( + execution, + base.dispatchExecutions[index % Math.max(1, base.dispatchExecutions.length)], + ), + ), otaUpdates: ensureArray(raw.otaUpdates, base.otaUpdates).map((update, index) => ({ ...base.otaUpdates[index % base.otaUpdates.length], ...update, @@ -2637,6 +2744,12 @@ function syncDerivedState(input: BossState) { state.masterAgentTasks = state.masterAgentTasks .sort((a, b) => b.requestedAt.localeCompare(a.requestedAt)) .slice(0, 80); + state.dispatchPlans = state.dispatchPlans + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .slice(0, 80); + state.dispatchExecutions = state.dispatchExecutions + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .slice(0, 160); state.devices = state.devices.filter(isProductionDevice); const visibleDeviceIds = new Set(state.devices.map((device) => device.id)); @@ -3516,6 +3629,130 @@ export async function queueMasterAgentTask(payload: { return task; } +export async function createDispatchPlan(input: { + groupProjectId: string; + requestMessageId: string; + requestedBy: string; + summary?: string; + targets: DispatchPlanTarget[]; +}) { + return mutateState((state) => { + const groupProjectId = input.groupProjectId.trim(); + const requestMessageId = input.requestMessageId.trim(); + const requestedBy = input.requestedBy.trim(); + const targets = input.targets.map((target) => normalizeDispatchPlanTarget(target)); + + if (!groupProjectId) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_REQUIRED"); + if (!requestMessageId) throw new Error("DISPATCH_PLAN_REQUEST_MESSAGE_REQUIRED"); + if (!requestedBy) throw new Error("DISPATCH_PLAN_REQUESTED_BY_REQUIRED"); + if (targets.length === 0) throw new Error("DISPATCH_PLAN_TARGETS_REQUIRED"); + + const plan: DispatchPlan = { + planId: randomToken("dispatch-plan"), + groupProjectId, + requestMessageId, + requestedBy, + status: "pending_user_confirmation", + targets, + summary: input.summary?.trim() ?? "", + createdAt: nowIso(), + }; + state.dispatchPlans.unshift(plan); + return plan; + }); +} + +export async function listDispatchPlansByProject(groupProjectId: string) { + const state = await readState(); + const normalizedGroupProjectId = groupProjectId.trim(); + return state.dispatchPlans + .filter((plan) => plan.groupProjectId === normalizedGroupProjectId) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export async function confirmDispatchPlan(input: { + planId: string; + confirmedBy?: string; +}) { + return mutateState((state) => { + const plan = state.dispatchPlans.find((item) => item.planId === input.planId); + if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND"); + if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED"); + + if (plan.status !== "dispatched") { + plan.status = "approved"; + } + if (!plan.confirmedAt) { + plan.confirmedAt = nowIso(); + } + return plan; + }); +} + +export async function createDispatchExecutionsFromPlan(input: { + planId: string; + approvedTargetProjectIds?: string[]; + confirmedBy?: string; +}) { + return mutateState((state) => { + const plan = state.dispatchPlans.find((item) => item.planId === input.planId); + if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND"); + if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED"); + + const existingExecutions = state.dispatchExecutions.filter((item) => item.planId === plan.planId); + if (existingExecutions.length > 0) { + plan.status = "dispatched"; + return existingExecutions; + } + + const allowedTargets = input.approvedTargetProjectIds?.length + ? new Set(input.approvedTargetProjectIds) + : null; + const targets = plan.targets.filter((target) => + allowedTargets ? allowedTargets.has(target.projectId) : true, + ); + if (targets.length === 0) { + throw new Error("DISPATCH_EXECUTION_TARGETS_REQUIRED"); + } + if (plan.status === "pending_user_confirmation") { + plan.status = "approved"; + } + const createdAt = nowIso(); + const executions = targets.map((target) => { + const execution: DispatchExecution = { + executionId: randomToken("dispatch-exec"), + planId: plan.planId, + groupProjectId: plan.groupProjectId, + targetProjectId: target.projectId, + targetThreadId: target.threadId, + deviceId: target.deviceId, + status: "queued", + createdAt, + }; + state.dispatchExecutions.unshift(execution); + return execution; + }); + plan.status = "dispatched"; + return executions; + }); +} + +export async function completeDispatchExecution(payload: { + executionId: string; + status: "completed" | "failed"; + resultMessageId?: string; +}) { + return mutateState((state) => { + const execution = state.dispatchExecutions.find((item) => item.executionId === payload.executionId); + if (!execution) throw new Error("DISPATCH_EXECUTION_NOT_FOUND"); + + execution.status = payload.status; + execution.completedAt = nowIso(); + execution.resultMessageId = payload.resultMessageId?.trim() || undefined; + return execution; + }); +} + export async function getMasterAgentTask(taskId: string) { const state = await readState(); return state.masterAgentTasks.find((item) => item.taskId === taskId) ?? null;