feat: add master-agent prompts and memory management

This commit is contained in:
kris
2026-04-01 04:10:11 +08:00
parent 9000a9f185
commit d316f0490e
31 changed files with 4398 additions and 32 deletions

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
archiveUserMasterMemory,
updateUserMasterMemory,
type MasterMemoryScope,
type MasterMemoryType,
} from "@/lib/boss-data";
export const runtime = "nodejs";
const memoryScopes = new Set<MasterMemoryScope>(["global", "project"]);
const memoryTypes = new Set<MasterMemoryType>([
"user_preference",
"project_progress",
"decision",
"risk",
"blocking_issue",
"research_note",
"workflow_rule",
]);
export async function PATCH(
request: NextRequest,
context: { params: Promise<{ memoryId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { memoryId } = await context.params;
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
const payload = body as {
scope?: unknown;
projectId?: unknown;
title?: unknown;
content?: unknown;
memoryType?: unknown;
tags?: unknown;
sourceMessageId?: unknown;
lastUsedAt?: unknown;
};
const allowedKeys = new Set([
"scope",
"projectId",
"title",
"content",
"memoryType",
"tags",
"sourceMessageId",
"lastUsedAt",
]);
if (Object.keys(payload).some((key) => !allowedKeys.has(key))) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
if (payload.scope !== undefined && !memoryScopes.has(payload.scope as MasterMemoryScope)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_SCOPE" }, { status: 400 });
}
if (payload.memoryType !== undefined && !memoryTypes.has(payload.memoryType as MasterMemoryType)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_TYPE" }, { status: 400 });
}
if (payload.tags !== undefined && !Array.isArray(payload.tags)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
try {
const memory = await updateUserMasterMemory(memoryId, session.account, {
...(payload.scope !== undefined ? { scope: payload.scope as MasterMemoryScope } : {}),
...(payload.projectId !== undefined ? { projectId: typeof payload.projectId === "string" ? payload.projectId : "" } : {}),
...(payload.title !== undefined ? { title: typeof payload.title === "string" ? payload.title : "" } : {}),
...(payload.content !== undefined ? { content: typeof payload.content === "string" ? payload.content : "" } : {}),
...(payload.memoryType !== undefined ? { memoryType: payload.memoryType as MasterMemoryType } : {}),
...(payload.tags !== undefined ? { tags: payload.tags as string[] } : {}),
...(payload.sourceMessageId !== undefined
? { sourceMessageId: typeof payload.sourceMessageId === "string" ? payload.sourceMessageId : "" }
: {}),
...(payload.lastUsedAt !== undefined
? { lastUsedAt: typeof payload.lastUsedAt === "string" ? payload.lastUsedAt : "" }
: {}),
});
return NextResponse.json({ ok: true, memory });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return NextResponse.json(
{
ok: false,
message,
},
{ status: message === "USER_MASTER_MEMORY_NOT_FOUND" ? 404 : 400 },
);
}
}
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ memoryId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { memoryId } = await context.params;
try {
const memory = await archiveUserMasterMemory(memoryId, session.account);
return NextResponse.json({ ok: true, memory });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return NextResponse.json(
{
ok: false,
message,
},
{ status: message === "USER_MASTER_MEMORY_NOT_FOUND" ? 404 : 400 },
);
}
}

View File

@@ -0,0 +1,133 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
createUserMasterMemory,
listUserMasterMemories,
type MasterMemoryScope,
type MasterMemoryType,
} from "@/lib/boss-data";
export const runtime = "nodejs";
const memoryScopes = new Set<MasterMemoryScope>(["global", "project"]);
const memoryTypes = new Set<MasterMemoryType>([
"user_preference",
"project_progress",
"decision",
"risk",
"blocking_issue",
"research_note",
"workflow_rule",
]);
function parseBoolean(value: string | null) {
return value === "1" || value === "true";
}
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const includeArchived = parseBoolean(searchParams.get("includeArchived"));
const scope = searchParams.get("scope") as MasterMemoryScope | null;
const projectId = searchParams.get("projectId")?.trim() || undefined;
if (scope && !memoryScopes.has(scope)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_SCOPE" }, { status: 400 });
}
const memories = await listUserMasterMemories(session.account, {
includeArchived,
...(scope ? { scope } : {}),
...(projectId ? { projectId } : {}),
});
return NextResponse.json({ ok: true, memories });
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
const payload = body as {
scope?: unknown;
projectId?: unknown;
title?: unknown;
content?: unknown;
memoryType?: unknown;
tags?: unknown;
sourceMessageId?: unknown;
};
const allowedKeys = new Set([
"scope",
"projectId",
"title",
"content",
"memoryType",
"tags",
"sourceMessageId",
]);
if (Object.keys(payload).some((key) => !allowedKeys.has(key))) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
if (!memoryScopes.has(payload.scope as MasterMemoryScope)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_SCOPE" }, { status: 400 });
}
if (typeof payload.title !== "string" || typeof payload.content !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
if (!memoryTypes.has(payload.memoryType as MasterMemoryType)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_TYPE" }, { status: 400 });
}
if (payload.scope === "project" && (typeof payload.projectId !== "string" || !payload.projectId.trim())) {
return NextResponse.json({ ok: false, message: "USER_MASTER_MEMORY_PROJECT_ID_REQUIRED" }, { status: 400 });
}
if (payload.tags !== undefined && !Array.isArray(payload.tags)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
if (
payload.sourceMessageId !== undefined &&
payload.sourceMessageId !== null &&
typeof payload.sourceMessageId !== "string"
) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_PAYLOAD" }, { status: 400 });
}
try {
const memory = await createUserMasterMemory({
account: session.account,
scope: payload.scope as MasterMemoryScope,
projectId: typeof payload.projectId === "string" ? payload.projectId : undefined,
title: payload.title,
content: payload.content,
memoryType: payload.memoryType as MasterMemoryType,
tags: (payload.tags as string[] | undefined) ?? [],
sourceMessageId:
typeof payload.sourceMessageId === "string" ? payload.sourceMessageId : undefined,
});
return NextResponse.json({ ok: true, memory });
} catch (error) {
return NextResponse.json(
{
ok: false,
message: error instanceof Error ? error.message : "UNKNOWN_ERROR",
},
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
getMasterAgentPromptPolicy,
updateMasterAgentPromptPolicy,
} from "@/lib/boss-data";
export const runtime = "nodejs";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const policy = await getMasterAgentPromptPolicy();
return NextResponse.json({ ok: true, policy });
}
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 rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_POLICY_PAYLOAD" }, { status: 400 });
}
const payload = body as {
globalPrompt?: unknown;
};
const allowedKeys = new Set(["globalPrompt"]);
if (Object.keys(payload).some((key) => !allowedKeys.has(key))) {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_POLICY_PAYLOAD" }, { status: 400 });
}
if (typeof payload.globalPrompt !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_POLICY_PAYLOAD" }, { status: 400 });
}
try {
const policy = await updateMasterAgentPromptPolicy({
globalPrompt: payload.globalPrompt,
updatedBy: session.account,
});
return NextResponse.json({ ok: true, policy });
} catch (error) {
return NextResponse.json(
{
ok: false,
message: error instanceof Error ? error.message : "UNKNOWN_ERROR",
},
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
clearUserMasterPrompt,
getUserMasterPrompt,
updateUserMasterPrompt,
} from "@/lib/boss-data";
export const runtime = "nodejs";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const prompt = await getUserMasterPrompt(session.account);
return NextResponse.json({ ok: true, prompt });
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_USER_PROMPT_PAYLOAD" }, { status: 400 });
}
const payload = body as {
content?: unknown;
};
const allowedKeys = new Set(["content"]);
if (Object.keys(payload).some((key) => !allowedKeys.has(key))) {
return NextResponse.json({ ok: false, message: "INVALID_USER_PROMPT_PAYLOAD" }, { status: 400 });
}
if (payload.content !== undefined && payload.content !== null && typeof payload.content !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_USER_PROMPT_PAYLOAD" }, { status: 400 });
}
try {
const content = typeof payload.content === "string" ? payload.content : "";
if (!content.trim()) {
const result = await clearUserMasterPrompt(session.account);
return NextResponse.json({ ok: true, prompt: null, ...result });
}
const prompt = await updateUserMasterPrompt(session.account, content);
return NextResponse.json({ ok: true, prompt });
} catch (error) {
return NextResponse.json(
{
ok: false,
message: error instanceof Error ? error.message : "UNKNOWN_ERROR",
},
{ status: 400 },
);
}
}
export async function DELETE(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const result = await clearUserMasterPrompt(session.account);
return NextResponse.json({ ok: true, ...result });
}

