feat: refine mobile master agent sync and chat rendering
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
59
src/app/api/v1/accounts/validate-draft/route.ts
Normal file
59
src/app/api/v1/accounts/validate-draft/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 监督各线程提交,并在复核后自动发布。当前页面只读,不允许人工直接篡改正文。
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
108
src/lib/chat-markdown.ts
Normal 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;
|
||||
}
|
||||
13
src/lib/master-agent-model-options.ts
Normal file
13
src/lib/master-agent-model-options.ts
Normal 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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user