feat: add orchestration dispatch state
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user