View File

@@ -62,15 +62,17 @@ export async function POST(
const payload = body as {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
promptOverride?: unknown;
};
const hasModelOverride = Object.prototype.hasOwnProperty.call(payload, "modelOverride");
const hasReasoningEffortOverride = Object.prototype.hasOwnProperty.call(
payload,
"reasoningEffortOverride",
);
const allowedKeys = new Set(["modelOverride", "reasoningEffortOverride"]);
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
const allowedKeys = new Set(["modelOverride", "reasoningEffortOverride", "promptOverride"]);
const hasUnsupportedKeys = Object.keys(payload).some((key) => !allowedKeys.has(key));
if ((!hasModelOverride && !hasReasoningEffortOverride) || hasUnsupportedKeys) {
if ((!hasModelOverride && !hasReasoningEffortOverride && !hasPromptOverride) || hasUnsupportedKeys) {
return NextResponse.json({ ok: false, message: "INVALID_AGENT_CONTROLS_PAYLOAD" }, { status: 400 });
}
@@ -89,6 +91,9 @@ export async function POST(
{ 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 });
}
try {
const controls = await updateProjectAgentControls(
@@ -96,6 +101,7 @@ export async function POST(
{
...(hasModelOverride ? { modelOverride: payload.modelOverride } : {}),
...(hasReasoningEffortOverride ? { reasoningEffortOverride: payload.reasoningEffortOverride } : {}),
...(hasPromptOverride ? { promptOverride: payload.promptOverride } : {}),
},
);
return NextResponse.json({ ok: true, controls: controls ?? null });

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { archiveUserMasterMemory, hasPersistedProject, updateUserMasterMemory } from "@/lib/boss-data";
const memoryTypeValues = new Set([
"user_preference",
"project_progress",
"decision",
"risk",
"blocking_issue",
"research_note",
"workflow_rule",
]);
const memoryScopeValues = new Set(["global", "project"]);
function normalizeTags(input: unknown) {
if (!Array.isArray(input)) return [] as string[];
return input.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
}
export async function PATCH(
request: NextRequest,
context: { params: Promise<{ projectId: string; memoryId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId, memoryId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const body = (await request.json().catch(() => ({}))) as {
scope?: string;
projectId?: string;
title?: string;
content?: string;
memoryType?: string;
tags?: unknown;
sourceMessageId?: string;
lastUsedAt?: string;
};
try {
const patch: Parameters<typeof updateUserMasterMemory>[2] = {};
if (Object.prototype.hasOwnProperty.call(body, "scope")) {
if (!body.scope || !memoryScopeValues.has(body.scope)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_SCOPE" }, { status: 400 });
}
patch.scope = body.scope as "global" | "project";
}
if (Object.prototype.hasOwnProperty.call(body, "projectId")) {
patch.projectId = body.projectId;
}
if (Object.prototype.hasOwnProperty.call(body, "title")) {
patch.title = body.title ?? "";
}
if (Object.prototype.hasOwnProperty.call(body, "content")) {
patch.content = body.content ?? "";
}
if (Object.prototype.hasOwnProperty.call(body, "memoryType")) {
if (!body.memoryType || !memoryTypeValues.has(body.memoryType)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_TYPE" }, { status: 400 });
}
patch.memoryType = body.memoryType as
| "user_preference"
| "project_progress"
| "decision"
| "risk"
| "blocking_issue"
| "research_note"
| "workflow_rule";
}
if (Object.prototype.hasOwnProperty.call(body, "tags")) {
patch.tags = normalizeTags(body.tags);
}
if (Object.prototype.hasOwnProperty.call(body, "sourceMessageId")) {
patch.sourceMessageId = body.sourceMessageId;
}
if (Object.prototype.hasOwnProperty.call(body, "lastUsedAt")) {
patch.lastUsedAt = body.lastUsedAt;
}
const memory = await updateUserMasterMemory(memoryId, session.account, patch);
return NextResponse.json({ ok: true, memory });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: error instanceof Error && error.message === "USER_MASTER_MEMORY_NOT_FOUND" ? 404 : 400 },
);
}
}
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ projectId: string; memoryId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId, memoryId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
try {
const memory = await archiveUserMasterMemory(memoryId, session.account);
return NextResponse.json({ ok: true, memory });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: error instanceof Error && error.message === "USER_MASTER_MEMORY_NOT_FOUND" ? 404 : 400 },
);
}
}

View File

@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
createUserMasterMemory,
hasPersistedProject,
listUserMasterMemories,
} from "@/lib/boss-data";
const memoryTypeValues = new Set([
"user_preference",
"project_progress",
"decision",
"risk",
"blocking_issue",
"research_note",
"workflow_rule",
]);
const memoryScopeValues = new Set(["global", "project"]);
function normalizeTags(input: unknown) {
if (!Array.isArray(input)) return [] as string[];
return input.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
}
export async function GET(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const [globalMemories, projectMemories] = await Promise.all([
listUserMasterMemories(session.account, { scope: "global" }),
listUserMasterMemories(session.account, { scope: "project", projectId }),
]);
return NextResponse.json({
ok: true,
projectId,
memories: {
global: globalMemories,
project: projectMemories,
},
});
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const body = (await request.json().catch(() => ({}))) as {
scope?: string;
projectId?: string;
title?: string;
content?: string;
memoryType?: string;
tags?: unknown;
sourceMessageId?: string;
};
if (!body.scope || !memoryScopeValues.has(body.scope)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_SCOPE" }, { status: 400 });
}
if (!body.title?.trim()) {
return NextResponse.json({ ok: false, message: "USER_MASTER_MEMORY_TITLE_REQUIRED" }, { status: 400 });
}
if (!body.content?.trim()) {
return NextResponse.json({ ok: false, message: "USER_MASTER_MEMORY_CONTENT_REQUIRED" }, { status: 400 });
}
if (!body.memoryType || !memoryTypeValues.has(body.memoryType)) {
return NextResponse.json({ ok: false, message: "INVALID_MEMORY_TYPE" }, { status: 400 });
}
const targetProjectId = body.scope === "project" ? (body.projectId?.trim() || projectId) : undefined;
if (body.scope === "project" && !targetProjectId) {
return NextResponse.json({ ok: false, message: "USER_MASTER_MEMORY_PROJECT_ID_REQUIRED" }, { status: 400 });
}
try {
const memory = await createUserMasterMemory({
account: session.account,
scope: body.scope as "global" | "project",
projectId: targetProjectId,
title: body.title,
content: body.content,
memoryType: body.memoryType as
| "user_preference"
| "project_progress"
| "decision"
| "risk"
| "blocking_issue"
| "research_note"
| "workflow_rule",
tags: normalizeTags(body.tags),
sourceMessageId: typeof body.sourceMessageId === "string" && body.sourceMessageId.trim() ? body.sourceMessageId.trim() : undefined,
});
return NextResponse.json({ ok: true, memory });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,126 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
clearUserMasterPrompt,
getMasterAgentPromptPolicy,
getProjectAgentControls,
getUserMasterPrompt,
hasPersistedProject,
updateProjectAgentControls,
updateUserMasterPrompt,
} from "@/lib/boss-data";
export async function GET(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const [promptPolicy, userPrompt, projectControls] = await Promise.all([
getMasterAgentPromptPolicy(),
getUserMasterPrompt(session.account),
getProjectAgentControls(projectId),
]);
return NextResponse.json({
ok: true,
projectId,
promptPolicy,
userPrompt,
projectControls,
projectPromptOverride: projectControls?.promptOverride ?? null,
account: session.account,
});
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_PROFILE_PAYLOAD" }, { status: 400 });
}
const payload = body as {
userPromptContent?: unknown;
promptOverride?: unknown;
};
const hasUserPromptContent = Object.prototype.hasOwnProperty.call(payload, "userPromptContent");
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
const allowedKeys = new Set(["userPromptContent", "promptOverride"]);
const hasUnsupportedKeys = Object.keys(payload).some((key) => !allowedKeys.has(key));
if ((!hasUserPromptContent && !hasPromptOverride) || hasUnsupportedKeys) {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_PROFILE_PAYLOAD" }, { status: 400 });
}
if (hasUserPromptContent && payload.userPromptContent !== undefined && payload.userPromptContent !== null && typeof payload.userPromptContent !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_USER_PROMPT_CONTENT" }, { 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 });
}
try {
if (hasUserPromptContent) {
const userPromptContent = typeof payload.userPromptContent === "string" ? payload.userPromptContent.trim() : "";
if (userPromptContent) {
await updateUserMasterPrompt(session.account, userPromptContent);
} else {
await clearUserMasterPrompt(session.account);
}
}
if (hasPromptOverride) {
await updateProjectAgentControls(projectId, {
promptOverride: payload.promptOverride,
});
}
const [promptPolicy, userPrompt, projectControls] = await Promise.all([
getMasterAgentPromptPolicy(),
getUserMasterPrompt(session.account),
getProjectAgentControls(projectId),
]);
return NextResponse.json({
ok: true,
projectId,
promptPolicy,
userPrompt,
projectControls,
projectPromptOverride: projectControls?.promptOverride ?? null,
account: session.account,
});
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: error instanceof Error && error.message === "PROJECT_NOT_FOUND" ? 404 : 400 },
);
}
}

