feat: wire dispatch execution and device import flows
This commit is contained in:
@@ -134,7 +134,9 @@ export type MasterAgentTaskStatus = "queued" | "running" | "completed" | "failed
|
||||
export type MasterAgentTaskType =
|
||||
| "conversation_reply"
|
||||
| "attachment_analysis"
|
||||
| "group_dispatch_plan";
|
||||
| "group_dispatch_plan"
|
||||
| "dispatch_execution"
|
||||
| "device_import_resolution";
|
||||
export type DispatchPlanStatus =
|
||||
| "pending_user_confirmation"
|
||||
| "approved"
|
||||
@@ -312,6 +314,54 @@ export interface DispatchExecution {
|
||||
completedByDeviceId?: string;
|
||||
}
|
||||
|
||||
export interface DeviceImportCandidate {
|
||||
candidateId: string;
|
||||
deviceId: string;
|
||||
folderName: string;
|
||||
folderRef?: string;
|
||||
threadId: string;
|
||||
threadDisplayName: string;
|
||||
codexFolderRef?: string;
|
||||
codexThreadRef?: string;
|
||||
lastActiveAt: string;
|
||||
suggestedImport: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceImportDraft {
|
||||
draftId: string;
|
||||
deviceId: string;
|
||||
enrollmentId?: string;
|
||||
status: "pending_candidates" | "pending_selection" | "pending_resolution" | "resolved" | "applied";
|
||||
candidates: DeviceImportCandidate[];
|
||||
selectedCandidateIds: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
reviewedAt?: string;
|
||||
reviewedBy?: string;
|
||||
resolutionId?: string;
|
||||
}
|
||||
|
||||
export interface DeviceImportResolutionItem {
|
||||
candidateId: string;
|
||||
action: "create_thread_conversation" | "attach_existing" | "skip";
|
||||
threadDisplayName: string;
|
||||
folderName: string;
|
||||
targetProjectId?: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface DeviceImportResolution {
|
||||
resolutionId: string;
|
||||
draftId: string;
|
||||
deviceId: string;
|
||||
status: "ready" | "applied";
|
||||
summary: string;
|
||||
items: DeviceImportResolutionItem[];
|
||||
createdAt: string;
|
||||
appliedAt?: string;
|
||||
appliedBy?: string;
|
||||
}
|
||||
|
||||
export interface VerificationCode {
|
||||
id: string;
|
||||
account: string;
|
||||
@@ -467,6 +517,11 @@ export interface MasterAgentTask {
|
||||
attachmentDownloadExpiresAt?: string;
|
||||
attachmentDownloadUrl?: string;
|
||||
attachmentTextExcerpt?: string;
|
||||
dispatchExecutionId?: string;
|
||||
targetProjectId?: string;
|
||||
targetThreadId?: string;
|
||||
targetThreadDisplayName?: string;
|
||||
deviceImportDraftId?: string;
|
||||
status: MasterAgentTaskStatus;
|
||||
requestedAt: string;
|
||||
claimedAt?: string;
|
||||
@@ -718,6 +773,8 @@ export interface BossState {
|
||||
masterAgentTasks: MasterAgentTask[];
|
||||
dispatchPlans: DispatchPlan[];
|
||||
dispatchExecutions: DispatchExecution[];
|
||||
deviceImportDrafts: DeviceImportDraft[];
|
||||
deviceImportResolutions: DeviceImportResolution[];
|
||||
otaUpdates: OtaUpdate[];
|
||||
otaUpdateLogs: OtaUpdateLog[];
|
||||
deviceSkills: DeviceSkill[];
|
||||
@@ -1140,6 +1197,8 @@ const initialState: BossState = {
|
||||
masterAgentTasks: [],
|
||||
dispatchPlans: [],
|
||||
dispatchExecutions: [],
|
||||
deviceImportDrafts: [],
|
||||
deviceImportResolutions: [],
|
||||
otaUpdates: [
|
||||
{
|
||||
releaseId: "ota_140_to_141",
|
||||
@@ -1760,6 +1819,125 @@ function normalizeDispatchExecution(
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeviceImportCandidateId(input: {
|
||||
deviceId: string;
|
||||
folderName: string;
|
||||
threadId: string;
|
||||
codexFolderRef?: string;
|
||||
codexThreadRef?: string;
|
||||
}) {
|
||||
const signature = [
|
||||
input.deviceId,
|
||||
input.codexFolderRef?.trim() || input.folderName.trim(),
|
||||
input.codexThreadRef?.trim() || input.threadId.trim(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-");
|
||||
return `import-${slugify(signature)}`;
|
||||
}
|
||||
|
||||
function normalizeDeviceImportCandidate(
|
||||
raw: Partial<DeviceImportCandidate>,
|
||||
fallback?: DeviceImportCandidate,
|
||||
): DeviceImportCandidate {
|
||||
const deviceId = raw.deviceId ?? fallback?.deviceId ?? "";
|
||||
const folderName = raw.folderName ?? fallback?.folderName ?? "";
|
||||
const threadId = raw.threadId ?? fallback?.threadId ?? "";
|
||||
return {
|
||||
candidateId:
|
||||
raw.candidateId ??
|
||||
fallback?.candidateId ??
|
||||
buildDeviceImportCandidateId({
|
||||
deviceId,
|
||||
folderName,
|
||||
threadId,
|
||||
codexFolderRef: raw.codexFolderRef ?? fallback?.codexFolderRef,
|
||||
codexThreadRef: raw.codexThreadRef ?? fallback?.codexThreadRef,
|
||||
}),
|
||||
deviceId,
|
||||
folderName,
|
||||
folderRef: raw.folderRef ?? fallback?.folderRef,
|
||||
threadId,
|
||||
threadDisplayName: raw.threadDisplayName ?? fallback?.threadDisplayName ?? threadId,
|
||||
codexFolderRef: raw.codexFolderRef ?? fallback?.codexFolderRef,
|
||||
codexThreadRef: raw.codexThreadRef ?? fallback?.codexThreadRef,
|
||||
lastActiveAt: raw.lastActiveAt ?? fallback?.lastActiveAt ?? nowIso(),
|
||||
suggestedImport: raw.suggestedImport ?? fallback?.suggestedImport ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDeviceImportDraft(
|
||||
raw: Partial<DeviceImportDraft>,
|
||||
fallback?: DeviceImportDraft,
|
||||
): DeviceImportDraft {
|
||||
const fallbackCandidates = fallback?.candidates ?? [];
|
||||
return {
|
||||
draftId: raw.draftId ?? fallback?.draftId ?? randomToken("import-draft"),
|
||||
deviceId: raw.deviceId ?? fallback?.deviceId ?? "",
|
||||
enrollmentId: raw.enrollmentId ?? fallback?.enrollmentId,
|
||||
status: raw.status ?? fallback?.status ?? "pending_candidates",
|
||||
candidates: ensureArray(
|
||||
raw.candidates as Partial<DeviceImportCandidate>[] | undefined,
|
||||
fallbackCandidates,
|
||||
).map((candidate, index) =>
|
||||
normalizeDeviceImportCandidate(
|
||||
candidate,
|
||||
fallbackCandidates[index % Math.max(1, fallbackCandidates.length)],
|
||||
),
|
||||
),
|
||||
selectedCandidateIds: dedupeStrings(
|
||||
ensureArray(raw.selectedCandidateIds, fallback?.selectedCandidateIds ?? []),
|
||||
),
|
||||
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
|
||||
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
|
||||
reviewedAt: raw.reviewedAt ?? fallback?.reviewedAt,
|
||||
reviewedBy: raw.reviewedBy ?? fallback?.reviewedBy,
|
||||
resolutionId: raw.resolutionId ?? fallback?.resolutionId,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDeviceImportResolution(
|
||||
raw: Partial<DeviceImportResolution>,
|
||||
fallback?: DeviceImportResolution,
|
||||
): DeviceImportResolution {
|
||||
const fallbackItems = fallback?.items ?? [];
|
||||
return {
|
||||
resolutionId: raw.resolutionId ?? fallback?.resolutionId ?? randomToken("import-resolution"),
|
||||
draftId: raw.draftId ?? fallback?.draftId ?? "",
|
||||
deviceId: raw.deviceId ?? fallback?.deviceId ?? "",
|
||||
status: raw.status ?? fallback?.status ?? "ready",
|
||||
summary: raw.summary ?? fallback?.summary ?? "",
|
||||
items: ensureArray(
|
||||
raw.items as Partial<DeviceImportResolutionItem>[] | undefined,
|
||||
fallbackItems,
|
||||
).map((item, index) => ({
|
||||
candidateId: item.candidateId ?? fallbackItems[index % Math.max(1, fallbackItems.length)]?.candidateId ?? "",
|
||||
action:
|
||||
item.action ??
|
||||
fallbackItems[index % Math.max(1, fallbackItems.length)]?.action ??
|
||||
"skip",
|
||||
threadDisplayName:
|
||||
item.threadDisplayName ??
|
||||
fallbackItems[index % Math.max(1, fallbackItems.length)]?.threadDisplayName ??
|
||||
"",
|
||||
folderName:
|
||||
item.folderName ??
|
||||
fallbackItems[index % Math.max(1, fallbackItems.length)]?.folderName ??
|
||||
"",
|
||||
targetProjectId:
|
||||
item.targetProjectId ??
|
||||
fallbackItems[index % Math.max(1, fallbackItems.length)]?.targetProjectId,
|
||||
reason:
|
||||
item.reason ??
|
||||
fallbackItems[index % Math.max(1, fallbackItems.length)]?.reason ??
|
||||
"",
|
||||
})),
|
||||
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
|
||||
appliedAt: raw.appliedAt ?? fallback?.appliedAt,
|
||||
appliedBy: raw.appliedBy ?? fallback?.appliedBy,
|
||||
};
|
||||
}
|
||||
|
||||
function dedupeStrings(values: string[]) {
|
||||
return [...new Set(values.filter((value) => Boolean(value)))];
|
||||
}
|
||||
@@ -2424,6 +2602,11 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
attachmentDownloadExpiresAt: task.attachmentDownloadExpiresAt,
|
||||
attachmentDownloadUrl: task.attachmentDownloadUrl,
|
||||
attachmentTextExcerpt: task.attachmentTextExcerpt,
|
||||
dispatchExecutionId: task.dispatchExecutionId,
|
||||
targetProjectId: task.targetProjectId,
|
||||
targetThreadId: task.targetThreadId,
|
||||
targetThreadDisplayName: task.targetThreadDisplayName,
|
||||
deviceImportDraftId: task.deviceImportDraftId,
|
||||
status: task.status ?? "queued",
|
||||
requestedAt: task.requestedAt ?? nowIso(),
|
||||
claimedAt: task.claimedAt,
|
||||
@@ -2441,6 +2624,21 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
base.dispatchExecutions[index % Math.max(1, base.dispatchExecutions.length)],
|
||||
),
|
||||
),
|
||||
deviceImportDrafts: ensureArray(raw.deviceImportDrafts, base.deviceImportDrafts).map((draft, index) =>
|
||||
normalizeDeviceImportDraft(
|
||||
draft,
|
||||
base.deviceImportDrafts[index % Math.max(1, base.deviceImportDrafts.length)],
|
||||
),
|
||||
),
|
||||
deviceImportResolutions: ensureArray(
|
||||
raw.deviceImportResolutions,
|
||||
base.deviceImportResolutions,
|
||||
).map((resolution, index) =>
|
||||
normalizeDeviceImportResolution(
|
||||
resolution,
|
||||
base.deviceImportResolutions[index % Math.max(1, base.deviceImportResolutions.length)],
|
||||
),
|
||||
),
|
||||
otaUpdates: ensureArray(raw.otaUpdates, base.otaUpdates).map((update, index) => ({
|
||||
...base.otaUpdates[index % base.otaUpdates.length],
|
||||
...update,
|
||||
@@ -2928,6 +3126,12 @@ function syncDerivedState(input: BossState) {
|
||||
state.dispatchExecutions = state.dispatchExecutions
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, 160);
|
||||
state.deviceImportDrafts = state.deviceImportDrafts
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.slice(0, 40);
|
||||
state.deviceImportResolutions = state.deviceImportResolutions
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, 80);
|
||||
|
||||
state.devices = state.devices.filter(isProductionDevice);
|
||||
const visibleDeviceIds = new Set(state.devices.map((device) => device.id));
|
||||
@@ -2942,6 +3146,13 @@ function syncDerivedState(input: BossState) {
|
||||
visibleThreadIds.has(item.threadId),
|
||||
);
|
||||
state.deviceEnrollments = state.deviceEnrollments.filter((item) => visibleDeviceIds.has(item.deviceId));
|
||||
state.deviceImportDrafts = state.deviceImportDrafts.filter((item) =>
|
||||
visibleDeviceIds.has(item.deviceId),
|
||||
);
|
||||
const visibleImportDraftIds = new Set(state.deviceImportDrafts.map((item) => item.draftId));
|
||||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||||
(item) => visibleDeviceIds.has(item.deviceId) && visibleImportDraftIds.has(item.draftId),
|
||||
);
|
||||
state.deviceSkills = state.deviceSkills
|
||||
.filter((skill) => visibleDeviceIds.has(skill.deviceId))
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
@@ -3773,6 +3984,10 @@ export async function queueMasterAgentTask(payload: {
|
||||
attachmentDownloadExpiresAt?: string;
|
||||
attachmentDownloadUrl?: string;
|
||||
attachmentTextExcerpt?: string;
|
||||
dispatchExecutionId?: string;
|
||||
targetProjectId?: string;
|
||||
targetThreadId?: string;
|
||||
targetThreadDisplayName?: string;
|
||||
}) {
|
||||
const task = await mutateState((state) => {
|
||||
const task: MasterAgentTask = {
|
||||
@@ -3793,6 +4008,10 @@ export async function queueMasterAgentTask(payload: {
|
||||
attachmentDownloadExpiresAt: payload.attachmentDownloadExpiresAt,
|
||||
attachmentDownloadUrl: payload.attachmentDownloadUrl,
|
||||
attachmentTextExcerpt: payload.attachmentTextExcerpt,
|
||||
dispatchExecutionId: payload.dispatchExecutionId,
|
||||
targetProjectId: payload.targetProjectId,
|
||||
targetThreadId: payload.targetThreadId,
|
||||
targetThreadDisplayName: payload.targetThreadDisplayName,
|
||||
status: "queued",
|
||||
requestedAt: nowIso(),
|
||||
};
|
||||
@@ -3953,6 +4172,7 @@ export async function createDispatchExecutionsFromPlan(input: {
|
||||
if (plan.status !== "dispatched") {
|
||||
plan.status = "dispatched";
|
||||
}
|
||||
ensureDispatchExecutionTasksInState(state, plan, existingExecutions);
|
||||
return existingExecutions;
|
||||
}
|
||||
|
||||
@@ -3977,11 +4197,116 @@ export async function createDispatchExecutionsFromPlan(input: {
|
||||
state.dispatchExecutions.unshift(execution);
|
||||
return execution;
|
||||
});
|
||||
ensureDispatchExecutionTasksInState(state, plan, executions);
|
||||
plan.status = "dispatched";
|
||||
return executions;
|
||||
});
|
||||
}
|
||||
|
||||
function buildDispatchExecutionPrompt(input: {
|
||||
groupProject: Project;
|
||||
plan: DispatchPlan;
|
||||
target: DispatchPlanTarget;
|
||||
}) {
|
||||
const requestMessage = input.groupProject.messages.find(
|
||||
(message) => message.id === input.plan.requestMessageId,
|
||||
);
|
||||
const requestText = requestMessage?.body ?? input.plan.summary;
|
||||
return [
|
||||
"你正在执行 Boss 控制台的线程分发任务。",
|
||||
"你的输出必须是 JSON,并且只能包含两个字符串字段:rawThreadReply、replyBody。",
|
||||
"rawThreadReply:写成目标线程直接回到群里的原始结果,不要冒充主 Agent。",
|
||||
"replyBody:写成主 Agent 给群里的简短汇总,必须保留“主 Agent 汇总:”前缀。",
|
||||
"不要输出 Markdown 代码块,不要输出额外解释。",
|
||||
`groupProjectId: ${input.groupProject.id}`,
|
||||
`groupProjectName: ${input.groupProject.name}`,
|
||||
`threadProjectId: ${input.target.projectId}`,
|
||||
`threadId: ${input.target.threadId}`,
|
||||
`threadTitle: ${input.target.threadDisplayName}`,
|
||||
`folderName: ${input.target.folderName}`,
|
||||
`requestText: ${requestText}`,
|
||||
`dispatchSummary: ${input.plan.summary}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function ensureDispatchExecutionTaskInState(
|
||||
state: BossState,
|
||||
plan: DispatchPlan,
|
||||
execution: DispatchExecution,
|
||||
) {
|
||||
const groupProject = state.projects.find((item) => item.id === execution.groupProjectId);
|
||||
if (!groupProject) {
|
||||
throw new Error("DISPATCH_EXECUTION_GROUP_PROJECT_NOT_FOUND");
|
||||
}
|
||||
const target = plan.targets.find(
|
||||
(item) =>
|
||||
item.projectId === execution.targetProjectId &&
|
||||
item.threadId === execution.targetThreadId &&
|
||||
item.deviceId === execution.deviceId,
|
||||
);
|
||||
if (!target) {
|
||||
throw new Error("DISPATCH_EXECUTION_TARGET_NOT_FOUND");
|
||||
}
|
||||
|
||||
const existing = state.masterAgentTasks.find(
|
||||
(task) =>
|
||||
task.taskType === "dispatch_execution" &&
|
||||
(task.dispatchExecutionId === execution.executionId ||
|
||||
(task.projectId === execution.groupProjectId &&
|
||||
task.requestMessageId === plan.planId &&
|
||||
task.targetProjectId === execution.targetProjectId &&
|
||||
task.targetThreadId === execution.targetThreadId)),
|
||||
);
|
||||
if (existing) {
|
||||
existing.dispatchExecutionId = existing.dispatchExecutionId ?? execution.executionId;
|
||||
existing.targetProjectId = existing.targetProjectId ?? execution.targetProjectId;
|
||||
existing.targetThreadId = existing.targetThreadId ?? execution.targetThreadId;
|
||||
existing.targetThreadDisplayName = existing.targetThreadDisplayName ?? target.threadDisplayName;
|
||||
existing.executionPrompt =
|
||||
existing.executionPrompt ||
|
||||
buildDispatchExecutionPrompt({
|
||||
groupProject,
|
||||
plan,
|
||||
target,
|
||||
});
|
||||
return existing;
|
||||
}
|
||||
|
||||
const requestedBy = plan.confirmedBy ?? plan.requestedBy;
|
||||
const requestMessage = groupProject.messages.find((message) => message.id === plan.requestMessageId);
|
||||
const task: MasterAgentTask = {
|
||||
taskId: randomToken("mastertask"),
|
||||
projectId: execution.groupProjectId,
|
||||
taskType: "dispatch_execution",
|
||||
requestMessageId: plan.planId,
|
||||
requestText: requestMessage?.body ?? plan.summary,
|
||||
executionPrompt: buildDispatchExecutionPrompt({
|
||||
groupProject,
|
||||
plan,
|
||||
target,
|
||||
}),
|
||||
requestedBy,
|
||||
requestedByAccount: requestedBy,
|
||||
deviceId: execution.deviceId,
|
||||
dispatchExecutionId: execution.executionId,
|
||||
targetProjectId: execution.targetProjectId,
|
||||
targetThreadId: execution.targetThreadId,
|
||||
targetThreadDisplayName: target.threadDisplayName,
|
||||
status: "queued",
|
||||
requestedAt: nowIso(),
|
||||
};
|
||||
state.masterAgentTasks.unshift(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
function ensureDispatchExecutionTasksInState(
|
||||
state: BossState,
|
||||
plan: DispatchPlan,
|
||||
executions: DispatchExecution[],
|
||||
) {
|
||||
return executions.map((execution) => ensureDispatchExecutionTaskInState(state, plan, execution));
|
||||
}
|
||||
|
||||
export async function confirmDispatchPlanAndCreateExecutions(input: {
|
||||
groupProjectId: string;
|
||||
planId: string;
|
||||
@@ -4055,6 +4380,8 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
|
||||
});
|
||||
}
|
||||
|
||||
ensureDispatchExecutionTasksInState(state, plan, executions);
|
||||
|
||||
return {
|
||||
plan: { ...plan },
|
||||
executions: executions.map((execution) => ({ ...execution })),
|
||||
@@ -4109,6 +4436,115 @@ export async function completeDispatchExecution(payload: {
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeDispatchExecutionReply(rawThreadReply: string, threadTitle: string) {
|
||||
const compact = rawThreadReply.trim().replace(/\s+/g, " ");
|
||||
if (!compact) {
|
||||
return `主 Agent 汇总:${threadTitle} 已返回执行结果。`;
|
||||
}
|
||||
if (compact.length <= 72) {
|
||||
return `主 Agent 汇总:${threadTitle} 已返回执行结果:${compact}`;
|
||||
}
|
||||
return `主 Agent 汇总:${threadTitle} 已返回执行结果:${compact.slice(0, 69)}...`;
|
||||
}
|
||||
|
||||
function appendDispatchExecutionResultInState(
|
||||
state: BossState,
|
||||
payload: {
|
||||
dispatchExecutionId: string;
|
||||
completedByDeviceId: string;
|
||||
status: "completed" | "failed";
|
||||
groupProjectId: string;
|
||||
targetProjectId: string;
|
||||
targetThreadId: string;
|
||||
targetThreadDisplayName?: string;
|
||||
rawThreadReply?: string;
|
||||
masterSummary?: string;
|
||||
},
|
||||
) {
|
||||
const execution = state.dispatchExecutions.find(
|
||||
(item) => item.executionId === payload.dispatchExecutionId,
|
||||
);
|
||||
if (!execution) throw new Error("DISPATCH_EXECUTION_NOT_FOUND");
|
||||
if (execution.groupProjectId !== payload.groupProjectId) {
|
||||
throw new Error("DISPATCH_EXECUTION_GROUP_PROJECT_MISMATCH");
|
||||
}
|
||||
if (execution.targetProjectId !== payload.targetProjectId) {
|
||||
throw new Error("DISPATCH_EXECUTION_TARGET_PROJECT_MISMATCH");
|
||||
}
|
||||
if (execution.targetThreadId !== payload.targetThreadId) {
|
||||
throw new Error("DISPATCH_EXECUTION_TARGET_THREAD_MISMATCH");
|
||||
}
|
||||
if (execution.deviceId !== payload.completedByDeviceId) {
|
||||
throw new Error("DISPATCH_EXECUTION_DEVICE_MISMATCH");
|
||||
}
|
||||
|
||||
const device = state.devices.find((item) => item.id === payload.completedByDeviceId);
|
||||
const threadTitle =
|
||||
payload.targetThreadDisplayName?.trim() ||
|
||||
state.projects.find((item) => item.id === payload.targetProjectId)?.threadMeta.threadDisplayName ||
|
||||
payload.targetThreadId;
|
||||
|
||||
let mirroredResult: Message | null = null;
|
||||
let masterSummary: Message | null = null;
|
||||
|
||||
if (payload.status === "completed") {
|
||||
if (!payload.rawThreadReply?.trim()) {
|
||||
throw new Error("DISPATCH_EXECUTION_RAW_REPLY_REQUIRED");
|
||||
}
|
||||
mirroredResult = pushProjectLedgerMessage(state, payload.groupProjectId, {
|
||||
sender: "device",
|
||||
senderLabel: `${threadTitle} · ${device?.name ?? payload.completedByDeviceId}`,
|
||||
body: payload.rawThreadReply.trim(),
|
||||
kind: "text",
|
||||
});
|
||||
masterSummary = pushProjectLedgerMessage(state, payload.groupProjectId, {
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body:
|
||||
payload.masterSummary?.trim() ||
|
||||
summarizeDispatchExecutionReply(payload.rawThreadReply, threadTitle),
|
||||
kind: "text",
|
||||
});
|
||||
} else {
|
||||
masterSummary = pushProjectLedgerMessage(state, payload.groupProjectId, {
|
||||
sender: "ops",
|
||||
senderLabel: "主 Agent Relay",
|
||||
body: `${threadTitle} 执行失败,请稍后重试。`,
|
||||
kind: "text",
|
||||
});
|
||||
}
|
||||
|
||||
execution.status = payload.status;
|
||||
execution.completedAt = nowIso();
|
||||
execution.completedByDeviceId = payload.completedByDeviceId;
|
||||
execution.resultMessageId = mirroredResult?.id ?? execution.resultMessageId;
|
||||
|
||||
return {
|
||||
execution: { ...execution },
|
||||
mirroredResult,
|
||||
masterSummary,
|
||||
};
|
||||
}
|
||||
|
||||
export async function appendDispatchExecutionResult(payload: {
|
||||
dispatchExecutionId: string;
|
||||
completedByDeviceId: string;
|
||||
status: "completed" | "failed";
|
||||
groupProjectId: string;
|
||||
targetProjectId: string;
|
||||
targetThreadId: string;
|
||||
targetThreadDisplayName?: string;
|
||||
rawThreadReply?: string;
|
||||
masterSummary?: string;
|
||||
}) {
|
||||
const result = await mutateState((state) =>
|
||||
appendDispatchExecutionResultInState(state, payload),
|
||||
);
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.groupProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.groupProjectId });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMasterAgentTask(taskId: string) {
|
||||
const state = await readState();
|
||||
return state.masterAgentTasks.find((item) => item.taskId === taskId) ?? null;
|
||||
@@ -4116,6 +4552,7 @@ export async function getMasterAgentTask(taskId: string) {
|
||||
|
||||
export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
let attachmentProjectId: string | undefined;
|
||||
let dispatchExecutionProjectId: string | undefined;
|
||||
const task = await mutateState((state) => {
|
||||
const next = state.masterAgentTasks.find(
|
||||
(item) => item.deviceId === deviceId && item.status === "queued",
|
||||
@@ -4133,6 +4570,15 @@ export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
attachmentProjectId = next.projectId;
|
||||
}
|
||||
}
|
||||
if (next.taskType === "dispatch_execution" && next.dispatchExecutionId) {
|
||||
const execution = state.dispatchExecutions.find(
|
||||
(item) => item.executionId === next.dispatchExecutionId,
|
||||
);
|
||||
if (execution && execution.status === "queued") {
|
||||
execution.status = "running";
|
||||
dispatchExecutionProjectId = execution.groupProjectId;
|
||||
}
|
||||
}
|
||||
return { ...next };
|
||||
});
|
||||
if (task) {
|
||||
@@ -4145,6 +4591,9 @@ export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
publishBossEvent("project.messages.updated", { projectId: attachmentProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: attachmentProjectId });
|
||||
}
|
||||
if (dispatchExecutionProjectId) {
|
||||
publishBossEvent("conversation.updated", { projectId: dispatchExecutionProjectId });
|
||||
}
|
||||
}
|
||||
return task;
|
||||
}
|
||||
@@ -4156,6 +4605,10 @@ export async function completeMasterAgentTask(payload: {
|
||||
replyBody?: string;
|
||||
errorMessage?: string;
|
||||
requestId?: string;
|
||||
dispatchExecutionId?: string;
|
||||
targetProjectId?: string;
|
||||
targetThreadId?: string;
|
||||
rawThreadReply?: string;
|
||||
dispatchPlan?: {
|
||||
summary?: string;
|
||||
targets: DispatchPlanTarget[];
|
||||
@@ -4197,6 +4650,9 @@ export async function completeMasterAgentTask(payload: {
|
||||
|
||||
let attachmentProjectId: string | undefined;
|
||||
let createdDispatchPlan: DispatchPlan | undefined;
|
||||
let dispatchExecutionResult:
|
||||
| ReturnType<typeof appendDispatchExecutionResultInState>
|
||||
| undefined;
|
||||
if (task.taskType === "attachment_analysis" && task.attachmentId) {
|
||||
const project = state.projects.find((item) => item.id === task.projectId);
|
||||
const match = project ? findProjectAttachment(project, task.attachmentId) : null;
|
||||
@@ -4250,6 +4706,21 @@ export async function completeMasterAgentTask(payload: {
|
||||
targets: payload.dispatchPlan.targets,
|
||||
});
|
||||
}
|
||||
} else if (task.taskType === "dispatch_execution") {
|
||||
if (!task.dispatchExecutionId || !task.targetProjectId || !task.targetThreadId) {
|
||||
throw new Error("MASTER_AGENT_DISPATCH_EXECUTION_CONTEXT_REQUIRED");
|
||||
}
|
||||
dispatchExecutionResult = appendDispatchExecutionResultInState(state, {
|
||||
dispatchExecutionId: payload.dispatchExecutionId?.trim() || task.dispatchExecutionId,
|
||||
completedByDeviceId: payload.deviceId,
|
||||
status: payload.status,
|
||||
groupProjectId: task.projectId,
|
||||
targetProjectId: payload.targetProjectId?.trim() || task.targetProjectId,
|
||||
targetThreadId: payload.targetThreadId?.trim() || task.targetThreadId,
|
||||
targetThreadDisplayName: task.targetThreadDisplayName,
|
||||
rawThreadReply: payload.rawThreadReply?.trim() || task.replyBody,
|
||||
masterSummary: payload.replyBody?.trim(),
|
||||
});
|
||||
} else if (!attachmentProjectId && payload.status === "completed" && task.replyBody) {
|
||||
pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "master",
|
||||
@@ -4269,6 +4740,7 @@ export async function completeMasterAgentTask(payload: {
|
||||
return {
|
||||
...task,
|
||||
dispatchPlan: createdDispatchPlan ? { ...createdDispatchPlan } : undefined,
|
||||
dispatchExecution: dispatchExecutionResult?.execution,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4724,6 +5196,51 @@ export async function verifyDeviceToken(deviceId: string, token?: string) {
|
||||
return hasAuthorizedDeviceToken(state, deviceId, token);
|
||||
}
|
||||
|
||||
function upsertDeviceImportDraftFromHeartbeat(
|
||||
state: BossState,
|
||||
payload: {
|
||||
deviceId: string;
|
||||
enrollmentId?: string;
|
||||
candidates: DeviceImportCandidate[];
|
||||
},
|
||||
) {
|
||||
if (payload.candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = state.deviceImportDrafts.find((item) => item.deviceId === payload.deviceId);
|
||||
const selectedCandidateIds = dedupeStrings(
|
||||
(existing?.selectedCandidateIds ?? []).filter((candidateId) =>
|
||||
payload.candidates.some((candidate) => candidate.candidateId === candidateId),
|
||||
),
|
||||
);
|
||||
|
||||
const nextDraft = normalizeDeviceImportDraft({
|
||||
draftId: existing?.draftId ?? randomToken("import-draft"),
|
||||
deviceId: payload.deviceId,
|
||||
enrollmentId: payload.enrollmentId ?? existing?.enrollmentId,
|
||||
status:
|
||||
selectedCandidateIds.length > 0
|
||||
? existing?.resolutionId
|
||||
? "resolved"
|
||||
: "pending_resolution"
|
||||
: "pending_selection",
|
||||
candidates: payload.candidates,
|
||||
selectedCandidateIds,
|
||||
createdAt: existing?.createdAt ?? nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
reviewedAt: existing?.reviewedAt,
|
||||
reviewedBy: existing?.reviewedBy,
|
||||
resolutionId: existing?.resolutionId,
|
||||
}, existing);
|
||||
|
||||
state.deviceImportDrafts = [
|
||||
nextDraft,
|
||||
...state.deviceImportDrafts.filter((item) => item.draftId !== nextDraft.draftId),
|
||||
];
|
||||
return nextDraft;
|
||||
}
|
||||
|
||||
export async function upsertDeviceHeartbeat(payload: {
|
||||
deviceId: string;
|
||||
token?: string;
|
||||
@@ -4736,6 +5253,16 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
quota7d: number;
|
||||
projects: string[];
|
||||
endpoint?: string;
|
||||
projectCandidates?: Array<{
|
||||
folderName: string;
|
||||
folderRef?: string;
|
||||
threadId: string;
|
||||
threadDisplayName: string;
|
||||
codexFolderRef?: string;
|
||||
codexThreadRef?: string;
|
||||
lastActiveAt?: string;
|
||||
suggestedImport?: boolean;
|
||||
}>;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const claimedEnrollment = claimEnrollment(
|
||||
@@ -4817,10 +5344,30 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCandidates = ensureArray(payload.projectCandidates, []).map((candidate) =>
|
||||
normalizeDeviceImportCandidate({
|
||||
deviceId: payload.deviceId,
|
||||
folderName: candidate.folderName,
|
||||
folderRef: candidate.folderRef,
|
||||
threadId: candidate.threadId,
|
||||
threadDisplayName: candidate.threadDisplayName,
|
||||
codexFolderRef: candidate.codexFolderRef,
|
||||
codexThreadRef: candidate.codexThreadRef,
|
||||
lastActiveAt: candidate.lastActiveAt ?? nowIso(),
|
||||
suggestedImport: candidate.suggestedImport ?? true,
|
||||
}),
|
||||
);
|
||||
const draft = upsertDeviceImportDraftFromHeartbeat(state, {
|
||||
deviceId: payload.deviceId,
|
||||
enrollmentId: claimedEnrollment?.enrollmentId,
|
||||
candidates: normalizedCandidates,
|
||||
});
|
||||
|
||||
return {
|
||||
device,
|
||||
token: claimedEnrollment?.token ?? device.token,
|
||||
pairingStatus: claimedEnrollment?.status,
|
||||
importDraft: draft,
|
||||
};
|
||||
});
|
||||
publishBossEvent("devices.updated", { deviceId: payload.deviceId });
|
||||
@@ -4828,6 +5375,269 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveDeviceImportAction(
|
||||
state: BossState,
|
||||
deviceId: string,
|
||||
candidate: DeviceImportCandidate,
|
||||
): DeviceImportResolutionItem {
|
||||
const directMatch = state.projects.find(
|
||||
(project) =>
|
||||
!project.isGroup &&
|
||||
((candidate.codexThreadRef && project.threadMeta.codexThreadRef === candidate.codexThreadRef) ||
|
||||
project.threadMeta.threadId === candidate.threadId),
|
||||
);
|
||||
if (directMatch) {
|
||||
return {
|
||||
candidateId: candidate.candidateId,
|
||||
action: "attach_existing",
|
||||
threadDisplayName: candidate.threadDisplayName,
|
||||
folderName: candidate.folderName,
|
||||
targetProjectId: directMatch.id,
|
||||
reason: `已匹配到现有会话《${directMatch.name}》,直接补充设备与线程映射。`,
|
||||
};
|
||||
}
|
||||
|
||||
const similarByFolder = state.projects.find(
|
||||
(project) =>
|
||||
!project.isGroup &&
|
||||
project.deviceIds.includes(deviceId) &&
|
||||
project.threadMeta.folderName === candidate.folderName &&
|
||||
project.threadMeta.threadDisplayName === candidate.threadDisplayName,
|
||||
);
|
||||
if (similarByFolder) {
|
||||
return {
|
||||
candidateId: candidate.candidateId,
|
||||
action: "attach_existing",
|
||||
threadDisplayName: candidate.threadDisplayName,
|
||||
folderName: candidate.folderName,
|
||||
targetProjectId: similarByFolder.id,
|
||||
reason: `同设备下已有同名线程《${similarByFolder.name}》,避免重复导入。`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
candidateId: candidate.candidateId,
|
||||
action: "create_thread_conversation",
|
||||
threadDisplayName: candidate.threadDisplayName,
|
||||
folderName: candidate.folderName,
|
||||
reason: `建议把 ${candidate.threadDisplayName} 作为独立聊天窗口导入。`,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeDeviceImportResolution(
|
||||
deviceName: string,
|
||||
items: DeviceImportResolutionItem[],
|
||||
) {
|
||||
const createCount = items.filter((item) => item.action === "create_thread_conversation").length;
|
||||
const attachCount = items.filter((item) => item.action === "attach_existing").length;
|
||||
const skipCount = items.filter((item) => item.action === "skip").length;
|
||||
return `${deviceName} 导入建议:新建 ${createCount} 个会话,关联 ${attachCount} 个现有会话${skipCount > 0 ? `,跳过 ${skipCount} 项` : ""}。`;
|
||||
}
|
||||
|
||||
export async function getLatestDeviceImportDraft(deviceId: string) {
|
||||
const state = await readState();
|
||||
const draft = state.deviceImportDrafts.find((item) => item.deviceId === deviceId) ?? null;
|
||||
const resolution = draft?.resolutionId
|
||||
? state.deviceImportResolutions.find((item) => item.resolutionId === draft.resolutionId) ?? null
|
||||
: state.deviceImportResolutions.find((item) => item.deviceId === deviceId) ?? null;
|
||||
return { draft, resolution };
|
||||
}
|
||||
|
||||
export async function selectDeviceImportCandidates(input: {
|
||||
deviceId: string;
|
||||
selectedCandidateIds: string[];
|
||||
selectedBy: string;
|
||||
}) {
|
||||
const draft = await mutateState((state) => {
|
||||
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);
|
||||
if (!draft) throw new Error("DEVICE_IMPORT_DRAFT_NOT_FOUND");
|
||||
const availableCandidateIds = new Set(draft.candidates.map((item) => item.candidateId));
|
||||
const nextSelected = dedupeStrings(input.selectedCandidateIds).filter((candidateId) =>
|
||||
availableCandidateIds.has(candidateId),
|
||||
);
|
||||
if (nextSelected.length === 0) {
|
||||
throw new Error("DEVICE_IMPORT_SELECTION_REQUIRED");
|
||||
}
|
||||
draft.selectedCandidateIds = nextSelected;
|
||||
draft.status = "pending_resolution";
|
||||
draft.updatedAt = nowIso();
|
||||
draft.reviewedBy = input.selectedBy;
|
||||
draft.reviewedAt = undefined;
|
||||
draft.resolutionId = undefined;
|
||||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||||
(item) => item.draftId !== draft.draftId,
|
||||
);
|
||||
return { ...draft };
|
||||
});
|
||||
publishBossEvent("devices.updated", { deviceId: input.deviceId });
|
||||
return draft;
|
||||
}
|
||||
|
||||
export async function resolveDeviceImportDraft(input: {
|
||||
deviceId: string;
|
||||
reviewedBy: string;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);
|
||||
if (!draft) throw new Error("DEVICE_IMPORT_DRAFT_NOT_FOUND");
|
||||
if (draft.selectedCandidateIds.length === 0) {
|
||||
throw new Error("DEVICE_IMPORT_SELECTION_REQUIRED");
|
||||
}
|
||||
const device = state.devices.find((item) => item.id === input.deviceId);
|
||||
if (!device) throw new Error("DEVICE_NOT_FOUND");
|
||||
|
||||
const selectedCandidates = draft.candidates.filter((candidate) =>
|
||||
draft.selectedCandidateIds.includes(candidate.candidateId),
|
||||
);
|
||||
const items = selectedCandidates.map((candidate) =>
|
||||
resolveDeviceImportAction(state, input.deviceId, candidate),
|
||||
);
|
||||
const resolution = normalizeDeviceImportResolution({
|
||||
resolutionId: randomToken("import-resolution"),
|
||||
draftId: draft.draftId,
|
||||
deviceId: input.deviceId,
|
||||
status: "ready",
|
||||
summary: summarizeDeviceImportResolution(device.name, items),
|
||||
items,
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
|
||||
draft.status = "resolved";
|
||||
draft.updatedAt = nowIso();
|
||||
draft.reviewedAt = nowIso();
|
||||
draft.reviewedBy = input.reviewedBy;
|
||||
draft.resolutionId = resolution.resolutionId;
|
||||
|
||||
state.deviceImportResolutions = [
|
||||
resolution,
|
||||
...state.deviceImportResolutions.filter((item) => item.draftId !== draft.draftId),
|
||||
];
|
||||
|
||||
return { draft: { ...draft }, resolution };
|
||||
});
|
||||
publishBossEvent("devices.updated", { deviceId: input.deviceId });
|
||||
publishBossEvent("conversation.updated", { deviceId: input.deviceId });
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildImportedThreadProject(device: Device, candidate: DeviceImportCandidate) {
|
||||
const projectId =
|
||||
candidate.codexThreadRef?.trim() && candidate.codexFolderRef?.trim()
|
||||
? slugify(`${device.id}-${candidate.codexFolderRef}-${candidate.codexThreadRef}`)
|
||||
: slugify(`${device.id}-${candidate.folderName}-${candidate.threadId}`);
|
||||
const now = nowIso();
|
||||
return normalizeProject({
|
||||
id: projectId,
|
||||
name: candidate.threadDisplayName,
|
||||
pinned: false,
|
||||
systemPinned: false,
|
||||
deviceIds: [device.id],
|
||||
preview: `已从 ${device.name} 导入线程 ${candidate.threadDisplayName}`,
|
||||
updatedAt: now,
|
||||
lastMessageAt: now,
|
||||
isGroup: false,
|
||||
unreadCount: 0,
|
||||
riskLevel: "low",
|
||||
threadMeta: {
|
||||
projectId,
|
||||
threadId: candidate.threadId,
|
||||
threadDisplayName: candidate.threadDisplayName,
|
||||
folderName: candidate.folderName,
|
||||
activityIconCount: 1,
|
||||
updatedAt: candidate.lastActiveAt || now,
|
||||
codexFolderRef: candidate.codexFolderRef ?? candidate.folderRef,
|
||||
codexThreadRef: candidate.codexThreadRef,
|
||||
},
|
||||
groupMembers: [],
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
messages: [
|
||||
{
|
||||
id: randomToken("msg"),
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: `已从设备 ${device.name} 导入线程《${candidate.threadDisplayName}》。`,
|
||||
sentAt: now,
|
||||
kind: "text",
|
||||
},
|
||||
],
|
||||
goals: [],
|
||||
versions: [],
|
||||
});
|
||||
}
|
||||
|
||||
export async function applyDeviceImportResolution(input: {
|
||||
deviceId: string;
|
||||
appliedBy: string;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);
|
||||
if (!draft || !draft.resolutionId) throw new Error("DEVICE_IMPORT_RESOLUTION_NOT_FOUND");
|
||||
const resolution = state.deviceImportResolutions.find(
|
||||
(item) => item.resolutionId === draft.resolutionId,
|
||||
);
|
||||
if (!resolution) throw new Error("DEVICE_IMPORT_RESOLUTION_NOT_FOUND");
|
||||
const device = state.devices.find((item) => item.id === input.deviceId);
|
||||
if (!device) throw new Error("DEVICE_NOT_FOUND");
|
||||
|
||||
const importedProjects: Project[] = [];
|
||||
for (const item of resolution.items) {
|
||||
const candidate = draft.candidates.find((entry) => entry.candidateId === item.candidateId);
|
||||
if (!candidate || item.action === "skip") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let targetProject = item.targetProjectId
|
||||
? state.projects.find((project) => project.id === item.targetProjectId)
|
||||
: undefined;
|
||||
if (item.action === "create_thread_conversation" && !targetProject) {
|
||||
targetProject = buildImportedThreadProject(device, candidate);
|
||||
state.projects.unshift(targetProject);
|
||||
} else if (item.action === "attach_existing" && !targetProject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!targetProject) continue;
|
||||
if (!targetProject.deviceIds.includes(device.id)) {
|
||||
targetProject.deviceIds.push(device.id);
|
||||
}
|
||||
targetProject.threadMeta.threadDisplayName = candidate.threadDisplayName;
|
||||
targetProject.threadMeta.folderName = candidate.folderName;
|
||||
targetProject.threadMeta.threadId = candidate.threadId;
|
||||
targetProject.threadMeta.codexFolderRef = candidate.codexFolderRef ?? candidate.folderRef;
|
||||
targetProject.threadMeta.codexThreadRef = candidate.codexThreadRef;
|
||||
targetProject.threadMeta.updatedAt = candidate.lastActiveAt;
|
||||
targetProject.preview = `已导入 ${candidate.threadDisplayName}`;
|
||||
targetProject.updatedAt = nowIso();
|
||||
targetProject.lastMessageAt = targetProject.updatedAt;
|
||||
importedProjects.push({ ...targetProject });
|
||||
}
|
||||
|
||||
device.projects = dedupeStrings(
|
||||
draft.candidates
|
||||
.filter((candidate) => draft.selectedCandidateIds.includes(candidate.candidateId))
|
||||
.map((candidate) => candidate.folderName),
|
||||
);
|
||||
|
||||
resolution.status = "applied";
|
||||
resolution.appliedAt = nowIso();
|
||||
resolution.appliedBy = input.appliedBy;
|
||||
draft.status = "applied";
|
||||
draft.updatedAt = nowIso();
|
||||
|
||||
return {
|
||||
draft: { ...draft },
|
||||
resolution: { ...resolution },
|
||||
importedProjects,
|
||||
};
|
||||
});
|
||||
|
||||
publishBossEvent("devices.updated", { deviceId: input.deviceId });
|
||||
publishBossEvent("conversation.updated", { deviceId: input.deviceId });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function upsertDeviceSkills(payload: {
|
||||
deviceId: string;
|
||||
skills: Array<{
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
ContextBudgetLevel,
|
||||
Device,
|
||||
DeviceEnrollment,
|
||||
DeviceImportDraft,
|
||||
DeviceImportResolution,
|
||||
DeviceSkill,
|
||||
MasterIdentitySummary,
|
||||
OpsFault,
|
||||
@@ -94,6 +96,8 @@ export interface DeviceWorkspaceView {
|
||||
selectedDevice?: Device;
|
||||
relatedThreads: ThreadContextSnapshot[];
|
||||
activeEnrollment?: DeviceEnrollment;
|
||||
importDraft?: DeviceImportDraft;
|
||||
importResolution?: DeviceImportResolution;
|
||||
}
|
||||
|
||||
export interface OpsSummaryView {
|
||||
@@ -453,6 +457,8 @@ export function getDeviceWorkspaceView(
|
||||
selectedDevice: state.devices.find((item) => item.id === deviceId),
|
||||
relatedThreads: state.threadContextSnapshots.filter((item) => item.nodeId === deviceId),
|
||||
activeEnrollment: state.deviceEnrollments.find((item) => item.deviceId === deviceId),
|
||||
importDraft: state.deviceImportDrafts.find((item) => item.deviceId === deviceId),
|
||||
importResolution: state.deviceImportResolutions.find((item) => item.deviceId === deviceId),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user