feat(rectification): add durable case and evidence runtime

This commit is contained in:
Jesse
2026-08-11 16:18:53 +08:00
parent 60e2ce4fa4
commit d394dd0585
13 changed files with 3794 additions and 0 deletions
@@ -0,0 +1,80 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
RectificationCaseServiceError,
closeRectificationCase,
mapRectificationRpcError,
} from "@/lib/rectification-agentic/v9/case-service";
export const runtime = "nodejs";
type RouteContext = { params: Promise<{ caseId: string }> };
const closeRequestSchema = z
.object({
reason: z.enum(["completed_by_user", "abandoned_by_user", "other"]),
})
.strict();
/**
* POST /api/rectification/cases/[caseId]/close
*
* Explicit business close only; never wraps as engine confirmed.
*/
export async function POST(request: Request, context: RouteContext) {
let supabase;
let accounting;
try {
supabase = await createServerSupabaseClient();
accounting = createAdminSupabaseClient();
} catch {
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
}
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const { caseId } = await context.params;
if (!z.string().uuid().safeParse(caseId).success) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_case_id" },
{ status: 400 },
);
}
const parsed = closeRequestSchema.safeParse(
await request.json().catch(() => null),
);
if (!parsed.success) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_close_request" },
{ status: 400 },
);
}
try {
const result = await closeRectificationCase(
accounting,
user.id,
caseId,
parsed.data.reason,
);
return NextResponse.json(result);
} catch (error) {
if (error instanceof RectificationCaseServiceError) {
const view = mapRectificationRpcError(
new Error(`agentic_rectification_${error.code}`),
);
return NextResponse.json({ error: view.message, code: view.code }, { status: view.status });
}
return NextResponse.json(
{ error: "校正服务暂时不可用", code: "rectification_service_failed" },
{ status: 500 },
);
}
}
@@ -0,0 +1,62 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
RectificationCaseServiceError,
getRectificationCase,
mapRectificationRpcError,
} from "@/lib/rectification-agentic/v9/case-service";
export const runtime = "nodejs";
type RouteContext = { params: Promise<{ caseId: string }> };
/**
* GET /api/rectification/cases/[caseId]
*
* Sanitized case projection (never the baseline birth snapshot). Terminal
* cases are served read-only.
*/
export async function GET(_request: Request, context: RouteContext) {
let supabase;
let accounting;
try {
supabase = await createServerSupabaseClient();
accounting = createAdminSupabaseClient();
} catch {
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
}
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const { caseId } = await context.params;
if (!z.string().uuid().safeParse(caseId).success) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_case_id" },
{ status: 400 },
);
}
try {
return NextResponse.json(
await getRectificationCase(accounting, user.id, caseId),
);
} catch (error) {
if (error instanceof RectificationCaseServiceError) {
const view = mapRectificationRpcError(
new Error(`agentic_rectification_${error.code}`),
);
return NextResponse.json({ error: view.message, code: view.code }, { status: view.status });
}
return NextResponse.json(
{ error: "校正服务暂时不可用", code: "rectification_service_failed" },
{ status: 500 },
);
}
}
@@ -0,0 +1,81 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
RectificationCaseServiceError,
mapRectificationRpcError,
upgradeRectificationSkill,
} from "@/lib/rectification-agentic/v9/case-service";
export const runtime = "nodejs";
type RouteContext = { params: Promise<{ caseId: string }> };
const upgradeRequestSchema = z
.object({
skillVersion: z.string().trim().min(1).max(64),
})
.strict();
/**
* POST /api/rectification/cases/[caseId]/upgrade-skill
*
* Explicit skill-version migration for a running case; terminal cases are
* rejected and new cases pin the current default version.
*/
export async function POST(request: Request, context: RouteContext) {
let supabase;
let accounting;
try {
supabase = await createServerSupabaseClient();
accounting = createAdminSupabaseClient();
} catch {
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
}
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const { caseId } = await context.params;
if (!z.string().uuid().safeParse(caseId).success) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_case_id" },
{ status: 400 },
);
}
const parsed = upgradeRequestSchema.safeParse(
await request.json().catch(() => null),
);
if (!parsed.success) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_upgrade_request" },
{ status: 400 },
);
}
try {
const result = await upgradeRectificationSkill(
accounting,
user.id,
caseId,
parsed.data.skillVersion,
);
return NextResponse.json(result);
} catch (error) {
if (error instanceof RectificationCaseServiceError) {
const view = mapRectificationRpcError(
new Error(`agentic_rectification_${error.code}`),
);
return NextResponse.json({ error: view.message, code: view.code }, { status: view.status });
}
return NextResponse.json(
{ error: "校正服务暂时不可用", code: "rectification_service_failed" },
{ status: 500 },
);
}
}
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
RectificationCaseServiceError,
getRectificationEntrySummary,
mapRectificationRpcError,
} from "@/lib/rectification-agentic/v9/case-service";
export const runtime = "nodejs";
/**
* GET /api/rectification/cases/entry-summary
*
* Server-truth homepage CTA: "开始生时校正" / "继续上次校正" / "再次校正".
*/
export async function GET() {
let supabase;
let accounting;
try {
supabase = await createServerSupabaseClient();
accounting = createAdminSupabaseClient();
} catch {
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
}
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
try {
return NextResponse.json(await getRectificationEntrySummary(accounting, user.id));
} catch (error) {
if (error instanceof RectificationCaseServiceError) {
const view = mapRectificationRpcError(
new Error(`agentic_rectification_${error.code}`),
);
return NextResponse.json({ error: view.message, code: view.code }, { status: view.status });
}
return NextResponse.json(
{ error: "校正服务暂时不可用", code: "rectification_service_failed" },
{ status: 500 },
);
}
}
@@ -0,0 +1,66 @@
import { NextResponse } from "next/server";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
RectificationCaseServiceError,
mapRectificationRpcError,
openRectificationCase,
} from "@/lib/rectification-agentic/v9/case-service";
import { parseOpenRectificationCaseRequest } from "@/lib/rectification-agentic/v9/open-request";
export const runtime = "nodejs";
function serviceErrorResponse(error: unknown) {
if (error instanceof RectificationCaseServiceError) {
const view = mapRectificationRpcError(
new Error(`agentic_rectification_${error.code}`),
);
return NextResponse.json({ error: view.message, code: view.code }, { status: view.status });
}
return NextResponse.json(
{ error: "校正服务暂时不可用", code: "rectification_service_failed" },
{ status: 500 },
);
}
/**
* POST /api/rectification/cases/open
*
* Browser sends only { intent, requestId } (+ sessionId for intent=session).
* The server derives the user from the session, normalizes the profile and
* decides resume-or-create. Double-click / multi-tab are idempotent.
*/
export async function POST(request: Request) {
let supabase;
let accounting;
try {
supabase = await createServerSupabaseClient();
accounting = createAdminSupabaseClient();
} catch {
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
}
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const parsed = parseOpenRectificationCaseRequest(
await request.json().catch(() => null),
);
if (!parsed) {
return NextResponse.json(
{ error: "请求内容不正确", code: "invalid_open_request" },
{ status: 400 },
);
}
try {
const response = await openRectificationCase(accounting, user.id, parsed);
return NextResponse.json(response);
} catch (error) {
return serviceErrorResponse(error);
}
}