feat: refine mobile master agent sync and chat rendering
This commit is contained in:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user