8058 lines
267 KiB
TypeScript
8058 lines
267 KiB
TypeScript
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||
import { promises as fs } from "node:fs";
|
||
import path from "node:path";
|
||
import { publishBossEvent } from "@/lib/boss-events";
|
||
import type { VerificationDeliveryMode } from "@/lib/boss-mail";
|
||
import { getFixedVerificationCode, getVerificationDeliveryMode } from "@/lib/boss-mail";
|
||
import { getPublishedOtaAsset } from "@/lib/boss-ota";
|
||
|
||
export type DeviceStatus = "online" | "abnormal" | "offline";
|
||
export type DeviceSource = "production" | "demo";
|
||
export type GoalState = "pending" | "completed";
|
||
export type MessageSender = "master" | "device" | "user" | "ops" | "audit";
|
||
// Forwarding uses a structured contract so the route can distinguish
|
||
// single-message forwarding, bundle forwarding, and the legacy notice shape.
|
||
export type MessageKind =
|
||
| "text"
|
||
| "system_notice"
|
||
| "voice_intent"
|
||
| "image_intent"
|
||
| "video_intent"
|
||
| "forward_notice"
|
||
| "forward_single"
|
||
| "forward_bundle"
|
||
| "attachment"
|
||
| "analysis_card";
|
||
export type AttachmentKind = "image" | "video" | "pdf" | "text" | "office" | "binary";
|
||
export type AttachmentStorageBackend = "server_file" | "aliyun_oss";
|
||
export type AttachmentAnalysisState =
|
||
| "not_applicable"
|
||
| "queued_auto"
|
||
| "ready_manual"
|
||
| "processing"
|
||
| "completed"
|
||
| "failed";
|
||
|
||
export interface MessageAttachment {
|
||
attachmentId: string;
|
||
fileName: string;
|
||
mimeType: string;
|
||
fileSizeBytes: number;
|
||
attachmentKind: AttachmentKind;
|
||
storageBackend: AttachmentStorageBackend;
|
||
storagePath: string;
|
||
storageSnapshot?: {
|
||
provider: "aliyun_oss";
|
||
accessKeyId: string;
|
||
accessKeySecretEncrypted: string;
|
||
bucket: string;
|
||
endpoint: string;
|
||
region: string;
|
||
prefix?: string;
|
||
};
|
||
previewAvailable: boolean;
|
||
uploadedAt: string;
|
||
uploadedBy: string;
|
||
analysisState: AttachmentAnalysisState;
|
||
analysisSummary?: string;
|
||
analysisCardId?: string;
|
||
}
|
||
|
||
export interface ForwardSource {
|
||
sourceProjectId: string;
|
||
sourceProjectName: string;
|
||
sourceThreadId?: string;
|
||
sourceThreadTitle?: string;
|
||
sourceMessageId: string;
|
||
forwardedBy: string;
|
||
forwardedAt: string;
|
||
}
|
||
|
||
export interface ForwardBundleItem {
|
||
messageId: string;
|
||
senderLabel: string;
|
||
body: string;
|
||
kind: string;
|
||
sentAt: string;
|
||
}
|
||
|
||
export interface ForwardBundlePayload {
|
||
sourceProjectId: string;
|
||
sourceProjectName: string;
|
||
sourceThreadId?: string;
|
||
sourceThreadTitle?: string;
|
||
itemCount: number;
|
||
startedAt: string;
|
||
endedAt: string;
|
||
items: ForwardBundleItem[];
|
||
}
|
||
export type ContextBudgetLevel = "safe" | "watch" | "urgent" | "critical";
|
||
export type ThreadState =
|
||
| "idle"
|
||
| "running"
|
||
| "waiting_input"
|
||
| "waiting_approval"
|
||
| "context_watch"
|
||
| "context_urgent"
|
||
| "compacted"
|
||
| "handoff_pending"
|
||
| "completed"
|
||
| "failed";
|
||
export type RiskLevel = "low" | "medium" | "high";
|
||
export type AlertStatus = "opened" | "acked" | "resolved";
|
||
export type HandoffStatus = "draft" | "ready" | "consumed" | "expired";
|
||
export type OpsSeverity = "info" | "warning" | "critical";
|
||
export type OpsStatus = "opened" | "acked" | "repairing" | "resolved";
|
||
export type ApprovalStatus = "pending" | "approved" | "escalated";
|
||
export type ExecutionStatus = "queued" | "running" | "verified" | "failed";
|
||
export type VerificationStatus = "passed" | "failed" | "watching";
|
||
export type EnrollmentStatus = "ready" | "claimed" | "expired";
|
||
export type AuditTrigger =
|
||
| "milestone"
|
||
| "merge_candidate"
|
||
| "failure_escalation"
|
||
| "scheduled_check"
|
||
| "human_request";
|
||
export type AuditType = "software" | "hardware" | "multimodal" | "chief";
|
||
export type AuditResultStatus =
|
||
| "completed"
|
||
| "failed"
|
||
| "needs_retry"
|
||
| "needs_human_review";
|
||
export type AuditDecision = "pass" | "fail" | "warning" | "inconclusive";
|
||
export type CapabilityLeaseMode = "exclusive" | "shared_read" | "shared_stream";
|
||
export type AuthRole = "member" | "admin" | "highest_admin";
|
||
export type LoginMethod = "password" | "code";
|
||
export type OtaUpdateStatus = "available" | "scheduled" | "applied" | "skipped";
|
||
export type OtaLogStatus = "checked" | "applied" | "skipped";
|
||
export type AppLogLevel = "info" | "warn" | "error";
|
||
export type AiProvider = "master_codex_node" | "openai_api" | "aliyun_qwen_api";
|
||
export type AiAccountRole = "primary" | "backup" | "api_fallback";
|
||
export type AiAccountStatus = "ready" | "needs_login" | "needs_api_key" | "degraded" | "disabled";
|
||
export type MasterAgentTaskStatus = "queued" | "running" | "completed" | "failed";
|
||
export type MasterAgentTaskType =
|
||
| "conversation_reply"
|
||
| "attachment_analysis"
|
||
| "group_dispatch_plan"
|
||
| "dispatch_execution"
|
||
| "device_import_resolution";
|
||
export type DispatchPlanStatus =
|
||
| "pending_user_confirmation"
|
||
| "approved"
|
||
| "rejected"
|
||
| "dispatched";
|
||
export type DispatchExecutionStatus = "queued" | "running" | "completed" | "failed";
|
||
export type ReasoningEffort = "low" | "medium" | "high";
|
||
|
||
export interface UserSettings {
|
||
liveUpdates: boolean;
|
||
showRiskBadges: boolean;
|
||
confirmDangerousActions: boolean;
|
||
preferredEntryPoint: "conversations" | "devices" | "me";
|
||
}
|
||
|
||
export interface UserProfile {
|
||
id: string;
|
||
name: string;
|
||
avatar: string;
|
||
account: string;
|
||
verificationEmail?: string;
|
||
role: AuthRole;
|
||
roleLabel: string;
|
||
accountType: string;
|
||
qrCodeValue: string;
|
||
boundCodexNodeId?: string;
|
||
boundCodexNodeLabel?: string;
|
||
boundDeviceId?: string;
|
||
boundAt?: string;
|
||
version: string;
|
||
otaVersion?: string;
|
||
otaSummary?: string[];
|
||
hasOta: boolean;
|
||
settings: UserSettings;
|
||
}
|
||
|
||
export interface Device {
|
||
id: string;
|
||
name: string;
|
||
avatar: string;
|
||
account: string;
|
||
source: DeviceSource;
|
||
status: DeviceStatus;
|
||
projects: string[];
|
||
quota5h: number;
|
||
quota7d: number;
|
||
lastSeenAt: string;
|
||
endpoint?: string;
|
||
token?: string;
|
||
note?: string;
|
||
}
|
||
|
||
export interface Message {
|
||
id: string;
|
||
sender: MessageSender;
|
||
senderLabel: string;
|
||
body: string;
|
||
sentAt: string;
|
||
kind?: MessageKind;
|
||
attachments?: MessageAttachment[];
|
||
forwardSource?: ForwardSource;
|
||
forwardBundle?: ForwardBundlePayload;
|
||
}
|
||
|
||
export interface UserAttachmentStorageConfig {
|
||
account: string;
|
||
mode: "server_file" | "oss";
|
||
ossProvider?: "aliyun_oss";
|
||
aliyunOss?: {
|
||
enabled: boolean;
|
||
accessKeyId: string;
|
||
accessKeySecretEncrypted: string;
|
||
bucket: string;
|
||
endpoint: string;
|
||
region: string;
|
||
prefix?: string;
|
||
};
|
||
updatedAt: string;
|
||
validatedAt?: string;
|
||
}
|
||
|
||
export interface MasterAgentPromptPolicy {
|
||
globalPrompt: string;
|
||
updatedAt: string;
|
||
updatedBy?: string;
|
||
}
|
||
|
||
export interface UserMasterPrompt {
|
||
account: string;
|
||
content: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export type MasterMemoryScope = "global" | "project";
|
||
export type MasterMemoryType =
|
||
| "user_preference"
|
||
| "project_progress"
|
||
| "decision"
|
||
| "risk"
|
||
| "blocking_issue"
|
||
| "research_note"
|
||
| "workflow_rule";
|
||
|
||
export interface MasterAgentMemory {
|
||
memoryId: string;
|
||
account: string;
|
||
scope: MasterMemoryScope;
|
||
projectId?: string;
|
||
title: string;
|
||
content: string;
|
||
memoryType: MasterMemoryType;
|
||
tags: string[];
|
||
sourceMessageId?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
lastUsedAt?: string;
|
||
archived: boolean;
|
||
}
|
||
|
||
export interface GoalItem {
|
||
id: string;
|
||
text: string;
|
||
state: GoalState;
|
||
note: string;
|
||
completedAt?: string;
|
||
completedBy?: string;
|
||
}
|
||
|
||
export interface VersionEntry {
|
||
version: string;
|
||
summary: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface ThreadConversationMeta {
|
||
projectId: string;
|
||
threadId: string;
|
||
threadDisplayName: string;
|
||
folderName: string;
|
||
activityIconCount: number;
|
||
updatedAt: string;
|
||
codexThreadRef?: string;
|
||
codexFolderRef?: string;
|
||
}
|
||
|
||
export interface GroupConversationMember {
|
||
projectId: string;
|
||
deviceId: string;
|
||
threadId: string;
|
||
threadDisplayName: string;
|
||
folderName: string;
|
||
}
|
||
|
||
export interface Project {
|
||
id: string;
|
||
name: string;
|
||
pinned: boolean;
|
||
systemPinned?: boolean;
|
||
deviceIds: string[];
|
||
preview: string;
|
||
updatedAt: string;
|
||
lastMessageAt: string;
|
||
isGroup: boolean;
|
||
threadMeta: ThreadConversationMeta;
|
||
groupMembers: GroupConversationMember[];
|
||
createdByAgent: boolean;
|
||
collaborationMode: "development" | "approval_required";
|
||
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
|
||
agentControls?: ProjectAgentControls;
|
||
unreadCount: number;
|
||
riskLevel: RiskLevel;
|
||
contextBudgetPct?: number;
|
||
contextBudgetLabel?: string;
|
||
messages: Message[];
|
||
goals: GoalItem[];
|
||
versions: VersionEntry[];
|
||
}
|
||
|
||
export interface DispatchPlanTarget {
|
||
deviceId: string;
|
||
projectId: string;
|
||
threadId: string;
|
||
threadDisplayName: string;
|
||
folderName: string;
|
||
codexFolderRef?: string;
|
||
codexThreadRef?: string;
|
||
reason: string;
|
||
}
|
||
|
||
export interface DispatchPlan {
|
||
planId: string;
|
||
groupProjectId: string;
|
||
requestMessageId: string;
|
||
requestedBy: string;
|
||
status: DispatchPlanStatus;
|
||
targets: DispatchPlanTarget[];
|
||
summary: string;
|
||
createdAt: string;
|
||
confirmedAt?: string;
|
||
confirmedBy?: string;
|
||
confirmedTargetProjectIds?: string[];
|
||
}
|
||
|
||
export interface DispatchExecution {
|
||
executionId: string;
|
||
planId: string;
|
||
groupProjectId: string;
|
||
targetProjectId: string;
|
||
targetThreadId: string;
|
||
deviceId: string;
|
||
status: DispatchExecutionStatus;
|
||
createdAt: string;
|
||
completedAt?: string;
|
||
resultMessageId?: string;
|
||
completedByDeviceId?: string;
|
||
}
|
||
|
||
export function buildCollaborationGate(
|
||
project?: Pick<Project, "isGroup" | "collaborationMode" | "approvalState">,
|
||
) {
|
||
if (!project) {
|
||
return {
|
||
isGroup: false,
|
||
collaborationMode: "development" as const,
|
||
requiresMasterAgentApproval: false,
|
||
approvalState: "not_required" as const,
|
||
};
|
||
}
|
||
|
||
return {
|
||
isGroup: project.isGroup,
|
||
collaborationMode: project.collaborationMode,
|
||
requiresMasterAgentApproval: project.isGroup && project.collaborationMode === "approval_required",
|
||
approvalState: project.approvalState,
|
||
};
|
||
}
|
||
|
||
export interface ProjectAgentControls {
|
||
modelOverride?: string;
|
||
reasoningEffortOverride?: ReasoningEffort;
|
||
promptOverride?: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface UserProjectAgentControls {
|
||
account: string;
|
||
projectId: string;
|
||
controls: ProjectAgentControls;
|
||
}
|
||
|
||
export interface DeviceImportCandidate {
|
||
candidateId: string;
|
||
deviceId: string;
|
||
folderName: string;
|
||
folderRef?: string;
|
||
threadId: string;
|
||
threadDisplayName: string;
|
||
codexFolderRef?: string;
|
||
codexThreadRef?: string;
|
||
lastActiveAt: string;
|
||
suggestedImport: boolean;
|
||
}
|
||
|
||
export function isDispatchableThreadProject(project: Project) {
|
||
return (
|
||
project.id !== "master-agent" &&
|
||
!project.isGroup &&
|
||
Boolean(project.threadMeta.codexThreadRef?.trim()) &&
|
||
project.deviceIds.length > 0
|
||
);
|
||
}
|
||
|
||
export interface DeviceImportDraft {
|
||
draftId: string;
|
||
deviceId: string;
|
||
enrollmentId?: string;
|
||
status: "pending_candidates" | "pending_selection" | "pending_resolution" | "resolved" | "applied";
|
||
candidates: DeviceImportCandidate[];
|
||
selectedCandidateIds: string[];
|
||
appliedProjectNames: 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;
|
||
purpose: "login" | "register" | "forgot-password";
|
||
code: string;
|
||
expiresAt: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface VerificationDispatch {
|
||
dispatchId: string;
|
||
account: string;
|
||
purpose: VerificationCode["purpose"];
|
||
deliveryMode: VerificationDeliveryMode;
|
||
requestedAt: string;
|
||
status: "requested" | "delivered" | "failed" | "rate_limited";
|
||
note: string;
|
||
}
|
||
|
||
export interface AuthAccount {
|
||
id: string;
|
||
account: string;
|
||
passwordHash: string;
|
||
displayName: string;
|
||
role: AuthRole;
|
||
verificationEmail?: string;
|
||
codexNodeId?: string;
|
||
codexNodeLabel?: string;
|
||
primaryDeviceId?: string;
|
||
isPrimary?: boolean;
|
||
failedLoginAttempts?: number;
|
||
lockedUntil?: string;
|
||
lastLoginAt?: string;
|
||
lastLoginMethod?: LoginMethod;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface AuthSession {
|
||
sessionId: string;
|
||
sessionToken: string;
|
||
restoreToken: string;
|
||
account: string;
|
||
role: AuthRole;
|
||
displayName: string;
|
||
loginMethod: LoginMethod;
|
||
createdAt: string;
|
||
expiresAt: string;
|
||
lastSeenAt: string;
|
||
revokedAt?: string;
|
||
}
|
||
|
||
export interface AiAccount {
|
||
accountId: string;
|
||
label: string;
|
||
role: AiAccountRole;
|
||
provider: AiProvider;
|
||
displayName: string;
|
||
accountIdentifier?: string;
|
||
nodeId?: string;
|
||
nodeLabel?: string;
|
||
model?: string;
|
||
apiKey?: string;
|
||
apiKeyMasked?: string;
|
||
enabled: boolean;
|
||
isActive: boolean;
|
||
status: AiAccountStatus;
|
||
loginStatusNote?: string;
|
||
lastValidatedAt?: string;
|
||
lastUsedAt?: string;
|
||
lastError?: string;
|
||
lastSwitchedAt?: string;
|
||
switchReason?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface AiAccountSwitchRecord {
|
||
switchId: string;
|
||
fromAccountId?: string;
|
||
fromLabel?: string;
|
||
toAccountId: string;
|
||
toLabel: string;
|
||
role: AiAccountRole;
|
||
switchedAt: string;
|
||
reason: string;
|
||
}
|
||
|
||
export interface AiAccountSummary {
|
||
accountId: string;
|
||
label: string;
|
||
role: AiAccountRole;
|
||
roleLabel: string;
|
||
provider: AiProvider;
|
||
providerLabel: string;
|
||
displayName: string;
|
||
accountIdentifier?: string;
|
||
nodeId?: string;
|
||
nodeLabel?: string;
|
||
model?: string;
|
||
enabled: boolean;
|
||
isActive: boolean;
|
||
canGenerate: boolean;
|
||
status: AiAccountStatus;
|
||
statusLabel: string;
|
||
loginStatusNote?: string;
|
||
apiKeyConfigured: boolean;
|
||
apiKeyMasked?: string;
|
||
lastValidatedAt?: string;
|
||
lastUsedAt?: string;
|
||
lastError?: string;
|
||
lastSwitchedAt?: string;
|
||
switchReason?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
isEnvironmentFallback?: boolean;
|
||
}
|
||
|
||
export interface MasterIdentitySummary {
|
||
accountId?: string;
|
||
label: string;
|
||
role: AiAccountRole;
|
||
roleLabel: string;
|
||
provider: AiProvider;
|
||
providerLabel: string;
|
||
displayName: string;
|
||
nodeLabel?: string;
|
||
model?: string;
|
||
status: AiAccountStatus;
|
||
statusLabel: string;
|
||
canGenerate: boolean;
|
||
switchReason?: string;
|
||
lastSwitchedAt?: string;
|
||
note?: string;
|
||
isEnvironmentFallback?: boolean;
|
||
}
|
||
|
||
export interface MasterAgentTask {
|
||
taskId: string;
|
||
projectId: string;
|
||
taskType: MasterAgentTaskType;
|
||
requestMessageId: string;
|
||
requestText: string;
|
||
executionPrompt: string;
|
||
requestedBy: string;
|
||
requestedByAccount: string;
|
||
deviceId: string;
|
||
accountId?: string;
|
||
accountLabel?: string;
|
||
attachmentId?: string;
|
||
attachmentFileName?: string;
|
||
attachmentDownloadToken?: string;
|
||
attachmentDownloadExpiresAt?: string;
|
||
attachmentDownloadUrl?: string;
|
||
attachmentTextExcerpt?: string;
|
||
dispatchExecutionId?: string;
|
||
targetProjectId?: string;
|
||
targetThreadId?: string;
|
||
targetThreadDisplayName?: string;
|
||
targetCodexThreadRef?: string;
|
||
targetCodexFolderRef?: string;
|
||
deviceImportDraftId?: string;
|
||
status: MasterAgentTaskStatus;
|
||
requestedAt: string;
|
||
claimedAt?: string;
|
||
completedAt?: string;
|
||
replyBody?: string;
|
||
errorMessage?: string;
|
||
requestId?: string;
|
||
}
|
||
|
||
export interface OtaUpdate {
|
||
releaseId: string;
|
||
version: string;
|
||
currentVersion: string;
|
||
channel: "stable" | "beta";
|
||
packageType: "web_console" | "android_shell";
|
||
status: OtaUpdateStatus;
|
||
summary: string[];
|
||
targetScope: string;
|
||
requiredRole: AuthRole;
|
||
publishedAt: string;
|
||
packageFileName?: string;
|
||
packageSizeBytes?: number;
|
||
packageSha256?: string;
|
||
downloadUrl?: string;
|
||
assetUpdatedAt?: string;
|
||
}
|
||
|
||
export interface OtaUpdateLog {
|
||
logId: string;
|
||
releaseId: string;
|
||
version: string;
|
||
status: OtaLogStatus;
|
||
triggeredBy: string;
|
||
triggeredAt: string;
|
||
completedAt?: string;
|
||
note: string;
|
||
}
|
||
|
||
export interface DeviceSkill {
|
||
skillId: string;
|
||
deviceId: string;
|
||
name: string;
|
||
description: string;
|
||
path: string;
|
||
invocation: string;
|
||
category: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface AppLogEntry {
|
||
logId: string;
|
||
deviceId: string;
|
||
projectId?: string;
|
||
level: AppLogLevel;
|
||
source: "app_client" | "local_agent";
|
||
category: string;
|
||
message: string;
|
||
detail?: string;
|
||
mirroredToProject: boolean;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface ThreadContextSnapshot {
|
||
snapshotId: string;
|
||
projectId: string;
|
||
taskId: string;
|
||
threadId: string;
|
||
title: string;
|
||
summary: string;
|
||
nodeId: string;
|
||
workerId: string;
|
||
sourceKind: "codex_app_server" | "codex_sdk" | "worker_estimator";
|
||
status: ThreadState;
|
||
contextBudgetRemainingPct: number;
|
||
contextBudgetLevel: ContextBudgetLevel;
|
||
compactionExpectedAt?: string;
|
||
mustFinishBeforeCompaction: boolean;
|
||
estimatedRemainingTurns: number;
|
||
estimatedRemainingLargeMessages: number;
|
||
lastCompactionAt?: string;
|
||
compactionCount: number;
|
||
patchPending: boolean;
|
||
testsPending: boolean;
|
||
evidencePending: boolean;
|
||
checklist: string[];
|
||
capturedAt: string;
|
||
}
|
||
|
||
export interface ThreadHandoffPackage {
|
||
handoffPackageId: string;
|
||
projectId: string;
|
||
taskId: string;
|
||
fromThreadId: string;
|
||
toThreadId: string;
|
||
packageStatus: HandoffStatus;
|
||
summaryText: string;
|
||
openQuestions: string[];
|
||
criticalFiles: string[];
|
||
criticalCommands: string[];
|
||
criticalTests: string[];
|
||
criticalArtifacts: string[];
|
||
decisionLinks: string[];
|
||
createdAt: string;
|
||
readyAt?: string;
|
||
consumedAt?: string;
|
||
}
|
||
|
||
export interface ThreadContextAlert {
|
||
alertId: string;
|
||
threadId: string;
|
||
projectId: string;
|
||
alertType:
|
||
| "context_watch"
|
||
| "context_urgent"
|
||
| "context_critical"
|
||
| "compaction_risk"
|
||
| "handoff_missing";
|
||
alertStatus: AlertStatus;
|
||
openedAt: string;
|
||
resolvedAt?: string;
|
||
summary: string;
|
||
masterActions: string[];
|
||
}
|
||
|
||
export interface DeviceEnrollment {
|
||
enrollmentId: string;
|
||
deviceId: string;
|
||
label: string;
|
||
pairingCode: string;
|
||
token: string;
|
||
status: EnrollmentStatus;
|
||
note: string;
|
||
createdAt: string;
|
||
expiresAt: string;
|
||
claimedAt?: string;
|
||
claimedDeviceId?: string;
|
||
}
|
||
|
||
export interface OpsFault {
|
||
faultId: string;
|
||
faultKey: string;
|
||
severity: OpsSeverity;
|
||
status: OpsStatus;
|
||
nodeId: string;
|
||
serviceName: string;
|
||
projectId?: string;
|
||
threadRef?: string;
|
||
traceId: string;
|
||
runbookId: string;
|
||
firstSeenAt: string;
|
||
lastSeenAt: string;
|
||
summary: string;
|
||
suggestedNextAction: string;
|
||
autoRepairable: boolean;
|
||
}
|
||
|
||
export interface OpsRepairTicket {
|
||
ticketId: string;
|
||
faultId: string;
|
||
title: string;
|
||
approvalStatus: ApprovalStatus;
|
||
executionStatus: ExecutionStatus;
|
||
requestedBy: string;
|
||
approvedBy?: string;
|
||
targetNodeId: string;
|
||
actionSummary: string;
|
||
resultSummary?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface OpsRepairVerification {
|
||
verificationId: string;
|
||
ticketId: string;
|
||
verifier: string;
|
||
status: VerificationStatus;
|
||
summary: string;
|
||
verifiedAt: string;
|
||
}
|
||
|
||
export interface CapabilityRequirement {
|
||
capabilityType: string;
|
||
mode: CapabilityLeaseMode;
|
||
}
|
||
|
||
export interface Capability {
|
||
capabilityId: string;
|
||
providerId: string;
|
||
nodeId: string;
|
||
capabilityType: string;
|
||
displayName: string;
|
||
status: "online" | "offline" | "busy";
|
||
healthStatus: "healthy" | "warning" | "critical";
|
||
leaseMode: CapabilityLeaseMode;
|
||
preemptible: boolean;
|
||
supportedActions: string[];
|
||
evidenceModes: string[];
|
||
}
|
||
|
||
export interface AuditTaskRequest {
|
||
protocolVersion: string;
|
||
auditRequestId: string;
|
||
projectId: string;
|
||
projectName: string;
|
||
taskId: string;
|
||
sourceThreadRef: string;
|
||
trigger: AuditTrigger;
|
||
auditType: AuditType;
|
||
priority: number;
|
||
objective: string;
|
||
systemContextSummary: string;
|
||
acceptanceCriteria: string[];
|
||
riskFocus: string[];
|
||
evidenceRefs: string[];
|
||
artifactRefs: string[];
|
||
capabilityRequirements: CapabilityRequirement[];
|
||
timeBudgetSeconds: number;
|
||
responseMode: string;
|
||
createdAt: string;
|
||
metadataJson: Record<string, unknown>;
|
||
}
|
||
|
||
export interface AuditTaskResult {
|
||
auditRequestId: string;
|
||
auditType: AuditType;
|
||
status: AuditResultStatus;
|
||
decision: AuditDecision;
|
||
confidence: number;
|
||
summary: string;
|
||
findings: string[];
|
||
requiredActions: string[];
|
||
usedCapabilities: string[];
|
||
artifactRefs: string[];
|
||
timeline: string[];
|
||
durationMs: number;
|
||
completedAt: string;
|
||
}
|
||
|
||
export interface BossState {
|
||
user: UserProfile;
|
||
devices: Device[];
|
||
projects: Project[];
|
||
verificationCodes: VerificationCode[];
|
||
verificationDispatches: VerificationDispatch[];
|
||
authAccounts: AuthAccount[];
|
||
authSessions: AuthSession[];
|
||
aiAccounts: AiAccount[];
|
||
aiAccountSwitchHistory: AiAccountSwitchRecord[];
|
||
masterAgentTasks: MasterAgentTask[];
|
||
dispatchPlans: DispatchPlan[];
|
||
dispatchExecutions: DispatchExecution[];
|
||
deviceImportDrafts: DeviceImportDraft[];
|
||
deviceImportResolutions: DeviceImportResolution[];
|
||
otaUpdates: OtaUpdate[];
|
||
otaUpdateLogs: OtaUpdateLog[];
|
||
deviceSkills: DeviceSkill[];
|
||
appLogs: AppLogEntry[];
|
||
userAttachmentStorageConfigs: UserAttachmentStorageConfig[];
|
||
masterAgentPromptPolicy: MasterAgentPromptPolicy | null;
|
||
userMasterPrompts: UserMasterPrompt[];
|
||
masterAgentMemories: MasterAgentMemory[];
|
||
userProjectAgentControls: UserProjectAgentControls[];
|
||
threadContextSnapshots: ThreadContextSnapshot[];
|
||
threadHandoffPackages: ThreadHandoffPackage[];
|
||
threadContextAlerts: ThreadContextAlert[];
|
||
deviceEnrollments: DeviceEnrollment[];
|
||
opsFaults: OpsFault[];
|
||
opsRepairTickets: OpsRepairTicket[];
|
||
opsRepairVerifications: OpsRepairVerification[];
|
||
auditRequests: AuditTaskRequest[];
|
||
auditResults: AuditTaskResult[];
|
||
capabilities: Capability[];
|
||
}
|
||
|
||
function detectRuntimeRoot(startDir: string) {
|
||
let current = startDir;
|
||
while (true) {
|
||
if (existsSync(path.join(current, "package.json")) && existsSync(path.join(current, "src", "app"))) {
|
||
return current;
|
||
}
|
||
const parent = path.dirname(current);
|
||
if (parent === current) {
|
||
return startDir;
|
||
}
|
||
current = parent;
|
||
}
|
||
}
|
||
|
||
function resolveRuntimeRoot() {
|
||
if (process.env.BOSS_RUNTIME_ROOT?.trim()) {
|
||
return path.resolve(process.env.BOSS_RUNTIME_ROOT);
|
||
}
|
||
if (process.env.BOSS_STATE_FILE?.trim()) {
|
||
return path.dirname(path.dirname(path.resolve(process.env.BOSS_STATE_FILE)));
|
||
}
|
||
return detectRuntimeRoot(/* turbopackIgnore: true */ process.cwd());
|
||
}
|
||
|
||
const runtimeRoot = resolveRuntimeRoot();
|
||
const dataFile = process.env.BOSS_STATE_FILE
|
||
? path.resolve(process.env.BOSS_STATE_FILE)
|
||
: path.join(runtimeRoot, "data", "boss-state.json");
|
||
const dataDir = path.dirname(dataFile);
|
||
const backupFile = `${dataFile}.bak`;
|
||
const publishedApkPath = path.join(runtimeRoot, "public", "downloads", "boss-android-latest.apk");
|
||
const publishedApkMetaPath = path.join(runtimeRoot, "public", "downloads", "boss-android-latest.json");
|
||
|
||
const defaultSettings: UserSettings = {
|
||
liveUpdates: true,
|
||
showRiskBadges: true,
|
||
confirmDangerousActions: true,
|
||
preferredEntryPoint: "conversations",
|
||
};
|
||
|
||
const PRIMARY_ADMIN_ACCOUNT = "17600003315";
|
||
const PRIMARY_ADMIN_PASSWORD = "boss123456";
|
||
const PRIMARY_ADMIN_VERIFICATION_EMAIL = "verify@boss.hyzq.net";
|
||
const PRIMARY_CODEX_NODE_ID = "mac-studio";
|
||
const PRIMARY_CODEX_NODE_LABEL = "本机 Codex · Mac Studio";
|
||
const VERIFICATION_SEND_COOLDOWN_MS = 60_000;
|
||
const VERIFICATION_SEND_WINDOW_MS = 15 * 60_000;
|
||
const VERIFICATION_SEND_WINDOW_LIMIT = 5;
|
||
export const AUTH_SESSION_TTL_MS = 30 * 24 * 60 * 60_000;
|
||
const AUTH_LOGIN_LOCK_THRESHOLD = 5;
|
||
const AUTH_LOGIN_LOCK_MS = 10 * 60_000;
|
||
const ENV_OPENAI_ACCOUNT_ID = "env-openai-api";
|
||
|
||
function baseThreadChecklist(labels: string[]) {
|
||
return labels;
|
||
}
|
||
|
||
const initialState: BossState = {
|
||
user: {
|
||
id: "user-boss-admin",
|
||
name: "Boss 超级管理员",
|
||
avatar: "17",
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
verificationEmail: PRIMARY_ADMIN_VERIFICATION_EMAIL,
|
||
role: "highest_admin",
|
||
roleLabel: "最高管理员",
|
||
accountType: "最高管理员 · 本机 Codex 已绑定",
|
||
qrCodeValue: `boss://user/${PRIMARY_ADMIN_ACCOUNT}`,
|
||
boundCodexNodeId: PRIMARY_CODEX_NODE_ID,
|
||
boundCodexNodeLabel: PRIMARY_CODEX_NODE_LABEL,
|
||
boundDeviceId: PRIMARY_CODEX_NODE_ID,
|
||
boundAt: "2026-03-26T09:00:00+08:00",
|
||
version: "1.4.0",
|
||
otaVersion: "v1.4.1",
|
||
otaSummary: ["新增登录会话守卫", "新增验证码防重放", "开放 APK 下载 OTA 包元数据"],
|
||
hasOta: true,
|
||
settings: defaultSettings,
|
||
},
|
||
devices: [
|
||
{
|
||
id: "mac-studio",
|
||
name: "Mac Studio",
|
||
avatar: "M",
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
source: "production",
|
||
status: "online",
|
||
projects: ["Boss 移动控制台", "硬件审计协作"],
|
||
quota5h: 68,
|
||
quota7d: 81,
|
||
lastSeenAt: "2026-03-25T11:52:00+08:00",
|
||
endpoint: "mac://kris.local",
|
||
token: "boss-mac-studio-token",
|
||
note: "本机 Codex 主节点 · 17600003315 已绑定",
|
||
},
|
||
{
|
||
id: "win-gpu-01",
|
||
name: "Windows GPU",
|
||
avatar: "W",
|
||
account: "kris.plus.gpu",
|
||
source: "demo",
|
||
status: "abnormal",
|
||
projects: ["Boss 移动控制台", "硬件审计协作"],
|
||
quota5h: 31,
|
||
quota7d: 46,
|
||
lastSeenAt: "2026-03-25T11:40:00+08:00",
|
||
endpoint: "win://gpu.local",
|
||
token: "boss-win-gpu-token",
|
||
note: "摄像头证据通道偶发抖动",
|
||
},
|
||
{
|
||
id: "cloud-backup",
|
||
name: "Cloud Backup",
|
||
avatar: "C",
|
||
account: "kris.plus.backup",
|
||
source: "demo",
|
||
status: "offline",
|
||
projects: ["Boss 移动控制台"],
|
||
quota5h: 92,
|
||
quota7d: 95,
|
||
lastSeenAt: "2026-03-25T08:15:00+08:00",
|
||
endpoint: "cloud://standby",
|
||
token: "boss-cloud-backup-token",
|
||
note: "standby 节点",
|
||
},
|
||
],
|
||
projects: [
|
||
{
|
||
id: "master-agent",
|
||
name: "主 Agent",
|
||
pinned: true,
|
||
systemPinned: true,
|
||
deviceIds: ["mac-studio"],
|
||
preview: "已汇总 3 个项目,优先收尾 Boss 移动控制台里 must_finish_before_compaction 的线程。",
|
||
updatedAt: "2026-03-25T12:06:00+08:00",
|
||
lastMessageAt: "2026-03-25T12:06:00+08:00",
|
||
isGroup: false,
|
||
threadMeta: {
|
||
projectId: "master-agent",
|
||
threadId: "thread-master-main",
|
||
threadDisplayName: "主 Agent 汇总",
|
||
folderName: "主控线程",
|
||
activityIconCount: 1,
|
||
updatedAt: "2026-03-25T12:06:00+08:00",
|
||
codexThreadRef: "thread-master-main",
|
||
codexFolderRef: "master-agent",
|
||
},
|
||
groupMembers: [],
|
||
createdByAgent: true,
|
||
collaborationMode: "development",
|
||
approvalState: "not_required",
|
||
unreadCount: 0,
|
||
riskLevel: "medium",
|
||
contextBudgetPct: 71,
|
||
contextBudgetLabel: "71%",
|
||
messages: [
|
||
{
|
||
id: "master-summary",
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: "Boss 移动控制台存在 urgent 线程待交接,硬件审计协作还有 1 条摄像头证据待复核。",
|
||
sentAt: "2026-03-25T12:06:00+08:00",
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [],
|
||
versions: [],
|
||
},
|
||
{
|
||
id: "boss-console",
|
||
name: "Boss 移动控制台",
|
||
pinned: false,
|
||
deviceIds: ["mac-studio"],
|
||
preview: "登录、设备页、线程预算与设备绑定链路正在收口到 v13。",
|
||
updatedAt: "2026-03-25T11:52:00+08:00",
|
||
lastMessageAt: "2026-03-25T11:52:00+08:00",
|
||
isGroup: false,
|
||
threadMeta: {
|
||
projectId: "boss-console",
|
||
threadId: "thread-boss-ui",
|
||
threadDisplayName: "北区试产线回归",
|
||
folderName: "归档确认",
|
||
activityIconCount: 1,
|
||
updatedAt: "2026-03-25T11:52:00+08:00",
|
||
codexThreadRef: "thread-boss-ui",
|
||
codexFolderRef: "boss-console",
|
||
},
|
||
groupMembers: [],
|
||
createdByAgent: true,
|
||
collaborationMode: "development",
|
||
approvalState: "not_required",
|
||
unreadCount: 2,
|
||
riskLevel: "medium",
|
||
contextBudgetPct: 62,
|
||
contextBudgetLabel: "62%",
|
||
messages: [
|
||
{
|
||
id: "p1",
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: "项目目标页已切成可完成、可编辑、可记录完成时间的结构。",
|
||
sentAt: "2026-03-25T11:40:00+08:00",
|
||
kind: "text",
|
||
},
|
||
{
|
||
id: "p2",
|
||
sender: "device",
|
||
senderLabel: "Mac Studio / Codex",
|
||
body: "登录、注册、忘记密码页已经补齐,并带验证码发送状态回显。",
|
||
sentAt: "2026-03-25T11:48:00+08:00",
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [
|
||
{
|
||
id: "goal-1",
|
||
text: "完成北区试产线全链路回归,覆盖串口、视觉、OTA 和容灾切换。",
|
||
state: "completed",
|
||
note: "已完成 · 09:12 由主 Agent 复核",
|
||
completedAt: "2026-03-25T09:12:00+08:00",
|
||
completedBy: "主 Agent",
|
||
},
|
||
{
|
||
id: "goal-2",
|
||
text: "所有关键步骤必须留下可交接证据,禁止仅口头确认。",
|
||
state: "pending",
|
||
note: "进行中 · 允许用户编辑,主 Agent 会同步重排任务",
|
||
},
|
||
{
|
||
id: "goal-3",
|
||
text: "当线程上下文余量进入 urgent 前,必须完成阶段摘要与 handoff。",
|
||
state: "pending",
|
||
note: "待处理 · 主 Agent 会优先把压缩前必须收尾的任务推到前面",
|
||
},
|
||
],
|
||
versions: [
|
||
{
|
||
version: "v1.3.0",
|
||
summary: "登录页改为账号密码 / 验证码双模式,并新增 OTA 版本中心与本机最高管理员绑定。",
|
||
createdAt: "2026-03-26T09:20:00+08:00",
|
||
},
|
||
{
|
||
version: "v1.2.8",
|
||
summary: "补齐认证页、线程预算接口、设备绑定草稿与运维审计摘要。",
|
||
createdAt: "2026-03-25T11:30:00+08:00",
|
||
},
|
||
{
|
||
version: "v1.2.7",
|
||
summary: "会话页、设备页、我的页切到微信式一级导航。",
|
||
createdAt: "2026-03-25T09:15:00+08:00",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "audit-collab",
|
||
name: "硬件审计协作",
|
||
pinned: false,
|
||
deviceIds: ["mac-studio", "win-gpu-01"],
|
||
preview: "Windows 端等待摄像头证据回传,总审计 Agent 正在复核。",
|
||
updatedAt: "2026-03-25T10:58:00+08:00",
|
||
lastMessageAt: "2026-03-25T10:58:00+08:00",
|
||
isGroup: true,
|
||
threadMeta: {
|
||
projectId: "audit-collab",
|
||
threadId: "thread-audit-chief",
|
||
threadDisplayName: "审计对话",
|
||
folderName: "审计群聊",
|
||
activityIconCount: 2,
|
||
updatedAt: "2026-03-25T10:58:00+08:00",
|
||
codexThreadRef: "thread-audit-chief",
|
||
codexFolderRef: "audit-collab",
|
||
},
|
||
groupMembers: [
|
||
{
|
||
projectId: "audit-collab",
|
||
deviceId: "mac-studio",
|
||
threadId: "thread-audit-chief",
|
||
threadDisplayName: "审计对话",
|
||
folderName: "审计群聊",
|
||
},
|
||
{
|
||
projectId: "audit-collab",
|
||
deviceId: "win-gpu-01",
|
||
threadId: "thread-audit-hardware",
|
||
threadDisplayName: "Windows 摄像头证据",
|
||
folderName: "审计群聊",
|
||
},
|
||
],
|
||
createdByAgent: true,
|
||
collaborationMode: "development",
|
||
approvalState: "not_required",
|
||
unreadCount: 1,
|
||
riskLevel: "high",
|
||
messages: [
|
||
{
|
||
id: "a1",
|
||
sender: "audit",
|
||
senderLabel: "审计 Agent",
|
||
body: "群聊式协作项目不在首页展示单一线程预算,详情页查看各线程风险。",
|
||
sentAt: "2026-03-25T10:58:00+08:00",
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [
|
||
{
|
||
id: "goal-a1",
|
||
text: "同步 Windows GPU 节点的摄像头证据和串口日志。",
|
||
state: "pending",
|
||
note: "待处理 · 由审计 Agent 汇总",
|
||
},
|
||
],
|
||
versions: [
|
||
{
|
||
version: "v0.9.3",
|
||
summary: "接入跨设备线程对话、摄像头证据上传和 chief audit 汇总。",
|
||
createdAt: "2026-03-24T18:10:00+08:00",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
verificationCodes: [],
|
||
verificationDispatches: [],
|
||
authAccounts: [
|
||
{
|
||
id: "account-17600003315",
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
passwordHash: hashPassword(PRIMARY_ADMIN_PASSWORD),
|
||
displayName: "Boss 超级管理员",
|
||
role: "highest_admin",
|
||
verificationEmail: PRIMARY_ADMIN_VERIFICATION_EMAIL,
|
||
codexNodeId: PRIMARY_CODEX_NODE_ID,
|
||
codexNodeLabel: PRIMARY_CODEX_NODE_LABEL,
|
||
primaryDeviceId: PRIMARY_CODEX_NODE_ID,
|
||
isPrimary: true,
|
||
createdAt: "2026-03-25T09:00:00+08:00",
|
||
updatedAt: "2026-03-26T09:00:00+08:00",
|
||
},
|
||
],
|
||
authSessions: [],
|
||
aiAccounts: [
|
||
{
|
||
accountId: "master-codex-primary",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "17600003315 · Master Codex Node",
|
||
accountIdentifier: PRIMARY_ADMIN_ACCOUNT,
|
||
nodeId: PRIMARY_CODEX_NODE_ID,
|
||
nodeLabel: PRIMARY_CODEX_NODE_LABEL,
|
||
enabled: true,
|
||
isActive: true,
|
||
status: "ready",
|
||
loginStatusNote: "已绑定本机 Codex,可通过 local-agent relay 执行主 Agent 对话。",
|
||
createdAt: "2026-03-26T09:00:00+08:00",
|
||
updatedAt: "2026-03-26T09:00:00+08:00",
|
||
lastSwitchedAt: "2026-03-26T09:00:00+08:00",
|
||
switchReason: "默认主控身份预留位",
|
||
},
|
||
{
|
||
accountId: "master-codex-backup",
|
||
label: "备用 GPT",
|
||
role: "backup",
|
||
provider: "master_codex_node",
|
||
displayName: "备用 Master Codex Node",
|
||
enabled: false,
|
||
isActive: false,
|
||
status: "disabled",
|
||
loginStatusNote: "备用节点未启用。",
|
||
createdAt: "2026-03-26T09:00:00+08:00",
|
||
updatedAt: "2026-03-26T09:00:00+08:00",
|
||
},
|
||
{
|
||
accountId: "openai-api-fallback",
|
||
label: "API 容灾",
|
||
role: "api_fallback",
|
||
provider: "openai_api",
|
||
displayName: "OpenAI API",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
isActive: false,
|
||
status: "needs_api_key",
|
||
loginStatusNote: "配置 OpenAI API Key 后,可直接为主 Agent 生成真实回复。",
|
||
createdAt: "2026-03-26T09:00:00+08:00",
|
||
updatedAt: "2026-03-26T09:00:00+08:00",
|
||
},
|
||
],
|
||
aiAccountSwitchHistory: [
|
||
{
|
||
switchId: "aiswitch-seed-primary",
|
||
toAccountId: "master-codex-primary",
|
||
toLabel: "主 GPT",
|
||
role: "primary",
|
||
switchedAt: "2026-03-26T09:00:00+08:00",
|
||
reason: "初始化默认主控身份",
|
||
},
|
||
],
|
||
userAttachmentStorageConfigs: [
|
||
{
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
mode: "server_file",
|
||
updatedAt: nowIso(),
|
||
},
|
||
],
|
||
masterAgentPromptPolicy: null,
|
||
userMasterPrompts: [],
|
||
masterAgentMemories: [],
|
||
userProjectAgentControls: [],
|
||
masterAgentTasks: [],
|
||
dispatchPlans: [],
|
||
dispatchExecutions: [],
|
||
deviceImportDrafts: [],
|
||
deviceImportResolutions: [],
|
||
otaUpdates: [
|
||
{
|
||
releaseId: "ota_140_to_141",
|
||
version: "v2.0.0",
|
||
currentVersion: "1.4.0",
|
||
channel: "stable",
|
||
packageType: "android_shell",
|
||
status: "available",
|
||
summary: ["切到原生 Android 客户端", "新增原生会话 / 设备 / 我的三栏", "登录页改为原生一键进入"],
|
||
targetScope: "Boss Android 原生客户端与 Web 控制台",
|
||
requiredRole: "highest_admin",
|
||
publishedAt: "2026-03-26T11:20:00+08:00",
|
||
packageFileName: "boss-android-latest.apk",
|
||
downloadUrl: "/api/v1/user/ota/package",
|
||
},
|
||
],
|
||
otaUpdateLogs: [
|
||
{
|
||
logId: "otalog_seed_130",
|
||
releaseId: "ota_seed_check",
|
||
version: "1.4.0",
|
||
status: "checked",
|
||
triggeredBy: "Boss 超级管理员",
|
||
triggeredAt: "2026-03-26T09:05:00+08:00",
|
||
note: "本机 Codex 已切到 17600003315,等待管理员决定是否升级到 v1.4.1。",
|
||
},
|
||
],
|
||
deviceSkills: [],
|
||
appLogs: [],
|
||
threadContextSnapshots: [
|
||
{
|
||
snapshotId: "snapshot-master-main",
|
||
projectId: "master-agent",
|
||
taskId: "task-master-summary",
|
||
threadId: "thread-master-main",
|
||
title: "主控汇总线程",
|
||
summary: "正在整理 3 个项目状态、2 条未关闭告警和 1 个待交接线程。",
|
||
nodeId: "mac-studio",
|
||
workerId: "worker-mac-master",
|
||
sourceKind: "worker_estimator",
|
||
status: "running",
|
||
contextBudgetRemainingPct: 71,
|
||
contextBudgetLevel: "safe",
|
||
compactionExpectedAt: "2026-03-25T13:20:00+08:00",
|
||
mustFinishBeforeCompaction: false,
|
||
estimatedRemainingTurns: 18,
|
||
estimatedRemainingLargeMessages: 6,
|
||
lastCompactionAt: "2026-03-25T09:48:00+08:00",
|
||
compactionCount: 1,
|
||
patchPending: false,
|
||
testsPending: false,
|
||
evidencePending: false,
|
||
checklist: baseThreadChecklist(["持续刷新阶段摘要", "复核风险排序", "回写交接文档"]),
|
||
capturedAt: "2026-03-25T12:06:00+08:00",
|
||
},
|
||
{
|
||
snapshotId: "snapshot-boss-ui",
|
||
projectId: "boss-console",
|
||
taskId: "task-ui-refine",
|
||
threadId: "thread-boss-ui",
|
||
title: "Boss UI 收口线程",
|
||
summary: "会话页、设备页和我的页已稳定,剩设备绑定与运维摘要联调。",
|
||
nodeId: "mac-studio",
|
||
workerId: "worker-mac-ui",
|
||
sourceKind: "worker_estimator",
|
||
status: "running",
|
||
contextBudgetRemainingPct: 62,
|
||
contextBudgetLevel: "watch",
|
||
compactionExpectedAt: "2026-03-25T14:05:00+08:00",
|
||
mustFinishBeforeCompaction: false,
|
||
estimatedRemainingTurns: 10,
|
||
estimatedRemainingLargeMessages: 4,
|
||
lastCompactionAt: "2026-03-25T10:28:00+08:00",
|
||
compactionCount: 1,
|
||
patchPending: true,
|
||
testsPending: true,
|
||
evidencePending: false,
|
||
checklist: baseThreadChecklist(["设备绑定页接 API", "回归 lint/build", "回写 README"]),
|
||
capturedAt: "2026-03-25T11:52:00+08:00",
|
||
},
|
||
{
|
||
snapshotId: "snapshot-boss-auth",
|
||
projectId: "boss-console",
|
||
taskId: "task-auth-delivery",
|
||
threadId: "thread-boss-auth",
|
||
title: "认证链路交接线程",
|
||
summary: "登录/注册/忘记密码已完成,但验证码回显和文档收口必须先固化。",
|
||
nodeId: "mac-studio",
|
||
workerId: "worker-mac-auth",
|
||
sourceKind: "worker_estimator",
|
||
status: "handoff_pending",
|
||
contextBudgetRemainingPct: 34,
|
||
contextBudgetLevel: "urgent",
|
||
compactionExpectedAt: "2026-03-25T12:24:00+08:00",
|
||
mustFinishBeforeCompaction: true,
|
||
estimatedRemainingTurns: 4,
|
||
estimatedRemainingLargeMessages: 1,
|
||
lastCompactionAt: "2026-03-25T11:14:00+08:00",
|
||
compactionCount: 2,
|
||
patchPending: true,
|
||
testsPending: true,
|
||
evidencePending: false,
|
||
checklist: baseThreadChecklist(["固化 patch", "记录测试结果", "生成 handoff package"]),
|
||
capturedAt: "2026-03-25T11:54:00+08:00",
|
||
},
|
||
{
|
||
snapshotId: "snapshot-audit-chief",
|
||
projectId: "audit-collab",
|
||
taskId: "task-chief-audit",
|
||
threadId: "thread-audit-chief",
|
||
title: "Chief Audit 汇总线程",
|
||
summary: "总审计正在复核硬件证据、串口日志和运维修复票据。",
|
||
nodeId: "mac-studio",
|
||
workerId: "worker-chief-audit",
|
||
sourceKind: "worker_estimator",
|
||
status: "running",
|
||
contextBudgetRemainingPct: 58,
|
||
contextBudgetLevel: "watch",
|
||
compactionExpectedAt: "2026-03-25T13:12:00+08:00",
|
||
mustFinishBeforeCompaction: false,
|
||
estimatedRemainingTurns: 9,
|
||
estimatedRemainingLargeMessages: 3,
|
||
lastCompactionAt: "2026-03-25T10:01:00+08:00",
|
||
compactionCount: 1,
|
||
patchPending: false,
|
||
testsPending: false,
|
||
evidencePending: true,
|
||
checklist: baseThreadChecklist(["等待摄像头证据", "核对串口日志", "给出最终 verdict"]),
|
||
capturedAt: "2026-03-25T10:58:00+08:00",
|
||
},
|
||
{
|
||
snapshotId: "snapshot-audit-hw",
|
||
projectId: "audit-collab",
|
||
taskId: "task-hardware-evidence",
|
||
threadId: "thread-audit-hardware",
|
||
title: "Windows 摄像头证据线程",
|
||
summary: "摄像头关键帧仍缺 1 段,必须先收尾证据再继续扩上下文。",
|
||
nodeId: "win-gpu-01",
|
||
workerId: "worker-win-audit",
|
||
sourceKind: "worker_estimator",
|
||
status: "context_urgent",
|
||
contextBudgetRemainingPct: 21,
|
||
contextBudgetLevel: "critical",
|
||
compactionExpectedAt: "2026-03-25T12:12:00+08:00",
|
||
mustFinishBeforeCompaction: true,
|
||
estimatedRemainingTurns: 2,
|
||
estimatedRemainingLargeMessages: 1,
|
||
lastCompactionAt: "2026-03-25T11:36:00+08:00",
|
||
compactionCount: 3,
|
||
patchPending: false,
|
||
testsPending: false,
|
||
evidencePending: true,
|
||
checklist: baseThreadChecklist(["保存摄像头关键帧", "补录串口摘要", "通知 chief audit"]),
|
||
capturedAt: "2026-03-25T11:58:00+08:00",
|
||
},
|
||
],
|
||
threadHandoffPackages: [
|
||
{
|
||
handoffPackageId: "handoff-boss-auth",
|
||
projectId: "boss-console",
|
||
taskId: "task-auth-delivery",
|
||
fromThreadId: "thread-boss-auth",
|
||
toThreadId: "thread-boss-auth-followup",
|
||
packageStatus: "ready",
|
||
summaryText: "认证页功能已齐,剩余文档、回归和部署验证待在新线程继续。",
|
||
openQuestions: ["验证码直回显是否继续保留在公网环境", "是否把帮助页挂到登录页右上角"],
|
||
criticalFiles: ["src/app/auth", "src/app/api/auth", "src/components/app-ui.tsx"],
|
||
criticalCommands: ["npm run lint", "npm run build", "curl -sS http://127.0.0.1:3000/api/health"],
|
||
criticalTests: ["登录验证码发送", "注册后登录", "忘记密码重置"],
|
||
criticalArtifacts: ["docs/architecture/api_and_service_inventory_cn.md"],
|
||
decisionLinks: ["boss-console:auth-mvp"],
|
||
createdAt: "2026-03-25T11:49:00+08:00",
|
||
readyAt: "2026-03-25T11:54:00+08:00",
|
||
},
|
||
],
|
||
threadContextAlerts: [
|
||
{
|
||
alertId: "alert-boss-auth",
|
||
threadId: "thread-boss-auth",
|
||
projectId: "boss-console",
|
||
alertType: "context_urgent",
|
||
alertStatus: "opened",
|
||
openedAt: "2026-03-25T11:54:00+08:00",
|
||
summary: "认证链路线程已进入 urgent,必须优先固化 patch、测试结论和 handoff。",
|
||
masterActions: ["prepare_handoff", "avoid_large_context_append", "finalize_artifacts"],
|
||
},
|
||
{
|
||
alertId: "alert-audit-hw",
|
||
threadId: "thread-audit-hardware",
|
||
projectId: "audit-collab",
|
||
alertType: "context_critical",
|
||
alertStatus: "opened",
|
||
openedAt: "2026-03-25T11:58:00+08:00",
|
||
summary: "Windows 摄像头证据线程进入 critical,任何新增背景信息前先收尾证据。",
|
||
masterActions: ["complete_wrapup", "handoff_required", "freeze_context_growth"],
|
||
},
|
||
],
|
||
deviceEnrollments: [
|
||
{
|
||
enrollmentId: "enroll-cloud-backup",
|
||
deviceId: "cloud-backup",
|
||
label: "Cloud Backup",
|
||
pairingCode: "482913",
|
||
token: "boss-cloud-backup-token",
|
||
status: "ready",
|
||
note: "standby 节点待重新连接",
|
||
createdAt: "2026-03-25T10:40:00+08:00",
|
||
expiresAt: "2026-03-25T18:40:00+08:00",
|
||
},
|
||
],
|
||
opsFaults: [
|
||
{
|
||
faultId: "fault-win-camera",
|
||
faultKey: "OPS.LOCAL_AGENT.CAMERA_EVIDENCE.DELAYED",
|
||
severity: "warning",
|
||
status: "repairing",
|
||
nodeId: "win-gpu-01",
|
||
serviceName: "boss-local-agent",
|
||
projectId: "audit-collab",
|
||
threadRef: "thread-audit-hardware",
|
||
traceId: "trace-audit-001",
|
||
runbookId: "runbook-camera-sync",
|
||
firstSeenAt: "2026-03-25T11:34:00+08:00",
|
||
lastSeenAt: "2026-03-25T11:58:00+08:00",
|
||
summary: "Windows 节点摄像头证据回传延迟,chief audit 正在等待关键帧。",
|
||
suggestedNextAction: "优先重试本地 agent 证据上传,再做审计复验。",
|
||
autoRepairable: true,
|
||
},
|
||
{
|
||
faultId: "fault-context-auth",
|
||
faultKey: "THREAD.CONTEXT.HANDOFF.REQUIRED",
|
||
severity: "critical",
|
||
status: "opened",
|
||
nodeId: "mac-studio",
|
||
serviceName: "boss-web",
|
||
projectId: "boss-console",
|
||
threadRef: "thread-boss-auth",
|
||
traceId: "trace-boss-auth-002",
|
||
runbookId: "runbook-thread-handoff",
|
||
firstSeenAt: "2026-03-25T11:50:00+08:00",
|
||
lastSeenAt: "2026-03-25T11:54:00+08:00",
|
||
summary: "认证线程预算降到 urgent,未完成 handoff 前不应继续追加长上下文。",
|
||
suggestedNextAction: "执行压缩前收尾,写明关键文件、命令和测试结果。",
|
||
autoRepairable: false,
|
||
},
|
||
],
|
||
opsRepairTickets: [
|
||
{
|
||
ticketId: "ticket-win-camera",
|
||
faultId: "fault-win-camera",
|
||
title: "重试 Windows 摄像头证据上传",
|
||
approvalStatus: "approved",
|
||
executionStatus: "running",
|
||
requestedBy: "运维 Agent",
|
||
approvedBy: "主 Agent",
|
||
targetNodeId: "win-gpu-01",
|
||
actionSummary: "重新触发本地 agent 心跳和摄像头证据上传。",
|
||
resultSummary: "等待运维审计 Agent 复验中。",
|
||
createdAt: "2026-03-25T11:42:00+08:00",
|
||
updatedAt: "2026-03-25T11:57:00+08:00",
|
||
},
|
||
],
|
||
opsRepairVerifications: [
|
||
{
|
||
verificationId: "verify-win-camera",
|
||
ticketId: "ticket-win-camera",
|
||
verifier: "运维审计 Agent",
|
||
status: "watching",
|
||
summary: "主控在线,已允许修复动作,但仍需等待关键帧落库后才能关闭工单。",
|
||
verifiedAt: "2026-03-25T11:58:00+08:00",
|
||
},
|
||
],
|
||
auditRequests: [
|
||
{
|
||
protocolVersion: "1.0",
|
||
auditRequestId: "auditreq_001",
|
||
projectId: "audit-collab",
|
||
projectName: "硬件审计协作",
|
||
taskId: "task-hardware-evidence",
|
||
sourceThreadRef: "win-gpu-01:thread-audit-hardware",
|
||
trigger: "failure_escalation",
|
||
auditType: "multimodal",
|
||
priority: 75,
|
||
objective: "验证设备唤醒流程里的屏幕、LED、串口和摄像头证据是否一致。",
|
||
systemContextSummary: "设备端已刷入固件 v0.9.2,当前 chief audit 等待 Windows 节点关键帧。",
|
||
acceptanceCriteria: [
|
||
"LED 在 2 秒内变蓝",
|
||
"屏幕切换到 listening 状态",
|
||
"串口输出唤醒完成",
|
||
"摄像头观测到机械动作完成",
|
||
],
|
||
riskFocus: ["摄像头关键帧缺失", "串口时间线不一致"],
|
||
evidenceRefs: ["evidence://serial-log/20260325-1"],
|
||
artifactRefs: [],
|
||
capabilityRequirements: [
|
||
{ capabilityType: "camera", mode: "exclusive" },
|
||
{ capabilityType: "serial_port", mode: "exclusive" },
|
||
],
|
||
timeBudgetSeconds: 300,
|
||
responseMode: "structured_json",
|
||
createdAt: "2026-03-25T11:36:00+08:00",
|
||
metadataJson: { requestedBy: "chief-audit" },
|
||
},
|
||
],
|
||
auditResults: [
|
||
{
|
||
auditRequestId: "auditreq_001",
|
||
auditType: "multimodal",
|
||
status: "needs_human_review",
|
||
decision: "warning",
|
||
confidence: 0.72,
|
||
summary: "串口链路通过,但摄像头关键帧仍缺最后一段,暂不允许关闭工单。",
|
||
findings: ["LED 与串口时间线一致", "摄像头关键帧缺失最后 2 秒"],
|
||
requiredActions: ["补录关键帧", "回放 chief audit 证据摘要"],
|
||
usedCapabilities: ["camera", "serial_port"],
|
||
artifactRefs: ["artifact://camera/keyframe-20260325-1"],
|
||
timeline: ["11:36 接单", "11:44 拉串口", "11:58 等待摄像头关键帧"],
|
||
durationMs: 1_320_000,
|
||
completedAt: "2026-03-25T11:58:00+08:00",
|
||
},
|
||
],
|
||
capabilities: [
|
||
{
|
||
capabilityId: "cap-camera-win",
|
||
providerId: "provider-win-gpu",
|
||
nodeId: "win-gpu-01",
|
||
capabilityType: "camera",
|
||
displayName: "Windows 审计摄像头",
|
||
status: "busy",
|
||
healthStatus: "warning",
|
||
leaseMode: "exclusive",
|
||
preemptible: true,
|
||
supportedActions: ["snapshot", "record_clip", "start_stream", "stop_stream"],
|
||
evidenceModes: ["image", "video_clip"],
|
||
},
|
||
{
|
||
capabilityId: "cap-serial-win",
|
||
providerId: "provider-win-gpu",
|
||
nodeId: "win-gpu-01",
|
||
capabilityType: "serial_port",
|
||
displayName: "串口日志采集",
|
||
status: "online",
|
||
healthStatus: "healthy",
|
||
leaseMode: "exclusive",
|
||
preemptible: false,
|
||
supportedActions: ["open", "read", "write", "capture_log"],
|
||
evidenceModes: ["serial_log"],
|
||
},
|
||
],
|
||
};
|
||
|
||
const levelPriority: Record<ContextBudgetLevel, number> = {
|
||
critical: 0,
|
||
urgent: 1,
|
||
watch: 2,
|
||
safe: 3,
|
||
};
|
||
|
||
function cloneInitialState() {
|
||
return JSON.parse(JSON.stringify(initialState)) as BossState;
|
||
}
|
||
|
||
function nowIso() {
|
||
return new Date().toISOString();
|
||
}
|
||
|
||
function normalizeThreadMeta(
|
||
raw: Partial<ThreadConversationMeta> | undefined,
|
||
project: { id: string; name: string; isGroup: boolean; updatedAt: string },
|
||
fallback?: ThreadConversationMeta,
|
||
): ThreadConversationMeta {
|
||
return {
|
||
projectId: raw?.projectId ?? project.id,
|
||
threadId: raw?.threadId ?? `thread-${project.id}`,
|
||
threadDisplayName: raw?.threadDisplayName ?? project.name,
|
||
folderName: raw?.folderName ?? fallback?.folderName ?? (project.isGroup ? "群聊" : project.name),
|
||
activityIconCount: Math.max(1, raw?.activityIconCount ?? fallback?.activityIconCount ?? (project.isGroup ? 2 : 1)),
|
||
updatedAt: raw?.updatedAt ?? project.updatedAt ?? nowIso(),
|
||
codexThreadRef: raw?.codexThreadRef,
|
||
codexFolderRef: raw?.codexFolderRef,
|
||
};
|
||
}
|
||
|
||
function normalizeGroupMember(
|
||
raw: Partial<GroupConversationMember>,
|
||
fallbackProjectId: string,
|
||
fallbackThreadMeta: ThreadConversationMeta,
|
||
): GroupConversationMember {
|
||
return {
|
||
projectId: raw.projectId ?? fallbackProjectId,
|
||
deviceId: raw.deviceId ?? "",
|
||
threadId: raw.threadId ?? fallbackThreadMeta.threadId,
|
||
threadDisplayName: raw.threadDisplayName ?? fallbackThreadMeta.threadDisplayName,
|
||
folderName: raw.folderName ?? fallbackThreadMeta.folderName,
|
||
};
|
||
}
|
||
|
||
function trimToDefined(value?: string) {
|
||
const trimmed = value?.trim();
|
||
return trimmed ? trimmed : undefined;
|
||
}
|
||
|
||
function parseControlTextOverride(value: unknown) {
|
||
if (value === undefined || value === null) {
|
||
return { kind: "clear" as const };
|
||
}
|
||
if (typeof value !== "string") {
|
||
return { kind: "invalid" as const };
|
||
}
|
||
const trimmed = value.trim();
|
||
return trimmed ? { kind: "set" as const, value: trimmed } : { kind: "clear" as const };
|
||
}
|
||
|
||
function parseReasoningEffortOverride(value: unknown) {
|
||
if (value === undefined || value === null) {
|
||
return { kind: "clear" as const };
|
||
}
|
||
if (!isReasoningEffort(value)) {
|
||
return { kind: "invalid" as const };
|
||
}
|
||
return { kind: "set" as const, value };
|
||
}
|
||
|
||
function normalizeStringSet(values: string[]) {
|
||
return dedupeStrings(values.map((value) => value.trim()).filter(Boolean)).sort((a, b) => a.localeCompare(b));
|
||
}
|
||
|
||
function sameStringSet(a: string[] | undefined, b: string[] | undefined) {
|
||
const left = normalizeStringSet(a ?? []);
|
||
const right = normalizeStringSet(b ?? []);
|
||
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||
}
|
||
|
||
function dispatchPlanTargetSignature(target: DispatchPlanTarget) {
|
||
return [
|
||
target.projectId,
|
||
target.deviceId,
|
||
target.threadId,
|
||
target.threadDisplayName,
|
||
target.folderName,
|
||
target.codexFolderRef ?? "",
|
||
target.codexThreadRef ?? "",
|
||
target.reason,
|
||
].join("\u001f");
|
||
}
|
||
|
||
function sameDispatchPlanTargets(a: DispatchPlanTarget[], b: DispatchPlanTarget[]) {
|
||
return sameStringSet(
|
||
a.map((target) => dispatchPlanTargetSignature(target)),
|
||
b.map((target) => dispatchPlanTargetSignature(target)),
|
||
);
|
||
}
|
||
|
||
function normalizeDispatchPlanTargetsForCreate(
|
||
state: BossState,
|
||
targets: DispatchPlanTarget[],
|
||
) {
|
||
if (targets.length === 0) {
|
||
throw new Error("DISPATCH_PLAN_TARGETS_REQUIRED");
|
||
}
|
||
|
||
const validatedTargets = targets.map((target) =>
|
||
validateDispatchTargetAgainstState(state, normalizedDispatchPlanTarget(target, undefined, { allowInvalid: false }) as DispatchPlanTarget),
|
||
);
|
||
|
||
const uniqueProjectIds = normalizeStringSet(validatedTargets.map((target) => target.projectId));
|
||
if (uniqueProjectIds.length !== validatedTargets.length) {
|
||
throw new Error("DISPATCH_PLAN_TARGET_DUPLICATE");
|
||
}
|
||
|
||
return validatedTargets;
|
||
}
|
||
|
||
function normalizedDispatchPlanTarget(
|
||
raw: Partial<DispatchPlanTarget>,
|
||
fallback?: DispatchPlanTarget,
|
||
options?: { allowInvalid?: boolean },
|
||
): DispatchPlanTarget | null {
|
||
const deviceId = trimToDefined(raw.deviceId ?? fallback?.deviceId);
|
||
const projectId = trimToDefined(raw.projectId ?? fallback?.projectId);
|
||
const threadId = trimToDefined(raw.threadId ?? fallback?.threadId);
|
||
const threadDisplayName = trimToDefined(raw.threadDisplayName ?? fallback?.threadDisplayName);
|
||
const folderName = trimToDefined(raw.folderName ?? fallback?.folderName);
|
||
const codexFolderRef = trimToDefined(raw.codexFolderRef ?? fallback?.codexFolderRef);
|
||
const codexThreadRef = trimToDefined(raw.codexThreadRef ?? fallback?.codexThreadRef);
|
||
const reason = trimToDefined(raw.reason ?? fallback?.reason);
|
||
|
||
if (
|
||
!deviceId ||
|
||
!projectId ||
|
||
!threadId ||
|
||
!threadDisplayName ||
|
||
!folderName ||
|
||
!reason
|
||
) {
|
||
if (options?.allowInvalid) return null;
|
||
throw new Error("DISPATCH_PLAN_TARGET_INVALID");
|
||
}
|
||
|
||
return {
|
||
deviceId,
|
||
projectId,
|
||
threadId,
|
||
threadDisplayName,
|
||
folderName,
|
||
codexFolderRef,
|
||
codexThreadRef,
|
||
reason,
|
||
};
|
||
}
|
||
|
||
function activeSessionForAccount(state: BossState, account: string) {
|
||
return (
|
||
state.authSessions.find(
|
||
(session) =>
|
||
session.account === account &&
|
||
!session.revokedAt &&
|
||
new Date(session.expiresAt).getTime() > Date.now(),
|
||
) ?? null
|
||
);
|
||
}
|
||
|
||
function requireDispatchActorSession(state: BossState, account: string) {
|
||
const normalizedAccount = account.trim();
|
||
if (!normalizedAccount) {
|
||
throw new Error("DISPATCH_ACTOR_ACCOUNT_REQUIRED");
|
||
}
|
||
const authAccount = state.authAccounts.find((item) => item.account === normalizedAccount);
|
||
if (!authAccount) {
|
||
throw new Error("DISPATCH_ACTOR_ACCOUNT_NOT_FOUND");
|
||
}
|
||
const session = activeSessionForAccount(state, normalizedAccount);
|
||
if (!session) {
|
||
throw new Error("DISPATCH_ACTOR_SESSION_REQUIRED");
|
||
}
|
||
return { authAccount, session };
|
||
}
|
||
|
||
function validateDispatchTargetAgainstState(
|
||
state: BossState,
|
||
target: DispatchPlanTarget,
|
||
): DispatchPlanTarget {
|
||
const project = state.projects.find((item) => item.id === target.projectId);
|
||
if (!project) {
|
||
throw new Error("DISPATCH_TARGET_PROJECT_NOT_FOUND");
|
||
}
|
||
|
||
const device = state.devices.find((item) => item.id === target.deviceId);
|
||
if (!device || device.source !== "production") {
|
||
throw new Error("DISPATCH_TARGET_DEVICE_INVALID");
|
||
}
|
||
|
||
if (!project.deviceIds.includes(device.id)) {
|
||
throw new Error("DISPATCH_TARGET_DEVICE_PROJECT_MISMATCH");
|
||
}
|
||
|
||
const matchingGroupMember = project.groupMembers.find(
|
||
(member) => member.deviceId === device.id && member.threadId === target.threadId,
|
||
);
|
||
if (project.isGroup) {
|
||
if (!matchingGroupMember) {
|
||
throw new Error("DISPATCH_TARGET_THREAD_MISMATCH");
|
||
}
|
||
if (matchingGroupMember.threadDisplayName !== target.threadDisplayName) {
|
||
throw new Error("DISPATCH_TARGET_THREAD_DISPLAY_NAME_MISMATCH");
|
||
}
|
||
if (matchingGroupMember.folderName !== target.folderName) {
|
||
throw new Error("DISPATCH_TARGET_FOLDER_NAME_MISMATCH");
|
||
}
|
||
} else {
|
||
if (project.threadMeta.threadId !== target.threadId) {
|
||
throw new Error("DISPATCH_TARGET_THREAD_MISMATCH");
|
||
}
|
||
if (project.threadMeta.threadDisplayName !== target.threadDisplayName) {
|
||
throw new Error("DISPATCH_TARGET_THREAD_DISPLAY_NAME_MISMATCH");
|
||
}
|
||
if (project.threadMeta.folderName !== target.folderName) {
|
||
throw new Error("DISPATCH_TARGET_FOLDER_NAME_MISMATCH");
|
||
}
|
||
}
|
||
|
||
if (target.codexThreadRef && project.threadMeta.codexThreadRef && target.codexThreadRef !== project.threadMeta.codexThreadRef) {
|
||
throw new Error("DISPATCH_TARGET_CODEX_THREAD_MISMATCH");
|
||
}
|
||
if (target.codexFolderRef && project.threadMeta.codexFolderRef && target.codexFolderRef !== project.threadMeta.codexFolderRef) {
|
||
throw new Error("DISPATCH_TARGET_CODEX_FOLDER_MISMATCH");
|
||
}
|
||
|
||
return target;
|
||
}
|
||
|
||
function normalizeDispatchPlan(raw: Partial<DispatchPlan>, fallback?: DispatchPlan): DispatchPlan {
|
||
const fallbackTargets = fallback?.targets ?? [];
|
||
const targets = ensureArray(raw.targets as Partial<DispatchPlanTarget>[] | undefined, fallbackTargets)
|
||
.map((target, index) =>
|
||
normalizedDispatchPlanTarget(
|
||
target,
|
||
fallbackTargets[index % Math.max(1, fallbackTargets.length)],
|
||
{ allowInvalid: true },
|
||
),
|
||
)
|
||
.filter((target): target is DispatchPlanTarget => Boolean(target));
|
||
return {
|
||
planId: raw.planId ?? fallback?.planId ?? randomToken("dispatch-plan"),
|
||
groupProjectId: raw.groupProjectId ?? fallback?.groupProjectId ?? "",
|
||
requestMessageId: raw.requestMessageId ?? fallback?.requestMessageId ?? "",
|
||
requestedBy: raw.requestedBy ?? fallback?.requestedBy ?? "",
|
||
status: raw.status ?? fallback?.status ?? "pending_user_confirmation",
|
||
targets,
|
||
summary: raw.summary ?? fallback?.summary ?? "",
|
||
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
|
||
confirmedAt: raw.confirmedAt ?? fallback?.confirmedAt,
|
||
confirmedBy: raw.confirmedBy ?? fallback?.confirmedBy,
|
||
confirmedTargetProjectIds: (() => {
|
||
const values = normalizeStringSet(
|
||
ensureArray(raw.confirmedTargetProjectIds, fallback?.confirmedTargetProjectIds ?? []),
|
||
);
|
||
return values.length > 0 ? values : undefined;
|
||
})(),
|
||
};
|
||
}
|
||
|
||
function normalizeDispatchExecution(
|
||
raw: Partial<DispatchExecution>,
|
||
fallback?: DispatchExecution,
|
||
): DispatchExecution {
|
||
return {
|
||
executionId: raw.executionId ?? fallback?.executionId ?? randomToken("dispatch-exec"),
|
||
planId: raw.planId ?? fallback?.planId ?? "",
|
||
groupProjectId: raw.groupProjectId ?? fallback?.groupProjectId ?? "",
|
||
targetProjectId: raw.targetProjectId ?? fallback?.targetProjectId ?? "",
|
||
targetThreadId: raw.targetThreadId ?? fallback?.targetThreadId ?? "",
|
||
deviceId: raw.deviceId ?? fallback?.deviceId ?? "",
|
||
status: raw.status ?? fallback?.status ?? "queued",
|
||
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
|
||
completedAt: raw.completedAt ?? fallback?.completedAt,
|
||
resultMessageId: raw.resultMessageId ?? fallback?.resultMessageId,
|
||
completedByDeviceId: raw.completedByDeviceId ?? fallback?.completedByDeviceId,
|
||
};
|
||
}
|
||
|
||
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 ?? []),
|
||
),
|
||
appliedProjectNames: dedupeStrings(
|
||
ensureArray(raw.appliedProjectNames, fallback?.appliedProjectNames ?? []),
|
||
),
|
||
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)))];
|
||
}
|
||
|
||
function dedupeGroupMembers(members: GroupConversationMember[]) {
|
||
const seen = new Set<string>();
|
||
const deduped: GroupConversationMember[] = [];
|
||
for (const member of members) {
|
||
const key = `${member.projectId}:${member.deviceId}:${member.threadId}`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
deduped.push(member);
|
||
}
|
||
return deduped;
|
||
}
|
||
|
||
function buildLegacyGroupMembers(
|
||
projectId: string,
|
||
deviceIds: string[],
|
||
threadMeta: ThreadConversationMeta,
|
||
) {
|
||
return dedupeStrings(deviceIds).map((deviceId, index) => ({
|
||
projectId,
|
||
deviceId,
|
||
threadId:
|
||
index === 0 ? threadMeta.threadId : `${threadMeta.threadId}:${slugify(deviceId)}`,
|
||
threadDisplayName: threadMeta.threadDisplayName,
|
||
folderName: threadMeta.folderName,
|
||
}));
|
||
}
|
||
|
||
function normalizeProjectConversationShape(
|
||
project: Project,
|
||
options?: {
|
||
allowedDeviceIds?: Set<string>;
|
||
},
|
||
) {
|
||
const allowedDeviceIds = options?.allowedDeviceIds;
|
||
const normalizedThreadMeta = {
|
||
...project.threadMeta,
|
||
projectId: project.id,
|
||
};
|
||
const normalizedExplicitMembers = dedupeGroupMembers(
|
||
project.groupMembers.map((member) =>
|
||
normalizeGroupMember(member, project.id, normalizedThreadMeta),
|
||
),
|
||
);
|
||
const hasExplicitGroupMembers = normalizedExplicitMembers.length > 0;
|
||
const legacyGroupRequested = !hasExplicitGroupMembers && project.isGroup;
|
||
const resolvedGroupMembers = hasExplicitGroupMembers
|
||
? normalizedExplicitMembers
|
||
: legacyGroupRequested
|
||
? buildLegacyGroupMembers(project.id, project.deviceIds, normalizedThreadMeta)
|
||
: [];
|
||
const filteredGroupMembers = allowedDeviceIds
|
||
? resolvedGroupMembers.filter((member) => allowedDeviceIds.has(member.deviceId))
|
||
: resolvedGroupMembers;
|
||
|
||
if (filteredGroupMembers.length > 0) {
|
||
project.isGroup = true;
|
||
project.groupMembers = dedupeGroupMembers(filteredGroupMembers);
|
||
project.deviceIds = dedupeStrings(project.groupMembers.map((member) => member.deviceId));
|
||
project.threadMeta = {
|
||
...normalizedThreadMeta,
|
||
activityIconCount: Math.max(1, project.groupMembers.length),
|
||
};
|
||
return project;
|
||
}
|
||
|
||
project.isGroup = false;
|
||
project.groupMembers = [];
|
||
project.deviceIds = allowedDeviceIds
|
||
? project.deviceIds.filter((deviceId) => allowedDeviceIds.has(deviceId))
|
||
: project.deviceIds;
|
||
project.threadMeta = {
|
||
...normalizedThreadMeta,
|
||
activityIconCount: Math.max(1, normalizedThreadMeta.activityIconCount ?? 1),
|
||
};
|
||
return project;
|
||
}
|
||
|
||
function normalizeProjectAgentControls(
|
||
raw: Partial<ProjectAgentControls> | undefined,
|
||
): ProjectAgentControls | undefined {
|
||
const modelOverride = trimToDefined(raw?.modelOverride);
|
||
const reasoningEffortOverride = isReasoningEffort(raw?.reasoningEffortOverride)
|
||
? raw.reasoningEffortOverride
|
||
: undefined;
|
||
const promptOverride = trimToDefined(raw?.promptOverride);
|
||
|
||
if (!modelOverride && !reasoningEffortOverride && !promptOverride) {
|
||
return undefined;
|
||
}
|
||
|
||
return {
|
||
modelOverride,
|
||
reasoningEffortOverride,
|
||
promptOverride,
|
||
updatedAt: raw?.updatedAt ?? nowIso(),
|
||
};
|
||
}
|
||
|
||
function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
||
return value === "low" || value === "medium" || value === "high";
|
||
}
|
||
|
||
function resolveProjectUpdatedAt(project: Pick<Project, "updatedAt" | "lastMessageAt" | "threadMeta">, latestActivityAt?: string) {
|
||
return latestIsoTimestamp(
|
||
project.updatedAt,
|
||
project.lastMessageAt,
|
||
project.threadMeta.updatedAt,
|
||
latestActivityAt,
|
||
);
|
||
}
|
||
|
||
function latestIsoTimestamp(...values: Array<string | undefined>) {
|
||
let latestValue: string | undefined;
|
||
let latestTime = 0;
|
||
for (const value of values) {
|
||
const valueTime = messageTimeValue(value);
|
||
if (valueTime > latestTime) {
|
||
latestTime = valueTime;
|
||
latestValue = value;
|
||
}
|
||
}
|
||
return latestValue ?? nowIso();
|
||
}
|
||
|
||
function ensureArray<T>(value: T[] | undefined, fallback: T[]) {
|
||
return Array.isArray(value) ? value : fallback;
|
||
}
|
||
|
||
function deriveLevelFromPercent(percent: number): ContextBudgetLevel {
|
||
if (percent < 25) return "critical";
|
||
if (percent < 40) return "urgent";
|
||
if (percent < 60) return "watch";
|
||
return "safe";
|
||
}
|
||
|
||
function deriveRiskFromSnapshots(snapshots: ThreadContextSnapshot[]) {
|
||
const top = [...snapshots].sort(compareSnapshotsForRisk)[0];
|
||
if (!top) return "low" as RiskLevel;
|
||
if (top.contextBudgetLevel === "critical") return "high" as RiskLevel;
|
||
if (top.contextBudgetLevel === "urgent" || top.mustFinishBeforeCompaction) {
|
||
return "high" as RiskLevel;
|
||
}
|
||
if (top.contextBudgetLevel === "watch") return "medium" as RiskLevel;
|
||
return "low" as RiskLevel;
|
||
}
|
||
|
||
function compareSnapshotsForRisk(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];
|
||
}
|
||
if ((a.compactionExpectedAt ?? "") !== (b.compactionExpectedAt ?? "")) {
|
||
return (a.compactionExpectedAt ?? "").localeCompare(b.compactionExpectedAt ?? "");
|
||
}
|
||
return (b.capturedAt ?? "").localeCompare(a.capturedAt ?? "");
|
||
}
|
||
|
||
function messageTimeValue(value?: string) {
|
||
if (!value) return 0;
|
||
const parsed = Date.parse(value);
|
||
return Number.isNaN(parsed) ? 0 : parsed;
|
||
}
|
||
|
||
function randomToken(prefix: string) {
|
||
return `${prefix}-${randomBytes(4).toString("hex")}`;
|
||
}
|
||
|
||
function randomDigits(length: number) {
|
||
return Array.from({ length }, () => Math.floor(Math.random() * 10)).join("");
|
||
}
|
||
|
||
function slugify(value: string) {
|
||
return value
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/gi, "-")
|
||
.replace(/^-+|-+$/g, "")
|
||
.slice(0, 48) || `item-${randomBytes(2).toString("hex")}`;
|
||
}
|
||
|
||
const aiRolePriority: Record<AiAccountRole, number> = {
|
||
primary: 0,
|
||
backup: 1,
|
||
api_fallback: 2,
|
||
};
|
||
|
||
export function aiRoleLabel(role: AiAccountRole) {
|
||
switch (role) {
|
||
case "primary":
|
||
return "主 GPT";
|
||
case "backup":
|
||
return "备用 GPT";
|
||
case "api_fallback":
|
||
return "API 容灾";
|
||
default:
|
||
return role;
|
||
}
|
||
}
|
||
|
||
export function aiProviderLabel(provider: AiProvider) {
|
||
switch (provider) {
|
||
case "master_codex_node":
|
||
return "Master Codex Node / ChatGPT Plus 节点";
|
||
case "openai_api":
|
||
return "OpenAI API";
|
||
case "aliyun_qwen_api":
|
||
return "阿里百炼 Qwen";
|
||
default:
|
||
return provider;
|
||
}
|
||
}
|
||
|
||
export 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 maskApiKey(value?: string) {
|
||
if (!value?.trim()) return undefined;
|
||
const trimmed = value.trim();
|
||
if (trimmed.length <= 8) return "已配置";
|
||
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
|
||
}
|
||
|
||
function isApiKeyProvider(provider: AiProvider) {
|
||
return provider === "openai_api" || provider === "aliyun_qwen_api";
|
||
}
|
||
|
||
function deriveAiAccountStatus(account: AiAccount): AiAccountStatus {
|
||
if (!account.enabled) return "disabled";
|
||
if (isApiKeyProvider(account.provider)) {
|
||
if (!account.apiKey?.trim()) return "needs_api_key";
|
||
return account.status === "disabled" ? "ready" : account.status;
|
||
}
|
||
return account.status === "disabled" ? "needs_login" : account.status;
|
||
}
|
||
|
||
function aiAccountCanGenerate(account: AiAccount) {
|
||
if (!account.enabled) return false;
|
||
if (account.provider === "master_codex_node") {
|
||
return Boolean(account.nodeId?.trim());
|
||
}
|
||
return Boolean(account.apiKey?.trim());
|
||
}
|
||
|
||
function sortAiAccounts(accounts: AiAccount[]) {
|
||
return [...accounts].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 ?? "");
|
||
});
|
||
}
|
||
|
||
function normalizeAiAccount(account: AiAccount) {
|
||
const apiKeyMasked = account.apiKeyMasked ?? maskApiKey(account.apiKey);
|
||
const status = deriveAiAccountStatus({
|
||
...account,
|
||
apiKeyMasked,
|
||
});
|
||
return {
|
||
...account,
|
||
apiKeyMasked,
|
||
status,
|
||
} satisfies AiAccount;
|
||
}
|
||
|
||
function buildAiAccountSummary(account: AiAccount, options?: { isEnvironmentFallback?: boolean }) {
|
||
const normalized = normalizeAiAccount(account);
|
||
return {
|
||
accountId: normalized.accountId,
|
||
label: normalized.label,
|
||
role: normalized.role,
|
||
roleLabel: aiRoleLabel(normalized.role),
|
||
provider: normalized.provider,
|
||
providerLabel: aiProviderLabel(normalized.provider),
|
||
displayName: normalized.displayName,
|
||
accountIdentifier: normalized.accountIdentifier,
|
||
nodeId: normalized.nodeId,
|
||
nodeLabel: normalized.nodeLabel,
|
||
model: normalized.model,
|
||
enabled: normalized.enabled,
|
||
isActive: normalized.isActive,
|
||
canGenerate: aiAccountCanGenerate(normalized),
|
||
status: normalized.status,
|
||
statusLabel: aiStatusLabel(normalized.status),
|
||
loginStatusNote: normalized.loginStatusNote,
|
||
apiKeyConfigured: Boolean(normalized.apiKey?.trim()),
|
||
apiKeyMasked: normalized.apiKeyMasked,
|
||
lastValidatedAt: normalized.lastValidatedAt,
|
||
lastUsedAt: normalized.lastUsedAt,
|
||
lastError: normalized.lastError,
|
||
lastSwitchedAt: normalized.lastSwitchedAt,
|
||
switchReason: normalized.switchReason,
|
||
createdAt: normalized.createdAt,
|
||
updatedAt: normalized.updatedAt,
|
||
isEnvironmentFallback: options?.isEnvironmentFallback ?? false,
|
||
} satisfies AiAccountSummary;
|
||
}
|
||
|
||
function getEnvOpenAiAccount() {
|
||
const apiKey = process.env.OPENAI_API_KEY?.trim();
|
||
if (!apiKey) return null;
|
||
const createdAt = nowIso();
|
||
return normalizeAiAccount({
|
||
accountId: ENV_OPENAI_ACCOUNT_ID,
|
||
label: "API 容灾",
|
||
role: "api_fallback",
|
||
provider: "openai_api",
|
||
displayName: "环境变量 OpenAI API",
|
||
model: process.env.OPENAI_MODEL?.trim() || "gpt-5.4",
|
||
apiKey,
|
||
apiKeyMasked: maskApiKey(apiKey),
|
||
enabled: true,
|
||
isActive: false,
|
||
status: "ready",
|
||
loginStatusNote: "来自服务器环境变量 OPENAI_API_KEY。",
|
||
createdAt,
|
||
updatedAt: createdAt,
|
||
} satisfies AiAccount);
|
||
}
|
||
|
||
function resolveActiveAiAccount(state: BossState) {
|
||
const normalized = sortAiAccounts(state.aiAccounts.map(normalizeAiAccount));
|
||
const activeConfigured = normalized.find((account) => account.isActive);
|
||
const runnable = normalized.find(aiAccountCanGenerate);
|
||
const envAccount = getEnvOpenAiAccount();
|
||
|
||
if (activeConfigured && activeConfigured.enabled) {
|
||
return { account: activeConfigured, isEnvironmentFallback: false };
|
||
}
|
||
|
||
if (runnable) {
|
||
return { account: runnable, isEnvironmentFallback: false };
|
||
}
|
||
|
||
if (envAccount) {
|
||
return { account: envAccount, isEnvironmentFallback: true };
|
||
}
|
||
|
||
return {
|
||
account: activeConfigured ?? normalized[0] ?? envAccount ?? null,
|
||
isEnvironmentFallback: false,
|
||
};
|
||
}
|
||
|
||
export function getAiAccountSummariesFromState(state: BossState) {
|
||
const accounts = sortAiAccounts(state.aiAccounts.map(normalizeAiAccount)).map((account) =>
|
||
buildAiAccountSummary(account),
|
||
);
|
||
const envAccount = getEnvOpenAiAccount();
|
||
if (envAccount) {
|
||
accounts.push(buildAiAccountSummary(envAccount, { isEnvironmentFallback: true }));
|
||
}
|
||
return sortAiAccounts(
|
||
accounts.map((account) => ({
|
||
accountId: account.accountId,
|
||
label: account.label,
|
||
role: account.role,
|
||
provider: account.provider,
|
||
displayName: account.displayName,
|
||
accountIdentifier: account.accountIdentifier,
|
||
nodeId: account.nodeId,
|
||
nodeLabel: account.nodeLabel,
|
||
model: account.model,
|
||
enabled: account.enabled,
|
||
isActive: account.isActive,
|
||
status: account.status,
|
||
loginStatusNote: account.loginStatusNote,
|
||
apiKeyMasked: account.apiKeyMasked,
|
||
createdAt: account.createdAt,
|
||
updatedAt: account.updatedAt,
|
||
lastValidatedAt: account.lastValidatedAt,
|
||
lastUsedAt: account.lastUsedAt,
|
||
lastError: account.lastError,
|
||
lastSwitchedAt: account.lastSwitchedAt,
|
||
switchReason: account.switchReason,
|
||
apiKey: account.apiKeyConfigured ? "configured" : "",
|
||
} as AiAccount)),
|
||
).map((account) =>
|
||
buildAiAccountSummary(
|
||
account.accountId === ENV_OPENAI_ACCOUNT_ID ? (envAccount ?? account) : account,
|
||
{ isEnvironmentFallback: account.accountId === ENV_OPENAI_ACCOUNT_ID },
|
||
),
|
||
);
|
||
}
|
||
|
||
export function getMasterIdentitySummaryFromState(state: BossState): MasterIdentitySummary {
|
||
const resolved = resolveActiveAiAccount(state);
|
||
if (!resolved.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 账号”至少配置一个可用的 Master Codex Node、OpenAI API 或阿里百炼 Qwen 账号。",
|
||
};
|
||
}
|
||
|
||
const summary = buildAiAccountSummary(resolved.account, {
|
||
isEnvironmentFallback: resolved.isEnvironmentFallback,
|
||
});
|
||
return {
|
||
accountId: summary.accountId,
|
||
label: summary.label,
|
||
role: summary.role,
|
||
roleLabel: summary.roleLabel,
|
||
provider: summary.provider,
|
||
providerLabel: summary.providerLabel,
|
||
displayName: summary.displayName,
|
||
nodeLabel: summary.nodeLabel,
|
||
model: summary.model,
|
||
status: summary.status,
|
||
statusLabel: summary.statusLabel,
|
||
canGenerate: summary.canGenerate,
|
||
switchReason: summary.switchReason,
|
||
lastSwitchedAt: summary.lastSwitchedAt,
|
||
note:
|
||
summary.loginStatusNote ??
|
||
(summary.canGenerate
|
||
? "当前账号可直接生成主 Agent 回复。"
|
||
: "当前账号只完成了身份占位,还没有接通真实生成能力。"),
|
||
isEnvironmentFallback: summary.isEnvironmentFallback,
|
||
};
|
||
}
|
||
|
||
function normalizeMessage(raw: Partial<Message>): Message {
|
||
return {
|
||
id: raw.id ?? randomToken("msg"),
|
||
sender: raw.sender ?? "master",
|
||
senderLabel: raw.senderLabel ?? "主 Agent",
|
||
body: raw.body ?? "",
|
||
sentAt: raw.sentAt ?? nowIso(),
|
||
kind: raw.kind ?? "text",
|
||
attachments: Array.isArray(raw.attachments)
|
||
? raw.attachments.map((attachment) => normalizeMessageAttachment(attachment))
|
||
: undefined,
|
||
forwardSource: raw.forwardSource,
|
||
forwardBundle: raw.forwardBundle,
|
||
};
|
||
}
|
||
|
||
function normalizeMessageAttachment(raw: Partial<MessageAttachment>): MessageAttachment {
|
||
return {
|
||
attachmentId: raw.attachmentId ?? randomToken("att"),
|
||
fileName: raw.fileName ?? "",
|
||
mimeType: raw.mimeType ?? "application/octet-stream",
|
||
fileSizeBytes: raw.fileSizeBytes ?? 0,
|
||
attachmentKind: raw.attachmentKind ?? "binary",
|
||
storageBackend: raw.storageBackend ?? "server_file",
|
||
storagePath: raw.storagePath ?? "",
|
||
storageSnapshot:
|
||
raw.storageSnapshot?.provider === "aliyun_oss"
|
||
? {
|
||
provider: "aliyun_oss",
|
||
accessKeyId: raw.storageSnapshot.accessKeyId ?? "",
|
||
accessKeySecretEncrypted: raw.storageSnapshot.accessKeySecretEncrypted ?? "",
|
||
bucket: raw.storageSnapshot.bucket ?? "",
|
||
endpoint: raw.storageSnapshot.endpoint ?? "",
|
||
region: raw.storageSnapshot.region ?? "",
|
||
prefix: raw.storageSnapshot.prefix,
|
||
}
|
||
: undefined,
|
||
previewAvailable: raw.previewAvailable ?? false,
|
||
uploadedAt: raw.uploadedAt ?? nowIso(),
|
||
uploadedBy: raw.uploadedBy ?? "system",
|
||
analysisState: raw.analysisState ?? "not_applicable",
|
||
analysisSummary: raw.analysisSummary,
|
||
analysisCardId: raw.analysisCardId,
|
||
};
|
||
}
|
||
|
||
function buildAttachmentMessageBody(attachment: MessageAttachment) {
|
||
const sizeKb = Math.max(1, Math.round(attachment.fileSizeBytes / 1024));
|
||
return `已发送附件:${attachment.fileName}(${attachment.attachmentKind},${sizeKb} KB)`;
|
||
}
|
||
|
||
function normalizeAttachmentStorageConfig(
|
||
raw: Partial<UserAttachmentStorageConfig>,
|
||
fallback: UserAttachmentStorageConfig,
|
||
): UserAttachmentStorageConfig {
|
||
return {
|
||
account: raw.account ?? fallback.account,
|
||
mode: raw.mode ?? fallback.mode,
|
||
ossProvider: raw.ossProvider ?? fallback.ossProvider,
|
||
aliyunOss: raw.aliyunOss
|
||
? {
|
||
enabled: raw.aliyunOss.enabled ?? fallback.aliyunOss?.enabled ?? false,
|
||
accessKeyId: raw.aliyunOss.accessKeyId ?? fallback.aliyunOss?.accessKeyId ?? "",
|
||
accessKeySecretEncrypted:
|
||
raw.aliyunOss.accessKeySecretEncrypted ??
|
||
fallback.aliyunOss?.accessKeySecretEncrypted ??
|
||
"",
|
||
bucket: raw.aliyunOss.bucket ?? fallback.aliyunOss?.bucket ?? "",
|
||
endpoint: raw.aliyunOss.endpoint ?? fallback.aliyunOss?.endpoint ?? "",
|
||
region: raw.aliyunOss.region ?? fallback.aliyunOss?.region ?? "",
|
||
prefix: raw.aliyunOss.prefix ?? fallback.aliyunOss?.prefix,
|
||
}
|
||
: fallback.aliyunOss,
|
||
updatedAt: raw.updatedAt ?? fallback.updatedAt,
|
||
validatedAt: raw.validatedAt ?? fallback.validatedAt,
|
||
};
|
||
}
|
||
|
||
function normalizeMasterAgentPromptPolicy(
|
||
raw: Partial<MasterAgentPromptPolicy> | null | undefined,
|
||
fallback?: MasterAgentPromptPolicy | null,
|
||
): MasterAgentPromptPolicy | null {
|
||
if (!raw) {
|
||
return fallback ?? null;
|
||
}
|
||
const globalPrompt = raw.globalPrompt?.trim();
|
||
if (!globalPrompt) {
|
||
return fallback ?? null;
|
||
}
|
||
return {
|
||
globalPrompt,
|
||
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
|
||
updatedBy: raw.updatedBy?.trim() || fallback?.updatedBy,
|
||
};
|
||
}
|
||
|
||
function normalizeUserMasterPrompt(
|
||
raw: Partial<UserMasterPrompt>,
|
||
fallback?: UserMasterPrompt,
|
||
): UserMasterPrompt {
|
||
const account = raw.account ?? fallback?.account ?? "";
|
||
return {
|
||
account,
|
||
content: raw.content?.trim() ?? fallback?.content ?? "",
|
||
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
|
||
};
|
||
}
|
||
|
||
function normalizeUserProjectAgentControls(
|
||
raw: Partial<UserProjectAgentControls>,
|
||
fallback?: UserProjectAgentControls,
|
||
): UserProjectAgentControls | null {
|
||
const account = trimToDefined(raw.account) ?? trimToDefined(fallback?.account);
|
||
const projectId = trimToDefined(raw.projectId) ?? trimToDefined(fallback?.projectId);
|
||
const controls = normalizeProjectAgentControls(raw.controls ?? fallback?.controls);
|
||
if (!account || !projectId || !controls) {
|
||
return null;
|
||
}
|
||
return {
|
||
account,
|
||
projectId,
|
||
controls,
|
||
};
|
||
}
|
||
|
||
function normalizeMasterMemoryTags(values: string[] | undefined) {
|
||
return dedupeStrings(
|
||
(values ?? [])
|
||
.map((value) => value.trim())
|
||
.filter((value) => Boolean(value)),
|
||
);
|
||
}
|
||
|
||
function normalizeUserMasterMemory(
|
||
raw: Partial<MasterAgentMemory>,
|
||
fallback?: MasterAgentMemory,
|
||
): MasterAgentMemory {
|
||
const scope = raw.scope ?? fallback?.scope ?? "global";
|
||
const projectId = scope === "project" ? raw.projectId ?? fallback?.projectId : undefined;
|
||
return {
|
||
memoryId: raw.memoryId ?? fallback?.memoryId ?? randomToken("memory"),
|
||
account: raw.account ?? fallback?.account ?? "",
|
||
scope,
|
||
projectId,
|
||
title: raw.title?.trim() ?? fallback?.title ?? "",
|
||
content: raw.content?.trim() ?? fallback?.content ?? "",
|
||
memoryType: raw.memoryType ?? fallback?.memoryType ?? "user_preference",
|
||
tags: normalizeMasterMemoryTags(raw.tags ?? fallback?.tags ?? []),
|
||
sourceMessageId: raw.sourceMessageId ?? fallback?.sourceMessageId,
|
||
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
|
||
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
|
||
lastUsedAt: raw.lastUsedAt ?? fallback?.lastUsedAt,
|
||
archived: raw.archived ?? fallback?.archived ?? false,
|
||
};
|
||
}
|
||
|
||
function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||
const base = fallback ?? cloneInitialState().projects[0];
|
||
const projectId = raw.id ?? base.id;
|
||
const projectName = raw.name ?? base.name;
|
||
const projectUpdatedAt = latestIsoTimestamp(raw.updatedAt, raw.lastMessageAt, base.updatedAt, base.lastMessageAt);
|
||
const threadMetaFallback = fallback?.id === projectId ? fallback.threadMeta : undefined;
|
||
const threadMeta = normalizeThreadMeta(raw.threadMeta, {
|
||
id: projectId,
|
||
name: projectName,
|
||
isGroup: raw.isGroup ?? base.isGroup ?? false,
|
||
updatedAt: projectUpdatedAt,
|
||
}, threadMetaFallback);
|
||
const project: Project = {
|
||
...base,
|
||
...raw,
|
||
pinned: raw.pinned ?? base.pinned,
|
||
systemPinned: raw.systemPinned ?? base.systemPinned,
|
||
deviceIds: ensureArray(raw.deviceIds, base.deviceIds),
|
||
unreadCount:
|
||
typeof raw.unreadCount === "number" ? raw.unreadCount : base.unreadCount ?? 0,
|
||
riskLevel: raw.riskLevel ?? base.riskLevel ?? "low",
|
||
messages: ensureArray(raw.messages, base.messages).map(normalizeMessage),
|
||
goals: ensureArray(raw.goals, base.goals).map((goal) => ({
|
||
id: goal.id ?? randomToken("goal"),
|
||
text: goal.text ?? "",
|
||
state: goal.state ?? "pending",
|
||
note: goal.note ?? "",
|
||
completedAt: goal.completedAt,
|
||
completedBy: goal.completedBy,
|
||
})),
|
||
versions: ensureArray(raw.versions, base.versions).map((version) => ({
|
||
version: version.version ?? "v0.0.0",
|
||
summary: version.summary ?? "",
|
||
createdAt: version.createdAt ?? nowIso(),
|
||
})),
|
||
threadMeta,
|
||
createdByAgent: raw.createdByAgent ?? false,
|
||
collaborationMode: raw.collaborationMode ?? "development",
|
||
approvalState: raw.approvalState ?? "not_required",
|
||
agentControls: normalizeProjectAgentControls(raw.agentControls),
|
||
};
|
||
project.groupMembers = ensureArray(raw.groupMembers, []).map((member) =>
|
||
normalizeGroupMember(member, projectId, project.threadMeta),
|
||
);
|
||
normalizeProjectConversationShape(project);
|
||
project.updatedAt = resolveProjectUpdatedAt(project, project.threadMeta.updatedAt);
|
||
return project;
|
||
}
|
||
|
||
function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||
const base = cloneInitialState();
|
||
if (!raw) return syncDerivedState(base);
|
||
|
||
const state: BossState = {
|
||
user: {
|
||
...base.user,
|
||
...raw.user,
|
||
settings: {
|
||
...base.user.settings,
|
||
...(raw.user?.settings ?? {}),
|
||
},
|
||
},
|
||
devices: ensureArray(raw.devices, base.devices).map((device, index) => ({
|
||
...base.devices[index % base.devices.length],
|
||
...device,
|
||
source:
|
||
device.source ??
|
||
(device.id === PRIMARY_CODEX_NODE_ID ? "production" : "demo"),
|
||
})),
|
||
projects: ensureArray(raw.projects, base.projects).map((project, index) =>
|
||
normalizeProject(project, base.projects[index % base.projects.length]),
|
||
),
|
||
verificationCodes: ensureArray(raw.verificationCodes, base.verificationCodes),
|
||
verificationDispatches: ensureArray(raw.verificationDispatches, base.verificationDispatches).map(
|
||
(dispatch) => ({
|
||
dispatchId: dispatch.dispatchId ?? randomToken("vdispatch"),
|
||
account: dispatch.account ?? "",
|
||
purpose: dispatch.purpose ?? "login",
|
||
deliveryMode: dispatch.deliveryMode ?? getVerificationDeliveryMode(),
|
||
requestedAt: dispatch.requestedAt ?? nowIso(),
|
||
status: dispatch.status ?? "requested",
|
||
note: dispatch.note ?? "",
|
||
}),
|
||
),
|
||
authAccounts: ensureArray(raw.authAccounts, base.authAccounts).map((account) => ({
|
||
id: account.id ?? `account-${slugify(account.account ?? "unknown")}`,
|
||
account: account.account ?? "",
|
||
passwordHash: account.passwordHash ?? hashPassword("boss123456"),
|
||
displayName: account.displayName ?? "Boss 成员",
|
||
role: account.role ?? "member",
|
||
verificationEmail: account.verificationEmail,
|
||
codexNodeId: account.codexNodeId,
|
||
codexNodeLabel: account.codexNodeLabel,
|
||
primaryDeviceId: account.primaryDeviceId,
|
||
isPrimary: account.isPrimary ?? false,
|
||
failedLoginAttempts: account.failedLoginAttempts ?? 0,
|
||
lockedUntil: account.lockedUntil,
|
||
lastLoginAt: account.lastLoginAt,
|
||
lastLoginMethod: account.lastLoginMethod,
|
||
createdAt: account.createdAt ?? nowIso(),
|
||
updatedAt: account.updatedAt ?? account.createdAt ?? nowIso(),
|
||
})),
|
||
authSessions: ensureArray(raw.authSessions, base.authSessions).map((session) => ({
|
||
sessionId: session.sessionId ?? randomToken("session"),
|
||
sessionToken: session.sessionToken ?? randomBytes(24).toString("hex"),
|
||
restoreToken: session.restoreToken ?? randomBytes(24).toString("hex"),
|
||
account: session.account ?? "",
|
||
role: session.role ?? "member",
|
||
displayName: session.displayName ?? "Boss 成员",
|
||
loginMethod: session.loginMethod ?? "password",
|
||
createdAt: session.createdAt ?? nowIso(),
|
||
expiresAt: session.expiresAt ?? new Date(Date.now() + AUTH_SESSION_TTL_MS).toISOString(),
|
||
lastSeenAt: session.lastSeenAt ?? session.createdAt ?? nowIso(),
|
||
revokedAt: session.revokedAt,
|
||
})),
|
||
aiAccounts: ensureArray(raw.aiAccounts, base.aiAccounts).map((account, index) => {
|
||
const fallback = base.aiAccounts[index % base.aiAccounts.length];
|
||
return {
|
||
...fallback,
|
||
...account,
|
||
accountId: account.accountId ?? fallback.accountId ?? `ai-${randomToken("account")}`,
|
||
label: account.label ?? fallback.label ?? "AI 账号",
|
||
role: account.role ?? fallback.role ?? "api_fallback",
|
||
provider: account.provider ?? fallback.provider ?? "openai_api",
|
||
displayName: account.displayName ?? fallback.displayName ?? "未命名 AI",
|
||
model: account.model ?? fallback.model,
|
||
apiKey: account.apiKey,
|
||
apiKeyMasked: account.apiKeyMasked ?? fallback.apiKeyMasked,
|
||
enabled: account.enabled ?? fallback.enabled ?? true,
|
||
isActive: account.isActive ?? fallback.isActive ?? false,
|
||
status: account.status ?? fallback.status ?? "needs_api_key",
|
||
loginStatusNote: account.loginStatusNote ?? fallback.loginStatusNote,
|
||
createdAt: account.createdAt ?? nowIso(),
|
||
updatedAt: account.updatedAt ?? account.createdAt ?? nowIso(),
|
||
} satisfies AiAccount;
|
||
}),
|
||
aiAccountSwitchHistory: ensureArray(
|
||
raw.aiAccountSwitchHistory,
|
||
base.aiAccountSwitchHistory,
|
||
).map((item) => ({
|
||
switchId: item.switchId ?? randomToken("aiswitch"),
|
||
fromAccountId: item.fromAccountId,
|
||
fromLabel: item.fromLabel,
|
||
toAccountId: item.toAccountId ?? "",
|
||
toLabel: item.toLabel ?? "AI 账号",
|
||
role: item.role ?? "api_fallback",
|
||
switchedAt: item.switchedAt ?? nowIso(),
|
||
reason: item.reason ?? "未注明切换原因",
|
||
})),
|
||
masterAgentTasks: ensureArray(raw.masterAgentTasks, base.masterAgentTasks).map((task) => ({
|
||
taskId: task.taskId ?? randomToken("mastertask"),
|
||
projectId: task.projectId ?? "master-agent",
|
||
taskType: task.taskType ?? "conversation_reply",
|
||
requestMessageId: task.requestMessageId ?? "",
|
||
requestText: task.requestText ?? "",
|
||
executionPrompt: task.executionPrompt ?? task.requestText ?? "",
|
||
requestedBy: task.requestedBy ?? "用户",
|
||
requestedByAccount: task.requestedByAccount ?? "",
|
||
deviceId: task.deviceId ?? PRIMARY_CODEX_NODE_ID,
|
||
accountId: task.accountId,
|
||
accountLabel: task.accountLabel,
|
||
attachmentId: task.attachmentId,
|
||
attachmentFileName: task.attachmentFileName,
|
||
attachmentDownloadToken: task.attachmentDownloadToken,
|
||
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,
|
||
completedAt: task.completedAt,
|
||
replyBody: task.replyBody,
|
||
errorMessage: task.errorMessage,
|
||
requestId: task.requestId,
|
||
})),
|
||
dispatchPlans: ensureArray(raw.dispatchPlans, base.dispatchPlans).map((plan, index) =>
|
||
normalizeDispatchPlan(plan, base.dispatchPlans[index % Math.max(1, base.dispatchPlans.length)]),
|
||
),
|
||
dispatchExecutions: ensureArray(raw.dispatchExecutions, base.dispatchExecutions).map((execution, index) =>
|
||
normalizeDispatchExecution(
|
||
execution,
|
||
base.dispatchExecutions[index % Math.max(1, base.dispatchExecutions.length)],
|
||
),
|
||
),
|
||
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,
|
||
summary: ensureArray(update.summary, base.otaUpdates[index % base.otaUpdates.length].summary),
|
||
})),
|
||
otaUpdateLogs: ensureArray(raw.otaUpdateLogs, base.otaUpdateLogs).map((log, index) => ({
|
||
...base.otaUpdateLogs[index % base.otaUpdateLogs.length],
|
||
...log,
|
||
})),
|
||
deviceSkills: ensureArray(raw.deviceSkills, base.deviceSkills).map((skill) => ({
|
||
...skill,
|
||
invocation: skill.invocation ?? `$${skill.name}`,
|
||
category: skill.category ?? "本机技能",
|
||
updatedAt: skill.updatedAt ?? nowIso(),
|
||
})),
|
||
appLogs: ensureArray(raw.appLogs, base.appLogs).map((log) => ({
|
||
...log,
|
||
mirroredToProject: log.mirroredToProject ?? false,
|
||
createdAt: log.createdAt ?? nowIso(),
|
||
})),
|
||
userAttachmentStorageConfigs: ensureArray(
|
||
raw.userAttachmentStorageConfigs,
|
||
base.userAttachmentStorageConfigs,
|
||
).map((config, index) =>
|
||
normalizeAttachmentStorageConfig(
|
||
config,
|
||
base.userAttachmentStorageConfigs[index % base.userAttachmentStorageConfigs.length],
|
||
),
|
||
),
|
||
masterAgentPromptPolicy: normalizeMasterAgentPromptPolicy(
|
||
raw.masterAgentPromptPolicy,
|
||
base.masterAgentPromptPolicy,
|
||
),
|
||
userMasterPrompts: ensureArray(raw.userMasterPrompts, base.userMasterPrompts).map(
|
||
(prompt, index) =>
|
||
normalizeUserMasterPrompt(
|
||
prompt,
|
||
base.userMasterPrompts[index % Math.max(1, base.userMasterPrompts.length)],
|
||
),
|
||
),
|
||
masterAgentMemories: ensureArray(raw.masterAgentMemories, base.masterAgentMemories).map(
|
||
(memory, index) =>
|
||
normalizeUserMasterMemory(
|
||
memory,
|
||
base.masterAgentMemories[index % Math.max(1, base.masterAgentMemories.length)],
|
||
),
|
||
),
|
||
userProjectAgentControls: ensureArray(
|
||
raw.userProjectAgentControls,
|
||
base.userProjectAgentControls,
|
||
)
|
||
.map((controls, index) =>
|
||
normalizeUserProjectAgentControls(
|
||
controls,
|
||
base.userProjectAgentControls[index % Math.max(1, base.userProjectAgentControls.length)],
|
||
),
|
||
)
|
||
.filter((item): item is UserProjectAgentControls => Boolean(item)),
|
||
threadContextSnapshots: ensureArray(raw.threadContextSnapshots, base.threadContextSnapshots).map(
|
||
(snapshot, index) => ({
|
||
...base.threadContextSnapshots[index % base.threadContextSnapshots.length],
|
||
...snapshot,
|
||
contextBudgetLevel:
|
||
snapshot.contextBudgetLevel ??
|
||
deriveLevelFromPercent(snapshot.contextBudgetRemainingPct ?? 100),
|
||
checklist: ensureArray(snapshot.checklist, base.threadContextSnapshots[index % base.threadContextSnapshots.length].checklist),
|
||
}),
|
||
),
|
||
threadHandoffPackages: ensureArray(raw.threadHandoffPackages, base.threadHandoffPackages).map(
|
||
(item) => ({
|
||
...item,
|
||
openQuestions: ensureArray(item.openQuestions, []),
|
||
criticalFiles: ensureArray(item.criticalFiles, []),
|
||
criticalCommands: ensureArray(item.criticalCommands, []),
|
||
criticalTests: ensureArray(item.criticalTests, []),
|
||
criticalArtifacts: ensureArray(item.criticalArtifacts, []),
|
||
decisionLinks: ensureArray(item.decisionLinks, []),
|
||
}),
|
||
),
|
||
threadContextAlerts: ensureArray(raw.threadContextAlerts, base.threadContextAlerts).map((item) => ({
|
||
...item,
|
||
masterActions: ensureArray(item.masterActions, []),
|
||
})),
|
||
deviceEnrollments: ensureArray(raw.deviceEnrollments, base.deviceEnrollments),
|
||
opsFaults: ensureArray(raw.opsFaults, base.opsFaults),
|
||
opsRepairTickets: ensureArray(raw.opsRepairTickets, base.opsRepairTickets),
|
||
opsRepairVerifications: ensureArray(raw.opsRepairVerifications, base.opsRepairVerifications),
|
||
auditRequests: ensureArray(raw.auditRequests, base.auditRequests).map((item) => ({
|
||
...item,
|
||
acceptanceCriteria: ensureArray(item.acceptanceCriteria, []),
|
||
riskFocus: ensureArray(item.riskFocus, []),
|
||
evidenceRefs: ensureArray(item.evidenceRefs, []),
|
||
artifactRefs: ensureArray(item.artifactRefs, []),
|
||
capabilityRequirements: ensureArray(item.capabilityRequirements, []),
|
||
metadataJson: item.metadataJson ?? {},
|
||
})),
|
||
auditResults: ensureArray(raw.auditResults, base.auditResults).map((item) => ({
|
||
...item,
|
||
findings: ensureArray(item.findings, []),
|
||
requiredActions: ensureArray(item.requiredActions, []),
|
||
usedCapabilities: ensureArray(item.usedCapabilities, []),
|
||
artifactRefs: ensureArray(item.artifactRefs, []),
|
||
timeline: ensureArray(item.timeline, []),
|
||
})),
|
||
capabilities: ensureArray(raw.capabilities, base.capabilities).map((item) => ({
|
||
...item,
|
||
supportedActions: ensureArray(item.supportedActions, []),
|
||
evidenceModes: ensureArray(item.evidenceModes, []),
|
||
})),
|
||
};
|
||
|
||
if (!state.projects.some((project) => project.id === "master-agent")) {
|
||
state.projects.unshift(base.projects[0]);
|
||
}
|
||
|
||
return syncDerivedState(state);
|
||
}
|
||
|
||
function latestProjectTimestamp(state: BossState, projectId: string) {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
const messageTimes = (project?.messages ?? []).map((message) => messageTimeValue(message.sentAt));
|
||
const snapshotTimes = state.threadContextSnapshots
|
||
.filter((snapshot) => snapshot.projectId === projectId)
|
||
.map((snapshot) => messageTimeValue(snapshot.capturedAt));
|
||
const alertTimes = state.threadContextAlerts
|
||
.filter((alert) => alert.projectId === projectId)
|
||
.map((alert) => messageTimeValue(alert.openedAt));
|
||
const opsTimes = state.opsFaults
|
||
.filter((fault) => fault.projectId === projectId)
|
||
.map((fault) => messageTimeValue(fault.lastSeenAt));
|
||
const auditTimes = state.auditResults
|
||
.filter((result) =>
|
||
state.auditRequests.some(
|
||
(request) =>
|
||
request.auditRequestId === result.auditRequestId && request.projectId === projectId,
|
||
),
|
||
)
|
||
.map((result) => messageTimeValue(result.completedAt));
|
||
|
||
const latest = Math.max(0, ...messageTimes, ...snapshotTimes, ...alertTimes, ...opsTimes, ...auditTimes);
|
||
return latest ? new Date(latest).toISOString() : project?.lastMessageAt ?? nowIso();
|
||
}
|
||
|
||
function deriveProjectPreview(state: BossState, project: Project) {
|
||
if (project.id === "master-agent") {
|
||
return project.preview;
|
||
}
|
||
|
||
const relevantSnapshots = state.threadContextSnapshots
|
||
.filter((snapshot) => snapshot.projectId === project.id)
|
||
.sort(compareSnapshotsForRisk);
|
||
const topSnapshot = relevantSnapshots[0];
|
||
if (topSnapshot && (topSnapshot.contextBudgetLevel !== "safe" || topSnapshot.mustFinishBeforeCompaction)) {
|
||
return `${topSnapshot.title} · ${topSnapshot.contextBudgetLevel} ${topSnapshot.contextBudgetRemainingPct}% · ${topSnapshot.summary}`;
|
||
}
|
||
|
||
const latestAudit = state.auditResults
|
||
.map((result) => ({
|
||
result,
|
||
request: state.auditRequests.find((request) => request.auditRequestId === result.auditRequestId),
|
||
}))
|
||
.filter(
|
||
(item): item is { result: AuditTaskResult; request: AuditTaskRequest } =>
|
||
Boolean(item.request && item.request.projectId === project.id),
|
||
)
|
||
.sort((a, b) => messageTimeValue(b.result.completedAt) - messageTimeValue(a.result.completedAt))[0];
|
||
if (latestAudit) {
|
||
return `审计 · ${latestAudit.result.summary}`;
|
||
}
|
||
|
||
const openFault = state.opsFaults
|
||
.filter((fault) => fault.projectId === project.id && fault.status !== "resolved")
|
||
.sort((a, b) => messageTimeValue(b.lastSeenAt) - messageTimeValue(a.lastSeenAt))[0];
|
||
if (openFault) {
|
||
return `运维 · ${openFault.summary}`;
|
||
}
|
||
|
||
const lastMessage = [...project.messages].sort(
|
||
(a, b) => messageTimeValue(b.sentAt) - messageTimeValue(a.sentAt),
|
||
)[0];
|
||
if (lastMessage) {
|
||
return lastMessage.body;
|
||
}
|
||
|
||
return project.preview;
|
||
}
|
||
|
||
function updateMasterProjectSummary(state: BossState) {
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
if (!masterProject) return;
|
||
|
||
const riskThreads = [...state.threadContextSnapshots]
|
||
.filter(
|
||
(snapshot) =>
|
||
snapshot.projectId !== "master-agent" &&
|
||
(snapshot.contextBudgetLevel !== "safe" || snapshot.mustFinishBeforeCompaction),
|
||
)
|
||
.sort(compareSnapshotsForRisk);
|
||
const openFaults = state.opsFaults.filter((fault) => fault.status !== "resolved");
|
||
const pendingAudits = state.auditRequests.filter(
|
||
(request) =>
|
||
!state.auditResults.some((result) => result.auditRequestId === request.auditRequestId),
|
||
);
|
||
|
||
const summaryParts = [];
|
||
if (riskThreads[0]) {
|
||
summaryParts.push(
|
||
`${riskThreads[0].title} ${riskThreads[0].contextBudgetLevel} ${riskThreads[0].contextBudgetRemainingPct}%`,
|
||
);
|
||
}
|
||
if (openFaults[0]) {
|
||
summaryParts.push(`运维 ${openFaults[0].faultKey}`);
|
||
}
|
||
if (pendingAudits.length > 0) {
|
||
summaryParts.push(`审计待处理 ${pendingAudits.length} 条`);
|
||
}
|
||
|
||
masterProject.preview =
|
||
summaryParts.join(" · ") ||
|
||
"当前无高风险线程,主 Agent 正在维护项目总结、运维账本和交接文档。";
|
||
masterProject.riskLevel = riskThreads[0]
|
||
? deriveRiskFromSnapshots([riskThreads[0]])
|
||
: openFaults.some((fault) => fault.severity === "critical")
|
||
? "high"
|
||
: "low";
|
||
|
||
const masterSnapshot = state.threadContextSnapshots.find(
|
||
(snapshot) => snapshot.projectId === "master-agent",
|
||
);
|
||
masterProject.contextBudgetPct = masterSnapshot?.contextBudgetRemainingPct;
|
||
masterProject.contextBudgetLabel = masterSnapshot
|
||
? `${masterSnapshot.contextBudgetRemainingPct}%`
|
||
: undefined;
|
||
masterProject.lastMessageAt = latestProjectTimestamp(state, "master-agent");
|
||
masterProject.updatedAt = masterProject.lastMessageAt;
|
||
|
||
const summaryMessage = masterProject.messages.find((message) => message.id === "master-summary");
|
||
if (summaryMessage) {
|
||
summaryMessage.body = masterProject.preview;
|
||
summaryMessage.sentAt = masterProject.lastMessageAt;
|
||
}
|
||
}
|
||
|
||
function firstAvailableOta(state: BossState) {
|
||
return state.otaUpdates.find((item) => item.status === "available" || item.status === "scheduled") ?? null;
|
||
}
|
||
|
||
function readPublishedOtaAsset() {
|
||
if (!existsSync(publishedApkPath)) {
|
||
return null;
|
||
}
|
||
|
||
const stats = statSync(publishedApkPath);
|
||
type PublishedOtaMetadata = {
|
||
fileName?: string;
|
||
urlPath?: string;
|
||
sizeBytes?: number;
|
||
updatedAt?: string;
|
||
sha256?: string;
|
||
versionName?: string;
|
||
};
|
||
let metadata: PublishedOtaMetadata | null = null;
|
||
|
||
if (existsSync(publishedApkMetaPath)) {
|
||
try {
|
||
metadata = JSON.parse(readFileSync(publishedApkMetaPath, "utf8")) as PublishedOtaMetadata;
|
||
} catch {
|
||
metadata = null;
|
||
}
|
||
}
|
||
|
||
return {
|
||
fileName: metadata?.fileName ?? path.basename(publishedApkPath),
|
||
downloadUrl: metadata?.urlPath ?? "/api/v1/user/ota/package",
|
||
sizeBytes: metadata?.sizeBytes ?? stats.size,
|
||
assetUpdatedAt: metadata?.updatedAt ?? stats.mtime.toISOString(),
|
||
packageSha256: metadata?.sha256,
|
||
versionName: metadata?.versionName,
|
||
};
|
||
}
|
||
|
||
function syncPublishedOtaAsset(state: BossState) {
|
||
const asset = readPublishedOtaAsset();
|
||
for (const release of state.otaUpdates) {
|
||
if (release.packageType !== "android_shell") continue;
|
||
|
||
release.currentVersion = state.user.version;
|
||
if (!asset) {
|
||
release.packageFileName = undefined;
|
||
release.packageSizeBytes = undefined;
|
||
release.packageSha256 = undefined;
|
||
release.downloadUrl = undefined;
|
||
release.assetUpdatedAt = undefined;
|
||
continue;
|
||
}
|
||
|
||
release.packageFileName = asset.fileName;
|
||
release.packageSizeBytes = asset.sizeBytes;
|
||
release.packageSha256 = asset.packageSha256;
|
||
release.downloadUrl = asset.downloadUrl;
|
||
release.assetUpdatedAt = asset.assetUpdatedAt;
|
||
if (asset.versionName) {
|
||
release.version = asset.versionName.startsWith("v")
|
||
? asset.versionName
|
||
: `v${asset.versionName}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
function isProductionDevice(device: Device) {
|
||
return device.source === "production";
|
||
}
|
||
|
||
function pushProjectLedgerMessage(
|
||
state: BossState,
|
||
projectId: string,
|
||
message: Omit<Message, "id" | "sentAt"> & { sentAt?: string },
|
||
) {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) return null;
|
||
|
||
const entry: Message = {
|
||
id: randomToken("msg"),
|
||
sentAt: message.sentAt ?? nowIso(),
|
||
...message,
|
||
};
|
||
project.messages.push(entry);
|
||
project.lastMessageAt = entry.sentAt;
|
||
project.preview = entry.body;
|
||
return entry;
|
||
}
|
||
|
||
function shouldAutoReplyToMirroredLog(entry: AppLogEntry) {
|
||
if (entry.level !== "info") return true;
|
||
return (
|
||
entry.category.startsWith("apk.") ||
|
||
entry.category.startsWith("ota.") ||
|
||
entry.category.startsWith("runtime.") ||
|
||
entry.category.startsWith("chat.message_failed")
|
||
);
|
||
}
|
||
|
||
function buildMasterAgentLogReply(state: BossState, entry: AppLogEntry) {
|
||
const device = state.devices.find((item) => item.id === entry.deviceId);
|
||
const availableOta = firstAvailableOta(state);
|
||
const lines = [
|
||
`已收到 ${device?.name ?? entry.deviceId} 的${entry.level === "error" ? "错误" : "实时"}日志:${entry.category}。`,
|
||
`现象:${entry.message}${entry.detail ? `,附加信息:${entry.detail}` : ""}`,
|
||
];
|
||
|
||
if (entry.level === "error") {
|
||
lines.push("我会优先沿着这条报错路径继续收口,并尽量把 patch、验证和 APK 调整一起推进。");
|
||
} else {
|
||
lines.push("我会把这条最新日志并入当前调度判断,继续对齐主 Agent 对话和 APK 优化方向。");
|
||
}
|
||
|
||
if (availableOta?.downloadUrl) {
|
||
lines.push(`当前可验证的 OTA 包是 ${availableOta.version},下载地址已经就绪。`);
|
||
}
|
||
|
||
lines.push("继续把目标、复现步骤或预期结果发给我,我会结合最新日志继续推进。");
|
||
return lines.join("");
|
||
}
|
||
|
||
function ensurePrimaryAdminBinding(state: BossState) {
|
||
state.user.id = "user-boss-admin";
|
||
state.user.name = "Boss 超级管理员";
|
||
state.user.avatar = "17";
|
||
state.user.account = PRIMARY_ADMIN_ACCOUNT;
|
||
state.user.verificationEmail = PRIMARY_ADMIN_VERIFICATION_EMAIL;
|
||
state.user.role = "highest_admin";
|
||
state.user.roleLabel = "最高管理员";
|
||
state.user.accountType = "最高管理员 · 本机 Codex 已绑定";
|
||
state.user.qrCodeValue = `boss://user/${PRIMARY_ADMIN_ACCOUNT}`;
|
||
state.user.boundCodexNodeId = PRIMARY_CODEX_NODE_ID;
|
||
state.user.boundCodexNodeLabel = PRIMARY_CODEX_NODE_LABEL;
|
||
state.user.boundDeviceId = PRIMARY_CODEX_NODE_ID;
|
||
state.user.boundAt = state.user.boundAt ?? "2026-03-26T09:00:00+08:00";
|
||
if (!state.user.version || state.user.version === "1.2.7" || state.user.version === "1.3.0") {
|
||
state.user.version = "1.4.0";
|
||
}
|
||
|
||
let adminAccount = state.authAccounts.find((item) => item.account === PRIMARY_ADMIN_ACCOUNT);
|
||
if (!adminAccount) {
|
||
adminAccount = {
|
||
id: `account-${PRIMARY_ADMIN_ACCOUNT}`,
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
passwordHash: hashPassword(PRIMARY_ADMIN_PASSWORD),
|
||
displayName: "Boss 超级管理员",
|
||
role: "highest_admin",
|
||
verificationEmail: PRIMARY_ADMIN_VERIFICATION_EMAIL,
|
||
codexNodeId: PRIMARY_CODEX_NODE_ID,
|
||
codexNodeLabel: PRIMARY_CODEX_NODE_LABEL,
|
||
primaryDeviceId: PRIMARY_CODEX_NODE_ID,
|
||
isPrimary: true,
|
||
createdAt: nowIso(),
|
||
updatedAt: nowIso(),
|
||
};
|
||
state.authAccounts.unshift(adminAccount);
|
||
} else {
|
||
adminAccount.displayName = "Boss 超级管理员";
|
||
adminAccount.role = "highest_admin";
|
||
adminAccount.verificationEmail = PRIMARY_ADMIN_VERIFICATION_EMAIL;
|
||
adminAccount.codexNodeId = PRIMARY_CODEX_NODE_ID;
|
||
adminAccount.codexNodeLabel = PRIMARY_CODEX_NODE_LABEL;
|
||
adminAccount.primaryDeviceId = PRIMARY_CODEX_NODE_ID;
|
||
adminAccount.isPrimary = true;
|
||
}
|
||
|
||
for (const account of state.authAccounts) {
|
||
if (account.account !== PRIMARY_ADMIN_ACCOUNT) {
|
||
account.isPrimary = false;
|
||
}
|
||
}
|
||
|
||
const primaryDevice = state.devices.find((item) => item.id === PRIMARY_CODEX_NODE_ID);
|
||
if (primaryDevice) {
|
||
primaryDevice.account = PRIMARY_ADMIN_ACCOUNT;
|
||
primaryDevice.note = "本机 Codex 主节点 · 17600003315 已绑定";
|
||
}
|
||
|
||
const seededOta = state.otaUpdates.find((item) =>
|
||
item.releaseId === "ota_130_to_131" || item.releaseId === "ota_140_to_141",
|
||
);
|
||
if (seededOta) {
|
||
seededOta.releaseId = "ota_140_to_141";
|
||
seededOta.version = "v2.0.0";
|
||
seededOta.currentVersion = state.user.version;
|
||
seededOta.channel = "stable";
|
||
seededOta.packageType = "android_shell";
|
||
seededOta.status = seededOta.status === "applied" ? "applied" : "available";
|
||
seededOta.summary = ["切到原生 Android 客户端", "新增原生会话 / 设备 / 我的三栏", "登录页改为原生一键进入"];
|
||
seededOta.targetScope = "Boss Android 原生客户端与 Web 控制台";
|
||
seededOta.requiredRole = "highest_admin";
|
||
seededOta.publishedAt = seededOta.publishedAt || nowIso();
|
||
seededOta.packageFileName = seededOta.packageFileName || "boss-android-latest.apk";
|
||
seededOta.downloadUrl = seededOta.downloadUrl || "/api/v1/user/ota/package";
|
||
} else {
|
||
state.otaUpdates.push({
|
||
releaseId: "ota_140_to_141",
|
||
version: "v2.0.0",
|
||
currentVersion: state.user.version,
|
||
channel: "stable",
|
||
packageType: "android_shell",
|
||
status: "available",
|
||
summary: ["切到原生 Android 客户端", "新增原生会话 / 设备 / 我的三栏", "登录页改为原生一键进入"],
|
||
targetScope: "Boss Android 原生客户端与 Web 控制台",
|
||
requiredRole: "highest_admin",
|
||
publishedAt: nowIso(),
|
||
packageFileName: "boss-android-latest.apk",
|
||
downloadUrl: "/api/v1/user/ota/package",
|
||
});
|
||
}
|
||
}
|
||
|
||
function ensureDefaultAiAccounts(state: BossState) {
|
||
const defaults = cloneInitialState().aiAccounts;
|
||
for (const fallback of defaults) {
|
||
if (!state.aiAccounts.some((item) => item.accountId === fallback.accountId)) {
|
||
state.aiAccounts.push(fallback);
|
||
}
|
||
}
|
||
|
||
const primary = state.aiAccounts.find((item) => item.accountId === "master-codex-primary");
|
||
if (primary) {
|
||
primary.role = "primary";
|
||
primary.provider = "master_codex_node";
|
||
primary.enabled = true;
|
||
primary.nodeId = primary.nodeId || PRIMARY_CODEX_NODE_ID;
|
||
primary.nodeLabel = primary.nodeLabel || PRIMARY_CODEX_NODE_LABEL;
|
||
primary.displayName = primary.displayName || "17600003315 · Master Codex Node";
|
||
if (primary.status !== "degraded") {
|
||
primary.status = "ready";
|
||
}
|
||
if (!primary.loginStatusNote || primary.loginStatusNote.includes("还未接通")) {
|
||
primary.loginStatusNote = "已绑定本机 Codex,可通过 local-agent relay 执行主 Agent 对话。";
|
||
}
|
||
}
|
||
|
||
const fallbackApi = state.aiAccounts.find((item) => item.accountId === "openai-api-fallback");
|
||
if (fallbackApi) {
|
||
fallbackApi.role = "api_fallback";
|
||
fallbackApi.provider = "openai_api";
|
||
fallbackApi.model = fallbackApi.model || "gpt-5.4";
|
||
if (!fallbackApi.loginStatusNote) {
|
||
fallbackApi.loginStatusNote = "配置 OpenAI API Key 后,可直接为主 Agent 生成真实回复。";
|
||
}
|
||
}
|
||
}
|
||
|
||
function syncUserOtaState(state: BossState) {
|
||
const available = firstAvailableOta(state);
|
||
state.user.hasOta = Boolean(available);
|
||
state.user.otaVersion = available?.version;
|
||
state.user.otaSummary = available?.summary ?? [];
|
||
}
|
||
|
||
function syncDerivedState(input: BossState) {
|
||
const state = input;
|
||
ensurePrimaryAdminBinding(state);
|
||
ensureDefaultAiAccounts(state);
|
||
syncPublishedOtaAsset(state);
|
||
syncUserOtaState(state);
|
||
state.aiAccounts = sortAiAccounts(state.aiAccounts).slice(0, 24);
|
||
state.aiAccountSwitchHistory = state.aiAccountSwitchHistory
|
||
.sort((a, b) => b.switchedAt.localeCompare(a.switchedAt))
|
||
.slice(0, 40);
|
||
state.masterAgentTasks = state.masterAgentTasks
|
||
.sort((a, b) => b.requestedAt.localeCompare(a.requestedAt))
|
||
.slice(0, 80);
|
||
state.dispatchPlans = state.dispatchPlans
|
||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||
.slice(0, 80);
|
||
state.dispatchExecutions = state.dispatchExecutions
|
||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||
.slice(0, 160);
|
||
state.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));
|
||
state.threadContextSnapshots = state.threadContextSnapshots.filter((item) =>
|
||
visibleDeviceIds.has(item.nodeId),
|
||
);
|
||
const visibleThreadIds = new Set(state.threadContextSnapshots.map((item) => item.threadId));
|
||
state.threadHandoffPackages = state.threadHandoffPackages.filter((item) =>
|
||
visibleThreadIds.has(item.fromThreadId),
|
||
);
|
||
state.threadContextAlerts = state.threadContextAlerts.filter((item) =>
|
||
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));
|
||
state.appLogs = state.appLogs.slice(0, 120);
|
||
state.verificationDispatches = state.verificationDispatches.slice(0, 80);
|
||
state.authSessions = state.authSessions
|
||
.filter((session) => !session.revokedAt && new Date(session.expiresAt).getTime() > Date.now())
|
||
.slice(0, 20);
|
||
state.opsFaults = state.opsFaults.filter((item) => visibleDeviceIds.has(item.nodeId));
|
||
const visibleFaultIds = new Set(state.opsFaults.map((item) => item.faultId));
|
||
state.opsRepairTickets = state.opsRepairTickets.filter((item) => visibleFaultIds.has(item.faultId));
|
||
const visibleTicketIds = new Set(state.opsRepairTickets.map((item) => item.ticketId));
|
||
state.opsRepairVerifications = state.opsRepairVerifications.filter((item) =>
|
||
visibleTicketIds.has(item.ticketId),
|
||
);
|
||
state.auditRequests = state.auditRequests.filter((item) => {
|
||
const sourceNodeId = item.sourceThreadRef.split(":")[0];
|
||
return visibleDeviceIds.has(sourceNodeId);
|
||
});
|
||
const visibleAuditIds = new Set(state.auditRequests.map((item) => item.auditRequestId));
|
||
state.auditResults = state.auditResults.filter((item) => visibleAuditIds.has(item.auditRequestId));
|
||
state.capabilities = state.capabilities.filter((item) => visibleDeviceIds.has(item.nodeId));
|
||
|
||
for (const project of state.projects) {
|
||
project.deviceIds = project.deviceIds.filter((deviceId) => visibleDeviceIds.has(deviceId));
|
||
const projectSnapshots = state.threadContextSnapshots
|
||
.filter((snapshot) => snapshot.projectId === project.id)
|
||
.sort(compareSnapshotsForRisk);
|
||
|
||
normalizeProjectConversationShape(project, { allowedDeviceIds: visibleDeviceIds });
|
||
project.riskLevel = deriveRiskFromSnapshots(projectSnapshots);
|
||
if (project.isGroup) {
|
||
project.contextBudgetPct = undefined;
|
||
project.contextBudgetLabel = undefined;
|
||
} else {
|
||
const topSnapshot = projectSnapshots[0];
|
||
project.contextBudgetPct = topSnapshot?.contextBudgetRemainingPct;
|
||
project.contextBudgetLabel = topSnapshot
|
||
? `${topSnapshot.contextBudgetRemainingPct}%`
|
||
: undefined;
|
||
}
|
||
|
||
project.lastMessageAt = latestProjectTimestamp(state, project.id);
|
||
project.updatedAt = resolveProjectUpdatedAt(project, project.lastMessageAt);
|
||
project.preview = deriveProjectPreview(state, project);
|
||
project.unreadCount = Math.max(0, project.unreadCount ?? 0);
|
||
}
|
||
|
||
updateMasterProjectSummary(state);
|
||
return state;
|
||
}
|
||
|
||
async function ensureStateFile() {
|
||
await fs.mkdir(dataDir, { recursive: true });
|
||
try {
|
||
await fs.access(dataFile);
|
||
} catch {
|
||
const initialJson = JSON.stringify(syncDerivedState(cloneInitialState()), null, 2);
|
||
await fs.writeFile(dataFile, initialJson, "utf8");
|
||
await fs.writeFile(backupFile, initialJson, "utf8");
|
||
}
|
||
}
|
||
|
||
let stateWriteQueue: Promise<void> = Promise.resolve();
|
||
let lastPersistedStateText: string | null = null;
|
||
let stateMutationQueue: Promise<unknown> = Promise.resolve();
|
||
|
||
export async function readState(): Promise<BossState> {
|
||
await ensureStateFile();
|
||
const raw = await fs.readFile(dataFile, "utf8");
|
||
|
||
try {
|
||
const state = normalizeState(JSON.parse(raw) as Partial<BossState>);
|
||
lastPersistedStateText = JSON.stringify(state, null, 2);
|
||
return state;
|
||
} catch {
|
||
const fallbackText =
|
||
(await fs.readFile(backupFile, "utf8").catch(() => null)) ??
|
||
lastPersistedStateText ??
|
||
JSON.stringify(syncDerivedState(cloneInitialState()), null, 2);
|
||
const state = normalizeState(JSON.parse(fallbackText) as Partial<BossState>);
|
||
lastPersistedStateText = JSON.stringify(state, null, 2);
|
||
return state;
|
||
}
|
||
}
|
||
|
||
export async function writeState(state: BossState) {
|
||
const nextState = syncDerivedState(state);
|
||
const serialized = JSON.stringify(nextState, null, 2);
|
||
const tempFile = `${dataFile}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
||
lastPersistedStateText = serialized;
|
||
|
||
const persist = async () => {
|
||
await ensureStateFile();
|
||
await fs.writeFile(tempFile, serialized, "utf8");
|
||
await fs.rename(tempFile, dataFile);
|
||
await fs.writeFile(backupFile, serialized, "utf8");
|
||
};
|
||
|
||
stateWriteQueue = stateWriteQueue.then(persist, persist);
|
||
await stateWriteQueue;
|
||
}
|
||
|
||
async function mutateState<T>(mutator: (state: BossState) => Promise<T> | T) {
|
||
let result!: T;
|
||
const run = async () => {
|
||
const state = await readState();
|
||
result = await mutator(state);
|
||
await writeState(state);
|
||
};
|
||
|
||
stateMutationQueue = stateMutationQueue.then(run, run);
|
||
await stateMutationQueue;
|
||
return result;
|
||
}
|
||
|
||
async function mutateStateIfChanged<T>(
|
||
mutator: (state: BossState) => Promise<{ result: T; changed: boolean }> | { result: T; changed: boolean },
|
||
) {
|
||
let result!: T;
|
||
const run = async () => {
|
||
const state = await readState();
|
||
const outcome = await mutator(state);
|
||
result = outcome.result;
|
||
if (outcome.changed) {
|
||
await writeState(state);
|
||
}
|
||
};
|
||
|
||
stateMutationQueue = stateMutationQueue.then(run, run);
|
||
await stateMutationQueue;
|
||
return result;
|
||
}
|
||
|
||
async function loadPersistedStateRaw() {
|
||
await ensureStateFile();
|
||
|
||
const parseStateText = (text: string) => JSON.parse(text) as Partial<BossState>;
|
||
const tryRead = async (filePath: string) => {
|
||
const text = await fs.readFile(filePath, "utf8");
|
||
return parseStateText(text);
|
||
};
|
||
|
||
try {
|
||
return await tryRead(dataFile);
|
||
} catch {
|
||
try {
|
||
return await tryRead(backupFile);
|
||
} catch {
|
||
if (lastPersistedStateText) {
|
||
return parseStateText(lastPersistedStateText);
|
||
}
|
||
return JSON.parse(JSON.stringify(syncDerivedState(cloneInitialState()))) as Partial<BossState>;
|
||
}
|
||
}
|
||
}
|
||
|
||
export async function getProject(projectId: string) {
|
||
const state = await readState();
|
||
return state.projects.find((project) => project.id === projectId) ?? null;
|
||
}
|
||
|
||
export async function hasPersistedProject(projectId: string) {
|
||
const rawState = await loadPersistedStateRaw();
|
||
return Array.isArray(rawState.projects) && rawState.projects.some((project) => project?.id === projectId);
|
||
}
|
||
|
||
function findUserProjectAgentControls(
|
||
state: BossState,
|
||
projectId: string,
|
||
account?: string,
|
||
) {
|
||
const normalizedAccount = trimToDefined(account);
|
||
if (!normalizedAccount) {
|
||
return null;
|
||
}
|
||
return (
|
||
state.userProjectAgentControls.find(
|
||
(item) => item.projectId === projectId && item.account === normalizedAccount,
|
||
) ?? null
|
||
);
|
||
}
|
||
|
||
export async function getProjectAgentControls(projectId: string, account?: string) {
|
||
if (projectId !== "master-agent") {
|
||
return null;
|
||
}
|
||
const state = await readState();
|
||
const scopedControls = findUserProjectAgentControls(state, projectId, account);
|
||
if (scopedControls?.controls) {
|
||
return scopedControls.controls;
|
||
}
|
||
return state.projects.find((project) => project.id === projectId)?.agentControls ?? null;
|
||
}
|
||
|
||
export async function updateProjectAgentControls(
|
||
projectId: string,
|
||
payload: {
|
||
modelOverride?: unknown;
|
||
reasoningEffortOverride?: unknown;
|
||
promptOverride?: unknown;
|
||
},
|
||
account?: string,
|
||
) {
|
||
if (projectId !== "master-agent") {
|
||
throw new Error("MASTER_AGENT_CONTROLS_SCOPE_RESTRICTED");
|
||
}
|
||
|
||
const modelOverrideInput = Object.prototype.hasOwnProperty.call(payload, "modelOverride")
|
||
? parseControlTextOverride(payload.modelOverride)
|
||
: { kind: "preserve" as const };
|
||
const reasoningEffortInput = Object.prototype.hasOwnProperty.call(payload, "reasoningEffortOverride")
|
||
? parseReasoningEffortOverride(payload.reasoningEffortOverride)
|
||
: { kind: "preserve" as const };
|
||
const promptOverrideInput = Object.prototype.hasOwnProperty.call(payload, "promptOverride")
|
||
? parseControlTextOverride(payload.promptOverride)
|
||
: { kind: "preserve" as const };
|
||
if (modelOverrideInput.kind === "invalid") {
|
||
throw new Error("INVALID_MODEL_OVERRIDE");
|
||
}
|
||
if (reasoningEffortInput.kind === "invalid") {
|
||
throw new Error("INVALID_REASONING_EFFORT_OVERRIDE");
|
||
}
|
||
if (promptOverrideInput.kind === "invalid") {
|
||
throw new Error("INVALID_PROMPT_OVERRIDE");
|
||
}
|
||
|
||
return mutateStateIfChanged((state) => {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const normalizedAccount = trimToDefined(account);
|
||
const currentEntry = findUserProjectAgentControls(state, projectId, normalizedAccount ?? undefined);
|
||
const currentControls = currentEntry?.controls ?? project.agentControls;
|
||
const modelOverride =
|
||
modelOverrideInput.kind === "set"
|
||
? modelOverrideInput.value
|
||
: modelOverrideInput.kind === "clear"
|
||
? undefined
|
||
: currentControls?.modelOverride;
|
||
const reasoningEffortOverride =
|
||
reasoningEffortInput.kind === "set"
|
||
? reasoningEffortInput.value
|
||
: reasoningEffortInput.kind === "clear"
|
||
? undefined
|
||
: currentControls?.reasoningEffortOverride;
|
||
const promptOverride =
|
||
promptOverrideInput.kind === "set"
|
||
? promptOverrideInput.value
|
||
: promptOverrideInput.kind === "clear"
|
||
? undefined
|
||
: currentControls?.promptOverride;
|
||
|
||
const currentModelOverride = currentControls?.modelOverride;
|
||
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
|
||
const currentPromptOverride = currentControls?.promptOverride;
|
||
if (
|
||
currentModelOverride === modelOverride &&
|
||
currentReasoningEffortOverride === reasoningEffortOverride &&
|
||
currentPromptOverride === promptOverride
|
||
) {
|
||
return { result: currentControls, changed: false };
|
||
}
|
||
|
||
const nextControls = {
|
||
modelOverride,
|
||
reasoningEffortOverride,
|
||
promptOverride,
|
||
updatedAt: nowIso(),
|
||
} satisfies ProjectAgentControls;
|
||
const normalizedControls = normalizeProjectAgentControls(nextControls) ?? null;
|
||
|
||
if (normalizedAccount) {
|
||
state.userProjectAgentControls = state.userProjectAgentControls.filter(
|
||
(item) => !(item.projectId === projectId && item.account === normalizedAccount),
|
||
);
|
||
if (normalizedControls) {
|
||
state.userProjectAgentControls.unshift({
|
||
account: normalizedAccount,
|
||
projectId,
|
||
controls: normalizedControls,
|
||
});
|
||
}
|
||
} else {
|
||
project.agentControls = normalizedControls ?? undefined;
|
||
}
|
||
|
||
project.threadMeta.updatedAt = nextControls.updatedAt;
|
||
project.updatedAt = nextControls.updatedAt;
|
||
return { result: normalizedControls, changed: true };
|
||
});
|
||
}
|
||
|
||
export async function getDevice(deviceId: string) {
|
||
const state = await readState();
|
||
return state.devices.find((device) => device.id === deviceId) ?? null;
|
||
}
|
||
|
||
export async function getAttachmentStorageConfig(account: string) {
|
||
const state = await readState();
|
||
return (
|
||
state.userAttachmentStorageConfigs.find((item) => item.account === account) ?? {
|
||
account,
|
||
mode: "server_file" as const,
|
||
updatedAt: nowIso(),
|
||
}
|
||
);
|
||
}
|
||
|
||
export async function upsertAttachmentStorageConfig(config: UserAttachmentStorageConfig) {
|
||
return mutateState((state) => {
|
||
const index = state.userAttachmentStorageConfigs.findIndex(
|
||
(item) => item.account === config.account,
|
||
);
|
||
if (index >= 0) {
|
||
state.userAttachmentStorageConfigs[index] = config;
|
||
} else {
|
||
state.userAttachmentStorageConfigs.push(config);
|
||
}
|
||
return config;
|
||
});
|
||
}
|
||
|
||
export async function getMasterAgentPromptPolicy() {
|
||
const state = await readState();
|
||
return state.masterAgentPromptPolicy ?? null;
|
||
}
|
||
|
||
export async function updateMasterAgentPromptPolicy(input: {
|
||
globalPrompt: string;
|
||
updatedBy?: string;
|
||
}) {
|
||
const globalPrompt = input.globalPrompt.trim();
|
||
if (!globalPrompt) {
|
||
throw new Error("MASTER_AGENT_PROMPT_REQUIRED");
|
||
}
|
||
|
||
return mutateState((state) => {
|
||
const policy: MasterAgentPromptPolicy = {
|
||
globalPrompt,
|
||
updatedBy: input.updatedBy?.trim() || undefined,
|
||
updatedAt: nowIso(),
|
||
};
|
||
state.masterAgentPromptPolicy = policy;
|
||
return policy;
|
||
});
|
||
}
|
||
|
||
export async function getUserMasterPrompt(account: string) {
|
||
const state = await readState();
|
||
return state.userMasterPrompts.find((item) => item.account === account) ?? null;
|
||
}
|
||
|
||
export async function updateUserMasterPrompt(account: string, content: string) {
|
||
const trimmedContent = content.trim();
|
||
if (!trimmedContent) {
|
||
throw new Error("USER_MASTER_PROMPT_REQUIRED");
|
||
}
|
||
|
||
return mutateState((state) => {
|
||
const next: UserMasterPrompt = {
|
||
account,
|
||
content: trimmedContent,
|
||
updatedAt: nowIso(),
|
||
};
|
||
const existing = state.userMasterPrompts.find((item) => item.account === account);
|
||
if (existing) {
|
||
Object.assign(existing, next);
|
||
} else {
|
||
state.userMasterPrompts.unshift(next);
|
||
}
|
||
return next;
|
||
});
|
||
}
|
||
|
||
export async function clearUserMasterPrompt(account: string) {
|
||
return mutateState((state) => {
|
||
const before = state.userMasterPrompts.length;
|
||
state.userMasterPrompts = state.userMasterPrompts.filter((item) => item.account !== account);
|
||
return { cleared: before !== state.userMasterPrompts.length };
|
||
});
|
||
}
|
||
|
||
export async function listUserMasterMemories(
|
||
account: string,
|
||
options?: { includeArchived?: boolean; scope?: MasterMemoryScope; projectId?: string },
|
||
) {
|
||
const state = await readState();
|
||
const includeArchived = options?.includeArchived ?? false;
|
||
return [...state.masterAgentMemories]
|
||
.filter((memory) => {
|
||
if (memory.account !== account) return false;
|
||
if (!includeArchived && memory.archived) return false;
|
||
if (options?.scope && memory.scope !== options.scope) return false;
|
||
if (options?.projectId && memory.projectId !== options.projectId) return false;
|
||
return true;
|
||
})
|
||
.sort((a, b) => {
|
||
const timeDiff =
|
||
messageTimeValue(b.lastUsedAt ?? b.updatedAt ?? b.createdAt) -
|
||
messageTimeValue(a.lastUsedAt ?? a.updatedAt ?? a.createdAt);
|
||
if (timeDiff !== 0) return timeDiff;
|
||
return b.memoryId.localeCompare(a.memoryId);
|
||
});
|
||
}
|
||
|
||
export async function createUserMasterMemory(input: {
|
||
account: string;
|
||
scope: MasterMemoryScope;
|
||
projectId?: string;
|
||
title: string;
|
||
content: string;
|
||
memoryType: MasterMemoryType;
|
||
tags?: string[];
|
||
sourceMessageId?: string;
|
||
}) {
|
||
const title = input.title.trim();
|
||
const content = input.content.trim();
|
||
if (!title) {
|
||
throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
|
||
}
|
||
if (!content) {
|
||
throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
|
||
}
|
||
if (input.scope === "project" && !input.projectId?.trim()) {
|
||
throw new Error("USER_MASTER_MEMORY_PROJECT_ID_REQUIRED");
|
||
}
|
||
|
||
return mutateState((state) => {
|
||
const now = nowIso();
|
||
const memory: MasterAgentMemory = {
|
||
memoryId: randomToken("memory"),
|
||
account: input.account,
|
||
scope: input.scope,
|
||
projectId: input.scope === "project" ? input.projectId?.trim() : undefined,
|
||
title,
|
||
content,
|
||
memoryType: input.memoryType,
|
||
tags: normalizeMasterMemoryTags(input.tags),
|
||
sourceMessageId: input.sourceMessageId,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
lastUsedAt: now,
|
||
archived: false,
|
||
};
|
||
state.masterAgentMemories.unshift(memory);
|
||
return memory;
|
||
});
|
||
}
|
||
|
||
export async function updateUserMasterMemory(
|
||
memoryId: string,
|
||
account: string,
|
||
patch: Partial<
|
||
Pick<
|
||
MasterAgentMemory,
|
||
"scope" | "projectId" | "title" | "content" | "memoryType" | "tags" | "sourceMessageId" | "lastUsedAt"
|
||
>
|
||
>,
|
||
) {
|
||
return mutateState((state) => {
|
||
const memory = state.masterAgentMemories.find(
|
||
(item) => item.memoryId === memoryId && item.account === account,
|
||
);
|
||
if (!memory) {
|
||
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
|
||
}
|
||
|
||
if (patch.scope) {
|
||
memory.scope = patch.scope;
|
||
}
|
||
if (memory.scope === "project" && patch.projectId !== undefined) {
|
||
memory.projectId = patch.projectId.trim() || undefined;
|
||
}
|
||
if (memory.scope !== "project") {
|
||
memory.projectId = undefined;
|
||
}
|
||
if (patch.title !== undefined) {
|
||
const title = patch.title.trim();
|
||
if (!title) throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
|
||
memory.title = title;
|
||
}
|
||
if (patch.content !== undefined) {
|
||
const content = patch.content.trim();
|
||
if (!content) throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
|
||
memory.content = content;
|
||
}
|
||
if (patch.memoryType) {
|
||
memory.memoryType = patch.memoryType;
|
||
}
|
||
if (patch.tags) {
|
||
memory.tags = normalizeMasterMemoryTags(patch.tags);
|
||
}
|
||
if (patch.sourceMessageId !== undefined) {
|
||
memory.sourceMessageId = patch.sourceMessageId;
|
||
}
|
||
if (patch.lastUsedAt !== undefined) {
|
||
memory.lastUsedAt = patch.lastUsedAt;
|
||
}
|
||
memory.updatedAt = nowIso();
|
||
return memory;
|
||
});
|
||
}
|
||
|
||
export async function archiveUserMasterMemory(memoryId: string, account: string) {
|
||
return mutateState((state) => {
|
||
const memory = state.masterAgentMemories.find(
|
||
(item) => item.memoryId === memoryId && item.account === account,
|
||
);
|
||
if (!memory) {
|
||
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
|
||
}
|
||
|
||
memory.archived = true;
|
||
memory.updatedAt = nowIso();
|
||
return memory;
|
||
});
|
||
}
|
||
|
||
export async function touchUserMasterMemories(memoryIds: string[], account: string) {
|
||
const normalizedIds = Array.from(new Set(memoryIds.map((value) => value.trim()).filter(Boolean)));
|
||
if (normalizedIds.length === 0) {
|
||
return [];
|
||
}
|
||
return mutateState((state) => {
|
||
const now = nowIso();
|
||
const touched: MasterAgentMemory[] = [];
|
||
for (const memory of state.masterAgentMemories) {
|
||
if (memory.account !== account) continue;
|
||
if (!normalizedIds.includes(memory.memoryId)) continue;
|
||
memory.lastUsedAt = now;
|
||
touched.push(memory);
|
||
}
|
||
return touched;
|
||
});
|
||
}
|
||
|
||
function normalizeAutoMemoryText(value: string | undefined) {
|
||
return (value ?? "")
|
||
.replace(/\s+/g, " ")
|
||
.replace(/[。;;!!]+$/g, "")
|
||
.trim();
|
||
}
|
||
|
||
function isLowValueAutoMemoryText(text: string) {
|
||
const normalized = normalizeAutoMemoryText(text);
|
||
if (!normalized) {
|
||
return true;
|
||
}
|
||
if (normalized.length < 10) {
|
||
return true;
|
||
}
|
||
if (/^(好的|收到|明白|继续|先这样|可以|行|没问题|辛苦了|谢谢|了解|嗯嗯)$/i.test(normalized)) {
|
||
return true;
|
||
}
|
||
if (/(马上|稍后|回头|等下|一会|临时|先看下|先试试|先这样)/i.test(normalized)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function inferAutoMemoryType(text: string): MasterMemoryType | null {
|
||
if (!text.trim()) return null;
|
||
if (/(微信|wechat|中文回复|中文沟通|UI风格|交互风格|偏好|习惯|默认)/i.test(text)) {
|
||
return "user_preference";
|
||
}
|
||
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
|
||
return "workflow_rule";
|
||
}
|
||
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
|
||
return "blocking_issue";
|
||
}
|
||
if (/(风险|隐患|告警)/i.test(text)) {
|
||
return "risk";
|
||
}
|
||
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
|
||
return "decision";
|
||
}
|
||
if (/(调研|研究|结论)/i.test(text)) {
|
||
return "research_note";
|
||
}
|
||
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
|
||
return "project_progress";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function inferProjectAutoMemoryType(text: string): Exclude<MasterMemoryType, "user_preference"> | null {
|
||
if (!text.trim()) return null;
|
||
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
|
||
return "blocking_issue";
|
||
}
|
||
if (/(风险|隐患|告警)/i.test(text)) {
|
||
return "risk";
|
||
}
|
||
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
|
||
return "decision";
|
||
}
|
||
if (/(调研|研究|结论)/i.test(text)) {
|
||
return "research_note";
|
||
}
|
||
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
|
||
return "project_progress";
|
||
}
|
||
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
|
||
return "workflow_rule";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildAutoMemoryTitle(memoryType: MasterMemoryType, label?: string) {
|
||
const typeLabel =
|
||
memoryType === "user_preference"
|
||
? "偏好"
|
||
: memoryType === "workflow_rule"
|
||
? "工作规则"
|
||
: memoryType === "blocking_issue"
|
||
? "阻塞"
|
||
: memoryType === "risk"
|
||
? "风险"
|
||
: memoryType === "decision"
|
||
? "决策"
|
||
: memoryType === "research_note"
|
||
? "调研结论"
|
||
: "项目进度";
|
||
return label ? `${label} · ${typeLabel}` : typeLabel;
|
||
}
|
||
|
||
function detectReferencedProjectForMemory(state: BossState, text: string) {
|
||
const lowered = text.toLowerCase();
|
||
const candidates = state.projects
|
||
.filter((project) => project.id !== "master-agent")
|
||
.flatMap((project) => {
|
||
const rawAliases = [
|
||
project.id,
|
||
project.name,
|
||
project.threadMeta.folderName,
|
||
project.threadMeta.threadDisplayName,
|
||
]
|
||
.map((value) => value.trim())
|
||
.filter(Boolean);
|
||
|
||
const aliases = Array.from(
|
||
new Set(
|
||
rawAliases.flatMap((alias) => {
|
||
const normalized = alias.trim();
|
||
if (!normalized) {
|
||
return [];
|
||
}
|
||
|
||
const tokenCandidates = normalized
|
||
.split(/[\s\-_/]+/)
|
||
.map((token) => token.trim())
|
||
.filter((token) => token.length >= 3);
|
||
|
||
return [normalized, ...tokenCandidates];
|
||
}),
|
||
),
|
||
);
|
||
return aliases.map((alias) => ({
|
||
projectId: project.id,
|
||
projectName: project.name,
|
||
alias,
|
||
}));
|
||
})
|
||
.sort((left, right) => right.alias.length - left.alias.length);
|
||
|
||
return candidates.find((candidate) => lowered.includes(candidate.alias.toLowerCase())) ?? null;
|
||
}
|
||
|
||
function upsertAutoMasterMemoryInState(
|
||
state: BossState,
|
||
input: {
|
||
account: string;
|
||
scope: MasterMemoryScope;
|
||
projectId?: string;
|
||
title: string;
|
||
content: string;
|
||
memoryType: MasterMemoryType;
|
||
tags: string[];
|
||
sourceMessageId?: string;
|
||
},
|
||
) {
|
||
const now = nowIso();
|
||
const existing = state.masterAgentMemories.find(
|
||
(memory) =>
|
||
memory.account === input.account &&
|
||
memory.scope === input.scope &&
|
||
(memory.projectId ?? undefined) === (input.projectId ?? undefined) &&
|
||
memory.title === input.title,
|
||
);
|
||
|
||
if (existing) {
|
||
existing.content = input.content;
|
||
existing.memoryType = input.memoryType;
|
||
existing.tags = normalizeMasterMemoryTags(input.tags);
|
||
existing.sourceMessageId = input.sourceMessageId ?? existing.sourceMessageId;
|
||
existing.archived = false;
|
||
existing.updatedAt = now;
|
||
existing.lastUsedAt = now;
|
||
return existing;
|
||
}
|
||
|
||
const memory: MasterAgentMemory = {
|
||
memoryId: randomToken("memory"),
|
||
account: input.account,
|
||
scope: input.scope,
|
||
projectId: input.scope === "project" ? input.projectId : undefined,
|
||
title: input.title,
|
||
content: input.content,
|
||
memoryType: input.memoryType,
|
||
tags: normalizeMasterMemoryTags(input.tags),
|
||
sourceMessageId: input.sourceMessageId,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
lastUsedAt: now,
|
||
archived: false,
|
||
};
|
||
state.masterAgentMemories.unshift(memory);
|
||
return memory;
|
||
}
|
||
|
||
function autoCaptureMasterAgentMemoriesInState(
|
||
state: BossState,
|
||
input: {
|
||
account: string;
|
||
requestText: string;
|
||
replyText: string;
|
||
sourceMessageId?: string;
|
||
},
|
||
) {
|
||
const requestText = normalizeAutoMemoryText(input.requestText);
|
||
const replyText = normalizeAutoMemoryText(input.replyText);
|
||
if (!requestText && !replyText) {
|
||
return [];
|
||
}
|
||
|
||
const createdOrUpdated: MasterAgentMemory[] = [];
|
||
const combined = [requestText, replyText].filter(Boolean).join(" ");
|
||
const preferenceCandidate = isLowValueAutoMemoryText(requestText) ? "" : requestText;
|
||
const projectCandidate = isLowValueAutoMemoryText(replyText) ? combined : replyText || combined;
|
||
const preferenceType = inferAutoMemoryType(preferenceCandidate);
|
||
|
||
if (
|
||
preferenceCandidate &&
|
||
(preferenceType === "user_preference" || preferenceType === "workflow_rule")
|
||
) {
|
||
createdOrUpdated.push(
|
||
upsertAutoMasterMemoryInState(state, {
|
||
account: input.account,
|
||
scope: "global",
|
||
title: buildAutoMemoryTitle(preferenceType),
|
||
content: preferenceCandidate,
|
||
memoryType: preferenceType,
|
||
tags: preferenceType === "user_preference" ? ["用户偏好"] : ["工作方式"],
|
||
sourceMessageId: input.sourceMessageId,
|
||
}),
|
||
);
|
||
}
|
||
|
||
const referencedProject = detectReferencedProjectForMemory(state, combined);
|
||
const projectType = inferProjectAutoMemoryType(projectCandidate) ?? inferProjectAutoMemoryType(combined);
|
||
if (referencedProject && projectType && !isLowValueAutoMemoryText(projectCandidate)) {
|
||
createdOrUpdated.push(
|
||
upsertAutoMasterMemoryInState(state, {
|
||
account: input.account,
|
||
scope: "project",
|
||
projectId: referencedProject.projectId,
|
||
title: buildAutoMemoryTitle(projectType, referencedProject.projectName),
|
||
content: projectCandidate,
|
||
memoryType: projectType,
|
||
tags: [referencedProject.projectName, referencedProject.alias],
|
||
sourceMessageId: input.sourceMessageId,
|
||
}),
|
||
);
|
||
}
|
||
|
||
return createdOrUpdated;
|
||
}
|
||
|
||
function preferredDeviceForAccount(
|
||
state: BossState,
|
||
account: string,
|
||
preferredDeviceId?: string,
|
||
) {
|
||
if (preferredDeviceId) {
|
||
const preferred = state.devices.find(
|
||
(device) =>
|
||
device.id === preferredDeviceId &&
|
||
device.source === "production" &&
|
||
device.account === account,
|
||
);
|
||
if (preferred) {
|
||
return preferred;
|
||
}
|
||
}
|
||
|
||
return (
|
||
state.devices.find(
|
||
(device) => device.source === "production" && device.account === account,
|
||
) ?? null
|
||
);
|
||
}
|
||
|
||
export function getPreferredDeviceIdForAccountFromState(
|
||
state: BossState,
|
||
account: string,
|
||
preferredDeviceId?: string,
|
||
) {
|
||
return preferredDeviceForAccount(state, account, preferredDeviceId)?.id;
|
||
}
|
||
|
||
export async function getPreferredDeviceIdForAccount(
|
||
account: string,
|
||
preferredDeviceId?: string,
|
||
) {
|
||
const state = await readState();
|
||
return getPreferredDeviceIdForAccountFromState(state, account, preferredDeviceId);
|
||
}
|
||
|
||
export async function toggleGoal(projectId: string, goalId: string) {
|
||
return mutateState((state) => {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const goal = project.goals.find((item) => item.id === goalId);
|
||
if (!goal) throw new Error("GOAL_NOT_FOUND");
|
||
|
||
if (goal.state === "completed") {
|
||
goal.state = "pending";
|
||
goal.completedAt = undefined;
|
||
goal.completedBy = undefined;
|
||
goal.note = "重新打开 · 等待继续执行";
|
||
} else {
|
||
goal.state = "completed";
|
||
goal.completedAt = nowIso();
|
||
goal.completedBy = "用户 / 主 Agent";
|
||
goal.note = "已完成 · 由用户或主 Agent 标记";
|
||
}
|
||
|
||
project.lastMessageAt = nowIso();
|
||
return goal;
|
||
});
|
||
}
|
||
|
||
export async function updateGoalText(projectId: string, goalId: string, text: string) {
|
||
return mutateState((state) => {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const goal = project.goals.find((item) => item.id === goalId);
|
||
if (!goal) throw new Error("GOAL_NOT_FOUND");
|
||
if (!text.trim()) throw new Error("GOAL_TEXT_REQUIRED");
|
||
|
||
goal.text = text.trim();
|
||
goal.note = "已编辑 · 主 Agent 将据此重排后续任务";
|
||
project.lastMessageAt = nowIso();
|
||
return goal;
|
||
});
|
||
}
|
||
|
||
export async function createGoal(projectId: string, text: string) {
|
||
return mutateState((state) => {
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||
if (!text.trim()) throw new Error("GOAL_TEXT_REQUIRED");
|
||
|
||
const goal: GoalItem = {
|
||
id: randomToken("goal"),
|
||
text: text.trim(),
|
||
state: "pending",
|
||
note: "新建目标 · 等待主 Agent 安排优先级",
|
||
};
|
||
|
||
project.goals.unshift(goal);
|
||
project.lastMessageAt = nowIso();
|
||
return goal;
|
||
});
|
||
}
|
||
|
||
export async function issueVerificationCode(
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
) {
|
||
return mutateState((state) => {
|
||
validateVerificationPurpose(state, account, purpose);
|
||
validateVerificationSendWindow(state, account, purpose);
|
||
const code =
|
||
getVerificationDeliveryMode() === "email" ? randomDigits(6) : getFixedVerificationCode();
|
||
const record: VerificationCode = {
|
||
id: `${purpose}-${Date.now()}`,
|
||
account,
|
||
purpose,
|
||
code,
|
||
createdAt: nowIso(),
|
||
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
||
};
|
||
|
||
state.verificationCodes = [
|
||
record,
|
||
...state.verificationCodes.filter(
|
||
(item) => !(item.account === account && item.purpose === purpose),
|
||
),
|
||
].slice(0, 30);
|
||
recordVerificationDispatch(
|
||
state,
|
||
account,
|
||
purpose,
|
||
getVerificationDeliveryMode(),
|
||
"requested",
|
||
"验证码请求已创建,等待投递结果。",
|
||
);
|
||
|
||
return record;
|
||
});
|
||
}
|
||
|
||
export function hashPassword(password: string) {
|
||
const normalized = password.normalize("NFKC");
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = scryptSync(normalized, `boss:${salt}`, 64).toString("hex");
|
||
return `scrypt$${salt}$${hash}`;
|
||
}
|
||
|
||
function hashPasswordLegacy(password: string) {
|
||
return createHash("sha256").update(`boss:${password.normalize("NFKC")}`).digest("hex");
|
||
}
|
||
|
||
function verifyPasswordHash(password: string, passwordHash: string) {
|
||
const normalized = password.normalize("NFKC");
|
||
if (!passwordHash.startsWith("scrypt$")) {
|
||
return passwordHash === hashPasswordLegacy(normalized);
|
||
}
|
||
|
||
const [, salt, expectedHash] = passwordHash.split("$");
|
||
if (!salt || !expectedHash) return false;
|
||
|
||
const expectedBuffer = Buffer.from(expectedHash, "hex");
|
||
const actualBuffer = scryptSync(normalized, `boss:${salt}`, expectedBuffer.length);
|
||
|
||
return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
|
||
}
|
||
|
||
function recordVerificationDispatch(
|
||
state: BossState,
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
deliveryMode: VerificationDeliveryMode,
|
||
status: VerificationDispatch["status"],
|
||
note: string,
|
||
) {
|
||
state.verificationDispatches.unshift({
|
||
dispatchId: randomToken("vdispatch"),
|
||
account,
|
||
purpose,
|
||
deliveryMode,
|
||
requestedAt: nowIso(),
|
||
status,
|
||
note,
|
||
});
|
||
}
|
||
|
||
function validateVerificationSendWindow(
|
||
state: BossState,
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
) {
|
||
const relevant = state.verificationDispatches
|
||
.filter((item) => item.account === account && item.purpose === purpose)
|
||
.sort((a, b) => b.requestedAt.localeCompare(a.requestedAt));
|
||
const latest = relevant[0];
|
||
if (latest && Date.now() - new Date(latest.requestedAt).getTime() < VERIFICATION_SEND_COOLDOWN_MS) {
|
||
throw new Error("VERIFICATION_CODE_COOLDOWN");
|
||
}
|
||
const recentCount = relevant.filter(
|
||
(item) => Date.now() - new Date(item.requestedAt).getTime() < VERIFICATION_SEND_WINDOW_MS,
|
||
).length;
|
||
if (recentCount >= VERIFICATION_SEND_WINDOW_LIMIT) {
|
||
throw new Error("VERIFICATION_RATE_LIMITED");
|
||
}
|
||
}
|
||
|
||
function validateVerificationPurpose(
|
||
state: BossState,
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
) {
|
||
const existing = state.authAccounts.find((item) => item.account === account);
|
||
if (purpose === "register") {
|
||
if (existing) {
|
||
throw new Error("ACCOUNT_ALREADY_EXISTS");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!existing) {
|
||
throw new Error("ACCOUNT_NOT_FOUND");
|
||
}
|
||
}
|
||
|
||
function findValidCodeIndex(
|
||
state: BossState,
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
code: string,
|
||
) {
|
||
const index = state.verificationCodes.findIndex(
|
||
(item) => item.account === account && item.purpose === purpose && item.code === code,
|
||
);
|
||
if (index < 0) return null;
|
||
const record = state.verificationCodes[index];
|
||
if (new Date(record.expiresAt).getTime() <= Date.now()) {
|
||
return null;
|
||
}
|
||
return index;
|
||
}
|
||
|
||
function consumeVerificationCode(
|
||
state: BossState,
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
code: string,
|
||
) {
|
||
const index = findValidCodeIndex(state, account, purpose, code);
|
||
if (index === null) return false;
|
||
if (index >= 0) {
|
||
state.verificationCodes.splice(index, 1);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function shouldAcceptDirectFixedVerificationCode(
|
||
purpose: VerificationCode["purpose"],
|
||
code?: string,
|
||
) {
|
||
if (purpose !== "login") return false;
|
||
if (getVerificationDeliveryMode() !== "fixed") return false;
|
||
return code?.trim() === getFixedVerificationCode();
|
||
}
|
||
|
||
function isLikelyEmailAccount(account: string) {
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(account);
|
||
}
|
||
|
||
function verificationRecipientForAccount(state: BossState, account: string) {
|
||
if (isLikelyEmailAccount(account)) {
|
||
return account;
|
||
}
|
||
|
||
return state.authAccounts.find((item) => item.account === account)?.verificationEmail ?? null;
|
||
}
|
||
|
||
function registerLoginFailure(account: AuthAccount) {
|
||
const attempts = (account.failedLoginAttempts ?? 0) + 1;
|
||
account.failedLoginAttempts = attempts;
|
||
account.updatedAt = nowIso();
|
||
|
||
if (attempts >= AUTH_LOGIN_LOCK_THRESHOLD) {
|
||
account.lockedUntil = new Date(Date.now() + AUTH_LOGIN_LOCK_MS).toISOString();
|
||
account.failedLoginAttempts = 0;
|
||
}
|
||
}
|
||
|
||
function clearLoginFailure(account: AuthAccount) {
|
||
account.failedLoginAttempts = 0;
|
||
account.lockedUntil = undefined;
|
||
}
|
||
|
||
function activeAuthSession(state: BossState, token?: string | null) {
|
||
if (!token) return null;
|
||
const session = state.authSessions.find((item) => item.sessionToken === token);
|
||
if (!session || session.revokedAt) return null;
|
||
if (new Date(session.expiresAt).getTime() <= Date.now()) {
|
||
session.revokedAt = nowIso();
|
||
return null;
|
||
}
|
||
return session;
|
||
}
|
||
|
||
function activeAuthSessionByRestoreToken(state: BossState, restoreToken?: string | null) {
|
||
if (!restoreToken) return null;
|
||
const session = state.authSessions.find((item) => item.restoreToken === restoreToken);
|
||
if (!session || session.revokedAt) return null;
|
||
if (new Date(session.expiresAt).getTime() <= Date.now()) {
|
||
session.revokedAt = nowIso();
|
||
return null;
|
||
}
|
||
return session;
|
||
}
|
||
|
||
export async function createAuthSession(params: {
|
||
account: string;
|
||
role: AuthRole;
|
||
displayName: string;
|
||
loginMethod: LoginMethod;
|
||
}) {
|
||
return mutateState((state) => {
|
||
state.authSessions = state.authSessions.filter((session) => session.account !== params.account);
|
||
const createdAt = nowIso();
|
||
const session: AuthSession = {
|
||
sessionId: randomToken("session"),
|
||
sessionToken: randomBytes(24).toString("hex"),
|
||
restoreToken: randomBytes(24).toString("hex"),
|
||
account: params.account,
|
||
role: params.role,
|
||
displayName: params.displayName,
|
||
loginMethod: params.loginMethod,
|
||
createdAt,
|
||
expiresAt: new Date(Date.now() + AUTH_SESSION_TTL_MS).toISOString(),
|
||
lastSeenAt: createdAt,
|
||
};
|
||
state.authSessions.unshift(session);
|
||
return session;
|
||
});
|
||
}
|
||
|
||
export async function createPrimaryAdminSession() {
|
||
return mutateState((state) => {
|
||
let existing = state.authAccounts.find((item) => item.account === PRIMARY_ADMIN_ACCOUNT);
|
||
if (!existing) {
|
||
existing = {
|
||
id: `account-${PRIMARY_ADMIN_ACCOUNT}`,
|
||
account: PRIMARY_ADMIN_ACCOUNT,
|
||
passwordHash: hashPassword(PRIMARY_ADMIN_PASSWORD),
|
||
displayName: "Boss 超级管理员",
|
||
role: "highest_admin",
|
||
verificationEmail: PRIMARY_ADMIN_VERIFICATION_EMAIL,
|
||
codexNodeId: PRIMARY_CODEX_NODE_ID,
|
||
codexNodeLabel: PRIMARY_CODEX_NODE_LABEL,
|
||
primaryDeviceId: PRIMARY_CODEX_NODE_ID,
|
||
isPrimary: true,
|
||
createdAt: nowIso(),
|
||
updatedAt: nowIso(),
|
||
};
|
||
state.authAccounts.unshift(existing);
|
||
}
|
||
|
||
clearLoginFailure(existing);
|
||
existing.updatedAt = nowIso();
|
||
existing.lastLoginAt = nowIso();
|
||
existing.lastLoginMethod = "password";
|
||
|
||
const session = {
|
||
sessionId: randomToken("session"),
|
||
sessionToken: randomBytes(24).toString("hex"),
|
||
restoreToken: randomBytes(24).toString("hex"),
|
||
account: existing.account,
|
||
role: existing.role,
|
||
displayName: existing.displayName,
|
||
loginMethod: "password" as const,
|
||
createdAt: nowIso(),
|
||
expiresAt: new Date(Date.now() + AUTH_SESSION_TTL_MS).toISOString(),
|
||
lastSeenAt: nowIso(),
|
||
} satisfies AuthSession;
|
||
|
||
state.authSessions = [
|
||
session,
|
||
...state.authSessions.filter((item) => item.account !== existing.account),
|
||
].slice(0, 20);
|
||
|
||
return {
|
||
account: existing.account,
|
||
role: existing.role,
|
||
displayName: existing.displayName,
|
||
loginMethod: session.loginMethod,
|
||
sessionToken: session.sessionToken,
|
||
restoreToken: session.restoreToken,
|
||
sessionExpiresAt: session.expiresAt,
|
||
};
|
||
});
|
||
}
|
||
|
||
export async function getVerificationDeliveryTarget(account: string) {
|
||
const state = await readState();
|
||
return verificationRecipientForAccount(state, account);
|
||
}
|
||
|
||
export async function getAuthSession(token?: string | null) {
|
||
return mutateState((state) => {
|
||
const session = activeAuthSession(state, token);
|
||
if (!session) return null;
|
||
session.lastSeenAt = nowIso();
|
||
return { ...session };
|
||
});
|
||
}
|
||
|
||
export async function restoreAuthSession(restoreToken?: string | null) {
|
||
return mutateState((state) => {
|
||
const session = activeAuthSessionByRestoreToken(state, restoreToken);
|
||
if (!session) return null;
|
||
session.lastSeenAt = nowIso();
|
||
return { ...session };
|
||
});
|
||
}
|
||
|
||
export async function revokeAuthSession(token?: string | null) {
|
||
if (!token) return;
|
||
await mutateState((state) => {
|
||
const session = state.authSessions.find((item) => item.sessionToken === token);
|
||
if (session && !session.revokedAt) {
|
||
session.revokedAt = nowIso();
|
||
}
|
||
});
|
||
}
|
||
|
||
function setActiveAiAccountInState(
|
||
state: BossState,
|
||
accountId: string,
|
||
reason: string,
|
||
options?: { preserveLastSwitchAt?: boolean },
|
||
) {
|
||
const currentActive = state.aiAccounts.find((item) => item.isActive);
|
||
const nextActive = state.aiAccounts.find((item) => item.accountId === accountId);
|
||
if (!nextActive) {
|
||
throw new Error("AI_ACCOUNT_NOT_FOUND");
|
||
}
|
||
|
||
const switchedAt = nowIso();
|
||
for (const account of state.aiAccounts) {
|
||
account.isActive = account.accountId === accountId;
|
||
account.updatedAt = switchedAt;
|
||
if (account.accountId === accountId) {
|
||
account.lastSwitchedAt = options?.preserveLastSwitchAt ? account.lastSwitchedAt : switchedAt;
|
||
account.switchReason = reason;
|
||
}
|
||
}
|
||
|
||
if (currentActive?.accountId !== nextActive.accountId || reason !== nextActive.switchReason) {
|
||
state.aiAccountSwitchHistory.unshift({
|
||
switchId: randomToken("aiswitch"),
|
||
fromAccountId: currentActive?.accountId,
|
||
fromLabel: currentActive?.label,
|
||
toAccountId: nextActive.accountId,
|
||
toLabel: nextActive.label,
|
||
role: nextActive.role,
|
||
switchedAt,
|
||
reason,
|
||
});
|
||
}
|
||
|
||
return nextActive;
|
||
}
|
||
|
||
export async function listAiAccounts() {
|
||
const state = await readState();
|
||
return {
|
||
accounts: getAiAccountSummariesFromState(state),
|
||
activeIdentity: getMasterIdentitySummaryFromState(state),
|
||
switchHistory: [...state.aiAccountSwitchHistory].sort((a, b) =>
|
||
b.switchedAt.localeCompare(a.switchedAt),
|
||
),
|
||
};
|
||
}
|
||
|
||
export async function getAiAccount(accountId: string) {
|
||
const state = await readState();
|
||
const account = state.aiAccounts.find((item) => item.accountId === accountId);
|
||
if (!account) return null;
|
||
return buildAiAccountSummary(account);
|
||
}
|
||
|
||
export async function getRuntimeAiAccountById(accountId: string) {
|
||
if (accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||
return getEnvOpenAiAccount();
|
||
}
|
||
const state = await readState();
|
||
return state.aiAccounts.find((item) => item.accountId === accountId) ?? null;
|
||
}
|
||
|
||
export async function saveAiAccount(payload: {
|
||
accountId?: string;
|
||
label: string;
|
||
role: AiAccountRole;
|
||
provider: AiProvider;
|
||
displayName: string;
|
||
accountIdentifier?: string;
|
||
nodeId?: string;
|
||
nodeLabel?: string;
|
||
model?: string;
|
||
apiKey?: string;
|
||
enabled?: boolean;
|
||
setActive?: boolean;
|
||
loginStatusNote?: string;
|
||
}) {
|
||
return mutateState((state) => {
|
||
const existing = payload.accountId
|
||
? state.aiAccounts.find((item) => item.accountId === payload.accountId)
|
||
: null;
|
||
const accountId =
|
||
existing?.accountId ??
|
||
payload.accountId?.trim() ??
|
||
`ai-${slugify(`${payload.label}-${payload.displayName}`)}`;
|
||
const defaultModel =
|
||
payload.provider === "aliyun_qwen_api"
|
||
? "qwen3.5-plus"
|
||
: payload.provider === "openai_api"
|
||
? "gpt-5.4"
|
||
: undefined;
|
||
const next: AiAccount = normalizeAiAccount({
|
||
accountId,
|
||
label: payload.label.trim() || aiRoleLabel(payload.role),
|
||
role: payload.role,
|
||
provider: payload.provider,
|
||
displayName: payload.displayName.trim() || "未命名 AI",
|
||
accountIdentifier: payload.accountIdentifier?.trim() || undefined,
|
||
nodeId: payload.nodeId?.trim() || undefined,
|
||
nodeLabel: payload.nodeLabel?.trim() || undefined,
|
||
model: payload.model?.trim() || defaultModel,
|
||
apiKey:
|
||
isApiKeyProvider(payload.provider)
|
||
? payload.apiKey?.trim()
|
||
? payload.apiKey.trim()
|
||
: existing?.apiKey
|
||
: undefined,
|
||
apiKeyMasked:
|
||
isApiKeyProvider(payload.provider)
|
||
? maskApiKey(payload.apiKey?.trim() || existing?.apiKey)
|
||
: undefined,
|
||
enabled: payload.enabled ?? existing?.enabled ?? true,
|
||
isActive: existing?.isActive ?? false,
|
||
status:
|
||
isApiKeyProvider(payload.provider)
|
||
? payload.apiKey?.trim() || existing?.apiKey
|
||
? existing?.status === "degraded"
|
||
? "degraded"
|
||
: "ready"
|
||
: "needs_api_key"
|
||
: payload.nodeId?.trim() || existing?.nodeId
|
||
? existing?.status === "degraded"
|
||
? "degraded"
|
||
: "ready"
|
||
: "needs_login",
|
||
loginStatusNote: payload.loginStatusNote?.trim() || existing?.loginStatusNote,
|
||
lastValidatedAt: existing?.lastValidatedAt,
|
||
lastUsedAt: existing?.lastUsedAt,
|
||
lastError: existing?.lastError,
|
||
lastSwitchedAt: existing?.lastSwitchedAt,
|
||
switchReason: existing?.switchReason,
|
||
createdAt: existing?.createdAt ?? nowIso(),
|
||
updatedAt: nowIso(),
|
||
});
|
||
|
||
if (existing) {
|
||
const index = state.aiAccounts.findIndex((item) => item.accountId === existing.accountId);
|
||
state.aiAccounts[index] = next;
|
||
} else {
|
||
state.aiAccounts.unshift(next);
|
||
}
|
||
|
||
if (payload.setActive ?? (!existing && next.role === "primary")) {
|
||
setActiveAiAccountInState(state, next.accountId, existing ? "手动更新 AI 账号配置" : "新增 AI 账号并设为当前主控");
|
||
}
|
||
|
||
return buildAiAccountSummary(next);
|
||
});
|
||
}
|
||
|
||
export async function deleteAiAccount(accountId: string) {
|
||
if (accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||
throw new Error("ENV_AI_ACCOUNT_READ_ONLY");
|
||
}
|
||
return mutateState((state) => {
|
||
const target = state.aiAccounts.find((item) => item.accountId === accountId);
|
||
if (!target) {
|
||
throw new Error("AI_ACCOUNT_NOT_FOUND");
|
||
}
|
||
state.aiAccounts = state.aiAccounts.filter((item) => item.accountId !== accountId);
|
||
if (target.isActive) {
|
||
const fallback = sortAiAccounts(state.aiAccounts).find((item) => item.enabled);
|
||
if (fallback) {
|
||
setActiveAiAccountInState(state, fallback.accountId, `删除 ${target.label} 后自动切换`);
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
export async function activateAiAccount(accountId: string, reason: string) {
|
||
if (accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||
const state = await readState();
|
||
return {
|
||
activeIdentity: {
|
||
...getMasterIdentitySummaryFromState(state),
|
||
accountId: ENV_OPENAI_ACCOUNT_ID,
|
||
label: "API 容灾",
|
||
role: "api_fallback" as const,
|
||
roleLabel: aiRoleLabel("api_fallback"),
|
||
provider: "openai_api" as const,
|
||
providerLabel: aiProviderLabel("openai_api"),
|
||
isEnvironmentFallback: true,
|
||
},
|
||
};
|
||
}
|
||
return mutateState((state) => {
|
||
setActiveAiAccountInState(state, accountId, reason);
|
||
return {
|
||
activeIdentity: getMasterIdentitySummaryFromState(state),
|
||
};
|
||
});
|
||
}
|
||
|
||
export async function updateAiAccountHealth(params: {
|
||
accountId: string;
|
||
status: AiAccountStatus;
|
||
lastError?: string;
|
||
lastValidatedAt?: string;
|
||
lastUsedAt?: string;
|
||
switchReason?: string;
|
||
activate?: boolean;
|
||
}) {
|
||
if (params.accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||
return;
|
||
}
|
||
await mutateState((state) => {
|
||
const account = state.aiAccounts.find((item) => item.accountId === params.accountId);
|
||
if (!account) {
|
||
throw new Error("AI_ACCOUNT_NOT_FOUND");
|
||
}
|
||
account.status = params.status;
|
||
account.lastError = params.lastError;
|
||
account.lastValidatedAt = params.lastValidatedAt ?? account.lastValidatedAt;
|
||
account.lastUsedAt = params.lastUsedAt ?? account.lastUsedAt;
|
||
account.updatedAt = nowIso();
|
||
if (params.switchReason) {
|
||
account.switchReason = params.switchReason;
|
||
account.lastSwitchedAt = nowIso();
|
||
}
|
||
if (params.activate) {
|
||
setActiveAiAccountInState(
|
||
state,
|
||
account.accountId,
|
||
params.switchReason ?? "AI 账号状态更新后设为当前主控",
|
||
{ preserveLastSwitchAt: true },
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
export async function getMasterAgentRuntimeAccount() {
|
||
const state = await readState();
|
||
const resolved = resolveActiveAiAccount(state);
|
||
if (!resolved.account) {
|
||
return null;
|
||
}
|
||
return {
|
||
account: resolved.account,
|
||
summary: getMasterIdentitySummaryFromState(state),
|
||
isEnvironmentFallback: resolved.isEnvironmentFallback,
|
||
};
|
||
}
|
||
|
||
export async function queueMasterAgentTask(payload: {
|
||
taskId?: string;
|
||
projectId?: string;
|
||
taskType?: MasterAgentTaskType;
|
||
requestMessageId: string;
|
||
requestText: string;
|
||
executionPrompt: string;
|
||
requestedBy: string;
|
||
requestedByAccount: string;
|
||
deviceId: string;
|
||
accountId?: string;
|
||
accountLabel?: string;
|
||
attachmentId?: string;
|
||
attachmentFileName?: string;
|
||
attachmentDownloadToken?: string;
|
||
attachmentDownloadExpiresAt?: string;
|
||
attachmentDownloadUrl?: string;
|
||
attachmentTextExcerpt?: string;
|
||
deviceImportDraftId?: string;
|
||
dispatchExecutionId?: string;
|
||
targetProjectId?: string;
|
||
targetThreadId?: string;
|
||
targetThreadDisplayName?: string;
|
||
targetCodexThreadRef?: string;
|
||
targetCodexFolderRef?: string;
|
||
}) {
|
||
const task = await mutateState((state) => {
|
||
const task: MasterAgentTask = {
|
||
taskId: payload.taskId ?? randomToken("mastertask"),
|
||
projectId: payload.projectId ?? "master-agent",
|
||
taskType: payload.taskType ?? "conversation_reply",
|
||
requestMessageId: payload.requestMessageId,
|
||
requestText: payload.requestText,
|
||
executionPrompt: payload.executionPrompt,
|
||
requestedBy: payload.requestedBy,
|
||
requestedByAccount: payload.requestedByAccount,
|
||
deviceId: payload.deviceId,
|
||
accountId: payload.accountId,
|
||
accountLabel: payload.accountLabel,
|
||
attachmentId: payload.attachmentId,
|
||
attachmentFileName: payload.attachmentFileName,
|
||
attachmentDownloadToken: payload.attachmentDownloadToken,
|
||
attachmentDownloadExpiresAt: payload.attachmentDownloadExpiresAt,
|
||
attachmentDownloadUrl: payload.attachmentDownloadUrl,
|
||
attachmentTextExcerpt: payload.attachmentTextExcerpt,
|
||
deviceImportDraftId: payload.deviceImportDraftId,
|
||
dispatchExecutionId: payload.dispatchExecutionId,
|
||
targetProjectId: payload.targetProjectId,
|
||
targetThreadId: payload.targetThreadId,
|
||
targetThreadDisplayName: payload.targetThreadDisplayName,
|
||
targetCodexThreadRef: payload.targetCodexThreadRef,
|
||
targetCodexFolderRef: payload.targetCodexFolderRef,
|
||
status: "queued",
|
||
requestedAt: nowIso(),
|
||
};
|
||
state.masterAgentTasks.unshift(task);
|
||
return task;
|
||
});
|
||
publishBossEvent("master_agent.task.updated", {
|
||
taskId: task.taskId,
|
||
deviceId: task.deviceId,
|
||
status: task.status,
|
||
});
|
||
return task;
|
||
}
|
||
|
||
export async function createDispatchPlan(input: {
|
||
groupProjectId: string;
|
||
requestMessageId: string;
|
||
requestedBy: string;
|
||
summary?: string;
|
||
targets: DispatchPlanTarget[];
|
||
}) {
|
||
return mutateState((state) => {
|
||
return upsertDispatchPlanInState(state, input);
|
||
});
|
||
}
|
||
|
||
function upsertDispatchPlanInState(
|
||
state: BossState,
|
||
input: {
|
||
groupProjectId: string;
|
||
requestMessageId: string;
|
||
requestedBy: string;
|
||
summary?: string;
|
||
targets: DispatchPlanTarget[];
|
||
},
|
||
) {
|
||
const groupProjectId = input.groupProjectId.trim();
|
||
const requestMessageId = input.requestMessageId.trim();
|
||
const requestedBy = input.requestedBy.trim();
|
||
const summary = input.summary?.trim() ?? "";
|
||
|
||
if (!groupProjectId) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_REQUIRED");
|
||
if (!requestMessageId) throw new Error("DISPATCH_PLAN_REQUEST_MESSAGE_REQUIRED");
|
||
if (!requestedBy) throw new Error("DISPATCH_PLAN_REQUESTED_BY_REQUIRED");
|
||
const groupProject = state.projects.find((item) => item.id === groupProjectId);
|
||
if (!groupProject) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_NOT_FOUND");
|
||
if (!groupProject.isGroup) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_INVALID");
|
||
|
||
const validatedTargets = normalizeDispatchPlanTargetsForCreate(state, input.targets);
|
||
const existing = state.dispatchPlans.find(
|
||
(plan) => plan.groupProjectId === groupProjectId && plan.requestMessageId === requestMessageId,
|
||
);
|
||
if (existing) {
|
||
const payloadMatches =
|
||
existing.requestedBy === requestedBy &&
|
||
existing.summary === summary &&
|
||
sameDispatchPlanTargets(existing.targets, validatedTargets);
|
||
if (!payloadMatches) {
|
||
throw new Error("DISPATCH_PLAN_RETRY_MISMATCH");
|
||
}
|
||
if (groupProject.collaborationMode === "approval_required") {
|
||
groupProject.approvalState = "pending_user";
|
||
}
|
||
return existing;
|
||
}
|
||
|
||
const plan: DispatchPlan = {
|
||
planId: randomToken("dispatch-plan"),
|
||
groupProjectId,
|
||
requestMessageId,
|
||
requestedBy,
|
||
status: "pending_user_confirmation",
|
||
targets: validatedTargets,
|
||
summary,
|
||
createdAt: nowIso(),
|
||
};
|
||
state.dispatchPlans.unshift(plan);
|
||
if (groupProject.collaborationMode === "approval_required") {
|
||
groupProject.approvalState = "pending_user";
|
||
const targetSummary = validatedTargets
|
||
.map((target) => {
|
||
const project = state.projects.find((item) => item.id === target.projectId);
|
||
return `《${project?.threadMeta.threadDisplayName ?? project?.name ?? target.projectId}》`;
|
||
})
|
||
.join("、");
|
||
pushProjectLedgerMessage(state, groupProjectId, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: `主 Agent 已生成推荐,等待你确认后再下发到 ${validatedTargets.length} 个线程:${targetSummary}。`,
|
||
kind: "system_notice",
|
||
});
|
||
} else {
|
||
groupProject.approvalState = "not_required";
|
||
}
|
||
return plan;
|
||
}
|
||
|
||
export async function listDispatchPlansByProject(groupProjectId: string) {
|
||
const state = await readState();
|
||
const normalizedGroupProjectId = groupProjectId.trim();
|
||
return state.dispatchPlans
|
||
.filter((plan) => plan.groupProjectId === normalizedGroupProjectId)
|
||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||
}
|
||
|
||
function applyDispatchPlanConfirmationInState(
|
||
state: BossState,
|
||
input: {
|
||
planId: string;
|
||
confirmedBy: string;
|
||
approvedTargetProjectIds: string[];
|
||
},
|
||
) {
|
||
const plan = state.dispatchPlans.find((item) => item.planId === input.planId);
|
||
if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND");
|
||
if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED");
|
||
const confirmedBy = input.confirmedBy.trim();
|
||
if (!confirmedBy) throw new Error("DISPATCH_PLAN_CONFIRMED_BY_REQUIRED");
|
||
requireDispatchActorSession(state, confirmedBy);
|
||
const approvedTargetProjectIds = normalizeStringSet(input.approvedTargetProjectIds);
|
||
if (approvedTargetProjectIds.length === 0) {
|
||
throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_REQUIRED");
|
||
}
|
||
const canonicalTargetProjectIds = normalizeStringSet(plan.targets.map((target) => target.projectId));
|
||
if (approvedTargetProjectIds.some((projectId) => !canonicalTargetProjectIds.includes(projectId))) {
|
||
throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_INVALID");
|
||
}
|
||
if (plan.confirmedBy && plan.confirmedBy !== confirmedBy) {
|
||
throw new Error("DISPATCH_PLAN_CONFIRMED_BY_MISMATCH");
|
||
}
|
||
if (plan.confirmedTargetProjectIds?.length && !sameStringSet(plan.confirmedTargetProjectIds, approvedTargetProjectIds)) {
|
||
throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_MISMATCH");
|
||
}
|
||
|
||
if (plan.status !== "dispatched") {
|
||
plan.status = "approved";
|
||
}
|
||
if (!plan.confirmedAt) {
|
||
plan.confirmedAt = nowIso();
|
||
}
|
||
plan.confirmedBy = confirmedBy;
|
||
plan.confirmedTargetProjectIds = approvedTargetProjectIds;
|
||
return plan;
|
||
}
|
||
|
||
export async function confirmDispatchPlan(input: {
|
||
planId: string;
|
||
confirmedBy: string;
|
||
approvedTargetProjectIds: string[];
|
||
}) {
|
||
return mutateState((state) => {
|
||
return applyDispatchPlanConfirmationInState(state, input);
|
||
});
|
||
}
|
||
|
||
export async function rejectDispatchPlan(input: {
|
||
groupProjectId: string;
|
||
planId: string;
|
||
rejectedBy: string;
|
||
}) {
|
||
const result = await mutateState((state) => {
|
||
const groupProjectId = input.groupProjectId.trim();
|
||
if (!groupProjectId) throw new Error("PROJECT_NOT_FOUND");
|
||
const groupProject = state.projects.find((item) => item.id === groupProjectId);
|
||
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
|
||
if (!groupProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||
requireDispatchActorSession(state, input.rejectedBy);
|
||
|
||
const plan = state.dispatchPlans.find((item) => item.planId === input.planId);
|
||
if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND");
|
||
if (plan.groupProjectId !== groupProjectId) {
|
||
throw new Error("DISPATCH_PLAN_PROJECT_MISMATCH");
|
||
}
|
||
if (plan.status === "dispatched") {
|
||
throw new Error("DISPATCH_PLAN_ALREADY_DISPATCHED");
|
||
}
|
||
if (plan.status !== "rejected") {
|
||
plan.status = "rejected";
|
||
}
|
||
groupProject.approvalState = "rejected";
|
||
const notice =
|
||
pushProjectLedgerMessage(state, groupProjectId, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: "已拒绝主 Agent 推荐,本次不会下发到任何线程。",
|
||
kind: "system_notice",
|
||
}) ?? null;
|
||
|
||
return {
|
||
plan: { ...plan },
|
||
notice: notice ? { ...notice } : null,
|
||
collaborationGate: buildCollaborationGate(groupProject),
|
||
};
|
||
});
|
||
|
||
publishBossEvent("project.messages.updated", { projectId: input.groupProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: input.groupProjectId });
|
||
return result;
|
||
}
|
||
|
||
export async function createDispatchExecutionsFromPlan(input: {
|
||
planId: string;
|
||
confirmedBy: string;
|
||
}) {
|
||
return mutateState((state) => {
|
||
const plan = state.dispatchPlans.find((item) => item.planId === input.planId);
|
||
if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND");
|
||
if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED");
|
||
const confirmedBy = input.confirmedBy.trim();
|
||
if (!confirmedBy) throw new Error("DISPATCH_PLAN_CONFIRMED_BY_REQUIRED");
|
||
requireDispatchActorSession(state, confirmedBy);
|
||
if (!plan.confirmedAt || !plan.confirmedBy || !plan.confirmedTargetProjectIds?.length) {
|
||
throw new Error("DISPATCH_PLAN_NOT_CONFIRMED");
|
||
}
|
||
if (plan.confirmedBy !== confirmedBy) {
|
||
throw new Error("DISPATCH_PLAN_CONFIRMED_BY_MISMATCH");
|
||
}
|
||
const groupProject = state.projects.find((item) => item.id === plan.groupProjectId);
|
||
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const canonicalTargetProjectIds = normalizeStringSet(plan.confirmedTargetProjectIds);
|
||
const existingExecutions = state.dispatchExecutions.filter((item) => item.planId === plan.planId);
|
||
if (existingExecutions.length > 0) {
|
||
const existingTargetIds = normalizeStringSet(existingExecutions.map((execution) => execution.targetProjectId));
|
||
if (!sameStringSet(existingTargetIds, canonicalTargetProjectIds)) {
|
||
throw new Error("DISPATCH_EXECUTION_SET_MISMATCH");
|
||
}
|
||
if (plan.status !== "dispatched") {
|
||
plan.status = "dispatched";
|
||
}
|
||
groupProject.approvalState =
|
||
groupProject.collaborationMode === "approval_required" ? "approved" : "not_required";
|
||
ensureDispatchExecutionTasksInState(state, plan, existingExecutions);
|
||
return existingExecutions;
|
||
}
|
||
|
||
const targets = plan.targets.filter((target) =>
|
||
canonicalTargetProjectIds.includes(target.projectId),
|
||
);
|
||
if (targets.length === 0) {
|
||
throw new Error("DISPATCH_EXECUTION_TARGETS_REQUIRED");
|
||
}
|
||
const createdAt = nowIso();
|
||
const executions = targets.map((target) => {
|
||
const execution: DispatchExecution = {
|
||
executionId: randomToken("dispatch-exec"),
|
||
planId: plan.planId,
|
||
groupProjectId: plan.groupProjectId,
|
||
targetProjectId: target.projectId,
|
||
targetThreadId: target.threadId,
|
||
deviceId: target.deviceId,
|
||
status: "queued",
|
||
createdAt,
|
||
};
|
||
state.dispatchExecutions.unshift(execution);
|
||
return execution;
|
||
});
|
||
ensureDispatchExecutionTasksInState(state, plan, executions);
|
||
plan.status = "dispatched";
|
||
groupProject.approvalState =
|
||
groupProject.collaborationMode === "approval_required" ? "approved" : "not_required";
|
||
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.targetCodexThreadRef = existing.targetCodexThreadRef ?? target.codexThreadRef;
|
||
existing.targetCodexFolderRef = existing.targetCodexFolderRef ?? target.codexFolderRef;
|
||
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,
|
||
targetCodexThreadRef: target.codexThreadRef,
|
||
targetCodexFolderRef: target.codexFolderRef,
|
||
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;
|
||
confirmedBy: string;
|
||
approvedTargetProjectIds: string[];
|
||
}) {
|
||
const result = await mutateState((state) => {
|
||
const groupProjectId = input.groupProjectId.trim();
|
||
if (!groupProjectId) throw new Error("PROJECT_NOT_FOUND");
|
||
const groupProject = state.projects.find((item) => item.id === groupProjectId);
|
||
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
|
||
if (!groupProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||
|
||
const plan = applyDispatchPlanConfirmationInState(state, {
|
||
planId: input.planId,
|
||
confirmedBy: input.confirmedBy,
|
||
approvedTargetProjectIds: input.approvedTargetProjectIds,
|
||
});
|
||
if (plan.groupProjectId !== groupProjectId) {
|
||
throw new Error("DISPATCH_PLAN_PROJECT_MISMATCH");
|
||
}
|
||
|
||
const canonicalTargetProjectIds = normalizeStringSet(plan.confirmedTargetProjectIds ?? []);
|
||
const existingExecutions = state.dispatchExecutions.filter((item) => item.planId === plan.planId);
|
||
let executions: DispatchExecution[];
|
||
let createdNotice: Message | null = null;
|
||
|
||
if (existingExecutions.length > 0) {
|
||
const existingTargetIds = normalizeStringSet(existingExecutions.map((execution) => execution.targetProjectId));
|
||
if (!sameStringSet(existingTargetIds, canonicalTargetProjectIds)) {
|
||
throw new Error("DISPATCH_EXECUTION_SET_MISMATCH");
|
||
}
|
||
if (plan.status !== "dispatched") {
|
||
plan.status = "dispatched";
|
||
}
|
||
groupProject.approvalState =
|
||
groupProject.collaborationMode === "approval_required" ? "approved" : "not_required";
|
||
executions = existingExecutions;
|
||
} else {
|
||
const targets = plan.targets.filter((target) =>
|
||
canonicalTargetProjectIds.includes(target.projectId),
|
||
);
|
||
if (targets.length === 0) {
|
||
throw new Error("DISPATCH_EXECUTION_TARGETS_REQUIRED");
|
||
}
|
||
const createdAt = nowIso();
|
||
executions = targets.map((target) => {
|
||
const execution: DispatchExecution = {
|
||
executionId: randomToken("dispatch-exec"),
|
||
planId: plan.planId,
|
||
groupProjectId: plan.groupProjectId,
|
||
targetProjectId: target.projectId,
|
||
targetThreadId: target.threadId,
|
||
deviceId: target.deviceId,
|
||
status: "queued",
|
||
createdAt,
|
||
};
|
||
state.dispatchExecutions.unshift(execution);
|
||
return execution;
|
||
});
|
||
plan.status = "dispatched";
|
||
groupProject.approvalState =
|
||
groupProject.collaborationMode === "approval_required" ? "approved" : "not_required";
|
||
const targetSummary = executions
|
||
.map((execution) => {
|
||
const project = state.projects.find((item) => item.id === execution.targetProjectId);
|
||
return `《${project?.threadMeta.threadDisplayName ?? project?.name ?? execution.targetProjectId}》`;
|
||
})
|
||
.join("、");
|
||
createdNotice = pushProjectLedgerMessage(state, groupProjectId, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: `已确认下发到 ${executions.length} 个线程:${targetSummary}。`,
|
||
kind: "system_notice",
|
||
});
|
||
}
|
||
|
||
ensureDispatchExecutionTasksInState(state, plan, executions);
|
||
|
||
return {
|
||
plan: { ...plan },
|
||
executions: executions.map((execution) => ({ ...execution })),
|
||
notice: createdNotice ? { ...createdNotice } : null,
|
||
collaborationGate: buildCollaborationGate(groupProject),
|
||
};
|
||
});
|
||
|
||
publishBossEvent("project.messages.updated", { projectId: input.groupProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: input.groupProjectId });
|
||
return result;
|
||
}
|
||
|
||
export async function completeDispatchExecution(payload: {
|
||
executionId: string;
|
||
completedByDeviceId: string;
|
||
completedByDeviceToken: string;
|
||
status: "completed" | "failed";
|
||
resultMessageId?: string;
|
||
}) {
|
||
return mutateState((state) => {
|
||
const execution = state.dispatchExecutions.find((item) => item.executionId === payload.executionId);
|
||
if (!execution) throw new Error("DISPATCH_EXECUTION_NOT_FOUND");
|
||
const deviceId = payload.completedByDeviceId.trim();
|
||
if (!deviceId) throw new Error("DISPATCH_EXECUTION_DEVICE_REQUIRED");
|
||
if (execution.deviceId !== deviceId) {
|
||
throw new Error("DISPATCH_EXECUTION_DEVICE_MISMATCH");
|
||
}
|
||
|
||
const device = state.devices.find((item) => item.id === deviceId);
|
||
if (!device || device.source !== "production") {
|
||
throw new Error("DISPATCH_EXECUTION_DEVICE_INVALID");
|
||
}
|
||
if (!device.token || device.token !== payload.completedByDeviceToken.trim()) {
|
||
throw new Error("DISPATCH_EXECUTION_DEVICE_TOKEN_INVALID");
|
||
}
|
||
|
||
const nextResultMessageId = payload.resultMessageId?.trim() || undefined;
|
||
if (execution.status === "completed" || execution.status === "failed") {
|
||
const sameStatus = execution.status === payload.status;
|
||
const sameResultMessageId = (execution.resultMessageId ?? undefined) === nextResultMessageId;
|
||
if (!sameStatus || !sameResultMessageId) {
|
||
throw new Error("DISPATCH_EXECUTION_COMPLETION_MISMATCH");
|
||
}
|
||
return execution;
|
||
}
|
||
|
||
execution.status = payload.status;
|
||
execution.completedAt = nowIso();
|
||
execution.resultMessageId = nextResultMessageId ?? execution.resultMessageId;
|
||
execution.completedByDeviceId = deviceId;
|
||
return execution;
|
||
});
|
||
}
|
||
|
||
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 groupProject = state.projects.find((item) => item.id === payload.groupProjectId);
|
||
if (!groupProject) {
|
||
throw new Error("PROJECT_NOT_FOUND");
|
||
}
|
||
|
||
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;
|
||
|
||
if (execution.status === "completed" || execution.status === "failed") {
|
||
if (execution.status !== payload.status) {
|
||
throw new Error("DISPATCH_EXECUTION_COMPLETION_MISMATCH");
|
||
}
|
||
const existingMirroredResult = execution.resultMessageId
|
||
? findProjectMessage(groupProject, execution.resultMessageId)
|
||
: null;
|
||
if (
|
||
payload.status === "completed" &&
|
||
payload.rawThreadReply?.trim() &&
|
||
existingMirroredResult &&
|
||
existingMirroredResult.body !== payload.rawThreadReply.trim()
|
||
) {
|
||
throw new Error("DISPATCH_EXECUTION_COMPLETION_MISMATCH");
|
||
}
|
||
return {
|
||
execution: { ...execution },
|
||
mirroredResult: existingMirroredResult,
|
||
masterSummary: null,
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
export async function reassignMasterAgentTaskExecution(payload: {
|
||
taskId: string;
|
||
deviceId: string;
|
||
accountId?: string;
|
||
accountLabel?: string;
|
||
executionPrompt?: string;
|
||
}) {
|
||
const task = await mutateState((state) => {
|
||
const next = state.masterAgentTasks.find((item) => item.taskId === payload.taskId);
|
||
if (!next) {
|
||
throw new Error("MASTER_AGENT_TASK_NOT_FOUND");
|
||
}
|
||
if (next.status !== "queued") {
|
||
return { ...next };
|
||
}
|
||
next.deviceId = payload.deviceId;
|
||
next.accountId = payload.accountId;
|
||
next.accountLabel = payload.accountLabel;
|
||
if (payload.executionPrompt?.trim()) {
|
||
next.executionPrompt = payload.executionPrompt.trim();
|
||
}
|
||
return { ...next };
|
||
});
|
||
|
||
publishBossEvent("master_agent.task.updated", {
|
||
taskId: task.taskId,
|
||
deviceId: task.deviceId,
|
||
status: task.status,
|
||
});
|
||
|
||
return task;
|
||
}
|
||
|
||
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",
|
||
);
|
||
if (!next) return null;
|
||
next.status = "running";
|
||
next.claimedAt = nowIso();
|
||
if (next.taskType === "attachment_analysis" && next.attachmentId) {
|
||
const project = state.projects.find((item) => item.id === next.projectId);
|
||
const match = project ? findProjectAttachment(project, next.attachmentId) : null;
|
||
if (match) {
|
||
match.attachment.analysisState = "processing";
|
||
match.attachment.analysisSummary = undefined;
|
||
match.attachment.analysisCardId = undefined;
|
||
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) {
|
||
publishBossEvent("master_agent.task.updated", {
|
||
taskId: task.taskId,
|
||
deviceId: task.deviceId,
|
||
status: task.status,
|
||
});
|
||
if (attachmentProjectId) {
|
||
publishBossEvent("project.messages.updated", { projectId: attachmentProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: attachmentProjectId });
|
||
}
|
||
if (dispatchExecutionProjectId) {
|
||
publishBossEvent("conversation.updated", { projectId: dispatchExecutionProjectId });
|
||
}
|
||
}
|
||
return task;
|
||
}
|
||
|
||
export async function completeMasterAgentTask(payload: {
|
||
taskId: string;
|
||
deviceId: string;
|
||
status: "completed" | "failed";
|
||
replyBody?: string;
|
||
errorMessage?: string;
|
||
requestId?: string;
|
||
dispatchExecutionId?: string;
|
||
targetProjectId?: string;
|
||
targetThreadId?: string;
|
||
rawThreadReply?: string;
|
||
dispatchPlan?: {
|
||
summary?: string;
|
||
targets: DispatchPlanTarget[];
|
||
};
|
||
}) {
|
||
const result = await mutateState((state) => {
|
||
const task = state.masterAgentTasks.find((item) => item.taskId === payload.taskId);
|
||
if (!task) {
|
||
throw new Error("MASTER_AGENT_TASK_NOT_FOUND");
|
||
}
|
||
if (task.deviceId !== payload.deviceId) {
|
||
throw new Error("MASTER_AGENT_TASK_DEVICE_MISMATCH");
|
||
}
|
||
task.status = payload.status;
|
||
task.completedAt = nowIso();
|
||
task.replyBody = payload.replyBody?.trim() || undefined;
|
||
task.errorMessage = payload.errorMessage?.trim() || undefined;
|
||
task.requestId = payload.requestId;
|
||
const linkedAccount = task.accountId
|
||
? state.aiAccounts.find((item) => item.accountId === task.accountId)
|
||
: undefined;
|
||
if (linkedAccount) {
|
||
linkedAccount.updatedAt = task.completedAt;
|
||
linkedAccount.lastUsedAt = task.completedAt;
|
||
linkedAccount.lastValidatedAt = task.completedAt;
|
||
linkedAccount.lastError = task.errorMessage;
|
||
linkedAccount.status = payload.status === "completed" ? "ready" : "degraded";
|
||
if (!linkedAccount.isActive) {
|
||
setActiveAiAccountInState(
|
||
state,
|
||
linkedAccount.accountId,
|
||
payload.status === "completed"
|
||
? "Master Codex Node 完成回复后自动切回当前主控"
|
||
: "Master Codex Node 失败后记录当前主控身份",
|
||
{ preserveLastSwitchAt: true },
|
||
);
|
||
}
|
||
}
|
||
|
||
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;
|
||
if (match) {
|
||
attachmentProjectId = project?.id;
|
||
if (payload.status === "completed") {
|
||
const summary = summarizeAttachmentAnalysis(task.replyBody ?? "");
|
||
match.attachment.analysisState = "completed";
|
||
match.attachment.analysisSummary = summary;
|
||
pushProjectLedgerMessage(state, task.projectId, {
|
||
sender: "master",
|
||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||
body: summary,
|
||
kind: "text",
|
||
});
|
||
if (task.replyBody) {
|
||
const card = pushProjectLedgerMessage(state, task.projectId, {
|
||
sender: "master",
|
||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||
body: task.replyBody,
|
||
kind: "analysis_card",
|
||
});
|
||
match.attachment.analysisCardId = card?.id;
|
||
} else {
|
||
match.attachment.analysisCardId = undefined;
|
||
}
|
||
} else if (payload.status === "failed") {
|
||
match.attachment.analysisState = "failed";
|
||
match.attachment.analysisSummary = task.errorMessage ?? "附件分析失败,请稍后重试。";
|
||
match.attachment.analysisCardId = undefined;
|
||
pushProjectLedgerMessage(state, task.projectId, {
|
||
sender: "ops",
|
||
senderLabel: task.accountLabel ? `主 Agent Relay · ${task.accountLabel}` : "主 Agent Relay",
|
||
body: `附件分析失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
|
||
kind: "text",
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (task.taskType === "group_dispatch_plan") {
|
||
if (payload.status === "completed") {
|
||
if (!payload.dispatchPlan) {
|
||
throw new Error("MASTER_AGENT_GROUP_DISPATCH_PLAN_REQUIRED");
|
||
}
|
||
createdDispatchPlan = upsertDispatchPlanInState(state, {
|
||
groupProjectId: task.projectId,
|
||
requestMessageId: task.requestMessageId,
|
||
requestedBy: task.requestedByAccount,
|
||
summary: payload.dispatchPlan.summary,
|
||
targets: payload.dispatchPlan.targets,
|
||
});
|
||
}
|
||
} else if (task.taskType === "device_import_resolution") {
|
||
if (!task.deviceImportDraftId) {
|
||
throw new Error("MASTER_AGENT_DEVICE_IMPORT_DRAFT_REQUIRED");
|
||
}
|
||
const draft = state.deviceImportDrafts.find((item) => item.draftId === task.deviceImportDraftId);
|
||
if (!draft) {
|
||
throw new Error("DEVICE_IMPORT_DRAFT_NOT_FOUND");
|
||
}
|
||
if (payload.status === "completed") {
|
||
const resolutionReply = parseDeviceImportResolutionReply(state, draft, task.replyBody ?? "");
|
||
upsertDeviceImportResolutionInState(state, {
|
||
deviceId: draft.deviceId,
|
||
reviewedBy: task.requestedByAccount,
|
||
summary: resolutionReply.summary,
|
||
items: resolutionReply.items,
|
||
draftId: draft.draftId,
|
||
});
|
||
}
|
||
publishBossEvent("devices.updated", { deviceId: draft.deviceId });
|
||
} 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) {
|
||
const isThreadConversationReply =
|
||
task.taskType === "conversation_reply" &&
|
||
task.projectId !== "master-agent" &&
|
||
Boolean(task.targetProjectId && task.targetThreadId);
|
||
if (isThreadConversationReply) {
|
||
const threadProject = state.projects.find(
|
||
(item) => item.id === (task.targetProjectId ?? task.projectId),
|
||
);
|
||
const device = state.devices.find((item) => item.id === payload.deviceId);
|
||
pushProjectLedgerMessage(state, threadProject?.id ?? task.projectId, {
|
||
sender: "device",
|
||
senderLabel:
|
||
task.targetThreadDisplayName?.trim() ||
|
||
threadProject?.threadMeta.threadDisplayName ||
|
||
device?.name ||
|
||
"线程",
|
||
body: task.replyBody,
|
||
kind: "text",
|
||
});
|
||
} else {
|
||
pushProjectLedgerMessage(state, task.projectId, {
|
||
sender: "master",
|
||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||
body: task.replyBody,
|
||
kind: "text",
|
||
});
|
||
autoCaptureMasterAgentMemoriesInState(state, {
|
||
account: task.requestedByAccount,
|
||
requestText: task.requestText,
|
||
replyText: task.replyBody,
|
||
sourceMessageId: task.requestMessageId,
|
||
});
|
||
}
|
||
} else if (!attachmentProjectId && payload.status === "failed") {
|
||
const isThreadConversationReply =
|
||
task.taskType === "conversation_reply" &&
|
||
task.projectId !== "master-agent" &&
|
||
Boolean(task.targetProjectId && task.targetThreadId);
|
||
pushProjectLedgerMessage(state, task.projectId, {
|
||
sender: "ops",
|
||
senderLabel: isThreadConversationReply
|
||
? "线程执行失败"
|
||
: task.accountLabel
|
||
? `主 Agent Relay · ${task.accountLabel}`
|
||
: "主 Agent Relay",
|
||
body: isThreadConversationReply
|
||
? `${task.targetThreadDisplayName ?? "当前线程"} 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`
|
||
: `Master Codex Node 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
|
||
kind: "text",
|
||
});
|
||
}
|
||
|
||
return {
|
||
...task,
|
||
dispatchPlan: createdDispatchPlan ? { ...createdDispatchPlan } : undefined,
|
||
dispatchExecution: dispatchExecutionResult?.execution,
|
||
};
|
||
});
|
||
|
||
publishBossEvent("master_agent.task.updated", {
|
||
taskId: result.taskId,
|
||
deviceId: result.deviceId,
|
||
status: result.status,
|
||
});
|
||
publishBossEvent("project.messages.updated", { projectId: result.projectId });
|
||
publishBossEvent("conversation.updated", { projectId: result.projectId });
|
||
return result;
|
||
}
|
||
|
||
export async function recordVerificationDelivery(
|
||
account: string,
|
||
purpose: VerificationCode["purpose"],
|
||
status: VerificationDispatch["status"],
|
||
note: string,
|
||
) {
|
||
return mutateState((state) => {
|
||
recordVerificationDispatch(state, account, purpose, getVerificationDeliveryMode(), status, note);
|
||
});
|
||
}
|
||
|
||
export async function registerAccount(account: string, password: string, code: string) {
|
||
await mutateState((state) => {
|
||
if (!consumeVerificationCode(state, account, "register", code)) {
|
||
throw new Error("INVALID_VERIFICATION_CODE");
|
||
}
|
||
if (state.authAccounts.some((item) => item.account === account)) {
|
||
throw new Error("ACCOUNT_ALREADY_EXISTS");
|
||
}
|
||
|
||
state.authAccounts.push({
|
||
id: `account-${slugify(account)}`,
|
||
account,
|
||
passwordHash: hashPassword(password),
|
||
displayName: account,
|
||
role: "member",
|
||
verificationEmail: isLikelyEmailAccount(account) ? account : undefined,
|
||
createdAt: nowIso(),
|
||
updatedAt: nowIso(),
|
||
});
|
||
});
|
||
}
|
||
|
||
export async function loginAccount(params: {
|
||
account: string;
|
||
password?: string;
|
||
code?: string;
|
||
method?: LoginMethod;
|
||
}) {
|
||
return mutateState((state) => {
|
||
const method = params.method ?? (params.password?.trim() ? "password" : "code");
|
||
const existing = state.authAccounts.find((item) => item.account === params.account);
|
||
if (!existing) {
|
||
throw new Error("ACCOUNT_NOT_FOUND");
|
||
}
|
||
|
||
if (existing.lockedUntil && new Date(existing.lockedUntil).getTime() > Date.now()) {
|
||
throw new Error("LOGIN_TEMPORARILY_LOCKED");
|
||
}
|
||
|
||
if (method === "password") {
|
||
if (!params.password?.trim()) {
|
||
throw new Error("PASSWORD_REQUIRED");
|
||
}
|
||
if (!verifyPasswordHash(params.password, existing.passwordHash)) {
|
||
registerLoginFailure(existing);
|
||
throw new Error("INVALID_ACCOUNT_OR_PASSWORD");
|
||
}
|
||
if (!existing.passwordHash.startsWith("scrypt$")) {
|
||
existing.passwordHash = hashPassword(params.password);
|
||
}
|
||
} else {
|
||
if (!params.code?.trim()) {
|
||
throw new Error("VERIFICATION_CODE_REQUIRED");
|
||
}
|
||
const directFixedCode = shouldAcceptDirectFixedVerificationCode("login", params.code);
|
||
if (!directFixedCode && !consumeVerificationCode(state, params.account, "login", params.code)) {
|
||
registerLoginFailure(existing);
|
||
throw new Error("INVALID_VERIFICATION_CODE");
|
||
}
|
||
}
|
||
|
||
clearLoginFailure(existing);
|
||
existing.updatedAt = nowIso();
|
||
existing.lastLoginAt = nowIso();
|
||
existing.lastLoginMethod = method;
|
||
const session = {
|
||
sessionId: randomToken("session"),
|
||
sessionToken: randomBytes(24).toString("hex"),
|
||
restoreToken: randomBytes(24).toString("hex"),
|
||
account: existing.account,
|
||
role: existing.role,
|
||
displayName: existing.displayName,
|
||
loginMethod: method,
|
||
createdAt: nowIso(),
|
||
expiresAt: new Date(Date.now() + AUTH_SESSION_TTL_MS).toISOString(),
|
||
lastSeenAt: nowIso(),
|
||
} satisfies AuthSession;
|
||
state.authSessions = [
|
||
session,
|
||
...state.authSessions.filter((item) => item.account !== existing.account),
|
||
].slice(0, 20);
|
||
|
||
return {
|
||
account: existing.account,
|
||
role: existing.role,
|
||
displayName: existing.displayName,
|
||
loginMethod: method,
|
||
sessionToken: session.sessionToken,
|
||
restoreToken: session.restoreToken,
|
||
sessionExpiresAt: session.expiresAt,
|
||
};
|
||
});
|
||
}
|
||
|
||
export async function resetAccountPassword(account: string, password: string, code: string) {
|
||
await mutateState((state) => {
|
||
if (!consumeVerificationCode(state, account, "forgot-password", code)) {
|
||
throw new Error("INVALID_VERIFICATION_CODE");
|
||
}
|
||
const existing = state.authAccounts.find((item) => item.account === account);
|
||
if (!existing) {
|
||
throw new Error("ACCOUNT_NOT_FOUND");
|
||
}
|
||
existing.passwordHash = hashPassword(password);
|
||
clearLoginFailure(existing);
|
||
existing.updatedAt = nowIso();
|
||
state.authSessions = state.authSessions.filter((item) => item.account !== account);
|
||
});
|
||
}
|
||
|
||
function masterActionsForSnapshot(snapshot: ThreadContextSnapshot) {
|
||
const actions = new Set<string>();
|
||
if (snapshot.contextBudgetLevel === "watch") {
|
||
actions.add("prepare_handoff");
|
||
actions.add("avoid_large_context_append");
|
||
}
|
||
if (snapshot.contextBudgetLevel === "urgent") {
|
||
actions.add("prepare_handoff");
|
||
actions.add("avoid_large_context_append");
|
||
actions.add("finalize_artifacts");
|
||
}
|
||
if (snapshot.contextBudgetLevel === "critical") {
|
||
actions.add("complete_wrapup");
|
||
actions.add("handoff_required");
|
||
actions.add("freeze_context_growth");
|
||
}
|
||
if (snapshot.mustFinishBeforeCompaction) {
|
||
actions.add("finalize_artifacts");
|
||
}
|
||
if (!actions.size) {
|
||
actions.add("refresh_summary");
|
||
}
|
||
return [...actions];
|
||
}
|
||
|
||
function nextReportSeconds(level: ContextBudgetLevel) {
|
||
switch (level) {
|
||
case "critical":
|
||
case "urgent":
|
||
return 5;
|
||
case "watch":
|
||
return 15;
|
||
default:
|
||
return 30;
|
||
}
|
||
}
|
||
|
||
function upsertThreadAlert(state: BossState, snapshot: ThreadContextSnapshot) {
|
||
const existing = state.threadContextAlerts.find((alert) => alert.threadId === snapshot.threadId);
|
||
const shouldOpen =
|
||
snapshot.contextBudgetLevel !== "safe" || snapshot.mustFinishBeforeCompaction;
|
||
|
||
if (!shouldOpen) {
|
||
if (existing && existing.alertStatus !== "resolved") {
|
||
existing.alertStatus = "resolved";
|
||
existing.resolvedAt = nowIso();
|
||
existing.summary = "线程预算已恢复到 safe,handoff 风险暂时解除。";
|
||
}
|
||
return existing ?? null;
|
||
}
|
||
|
||
const alertType =
|
||
snapshot.contextBudgetLevel === "critical"
|
||
? "context_critical"
|
||
: snapshot.contextBudgetLevel === "urgent"
|
||
? "context_urgent"
|
||
: snapshot.mustFinishBeforeCompaction
|
||
? "compaction_risk"
|
||
: "context_watch";
|
||
|
||
const summary = `${snapshot.title} 进入 ${snapshot.contextBudgetLevel},预算 ${snapshot.contextBudgetRemainingPct}%${snapshot.mustFinishBeforeCompaction ? ",必须先收尾" : ""}。`;
|
||
const masterActions = masterActionsForSnapshot(snapshot);
|
||
|
||
if (existing) {
|
||
existing.alertType = alertType;
|
||
existing.alertStatus = "opened";
|
||
existing.summary = summary;
|
||
existing.masterActions = masterActions;
|
||
existing.resolvedAt = undefined;
|
||
return existing;
|
||
}
|
||
|
||
const alert: ThreadContextAlert = {
|
||
alertId: randomToken("alert"),
|
||
threadId: snapshot.threadId,
|
||
projectId: snapshot.projectId,
|
||
alertType,
|
||
alertStatus: "opened",
|
||
openedAt: nowIso(),
|
||
summary,
|
||
masterActions,
|
||
};
|
||
state.threadContextAlerts.unshift(alert);
|
||
return alert;
|
||
}
|
||
|
||
function ensureHandoffPackage(state: BossState, snapshot: ThreadContextSnapshot) {
|
||
const existing = state.threadHandoffPackages.find(
|
||
(item) => item.fromThreadId === snapshot.threadId && item.packageStatus !== "consumed",
|
||
);
|
||
|
||
if (snapshot.contextBudgetLevel === "safe" && !snapshot.mustFinishBeforeCompaction) {
|
||
return existing ?? null;
|
||
}
|
||
|
||
if (existing) {
|
||
if (snapshot.contextBudgetLevel === "critical" || snapshot.mustFinishBeforeCompaction) {
|
||
existing.packageStatus = "ready";
|
||
existing.readyAt = nowIso();
|
||
}
|
||
existing.summaryText = snapshot.summary;
|
||
existing.criticalFiles = Array.from(new Set([...existing.criticalFiles]));
|
||
return existing;
|
||
}
|
||
|
||
const handoff: ThreadHandoffPackage = {
|
||
handoffPackageId: randomToken("handoff"),
|
||
projectId: snapshot.projectId,
|
||
taskId: snapshot.taskId,
|
||
fromThreadId: snapshot.threadId,
|
||
toThreadId: `${snapshot.threadId}-followup`,
|
||
packageStatus:
|
||
snapshot.contextBudgetLevel === "critical" || snapshot.mustFinishBeforeCompaction
|
||
? "ready"
|
||
: "draft",
|
||
summaryText: snapshot.summary,
|
||
openQuestions: [],
|
||
criticalFiles: [],
|
||
criticalCommands: [],
|
||
criticalTests: [],
|
||
criticalArtifacts: [],
|
||
decisionLinks: [],
|
||
createdAt: nowIso(),
|
||
readyAt:
|
||
snapshot.contextBudgetLevel === "critical" || snapshot.mustFinishBeforeCompaction
|
||
? nowIso()
|
||
: undefined,
|
||
};
|
||
|
||
state.threadHandoffPackages.unshift(handoff);
|
||
return handoff;
|
||
}
|
||
|
||
export async function upsertThreadContextSnapshot(
|
||
workerId: string,
|
||
payload: Omit<ThreadContextSnapshot, "snapshotId" | "workerId" | "contextBudgetLevel"> & {
|
||
contextBudgetLevel?: ContextBudgetLevel;
|
||
},
|
||
) {
|
||
const normalized = await mutateState((state) => {
|
||
const snapshot: ThreadContextSnapshot = {
|
||
snapshotId: randomToken("snapshot"),
|
||
workerId,
|
||
contextBudgetLevel:
|
||
payload.contextBudgetLevel ??
|
||
deriveLevelFromPercent(payload.contextBudgetRemainingPct),
|
||
...payload,
|
||
};
|
||
|
||
state.threadContextSnapshots = [
|
||
snapshot,
|
||
...state.threadContextSnapshots.filter((item) => item.threadId !== payload.threadId),
|
||
];
|
||
|
||
upsertThreadAlert(state, snapshot);
|
||
ensureHandoffPackage(state, snapshot);
|
||
return snapshot;
|
||
});
|
||
publishBossEvent("project.context_risk.updated", {
|
||
projectId: normalized.projectId,
|
||
deviceId: normalized.nodeId,
|
||
note: normalized.threadId,
|
||
});
|
||
publishBossEvent("conversation.updated", {
|
||
projectId: normalized.projectId,
|
||
deviceId: normalized.nodeId,
|
||
});
|
||
|
||
return {
|
||
accepted: true,
|
||
thread_id: normalized.threadId,
|
||
context_budget_level: normalized.contextBudgetLevel,
|
||
next_required_report_in_seconds: nextReportSeconds(normalized.contextBudgetLevel),
|
||
master_actions: masterActionsForSnapshot(normalized),
|
||
};
|
||
}
|
||
|
||
export async function createDeviceEnrollment(payload: {
|
||
name: string;
|
||
avatar: string;
|
||
account: string;
|
||
endpoint?: string;
|
||
projects: string[];
|
||
note?: string;
|
||
}) {
|
||
const { device, enrollment, deviceId } = await mutateState((state) => {
|
||
const nextDeviceId = slugify(payload.name);
|
||
const token = randomToken("boss");
|
||
const pairingCode = randomDigits(6);
|
||
|
||
let device = state.devices.find((item) => item.id === nextDeviceId);
|
||
if (!device) {
|
||
device = {
|
||
id: nextDeviceId,
|
||
name: payload.name,
|
||
avatar: payload.avatar,
|
||
account: payload.account,
|
||
source: "production",
|
||
status: "offline",
|
||
projects: payload.projects,
|
||
quota5h: 100,
|
||
quota7d: 100,
|
||
lastSeenAt: nowIso(),
|
||
endpoint: payload.endpoint,
|
||
token,
|
||
note: payload.note,
|
||
};
|
||
state.devices.push(device);
|
||
} else {
|
||
device.name = payload.name;
|
||
device.avatar = payload.avatar;
|
||
device.account = payload.account;
|
||
device.source = "production";
|
||
device.projects = payload.projects;
|
||
device.endpoint = payload.endpoint;
|
||
device.note = payload.note;
|
||
device.token = token;
|
||
}
|
||
|
||
const enrollment: DeviceEnrollment = {
|
||
enrollmentId: randomToken("enroll"),
|
||
deviceId: nextDeviceId,
|
||
label: payload.name,
|
||
pairingCode,
|
||
token,
|
||
status: "ready",
|
||
note: payload.note ?? "等待本地 agent 首次心跳 claim",
|
||
createdAt: nowIso(),
|
||
expiresAt: new Date(Date.now() + 8 * 60 * 60_000).toISOString(),
|
||
};
|
||
|
||
state.deviceEnrollments = [
|
||
enrollment,
|
||
...state.deviceEnrollments.filter((item) => item.deviceId !== nextDeviceId),
|
||
];
|
||
|
||
return { device, enrollment, deviceId: nextDeviceId };
|
||
});
|
||
publishBossEvent("devices.updated", { deviceId });
|
||
return { device, enrollment };
|
||
}
|
||
|
||
export async function updateDevice(deviceId: string, payload: Partial<Device>) {
|
||
const device = await mutateState((state) => {
|
||
const nextDevice = state.devices.find((item) => item.id === deviceId);
|
||
if (!nextDevice) throw new Error("DEVICE_NOT_FOUND");
|
||
|
||
if (payload.name) nextDevice.name = payload.name.trim();
|
||
if (payload.avatar) nextDevice.avatar = payload.avatar.trim().slice(0, 2) || nextDevice.avatar;
|
||
if (payload.account) nextDevice.account = payload.account.trim();
|
||
if (payload.status) nextDevice.status = payload.status;
|
||
if (payload.endpoint !== undefined) nextDevice.endpoint = payload.endpoint;
|
||
if (payload.note !== undefined) nextDevice.note = payload.note;
|
||
if (payload.projects) {
|
||
nextDevice.projects = payload.projects.filter(Boolean);
|
||
}
|
||
nextDevice.lastSeenAt = nowIso();
|
||
return nextDevice;
|
||
});
|
||
publishBossEvent("devices.updated", { deviceId });
|
||
return device;
|
||
}
|
||
|
||
function claimEnrollment(
|
||
state: BossState,
|
||
deviceId: string,
|
||
pairingCode?: string,
|
||
token?: string,
|
||
) {
|
||
const enrollment = state.deviceEnrollments.find((item) => item.deviceId === deviceId);
|
||
if (!enrollment) return null;
|
||
|
||
const expired = new Date(enrollment.expiresAt).getTime() < Date.now();
|
||
if (expired) {
|
||
enrollment.status = "expired";
|
||
return null;
|
||
}
|
||
|
||
if (token && token === enrollment.token) {
|
||
enrollment.status = "claimed";
|
||
enrollment.claimedAt = nowIso();
|
||
enrollment.claimedDeviceId = deviceId;
|
||
return enrollment;
|
||
}
|
||
|
||
if (pairingCode && pairingCode === enrollment.pairingCode) {
|
||
enrollment.status = "claimed";
|
||
enrollment.claimedAt = nowIso();
|
||
enrollment.claimedDeviceId = deviceId;
|
||
return enrollment;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function hasAuthorizedDeviceToken(
|
||
state: BossState,
|
||
deviceId: string,
|
||
token?: string,
|
||
) {
|
||
if (!token) return false;
|
||
|
||
const device = state.devices.find((item) => item.id === deviceId);
|
||
if (device?.token && device.token === token) {
|
||
return true;
|
||
}
|
||
|
||
const enrollment = state.deviceEnrollments.find((item) => item.deviceId === deviceId);
|
||
if (!enrollment || enrollment.token !== token) {
|
||
return false;
|
||
}
|
||
|
||
return new Date(enrollment.expiresAt).getTime() > Date.now();
|
||
}
|
||
|
||
export async function verifyDeviceToken(deviceId: string, token?: string) {
|
||
if (!token) return false;
|
||
const state = await readState();
|
||
return hasAuthorizedDeviceToken(state, deviceId, token);
|
||
}
|
||
|
||
function upsertDeviceImportDraftFromHeartbeat(
|
||
state: BossState,
|
||
payload: {
|
||
deviceId: string;
|
||
enrollmentId?: string;
|
||
candidates: DeviceImportCandidate[];
|
||
},
|
||
) {
|
||
const existing = state.deviceImportDrafts.find((item) => item.deviceId === payload.deviceId);
|
||
if (payload.candidates.length === 0) {
|
||
if (existing?.status === "applied" && existing.appliedProjectNames.length > 0) {
|
||
return existing;
|
||
}
|
||
const waitingDraft = normalizeDeviceImportDraft({
|
||
draftId: existing?.draftId ?? randomToken("import-draft"),
|
||
deviceId: payload.deviceId,
|
||
enrollmentId: payload.enrollmentId ?? existing?.enrollmentId,
|
||
status: "pending_candidates",
|
||
candidates: [],
|
||
selectedCandidateIds: [],
|
||
appliedProjectNames: [],
|
||
createdAt: existing?.createdAt ?? nowIso(),
|
||
updatedAt: nowIso(),
|
||
}, existing);
|
||
waitingDraft.reviewedAt = undefined;
|
||
waitingDraft.reviewedBy = undefined;
|
||
waitingDraft.resolutionId = undefined;
|
||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||
(item) => item.draftId !== waitingDraft.draftId,
|
||
);
|
||
state.deviceImportDrafts = [
|
||
waitingDraft,
|
||
...state.deviceImportDrafts.filter((item) => item.draftId !== waitingDraft.draftId),
|
||
];
|
||
return waitingDraft;
|
||
}
|
||
|
||
const selectedCandidateIds = dedupeStrings(
|
||
(existing?.selectedCandidateIds ?? []).filter((candidateId) =>
|
||
payload.candidates.some((candidate) => candidate.candidateId === candidateId),
|
||
),
|
||
);
|
||
const previousCandidateIds = existing?.candidates.map((candidate) => candidate.candidateId) ?? [];
|
||
const nextCandidateIds = payload.candidates.map((candidate) => candidate.candidateId);
|
||
const selectionChanged =
|
||
!sameStringSet(existing?.selectedCandidateIds ?? [], selectedCandidateIds) ||
|
||
!sameStringSet(previousCandidateIds, nextCandidateIds);
|
||
const keepAppliedState =
|
||
!selectionChanged &&
|
||
existing?.status === "applied" &&
|
||
Boolean(existing.resolutionId) &&
|
||
selectedCandidateIds.length > 0;
|
||
const keepResolvedState =
|
||
!selectionChanged &&
|
||
selectedCandidateIds.length > 0 &&
|
||
Boolean(existing?.resolutionId);
|
||
|
||
const nextDraft = normalizeDeviceImportDraft({
|
||
draftId: existing?.draftId ?? randomToken("import-draft"),
|
||
deviceId: payload.deviceId,
|
||
enrollmentId: payload.enrollmentId ?? existing?.enrollmentId,
|
||
status:
|
||
keepAppliedState
|
||
? "applied"
|
||
: selectedCandidateIds.length > 0
|
||
? keepResolvedState
|
||
? "resolved"
|
||
: "pending_resolution"
|
||
: "pending_selection",
|
||
candidates: payload.candidates,
|
||
selectedCandidateIds,
|
||
appliedProjectNames:
|
||
keepAppliedState
|
||
? existing.appliedProjectNames
|
||
: [],
|
||
createdAt: existing?.createdAt ?? nowIso(),
|
||
updatedAt: nowIso(),
|
||
reviewedAt: keepResolvedState || keepAppliedState ? existing?.reviewedAt : undefined,
|
||
reviewedBy: keepResolvedState || keepAppliedState ? existing?.reviewedBy : undefined,
|
||
resolutionId: keepResolvedState || keepAppliedState ? existing?.resolutionId : undefined,
|
||
}, existing);
|
||
|
||
if (!keepResolvedState && !keepAppliedState) {
|
||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||
(item) => item.draftId !== nextDraft.draftId,
|
||
);
|
||
}
|
||
|
||
state.deviceImportDrafts = [
|
||
nextDraft,
|
||
...state.deviceImportDrafts.filter((item) => item.draftId !== nextDraft.draftId),
|
||
];
|
||
return nextDraft;
|
||
}
|
||
|
||
export async function upsertDeviceHeartbeat(payload: {
|
||
deviceId: string;
|
||
token?: string;
|
||
pairingCode?: string;
|
||
name: string;
|
||
avatar: string;
|
||
account: string;
|
||
status: DeviceStatus;
|
||
quota5h: number;
|
||
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 existingDevice = state.devices.find((item) => item.id === payload.deviceId) ?? null;
|
||
const claimedEnrollment = claimEnrollment(
|
||
state,
|
||
payload.deviceId,
|
||
payload.pairingCode,
|
||
payload.token,
|
||
);
|
||
|
||
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 reportedProjectCandidates = Array.isArray(payload.projectCandidates);
|
||
const shouldAutoImportLegacyProjects = !reportedProjectCandidates && normalizedCandidates.length === 0;
|
||
|
||
let device = existingDevice;
|
||
if (!device) {
|
||
device = {
|
||
id: payload.deviceId,
|
||
name: payload.name,
|
||
avatar: payload.avatar,
|
||
account: payload.account,
|
||
source: "production",
|
||
status: payload.status,
|
||
projects: payload.projects,
|
||
quota5h: payload.quota5h,
|
||
quota7d: payload.quota7d,
|
||
lastSeenAt: nowIso(),
|
||
endpoint: payload.endpoint,
|
||
token: claimedEnrollment?.token ?? payload.token ?? randomToken("boss"),
|
||
note: claimedEnrollment?.note,
|
||
};
|
||
state.devices.push(device);
|
||
} else {
|
||
if (device.token && payload.token && device.token !== payload.token && !claimedEnrollment) {
|
||
throw new Error("DEVICE_TOKEN_MISMATCH");
|
||
}
|
||
device.name = payload.name;
|
||
device.avatar = payload.avatar;
|
||
device.account = payload.account;
|
||
device.source = "production";
|
||
device.status = payload.status;
|
||
device.projects = payload.projects;
|
||
device.quota5h = payload.quota5h;
|
||
device.quota7d = payload.quota7d;
|
||
device.lastSeenAt = nowIso();
|
||
device.endpoint = payload.endpoint ?? device.endpoint;
|
||
device.token = claimedEnrollment?.token ?? payload.token ?? device.token;
|
||
}
|
||
|
||
if (shouldAutoImportLegacyProjects) {
|
||
for (const projectName of payload.projects) {
|
||
const existing = state.projects.find((item) => item.name === projectName);
|
||
if (!existing) {
|
||
state.projects.push(
|
||
normalizeProject({
|
||
id: slugify(projectName),
|
||
name: projectName,
|
||
pinned: false,
|
||
deviceIds: [payload.deviceId],
|
||
preview: `${payload.name} 已自动上报项目文件夹`,
|
||
updatedAt: nowIso(),
|
||
lastMessageAt: nowIso(),
|
||
isGroup: false,
|
||
unreadCount: 0,
|
||
riskLevel: "low",
|
||
contextBudgetPct: 80,
|
||
contextBudgetLabel: "80%",
|
||
messages: [
|
||
{
|
||
id: randomToken("auto"),
|
||
sender: "device",
|
||
senderLabel: payload.name,
|
||
body: `本机发现新的项目目录:${projectName}`,
|
||
sentAt: nowIso(),
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [],
|
||
versions: [],
|
||
}),
|
||
);
|
||
} else if (!existing.deviceIds.includes(payload.deviceId)) {
|
||
existing.deviceIds.push(payload.deviceId);
|
||
existing.isGroup = existing.deviceIds.length > 1;
|
||
}
|
||
}
|
||
}
|
||
let draft = upsertDeviceImportDraftFromHeartbeat(state, {
|
||
deviceId: payload.deviceId,
|
||
enrollmentId: claimedEnrollment?.enrollmentId,
|
||
candidates: normalizedCandidates,
|
||
});
|
||
|
||
if (
|
||
draft &&
|
||
shouldAutoSyncHeartbeatCandidates({
|
||
wasExistingDevice: Boolean(existingDevice),
|
||
device,
|
||
claimedEnrollment,
|
||
draft,
|
||
})
|
||
) {
|
||
const autoSyncDraft = draft;
|
||
const selectedCandidateIds = resolveAutoSyncCandidateIds(autoSyncDraft);
|
||
if (selectedCandidateIds.length > 0) {
|
||
autoSyncDraft.selectedCandidateIds = selectedCandidateIds;
|
||
autoSyncDraft.status = "pending_resolution";
|
||
autoSyncDraft.updatedAt = nowIso();
|
||
autoSyncDraft.reviewedAt = undefined;
|
||
autoSyncDraft.reviewedBy = undefined;
|
||
autoSyncDraft.resolutionId = undefined;
|
||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||
(item) => item.draftId !== autoSyncDraft.draftId,
|
||
);
|
||
|
||
const selectedCandidates = autoSyncDraft.candidates.filter((candidate) =>
|
||
autoSyncDraft.selectedCandidateIds.includes(candidate.candidateId),
|
||
);
|
||
const items = selectedCandidates.map((candidate) =>
|
||
resolveDeviceImportAction(state, payload.deviceId, candidate),
|
||
);
|
||
upsertDeviceImportResolutionInState(state, {
|
||
deviceId: payload.deviceId,
|
||
reviewedBy: "system:auto_sync",
|
||
summary: summarizeDeviceImportResolution(device.name, items),
|
||
items,
|
||
draftId: autoSyncDraft.draftId,
|
||
});
|
||
const applied = applyDeviceImportResolutionInState(state, {
|
||
deviceId: payload.deviceId,
|
||
appliedBy: "system:auto_sync",
|
||
draftId: autoSyncDraft.draftId,
|
||
pruneMissingCandidates: true,
|
||
});
|
||
draft = applied.draft;
|
||
}
|
||
}
|
||
|
||
return {
|
||
device,
|
||
token: claimedEnrollment?.token ?? device.token,
|
||
pairingStatus: claimedEnrollment?.status,
|
||
importDraft: draft,
|
||
};
|
||
});
|
||
publishBossEvent("devices.updated", { deviceId: payload.deviceId });
|
||
publishBossEvent("conversation.updated", { deviceId: payload.deviceId });
|
||
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} 项` : ""}。`;
|
||
}
|
||
|
||
function resolveAutoSyncCandidateIds(draft: DeviceImportDraft) {
|
||
const suggestedCandidateIds = draft.candidates
|
||
.filter((candidate) => candidate.suggestedImport !== false)
|
||
.map((candidate) => candidate.candidateId);
|
||
return dedupeStrings(
|
||
suggestedCandidateIds.length > 0
|
||
? suggestedCandidateIds
|
||
: draft.candidates.map((candidate) => candidate.candidateId),
|
||
);
|
||
}
|
||
|
||
function shouldAutoSyncHeartbeatCandidates(input: {
|
||
wasExistingDevice: boolean;
|
||
device: Device;
|
||
claimedEnrollment: DeviceEnrollment | null;
|
||
draft: DeviceImportDraft | null;
|
||
}) {
|
||
if (!input.wasExistingDevice) return false;
|
||
if (input.device.source !== "production") return false;
|
||
if (!input.draft || input.draft.candidates.length === 0) return false;
|
||
if (
|
||
input.claimedEnrollment?.enrollmentId &&
|
||
input.draft.enrollmentId === input.claimedEnrollment.enrollmentId
|
||
) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
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;
|
||
const reviewTask = draft
|
||
? state.masterAgentTasks.find(
|
||
(item) =>
|
||
item.taskType === "device_import_resolution" &&
|
||
item.deviceImportDraftId === draft.draftId,
|
||
) ?? null
|
||
: null;
|
||
return { draft, resolution, reviewTask };
|
||
}
|
||
|
||
export async function previewDeviceImportResolution(input: { deviceId: string }) {
|
||
const state = await readState();
|
||
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),
|
||
);
|
||
|
||
return {
|
||
draft: { ...draft },
|
||
device: { ...device },
|
||
items,
|
||
summary: summarizeDeviceImportResolution(device.name, items),
|
||
};
|
||
}
|
||
|
||
function upsertDeviceImportResolutionInState(
|
||
state: BossState,
|
||
input: {
|
||
deviceId: string;
|
||
reviewedBy: string;
|
||
summary: string;
|
||
items: DeviceImportResolutionItem[];
|
||
draftId?: string;
|
||
},
|
||
) {
|
||
const draft =
|
||
state.deviceImportDrafts.find(
|
||
(item) => item.draftId === input.draftId || item.deviceId === input.deviceId,
|
||
) ?? null;
|
||
if (!draft) throw new Error("DEVICE_IMPORT_DRAFT_NOT_FOUND");
|
||
if (draft.selectedCandidateIds.length === 0) {
|
||
throw new Error("DEVICE_IMPORT_SELECTION_REQUIRED");
|
||
}
|
||
|
||
const existingResolution = state.deviceImportResolutions.find((item) => item.draftId === draft.draftId);
|
||
const resolution = normalizeDeviceImportResolution({
|
||
resolutionId: existingResolution?.resolutionId ?? draft.resolutionId ?? randomToken("import-resolution"),
|
||
draftId: draft.draftId,
|
||
deviceId: input.deviceId,
|
||
status: "ready",
|
||
summary: input.summary,
|
||
items: input.items,
|
||
createdAt: existingResolution?.createdAt ?? nowIso(),
|
||
});
|
||
|
||
draft.status = "resolved";
|
||
draft.updatedAt = nowIso();
|
||
draft.reviewedAt = nowIso();
|
||
draft.reviewedBy = input.reviewedBy;
|
||
draft.resolutionId = resolution.resolutionId;
|
||
draft.appliedProjectNames = [];
|
||
|
||
state.deviceImportResolutions = [
|
||
resolution,
|
||
...state.deviceImportResolutions.filter((item) => item.draftId !== draft.draftId),
|
||
];
|
||
|
||
return { draft: { ...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),
|
||
);
|
||
draft.selectedCandidateIds = nextSelected;
|
||
draft.status = nextSelected.length > 0 ? "pending_resolution" : "pending_selection";
|
||
draft.appliedProjectNames = [];
|
||
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),
|
||
);
|
||
return upsertDeviceImportResolutionInState(state, {
|
||
deviceId: input.deviceId,
|
||
reviewedBy: input.reviewedBy,
|
||
summary: summarizeDeviceImportResolution(device.name, items),
|
||
items,
|
||
draftId: draft.draftId,
|
||
});
|
||
});
|
||
publishBossEvent("devices.updated", { deviceId: input.deviceId });
|
||
publishBossEvent("conversation.updated", { deviceId: input.deviceId });
|
||
return result;
|
||
}
|
||
|
||
function parseDeviceImportResolutionReply(
|
||
state: BossState,
|
||
draft: DeviceImportDraft,
|
||
replyBody: string,
|
||
) {
|
||
const trimmed = replyBody.trim();
|
||
if (!trimmed) {
|
||
throw new Error("DEVICE_IMPORT_RESOLUTION_REPLY_REQUIRED");
|
||
}
|
||
|
||
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
||
const jsonCandidate = fencedMatch?.[1]?.trim() ?? trimmed;
|
||
let parsed:
|
||
| {
|
||
summary?: string;
|
||
items?: Array<{
|
||
candidateId?: string;
|
||
action?: DeviceImportResolutionItem["action"];
|
||
targetProjectId?: string;
|
||
reason?: string;
|
||
}>;
|
||
}
|
||
| null = null;
|
||
|
||
try {
|
||
parsed = JSON.parse(jsonCandidate);
|
||
} catch {
|
||
throw new Error("DEVICE_IMPORT_RESOLUTION_JSON_INVALID");
|
||
}
|
||
|
||
const selectedCandidates = draft.candidates.filter((candidate) =>
|
||
draft.selectedCandidateIds.includes(candidate.candidateId),
|
||
);
|
||
const candidateMap = new Map(selectedCandidates.map((candidate) => [candidate.candidateId, candidate]));
|
||
const seenCandidateIds = new Set<string>();
|
||
const items: DeviceImportResolutionItem[] = [];
|
||
|
||
for (const rawItem of ensureArray(parsed?.items, [])) {
|
||
const candidateId = rawItem?.candidateId?.trim();
|
||
if (!candidateId || seenCandidateIds.has(candidateId)) continue;
|
||
const candidate = candidateMap.get(candidateId);
|
||
if (!candidate) continue;
|
||
seenCandidateIds.add(candidateId);
|
||
const heuristic = resolveDeviceImportAction(state, draft.deviceId, candidate);
|
||
items.push({
|
||
candidateId,
|
||
action:
|
||
rawItem.action === "attach_existing" ||
|
||
rawItem.action === "create_thread_conversation" ||
|
||
rawItem.action === "skip"
|
||
? rawItem.action
|
||
: heuristic.action,
|
||
threadDisplayName: candidate.threadDisplayName,
|
||
folderName: candidate.folderName,
|
||
targetProjectId:
|
||
typeof rawItem.targetProjectId === "string" && rawItem.targetProjectId.trim()
|
||
? rawItem.targetProjectId.trim()
|
||
: heuristic.targetProjectId,
|
||
reason: rawItem.reason?.trim() || heuristic.reason,
|
||
});
|
||
}
|
||
|
||
for (const candidate of selectedCandidates) {
|
||
if (!seenCandidateIds.has(candidate.candidateId)) {
|
||
items.push(resolveDeviceImportAction(state, draft.deviceId, candidate));
|
||
}
|
||
}
|
||
|
||
const device = state.devices.find((item) => item.id === draft.deviceId);
|
||
return {
|
||
summary:
|
||
parsed?.summary?.trim() ||
|
||
summarizeDeviceImportResolution(device?.name ?? draft.deviceId, items),
|
||
items,
|
||
};
|
||
}
|
||
|
||
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: [],
|
||
});
|
||
}
|
||
|
||
function candidateThreadSignature(candidate: DeviceImportCandidate) {
|
||
return (
|
||
trimToDefined(candidate.codexThreadRef) ??
|
||
trimToDefined(candidate.threadId) ??
|
||
`${candidate.folderName}:${candidate.threadDisplayName}`
|
||
);
|
||
}
|
||
|
||
function projectThreadSignature(project: Project) {
|
||
return (
|
||
trimToDefined(project.threadMeta.codexThreadRef) ??
|
||
trimToDefined(project.threadMeta.threadId) ??
|
||
`${project.threadMeta.folderName}:${project.threadMeta.threadDisplayName}`
|
||
);
|
||
}
|
||
|
||
function pruneStaleAutoImportedProjectsForDevice(
|
||
state: BossState,
|
||
device: Device,
|
||
selectedCandidates: DeviceImportCandidate[],
|
||
) {
|
||
const activeSignatures = new Set(selectedCandidates.map((candidate) => candidateThreadSignature(candidate)));
|
||
const reservedProjectIds = new Set(["master-agent", "boss-console", "audit-collab"]);
|
||
|
||
state.projects = state.projects.filter((project) => {
|
||
if (reservedProjectIds.has(project.id)) return true;
|
||
if (project.isGroup) return true;
|
||
if (!project.createdByAgent) return true;
|
||
if (!project.deviceIds.includes(device.id)) return true;
|
||
if (project.deviceIds.length !== 1) return true;
|
||
if (!trimToDefined(project.threadMeta.codexFolderRef) && !trimToDefined(project.threadMeta.folderName)) {
|
||
return true;
|
||
}
|
||
return activeSignatures.has(projectThreadSignature(project));
|
||
});
|
||
}
|
||
|
||
function applyDeviceImportResolutionInState(
|
||
state: BossState,
|
||
input: {
|
||
deviceId: string;
|
||
appliedBy: string;
|
||
draftId?: string;
|
||
pruneMissingCandidates?: boolean;
|
||
},
|
||
) {
|
||
const draft =
|
||
state.deviceImportDrafts.find(
|
||
(item) => item.draftId === input.draftId || item.deviceId === input.deviceId,
|
||
) ?? null;
|
||
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");
|
||
|
||
if (draft.status === "applied" && resolution.status === "applied") {
|
||
const importedProjects = state.projects.filter(
|
||
(project) =>
|
||
!project.isGroup &&
|
||
project.deviceIds.includes(device.id) &&
|
||
draft.appliedProjectNames.includes(project.name),
|
||
);
|
||
return {
|
||
draft: { ...draft },
|
||
resolution: { ...resolution },
|
||
importedProjects: importedProjects.map((project) => ({ ...project })),
|
||
};
|
||
}
|
||
if (draft.status !== "resolved") {
|
||
throw new Error("DEVICE_IMPORT_RESOLUTION_STALE");
|
||
}
|
||
|
||
const selectedCandidates = draft.candidates.filter((candidate) =>
|
||
draft.selectedCandidateIds.includes(candidate.candidateId),
|
||
);
|
||
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) {
|
||
const draftProject = buildImportedThreadProject(device, candidate);
|
||
targetProject =
|
||
state.projects.find((project) => project.id === draftProject.id) ??
|
||
state.projects.find(
|
||
(project) =>
|
||
!project.isGroup &&
|
||
project.deviceIds.includes(device.id) &&
|
||
((candidate.codexThreadRef &&
|
||
project.threadMeta.codexThreadRef === candidate.codexThreadRef) ||
|
||
project.threadMeta.threadId === candidate.threadId),
|
||
);
|
||
if (!targetProject) {
|
||
targetProject = draftProject;
|
||
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 });
|
||
}
|
||
|
||
if (input.pruneMissingCandidates) {
|
||
pruneStaleAutoImportedProjectsForDevice(state, device, selectedCandidates);
|
||
}
|
||
|
||
device.projects = dedupeStrings(
|
||
selectedCandidates.map((candidate) => candidate.folderName),
|
||
);
|
||
|
||
resolution.status = "applied";
|
||
resolution.appliedAt = nowIso();
|
||
resolution.appliedBy = input.appliedBy;
|
||
draft.status = "applied";
|
||
draft.appliedProjectNames = importedProjects.map((project) => project.name);
|
||
draft.updatedAt = nowIso();
|
||
|
||
return {
|
||
draft: { ...draft },
|
||
resolution: { ...resolution },
|
||
importedProjects,
|
||
};
|
||
}
|
||
|
||
export async function applyDeviceImportResolution(input: {
|
||
deviceId: string;
|
||
appliedBy: string;
|
||
}) {
|
||
const result = await mutateState((state) =>
|
||
applyDeviceImportResolutionInState(state, {
|
||
deviceId: input.deviceId,
|
||
appliedBy: input.appliedBy,
|
||
}),
|
||
);
|
||
|
||
publishBossEvent("devices.updated", { deviceId: input.deviceId });
|
||
publishBossEvent("conversation.updated", { deviceId: input.deviceId });
|
||
return result;
|
||
}
|
||
|
||
export async function upsertDeviceSkills(payload: {
|
||
deviceId: string;
|
||
skills: Array<{
|
||
name: string;
|
||
description?: string;
|
||
path: string;
|
||
invocation?: string;
|
||
category?: string;
|
||
}>;
|
||
}) {
|
||
const nextSkills = await mutateState((state) => {
|
||
const device = state.devices.find((item) => item.id === payload.deviceId);
|
||
if (!device) throw new Error("DEVICE_NOT_FOUND");
|
||
|
||
const syncedAt = nowIso();
|
||
const skills = payload.skills.map((skill) => ({
|
||
skillId: `${payload.deviceId}:${slugify(skill.name)}`,
|
||
deviceId: payload.deviceId,
|
||
name: skill.name,
|
||
description: skill.description?.trim() || "未提供说明",
|
||
path: skill.path,
|
||
invocation: skill.invocation?.trim() || `[$${skill.name}](${skill.path})`,
|
||
category: skill.category?.trim() || device.name,
|
||
updatedAt: syncedAt,
|
||
}));
|
||
|
||
state.deviceSkills = [
|
||
...skills,
|
||
...state.deviceSkills.filter((item) => item.deviceId !== payload.deviceId),
|
||
];
|
||
|
||
device.lastSeenAt = syncedAt;
|
||
return skills;
|
||
});
|
||
publishBossEvent("devices.skills.updated", {
|
||
deviceId: payload.deviceId,
|
||
note: `${nextSkills.length}`,
|
||
});
|
||
return nextSkills;
|
||
}
|
||
|
||
export async function appendAppLog(payload: {
|
||
deviceId: string;
|
||
projectId?: string;
|
||
level: AppLogLevel;
|
||
source: "app_client" | "local_agent";
|
||
category: string;
|
||
message: string;
|
||
detail?: string;
|
||
mirrorToMaster?: boolean;
|
||
}) {
|
||
const { entry, mirroredProjectId } = await mutateState((state) => {
|
||
const entry: AppLogEntry = {
|
||
logId: randomToken("applog"),
|
||
deviceId: payload.deviceId,
|
||
projectId: payload.projectId,
|
||
level: payload.level,
|
||
source: payload.source,
|
||
category: payload.category,
|
||
message: payload.message.trim(),
|
||
detail: payload.detail?.trim(),
|
||
mirroredToProject: Boolean(payload.mirrorToMaster),
|
||
createdAt: nowIso(),
|
||
};
|
||
|
||
state.appLogs.unshift(entry);
|
||
let mirroredProjectId: string | undefined;
|
||
if (payload.mirrorToMaster) {
|
||
const device = state.devices.find((item) => item.id === payload.deviceId);
|
||
pushProjectLedgerMessage(state, "master-agent", {
|
||
sender: payload.level === "error" ? "ops" : "device",
|
||
senderLabel: `${device?.name ?? payload.deviceId} · APP 日志`,
|
||
body: `[${payload.category}] ${payload.message}${payload.detail ? `\n${payload.detail}` : ""}`,
|
||
kind: "text",
|
||
});
|
||
if (shouldAutoReplyToMirroredLog(entry)) {
|
||
pushProjectLedgerMessage(state, "master-agent", {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: buildMasterAgentLogReply(state, entry),
|
||
kind: "text",
|
||
});
|
||
}
|
||
mirroredProjectId = "master-agent";
|
||
}
|
||
|
||
return { entry, mirroredProjectId };
|
||
});
|
||
publishBossEvent("app.logs.updated", {
|
||
deviceId: payload.deviceId,
|
||
projectId: payload.projectId,
|
||
note: payload.category,
|
||
});
|
||
if (mirroredProjectId) {
|
||
publishBossEvent("project.messages.updated", {
|
||
projectId: mirroredProjectId,
|
||
deviceId: payload.deviceId,
|
||
});
|
||
publishBossEvent("conversation.updated", {
|
||
projectId: mirroredProjectId,
|
||
deviceId: payload.deviceId,
|
||
});
|
||
}
|
||
return entry;
|
||
}
|
||
|
||
export async function updateConversationAction(
|
||
projectId: string,
|
||
action: "toggle_pin" | "mark_read",
|
||
) {
|
||
const project = await mutateState((state) => {
|
||
const nextProject = state.projects.find((item) => item.id === projectId);
|
||
if (!nextProject) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
if (action === "toggle_pin") {
|
||
if (nextProject.systemPinned) {
|
||
throw new Error("MASTER_PROJECT_PIN_LOCKED");
|
||
}
|
||
nextProject.pinned = !nextProject.pinned;
|
||
}
|
||
|
||
if (action === "mark_read") {
|
||
nextProject.unreadCount = 0;
|
||
}
|
||
|
||
return nextProject;
|
||
});
|
||
publishBossEvent("conversation.updated", { projectId });
|
||
return project;
|
||
}
|
||
|
||
export async function renameProjectThread(input: {
|
||
projectId: string;
|
||
threadDisplayName: string;
|
||
requestedBy: string;
|
||
}) {
|
||
const threadDisplayName = input.threadDisplayName.trim();
|
||
if (!threadDisplayName) {
|
||
throw new Error("THREAD_DISPLAY_NAME_REQUIRED");
|
||
}
|
||
|
||
const project = await mutateState((state) => {
|
||
const nextProject = state.projects.find((item) => item.id === input.projectId);
|
||
if (!nextProject) throw new Error("PROJECT_NOT_FOUND");
|
||
if (nextProject.isGroup) throw new Error("PROJECT_IS_GROUP_CHAT");
|
||
|
||
const updatedAt = nowIso();
|
||
nextProject.name = threadDisplayName;
|
||
nextProject.threadMeta.threadDisplayName = threadDisplayName;
|
||
nextProject.threadMeta.updatedAt = updatedAt;
|
||
nextProject.updatedAt = updatedAt;
|
||
return nextProject;
|
||
});
|
||
publishBossEvent("conversation.updated", {
|
||
projectId: input.projectId,
|
||
note: `renamed by ${input.requestedBy}`,
|
||
});
|
||
return project;
|
||
}
|
||
|
||
export async function renameGroupChat(input: {
|
||
projectId: string;
|
||
name: string;
|
||
requestedBy: string;
|
||
}) {
|
||
const name = input.name.trim();
|
||
if (!name) {
|
||
throw new Error("GROUP_CHAT_NAME_REQUIRED");
|
||
}
|
||
|
||
const project = await mutateState((state) => {
|
||
const nextProject = state.projects.find((item) => item.id === input.projectId);
|
||
if (!nextProject) throw new Error("PROJECT_NOT_FOUND");
|
||
if (!nextProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||
|
||
const updatedAt = nowIso();
|
||
nextProject.name = name;
|
||
nextProject.threadMeta.threadDisplayName = name;
|
||
nextProject.threadMeta.updatedAt = updatedAt;
|
||
nextProject.updatedAt = updatedAt;
|
||
return nextProject;
|
||
});
|
||
publishBossEvent("conversation.updated", {
|
||
projectId: input.projectId,
|
||
note: `renamed by ${input.requestedBy}`,
|
||
});
|
||
return project;
|
||
}
|
||
|
||
export async function createProjectGroupChat(input: {
|
||
sourceProjectId: string;
|
||
memberProjectIds: string[];
|
||
createdBy: string;
|
||
}) {
|
||
const project = await mutateState((state) => {
|
||
const source = state.projects.find((item) => item.id === input.sourceProjectId);
|
||
if (!source) throw new Error("GROUP_CHAT_SOURCE_NOT_FOUND");
|
||
return createGroupChatFromProjectIds(state, {
|
||
requestedProjectIds: [input.sourceProjectId, ...input.memberProjectIds],
|
||
createdBy: input.createdBy,
|
||
defaultRiskLevel: source.riskLevel,
|
||
});
|
||
});
|
||
publishBossEvent("project.messages.updated", { projectId: project.id });
|
||
publishBossEvent("conversation.updated", { projectId: project.id });
|
||
return project;
|
||
}
|
||
|
||
export async function createIndependentGroupChat(input: {
|
||
memberProjectIds: string[];
|
||
createdBy: string;
|
||
}) {
|
||
const project = await mutateState((state) =>
|
||
createGroupChatFromProjectIds(state, {
|
||
requestedProjectIds: input.memberProjectIds,
|
||
createdBy: input.createdBy,
|
||
}),
|
||
);
|
||
publishBossEvent("project.messages.updated", { projectId: project.id });
|
||
publishBossEvent("conversation.updated", { projectId: project.id });
|
||
return project;
|
||
}
|
||
|
||
function resolveGroupChatThreadProjects(
|
||
state: BossState,
|
||
requestedProjectIds: string[],
|
||
) {
|
||
const memberProjects: Project[] = [];
|
||
const seenProjectIds = new Set<string>();
|
||
for (const projectId of requestedProjectIds) {
|
||
if (!projectId || seenProjectIds.has(projectId)) {
|
||
continue;
|
||
}
|
||
seenProjectIds.add(projectId);
|
||
|
||
const memberProject = state.projects.find((item) => item.id === projectId);
|
||
if (!memberProject) {
|
||
throw new Error("GROUP_CHAT_MEMBER_NOT_FOUND");
|
||
}
|
||
if (!isDispatchableThreadProject(memberProject)) {
|
||
throw new Error("GROUP_CHAT_MEMBER_NOT_THREAD");
|
||
}
|
||
memberProjects.push(memberProject);
|
||
}
|
||
return memberProjects;
|
||
}
|
||
|
||
export async function replaceGroupChatMembers(input: {
|
||
projectId: string;
|
||
memberProjectIds: string[];
|
||
requestedBy: string;
|
||
}) {
|
||
const result = await mutateState((state) => {
|
||
const groupProject = state.projects.find((item) => item.id === input.projectId);
|
||
if (!groupProject) {
|
||
throw new Error("PROJECT_NOT_FOUND");
|
||
}
|
||
if (!groupProject.isGroup) {
|
||
throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||
}
|
||
|
||
const memberProjects = resolveGroupChatThreadProjects(state, input.memberProjectIds);
|
||
if (memberProjects.length < 2) {
|
||
throw new Error("GROUP_CHAT_REQUIRES_AT_LEAST_TWO_THREADS");
|
||
}
|
||
|
||
const now = nowIso();
|
||
groupProject.groupMembers = memberProjects.map((memberProject) => ({
|
||
projectId: memberProject.id,
|
||
deviceId: memberProject.deviceIds[0] ?? memberProject.id,
|
||
threadId: memberProject.threadMeta.threadId,
|
||
threadDisplayName: memberProject.threadMeta.threadDisplayName,
|
||
folderName: memberProject.threadMeta.folderName,
|
||
}));
|
||
groupProject.deviceIds = dedupeStrings(groupProject.groupMembers.map((member) => member.deviceId));
|
||
groupProject.threadMeta.activityIconCount = Math.max(1, groupProject.groupMembers.length);
|
||
groupProject.threadMeta.folderName = "群聊";
|
||
groupProject.threadMeta.updatedAt = now;
|
||
groupProject.updatedAt = now;
|
||
groupProject.lastMessageAt = now;
|
||
groupProject.approvalState = "not_required";
|
||
const memberLabel = memberProjects
|
||
.map((project) => project.threadMeta.threadDisplayName || project.name)
|
||
.join("、");
|
||
pushProjectLedgerMessage(state, groupProject.id, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: `已更新群成员:${memberLabel}`,
|
||
kind: "system_notice",
|
||
sentAt: now,
|
||
});
|
||
|
||
return {
|
||
project: { ...groupProject },
|
||
groupMembers: groupProject.groupMembers.map((member) => ({ ...member })),
|
||
};
|
||
});
|
||
|
||
publishBossEvent("project.messages.updated", { projectId: input.projectId });
|
||
publishBossEvent("conversation.updated", {
|
||
projectId: input.projectId,
|
||
note: `group members updated by ${input.requestedBy}`,
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function createGroupChatFromProjectIds(
|
||
state: BossState,
|
||
input: {
|
||
requestedProjectIds: string[];
|
||
createdBy: string;
|
||
defaultRiskLevel?: Project["riskLevel"];
|
||
},
|
||
) {
|
||
const memberProjects = resolveGroupChatThreadProjects(state, input.requestedProjectIds);
|
||
if (memberProjects.length < 2) {
|
||
throw new Error("GROUP_CHAT_REQUIRES_AT_LEAST_TWO_THREADS");
|
||
}
|
||
|
||
const now = nowIso();
|
||
const projectId = randomToken("project");
|
||
const threadId = randomToken("thread");
|
||
const threadDisplayName = buildAutoGroupChatName(memberProjects);
|
||
const folderName = "群聊";
|
||
const groupMembers = memberProjects.map((memberProject) => ({
|
||
projectId: memberProject.id,
|
||
deviceId: memberProject.deviceIds[0] ?? memberProject.id,
|
||
threadId: memberProject.threadMeta.threadId,
|
||
threadDisplayName: memberProject.threadMeta.threadDisplayName,
|
||
folderName: memberProject.threadMeta.folderName,
|
||
}));
|
||
const seedProject = memberProjects[0];
|
||
const nextProject = normalizeProject({
|
||
id: projectId,
|
||
name: threadDisplayName,
|
||
pinned: false,
|
||
systemPinned: false,
|
||
deviceIds: dedupeStrings(groupMembers.map((member) => member.deviceId)),
|
||
preview: `已创建群聊《${threadDisplayName}》`,
|
||
updatedAt: now,
|
||
lastMessageAt: now,
|
||
isGroup: true,
|
||
unreadCount: 0,
|
||
riskLevel: input.defaultRiskLevel ?? seedProject?.riskLevel ?? "normal",
|
||
threadMeta: {
|
||
projectId,
|
||
threadId,
|
||
threadDisplayName,
|
||
folderName,
|
||
activityIconCount: Math.max(1, memberProjects.length),
|
||
updatedAt: now,
|
||
},
|
||
groupMembers,
|
||
createdByAgent: true,
|
||
collaborationMode: "development",
|
||
approvalState: "not_required",
|
||
messages: [
|
||
{
|
||
id: randomToken("msg"),
|
||
sender: "master",
|
||
senderLabel: input.createdBy || "群聊创建",
|
||
body: `已由 ${input.createdBy || "系统"} 创建群聊《${threadDisplayName}》。`,
|
||
sentAt: now,
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [],
|
||
versions: [],
|
||
});
|
||
|
||
state.projects.unshift(nextProject);
|
||
return nextProject;
|
||
}
|
||
|
||
function buildAutoGroupChatName(memberProjects: Project[]) {
|
||
const titles = memberProjects
|
||
.map((project) => project.threadMeta.threadDisplayName || project.name)
|
||
.filter((title) => typeof title === "string" && title.trim().length > 0);
|
||
if (titles.length === 0) {
|
||
return "新群聊";
|
||
}
|
||
if (titles.length === 1) {
|
||
return titles[0];
|
||
}
|
||
if (titles.length === 2) {
|
||
return `${titles[0]}、${titles[1]}`;
|
||
}
|
||
return `${titles[0]}、${titles[1]}等${titles.length}个线程`;
|
||
}
|
||
|
||
export async function appendProjectMessage(payload: {
|
||
projectId: string;
|
||
sender?: MessageSender;
|
||
senderLabel?: string;
|
||
body?: string;
|
||
kind?: MessageKind;
|
||
attachments?: MessageAttachment[];
|
||
}) {
|
||
const message = await mutateState((state) => {
|
||
const project = state.projects.find((item) => item.id === payload.projectId);
|
||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const body = payload.body?.trim();
|
||
if (!body && payload.kind === "text") {
|
||
throw new Error("MESSAGE_BODY_REQUIRED");
|
||
}
|
||
if (payload.kind === "attachment" && (!payload.attachments || payload.attachments.length === 0)) {
|
||
throw new Error("ATTACHMENT_REQUIRED");
|
||
}
|
||
|
||
const firstAttachment = payload.attachments?.[0];
|
||
const message: Message = {
|
||
id: randomToken("msg"),
|
||
sender: payload.sender ?? "user",
|
||
senderLabel: payload.senderLabel ?? "你",
|
||
body:
|
||
body ??
|
||
(payload.kind === "attachment"
|
||
? buildAttachmentMessageBody(
|
||
firstAttachment ?? {
|
||
attachmentId: randomToken("att"),
|
||
fileName: "附件",
|
||
mimeType: "application/octet-stream",
|
||
fileSizeBytes: 0,
|
||
attachmentKind: "binary",
|
||
storageBackend: "server_file",
|
||
storagePath: "",
|
||
previewAvailable: false,
|
||
uploadedAt: nowIso(),
|
||
uploadedBy: payload.senderLabel ?? "你",
|
||
analysisState: "not_applicable",
|
||
},
|
||
)
|
||
: payload.kind === "voice_intent"
|
||
? "已提交语音转文字请求,等待主 Agent 记录语音摘要。"
|
||
: payload.kind === "image_intent"
|
||
? "已登记图片证据上传请求,等待对象存储通道接入。"
|
||
: payload.kind === "video_intent"
|
||
? "已登记视频证据上传请求,等待对象存储通道接入。"
|
||
: "已提交消息。"),
|
||
sentAt: nowIso(),
|
||
kind: payload.kind ?? "text",
|
||
attachments: payload.attachments?.map((attachment) => normalizeMessageAttachment(attachment)),
|
||
};
|
||
|
||
project.messages.push(message);
|
||
project.unreadCount = 0;
|
||
project.lastMessageAt = message.sentAt;
|
||
project.preview = message.body;
|
||
|
||
return message;
|
||
});
|
||
publishBossEvent("project.messages.updated", { projectId: payload.projectId });
|
||
publishBossEvent("conversation.updated", { projectId: payload.projectId });
|
||
return message;
|
||
}
|
||
|
||
export async function appendAttachmentMessage(payload: {
|
||
projectId: string;
|
||
sender?: MessageSender;
|
||
senderLabel?: string;
|
||
attachment: MessageAttachment;
|
||
body?: string;
|
||
}) {
|
||
return appendProjectMessage({
|
||
projectId: payload.projectId,
|
||
sender: payload.sender ?? "user",
|
||
senderLabel: payload.senderLabel ?? "你",
|
||
body: payload.body ?? buildAttachmentMessageBody(payload.attachment),
|
||
kind: "attachment",
|
||
attachments: [payload.attachment],
|
||
});
|
||
}
|
||
|
||
function findProjectMessage(project: Project, messageId: string) {
|
||
return project.messages.find((message) => message.id === messageId) ?? null;
|
||
}
|
||
|
||
export function findProjectAttachment(
|
||
project: Project,
|
||
attachmentId: string,
|
||
): { message: Message; attachment: MessageAttachment } | null {
|
||
for (const message of project.messages) {
|
||
const attachment = message.attachments?.find((item) => item.attachmentId === attachmentId);
|
||
if (attachment) {
|
||
return { message, attachment };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export async function getProjectAttachment(projectId: string, attachmentId: string) {
|
||
const state = await readState();
|
||
const project = state.projects.find((item) => item.id === projectId);
|
||
if (!project) {
|
||
return null;
|
||
}
|
||
const match = findProjectAttachment(project, attachmentId);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
return {
|
||
project,
|
||
message: match.message,
|
||
attachment: match.attachment,
|
||
};
|
||
}
|
||
|
||
export async function getAttachmentById(attachmentId: string) {
|
||
const state = await readState();
|
||
for (const project of state.projects) {
|
||
const match = findProjectAttachment(project, attachmentId);
|
||
if (match) {
|
||
return {
|
||
project,
|
||
message: match.message,
|
||
attachment: match.attachment,
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function summarizeAttachmentAnalysis(body: string) {
|
||
const compact = body.replace(/\s+/g, " ").trim();
|
||
if (!compact) {
|
||
return "附件分析已完成。";
|
||
}
|
||
return compact.length <= 120 ? compact : `${compact.slice(0, 117)}...`;
|
||
}
|
||
|
||
export async function updateAttachmentAnalysisResult(payload: {
|
||
projectId: string;
|
||
attachmentId: string;
|
||
status: Exclude<AttachmentAnalysisState, "not_applicable" | "queued_auto" | "ready_manual">;
|
||
summary?: string;
|
||
cardBody?: string;
|
||
}) {
|
||
return mutateState((state) => {
|
||
const project = state.projects.find((item) => item.id === payload.projectId);
|
||
if (!project) {
|
||
throw new Error("PROJECT_NOT_FOUND");
|
||
}
|
||
const match = findProjectAttachment(project, payload.attachmentId);
|
||
if (!match) {
|
||
throw new Error("ATTACHMENT_NOT_FOUND");
|
||
}
|
||
|
||
match.attachment.analysisState = payload.status;
|
||
match.attachment.analysisSummary =
|
||
payload.status === "completed"
|
||
? payload.summary?.trim() || summarizeAttachmentAnalysis(payload.cardBody ?? "")
|
||
: payload.summary;
|
||
match.attachment.analysisCardId = undefined;
|
||
|
||
if (payload.status === "completed" && payload.cardBody?.trim()) {
|
||
const summary = payload.summary?.trim() || summarizeAttachmentAnalysis(payload.cardBody);
|
||
pushProjectLedgerMessage(state, payload.projectId, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: summary,
|
||
kind: "text",
|
||
});
|
||
const card = pushProjectLedgerMessage(state, payload.projectId, {
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: payload.cardBody.trim(),
|
||
kind: "analysis_card",
|
||
});
|
||
match.attachment.analysisCardId = card?.id;
|
||
match.attachment.analysisSummary = summary;
|
||
}
|
||
|
||
return {
|
||
projectId: payload.projectId,
|
||
attachmentId: payload.attachmentId,
|
||
analysisState: match.attachment.analysisState,
|
||
analysisSummary: match.attachment.analysisSummary,
|
||
analysisCardId: match.attachment.analysisCardId,
|
||
};
|
||
}).then((result) => {
|
||
publishBossEvent("project.messages.updated", { projectId: result.projectId });
|
||
publishBossEvent("conversation.updated", { projectId: result.projectId });
|
||
return result;
|
||
});
|
||
}
|
||
|
||
function requiresForwardApproval(source: Project, target: Project) {
|
||
return source.collaborationMode === "approval_required" && target.id !== "master-agent";
|
||
}
|
||
|
||
function buildForwardSingleMessage(input: {
|
||
source: Project;
|
||
target: Project;
|
||
message: Message;
|
||
requestedBy: string;
|
||
}) {
|
||
const sentAt = nowIso();
|
||
const body = `转发自《${input.source.name}》到《${input.target.name}》:${input.message.body}`;
|
||
return {
|
||
id: randomToken("forward"),
|
||
sender: "user" as const,
|
||
senderLabel: "你",
|
||
body,
|
||
sentAt,
|
||
kind: "forward_single" as const,
|
||
forwardSource: {
|
||
sourceProjectId: input.source.id,
|
||
sourceProjectName: input.source.name,
|
||
sourceThreadId: input.source.threadMeta?.threadId,
|
||
sourceThreadTitle: input.source.threadMeta?.threadDisplayName,
|
||
sourceMessageId: input.message.id,
|
||
forwardedBy: input.requestedBy,
|
||
forwardedAt: sentAt,
|
||
},
|
||
} satisfies Message;
|
||
}
|
||
|
||
function buildForwardBundleMessage(input: {
|
||
source: Project;
|
||
target: Project;
|
||
messages: Message[];
|
||
requestedBy: string;
|
||
}) {
|
||
const sentAt = nowIso();
|
||
const startedAt = input.messages[0]?.sentAt ?? sentAt;
|
||
const endedAt = input.messages[input.messages.length - 1]?.sentAt ?? sentAt;
|
||
const body = `转发自《${input.source.name}》到《${input.target.name}》:${input.messages.length} 条消息,最后一条:${
|
||
input.messages[input.messages.length - 1]?.body ?? ""
|
||
}`;
|
||
return {
|
||
id: randomToken("forward"),
|
||
sender: "user" as const,
|
||
senderLabel: "你",
|
||
body,
|
||
sentAt,
|
||
kind: "forward_bundle" as const,
|
||
forwardBundle: {
|
||
sourceProjectId: input.source.id,
|
||
sourceProjectName: input.source.name,
|
||
sourceThreadId: input.source.threadMeta?.threadId,
|
||
sourceThreadTitle: input.source.threadMeta?.threadDisplayName,
|
||
itemCount: input.messages.length,
|
||
startedAt,
|
||
endedAt,
|
||
items: input.messages.map((message) => ({
|
||
messageId: message.id,
|
||
senderLabel: message.senderLabel,
|
||
body: message.body,
|
||
kind: message.kind ?? "text",
|
||
sentAt: message.sentAt,
|
||
})),
|
||
},
|
||
} satisfies Message;
|
||
}
|
||
|
||
export async function forwardProjectMessage(
|
||
payload:
|
||
| {
|
||
sourceProjectId: string;
|
||
mode: "single";
|
||
targetProjectId: string;
|
||
sourceMessageId: string;
|
||
requestedBy: string;
|
||
}
|
||
| {
|
||
sourceProjectId: string;
|
||
mode: "bundle";
|
||
targetProjectId: string;
|
||
sourceMessageIds: string[];
|
||
requestedBy: string;
|
||
},
|
||
) {
|
||
const state = await readState();
|
||
const source = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||
const target = state.projects.find((item) => item.id === payload.targetProjectId);
|
||
if (!source || !target) throw new Error("PROJECT_NOT_FOUND");
|
||
if (requiresForwardApproval(source, target)) {
|
||
return {
|
||
approvalRequired: true,
|
||
approvalReason: "NON_DEVELOPMENT_THREAD_FORWARD",
|
||
} as const;
|
||
}
|
||
|
||
if (payload.mode === "single") {
|
||
const sourceMessage = findProjectMessage(source, payload.sourceMessageId);
|
||
if (!sourceMessage) throw new Error("MESSAGE_NOT_FOUND");
|
||
|
||
const message = await mutateState((state) => {
|
||
const sourceProject = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||
const targetProject = state.projects.find((item) => item.id === payload.targetProjectId);
|
||
if (!sourceProject || !targetProject) throw new Error("PROJECT_NOT_FOUND");
|
||
const sourceLedgerMessage = findProjectMessage(sourceProject, payload.sourceMessageId);
|
||
if (!sourceLedgerMessage) throw new Error("MESSAGE_NOT_FOUND");
|
||
|
||
const message = buildForwardSingleMessage({
|
||
source: sourceProject,
|
||
target: targetProject,
|
||
message: sourceLedgerMessage,
|
||
requestedBy: payload.requestedBy,
|
||
});
|
||
|
||
targetProject.messages.push(message);
|
||
targetProject.unreadCount += 1;
|
||
targetProject.lastMessageAt = message.sentAt;
|
||
targetProject.preview = message.body;
|
||
|
||
sourceProject.messages.push({
|
||
id: randomToken("forward-log"),
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: `已转发到《${targetProject.name}》。`,
|
||
sentAt: message.sentAt,
|
||
kind: "forward_notice",
|
||
});
|
||
sourceProject.lastMessageAt = message.sentAt;
|
||
return message;
|
||
});
|
||
|
||
publishBossEvent("project.messages.updated", { projectId: payload.sourceProjectId });
|
||
publishBossEvent("project.messages.updated", { projectId: payload.targetProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: payload.sourceProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: payload.targetProjectId });
|
||
return { message };
|
||
}
|
||
|
||
const sourceMessageIds = payload.mode === "bundle" ? payload.sourceMessageIds : [];
|
||
const sourceMessages = sourceMessageIds
|
||
.map((messageId) => findProjectMessage(source, messageId))
|
||
.filter((message): message is Message => Boolean(message));
|
||
if (sourceMessages.length <= 1 || sourceMessages.length !== sourceMessageIds.length) {
|
||
throw new Error("MESSAGE_NOT_FOUND");
|
||
}
|
||
|
||
const message = await mutateState((state) => {
|
||
const sourceProject = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||
const targetProject = state.projects.find((item) => item.id === payload.targetProjectId);
|
||
if (!sourceProject || !targetProject) throw new Error("PROJECT_NOT_FOUND");
|
||
|
||
const bundleMessages = sourceMessageIds
|
||
.map((messageId) => findProjectMessage(sourceProject, messageId))
|
||
.filter((item): item is Message => Boolean(item));
|
||
if (bundleMessages.length <= 1 || bundleMessages.length !== sourceMessageIds.length) {
|
||
throw new Error("MESSAGE_NOT_FOUND");
|
||
}
|
||
|
||
const message = buildForwardBundleMessage({
|
||
source: sourceProject,
|
||
target: targetProject,
|
||
messages: bundleMessages,
|
||
requestedBy: payload.requestedBy,
|
||
});
|
||
|
||
targetProject.messages.push(message);
|
||
targetProject.unreadCount += 1;
|
||
targetProject.lastMessageAt = message.sentAt;
|
||
targetProject.preview = message.body;
|
||
|
||
sourceProject.messages.push({
|
||
id: randomToken("forward-log"),
|
||
sender: "master",
|
||
senderLabel: "主 Agent",
|
||
body: `已转发到《${targetProject.name}》。`,
|
||
sentAt: message.sentAt,
|
||
kind: "forward_notice",
|
||
});
|
||
sourceProject.lastMessageAt = message.sentAt;
|
||
return message;
|
||
});
|
||
|
||
publishBossEvent("project.messages.updated", { projectId: payload.sourceProjectId });
|
||
publishBossEvent("project.messages.updated", { projectId: payload.targetProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: payload.sourceProjectId });
|
||
publishBossEvent("conversation.updated", { projectId: payload.targetProjectId });
|
||
return { message };
|
||
}
|
||
|
||
export async function updateUserSettings(settings: Partial<UserSettings>) {
|
||
const nextSettings = await mutateState((state) => {
|
||
state.user.settings = {
|
||
...state.user.settings,
|
||
...settings,
|
||
};
|
||
return state.user.settings;
|
||
});
|
||
publishBossEvent("conversation.updated", { deviceId: PRIMARY_CODEX_NODE_ID });
|
||
return nextSettings;
|
||
}
|
||
|
||
export async function getOtaStatus() {
|
||
const state = await readState();
|
||
const available = firstAvailableOta(state);
|
||
const asset = await getPublishedOtaAsset();
|
||
const availableRelease = available
|
||
? {
|
||
...available,
|
||
packageFileName: asset?.fileName,
|
||
packageSizeBytes: asset?.sizeBytes,
|
||
packageSha256: asset?.sha256,
|
||
downloadUrl: asset?.downloadUrl,
|
||
assetUpdatedAt: asset?.updatedAt,
|
||
}
|
||
: null;
|
||
|
||
return {
|
||
currentVersion: state.user.version,
|
||
hasOta: state.user.hasOta,
|
||
availableRelease,
|
||
logs: state.otaUpdateLogs,
|
||
canApply: state.user.role === "highest_admin",
|
||
boundCodexNodeLabel: state.user.boundCodexNodeLabel,
|
||
roleLabel: state.user.roleLabel,
|
||
};
|
||
}
|
||
|
||
export async function checkForOta() {
|
||
const result = await mutateState((state) => {
|
||
const available = firstAvailableOta(state);
|
||
state.otaUpdateLogs.unshift({
|
||
logId: randomToken("otalog"),
|
||
releaseId: available?.releaseId ?? "ota_check_empty",
|
||
version: available?.version ?? state.user.version,
|
||
status: "checked",
|
||
triggeredBy: state.user.name,
|
||
triggeredAt: nowIso(),
|
||
note: available
|
||
? `检查到可用 OTA:${available.version},目标范围:${available.targetScope}。`
|
||
: "检查完成,当前已是最新版本。",
|
||
});
|
||
return {
|
||
currentVersion: state.user.version,
|
||
availableRelease: available,
|
||
hasOta: Boolean(available),
|
||
deviceId: state.user.boundDeviceId,
|
||
note: available?.version ?? state.user.version,
|
||
};
|
||
});
|
||
const asset = await getPublishedOtaAsset();
|
||
publishBossEvent("ota.updated", {
|
||
deviceId: result.deviceId,
|
||
note: result.note,
|
||
});
|
||
return {
|
||
...result,
|
||
availableRelease: result.availableRelease
|
||
? {
|
||
...result.availableRelease,
|
||
packageFileName: asset?.fileName,
|
||
packageSizeBytes: asset?.sizeBytes,
|
||
packageSha256: asset?.sha256,
|
||
downloadUrl: asset?.downloadUrl,
|
||
assetUpdatedAt: asset?.updatedAt,
|
||
}
|
||
: null,
|
||
};
|
||
}
|
||
|
||
export async function performOta() {
|
||
const result = await mutateState((state) => {
|
||
const available = firstAvailableOta(state);
|
||
if (!available) {
|
||
throw new Error("NO_OTA_AVAILABLE");
|
||
}
|
||
if (state.user.role !== "highest_admin") {
|
||
throw new Error("ADMIN_REQUIRED_FOR_OTA");
|
||
}
|
||
|
||
const nextVersion = available.version;
|
||
const otaSummary = [...available.summary];
|
||
state.user.version = nextVersion;
|
||
available.status = "applied";
|
||
state.otaUpdateLogs.unshift({
|
||
logId: randomToken("otalog"),
|
||
releaseId: available.releaseId,
|
||
version: available.version,
|
||
status: "applied",
|
||
triggeredBy: state.user.name,
|
||
triggeredAt: nowIso(),
|
||
completedAt: nowIso(),
|
||
note: `由 ${state.user.roleLabel} 在 ${state.user.boundCodexNodeLabel ?? PRIMARY_CODEX_NODE_LABEL} 发起 OTA。`,
|
||
});
|
||
|
||
const project = state.projects.find((item) => item.id === "master-agent");
|
||
if (project) {
|
||
project.messages.push({
|
||
id: randomToken("ota"),
|
||
sender: "ops",
|
||
senderLabel: "OTA 控制面",
|
||
body: `已完成 OTA 升级到 ${nextVersion},变更:${otaSummary.join(";") || "无"}`,
|
||
sentAt: nowIso(),
|
||
kind: "text",
|
||
});
|
||
project.lastMessageAt = nowIso();
|
||
if (!project.versions.some((entry) => entry.version === nextVersion)) {
|
||
project.versions.unshift({
|
||
version: nextVersion,
|
||
summary: `通过 OTA 发布:${otaSummary.join(";") || "无"}`,
|
||
createdAt: nowIso(),
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
version: state.user.version,
|
||
summary: otaSummary,
|
||
deviceId: state.user.boundDeviceId,
|
||
nextVersion,
|
||
};
|
||
});
|
||
const asset = await getPublishedOtaAsset();
|
||
publishBossEvent("ota.updated", {
|
||
deviceId: result.deviceId,
|
||
projectId: "master-agent",
|
||
note: result.nextVersion,
|
||
});
|
||
publishBossEvent("project.messages.updated", { projectId: "master-agent" });
|
||
publishBossEvent("conversation.updated", { projectId: "master-agent" });
|
||
return {
|
||
...result,
|
||
downloadUrl: asset?.downloadUrl,
|
||
packageFileName: asset?.fileName,
|
||
packageSizeBytes: asset?.sizeBytes,
|
||
packageSha256: asset?.sha256,
|
||
};
|
||
}
|
||
|
||
export async function approveRepairTicket(ticketId: string) {
|
||
return mutateState((state) => {
|
||
const ticket = state.opsRepairTickets.find((item) => item.ticketId === ticketId);
|
||
if (!ticket) throw new Error("TICKET_NOT_FOUND");
|
||
ticket.approvalStatus = "approved";
|
||
ticket.executionStatus = "running";
|
||
ticket.approvedBy = "主 Agent";
|
||
ticket.updatedAt = nowIso();
|
||
return ticket;
|
||
});
|
||
}
|
||
|
||
export async function verifyRepairTicket(ticketId: string) {
|
||
return mutateState((state) => {
|
||
const ticket = state.opsRepairTickets.find((item) => item.ticketId === ticketId);
|
||
if (!ticket) throw new Error("TICKET_NOT_FOUND");
|
||
ticket.executionStatus = "verified";
|
||
ticket.updatedAt = nowIso();
|
||
|
||
const verification = state.opsRepairVerifications.find((item) => item.ticketId === ticketId);
|
||
if (verification) {
|
||
verification.status = "passed";
|
||
verification.summary = "最新一轮修复已通过复验,可关闭工单。";
|
||
verification.verifiedAt = nowIso();
|
||
}
|
||
|
||
const fault = state.opsFaults.find((item) => item.faultId === ticket.faultId);
|
||
if (fault) {
|
||
fault.status = "resolved";
|
||
fault.lastSeenAt = nowIso();
|
||
}
|
||
|
||
return ticket;
|
||
});
|
||
}
|