720 lines
23 KiB
TypeScript
720 lines
23 KiB
TypeScript
import type {
|
|
AiAccountRole,
|
|
AiProvider,
|
|
AiAccountStatus,
|
|
AppLogEntry,
|
|
AuditTaskRequest,
|
|
AuditTaskResult,
|
|
BossState,
|
|
Capability,
|
|
ContextBudgetLevel,
|
|
Device,
|
|
DeviceEnrollment,
|
|
DeviceImportDraft,
|
|
DeviceImportResolution,
|
|
DeviceSkill,
|
|
MasterIdentitySummary,
|
|
MasterAgentMemory,
|
|
MasterAgentPromptPolicy,
|
|
OpsFault,
|
|
OpsRepairTicket,
|
|
OpsRepairVerification,
|
|
Project,
|
|
ProjectAgentControls,
|
|
RiskLevel,
|
|
ThreadContextAlert,
|
|
ThreadContextSnapshot,
|
|
ThreadHandoffPackage,
|
|
UserMasterPrompt,
|
|
} from "@/lib/boss-data";
|
|
|
|
export interface ContextIndicator {
|
|
visible: boolean;
|
|
style: "ring_percent";
|
|
percent?: number;
|
|
level?: ContextBudgetLevel;
|
|
}
|
|
|
|
export interface ConversationItem {
|
|
conversationId: string;
|
|
conversationType: "master_agent" | "single_device" | "group" | "folder_archive";
|
|
projectId: string;
|
|
projectTitle: string;
|
|
threadTitle: string;
|
|
folderLabel: string;
|
|
folderKey?: string;
|
|
threadCount?: number;
|
|
preview: string;
|
|
lastMessagePreview: string;
|
|
activityIconCount: number;
|
|
topPinnedLabel?: "置顶";
|
|
manualPinned: boolean;
|
|
latestReplyAt: string;
|
|
latestReplyLabel: string;
|
|
unreadCount: number;
|
|
riskLevel: RiskLevel;
|
|
activeDeviceCount: number;
|
|
deviceNamesPreview: string[];
|
|
avatar: {
|
|
primary: string;
|
|
secondary?: string;
|
|
overflowCount?: number;
|
|
};
|
|
groupMembers?: Array<{
|
|
threadId: string;
|
|
avatar: string;
|
|
title: string;
|
|
}>;
|
|
contextBudgetIndicator: ContextIndicator;
|
|
contextBudgetSourceNodeId?: string;
|
|
contextBudgetUpdatedAt?: string;
|
|
mustFinishBeforeCompaction: boolean;
|
|
}
|
|
|
|
export interface ThreadContextView {
|
|
snapshot: ThreadContextSnapshot;
|
|
handoffPackage?: ThreadHandoffPackage;
|
|
alerts: ThreadContextAlert[];
|
|
}
|
|
|
|
export interface ProjectDetailView {
|
|
project: Project;
|
|
agentControls?: ProjectAgentControls | null;
|
|
devices: Device[];
|
|
masterIdentity?: MasterIdentitySummary;
|
|
activeThreadContexts: ThreadContextView[];
|
|
nextCompactionRiskThreadId?: string;
|
|
threadsRequiringHandoff: ThreadContextView[];
|
|
masterContextStrategySummary: string;
|
|
recentAppLogs: AppLogEntry[];
|
|
openFaults: OpsFault[];
|
|
relatedAuditResults: AuditTaskResult[];
|
|
}
|
|
|
|
export interface ThreadContextDetailView {
|
|
snapshot: ThreadContextSnapshot;
|
|
handoffPackage?: ThreadHandoffPackage;
|
|
alerts: ThreadContextAlert[];
|
|
currentChecklist: string[];
|
|
masterActions: string[];
|
|
}
|
|
|
|
export interface DeviceWorkspaceView {
|
|
selectedDevice?: Device;
|
|
relatedThreads: ThreadContextSnapshot[];
|
|
activeEnrollment?: DeviceEnrollment;
|
|
importDraft?: DeviceImportDraft;
|
|
importResolution?: DeviceImportResolution;
|
|
}
|
|
|
|
export interface OpsSummaryView {
|
|
mode: "active" | "idle";
|
|
faults: OpsFault[];
|
|
tickets: Array<
|
|
OpsRepairTicket & {
|
|
verification?: OpsRepairVerification;
|
|
}
|
|
>;
|
|
}
|
|
|
|
export interface AuditSummaryView {
|
|
pendingRequests: AuditTaskRequest[];
|
|
latestResults: AuditTaskResult[];
|
|
capabilities: Capability[];
|
|
}
|
|
|
|
export interface SkillInventoryDeviceGroup {
|
|
device: Device;
|
|
skills: DeviceSkill[];
|
|
}
|
|
|
|
export interface SkillInventoryView {
|
|
boundDeviceId?: string;
|
|
groups: SkillInventoryDeviceGroup[];
|
|
}
|
|
|
|
const levelPriority: Record<ContextBudgetLevel, number> = {
|
|
critical: 0,
|
|
urgent: 1,
|
|
watch: 2,
|
|
safe: 3,
|
|
};
|
|
|
|
const aiRolePriority: Record<AiAccountRole, number> = {
|
|
primary: 0,
|
|
backup: 1,
|
|
api_fallback: 2,
|
|
};
|
|
|
|
const shanghaiFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
timeZone: "Asia/Shanghai",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
});
|
|
|
|
const shanghaiDayFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
timeZone: "Asia/Shanghai",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
});
|
|
|
|
export function formatTimestampLabel(value?: string, fallback = "刚刚") {
|
|
if (!value) return fallback;
|
|
if (!value.includes("T")) return value;
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
|
|
const diff = Date.now() - date.getTime();
|
|
if (Math.abs(diff) < 60_000) return "刚刚";
|
|
if (diff >= 0 && diff < 24 * 60 * 60_000) {
|
|
return shanghaiFormatter.format(date);
|
|
}
|
|
return shanghaiDayFormatter.format(date);
|
|
}
|
|
|
|
function compareSnapshots(a: ThreadContextSnapshot, b: ThreadContextSnapshot) {
|
|
if (a.mustFinishBeforeCompaction !== b.mustFinishBeforeCompaction) {
|
|
return a.mustFinishBeforeCompaction ? -1 : 1;
|
|
}
|
|
if (levelPriority[a.contextBudgetLevel] !== levelPriority[b.contextBudgetLevel]) {
|
|
return levelPriority[a.contextBudgetLevel] - levelPriority[b.contextBudgetLevel];
|
|
}
|
|
return (a.compactionExpectedAt ?? "").localeCompare(b.compactionExpectedAt ?? "");
|
|
}
|
|
|
|
function projectType(project: Project): ConversationItem["conversationType"] {
|
|
if (project.id === "master-agent") return "master_agent";
|
|
return project.isGroup ? "group" : "single_device";
|
|
}
|
|
|
|
function buildFolderKey(project: Project) {
|
|
if (project.id === "master-agent" || project.isGroup) return undefined;
|
|
const deviceId = project.deviceIds[0];
|
|
const folderRef = project.threadMeta.codexFolderRef?.trim() || project.threadMeta.folderName.trim();
|
|
if (!deviceId || !folderRef) return undefined;
|
|
return `${deviceId}:${folderRef}`;
|
|
}
|
|
|
|
function isTopPinnedConversation(project: Project) {
|
|
return Boolean(project.pinned || project.systemPinned || project.id === "audit-collab");
|
|
}
|
|
|
|
function getThreadAvatarFallback(title: string) {
|
|
const trimmed = title.trim();
|
|
if (!trimmed) return "A";
|
|
return trimmed.slice(0, 1).toUpperCase();
|
|
}
|
|
|
|
function getGroupMemberAvatar(member: Project["groupMembers"][number], device?: Device) {
|
|
const avatar = device?.avatar?.trim();
|
|
if (avatar) return avatar;
|
|
return getThreadAvatarFallback(member.threadDisplayName);
|
|
}
|
|
|
|
function aiRoleLabel(role: AiAccountRole) {
|
|
switch (role) {
|
|
case "primary":
|
|
return "主 GPT";
|
|
case "backup":
|
|
return "备用 GPT";
|
|
case "api_fallback":
|
|
return "API 容灾";
|
|
default:
|
|
return role;
|
|
}
|
|
}
|
|
|
|
function aiProviderLabel(provider: AiProvider) {
|
|
switch (provider) {
|
|
case "master_codex_node":
|
|
return "Master Codex Node";
|
|
case "openai_api":
|
|
return "OpenAI API";
|
|
case "aliyun_qwen_api":
|
|
return "阿里百炼 Qwen";
|
|
default:
|
|
return provider;
|
|
}
|
|
}
|
|
|
|
function aiStatusLabel(status: AiAccountStatus) {
|
|
switch (status) {
|
|
case "ready":
|
|
return "可用";
|
|
case "needs_login":
|
|
return "待登录";
|
|
case "needs_api_key":
|
|
return "待配置 Key";
|
|
case "degraded":
|
|
return "异常";
|
|
case "disabled":
|
|
return "已停用";
|
|
default:
|
|
return status;
|
|
}
|
|
}
|
|
|
|
function canGenerateAiAccount(account: BossState["aiAccounts"][number]) {
|
|
if (!account.enabled) return false;
|
|
if (account.provider === "master_codex_node") {
|
|
return Boolean(account.nodeId?.trim());
|
|
}
|
|
return Boolean(account.apiKey?.trim());
|
|
}
|
|
|
|
function getProjectMasterIdentity(state: BossState): MasterIdentitySummary {
|
|
const accounts = [...state.aiAccounts].sort((a, b) => {
|
|
if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
|
|
if (aiRolePriority[a.role] !== aiRolePriority[b.role]) {
|
|
return aiRolePriority[a.role] - aiRolePriority[b.role];
|
|
}
|
|
return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
|
|
});
|
|
const account = accounts.find((item) => item.isActive) ?? accounts.find(canGenerateAiAccount) ?? accounts[0];
|
|
|
|
if (!account) {
|
|
return {
|
|
label: "API 容灾",
|
|
role: "api_fallback",
|
|
roleLabel: aiRoleLabel("api_fallback"),
|
|
provider: "openai_api",
|
|
providerLabel: aiProviderLabel("openai_api"),
|
|
displayName: "未配置 AI 账号",
|
|
status: "needs_api_key",
|
|
statusLabel: aiStatusLabel("needs_api_key"),
|
|
canGenerate: false,
|
|
note: "请到“我的 > AI 账号”补齐主控 AI 账号。",
|
|
};
|
|
}
|
|
|
|
return {
|
|
accountId: account.accountId,
|
|
label: account.label,
|
|
role: account.role,
|
|
roleLabel: aiRoleLabel(account.role),
|
|
provider: account.provider,
|
|
providerLabel: aiProviderLabel(account.provider),
|
|
displayName: account.displayName,
|
|
nodeLabel: account.nodeLabel,
|
|
model: account.model,
|
|
status: account.status,
|
|
statusLabel: aiStatusLabel(account.status),
|
|
canGenerate: canGenerateAiAccount(account),
|
|
switchReason: account.switchReason,
|
|
lastSwitchedAt: account.lastSwitchedAt,
|
|
note: account.loginStatusNote,
|
|
};
|
|
}
|
|
|
|
export function getMasterAgentPromptPolicyView(state: BossState): MasterAgentPromptPolicy | null {
|
|
return state.masterAgentPromptPolicy ?? null;
|
|
}
|
|
|
|
export function getUserMasterPromptView(state: BossState, account: string): UserMasterPrompt | null {
|
|
return state.userMasterPrompts.find((item) => item.account === account) ?? null;
|
|
}
|
|
|
|
export function listUserMasterMemoriesView(
|
|
state: BossState,
|
|
account: string,
|
|
options?: { includeArchived?: boolean },
|
|
): MasterAgentMemory[] {
|
|
const includeArchived = options?.includeArchived ?? false;
|
|
return [...state.masterAgentMemories]
|
|
.filter((memory) => memory.account === account && (includeArchived || !memory.archived))
|
|
.sort((a, b) => {
|
|
const aTime = Date.parse(a.lastUsedAt ?? a.updatedAt ?? a.createdAt) || 0;
|
|
const bTime = Date.parse(b.lastUsedAt ?? b.updatedAt ?? b.createdAt) || 0;
|
|
if (aTime !== bTime) return bTime - aTime;
|
|
return b.memoryId.localeCompare(a.memoryId);
|
|
});
|
|
}
|
|
|
|
function threadViewsForProject(state: BossState, projectId: string) {
|
|
return state.threadContextSnapshots
|
|
.filter((snapshot) => snapshot.projectId === projectId)
|
|
.sort(compareSnapshots)
|
|
.map((snapshot) => ({
|
|
snapshot,
|
|
handoffPackage: state.threadHandoffPackages.find(
|
|
(item) => item.fromThreadId === snapshot.threadId && item.packageStatus !== "expired",
|
|
),
|
|
alerts: state.threadContextAlerts.filter((alert) => alert.threadId === snapshot.threadId),
|
|
}));
|
|
}
|
|
|
|
function buildConversationItem(state: BossState, project: Project): ConversationItem {
|
|
const devices = state.devices.filter((device) => project.deviceIds.includes(device.id));
|
|
const threadViews = threadViewsForProject(state, project.id);
|
|
const topThread = threadViews[0]?.snapshot;
|
|
const threadTitle = project.threadMeta?.threadDisplayName ?? project.name;
|
|
const folderLabel = project.threadMeta?.folderName ?? "";
|
|
const activityIconCount = project.threadMeta?.activityIconCount ?? 1;
|
|
const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined;
|
|
const groupMembers = project.isGroup
|
|
? project.groupMembers.map((member) => ({
|
|
threadId: member.threadId,
|
|
avatar: getGroupMemberAvatar(
|
|
member,
|
|
state.devices.find((device) => device.id === member.deviceId),
|
|
),
|
|
title: member.threadDisplayName,
|
|
}))
|
|
: undefined;
|
|
|
|
return {
|
|
conversationId: `conv-${project.id}`,
|
|
conversationType: projectType(project),
|
|
projectId: project.id,
|
|
projectTitle: project.name,
|
|
threadTitle,
|
|
folderLabel,
|
|
folderKey: buildFolderKey(project),
|
|
preview: project.preview,
|
|
lastMessagePreview: project.preview,
|
|
activityIconCount,
|
|
topPinnedLabel,
|
|
manualPinned: Boolean(project.pinned && !project.systemPinned),
|
|
latestReplyAt: project.lastMessageAt,
|
|
latestReplyLabel: formatTimestampLabel(project.lastMessageAt),
|
|
unreadCount: project.unreadCount,
|
|
riskLevel: project.riskLevel,
|
|
activeDeviceCount: devices.length,
|
|
deviceNamesPreview: devices.map((device) => device.name),
|
|
avatar: {
|
|
primary: devices[0]?.avatar ?? "A",
|
|
secondary: project.isGroup ? devices[1]?.avatar : undefined,
|
|
overflowCount: Math.max(0, devices.length - 2) || undefined,
|
|
},
|
|
groupMembers,
|
|
contextBudgetIndicator: {
|
|
visible: !project.isGroup && Boolean(topThread),
|
|
style: "ring_percent",
|
|
percent: !project.isGroup ? topThread?.contextBudgetRemainingPct : undefined,
|
|
level: !project.isGroup ? topThread?.contextBudgetLevel : undefined,
|
|
},
|
|
contextBudgetSourceNodeId: !project.isGroup ? topThread?.nodeId : undefined,
|
|
contextBudgetUpdatedAt: !project.isGroup ? topThread?.capturedAt : undefined,
|
|
mustFinishBeforeCompaction: Boolean(topThread?.mustFinishBeforeCompaction),
|
|
} satisfies ConversationItem;
|
|
}
|
|
|
|
function sortConversationItems(items: ConversationItem[]) {
|
|
return items.sort((a, b) => {
|
|
if (a.projectId === "master-agent") return -1;
|
|
if (b.projectId === "master-agent") return 1;
|
|
const aPinned = Boolean(a.topPinnedLabel);
|
|
const bPinned = Boolean(b.topPinnedLabel);
|
|
if (aPinned !== bPinned) return aPinned ? -1 : 1;
|
|
return b.latestReplyAt.localeCompare(a.latestReplyAt);
|
|
});
|
|
}
|
|
|
|
export function getConversationItems(state: BossState): ConversationItem[] {
|
|
const conversations = state.projects.map((project) => buildConversationItem(state, project));
|
|
|
|
return sortConversationItems(conversations);
|
|
}
|
|
|
|
export interface ConversationFolderView {
|
|
folderKey: string;
|
|
folderLabel: string;
|
|
deviceId?: string;
|
|
deviceName?: string;
|
|
threadCount: number;
|
|
threads: ConversationItem[];
|
|
}
|
|
|
|
export function getConversationHomeItems(state: BossState): ConversationItem[] {
|
|
const flatItems = getConversationItems(state);
|
|
const projectMap = new Map(state.projects.map((project) => [project.id, project]));
|
|
const grouped = new Map<string, ConversationItem[]>();
|
|
const passthrough: ConversationItem[] = [];
|
|
|
|
for (const item of flatItems) {
|
|
const project = projectMap.get(item.projectId);
|
|
if (!project || item.conversationType !== "single_device") {
|
|
passthrough.push(item);
|
|
continue;
|
|
}
|
|
const folderKey = buildFolderKey(project);
|
|
if (!folderKey) {
|
|
passthrough.push(item);
|
|
continue;
|
|
}
|
|
const bucket = grouped.get(folderKey) ?? [];
|
|
bucket.push(item);
|
|
grouped.set(folderKey, bucket);
|
|
}
|
|
|
|
for (const [folderKey, items] of grouped) {
|
|
if (items.length <= 1) {
|
|
passthrough.push(items[0]);
|
|
continue;
|
|
}
|
|
const latestItem = [...items].sort((a, b) => b.latestReplyAt.localeCompare(a.latestReplyAt))[0];
|
|
const project = projectMap.get(latestItem.projectId);
|
|
const device = project?.deviceIds[0]
|
|
? state.devices.find((entry) => entry.id === project.deviceIds[0])
|
|
: undefined;
|
|
passthrough.push({
|
|
conversationId: `folder-${folderKey}`,
|
|
conversationType: "folder_archive",
|
|
projectId: folderKey,
|
|
projectTitle:
|
|
project?.threadMeta.folderName ??
|
|
(latestItem.folderLabel || latestItem.projectTitle),
|
|
threadTitle:
|
|
project?.threadMeta.folderName ??
|
|
(latestItem.folderLabel || latestItem.threadTitle),
|
|
folderLabel: `${device?.name ?? latestItem.deviceNamesPreview[0] ?? "设备"} · ${items.length} 个线程`,
|
|
folderKey,
|
|
threadCount: items.length,
|
|
preview:
|
|
latestItem.preview || `包含 ${items.length} 个线程,最近活跃:《${latestItem.threadTitle}》`,
|
|
lastMessagePreview:
|
|
latestItem.lastMessagePreview ||
|
|
latestItem.preview ||
|
|
`包含 ${items.length} 个线程,最近活跃:《${latestItem.threadTitle}》`,
|
|
activityIconCount: Math.max(
|
|
1,
|
|
Math.min(
|
|
4,
|
|
items.reduce((sum, entry) => sum + Math.max(1, entry.activityIconCount), 0),
|
|
),
|
|
),
|
|
manualPinned: false,
|
|
topPinnedLabel: undefined,
|
|
latestReplyAt: latestItem.latestReplyAt,
|
|
latestReplyLabel: latestItem.latestReplyLabel,
|
|
unreadCount: items.reduce((sum, entry) => sum + entry.unreadCount, 0),
|
|
riskLevel: items.some((entry) => entry.riskLevel === "high")
|
|
? "high"
|
|
: items.some((entry) => entry.riskLevel === "medium")
|
|
? "medium"
|
|
: "low",
|
|
activeDeviceCount: 1,
|
|
deviceNamesPreview: device ? [device.name] : latestItem.deviceNamesPreview,
|
|
avatar: {
|
|
primary: device?.avatar ?? latestItem.avatar.primary,
|
|
},
|
|
contextBudgetIndicator: {
|
|
visible: false,
|
|
style: "ring_percent",
|
|
},
|
|
mustFinishBeforeCompaction: false,
|
|
});
|
|
}
|
|
|
|
return sortConversationItems(passthrough);
|
|
}
|
|
|
|
export function getConversationFolderView(
|
|
state: BossState,
|
|
folderKey: string,
|
|
): ConversationFolderView | null {
|
|
const flatItems = getConversationItems(state).filter(
|
|
(item) => item.conversationType === "single_device" && item.folderKey === folderKey,
|
|
);
|
|
if (flatItems.length === 0) {
|
|
return null;
|
|
}
|
|
const project = state.projects.find((entry) => buildFolderKey(entry) === folderKey);
|
|
const deviceId = project?.deviceIds[0];
|
|
const device = deviceId ? state.devices.find((entry) => entry.id === deviceId) : undefined;
|
|
return {
|
|
folderKey,
|
|
folderLabel: project?.threadMeta.folderName ?? flatItems[0].folderLabel,
|
|
deviceId,
|
|
deviceName: device?.name,
|
|
threadCount: flatItems.length,
|
|
threads: sortConversationItems(flatItems),
|
|
};
|
|
}
|
|
|
|
function resolveProjectAgentControls(
|
|
state: BossState,
|
|
projectId: string,
|
|
account?: string,
|
|
) {
|
|
if (projectId !== "master-agent") {
|
|
return undefined;
|
|
}
|
|
const normalizedAccount = account?.trim();
|
|
if (normalizedAccount) {
|
|
const scoped = state.userProjectAgentControls.find(
|
|
(item) => item.projectId === projectId && item.account === normalizedAccount,
|
|
);
|
|
if (scoped?.controls) {
|
|
return scoped.controls;
|
|
}
|
|
}
|
|
return state.projects.find((item) => item.id === projectId)?.agentControls ?? null;
|
|
}
|
|
|
|
export function getProjectDetailView(state: BossState, projectId: string, account?: string): ProjectDetailView | null {
|
|
const project = state.projects.find((item) => item.id === projectId);
|
|
if (!project) return null;
|
|
|
|
const activeThreadContexts = threadViewsForProject(state, projectId);
|
|
const threadsRequiringHandoff = activeThreadContexts.filter(
|
|
(item) =>
|
|
item.snapshot.mustFinishBeforeCompaction ||
|
|
item.snapshot.contextBudgetLevel === "urgent" ||
|
|
item.snapshot.contextBudgetLevel === "critical",
|
|
);
|
|
|
|
const openFaults = state.opsFaults.filter(
|
|
(fault) => fault.projectId === projectId && fault.status !== "resolved",
|
|
);
|
|
const relatedAuditResults = state.auditResults.filter((result) =>
|
|
state.auditRequests.some(
|
|
(request) => request.auditRequestId === result.auditRequestId && request.projectId === projectId,
|
|
),
|
|
);
|
|
|
|
const topRisk = threadsRequiringHandoff[0]?.snapshot ?? activeThreadContexts[0]?.snapshot;
|
|
const masterContextStrategySummary = topRisk
|
|
? `${topRisk.title} 需要优先处理,当前 ${topRisk.contextBudgetLevel} ${topRisk.contextBudgetRemainingPct}%${topRisk.mustFinishBeforeCompaction ? ",必须先固化 patch / 测试 / 证据" : ""}。`
|
|
: "当前没有高风险线程,主 Agent 可以继续按正常优先级调度。";
|
|
const projectDeviceIds = new Set(project.deviceIds);
|
|
const recentAppLogs = [...state.appLogs]
|
|
.filter((log) =>
|
|
projectId === "master-agent"
|
|
? true
|
|
: log.projectId === projectId || projectDeviceIds.has(log.deviceId),
|
|
)
|
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
.slice(0, 6);
|
|
|
|
return {
|
|
project,
|
|
agentControls: project.id === "master-agent" ? resolveProjectAgentControls(state, projectId, account) : undefined,
|
|
devices: state.devices.filter((device) => project.deviceIds.includes(device.id)),
|
|
masterIdentity: projectId === "master-agent" ? getProjectMasterIdentity(state) : undefined,
|
|
activeThreadContexts,
|
|
nextCompactionRiskThreadId: topRisk?.threadId,
|
|
threadsRequiringHandoff,
|
|
masterContextStrategySummary,
|
|
recentAppLogs,
|
|
openFaults,
|
|
relatedAuditResults,
|
|
};
|
|
}
|
|
|
|
export function getThreadContextDetailView(
|
|
state: BossState,
|
|
threadId: string,
|
|
): ThreadContextDetailView | null {
|
|
const snapshot = state.threadContextSnapshots.find((item) => item.threadId === threadId);
|
|
if (!snapshot) return null;
|
|
const handoffPackage = state.threadHandoffPackages.find(
|
|
(item) => item.fromThreadId === threadId && item.packageStatus !== "expired",
|
|
);
|
|
const alerts = state.threadContextAlerts.filter((item) => item.threadId === threadId);
|
|
const masterActions = Array.from(
|
|
new Set(alerts.flatMap((alert) => alert.masterActions)),
|
|
);
|
|
|
|
return {
|
|
snapshot,
|
|
handoffPackage,
|
|
alerts,
|
|
currentChecklist: snapshot.checklist,
|
|
masterActions,
|
|
};
|
|
}
|
|
|
|
export function getDeviceWorkspaceView(
|
|
state: BossState,
|
|
deviceId?: string,
|
|
): DeviceWorkspaceView {
|
|
if (!deviceId) {
|
|
return {
|
|
relatedThreads: [],
|
|
};
|
|
}
|
|
return {
|
|
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),
|
|
};
|
|
}
|
|
|
|
export function getOpsSummaryView(state: BossState): OpsSummaryView {
|
|
const tickets = state.opsRepairTickets.map((ticket) => ({
|
|
...ticket,
|
|
verification: state.opsRepairVerifications.find((item) => item.ticketId === ticket.ticketId),
|
|
}));
|
|
const mode =
|
|
state.opsFaults.some((fault) => fault.status !== "resolved") ||
|
|
state.threadContextSnapshots.some(
|
|
(snapshot) =>
|
|
snapshot.contextBudgetLevel === "urgent" || snapshot.contextBudgetLevel === "critical",
|
|
)
|
|
? "active"
|
|
: "idle";
|
|
|
|
return {
|
|
mode,
|
|
faults: [...state.opsFaults].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)),
|
|
tickets: tickets.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)),
|
|
};
|
|
}
|
|
|
|
export function getAuditSummaryView(state: BossState): AuditSummaryView {
|
|
const completedIds = new Set(state.auditResults.map((result) => result.auditRequestId));
|
|
return {
|
|
pendingRequests: state.auditRequests.filter(
|
|
(request) => !completedIds.has(request.auditRequestId),
|
|
),
|
|
latestResults: [...state.auditResults].sort((a, b) =>
|
|
b.completedAt.localeCompare(a.completedAt),
|
|
),
|
|
capabilities: state.capabilities,
|
|
};
|
|
}
|
|
|
|
export function getSkillInventoryView(state: BossState): SkillInventoryView {
|
|
return getSkillInventoryViewForAccount(
|
|
state,
|
|
state.user.account,
|
|
state.user.boundDeviceId,
|
|
);
|
|
}
|
|
|
|
export function getSkillInventoryViewForAccount(
|
|
state: BossState,
|
|
account: string,
|
|
boundDeviceId?: string,
|
|
): SkillInventoryView {
|
|
const devices = state.devices
|
|
.filter(
|
|
(device) =>
|
|
device.account === account || device.id === boundDeviceId,
|
|
)
|
|
.sort((a, b) => {
|
|
if (a.id === boundDeviceId) return -1;
|
|
if (b.id === boundDeviceId) return 1;
|
|
return b.lastSeenAt.localeCompare(a.lastSeenAt);
|
|
});
|
|
|
|
return {
|
|
boundDeviceId,
|
|
groups: devices
|
|
.map((device) => ({
|
|
device,
|
|
skills: state.deviceSkills
|
|
.filter((skill) => skill.deviceId === device.id)
|
|
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN")),
|
|
}))
|
|
.filter((group) => group.skills.length > 0),
|
|
};
|
|
}
|