feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -13,6 +13,7 @@ import {
type OmxTeamBackendSelectionState,
} from "@/lib/execution/backends/omx-team-backend";
import { selectOrchestrationBackend } from "@/lib/execution/orchestration-backend-selector";
import { hasRecentThreadConversationExternalActivity } from "@/lib/thread-execution-conflict";
export type DeviceStatus = "online" | "abnormal" | "offline";
export type DeviceSource = "production" | "demo";
@@ -137,7 +138,16 @@ export type ProjectConflictState = "none" | "warning" | "blocked";
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 AiProvider =
| "master_codex_node"
| "google_oauth"
| "chatgpt_oauth"
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_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";
@@ -285,6 +295,15 @@ export interface VersionEntry {
createdAt: string;
}
type ProjectUnderstandingSyncReply = {
projectGoal?: string;
currentProgress?: string;
technicalArchitecture?: string;
currentBlockers?: string;
recommendedNextStep?: string;
versionRecord?: string;
};
export interface ThreadConversationMeta {
projectId: string;
threadId: string;
@@ -405,6 +424,8 @@ export function buildCollaborationGate(
export interface ProjectAgentControls {
modelOverride?: string;
reasoningEffortOverride?: ReasoningEffort;
fastModelOverride?: string;
deepModelOverride?: string;
promptOverride?: string;
backendOverride?: "claw-runtime";
takeoverEnabled?: boolean;
@@ -658,6 +679,7 @@ export interface AiAccount {
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
apiKeyMasked?: string;
enabled: boolean;
@@ -696,6 +718,7 @@ export interface AiAccountSummary {
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
enabled: boolean;
isActive: boolean;
canGenerate: boolean;
@@ -764,6 +787,7 @@ export interface MasterAgentTask {
deviceImportCandidateFolderName?: string;
projectUnderstandingTargetProjectId?: string;
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
relayViaMasterAgent?: boolean;
status: MasterAgentTaskStatus;
requestedAt: string;
claimedAt?: string;
@@ -1092,6 +1116,7 @@ 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 THREAD_STATUS_FULL_SYNC_INTERVAL_MS = 30 * 60_000;
const ENV_OPENAI_ACCOUNT_ID = "env-openai-api";
function baseThreadChecklist(labels: string[]) {
@@ -2352,6 +2377,8 @@ function normalizeProjectAgentControls(
const reasoningEffortOverride = isReasoningEffort(raw?.reasoningEffortOverride)
? raw.reasoningEffortOverride
: undefined;
const fastModelOverride = trimToDefined(raw?.fastModelOverride);
const deepModelOverride = trimToDefined(raw?.deepModelOverride);
const promptOverride = trimToDefined(raw?.promptOverride);
const backendOverride = raw?.backendOverride === "claw-runtime" ? raw.backendOverride : undefined;
const takeoverEnabled = typeof raw?.takeoverEnabled === "boolean" ? raw.takeoverEnabled : undefined;
@@ -2361,6 +2388,8 @@ function normalizeProjectAgentControls(
if (
!modelOverride &&
!reasoningEffortOverride &&
!fastModelOverride &&
!deepModelOverride &&
!promptOverride &&
!backendOverride &&
takeoverEnabled === undefined &&
@@ -2372,6 +2401,8 @@ function normalizeProjectAgentControls(
return {
modelOverride,
reasoningEffortOverride,
fastModelOverride,
deepModelOverride,
promptOverride,
backendOverride,
takeoverEnabled,
@@ -2492,15 +2523,61 @@ export function aiProviderLabel(provider: AiProvider) {
switch (provider) {
case "master_codex_node":
return "Master Codex Node / ChatGPT Plus 节点";
case "google_oauth":
return "谷歌登录";
case "chatgpt_oauth":
return "ChatGPT登录";
case "openai_api":
return "OpenAI API";
case "aliyun_qwen_api":
return "阿里百炼 Qwen";
case "minimax_api":
return "MiniMax API";
case "glm_api":
return "GLM API";
case "hyzq_api":
return "环宇智擎 API";
case "custom_api":
return "自定义 API";
default:
return provider;
}
}
export function aiProviderDefaultApiBaseUrl(provider: AiProvider) {
switch (provider) {
case "openai_api":
return "https://api.openai.com/v1";
case "aliyun_qwen_api":
return "https://dashscope.aliyuncs.com/compatible-mode/v1";
case "minimax_api":
return "https://api.minimaxi.com/v1";
case "glm_api":
return "https://open.bigmodel.cn/api/paas/v4";
case "hyzq_api":
return "https://api.hyzq2046.com/v1";
default:
return undefined;
}
}
export function aiProviderDefaultModel(provider: AiProvider) {
switch (provider) {
case "openai_api":
return "gpt-5.4";
case "aliyun_qwen_api":
return "qwen3.5-plus";
case "minimax_api":
return "MiniMax-M1";
case "glm_api":
return "glm-4.5";
case "hyzq_api":
return "gpt-5.4-mini";
default:
return undefined;
}
}
export function aiStatusLabel(status: AiAccountStatus) {
switch (status) {
case "ready":
@@ -2525,8 +2602,20 @@ function maskApiKey(value?: string) {
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
}
function isApiKeyProvider(provider: AiProvider) {
return provider === "openai_api" || provider === "aliyun_qwen_api";
function normalizeApiBaseUrl(value?: string) {
if (!value?.trim()) return undefined;
return value.trim().replace(/\/+$/, "");
}
export function isApiKeyProvider(provider: AiProvider) {
return (
provider === "openai_api" ||
provider === "aliyun_qwen_api" ||
provider === "minimax_api" ||
provider === "glm_api" ||
provider === "hyzq_api" ||
provider === "custom_api"
);
}
function deriveAiAccountStatus(account: AiAccount): AiAccountStatus {
@@ -2585,6 +2674,7 @@ function buildAiAccountSummary(account: AiAccount, options?: { isEnvironmentFall
nodeId: normalized.nodeId,
nodeLabel: normalized.nodeLabel,
model: normalized.model,
apiBaseUrl: normalized.apiBaseUrl,
enabled: normalized.enabled,
isActive: normalized.isActive,
canGenerate: aiAccountCanGenerate(normalized),
@@ -2615,6 +2705,7 @@ function getEnvOpenAiAccount() {
provider: "openai_api",
displayName: "环境变量 OpenAI API",
model: process.env.OPENAI_MODEL?.trim() || "gpt-5.4",
apiBaseUrl: normalizeApiBaseUrl(process.env.OPENAI_API_BASE_URL),
apiKey,
apiKeyMasked: maskApiKey(apiKey),
enabled: true,
@@ -3252,6 +3343,8 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
targetProjectId: task.targetProjectId,
targetThreadId: task.targetThreadId,
targetThreadDisplayName: task.targetThreadDisplayName,
targetCodexThreadRef: task.targetCodexThreadRef,
targetCodexFolderRef: task.targetCodexFolderRef,
orchestrationBackendId:
task.orchestrationBackendId === "omx-team" || task.orchestrationBackendId === "boss-native-orchestrator"
? task.orchestrationBackendId
@@ -3265,6 +3358,7 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
task.projectUnderstandingReason === "heartbeat_activity" || task.projectUnderstandingReason === "thread_reply"
? task.projectUnderstandingReason
: undefined,
relayViaMasterAgent: task.relayViaMasterAgent === true ? true : undefined,
status: task.status ?? "queued",
requestedAt: task.requestedAt ?? nowIso(),
claimedAt: task.claimedAt,
@@ -4260,6 +4354,8 @@ export async function updateProjectAgentControls(
payload: {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
fastModelOverride?: unknown;
deepModelOverride?: unknown;
promptOverride?: unknown;
backendOverride?: unknown;
takeoverEnabled?: unknown;
@@ -4278,6 +4374,12 @@ export async function updateProjectAgentControls(
const reasoningEffortInput = Object.prototype.hasOwnProperty.call(payload, "reasoningEffortOverride")
? parseReasoningEffortOverride(payload.reasoningEffortOverride)
: { kind: "preserve" as const };
const fastModelOverrideInput = Object.prototype.hasOwnProperty.call(payload, "fastModelOverride")
? parseControlTextOverride(payload.fastModelOverride)
: { kind: "preserve" as const };
const deepModelOverrideInput = Object.prototype.hasOwnProperty.call(payload, "deepModelOverride")
? parseControlTextOverride(payload.deepModelOverride)
: { kind: "preserve" as const };
const promptOverrideInput = Object.prototype.hasOwnProperty.call(payload, "promptOverride")
? parseControlTextOverride(payload.promptOverride)
: { kind: "preserve" as const };
@@ -4296,6 +4398,12 @@ export async function updateProjectAgentControls(
if (reasoningEffortInput.kind === "invalid") {
throw new Error("INVALID_REASONING_EFFORT_OVERRIDE");
}
if (fastModelOverrideInput.kind === "invalid") {
throw new Error("INVALID_FAST_MODEL_OVERRIDE");
}
if (deepModelOverrideInput.kind === "invalid") {
throw new Error("INVALID_DEEP_MODEL_OVERRIDE");
}
if (promptOverrideInput.kind === "invalid") {
throw new Error("INVALID_PROMPT_OVERRIDE");
}
@@ -4312,6 +4420,8 @@ export async function updateProjectAgentControls(
if (
modelOverrideInput.kind !== "preserve" ||
reasoningEffortInput.kind !== "preserve" ||
fastModelOverrideInput.kind !== "preserve" ||
deepModelOverrideInput.kind !== "preserve" ||
promptOverrideInput.kind !== "preserve" ||
backendOverrideInput.kind !== "preserve" ||
globalTakeoverEnabledInput.kind !== "preserve"
@@ -4341,6 +4451,18 @@ export async function updateProjectAgentControls(
: reasoningEffortInput.kind === "clear"
? undefined
: currentControls?.reasoningEffortOverride;
const fastModelOverride =
fastModelOverrideInput.kind === "set"
? fastModelOverrideInput.value
: fastModelOverrideInput.kind === "clear"
? undefined
: currentControls?.fastModelOverride;
const deepModelOverride =
deepModelOverrideInput.kind === "set"
? deepModelOverrideInput.value
: deepModelOverrideInput.kind === "clear"
? undefined
: currentControls?.deepModelOverride;
const promptOverride =
promptOverrideInput.kind === "set"
? promptOverrideInput.value
@@ -4368,6 +4490,8 @@ export async function updateProjectAgentControls(
const currentModelOverride = currentControls?.modelOverride;
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
const currentFastModelOverride = currentControls?.fastModelOverride;
const currentDeepModelOverride = currentControls?.deepModelOverride;
const currentPromptOverride = currentControls?.promptOverride;
const currentBackendOverride = currentControls?.backendOverride;
const currentTakeoverEnabled = currentControls?.takeoverEnabled;
@@ -4375,6 +4499,8 @@ export async function updateProjectAgentControls(
if (
currentModelOverride === modelOverride &&
currentReasoningEffortOverride === reasoningEffortOverride &&
currentFastModelOverride === fastModelOverride &&
currentDeepModelOverride === deepModelOverride &&
currentPromptOverride === promptOverride &&
currentBackendOverride === backendOverride &&
currentTakeoverEnabled === takeoverEnabled &&
@@ -4394,6 +4520,8 @@ export async function updateProjectAgentControls(
const nextControls = {
modelOverride,
reasoningEffortOverride,
fastModelOverride,
deepModelOverride,
promptOverride,
backendOverride,
takeoverEnabled,
@@ -5522,6 +5650,7 @@ export async function saveAiAccount(payload: {
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
enabled?: boolean;
setActive?: boolean;
@@ -5535,12 +5664,9 @@ export async function saveAiAccount(payload: {
existing?.accountId ??
payload.accountId?.trim() ??
`ai-${slugify(`${payload.label}-${payload.displayName}`)}`;
const providerChanged = Boolean(existing && existing.provider !== payload.provider);
const defaultModel =
payload.provider === "aliyun_qwen_api"
? "qwen3.5-plus"
: payload.provider === "openai_api"
? "gpt-5.4"
: undefined;
aiProviderDefaultModel(payload.provider);
const next: AiAccount = normalizeAiAccount({
accountId,
label: payload.label.trim() || aiRoleLabel(payload.role),
@@ -5551,6 +5677,12 @@ export async function saveAiAccount(payload: {
nodeId: payload.nodeId?.trim() || undefined,
nodeLabel: payload.nodeLabel?.trim() || undefined,
model: payload.model?.trim() || defaultModel,
apiBaseUrl:
isApiKeyProvider(payload.provider)
? normalizeApiBaseUrl(payload.apiBaseUrl) ??
(!providerChanged ? existing?.apiBaseUrl : undefined) ??
aiProviderDefaultApiBaseUrl(payload.provider)
: undefined,
apiKey:
isApiKeyProvider(payload.provider)
? payload.apiKey?.trim()
@@ -5593,7 +5725,19 @@ export async function saveAiAccount(payload: {
}
if (payload.setActive ?? (!existing && next.role === "primary")) {
setActiveAiAccountInState(state, next.accountId, existing ? "手动更新 AI 账号配置" : "新增 AI 账号并设为当前主控");
if (!aiAccountCanGenerate(next)) {
next.isActive = false;
} else {
setActiveAiAccountInState(state, next.accountId, existing ? "手动更新 AI 账号配置" : "新增 AI 账号并设为当前主控");
}
} else if (next.isActive && !aiAccountCanGenerate(next)) {
next.isActive = false;
const fallback = sortAiAccounts(state.aiAccounts).find((item) =>
item.accountId !== next.accountId && aiAccountCanGenerate(item),
);
if (fallback) {
setActiveAiAccountInState(state, fallback.accountId, `当前主控 ${next.label} 暂不可用,自动切换`);
}
}
return buildAiAccountSummary(next);
@@ -5643,6 +5787,13 @@ export async function activateAiAccount(accountId: string, reason: string) {
return result;
}
const result = await mutateState((state) => {
const target = state.aiAccounts.find((item) => item.accountId === accountId);
if (!target) {
throw new Error("AI_ACCOUNT_NOT_FOUND");
}
if (!aiAccountCanGenerate(target)) {
throw new Error("AI_ACCOUNT_NOT_READY_FOR_ACTIVATION");
}
setActiveAiAccountInState(state, accountId, reason);
return {
activeIdentity: getMasterIdentitySummaryFromState(state),
@@ -5692,6 +5843,10 @@ export async function updateAiAccountHealth(params: {
export async function getMasterAgentRuntimeAccount() {
const state = await readState();
return resolveMasterAgentRuntimeAccountFromState(state);
}
export function resolveMasterAgentRuntimeAccountFromState(state: BossState) {
const resolved = resolveActiveAiAccount(state);
if (!resolved.account) {
return null;
@@ -5734,6 +5889,7 @@ export async function queueMasterAgentTask(payload: {
deviceImportCandidateFolderName?: string;
projectUnderstandingTargetProjectId?: string;
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
relayViaMasterAgent?: boolean;
}) {
const task = await mutateState((state) => {
const task: MasterAgentTask = {
@@ -5767,6 +5923,7 @@ export async function queueMasterAgentTask(payload: {
deviceImportCandidateFolderName: payload.deviceImportCandidateFolderName,
projectUnderstandingTargetProjectId: payload.projectUnderstandingTargetProjectId,
projectUnderstandingReason: payload.projectUnderstandingReason,
relayViaMasterAgent: payload.relayViaMasterAgent === true ? true : undefined,
status: "queued",
requestedAt: nowIso(),
};
@@ -6583,6 +6740,8 @@ export async function claimNextMasterAgentTask(deviceId: string) {
if (isCliWriteTask(queued)) {
const scope = resolveProjectConflictScopeForTask(snapshot, queued);
const externalActivityAt = scope?.project?.threadMeta.lastObservedCodexActivityAt;
const claimActivityAt = nowIso();
let conflictActivityAt = externalActivityAt;
if (scope) {
const existingPolicy = findProjectExecutionPolicyInState(snapshot, scope);
const fallbackPolicy =
@@ -6590,18 +6749,28 @@ export async function claimNextMasterAgentTask(deviceId: string) {
snapshot.projectExecutionPolicies.find(
(policy) => policy.deviceId === deviceId && policy.projectId === scope.projectId,
);
if (fallbackPolicy?.conflictState === "blocked" && fallbackPolicy.allowPolicy === "forbid") {
const policyActivityAt = fallbackPolicy?.recentExternalActivityAt;
conflictActivityAt = policyActivityAt ?? externalActivityAt;
if (
fallbackPolicy?.conflictState === "blocked" &&
fallbackPolicy.allowPolicy === "forbid" &&
(!policyActivityAt ||
hasRecentThreadConversationExternalActivity({
activityAt: claimActivityAt,
externalActivityAt: policyActivityAt,
}))
) {
return null;
}
}
if (scope && externalActivityAt) {
if (scope && conflictActivityAt) {
const conflict = await detectProjectExecutionConflict({
deviceId,
folderKey: scope.folderKey,
projectId: scope.projectId,
executionMode: "cli",
activityAt: nowIso(),
externalActivityAt,
activityAt: claimActivityAt,
externalActivityAt: conflictActivityAt,
});
if (conflict.blocked) {
return null;
@@ -6847,42 +7016,51 @@ export async function completeMasterAgentTask(payload: {
applyProjectUnderstandingSnapshotInState(state, {
projectId: task.projectUnderstandingTargetProjectId,
account: task.requestedByAccount,
snapshot: understanding,
snapshot: understanding.snapshot,
sourceMessageId: task.requestMessageId,
sourceKind: "thread_sync",
});
const versionRecordAppended = appendProjectVersionFromUnderstandingSyncInState(state, {
projectId: task.projectUnderstandingTargetProjectId,
versionRecord: understanding.versionRecord,
updatedAt: understanding.snapshot.updatedAt,
});
if (
targetProject &&
shouldAnnounceProjectUnderstandingUpdate(previousUnderstanding, understanding)
shouldAnnounceProjectUnderstandingUpdate(previousUnderstanding, understanding.snapshot)
) {
const projectDisplayName =
targetProject.threadMeta.threadDisplayName?.trim() || targetProject.name;
pushProjectLedgerMessage(state, "master-agent", {
sender: "master",
senderLabel: "主 Agent",
body: buildProjectUnderstandingUpdateDigest(projectDisplayName, understanding),
body: buildProjectUnderstandingUpdateDigest(projectDisplayName, understanding.snapshot),
kind: "system_notice",
});
if (
understanding.recommendedNextStep?.trim() &&
previousUnderstanding?.recommendedNextStep !== understanding.recommendedNextStep
understanding.snapshot.recommendedNextStep?.trim() &&
previousUnderstanding?.recommendedNextStep !== understanding.snapshot.recommendedNextStep
) {
pushProjectLedgerMessage(state, "master-agent", {
sender: "master",
senderLabel: "主 Agent",
body: buildProjectUnderstandingNextStepNotice(projectDisplayName, understanding),
body: buildProjectUnderstandingNextStepNotice(projectDisplayName, understanding.snapshot),
kind: "system_notice",
});
pushProjectLedgerMessage(state, "master-agent", {
sender: "master",
senderLabel: "主 Agent",
body: buildProjectUnderstandingCollaborationNotice(projectDisplayName, understanding),
body: buildProjectUnderstandingCollaborationNotice(projectDisplayName, understanding.snapshot),
kind: "system_notice",
});
}
publishBossEvent("project.messages.updated", { projectId: "master-agent" });
publishBossEvent("conversation.updated", { projectId: "master-agent" });
}
if (versionRecordAppended) {
publishBossEvent("conversation.updated", { projectId: task.projectUnderstandingTargetProjectId, note: "project_versions.updated" });
}
publishBossEvent("conversation.updated", { projectId: task.projectUnderstandingTargetProjectId, note: "project_goals.updated" });
publishBossEvent("conversation.updated", { projectId: task.projectUnderstandingTargetProjectId });
}
} else if (isThreadConversationReply) {
@@ -6891,12 +7069,15 @@ export async function completeMasterAgentTask(payload: {
);
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 ||
"线程",
sender: task.relayViaMasterAgent ? "master" : "device",
senderLabel: task.relayViaMasterAgent
? task.accountLabel
? `主 Agent · ${task.accountLabel}`
: "主 Agent"
: task.targetThreadDisplayName?.trim() ||
threadProject?.threadMeta.threadDisplayName ||
device?.name ||
"线程",
body: task.replyBody,
kind: "text",
});
@@ -6922,12 +7103,16 @@ export async function completeMasterAgentTask(payload: {
pushProjectLedgerMessage(state, task.projectId, {
sender: "ops",
senderLabel: isThreadConversationReply
? "线程执行失败"
? task.relayViaMasterAgent
? "主 Agent Relay"
: "线程执行失败"
: task.accountLabel
? `主 Agent Relay · ${task.accountLabel}`
: "主 Agent Relay",
body: isThreadConversationReply
? `${task.targetThreadDisplayName ?? "当前线程"} 执行失败:${buildFriendlyThreadExecutionError(task.errorMessage)}`
? task.relayViaMasterAgent
? `主 Agent 转述失败:${task.targetThreadDisplayName ?? "当前线程"} 暂时无法返回结果,${buildFriendlyThreadExecutionError(task.errorMessage)}`
: `${task.targetThreadDisplayName ?? "当前线程"} 执行失败:${buildFriendlyThreadExecutionError(task.errorMessage)}`
: `Master Codex Node 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
kind: "text",
});
@@ -7408,20 +7593,36 @@ export async function detectProjectExecutionConflict(input: {
const existingPolicy = findProjectExecutionPolicyInState(state, scope);
const hasConflict =
input.executionMode === "cli" &&
Boolean(input.externalActivityAt) &&
input.externalActivityAt! <= input.activityAt;
hasRecentThreadConversationExternalActivity({
activityAt: input.activityAt,
externalActivityAt: input.externalActivityAt,
});
if (!hasConflict) {
const clearedPolicy = existingPolicy
? upsertProjectExecutionPolicyInState(state, {
...existingPolicy,
...scope,
allowPolicy: existingPolicy.allowPolicy ?? "forbid",
conflictState: "none",
activeCliExecution: false,
recentExternalActivityAt: undefined,
updatedAt: nowIso(),
})
: null;
result = {
blocked: false,
policy: normalizeProjectExecutionPolicy({
...existingPolicy,
...scope,
allowPolicy: existingPolicy?.allowPolicy ?? "forbid",
conflictState: existingPolicy?.conflictState ?? "none",
activeCliExecution: false,
updatedAt: existingPolicy?.updatedAt ?? nowIso(),
}),
policy: normalizeProjectExecutionPolicy(
clearedPolicy ?? {
...existingPolicy,
...scope,
allowPolicy: existingPolicy?.allowPolicy ?? "forbid",
conflictState: "none",
activeCliExecution: false,
recentExternalActivityAt: undefined,
updatedAt: existingPolicy?.updatedAt ?? nowIso(),
},
),
};
return;
}
@@ -8034,7 +8235,7 @@ export async function getLatestDeviceImportDraft(deviceId: string) {
function parseStructuredProjectUnderstandingReply(
task: Pick<MasterAgentTask, "replyBody" | "taskId" | "completedAt" | "requestedAt">,
): ProjectUnderstandingSnapshot | null {
): { snapshot: ProjectUnderstandingSnapshot; versionRecord: string } | null {
const replyBody = task.replyBody?.trim();
if (!replyBody) {
return null;
@@ -8042,15 +8243,7 @@ function parseStructuredProjectUnderstandingReply(
const fencedMatch = replyBody.match(/```(?:json)?\s*([\s\S]*?)```/i);
const jsonCandidate = fencedMatch?.[1]?.trim() ?? replyBody;
let parsed:
| {
projectGoal?: string;
currentProgress?: string;
technicalArchitecture?: string;
currentBlockers?: string;
recommendedNextStep?: string;
}
| null = null;
let parsed: ProjectUnderstandingSyncReply | null = null;
try {
parsed = JSON.parse(jsonCandidate);
} catch {
@@ -8062,22 +8255,60 @@ function parseStructuredProjectUnderstandingReply(
const technicalArchitecture = parsed?.technicalArchitecture?.trim() ?? "";
const currentBlockers = parsed?.currentBlockers?.trim() ?? "";
const recommendedNextStep = parsed?.recommendedNextStep?.trim() ?? "";
if (!projectGoal && !currentProgress && !technicalArchitecture && !currentBlockers && !recommendedNextStep) {
const versionRecord = parsed?.versionRecord?.trim() ?? "";
if (
!projectGoal &&
!currentProgress &&
!technicalArchitecture &&
!currentBlockers &&
!recommendedNextStep &&
!versionRecord
) {
return null;
}
return {
projectGoal,
currentProgress,
technicalArchitecture,
currentBlockers,
recommendedNextStep,
sourceTaskId: task.taskId,
updatedAt: task.completedAt ?? task.requestedAt,
sourceKind: "thread_sync",
snapshot: {
projectGoal,
currentProgress,
technicalArchitecture,
currentBlockers,
recommendedNextStep,
sourceTaskId: task.taskId,
updatedAt: task.completedAt ?? task.requestedAt,
sourceKind: "thread_sync",
},
versionRecord,
};
}
function appendProjectVersionFromUnderstandingSyncInState(
state: BossState,
input: {
projectId: string;
versionRecord: string;
updatedAt: string;
},
) {
const versionRecord = input.versionRecord.trim();
if (!versionRecord) {
return false;
}
const project = state.projects.find((item) => item.id === input.projectId);
if (!project) {
return false;
}
if (project.versions.some((entry) => entry.summary === versionRecord)) {
return false;
}
project.versions.unshift({
version: `同步更新 ${input.updatedAt.slice(0, 10)}`,
summary: versionRecord,
createdAt: input.updatedAt,
});
return true;
}
function applyProjectUnderstandingSnapshotInState(
state: BossState,
input: {
@@ -8259,18 +8490,18 @@ function shouldQueueProjectUnderstandingSync(
state: BossState,
reason: "heartbeat_activity" | "thread_reply" = "heartbeat_activity",
) {
// 主 Agent 自动向线程发隐藏理解对话当前整体关闭。
// 保留现有数据模型,后续如果需要恢复,可在明确产品决策后重新开启。
void project;
void observedActivityAt;
void state;
void reason;
return false;
/*
if (!isDispatchableThreadProject(project)) {
return false;
}
const takeoverControls = applyDerivedTakeoverControls(
state,
project.id,
state.user.account,
resolveStoredProjectAgentControls(state, project.id, state.user.account),
);
if (takeoverControls?.effectiveTakeoverEnabled !== true) {
return false;
}
const observedTs = Date.parse(observedActivityAt);
if (!Number.isFinite(observedTs)) {
return false;
@@ -8314,7 +8545,6 @@ function shouldQueueProjectUnderstandingSync(
task.projectUnderstandingTargetProjectId === project.id &&
(task.status === "queued" || task.status === "running"),
);
*/
}
function buildProjectUnderstandingSyncPrompt(project: Project, reason: "heartbeat_activity" | "thread_reply") {
@@ -8325,14 +8555,19 @@ function buildProjectUnderstandingSyncPrompt(project: Project, reason: "heartbea
`文件夹:${project.threadMeta.folderName}`,
`同步原因:${reason === "heartbeat_activity" ? "检测到线程有新活动" : "线程刚刚产生了新的执行结果"}`,
"",
"先基于当前项目本地可见的开发文档和实际代码进行汇总,再回答。",
"优先检查 README、docs、架构文档、版本记录和最近改动的关键代码文件不要只依赖当前对话残留上下文。",
"如果文档与代码不一致,以当前代码和最新开发文档为准。",
"",
"只输出 JSON不要输出解释性文字或 Markdown。",
"JSON 结构固定为:",
'{ "projectGoal": "一句中文目标", "currentProgress": "一句中文进度", "technicalArchitecture": "一句中文架构说明", "currentBlockers": "一句中文阻塞说明", "recommendedNextStep": "一句中文建议动作" }',
'{ "projectGoal": "一句中文目标", "currentProgress": "一句中文进度", "technicalArchitecture": "一句中文架构说明", "currentBlockers": "一句中文阻塞说明", "recommendedNextStep": "一句中文建议动作", "versionRecord": "一句中文版本记录摘要" }',
"",
"要求:",
"1. 只写当前项目最重要、对主 Agent 接手有帮助的事实。",
"2. 不要重复内部字段、线程编号、目录路径、设备 ID。",
"3. 如果某个字段暂时不清楚,填空字符串。",
"4. versionRecord 只写本次同步最值得写入版本记录的一条变化;如果没有,填空字符串。",
].join("\n");
}
@@ -8340,10 +8575,25 @@ async function queueProjectUnderstandingSyncTask(input: {
projectId: string;
observedActivityAt: string;
reason: "heartbeat_activity" | "thread_reply";
}) {
}, options?: { force?: boolean }) {
const state = await readState();
const project = state.projects.find((item) => item.id === input.projectId);
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state, input.reason)) {
if (!project) {
return null;
}
const existingTask = state.masterAgentTasks.find(
(task) =>
task.projectId === "master-agent" &&
task.projectUnderstandingTargetProjectId === project.id &&
(task.status === "queued" || task.status === "running"),
);
if (existingTask) {
return existingTask;
}
if (
options?.force !== true &&
!shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state, input.reason)
) {
return null;
}
const requestedByAccount = state.user.account || project.deviceIds[0] || "17600003315";
@@ -8379,6 +8629,14 @@ async function queueProjectUnderstandingSyncTask(input: {
return task;
}
export async function forceProjectUnderstandingSyncTask(input: {
projectId: string;
observedActivityAt: string;
reason: "heartbeat_activity" | "thread_reply";
}) {
return queueProjectUnderstandingSyncTask(input, { force: true });
}
export async function previewDeviceImportResolution(input: { deviceId: string }) {
const state = await readState();
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);
@@ -9212,107 +9470,161 @@ function buildAutoGroupChatName(memberProjects: Project[]) {
return `${titles[0]}${titles[1]}${titles.length}个线程`;
}
export async function appendProjectMessage(payload: {
type AppendProjectMessagePayload = {
projectId: string;
sender?: MessageSender;
senderLabel?: string;
body?: string;
kind?: MessageKind;
attachments?: MessageAttachment[];
};
function appendProjectMessageInState(
state: BossState,
project: Project,
payload: Omit<AppendProjectMessagePayload, "projectId">,
) {
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;
const shouldTrackThreadProgress =
payload.sender === "device" &&
(payload.kind ?? "text") === "text" &&
isDispatchableThreadProject(project) &&
Boolean(project.threadMeta.codexThreadRef?.trim());
if (shouldTrackThreadProgress) {
project.threadMeta.lastObservedCodexActivityAt = latestIsoTimestamp(
project.threadMeta.lastObservedCodexActivityAt,
message.sentAt,
) ?? message.sentAt;
appendThreadProgressEventInState(state, {
projectId: project.id,
threadId: project.threadMeta.threadId,
threadDisplayName: project.threadMeta.threadDisplayName,
deviceId: project.deviceIds[0] ?? project.id,
eventType: "progress_updated",
summary: summarizeThreadReplyBody(message.body),
phase: project.projectUnderstanding ? "增量同步" : "线程回复",
createdAt: message.sentAt,
sourceTaskId: message.id,
sourceMessageId: message.id,
});
}
return {
message,
shouldQueueUnderstandingSync:
shouldTrackThreadProgress &&
shouldQueueProjectUnderstandingSync(project, message.sentAt, state, "thread_reply"),
};
}
export async function appendProjectMessages(payload: {
projectId: string;
messages: Array<Omit<AppendProjectMessagePayload, "projectId">>;
}) {
const result = 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;
const shouldTrackThreadProgress =
payload.sender === "device" &&
(payload.kind ?? "text") === "text" &&
isDispatchableThreadProject(project) &&
Boolean(project.threadMeta.codexThreadRef?.trim());
if (shouldTrackThreadProgress) {
project.threadMeta.lastObservedCodexActivityAt = latestIsoTimestamp(
project.threadMeta.lastObservedCodexActivityAt,
message.sentAt,
) ?? message.sentAt;
appendThreadProgressEventInState(state, {
projectId: project.id,
threadId: project.threadMeta.threadId,
threadDisplayName: project.threadMeta.threadDisplayName,
deviceId: project.deviceIds[0] ?? project.id,
eventType: "progress_updated",
summary: summarizeThreadReplyBody(message.body),
phase: project.projectUnderstanding ? "增量同步" : "线程回复",
createdAt: message.sentAt,
sourceTaskId: message.id,
sourceMessageId: message.id,
});
}
const appended = payload.messages.map((messagePayload) =>
appendProjectMessageInState(state, project, messagePayload),
);
return {
message,
shouldQueueUnderstandingSync:
shouldTrackThreadProgress &&
shouldQueueProjectUnderstandingSync(project, message.sentAt, state, "thread_reply"),
messages: appended.map((item) => item.message),
shouldQueueUnderstandingSync: appended.some((item) => item.shouldQueueUnderstandingSync),
};
});
if (result.shouldQueueUnderstandingSync) {
await queueProjectUnderstandingSyncTask({
projectId: payload.projectId,
observedActivityAt: result.message.sentAt,
observedActivityAt: result.messages.at(-1)?.sentAt ?? nowIso(),
reason: "thread_reply",
});
}
publishBossEvent("project.messages.updated", { projectId: payload.projectId });
publishBossEvent("conversation.updated", { projectId: payload.projectId });
return result.message;
return result.messages;
}
export async function appendProjectMessage(payload: AppendProjectMessagePayload) {
const [message] = await appendProjectMessages({
projectId: payload.projectId,
messages: [
{
sender: payload.sender,
senderLabel: payload.senderLabel,
body: payload.body,
kind: payload.kind,
attachments: payload.attachments,
},
],
});
if (!message) {
throw new Error("MESSAGE_NOT_CREATED");
}
return message;
}
export async function requestProjectUnderstandingSyncForProject(input: {
projectId: string;
observedActivityAt?: string;
reason?: "heartbeat_activity" | "thread_reply";
}) {
return queueProjectUnderstandingSyncTask(
{
projectId: input.projectId,
observedActivityAt: input.observedActivityAt ?? nowIso(),
reason: input.reason ?? "thread_reply",
},
{ force: true },
);
}
export async function appendAttachmentMessage(payload: {