Files
boss/src/lib/boss-projections.ts
2026-04-01 05:33:35 +08:00

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),
};
}