import type { AiAccountRole, AiProvider, AiAccountStatus, AppLogEntry, AuthSession, AuditTaskRequest, AuditTaskResult, BossState, Capability, ContextBudgetLevel, Device, DeviceEnrollment, DeviceImportDraft, DeviceImportResolution, ProjectExecutionPolicy, DeviceSkill, MasterIdentitySummary, MasterAgentMemory, MasterAgentPromptPolicy, OpsFault, OpsRepairTicket, OpsRepairVerification, Project, ProjectAgentControls, RiskLevel, ThreadContextAlert, ThreadContextSnapshot, ThreadHandoffPackage, UserMasterPrompt, } from "@/lib/boss-data"; import { canAccessDevice, canAccessProject, canViewSkill, filterDevicesForSession, filterProjectDevicesForSession, filterProjectsForSession, type PermissionSession, } from "@/lib/boss-permissions"; 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; searchAliases?: string[]; searchTargetProjectIds?: string[]; 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; } function conversationHistoryWasCleared(state: BossState) { return Boolean(state.conversationHistoryClearedAt?.trim()); } 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; projectExecutionPolicies?: ProjectExecutionPolicy[]; } 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 = { critical: 0, urgent: 1, watch: 2, safe: 3, }; const aiRolePriority: Record = { 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); } const STALE_CONTEXT_SYNC_LABEL = "待同步"; const STALE_CONTEXT_REPLY_THRESHOLD_MS = 7 * 24 * 60 * 60_000; const PROCESS_PREVIEW_PREFIXES = [ "我先", "我现在", "我会先", "我发现", "我准备", "接下来", "正在", "先看", "先读", "我把", "我再", "目前在", "现在在", "补一组", "处理一下", "先确认", "准备", "同步一下", "我这边已经", ]; const PROCESS_PREVIEW_CONTAINS = [ "我继续", "我已经在", "正在跑", "正在检查", "正在处理", "正在同步", "我会直接", "我先把", "先补", "再接", ]; const PROCESS_PREVIEW_NUMBERED_HINTS = [ "先", "再", "接下来", "然后", "检查", "确认", "处理", "同步", "补", "排查", "推进", "回你", "回传", "会把", "我会", ]; const PROCESS_PREVIEW_BLOCK_MARKERS = [ "失败", "报错", "错误", "阻塞", "不能", "无法", "崩溃", "超时", "exception", "error", "fatal", "结论", "最终", "总结", "已完成", "已经完成", "验证通过", "测试通过", "已修复", "修好了", "已部署", "已安装", "可以直接", ]; const LEAKED_TITLE_PREFIXES = [ "你当前接手的项目根目录是", "你现在接手的项目根目录是", "你现在以目标线程身份直接回复用户", "你正在向主 Agent 同步当前项目状态", "只回复对用户真正有用的内容", "只输出 JSON", ]; const LEAKED_TITLE_CONTAINS = [ "不要发送内部字段", "不要自称主 Agent", "不要解释系统如何分发", "不要输出 JSON", "项目名称:", "线程名称:", "文件夹:", "同步原因:", "当前消息:", "用户当前消息:", ]; function formatConversationLatestReplyLabel(value: string, hasVisibleContext: boolean) { if (hasVisibleContext && value.includes("T")) { const date = new Date(value); const diff = Date.now() - date.getTime(); if (!Number.isNaN(date.getTime()) && diff >= STALE_CONTEXT_REPLY_THRESHOLD_MS) { return STALE_CONTEXT_SYNC_LABEL; } } return formatTimestampLabel(value); } 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()).toLowerCase(); 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"; case "deepseek_api": return "DeepSeek API"; 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 { folderLabel, threadTitle, projectTitle } = buildProjectDisplayTitles(project); const activityIconCount = deriveConversationActivityIconCount(state, project); const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined; const latestConversationActivityAt = deriveLatestConversationActivityAt(project); const compactPreview = compactImportedThreadPreview(project.preview); 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, threadTitle, folderLabel, folderKey: buildFolderKey(project), preview: compactPreview, lastMessagePreview: compactPreview, activityIconCount, topPinnedLabel, manualPinned: Boolean(project.pinned && !project.systemPinned), latestReplyAt: latestConversationActivityAt, latestReplyLabel: formatConversationLatestReplyLabel( latestConversationActivityAt, Boolean(topThread), ), 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: Boolean(topThread), style: "ring_percent", percent: topThread?.contextBudgetRemainingPct, level: topThread?.contextBudgetLevel, }, contextBudgetSourceNodeId: topThread?.nodeId, contextBudgetUpdatedAt: topThread?.capturedAt, mustFinishBeforeCompaction: Boolean(topThread?.mustFinishBeforeCompaction), } satisfies ConversationItem; } function buildProjectDisplayTitles(project: Project) { const folderLabel = normalizeConversationTitle(project.threadMeta?.folderName ?? ""); const folderFallback = pickConversationTitleFallback([ folderLabel, project.threadMeta?.codexFolderRef, project.name, ]); const threadTitle = sanitizeConversationTitle(project.threadMeta?.threadDisplayName ?? project.name, [ folderFallback, project.name, project.threadMeta?.codexFolderRef, ]); const projectTitle = projectType(project) === "single_device" ? threadTitle || sanitizeConversationTitle(project.name, [folderFallback, project.threadMeta?.codexFolderRef]) : sanitizeConversationTitle(project.name, [ threadTitle, folderFallback, project.threadMeta?.codexFolderRef, ]); return { folderLabel, threadTitle, projectTitle, }; } function cloneProjectWithDisplayTitles(project: Project): Project { const { folderLabel, threadTitle, projectTitle } = buildProjectDisplayTitles(project); return { ...project, name: projectTitle || project.name, threadMeta: { ...project.threadMeta, threadDisplayName: threadTitle || project.threadMeta.threadDisplayName, folderName: folderLabel || project.threadMeta.folderName, }, }; } function deriveLatestConversationActivityAt(project: Project) { const messageCandidates = [ project.lastMessageAt, ...project.messages.map((message) => message.sentAt), ].filter(Boolean) as string[]; let latest = messageCandidates[0]; let latestTs = latest ? Date.parse(latest) : Number.NEGATIVE_INFINITY; for (const candidate of messageCandidates.slice(1)) { const candidateTs = Date.parse(candidate); if (!Number.isFinite(candidateTs)) { continue; } if (!Number.isFinite(latestTs) || candidateTs > latestTs) { latest = candidate; latestTs = candidateTs; } } return latest ?? project.lastMessageAt ?? project.updatedAt; } function deriveConversationActivityIconCount(state: BossState, project: Project): number { let count = 0; if ( state.dispatchPlans.some( (plan) => plan.groupProjectId === project.id && plan.status === "pending_user_confirmation", ) ) { count += 1; } count += state.dispatchExecutions.filter( (execution) => (execution.groupProjectId === project.id || execution.targetProjectId === project.id) && (execution.status === "queued" || execution.status === "running"), ).length; count += state.masterAgentTasks.filter( (task) => task.projectId === project.id && task.taskType !== "device_import_resolution" && (task.status === "queued" || task.status === "running"), ).length; return Math.max(0, Math.min(4, count)); } 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); }); } function buildFolderSearchAliases(items: ConversationItem[]) { const aliases: string[] = []; const targetProjectIds: string[] = []; for (const item of items) { const alias = (item.threadTitle?.trim() || item.projectTitle?.trim() || "").trim(); if (!alias) continue; aliases.push(alias); targetProjectIds.push(item.projectId); } return aliases.length > 0 ? { aliases, targetProjectIds, } : undefined; } function compactImportedThreadPreview(preview?: string) { const value = preview?.trim(); if (!value) return ""; if (/^已从设备.+导入线程《.+》[。.]?$/.test(value)) { return "已导入线程"; } if (isLikelyProcessPreview(value)) { return ""; } return compactConversationPreview(value); } function isLikelyProcessPreview(preview: string) { const normalized = compactProcessPreview(preview); if (!normalized) { return false; } if (containsProcessPreviewMarker(normalized, PROCESS_PREVIEW_BLOCK_MARKERS)) { return false; } if (isStructuredNumberedProcessPreview(preview)) { return true; } return ( PROCESS_PREVIEW_PREFIXES.some((marker) => normalized.startsWith(marker)) || containsProcessPreviewMarker(normalized, PROCESS_PREVIEW_CONTAINS) ); } function compactProcessPreview(value: string) { return value .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") .replace(/\n{2,}/g, "\n") .trim() .toLowerCase(); } function containsProcessPreviewMarker(value: string, markers: string[]) { return markers.some((marker) => value.includes(marker)); } function isStructuredNumberedProcessPreview(preview: string) { const numberedLines = preview .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") .split("\n") .map((line) => compactProcessPreview(line)) .filter((line) => /^\d+[.)\u3001]\s*/.test(line)); if (numberedLines.length < 2) { return false; } return containsProcessPreviewMarker( numberedLines.join(" "), PROCESS_PREVIEW_NUMBERED_HINTS, ); } function compactConversationPreview(preview: string) { const structuredPreview = compactStructuredSummaryPreview(preview); const flattened = (structuredPreview || preview) .replace(/\[[^\]]+\]\(([^)]+)\)/g, "$1") .replace(/`([^`]+)`/g, "$1") .replace(/\s+/g, " ") .trim(); if (!flattened) { return ""; } return flattened.length <= 72 ? flattened : `${flattened.slice(0, 72).trimEnd()}…`; } function compactStructuredSummaryPreview(preview: string) { const raw = preview.trim(); if (!raw.startsWith("{") || !raw.endsWith("}")) { return ""; } try { const parsed = JSON.parse(raw) as Record; if (!parsed || Array.isArray(parsed)) { return ""; } const segments = [ formatStructuredSummarySegment("目标", parsed.projectGoal), formatStructuredSummarySegment("进度", parsed.currentProgress), formatStructuredSummarySegment("版本", parsed.versionRecord), formatStructuredSummarySegment("下一步", parsed.recommendedNextStep), ].filter(Boolean); return segments.join(" "); } catch { return ""; } } function formatStructuredSummarySegment(label: string, value: unknown) { const normalized = typeof value === "string" ? value.trim() : ""; return normalized ? `${label}:${normalized}` : ""; } function normalizeConversationTitle(value?: string) { const source = value?.replace(/\u0000/g, "") ?? ""; const firstLine = source .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean); if (!firstLine) { return ""; } return firstLine.replace(/\s+/g, " ").trim(); } function stripTrailingConversationTitleNoise(value: string) { return value.replace(/['"}\]]{2,}$/g, "").trimEnd(); } function looksLikeLeakedConversationTitle(value?: string) { const normalized = normalizeConversationTitle(value); if (!normalized) { return false; } return ( LEAKED_TITLE_PREFIXES.some((marker) => normalized.startsWith(marker)) || LEAKED_TITLE_CONTAINS.some((marker) => normalized.includes(marker)) ); } function extractWorkspaceProjectName(value?: string) { const normalized = normalizeConversationTitle(value).replaceAll("\\", "/"); if (!normalized) { return ""; } const patterns = [ /\/Users\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i, /\/home\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i, /[A-Za-z]:\/Users\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i, ]; for (const pattern of patterns) { const match = normalized.match(pattern); if (match?.[1]) { return match[1].split("/")[0]?.trim() ?? ""; } } return ""; } function pickConversationTitleFallback(candidates: Array) { for (const candidate of candidates) { const extractedProjectName = extractWorkspaceProjectName(candidate); if (extractedProjectName && !looksLikeLeakedConversationTitle(extractedProjectName)) { return extractedProjectName; } const normalized = stripTrailingConversationTitleNoise( trimLocalWorkspacePrefix(normalizeConversationTitle(candidate)), ); if (normalized && !looksLikeLeakedConversationTitle(normalized)) { return normalized; } } return ""; } function sanitizeConversationTitle(value: string | undefined, fallbackCandidates: Array = []) { const normalized = normalizeConversationTitle(value); const trimmed = stripTrailingConversationTitleNoise(trimLocalWorkspacePrefix(normalized)); if (trimmed && !looksLikeLeakedConversationTitle(normalized) && !looksLikeLeakedConversationTitle(trimmed)) { return trimmed; } const extractedProjectName = extractWorkspaceProjectName(normalized); if (extractedProjectName && !looksLikeLeakedConversationTitle(extractedProjectName)) { return extractedProjectName; } const fallback = pickConversationTitleFallback(fallbackCandidates); return fallback || trimmed; } function trimLocalWorkspacePrefix(label?: string) { const value = label?.trim(); if (!value) return ""; const normalized = value.replaceAll("\\", "/"); const patterns = [ /^\/Users\/[^/]+\/code\/(.+)$/i, /^\/home\/[^/]+\/code\/(.+)$/i, /^[A-Za-z]:\/Users\/[^/]+\/code\/(.+)$/i, ]; for (const pattern of patterns) { const match = normalized.match(pattern); if (match?.[1]) { return match[1]; } } return value; } export function getConversationItems(state: BossState): ConversationItem[] { const conversations = state.projects.map((project) => buildConversationItem(state, project)); return sortConversationItems(conversations); } function stateForSession(state: BossState, session: PermissionSession): BossState { const visibleDevices = filterDevicesForSession(state, session); const visibleDeviceIds = new Set(visibleDevices.map((device) => device.id)); const visibleProjects = filterProjectsForSession(state, session).map((project) => ({ ...project, deviceIds: project.deviceIds.filter((deviceId) => visibleDeviceIds.has(deviceId)), groupMembers: project.groupMembers.filter((member) => visibleDeviceIds.has(member.deviceId)), })); const scopedVisibleProjects = visibleProjects.map((project) => project.id === "master-agent" && session.role !== "highest_admin" ? projectWithAccountScopedMasterMessages(project, session.account) : project, ); const visibleProjectIds = new Set(scopedVisibleProjects.map((project) => project.id)); const canSeeThreadOnDevice = (projectId: string, deviceId: string) => visibleProjectIds.has(projectId) && visibleDeviceIds.has(deviceId); return { ...state, devices: visibleDevices, projects: scopedVisibleProjects, deviceSkills: state.deviceSkills.filter((skill) => visibleDeviceIds.has(skill.deviceId) && (session.role === "highest_admin" || canViewSkill(state, session, skill.skillId, { deviceId: skill.deviceId })), ), threadStatusDocuments: state.threadStatusDocuments.filter((document) => canSeeThreadOnDevice(document.projectId, document.deviceId), ), threadProgressEvents: state.threadProgressEvents.filter((event) => canSeeThreadOnDevice(event.projectId, event.deviceId), ), threadContextSnapshots: state.threadContextSnapshots.filter((snapshot) => canSeeThreadOnDevice(snapshot.projectId, snapshot.nodeId), ), threadHandoffPackages: state.threadHandoffPackages.filter((item) => visibleProjectIds.has(item.projectId), ), threadContextAlerts: state.threadContextAlerts.filter((alert) => visibleProjectIds.has(alert.projectId), ), dispatchPlans: state.dispatchPlans .filter((plan) => visibleProjectIds.has(plan.groupProjectId)) .map((plan) => ({ ...plan, targets: plan.targets.filter( (target) => visibleProjectIds.has(target.projectId) && visibleDeviceIds.has(target.deviceId), ), confirmedTargetProjectIds: plan.confirmedTargetProjectIds?.filter((projectId) => visibleProjectIds.has(projectId), ), })), dispatchExecutions: state.dispatchExecutions.filter( (execution) => visibleProjectIds.has(execution.groupProjectId) && visibleProjectIds.has(execution.targetProjectId) && visibleDeviceIds.has(execution.deviceId), ), masterAgentTasks: state.masterAgentTasks.filter( (task) => task.requestedByAccount === session.account || visibleProjectIds.has(task.projectId) || Boolean(task.targetProjectId && visibleProjectIds.has(task.targetProjectId)), ), appLogs: state.appLogs.filter((log) => visibleDeviceIds.has(log.deviceId) || Boolean(log.projectId && visibleProjectIds.has(log.projectId)), ), }; } function projectWithAccountScopedMasterMessages(project: Project, account: string): Project { const messages = project.messages.filter((message) => message.account === account); const latestMessage = [...messages].sort( (left, right) => Date.parse(right.sentAt) - Date.parse(left.sentAt), )[0]; return { ...project, messages, preview: latestMessage?.body ?? "", lastMessageAt: latestMessage?.sentAt ?? project.updatedAt, unreadCount: messages.filter((message) => message.sender !== "user").length, }; } export function getAuthorizedStateSnapshot( state: BossState, session: Pick, ): BossState { return stateForSession(state, session); } export function getConversationItemsForSession( state: BossState, session: Pick, ): ConversationItem[] { return getConversationItems(stateForSession(state, session)); } export interface ConversationFolderView { folderKey: string; folderLabel: string; deviceId?: string; deviceName?: string; threadCount: number; threads: ConversationItem[]; } export interface ProjectMessagesRealtimePayload { ok: true; project: Project; devices: Device[]; } 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(); 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; const topContextItem = [...items] .filter((item) => item.contextBudgetIndicator.visible) .sort((a, b) => { if (a.mustFinishBeforeCompaction !== b.mustFinishBeforeCompaction) { return a.mustFinishBeforeCompaction ? -1 : 1; } const aLevel = a.contextBudgetIndicator.level ?? "safe"; const bLevel = b.contextBudgetIndicator.level ?? "safe"; if (levelPriority[aLevel] !== levelPriority[bLevel]) { return levelPriority[aLevel] - levelPriority[bLevel]; } return b.latestReplyAt.localeCompare(a.latestReplyAt); })[0]; const recentThreadLabel = trimLocalWorkspacePrefix(latestItem.threadTitle); const searchAliases = buildFolderSearchAliases(items); const latestPreview = compactImportedThreadPreview(latestItem.preview); const latestMessagePreview = compactImportedThreadPreview( latestItem.lastMessagePreview || latestItem.preview, ); const historyCleared = conversationHistoryWasCleared(state); 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: recentThreadLabel ? `${items.length} 个线程 · 最近:${recentThreadLabel}` : `${items.length} 个线程`, folderKey, threadCount: items.length, topPinnedLabel: items.some((entry) => entry.topPinnedLabel) ? "置顶" : undefined, manualPinned: items.some((entry) => entry.manualPinned), ...(searchAliases ? { searchAliases: searchAliases.aliases, searchTargetProjectIds: searchAliases.targetProjectIds, } : {}), preview: latestPreview || (historyCleared ? "" : `包含 ${items.length} 个线程,最近活跃:《${recentThreadLabel || latestItem.threadTitle}》`), lastMessagePreview: latestMessagePreview || latestPreview || (historyCleared ? "" : `包含 ${items.length} 个线程,最近活跃:《${recentThreadLabel || latestItem.threadTitle}》`), activityIconCount: Math.max(0, Math.min(4, items.reduce((sum, entry) => sum + entry.activityIconCount, 0))), 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: Boolean(topContextItem), style: "ring_percent", percent: topContextItem?.contextBudgetIndicator.percent, level: topContextItem?.contextBudgetIndicator.level, }, contextBudgetSourceNodeId: topContextItem?.contextBudgetSourceNodeId, contextBudgetUpdatedAt: topContextItem?.contextBudgetUpdatedAt, mustFinishBeforeCompaction: Boolean(topContextItem?.mustFinishBeforeCompaction), }); } return sortConversationItems(passthrough); } export function getConversationHomeItemsForSession( state: BossState, session: Pick, ): ConversationItem[] { return getConversationHomeItems(stateForSession(state, session)); } export function getConversationWebItems(state: BossState): ConversationItem[] { return getConversationHomeItems(state).map((item) => ({ ...item, topPinnedLabel: undefined, manualPinned: false, })); } export function getConversationHomeItemForProject(state: BossState, projectId: string): ConversationItem | null { const normalizedProjectId = projectId.trim(); if (!normalizedProjectId) { return null; } return ( getConversationHomeItems(state).find((item) => { if (item.projectId === normalizedProjectId) { return true; } return Array.isArray(item.searchTargetProjectIds) ? item.searchTargetProjectIds.includes(normalizedProjectId) : false; }) ?? null ); } export function getConversationThreadItemForProject(state: BossState, projectId: string): ConversationItem | null { const normalizedProjectId = projectId.trim(); if (!normalizedProjectId) { return null; } return getConversationItems(state).find((item) => item.projectId === normalizedProjectId) ?? null; } 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), }; } export function getConversationFolderViewForSession( state: BossState, session: Pick, folderKey: string, ): ConversationFolderView | null { return getConversationFolderView(stateForSession(state, session), folderKey); } export function buildProjectMessagesRealtimePayload( state: BossState, projectId: string, ): ProjectMessagesRealtimePayload | null { const normalizedProjectId = projectId.trim(); if (!normalizedProjectId) { return null; } const project = state.projects.find((item) => item.id === normalizedProjectId); if (!project) { return null; } return { ok: true, project: cloneProjectWithDisplayTitles(project), devices: state.devices.filter((device) => project.deviceIds.includes(device.id)), }; } export function buildProjectMessagesRealtimePayloadForSession( state: BossState, session: Pick, projectId: string, ): ProjectMessagesRealtimePayload | null { if (!canAccessProject(state, session, projectId, "project.view")) { return null; } const project = state.projects.find((item) => item.id === projectId); if (!project) { return null; } const scopedProject = project.id === "master-agent" && session.role !== "highest_admin" ? projectWithAccountScopedMasterMessages(cloneProjectWithDisplayTitles(project), session.account) : cloneProjectWithDisplayTitles(project); return { ok: true, project: scopedProject, devices: filterProjectDevicesForSession(state, session, project), }; } function resolveProjectAgentControls( state: BossState, projectId: string, account?: string, ) { const normalizedAccount = account?.trim(); const scoped = normalizedAccount ? ( state.userProjectAgentControls.find( (item) => item.projectId === projectId && item.account === normalizedAccount, ) ?? null ) : null; const projectControls = scoped?.controls ?? state.projects.find((item) => item.id === projectId)?.agentControls ?? null; if (projectId === "master-agent") { return projectControls; } const globalControls = normalizedAccount ? ( state.userProjectAgentControls.find( (item) => item.projectId === "master-agent" && item.account === normalizedAccount, )?.controls ?? state.projects.find((item) => item.id === "master-agent")?.agentControls ?? null ) : state.projects.find((item) => item.id === "master-agent")?.agentControls ?? null; const explicitTakeover = projectControls?.takeoverEnabled; const inheritedGlobalTakeover = globalControls?.globalTakeoverEnabled; const effectiveTakeoverEnabled = explicitTakeover !== undefined ? explicitTakeover : Boolean(inheritedGlobalTakeover); const takeoverInheritedFromGlobal = explicitTakeover === undefined && inheritedGlobalTakeover !== undefined; if (!projectControls && !takeoverInheritedFromGlobal && !effectiveTakeoverEnabled) { return null; } return { ...(projectControls ?? { updatedAt: globalControls?.updatedAt ?? new Date().toISOString() }), updatedAt: projectControls?.updatedAt ?? globalControls?.updatedAt ?? new Date().toISOString(), effectiveTakeoverEnabled, takeoverInheritedFromGlobal, }; } 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 displayProject = cloneProjectWithDisplayTitles(project); 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: displayProject, agentControls: resolveProjectAgentControls(state, projectId, account), 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 getProjectDetailViewForSession( state: BossState, projectId: string, session: Pick, ): ProjectDetailView | null { if (!canAccessProject(state, session, projectId, "project.view")) { return null; } const detail = getProjectDetailView(state, projectId, session.account); if (!detail) { return null; } const visibleProjectIds = new Set(filterProjectsForSession(state, session).map((project) => project.id)); const visibleDeviceIds = new Set(filterDevicesForSession(state, session).map((device) => device.id)); return { ...detail, devices: filterProjectDevicesForSession(state, session, detail.project), activeThreadContexts: detail.activeThreadContexts.filter((item) => visibleProjectIds.has(item.snapshot.projectId) && visibleDeviceIds.has(item.snapshot.nodeId), ), threadsRequiringHandoff: detail.threadsRequiringHandoff.filter((item) => visibleProjectIds.has(item.snapshot.projectId) && visibleDeviceIds.has(item.snapshot.nodeId), ), recentAppLogs: detail.recentAppLogs.filter((log) => visibleDeviceIds.has(log.deviceId) || Boolean(log.projectId && visibleProjectIds.has(log.projectId)), ), }; } 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: [], }; } const selectedDevice = state.devices.find((item) => item.id === deviceId); return { selectedDevice: selectedDevice ? { ...selectedDevice } : undefined, 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), projectExecutionPolicies: state.projectExecutionPolicies.filter((item) => item.deviceId === deviceId), }; } export function getDeviceWorkspaceViewForSession( state: BossState, session: Pick, deviceId?: string, ): DeviceWorkspaceView { if (!deviceId || !canAccessDevice(state, session, deviceId, "device.view")) { return { relatedThreads: [] }; } return getDeviceWorkspaceView(stateForSession(state, session), 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), }; } export function getSkillInventoryViewForSession( state: BossState, session: Pick, boundDeviceId?: string, ): SkillInventoryView { const devices = filterDevicesForSession(state, session) .filter((device) => !boundDeviceId || device.id === boundDeviceId || session.role === "highest_admin") .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) .filter((skill) => session.role === "highest_admin" || canViewSkill(state, session, skill.skillId, { deviceId: device.id }), ) .sort((a, b) => a.name.localeCompare(b.name, "zh-CN")), })) .filter((group) => group.skills.length > 0), }; }