feat: add orchestration dispatch state

This commit is contained in:
kris
2026-03-30 01:13:45 +08:00
parent 949dcf7845
commit 40d93c05d1

View File

@@ -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<DispatchPlanTarget>,
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<DispatchPlan>, 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<DispatchPlanTarget>[] | 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<DispatchExecution>,
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<BossState> | 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;