View File

@@ -0,0 +1,53 @@
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { MasterAgentPromptMemoryClient } from "@/components/master-agent-prompt-memory-client";
import { requirePageSession } from "@/lib/boss-auth";
import {
getMasterAgentPromptPolicy,
getProjectAgentControls,
getUserMasterPrompt,
listUserMasterMemories,
} from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function MasterAgentPromptMemoryPage() {
const session = await requirePageSession();
const [promptPolicy, userPrompt, projectControls, globalMemories, projectMemories] =
await Promise.all([
getMasterAgentPromptPolicy(),
getUserMasterPrompt(session.account),
getProjectAgentControls("master-agent"),
listUserMasterMemories(session.account, { includeArchived: false, scope: "global" }),
listUserMasterMemories(session.account, {
includeArchived: false,
scope: "project",
projectId: "master-agent",
}),
]);
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="主 Agent 提示词 / 记忆" backHref="/me" />
<div className="px-[18px] pb-3">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
<span className="font-semibold text-[#111111]">{session.account}</span>
<br />
{session.role === "highest_admin"
? "你是管理员,可以编辑全局主提示词与当前对话附加提示词。"
: "你可以编辑自己的提示词与记忆;管理员全局主提示词只读。"}
</div>
</div>
<MasterAgentPromptMemoryClient
key={`${promptPolicy?.updatedAt ?? "none"}:${userPrompt?.updatedAt ?? "none"}:${projectControls?.updatedAt ?? "none"}:${globalMemories.length}:${projectMemories.length}`}
isAdmin={session.role === "highest_admin"}
promptPolicy={promptPolicy}
userPrompt={userPrompt}
projectControls={projectControls}
globalMemories={globalMemories}
projectMemories={projectMemories}
/>
</AppShell>
);
}

View File

