feat: add group repair and dispatch rejection flows

This commit is contained in:
kris
2026-03-31 03:56:28 +08:00
parent 9c02ebb574
commit 4336dc22a7
21 changed files with 832 additions and 83 deletions

View File

@@ -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 },
);
}
}

View File

@@ -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 },
);
}
}