feat: add group repair and dispatch rejection flows
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { rejectDispatchPlan } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string; planId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId, planId } = await context.params;
|
||||
|
||||
try {
|
||||
const result = await rejectDispatchPlan({
|
||||
groupProjectId: projectId,
|
||||
planId,
|
||||
rejectedBy: session.account,
|
||||
});
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
plan: result.plan,
|
||||
notice: result.notice,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
import { isDispatchableThreadProject, readState, replaceGroupChatMembers } from "@/lib/boss-data";
|
||||
|
||||
type ConversationParticipant = {
|
||||
projectId: string;
|
||||
@@ -10,6 +10,9 @@ type ConversationParticipant = {
|
||||
folderName: string;
|
||||
avatar: string;
|
||||
isSourceProject: boolean;
|
||||
status: "active" | "missing_project" | "invalid_target";
|
||||
statusLabel?: string;
|
||||
canOpenProject: boolean;
|
||||
};
|
||||
|
||||
function getFallbackAvatar(label: string) {
|
||||
@@ -26,6 +29,8 @@ function buildParticipant(
|
||||
folderName: string,
|
||||
avatar?: string,
|
||||
isSourceProject = false,
|
||||
status: ConversationParticipant["status"] = "active",
|
||||
canOpenProject = true,
|
||||
): ConversationParticipant {
|
||||
return {
|
||||
projectId,
|
||||
@@ -35,6 +40,81 @@ function buildParticipant(
|
||||
folderName,
|
||||
avatar: avatar?.trim() || getFallbackAvatar(threadDisplayName),
|
||||
isSourceProject,
|
||||
status,
|
||||
statusLabel:
|
||||
status === "missing_project"
|
||||
? "引用已失效"
|
||||
: status === "invalid_target"
|
||||
? "不是可下发线程"
|
||||
: undefined,
|
||||
canOpenProject,
|
||||
};
|
||||
}
|
||||
|
||||
function buildParticipantsPayload(
|
||||
state: Awaited<ReturnType<typeof readState>>,
|
||||
projectId: string,
|
||||
) {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const participants = project.isGroup
|
||||
? project.groupMembers.map((member) => {
|
||||
const candidateProject = state.projects.find((item) => item.id === member.projectId);
|
||||
const device = state.devices.find((item) => item.id === member.deviceId);
|
||||
const status: ConversationParticipant["status"] = !candidateProject
|
||||
? "missing_project"
|
||||
: isDispatchableThreadProject(candidateProject)
|
||||
? "active"
|
||||
: "invalid_target";
|
||||
return buildParticipant(
|
||||
member.projectId,
|
||||
member.deviceId,
|
||||
member.threadId,
|
||||
member.threadDisplayName,
|
||||
member.folderName,
|
||||
device?.avatar,
|
||||
member.projectId === project.id,
|
||||
status,
|
||||
Boolean(candidateProject),
|
||||
);
|
||||
})
|
||||
: [
|
||||
buildParticipant(
|
||||
project.id,
|
||||
project.deviceIds[0] ?? project.id,
|
||||
project.threadMeta.threadId,
|
||||
project.threadMeta.threadDisplayName,
|
||||
project.threadMeta.folderName,
|
||||
state.devices.find((item) => item.id === project.deviceIds[0])?.avatar,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const validParticipantCount = participants.filter((item) => item.status === "active").length;
|
||||
const invalidParticipantCount = participants.length - validParticipantCount;
|
||||
const repairRequired =
|
||||
project.isGroup && (invalidParticipantCount > 0 || validParticipantCount < 2);
|
||||
const repairReason = !repairRequired
|
||||
? undefined
|
||||
: validParticipantCount === 0
|
||||
? "当前群聊里还没有可下发的真实线程,请重新添加线程。"
|
||||
: invalidParticipantCount > 0
|
||||
? "当前群聊里有失效或不可下发的线程引用,请重新整理群成员。"
|
||||
: "当前群聊至少需要 2 个真实线程成员。";
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
projectId: project.id,
|
||||
isGroup: project.isGroup,
|
||||
threadMeta: project.threadMeta,
|
||||
participants,
|
||||
repairRequired,
|
||||
repairReason,
|
||||
validParticipantCount,
|
||||
invalidParticipantCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,54 +129,47 @@ export async function GET(
|
||||
|
||||
const { projectId } = await context.params;
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
const payload = buildParticipantsPayload(state, projectId);
|
||||
if (!payload) {
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
const participants = project.isGroup
|
||||
? (project.groupMembers.length > 0
|
||||
? project.groupMembers.map((member) => {
|
||||
const device = state.devices.find((item) => item.id === member.deviceId);
|
||||
return buildParticipant(
|
||||
member.projectId,
|
||||
member.deviceId,
|
||||
member.threadId,
|
||||
member.threadDisplayName,
|
||||
member.folderName,
|
||||
device?.avatar,
|
||||
member.projectId === project.id,
|
||||
);
|
||||
})
|
||||
: [
|
||||
buildParticipant(
|
||||
project.id,
|
||||
project.deviceIds[0] ?? project.id,
|
||||
project.threadMeta.threadId,
|
||||
project.threadMeta.threadDisplayName,
|
||||
project.threadMeta.folderName,
|
||||
state.devices.find((item) => item.id === project.deviceIds[0])?.avatar,
|
||||
true,
|
||||
),
|
||||
])
|
||||
: [
|
||||
buildParticipant(
|
||||
project.id,
|
||||
project.deviceIds[0] ?? project.id,
|
||||
project.threadMeta.threadId,
|
||||
project.threadMeta.threadDisplayName,
|
||||
project.threadMeta.folderName,
|
||||
state.devices.find((item) => item.id === project.deviceIds[0])?.avatar,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
projectId: project.id,
|
||||
isGroup: project.isGroup,
|
||||
threadMeta: project.threadMeta,
|
||||
participants,
|
||||
});
|
||||
return NextResponse.json(payload);
|
||||
}
|
||||
|
||||
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 body = (await request.json().catch(() => ({}))) as {
|
||||
memberProjectIds?: string[];
|
||||
};
|
||||
const { projectId } = await context.params;
|
||||
|
||||
try {
|
||||
await replaceGroupChatMembers({
|
||||
projectId,
|
||||
memberProjectIds: Array.isArray(body.memberProjectIds)
|
||||
? body.memberProjectIds.filter(
|
||||
(item): item is string => typeof item === "string" && item.trim().length > 0,
|
||||
)
|
||||
: [],
|
||||
requestedBy: session.account,
|
||||
});
|
||||
const nextState = await readState();
|
||||
const payload = buildParticipantsPayload(nextState, projectId);
|
||||
if (!payload) {
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(payload);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4175,6 +4175,50 @@ export async function confirmDispatchPlan(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function rejectDispatchPlan(input: {
|
||||
groupProjectId: string;
|
||||
planId: string;
|
||||
rejectedBy: string;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const groupProjectId = input.groupProjectId.trim();
|
||||
if (!groupProjectId) throw new Error("PROJECT_NOT_FOUND");
|
||||
const groupProject = state.projects.find((item) => item.id === groupProjectId);
|
||||
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (!groupProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||||
requireDispatchActorSession(state, input.rejectedBy);
|
||||
|
||||
const plan = state.dispatchPlans.find((item) => item.planId === input.planId);
|
||||
if (!plan) throw new Error("DISPATCH_PLAN_NOT_FOUND");
|
||||
if (plan.groupProjectId !== groupProjectId) {
|
||||
throw new Error("DISPATCH_PLAN_PROJECT_MISMATCH");
|
||||
}
|
||||
if (plan.status === "dispatched") {
|
||||
throw new Error("DISPATCH_PLAN_ALREADY_DISPATCHED");
|
||||
}
|
||||
if (plan.status !== "rejected") {
|
||||
plan.status = "rejected";
|
||||
}
|
||||
groupProject.approvalState = "rejected";
|
||||
const notice =
|
||||
pushProjectLedgerMessage(state, groupProjectId, {
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: "已拒绝主 Agent 推荐,本次不会下发到任何线程。",
|
||||
kind: "system_notice",
|
||||
}) ?? null;
|
||||
|
||||
return {
|
||||
plan: { ...plan },
|
||||
notice: notice ? { ...notice } : null,
|
||||
};
|
||||
});
|
||||
|
||||
publishBossEvent("project.messages.updated", { projectId: input.groupProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: input.groupProjectId });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createDispatchExecutionsFromPlan(input: {
|
||||
planId: string;
|
||||
confirmedBy: string;
|
||||
@@ -6266,17 +6310,13 @@ export async function createIndependentGroupChat(input: {
|
||||
return project;
|
||||
}
|
||||
|
||||
function createGroupChatFromProjectIds(
|
||||
function resolveGroupChatThreadProjects(
|
||||
state: BossState,
|
||||
input: {
|
||||
requestedProjectIds: string[];
|
||||
createdBy: string;
|
||||
defaultRiskLevel?: Project["riskLevel"];
|
||||
},
|
||||
requestedProjectIds: string[],
|
||||
) {
|
||||
const memberProjects: Project[] = [];
|
||||
const seenProjectIds = new Set<string>();
|
||||
for (const projectId of input.requestedProjectIds) {
|
||||
for (const projectId of requestedProjectIds) {
|
||||
if (!projectId || seenProjectIds.has(projectId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -6291,6 +6331,77 @@ function createGroupChatFromProjectIds(
|
||||
}
|
||||
memberProjects.push(memberProject);
|
||||
}
|
||||
return memberProjects;
|
||||
}
|
||||
|
||||
export async function replaceGroupChatMembers(input: {
|
||||
projectId: string;
|
||||
memberProjectIds: string[];
|
||||
requestedBy: string;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const groupProject = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!groupProject) {
|
||||
throw new Error("PROJECT_NOT_FOUND");
|
||||
}
|
||||
if (!groupProject.isGroup) {
|
||||
throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||||
}
|
||||
|
||||
const memberProjects = resolveGroupChatThreadProjects(state, input.memberProjectIds);
|
||||
if (memberProjects.length < 2) {
|
||||
throw new Error("GROUP_CHAT_REQUIRES_AT_LEAST_TWO_THREADS");
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
groupProject.groupMembers = memberProjects.map((memberProject) => ({
|
||||
projectId: memberProject.id,
|
||||
deviceId: memberProject.deviceIds[0] ?? memberProject.id,
|
||||
threadId: memberProject.threadMeta.threadId,
|
||||
threadDisplayName: memberProject.threadMeta.threadDisplayName,
|
||||
folderName: memberProject.threadMeta.folderName,
|
||||
}));
|
||||
groupProject.deviceIds = dedupeStrings(groupProject.groupMembers.map((member) => member.deviceId));
|
||||
groupProject.threadMeta.activityIconCount = Math.max(1, groupProject.groupMembers.length);
|
||||
groupProject.threadMeta.folderName = "群聊";
|
||||
groupProject.threadMeta.updatedAt = now;
|
||||
groupProject.updatedAt = now;
|
||||
groupProject.lastMessageAt = now;
|
||||
groupProject.approvalState = "not_required";
|
||||
const memberLabel = memberProjects
|
||||
.map((project) => project.threadMeta.threadDisplayName || project.name)
|
||||
.join("、");
|
||||
pushProjectLedgerMessage(state, groupProject.id, {
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: `已更新群成员:${memberLabel}`,
|
||||
kind: "system_notice",
|
||||
sentAt: now,
|
||||
});
|
||||
|
||||
return {
|
||||
project: { ...groupProject },
|
||||
groupMembers: groupProject.groupMembers.map((member) => ({ ...member })),
|
||||
};
|
||||
});
|
||||
|
||||
publishBossEvent("project.messages.updated", { projectId: input.projectId });
|
||||
publishBossEvent("conversation.updated", {
|
||||
projectId: input.projectId,
|
||||
note: `group members updated by ${input.requestedBy}`,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function createGroupChatFromProjectIds(
|
||||
state: BossState,
|
||||
input: {
|
||||
requestedProjectIds: string[];
|
||||
createdBy: string;
|
||||
defaultRiskLevel?: Project["riskLevel"];
|
||||
},
|
||||
) {
|
||||
const memberProjects = resolveGroupChatThreadProjects(state, input.requestedProjectIds);
|
||||
if (memberProjects.length < 2) {
|
||||
throw new Error("GROUP_CHAT_REQUIRES_AT_LEAST_TWO_THREADS");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user