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

@@ -7,8 +7,29 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
function isValidProvider(
value: string,
): value is
| "master_codex_node"
| "google_oauth"
| "chatgpt_oauth"
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "master_codex_node" ||
value === "google_oauth" ||
value === "chatgpt_oauth" ||
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
export async function GET(
@@ -49,6 +70,7 @@ export async function PATCH(
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
enabled?: boolean;
setActive?: boolean;
@@ -79,6 +101,7 @@ export async function PATCH(
nodeId: body.nodeId,
nodeLabel: body.nodeLabel,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey,
enabled: body.enabled,
setActive: body.setActive,

View File

@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
displayName?: string;
accountIdentifier?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
};
@@ -39,6 +40,7 @@ export async function POST(request: NextRequest) {
provider: "aliyun_qwen_api",
apiKey: body.apiKey,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
});
const state = await readState();
@@ -51,6 +53,7 @@ export async function POST(request: NextRequest) {
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: false,

View File

@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
displayName?: string;
accountIdentifier?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
};
@@ -38,6 +39,7 @@ export async function POST(request: NextRequest) {
const probe = await probeOpenAiApiAccount({
apiKey: body.apiKey,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
});
const state = await readState();
@@ -50,6 +52,7 @@ export async function POST(request: NextRequest) {
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: true,

View File

@@ -7,8 +7,29 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
function isValidProvider(
value: string,
): value is
| "master_codex_node"
| "google_oauth"
| "chatgpt_oauth"
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "master_codex_node" ||
value === "google_oauth" ||
value === "chatgpt_oauth" ||
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
export async function GET(request: NextRequest) {
@@ -38,6 +59,7 @@ export async function POST(request: NextRequest) {
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
enabled?: boolean;
setActive?: boolean;
@@ -66,6 +88,7 @@ export async function POST(request: NextRequest) {
nodeId: body.nodeId,
nodeLabel: body.nodeLabel,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey,
enabled: body.enabled,
setActive: body.setActive,

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { validateAiAccountDraftConnection } from "@/lib/boss-master-agent";
function isValidProvider(
value: string,
): value is
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json()) as {
provider?: string;
apiKey?: string;
apiBaseUrl?: string;
};
if (!body.provider || !isValidProvider(body.provider)) {
return NextResponse.json({ ok: false, message: "API 接入商不合法。" }, { status: 400 });
}
if (!body.apiKey?.trim()) {
return NextResponse.json({ ok: false, message: "API Key 不能为空。" }, { status: 400 });
}
try {
const result = await validateAiAccountDraftConnection({
provider: body.provider,
apiKey: body.apiKey,
apiBaseUrl: body.apiBaseUrl,
});
return NextResponse.json(result, { status: 200 });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -56,6 +56,8 @@ export async function POST(
const payload = body as {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
fastModelOverride?: unknown;
deepModelOverride?: unknown;
promptOverride?: unknown;
backendOverride?: unknown;
takeoverEnabled?: unknown;
@@ -66,6 +68,8 @@ export async function POST(
payload,
"reasoningEffortOverride",
);
const hasFastModelOverride = Object.prototype.hasOwnProperty.call(payload, "fastModelOverride");
const hasDeepModelOverride = Object.prototype.hasOwnProperty.call(payload, "deepModelOverride");
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
const hasBackendOverride = Object.prototype.hasOwnProperty.call(payload, "backendOverride");
const hasTakeoverEnabled = Object.prototype.hasOwnProperty.call(payload, "takeoverEnabled");
@@ -75,6 +79,8 @@ export async function POST(
? new Set([
"modelOverride",
"reasoningEffortOverride",
"fastModelOverride",
"deepModelOverride",
"promptOverride",
"backendOverride",
"globalTakeoverEnabled",
@@ -85,6 +91,8 @@ export async function POST(
(
!hasModelOverride &&
!hasReasoningEffortOverride &&
!hasFastModelOverride &&
!hasDeepModelOverride &&
!hasPromptOverride &&
!hasBackendOverride &&
!hasTakeoverEnabled &&
@@ -110,6 +118,12 @@ export async function POST(
{ status: 400 },
);
}
if (hasFastModelOverride && payload.fastModelOverride !== undefined && payload.fastModelOverride !== null && typeof payload.fastModelOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_FAST_MODEL_OVERRIDE" }, { status: 400 });
}
if (hasDeepModelOverride && payload.deepModelOverride !== undefined && payload.deepModelOverride !== null && typeof payload.deepModelOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_DEEP_MODEL_OVERRIDE" }, { status: 400 });
}
if (hasPromptOverride && payload.promptOverride !== undefined && payload.promptOverride !== null && typeof payload.promptOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_OVERRIDE" }, { status: 400 });
}
@@ -154,6 +168,8 @@ export async function POST(
{
...(hasModelOverride ? { modelOverride: payload.modelOverride } : {}),
...(hasReasoningEffortOverride ? { reasoningEffortOverride: payload.reasoningEffortOverride } : {}),
...(hasFastModelOverride ? { fastModelOverride: payload.fastModelOverride } : {}),
...(hasDeepModelOverride ? { deepModelOverride: payload.deepModelOverride } : {}),
...(hasPromptOverride ? { promptOverride: payload.promptOverride } : {}),
...(hasBackendOverride ? { backendOverride: payload.backendOverride } : {}),
...(hasTakeoverEnabled ? { takeoverEnabled: payload.takeoverEnabled } : {}),

View File

@@ -1,6 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { appendProjectMessage, buildCollaborationGate, readState } from "@/lib/boss-data";
import {
appendProjectMessage,
appendProjectMessages,
buildCollaborationGate,
getProjectAgentControls,
readState,
requestProjectUnderstandingSyncForProject,
} from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
import { buildProjectMessagesRealtimePayload } from "@/lib/boss-projections";
import {
@@ -10,6 +17,7 @@ import {
replyToMasterAgentUserMessage,
shouldRecommendMasterAgentDispatchPlan,
ThreadConversationExecutionConflictError,
tryBuildLocalMasterAgentFastReply,
} from "@/lib/boss-master-agent";
import { evaluatePermissionPolicy } from "@/lib/execution/permission-policy";
@@ -105,14 +113,19 @@ export async function POST(
);
}
const singleThreadExecutionConflict =
project &&
const isSingleThreadTextMessage =
Boolean(project) &&
projectId !== "master-agent" &&
!project.isGroup &&
!project?.isGroup &&
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0
? await getThreadConversationExecutionConflict(projectId)
: null;
(body.body ?? "").trim().length > 0;
const singleThreadAgentControls = isSingleThreadTextMessage
? await getProjectAgentControls(projectId, session.account)
: null;
const singleThreadTakeoverEnabled = singleThreadAgentControls?.effectiveTakeoverEnabled === true;
const singleThreadExecutionConflict = isSingleThreadTextMessage && !singleThreadTakeoverEnabled
? await getThreadConversationExecutionConflict(projectId)
: null;
if (singleThreadExecutionConflict) {
return NextResponse.json(
@@ -126,6 +139,49 @@ export async function POST(
);
}
if (projectId === "master-agent" && (body.kind ?? "text") === "text" && (body.body ?? "").trim()) {
const localMasterReply = await tryBuildLocalMasterAgentFastReply({
requestText: (body.body ?? "").trim(),
requestedByAccount: session.account,
projectId,
state,
});
if (localMasterReply) {
const [message, replyMessage] = await appendProjectMessages({
projectId,
messages: [
{
senderLabel: session.displayName || "你",
body: body.body,
kind: body.kind ?? "text",
},
{
sender: "master",
senderLabel: localMasterReply.senderLabel,
body: localMasterReply.replyBody,
kind: "text",
},
],
});
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply: localMasterReply.masterReply,
task: null,
replyPresenter: "master",
masterReplyState: "completed",
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(project),
});
}
}
const message = await appendProjectMessage({
projectId,
senderLabel: session.displayName || "你",
@@ -155,8 +211,10 @@ export async function POST(
taskType: "conversation_reply";
status: "queued" | "running" | "completed";
};
replyMessage?: Awaited<ReturnType<typeof appendProjectMessage>>;
}
| undefined;
let replyMessage: Awaited<ReturnType<typeof appendProjectMessage>> | undefined;
let task:
| {
taskId: string;
@@ -169,6 +227,7 @@ export async function POST(
| "running"
| "completed"
| null = null;
let replyPresenter: "thread" | "master" | undefined;
if (shouldCreateDispatchPlan) {
try {
@@ -204,18 +263,49 @@ export async function POST(
});
}
} else if (project && projectId !== "master-agent" && !project.isGroup && message.body.trim().length > 0) {
const queuedTask = await queueThreadConversationReplyTask({
projectId,
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
});
task = {
taskId: queuedTask.taskId,
taskType: "conversation_reply",
status: "queued",
};
const relayViaMasterAgent = singleThreadTakeoverEnabled;
if (relayViaMasterAgent) {
if (shouldRequestVerifiedProjectSummarySync(message.body)) {
await requestProjectUnderstandingSyncForProject({
projectId,
observedActivityAt: message.sentAt,
reason: "thread_reply",
});
}
masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
projectId,
interactionMode: "takeover_single_thread",
mode: "enqueue",
})
if (masterReply?.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
};
masterReplyState = masterReply.masterReplyState ?? null;
}
replyMessage = masterReply?.replyMessage;
} else {
const queuedTask = await queueThreadConversationReplyTask({
projectId,
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
});
task = {
taskId: queuedTask.taskId,
taskType: "conversation_reply",
status: "queued",
};
}
replyPresenter = relayViaMasterAgent ? "master" : "thread";
} else {
dispatchRecommendation = {
ok: false,
@@ -230,11 +320,19 @@ export async function POST(
requestedBy: session.displayName,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
mode: "enqueue",
mode: "smart",
});
if (masterReply?.ok && masterReply.taskId) {
task = masterReply.task ?? null;
masterReplyState = masterReply.masterReplyState ?? null;
if (masterReply?.ok) {
if (masterReply.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
};
}
masterReplyState = masterReply.masterReplyState ?? (masterReply.taskId ? null : "completed");
replyPresenter = "master";
replyMessage = masterReply.replyMessage;
} else {
masterReplyState = null;
}
@@ -247,8 +345,10 @@ export async function POST(
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply,
task,
replyPresenter,
masterReplyState,
dispatchPlan,
dispatchRecommendation,
@@ -277,3 +377,14 @@ export async function POST(
);
}
}
function shouldRequestVerifiedProjectSummarySync(text: string) {
const normalized = text.trim();
if (!normalized) {
return false;
}
const mentionsGoal = /项目目标|目标/.test(normalized);
const mentionsVersion = /版本记录|版本迭代|版本/.test(normalized);
const mentionsReviewOrSync = /核对|确认|同步|更新|刷新|整理|汇总/.test(normalized);
return mentionsReviewOrSync && (mentionsGoal || mentionsVersion);
}

View File

@@ -23,6 +23,15 @@ export default async function GoalsPage({
if (!project) notFound();
const completedCount = project.goals.filter((item) => item.state === "completed").length;
const understandingUpdatedAt = project.projectUnderstanding?.updatedAt
? new Date(project.projectUnderstanding.updatedAt).toLocaleString("zh-CN", {
hour12: false,
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: null;
return (
<AppShell bottomNav={false}>
@@ -42,6 +51,34 @@ export default async function GoalsPage({
09:18 · · 线
</div>
</div>
{project.projectUnderstanding ? (
<div className="rounded-2xl border border-[#D6EEDC] bg-[#F5FBF7] px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[15px] font-semibold text-[#215B39]"></div>
<div className="text-[12px] text-[#6B8A77]">{understandingUpdatedAt ?? "刚刚更新"}</div>
</div>
<div className="mt-3 space-y-3">
<div className="rounded-2xl bg-white/80 px-3 py-3">
<div className="text-[12px] font-semibold text-[#6B8A77]"></div>
<div className="mt-1 text-[14px] leading-6 text-[#215B39]">
{project.projectUnderstanding?.projectGoal}
</div>
</div>
<div className="rounded-2xl bg-white/80 px-3 py-3">
<div className="text-[12px] font-semibold text-[#6B8A77]"></div>
<div className="mt-1 text-[14px] leading-6 text-[#215B39]">
{project.projectUnderstanding?.currentProgress}
</div>
</div>
<div className="rounded-2xl bg-white/80 px-3 py-3">
<div className="text-[12px] font-semibold text-[#6B8A77]"></div>
<div className="mt-1 text-[14px] leading-6 text-[#215B39]">
{project.projectUnderstanding?.recommendedNextStep}
</div>
</div>
</div>
</div>
) : null}
<GoalChecklist projectId={projectId} goals={project.goals} />
<div className="rounded-2xl bg-[#EAF7F0] px-4 py-4">
<div className="text-[14px] font-semibold text-[#215B39]"></div>

View File

@@ -26,10 +26,10 @@ export default async function VersionsPage({
<RealtimeRefresh
projectId={projectId}
events={["conversation.updated", "project.messages.updated", "ota.updated"]}
conversationUpdatedNotes={["project_goals.updated"]}
conversationUpdatedNotes={["project_versions.updated"]}
/>
<StatusBar />
<PageNav title="版本迭代记录" backHref={`/conversations/${projectId}`} />
<PageNav title="版本记录" backHref={`/conversations/${projectId}`} />
<div className="flex flex-col gap-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
Agent 线

View File

@@ -25,6 +25,7 @@ import type {
ThreadConversationExecutionConflict,
ThreadConversationExecutionConflictAction,
} from "@/lib/thread-execution-conflict";
import { parseChatMarkdown, type ChatMarkdownBlock } from "@/lib/chat-markdown";
import {
describeThreadConversationExecutionConflict,
labelForProjectConflictAllowPolicy,
@@ -907,13 +908,101 @@ export function ChatBubble({ message }: { message: Message }) {
{tag ? (
<div className="mb-2 text-[11px] font-semibold opacity-80">{tag}</div>
) : null}
{message.body}
<ChatBubbleMarkdown body={message.body} mine={mine} green={green} />
</div>
</div>
</div>
);
}
function ChatBubbleMarkdown({
body,
mine,
green,
}: {
body: string;
mine: boolean;
green: boolean;
}) {
const blocks = parseChatMarkdown(body);
return (
<div className="space-y-2 break-words">
{blocks.map((block, index) => (
<ChatMarkdownBlockView key={`${block.kind}-${index}`} block={block} mine={mine} green={green} />
))}
</div>
);
}
function ChatMarkdownBlockView({
block,
mine,
green,
}: {
block: ChatMarkdownBlock;
mine: boolean;
green: boolean;
}) {
const mutedClass = mine ? "text-white/82" : green ? "text-[#4E7A60]" : "text-[#57606A]";
const markerClass = mine ? "text-white/72" : green ? "text-[#44A064]" : "text-[#8C8C8C]";
switch (block.kind) {
case "heading":
return (
<div
className={clsx(
"font-semibold leading-6",
block.level === 1 ? "text-[16px]" : block.level === 2 ? "text-[15px]" : "text-[14px]",
)}
>
{block.text}
</div>
);
case "label":
return (
<div className="rounded-2xl bg-black/[0.035] px-3 py-2">
<div className={clsx("text-[12px] font-semibold", markerClass)}>{block.label}</div>
<div className="mt-1 whitespace-pre-wrap text-[14px] leading-6">{block.text}</div>
</div>
);
case "bullet":
return (
<div className="flex gap-2 leading-6">
<span className={markerClass}></span>
<span className="min-w-0 flex-1">{block.text}</span>
</div>
);
case "ordered":
return (
<div className="flex gap-2 leading-6">
<span className={clsx("tabular-nums", markerClass)}>{block.order}</span>
<span className="min-w-0 flex-1">{block.text}</span>
</div>
);
case "quote":
return (
<div className={clsx("border-l-2 pl-3 text-[14px] leading-6", mine ? "border-white/50" : "border-[#D8DEE4]", mutedClass)}>
{block.text}
</div>
);
case "code":
return (
<pre
className={clsx(
"overflow-x-auto rounded-2xl px-3 py-2 text-[12px] leading-5",
mine ? "bg-white/16 text-white" : "bg-[#F2F3F5] text-[#24292F]",
)}
>
<code>{block.text}</code>
</pre>
);
case "paragraph":
default:
return <div className="whitespace-pre-wrap leading-6">{block.text}</div>;
}
}
export function ProjectHeaderActions({ projectId }: { projectId: string }) {
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">

View File

@@ -12,6 +12,7 @@ import type {
UserMasterPrompt,
} from "@/lib/boss-data";
import type { MasterAgentChatPageAnchors } from "@/lib/master-agent-chat-menu";
import { getMasterAgentModelOptions } from "@/lib/master-agent-model-options";
import { formatTimestampLabel } from "@/lib/boss-projections";
type MemoryDraft = {
@@ -191,6 +192,7 @@ export function MasterAgentPromptMemoryClient({
});
const allMemories = useMemo(() => [...projectMemories, ...globalMemories], [projectMemories, globalMemories]);
const modelOptions = useMemo(() => getMasterAgentModelOptions(modelOverride), [modelOverride]);
const promptPreview = useMemo(() => {
const sections = [
globalPrompt.trim() ? `【管理员全局主提示词】\n${globalPrompt.trim()}` : null,
@@ -431,9 +433,11 @@ export function MasterAgentPromptMemoryClient({
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
<option value=""></option>
<option value="gpt-5.4">gpt-5.4</option>
<option value="gpt-4.1">gpt-4.1</option>
<option value="gpt-4.1-mini">gpt-4.1-mini</option>
{modelOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label id={anchors.reasoningEffort.split("#")[1]} className="space-y-1 scroll-mt-4">

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: {

File diff suppressed because it is too large Load Diff

108
src/lib/chat-markdown.ts Normal file
View File

@@ -0,0 +1,108 @@
export type ChatMarkdownBlock =
| { kind: "heading"; text: string; level: 1 | 2 | 3 }
| { kind: "bullet"; text: string }
| { kind: "ordered"; text: string; order: string }
| { kind: "quote"; text: string }
| { kind: "code"; text: string }
| { kind: "label"; label: string; text: string }
| { kind: "paragraph"; text: string };
const headingPattern = /^(#{1,3})\s+(.+)$/;
const bulletPattern = /^[-*]\s+(.+)$/;
const orderedPattern = /^(\d+)\.\s+(.+)$/;
const labelPattern = /^([^:\n]{1,24})[:]\s*(.+)$/;
export function parseChatMarkdown(markdown: string): ChatMarkdownBlock[] {
const normalized = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const blocks: ChatMarkdownBlock[] = [];
const paragraphLines: string[] = [];
const codeLines: string[] = [];
let inCodeFence = false;
const flushParagraph = () => {
const text = paragraphLines.join("\n").trim();
paragraphLines.length = 0;
if (text) {
blocks.push({ kind: "paragraph", text });
}
};
const flushCode = () => {
const text = codeLines.join("\n").trimEnd();
codeLines.length = 0;
if (text) {
blocks.push({ kind: "code", text });
}
};
for (const line of normalized.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("```")) {
if (inCodeFence) {
flushCode();
} else {
flushParagraph();
}
inCodeFence = !inCodeFence;
continue;
}
if (inCodeFence) {
codeLines.push(line);
continue;
}
if (!trimmed) {
flushParagraph();
continue;
}
const heading = headingPattern.exec(line);
if (heading) {
flushParagraph();
blocks.push({
kind: "heading",
level: Math.min(heading[1]!.length, 3) as 1 | 2 | 3,
text: heading[2]!.trim(),
});
continue;
}
const bullet = bulletPattern.exec(trimmed);
if (bullet) {
flushParagraph();
blocks.push({ kind: "bullet", text: bullet[1]!.trim() });
continue;
}
const ordered = orderedPattern.exec(trimmed);
if (ordered) {
flushParagraph();
blocks.push({ kind: "ordered", order: `${ordered[1]}.`, text: ordered[2]!.trim() });
continue;
}
if (trimmed.startsWith(">")) {
flushParagraph();
blocks.push({ kind: "quote", text: trimmed.slice(1).trim() || "引用" });
continue;
}
const label = labelPattern.exec(trimmed);
if (label) {
flushParagraph();
blocks.push({ kind: "label", label: label[1]!.trim(), text: label[2]!.trim() });
continue;
}
paragraphLines.push(line);
}
if (inCodeFence) {
flushCode();
}
flushParagraph();
return blocks;
}

View File

@@ -0,0 +1,13 @@
const DEFAULT_MASTER_AGENT_MODEL_OPTIONS = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"] as const;
export function getMasterAgentModelOptions(currentModelOverride?: string | null) {
const current = currentModelOverride?.trim();
if (!current) {
return [...DEFAULT_MASTER_AGENT_MODEL_OPTIONS];
}
if (DEFAULT_MASTER_AGENT_MODEL_OPTIONS.includes(current as (typeof DEFAULT_MASTER_AGENT_MODEL_OPTIONS)[number])) {
return [...DEFAULT_MASTER_AGENT_MODEL_OPTIONS];
}
return [current, ...DEFAULT_MASTER_AGENT_MODEL_OPTIONS];
}

View File

@@ -5,6 +5,38 @@ export type ThreadConversationExecutionConflictReason =
| "preferred_gui_mode"
| "project_conflict_forbid";
export const THREAD_CONVERSATION_EXTERNAL_ACTIVITY_WINDOW_MS = 5 * 60 * 1000;
function parseTimestampMs(value?: string | null) {
if (!value?.trim()) {
return null;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
export function hasRecentThreadConversationExternalActivity(input: {
activityAt: string;
externalActivityAt?: string | null;
windowMs?: number;
}) {
const activityTs = parseTimestampMs(input.activityAt);
const externalTs = parseTimestampMs(input.externalActivityAt);
if (activityTs === null || externalTs === null) {
return false;
}
if (externalTs > activityTs) {
return false;
}
const windowMs =
Number.isFinite(input.windowMs) && Number(input.windowMs) >= 0
? Number(input.windowMs)
: THREAD_CONVERSATION_EXTERNAL_ACTIVITY_WINDOW_MS;
return activityTs - externalTs <= windowMs;
}
export interface ThreadConversationExecutionConflict {
projectId: string;
projectName: string;