@@ -21,6 +21,11 @@ export default async function MePage() {
<HeaderTitle title="我的" />
<div className="flex flex-col gap-3 px-[18px] pb-5">
<ProfileHero user={state.user} />
<MenuRow
href="/me/master-agent"
title="主 Agent 提示词 / 记忆"
description="配置全局主提示词、当前主提示词和用户记忆"
/>
<MenuRow
href="/me/storage"
title="附件与存储"

View File

@@ -0,0 +1,771 @@
"use client";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
import type {
MasterAgentMemory,
MasterAgentPromptPolicy,
MasterMemoryScope,
MasterMemoryType,
ProjectAgentControls,
UserMasterPrompt,
} from "@/lib/boss-data";
import { formatTimestampLabel } from "@/lib/boss-projections";
type MemoryDraft = {
scope: MasterMemoryScope;
projectId: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string;
sourceMessageId: string;
};
const memoryScopeOptions: Array<{ value: MasterMemoryScope; label: string }> = [
{ value: "global", label: "通用记忆" },
{ value: "project", label: "项目记忆" },
];
const memoryTypeOptions: Array<{ value: MasterMemoryType; label: string }> = [
{ value: "user_preference", label: "用户偏好" },
{ value: "project_progress", label: "项目进度" },
{ value: "decision", label: "决策" },
{ value: "risk", label: "风险" },
{ value: "blocking_issue", label: "阻塞" },
{ value: "research_note", label: "调研" },
{ value: "workflow_rule", label: "工作规则" },
];
function memoryTypeLabel(value: MasterMemoryType) {
return memoryTypeOptions.find((item) => item.value === value)?.label ?? value;
}
function memoryScopeLabel(value: MasterMemoryScope) {
return memoryScopeOptions.find((item) => item.value === value)?.label ?? value;
}
function tagsToText(tags: string[]) {
return tags.join(", ");
}
function textToTags(value: string) {
return value
.split(/[,,、\n]/)
.map((item) => item.trim())
.filter(Boolean);
}
function draftFromMemory(memory: MasterAgentMemory): MemoryDraft {
return {
scope: memory.scope,
projectId: memory.projectId ?? "master-agent",
title: memory.title,
content: memory.content,
memoryType: memory.memoryType,
tags: tagsToText(memory.tags),
sourceMessageId: memory.sourceMessageId ?? "",
};
}
function makeNewMemoryDraft(): MemoryDraft {
return {
scope: "global",
projectId: "master-agent",
title: "",
content: "",
memoryType: "user_preference",
tags: "",
sourceMessageId: "",
};
}
function Field({
label,
value,
onChange,
placeholder,
type = "text",
}: {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
type?: "text" | "password";
}) {
return (
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]">{label}</div>
<input
type={type}
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
/>
</label>
);
}
function TextArea({
label,
value,
onChange,
placeholder,
readOnly = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) {
return (
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]">{label}</div>
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
readOnly={readOnly}
placeholder={placeholder}
rows={6}
className={clsx(
"w-full rounded-xl border px-3 py-2 text-[13px] leading-6 text-[#111111] outline-none",
readOnly ? "border-[#E5E5EA] bg-[#F7F8FA] text-[#57606A]" : "border-[#E5E5EA] bg-white",
)}
/>
</label>
);
}
export function MasterAgentPromptMemoryClient({
isAdmin,
promptPolicy,
userPrompt,
projectControls,
globalMemories,
projectMemories,
}: {
isAdmin: boolean;
promptPolicy: MasterAgentPromptPolicy | null;
userPrompt: UserMasterPrompt | null;
projectControls: ProjectAgentControls | null;
globalMemories: MasterAgentMemory[];
projectMemories: MasterAgentMemory[];
}) {
const router = useRouter();
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
const [globalPrompt, setGlobalPrompt] = useState(promptPolicy?.globalPrompt ?? "");
const [userPromptContent, setUserPromptContent] = useState(userPrompt?.content ?? "");
const [modelOverride, setModelOverride] = useState(projectControls?.modelOverride ?? "");
const [reasoningEffortOverride, setReasoningEffortOverride] = useState(
projectControls?.reasoningEffortOverride ?? "",
);
const [promptOverride, setPromptOverride] = useState(projectControls?.promptOverride ?? "");
const [newMemory, setNewMemory] = useState<MemoryDraft>(makeNewMemoryDraft());
const [memoryDrafts, setMemoryDrafts] = useState<Record<string, MemoryDraft>>(() => {
const next: Record<string, MemoryDraft> = {};
[...globalMemories, ...projectMemories].forEach((memory) => {
next[memory.memoryId] = draftFromMemory(memory);
});
return next;
});
const allMemories = useMemo(() => [...projectMemories, ...globalMemories], [projectMemories, globalMemories]);
function updateMemoryDraft(memoryId: string, updater: (draft: MemoryDraft) => MemoryDraft) {
setMemoryDrafts((current) => ({
...current,
[memoryId]: updater(current[memoryId] ?? draftFromMemory(allMemories.find((item) => item.memoryId === memoryId)!)),
}));
}
async function saveGlobalPrompt() {
if (!isAdmin) {
setMessage("只有管理员可以编辑全局主提示词。");
return;
}
setBusyKey("global_prompt");
const response = await fetch("/api/v1/master-agent/prompt-policy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ globalPrompt }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "管理员全局主提示词已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function saveUserPrompt() {
setBusyKey("user_prompt");
const response = await fetch("/api/v1/master-agent/prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: userPromptContent }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "用户主提示词已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function clearUserPrompt() {
setBusyKey("user_prompt_clear");
const response = await fetch("/api/v1/master-agent/prompt", {
method: "DELETE",
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "用户主提示词已清空。" : result.message ?? "清空失败。");
if (result.ok) {
setUserPromptContent("");
router.refresh();
}
}
async function saveConversationPrompt() {
if (!isAdmin) {
setMessage("只有管理员可以修改当前对话附加提示词。");
return;
}
setBusyKey("conversation_prompt");
const response = await fetch("/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
modelOverride: modelOverride.trim() || null,
reasoningEffortOverride: reasoningEffortOverride.trim() || null,
promptOverride: promptOverride.trim() || null,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "当前对话覆盖已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function createMemory() {
if (!newMemory.title.trim() || !newMemory.content.trim()) {
setMessage("记忆标题和内容不能为空。");
return;
}
if (newMemory.scope === "project" && !newMemory.projectId.trim()) {
setMessage("项目记忆必须填写 projectId。");
return;
}
setBusyKey("memory_create");
const response = await fetch("/api/v1/master-agent/memories", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: newMemory.scope,
projectId: newMemory.scope === "project" ? newMemory.projectId.trim() : undefined,
title: newMemory.title.trim(),
content: newMemory.content.trim(),
memoryType: newMemory.memoryType,
tags: textToTags(newMemory.tags),
sourceMessageId: newMemory.sourceMessageId.trim() || undefined,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已新增。" : result.message ?? "新增失败。");
if (result.ok) {
setNewMemory(makeNewMemoryDraft());
router.refresh();
}
}
async function saveMemory(memoryId: string) {
const draft = memoryDrafts[memoryId];
if (!draft?.title.trim() || !draft.content.trim()) {
setMessage("记忆标题和内容不能为空。");
return;
}
if (draft.scope === "project" && !draft.projectId.trim()) {
setMessage("项目记忆必须填写 projectId。");
return;
}
setBusyKey(`memory_save:${memoryId}`);
const response = await fetch(`/api/v1/master-agent/memories/${memoryId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: draft.scope,
projectId: draft.scope === "project" ? draft.projectId.trim() : null,
title: draft.title.trim(),
content: draft.content.trim(),
memoryType: draft.memoryType,
tags: textToTags(draft.tags),
sourceMessageId: draft.sourceMessageId.trim() || null,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function archiveMemory(memoryId: string) {
setBusyKey(`memory_delete:${memoryId}`);
const response = await fetch(`/api/v1/master-agent/memories/${memoryId}`, {
method: "DELETE",
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已归档。" : result.message ?? "删除失败。");
if (result.ok) router.refresh();
}
return (
<div className="flex flex-col gap-4 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"> Agent </div>
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] text-[#8C8C8C]"></div>
</div>
<span className="rounded-full bg-[#EEF5FF] px-3 py-1 text-[11px] font-semibold text-[#2457C5]">
</span>
</div>
<TextArea
label="全局主提示词"
value={globalPrompt}
onChange={setGlobalPrompt}
placeholder="请输入管理员全局主提示词"
readOnly={!isAdmin}
/>
<button
type="button"
onClick={() => void saveGlobalPrompt()}
disabled={!isAdmin || busyKey === "global_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "global_prompt" ? "保存中" : isAdmin ? "保存全局主提示词" : "仅管理员可修改"}
</button>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] leading-6 text-[#8C8C8C]"> Agent </div>
<TextArea
label="用户私有主提示词"
value={userPromptContent}
onChange={setUserPromptContent}
placeholder="例如:回复要简洁、直接、中文优先"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveUserPrompt()}
disabled={busyKey === "user_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "user_prompt" ? "保存中" : "保存用户提示词"}
</button>
<button
type="button"
onClick={() => void clearUserPrompt()}
disabled={busyKey === "user_prompt_clear"}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "user_prompt_clear" ? "清空中" : "清空"}
</button>
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] text-[#8C8C8C]"> master-agent </div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-3 py-1 text-[11px] font-semibold text-[#B54708]">
</span>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={modelOverride}
onChange={(event) => setModelOverride(event.target.value)}
disabled={!isAdmin}
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>
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={reasoningEffortOverride}
onChange={(event) => setReasoningEffortOverride(event.target.value)}
disabled={!isAdmin}
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="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
</select>
</label>
</div>
<TextArea
label="当前对话附加提示词"
value={promptOverride}
onChange={setPromptOverride}
placeholder="例如:这轮先输出结论,再输出执行计划"
readOnly={!isAdmin}
/>
<button
type="button"
onClick={() => void saveConversationPrompt()}
disabled={!isAdmin || busyKey === "conversation_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "conversation_prompt" ? "保存中" : isAdmin ? "保存当前对话设置" : "仅管理员可修改"}
</button>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
</div>
<div className="mt-4 space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={newMemory.memoryType}
onChange={(event) =>
setNewMemory((current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={newMemory.scope}
onChange={(event) =>
setNewMemory((current) => ({
...current,
scope: event.target.value as MasterMemoryScope,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryScopeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
{newMemory.scope === "project" ? (
<Field
label="projectId"
value={newMemory.projectId}
onChange={(value) => setNewMemory((current) => ({ ...current, projectId: value }))}
placeholder="例如 master-agent"
/>
) : null}
<Field
label="标题"
value={newMemory.title}
onChange={(value) => setNewMemory((current) => ({ ...current, title: value }))}
placeholder="例如:项目进度"
/>
<TextArea
label="内容"
value={newMemory.content}
onChange={(value) => setNewMemory((current) => ({ ...current, content: value }))}
placeholder="例如:主 Agent 提示词与记忆链路已经接通。"
/>
<Field
label="标签(逗号分隔)"
value={newMemory.tags}
onChange={(value) => setNewMemory((current) => ({ ...current, tags: value }))}
placeholder="例如主Agent, 记忆"
/>
<Field
label="sourceMessageId可选"
value={newMemory.sourceMessageId}
onChange={(value) => setNewMemory((current) => ({ ...current, sourceMessageId: value }))}
placeholder="可留空"
/>
<button
type="button"
onClick={() => void createMemory()}
disabled={busyKey === "memory_create"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "memory_create" ? "新增中" : "新增记忆"}
</button>
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] text-[#8C8C8C]"> master-agent </div>
{projectMemories.length === 0 ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] text-[#57606A]"></div>
) : null}
<div className="space-y-3">
{projectMemories.map((memory) => {
const draft = memoryDrafts[memory.memoryId] ?? draftFromMemory(memory);
return (
<div key={memory.memoryId} className="rounded-2xl border border-[#E5E5EA] bg-[#FCFCFD] px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[13px] font-semibold text-[#111111]">{memory.title}</div>
<div className="mt-1 text-[11px] text-[#8C8C8C]">
{memoryScopeLabel(memory.scope)} · {memoryTypeLabel(memory.memoryType)}
{memory.projectId ? ` · ${memory.projectId}` : ""}
</div>
</div>
<div className="text-[11px] text-[#8C8C8C]">
{memory.archived ? "已归档" : formatTimestampLabel(memory.updatedAt)}
</div>
</div>
<div className="mt-3 grid gap-3">
<Field
label="标题"
value={draft.title}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, title: value }))
}
placeholder="记忆标题"
/>
<TextArea
label="内容"
value={draft.content}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, content: value }))
}
placeholder="记忆内容"
/>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.scope}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
scope: event.target.value as MasterMemoryScope,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryScopeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.memoryType}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
{draft.scope === "project" ? (
<Field
label="projectId"
value={draft.projectId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, projectId: value }))
}
placeholder="例如 master-agent"
/>
) : null}
<Field
label="标签"
value={draft.tags}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, tags: value }))
}
placeholder="逗号分隔"
/>
<Field
label="sourceMessageId"
value={draft.sourceMessageId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, sourceMessageId: value }))
}
placeholder="可选"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveMemory(memory.memoryId)}
disabled={busyKey === `memory_save:${memory.memoryId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `memory_save:${memory.memoryId}` ? "保存中" : "保存"}
</button>
<button
type="button"
onClick={() => void archiveMemory(memory.memoryId)}
disabled={busyKey === `memory_delete:${memory.memoryId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `memory_delete:${memory.memoryId}` ? "归档中" : "删除"}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] text-[#8C8C8C]"></div>
{globalMemories.length === 0 ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] text-[#57606A]"></div>
) : null}
<div className="space-y-3">
{globalMemories.map((memory) => {
const draft = memoryDrafts[memory.memoryId] ?? draftFromMemory(memory);
return (
<div key={memory.memoryId} className="rounded-2xl border border-[#E5E5EA] bg-[#FCFCFD] px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[13px] font-semibold text-[#111111]">{memory.title}</div>
<div className="mt-1 text-[11px] text-[#8C8C8C]">
{memoryScopeLabel(memory.scope)} · {memoryTypeLabel(memory.memoryType)}
</div>
</div>
<div className="text-[11px] text-[#8C8C8C]">
{memory.archived ? "已归档" : formatTimestampLabel(memory.updatedAt)}
</div>
</div>
<div className="mt-3 grid gap-3">
<Field
label="标题"
value={draft.title}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, title: value }))
}
placeholder="记忆标题"
/>
<TextArea
label="内容"
value={draft.content}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, content: value }))
}
placeholder="记忆内容"
/>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.memoryType}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<Field
label="标签"
value={draft.tags}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, tags: value }))
}
placeholder="逗号分隔"
/>
</div>
<Field
label="sourceMessageId"
value={draft.sourceMessageId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, sourceMessageId: value }))
}
placeholder="可选"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveMemory(memory.memoryId)}
disabled={busyKey === `memory_save:${memory.memoryId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `memory_save:${memory.memoryId}` ? "保存中" : "保存"}
</button>
<button
type="button"
onClick={() => void archiveMemory(memory.memoryId)}
disabled={busyKey === `memory_delete:${memory.memoryId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `memory_delete:${memory.memoryId}` ? "归档中" : "删除"}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
{message ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">{message}</div>
) : null}
</div>
);
}

View File

@@ -218,6 +218,44 @@ export interface UserAttachmentStorageConfig {
validatedAt?: string;
}
export interface MasterAgentPromptPolicy {
globalPrompt: string;
updatedAt: string;
updatedBy?: string;
}
export interface UserMasterPrompt {
account: string;
content: string;
updatedAt: string;
}
export type MasterMemoryScope = "global" | "project";
export type MasterMemoryType =
| "user_preference"
| "project_progress"
| "decision"
| "risk"
| "blocking_issue"
| "research_note"
| "workflow_rule";
export interface MasterAgentMemory {
memoryId: string;
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string[];
sourceMessageId?: string;
createdAt: string;
updatedAt: string;
lastUsedAt?: string;
archived: boolean;
}
export interface GoalItem {
id: string;
text: string;
@@ -328,6 +366,7 @@ function buildCollaborationGate(project: Pick<Project, "isGroup" | "collaboratio
export interface ProjectAgentControls {
modelOverride?: string;
reasoningEffortOverride?: ReasoningEffort;
promptOverride?: string;
updatedAt: string;
}
@@ -809,6 +848,9 @@ export interface BossState {
deviceSkills: DeviceSkill[];
appLogs: AppLogEntry[];
userAttachmentStorageConfigs: UserAttachmentStorageConfig[];
masterAgentPromptPolicy: MasterAgentPromptPolicy | null;
userMasterPrompts: UserMasterPrompt[];
masterAgentMemories: MasterAgentMemory[];
threadContextSnapshots: ThreadContextSnapshot[];
threadHandoffPackages: ThreadHandoffPackage[];
threadContextAlerts: ThreadContextAlert[];
@@ -1223,6 +1265,9 @@ const initialState: BossState = {
updatedAt: nowIso(),
},
],
masterAgentPromptPolicy: null,
userMasterPrompts: [],
masterAgentMemories: [],
masterAgentTasks: [],
dispatchPlans: [],
dispatchExecutions: [],
@@ -2079,14 +2124,16 @@ function normalizeProjectAgentControls(
const reasoningEffortOverride = isReasoningEffort(raw?.reasoningEffortOverride)
? raw.reasoningEffortOverride
: undefined;
const promptOverride = trimToDefined(raw?.promptOverride);
if (!modelOverride && !reasoningEffortOverride) {
if (!modelOverride && !reasoningEffortOverride && !promptOverride) {
return undefined;
}
return {
modelOverride,
reasoningEffortOverride,
promptOverride,
updatedAt: raw?.updatedAt ?? nowIso(),
};
}
@@ -2512,6 +2559,67 @@ function normalizeAttachmentStorageConfig(
};
}
function normalizeMasterAgentPromptPolicy(
raw: Partial<MasterAgentPromptPolicy> | null | undefined,
fallback?: MasterAgentPromptPolicy | null,
): MasterAgentPromptPolicy | null {
if (!raw) {
return fallback ?? null;
}
const globalPrompt = raw.globalPrompt?.trim();
if (!globalPrompt) {
return fallback ?? null;
}
return {
globalPrompt,
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
updatedBy: raw.updatedBy?.trim() || fallback?.updatedBy,
};
}
function normalizeUserMasterPrompt(
raw: Partial<UserMasterPrompt>,
fallback?: UserMasterPrompt,
): UserMasterPrompt {
const account = raw.account ?? fallback?.account ?? "";
return {
account,
content: raw.content?.trim() ?? fallback?.content ?? "",
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
};
}
function normalizeMasterMemoryTags(values: string[] | undefined) {
return dedupeStrings(
(values ?? [])
.map((value) => value.trim())
.filter((value) => Boolean(value)),
);
}
function normalizeUserMasterMemory(
raw: Partial<MasterAgentMemory>,
fallback?: MasterAgentMemory,
): MasterAgentMemory {
const scope = raw.scope ?? fallback?.scope ?? "global";
const projectId = scope === "project" ? raw.projectId ?? fallback?.projectId : undefined;
return {
memoryId: raw.memoryId ?? fallback?.memoryId ?? randomToken("memory"),
account: raw.account ?? fallback?.account ?? "",
scope,
projectId,
title: raw.title?.trim() ?? fallback?.title ?? "",
content: raw.content?.trim() ?? fallback?.content ?? "",
memoryType: raw.memoryType ?? fallback?.memoryType ?? "user_preference",
tags: normalizeMasterMemoryTags(raw.tags ?? fallback?.tags ?? []),
sourceMessageId: raw.sourceMessageId ?? fallback?.sourceMessageId,
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
lastUsedAt: raw.lastUsedAt ?? fallback?.lastUsedAt,
archived: raw.archived ?? fallback?.archived ?? false,
};
}
function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
const base = fallback ?? cloneInitialState().projects[0];
const projectId = raw.id ?? base.id;
@@ -2745,6 +2853,24 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
base.userAttachmentStorageConfigs[index % base.userAttachmentStorageConfigs.length],
),
),
masterAgentPromptPolicy: normalizeMasterAgentPromptPolicy(
raw.masterAgentPromptPolicy,
base.masterAgentPromptPolicy,
),
userMasterPrompts: ensureArray(raw.userMasterPrompts, base.userMasterPrompts).map(
(prompt, index) =>
normalizeUserMasterPrompt(
prompt,
base.userMasterPrompts[index % Math.max(1, base.userMasterPrompts.length)],
),
),
masterAgentMemories: ensureArray(raw.masterAgentMemories, base.masterAgentMemories).map(
(memory, index) =>
normalizeUserMasterMemory(
memory,
base.masterAgentMemories[index % Math.max(1, base.masterAgentMemories.length)],
),
),
threadContextSnapshots: ensureArray(raw.threadContextSnapshots, base.threadContextSnapshots).map(
(snapshot, index) => ({
...base.threadContextSnapshots[index % base.threadContextSnapshots.length],
@@ -3410,6 +3536,7 @@ export async function updateProjectAgentControls(
payload: {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
promptOverride?: unknown;
},
) {
if (projectId !== "master-agent") {
@@ -3422,12 +3549,18 @@ export async function updateProjectAgentControls(
const reasoningEffortInput = Object.prototype.hasOwnProperty.call(payload, "reasoningEffortOverride")
? parseReasoningEffortOverride(payload.reasoningEffortOverride)
: { kind: "preserve" as const };
const promptOverrideInput = Object.prototype.hasOwnProperty.call(payload, "promptOverride")
? parseControlTextOverride(payload.promptOverride)
: { kind: "preserve" as const };
if (modelOverrideInput.kind === "invalid") {
throw new Error("INVALID_MODEL_OVERRIDE");
}
if (reasoningEffortInput.kind === "invalid") {
throw new Error("INVALID_REASONING_EFFORT_OVERRIDE");
}
if (promptOverrideInput.kind === "invalid") {
throw new Error("INVALID_PROMPT_OVERRIDE");
}
return mutateStateIfChanged((state) => {
const project = state.projects.find((item) => item.id === projectId);
@@ -3446,16 +3579,28 @@ export async function updateProjectAgentControls(
: reasoningEffortInput.kind === "clear"
? undefined
: currentControls?.reasoningEffortOverride;
const promptOverride =
promptOverrideInput.kind === "set"
? promptOverrideInput.value
: promptOverrideInput.kind === "clear"
? undefined
: currentControls?.promptOverride;
const currentModelOverride = currentControls?.modelOverride;
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
if (currentModelOverride === modelOverride && currentReasoningEffortOverride === reasoningEffortOverride) {
const currentPromptOverride = currentControls?.promptOverride;
if (
currentModelOverride === modelOverride &&
currentReasoningEffortOverride === reasoningEffortOverride &&
currentPromptOverride === promptOverride
) {
return { result: currentControls, changed: false };
}
const nextControls = {
modelOverride,
reasoningEffortOverride,
promptOverride,
updatedAt: nowIso(),
} satisfies ProjectAgentControls;
@@ -3496,6 +3641,423 @@ export async function upsertAttachmentStorageConfig(config: UserAttachmentStorag
});
}
export async function getMasterAgentPromptPolicy() {
const state = await readState();
return state.masterAgentPromptPolicy ?? null;
}
export async function updateMasterAgentPromptPolicy(input: {
globalPrompt: string;
updatedBy?: string;
}) {
const globalPrompt = input.globalPrompt.trim();
if (!globalPrompt) {
throw new Error("MASTER_AGENT_PROMPT_REQUIRED");
}
return mutateState((state) => {
const policy: MasterAgentPromptPolicy = {
globalPrompt,
updatedBy: input.updatedBy?.trim() || undefined,
updatedAt: nowIso(),
};
state.masterAgentPromptPolicy = policy;
return policy;
});
}
export async function getUserMasterPrompt(account: string) {
const state = await readState();
return state.userMasterPrompts.find((item) => item.account === account) ?? null;
}
export async function updateUserMasterPrompt(account: string, content: string) {
const trimmedContent = content.trim();
if (!trimmedContent) {
throw new Error("USER_MASTER_PROMPT_REQUIRED");
}
return mutateState((state) => {
const next: UserMasterPrompt = {
account,
content: trimmedContent,
updatedAt: nowIso(),
};
const existing = state.userMasterPrompts.find((item) => item.account === account);
if (existing) {
Object.assign(existing, next);
} else {
state.userMasterPrompts.unshift(next);
}
return next;
});
}
export async function clearUserMasterPrompt(account: string) {
return mutateState((state) => {
const before = state.userMasterPrompts.length;
state.userMasterPrompts = state.userMasterPrompts.filter((item) => item.account !== account);
return { cleared: before !== state.userMasterPrompts.length };
});
}
export async function listUserMasterMemories(
account: string,
options?: { includeArchived?: boolean; scope?: MasterMemoryScope; projectId?: string },
) {
const state = await readState();
const includeArchived = options?.includeArchived ?? false;
return [...state.masterAgentMemories]
.filter((memory) => {
if (memory.account !== account) return false;
if (!includeArchived && memory.archived) return false;
if (options?.scope && memory.scope !== options.scope) return false;
if (options?.projectId && memory.projectId !== options.projectId) return false;
return true;
})
.sort((a, b) => {
const timeDiff =
messageTimeValue(b.lastUsedAt ?? b.updatedAt ?? b.createdAt) -
messageTimeValue(a.lastUsedAt ?? a.updatedAt ?? a.createdAt);
if (timeDiff !== 0) return timeDiff;
return b.memoryId.localeCompare(a.memoryId);
});
}
export async function createUserMasterMemory(input: {
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags?: string[];
sourceMessageId?: string;
}) {
const title = input.title.trim();
const content = input.content.trim();
if (!title) {
throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
}
if (!content) {
throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
}
if (input.scope === "project" && !input.projectId?.trim()) {
throw new Error("USER_MASTER_MEMORY_PROJECT_ID_REQUIRED");
}
return mutateState((state) => {
const now = nowIso();
const memory: MasterAgentMemory = {
memoryId: randomToken("memory"),
account: input.account,
scope: input.scope,
projectId: input.scope === "project" ? input.projectId?.trim() : undefined,
title,
content,
memoryType: input.memoryType,
tags: normalizeMasterMemoryTags(input.tags),
sourceMessageId: input.sourceMessageId,
createdAt: now,
updatedAt: now,
lastUsedAt: now,
archived: false,
};
state.masterAgentMemories.unshift(memory);
return memory;
});
}
export async function updateUserMasterMemory(
memoryId: string,
account: string,
patch: Partial<
Pick<
MasterAgentMemory,
"scope" | "projectId" | "title" | "content" | "memoryType" | "tags" | "sourceMessageId" | "lastUsedAt"
>
>,
) {
return mutateState((state) => {
const memory = state.masterAgentMemories.find(
(item) => item.memoryId === memoryId && item.account === account,
);
if (!memory) {
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
}
if (patch.scope) {
memory.scope = patch.scope;
}
if (memory.scope === "project" && patch.projectId !== undefined) {
memory.projectId = patch.projectId.trim() || undefined;
}
if (memory.scope !== "project") {
memory.projectId = undefined;
}
if (patch.title !== undefined) {
const title = patch.title.trim();
if (!title) throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
memory.title = title;
}
if (patch.content !== undefined) {
const content = patch.content.trim();
if (!content) throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
memory.content = content;
}
if (patch.memoryType) {
memory.memoryType = patch.memoryType;
}
if (patch.tags) {
memory.tags = normalizeMasterMemoryTags(patch.tags);
}
if (patch.sourceMessageId !== undefined) {
memory.sourceMessageId = patch.sourceMessageId;
}
if (patch.lastUsedAt !== undefined) {
memory.lastUsedAt = patch.lastUsedAt;
}
memory.updatedAt = nowIso();
return memory;
});
}
export async function archiveUserMasterMemory(memoryId: string, account: string) {
return mutateState((state) => {
const memory = state.masterAgentMemories.find(
(item) => item.memoryId === memoryId && item.account === account,
);
if (!memory) {
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
}
memory.archived = true;
memory.updatedAt = nowIso();
return memory;
});
}
function normalizeAutoMemoryText(value: string | undefined) {
return (value ?? "")
.replace(/\s+/g, " ")
.replace(/[。;;!]+$/g, "")
.trim();
}
function inferAutoMemoryType(text: string): MasterMemoryType | null {
if (!text.trim()) return null;
if (/(微信|wechat|中文回复|中文沟通|UI风格|交互风格|偏好|习惯|默认)/i.test(text)) {
return "user_preference";
}
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
return "workflow_rule";
}
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
return "blocking_issue";
}
if (/(风险|隐患|告警)/i.test(text)) {
return "risk";
}
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
return "decision";
}
if (/(调研|研究|结论)/i.test(text)) {
return "research_note";
}
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
return "project_progress";
}
return null;
}
function inferProjectAutoMemoryType(text: string): Exclude<MasterMemoryType, "user_preference"> | null {
if (!text.trim()) return null;
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
return "blocking_issue";
}
if (/(风险|隐患|告警)/i.test(text)) {
return "risk";
}
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
return "decision";
}
if (/(调研|研究|结论)/i.test(text)) {
return "research_note";
}
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
return "project_progress";
}
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
return "workflow_rule";
}
return null;
}
function buildAutoMemoryTitle(memoryType: MasterMemoryType, label?: string) {
const typeLabel =
memoryType === "user_preference"
? "偏好"
: memoryType === "workflow_rule"
? "工作规则"
: memoryType === "blocking_issue"
? "阻塞"
: memoryType === "risk"
? "风险"
: memoryType === "decision"
? "决策"
: memoryType === "research_note"
? "调研结论"
: "项目进度";
return label ? `${label} · ${typeLabel}` : typeLabel;
}
function detectReferencedProjectForMemory(state: BossState, text: string) {
const lowered = text.toLowerCase();
const candidates = state.projects
.filter((project) => project.id !== "master-agent")
.flatMap((project) => {
const rawAliases = [
project.id,
project.name,
project.threadMeta.folderName,
project.threadMeta.threadDisplayName,
]
.map((value) => value.trim())
.filter(Boolean);
const aliases = Array.from(
new Set(
rawAliases.flatMap((alias) => {
const normalized = alias.trim();
if (!normalized) {
return [];
}
const tokenCandidates = normalized
.split(/[\s\-_/]+/)
.map((token) => token.trim())
.filter((token) => token.length >= 3);
return [normalized, ...tokenCandidates];
}),
),
);
return aliases.map((alias) => ({
projectId: project.id,
projectName: project.name,
alias,
}));
})
.sort((left, right) => right.alias.length - left.alias.length);
return candidates.find((candidate) => lowered.includes(candidate.alias.toLowerCase())) ?? null;
}
function upsertAutoMasterMemoryInState(
state: BossState,
input: {
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string[];
sourceMessageId?: string;
},
) {
const now = nowIso();
const existing = state.masterAgentMemories.find(
(memory) =>
memory.account === input.account &&
memory.scope === input.scope &&
(memory.projectId ?? undefined) === (input.projectId ?? undefined) &&
memory.title === input.title,
);
if (existing) {
existing.content = input.content;
existing.memoryType = input.memoryType;
existing.tags = normalizeMasterMemoryTags(input.tags);
existing.sourceMessageId = input.sourceMessageId ?? existing.sourceMessageId;
existing.archived = false;
existing.updatedAt = now;
existing.lastUsedAt = now;
return existing;
}
const memory: MasterAgentMemory = {
memoryId: randomToken("memory"),
account: input.account,
scope: input.scope,
projectId: input.scope === "project" ? input.projectId : undefined,
title: input.title,
content: input.content,
memoryType: input.memoryType,
tags: normalizeMasterMemoryTags(input.tags),
sourceMessageId: input.sourceMessageId,
createdAt: now,
updatedAt: now,
lastUsedAt: now,
archived: false,
};
state.masterAgentMemories.unshift(memory);
return memory;
}
function autoCaptureMasterAgentMemoriesInState(
state: BossState,
input: {
account: string;
requestText: string;
replyText: string;
sourceMessageId?: string;
},
) {
const requestText = normalizeAutoMemoryText(input.requestText);
const replyText = normalizeAutoMemoryText(input.replyText);
if (!requestText && !replyText) {
return [];
}
const createdOrUpdated: MasterAgentMemory[] = [];
const combined = [requestText, replyText].filter(Boolean).join(" ");
const preferenceType = inferAutoMemoryType(requestText);
if (preferenceType === "user_preference" || preferenceType === "workflow_rule") {
createdOrUpdated.push(
upsertAutoMasterMemoryInState(state, {
account: input.account,
scope: "global",
title: buildAutoMemoryTitle(preferenceType),
content: requestText,
memoryType: preferenceType,
tags: preferenceType === "user_preference" ? ["用户偏好"] : ["工作方式"],
sourceMessageId: input.sourceMessageId,
}),
);
}
const referencedProject = detectReferencedProjectForMemory(state, combined);
const projectType = inferProjectAutoMemoryType(replyText) ?? inferProjectAutoMemoryType(combined);
if (referencedProject && projectType) {
createdOrUpdated.push(
upsertAutoMasterMemoryInState(state, {
account: input.account,
scope: "project",
projectId: referencedProject.projectId,
title: buildAutoMemoryTitle(projectType, referencedProject.projectName),
content: replyText || requestText,
memoryType: projectType,
tags: [referencedProject.projectName, referencedProject.alias],
sourceMessageId: input.sourceMessageId,
}),
);
}
return createdOrUpdated;
}
function preferredDeviceForAccount(
state: BossState,
account: string,
@@ -5078,6 +5640,12 @@ export async function completeMasterAgentTask(payload: {
body: task.replyBody,
kind: "text",
});
autoCaptureMasterAgentMemoriesInState(state, {
account: task.requestedByAccount,
requestText: task.requestText,
replyText: task.replyBody,
sourceMessageId: task.requestMessageId,
});
}
} else if (!attachmentProjectId && payload.status === "failed") {
const isThreadConversationReply =

View File

@@ -22,6 +22,11 @@ import type { DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
import {
getMasterAgentPromptPolicyView,
getUserMasterPromptView,
listUserMasterMemoriesView,
} from "@/lib/boss-projections";
type MasterAgentReplyState = "queued" | "running" | "completed";
const OPENAI_MASTER_AGENT_DEVICE_ID = "master-agent-openai";
@@ -38,28 +43,83 @@ type QueuedMasterAgentReplyEnvelope = {
};
};
export async function resolveMasterAgentExecutionConfig(projectId: string) {
export async function resolveMasterAgentExecutionConfig(
projectId: string,
accountId?: string,
requestText?: string,
) {
const runtime = await getMasterAgentRuntimeAccount();
if (!runtime?.account) {
throw new Error("NO_MASTER_AGENT_RUNTIME_ACCOUNT");
}
const agentControls = await getProjectAgentControls(projectId);
const state = await readState();
const resolvedAccountId = accountId?.trim() || state.user.account || runtime.account.accountId;
const reasoningEffort =
agentControls?.reasoningEffortOverride ||
(runtime.account as typeof runtime.account & { reasoningEffort?: ReasoningEffort }).reasoningEffort ||
"medium";
const promptPolicy = getMasterAgentPromptPolicyView(state);
const userPrompt = getUserMasterPromptView(state, resolvedAccountId);
const memoryScope = listUserMasterMemoriesView(state, resolvedAccountId, { includeArchived: false });
const projectMemories = selectRelevantProjectMemories(memoryScope, projectId, requestText);
const userMemories = memoryScope.filter((memory) => memory.scope === "global");
return {
runtime,
account: runtime.account,
agentControls,
projectPromptOverride: agentControls?.promptOverride ?? null,
provider: runtime.account.provider,
model: agentControls?.modelOverride || runtime.account.model || "gpt-5.4",
reasoningEffort,
promptPolicy,
userPrompt,
projectMemories,
userMemories,
executionPrompt: buildMasterAgentExecutionPrompt({
state,
projectId,
requestText: requestText ?? "",
currentSessionExpiresAt: undefined,
agentControls,
accountId: resolvedAccountId,
promptPolicy,
userPrompt,
projectMemories,
userMemories,
}),
};
}
function selectRelevantProjectMemories(
memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
projectId: string,
requestText?: string,
) {
const projectScoped = memories.filter((memory) => memory.scope === "project");
if (projectId !== "master-agent") {
return projectScoped.filter((memory) => memory.projectId === projectId);
}
if (projectScoped.length === 0) {
return [];
}
const lowered = requestText?.trim().toLowerCase() ?? "";
if (!lowered) {
return projectScoped.slice(0, 6);
}
const matched = projectScoped.filter((memory) => {
const haystacks = [memory.title, memory.content, ...(memory.tags ?? [])]
.map((value) => value.toLowerCase());
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
});
return (matched.length > 0 ? matched : projectScoped).slice(0, 6);
}
function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
if (!agentControls) {
return "当前对话覆盖:无";
@@ -69,9 +129,59 @@ function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
"当前对话覆盖:",
`model=${agentControls.modelOverride ?? "默认"}`,
`reasoning=${agentControls.reasoningEffortOverride ?? "默认"}`,
`prompt=${agentControls.promptOverride ? "已配置" : "默认"}`,
].join(" ");
}
function buildPromptPolicyDigest(promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>) {
return promptPolicy?.globalPrompt?.trim()
? [`管理员全局主提示词:`, promptPolicy.globalPrompt.trim()].join("\n")
: "管理员全局主提示词:无";
}
function buildUserPromptDigest(userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>) {
return userPrompt?.content?.trim()
? [`用户私有主提示词:`, userPrompt.content.trim()].join("\n")
: "用户私有主提示词:无";
}
function buildMemoryDigest(title: string, memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>) {
if (memories.length === 0) {
return `${title}:无`;
}
return [
`${title}`,
...memories.map((memory) => `- ${memory.title}${memory.content}`),
].join("\n");
}
function buildMasterAgentExecutionPrompt(params: {
state: Awaited<ReturnType<typeof readState>>;
projectId: string;
requestText: string;
currentSessionExpiresAt?: string;
agentControls?: ProjectAgentControls | null;
accountId: string;
promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
return [
buildMasterAgentInstructions(),
buildPromptPolicyDigest(params.promptPolicy),
buildUserPromptDigest(params.userPrompt),
params.agentControls?.promptOverride?.trim()
? ["当前对话附加提示词:", params.agentControls.promptOverride.trim()].join("\n")
: "当前对话附加提示词:无",
buildMemoryDigest("项目记忆", params.projectMemories),
buildMemoryDigest("用户记忆", params.userMemories),
buildAgentControlsDigest(params.agentControls),
"",
buildRuntimeDigest(params.state, params.requestText, params.currentSessionExpiresAt, params.agentControls),
].join("\n\n");
}
function buildMasterAgentInstructions() {
return [
"你是 Boss 控制台的主 Agent。",
@@ -262,6 +372,10 @@ async function replyViaOpenAiAccount(params: {
currentSessionExpiresAt?: string;
senderLabel: string;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
if (!params.account?.apiKey?.trim()) {
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
@@ -274,6 +388,10 @@ async function replyViaOpenAiAccount(params: {
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
});
await appendMasterAgentSystemReply(generated.content, params.senderLabel);
@@ -298,8 +416,22 @@ async function generateOpenAiReply(params: {
requestText: string;
currentSessionExpiresAt?: string;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
const state = await readState();
const effectiveProjectMemories =
params.projectMemories && params.projectMemories.length > 0
? params.projectMemories
: selectRelevantProjectMemories(
listUserMasterMemoriesView(state, params.userPrompt?.account ?? state.user.account, {
includeArchived: false,
}),
"master-agent",
params.requestText,
);
let response: Response;
try {
response = await fetch("https://api.openai.com/v1/responses", {
@@ -311,13 +443,19 @@ async function generateOpenAiReply(params: {
body: JSON.stringify({
model: params.model,
reasoning: { effort: params.reasoningEffort },
instructions: buildMasterAgentInstructions(),
input: buildRuntimeDigest(
instructions: buildMasterAgentExecutionPrompt({
state,
params.requestText,
params.currentSessionExpiresAt,
params.agentControls,
),
projectId: "master-agent",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
accountId: "master-agent",
promptPolicy: params.promptPolicy ?? null,
userPrompt: params.userPrompt ?? null,
projectMemories: effectiveProjectMemories,
userMemories: params.userMemories ?? [],
}),
input: params.requestText,
}),
signal: AbortSignal.timeout(45_000),
});
@@ -362,12 +500,23 @@ function buildMasterOpenAiReplyPrompt(
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
) {
return [
buildMasterAgentInstructions(),
"",
buildRuntimeDigest(state, requestText, currentSessionExpiresAt, agentControls),
].join("\n");
return buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText,
currentSessionExpiresAt,
agentControls,
accountId: "master-agent",
promptPolicy: promptPolicy ?? null,
userPrompt: userPrompt ?? null,
projectMemories: projectMemories ?? [],
userMemories: userMemories ?? [],
});
}
async function queueAndStartOpenAiMasterAgentReply(params: {
@@ -379,6 +528,10 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
const timer = setTimeout(() => {
void (async () => {
@@ -395,6 +548,10 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
});
await completeMasterAgentTask({
@@ -429,6 +586,10 @@ async function enqueueOpenAiMasterAgentReply(params: {
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
const state = await readState();
const task = await queueMasterAgentTask({
@@ -439,6 +600,10 @@ async function enqueueOpenAiMasterAgentReply(params: {
params.requestText,
params.currentSessionExpiresAt,
params.agentControls,
params.promptPolicy,
params.userPrompt,
params.projectMemories,
params.userMemories,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
@@ -455,6 +620,10 @@ async function enqueueOpenAiMasterAgentReply(params: {
model: params.model,
reasoningEffort: params.reasoningEffort,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
});
const queuedReply: QueuedMasterAgentReplyEnvelope = {
@@ -542,16 +711,23 @@ function buildMasterCodexNodePrompt(
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
) {
return [
"你是 Boss 控制台的主 Agent运行在用户自己的 Master Codex Node 上。",
"请结合下面的运行时状态和用户消息,直接给出中文回复。",
"如果你认为需要继续在当前仓库里推进实现、排障或验证,可以直接说明你下一步会做什么;如果必须先做交接或收尾,也要明确说出原因。",
"保持简洁,优先给出结论、动作、验证点。",
buildAgentControlsDigest(agentControls),
"",
buildRuntimeDigest(state, requestText, currentSessionExpiresAt, agentControls),
].join("\n");
return buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText,
currentSessionExpiresAt,
agentControls,
accountId: "master-agent",
promptPolicy: promptPolicy ?? null,
userPrompt: userPrompt ?? null,
projectMemories: projectMemories ?? [],
userMemories: userMemories ?? [],
});
}
function summarizeDispatchRequest(requestText: string) {
@@ -1159,7 +1335,11 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: false as const, reason: "NO_AI_ACCOUNT" };
}
const executionConfig = await resolveMasterAgentExecutionConfig("master-agent");
const executionConfig = await resolveMasterAgentExecutionConfig(
"master-agent",
params.requestedByAccount,
params.requestText,
);
const agentControls = executionConfig.agentControls;
if (params.mode === "enqueue") {
@@ -1195,6 +1375,10 @@ export async function replyToMasterAgentUserMessage(params: {
model: agentControls?.modelOverride || fallbackAccount.model || "gpt-5.4",
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
}
@@ -1213,6 +1397,10 @@ export async function replyToMasterAgentUserMessage(params: {
params.requestText,
params.currentSessionExpiresAt,
agentControls,
executionConfig.promptPolicy,
executionConfig.userPrompt,
executionConfig.projectMemories,
executionConfig.userMemories,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
@@ -1248,6 +1436,10 @@ export async function replyToMasterAgentUserMessage(params: {
model: executionConfig.model,
reasoningEffort: executionConfig.reasoningEffort,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
}
}
@@ -1278,6 +1470,10 @@ export async function replyToMasterAgentUserMessage(params: {
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
} catch {
// Fall through to the original offline guidance when the fallback API account cannot respond.
@@ -1298,6 +1494,10 @@ export async function replyToMasterAgentUserMessage(params: {
params.requestText,
params.currentSessionExpiresAt,
agentControls,
executionConfig.promptPolicy,
executionConfig.userPrompt,
executionConfig.projectMemories,
executionConfig.userMemories,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
@@ -1324,6 +1524,10 @@ export async function replyToMasterAgentUserMessage(params: {
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
} catch {
// Preserve the original execution failure below if the fallback account also fails.
@@ -1367,6 +1571,10 @@ export async function replyToMasterAgentUserMessage(params: {
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
await appendMasterAgentSystemReply(

View File

@@ -14,6 +14,8 @@ import type {
DeviceImportResolution,
DeviceSkill,
MasterIdentitySummary,
MasterAgentMemory,
MasterAgentPromptPolicy,
OpsFault,
OpsRepairTicket,
OpsRepairVerification,
@@ -23,6 +25,7 @@ import type {
ThreadContextAlert,
ThreadContextSnapshot,
ThreadHandoffPackage,
UserMasterPrompt,
} from "@/lib/boss-data";
export interface ContextIndicator {
@@ -305,6 +308,30 @@ function getProjectMasterIdentity(state: BossState): MasterIdentitySummary {
};
}
export function getMasterAgentPromptPolicyView(state: BossState): MasterAgentPromptPolicy | null {
return state.masterAgentPromptPolicy ?? null;
}
export function getUserMasterPromptView(state: BossState, account: string): UserMasterPrompt | null {
return state.userMasterPrompts.find((item) => item.account === account) ?? null;
}
export function listUserMasterMemoriesView(
state: BossState,
account: string,
options?: { includeArchived?: boolean },
): MasterAgentMemory[] {
const includeArchived = options?.includeArchived ?? false;
return [...state.masterAgentMemories]
.filter((memory) => memory.account === account && (includeArchived || !memory.archived))
.sort((a, b) => {
const aTime = Date.parse(a.lastUsedAt ?? a.updatedAt ?? a.createdAt) || 0;
const bTime = Date.parse(b.lastUsedAt ?? b.updatedAt ?? b.createdAt) || 0;
if (aTime !== bTime) return bTime - aTime;
return b.memoryId.localeCompare(a.memoryId);
});
}
function threadViewsForProject(state: BossState, projectId: string) {
return state.threadContextSnapshots
.filter((snapshot) => snapshot.projectId === projectId)