fix(api): stop labeling database blips as unconfigured (BUG-542)
Deploy-window connection errors were translated into "服务尚未配置". Only real configuration errors keep that copy; other 503s use service_unavailable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-06 — 短暂连不上数据库时不再说「服务尚未配置」
|
||||
|
||||
部署刚切换、数据库暂时连不上时,页面会提示服务暂时不可用、请稍后重试,而不会再说成环境没配好。真的缺少数据库环境变量时,原来的「服务尚未配置」还在。Skill 版本未变。
|
||||
|
||||
## 2026-09-05 — 带年份经历没问完不出时间卡,也不写验证报告
|
||||
|
||||
生时校正里,学业、钱、搬家、身体或工作这些带年份的口述还没问完(也没说没有)时,页面只显示只读时间范围,没有可点的时间卡,助手也不写验证报告。这些问完之后,报告和卡片在同一轮出现,下面不再追问。你说「没有更多」、已经选定时间、或题目已经问尽,仍按原来的收口。Skill 版本仍是 10.0.14。
|
||||
|
||||
@@ -8373,6 +8373,22 @@
|
||||
- 复发自:BUG-390(质量探针只对学业发出,但学业内 kind 未收紧)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-542 | 部署窗口数据库瞬断被报成「服务尚未配置」
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-04
|
||||
- 最近更新:2026-09-06
|
||||
- 影响面:16 处 API 路由的 Supabase/数据库客户端创建兜底;共享 helper `frontend/src/lib/api/service-unavailable.ts`
|
||||
- 用户现象:staging 刚切完部署时,生时校正点选答案返回 `503 {"error":"服务尚未配置"}`。同一时刻 `/api/health` 显示数据库检查 degraded,数分钟后自愈。环境变量从未缺失。
|
||||
- 触发条件:部署切换窗口内数据库短暂不可用;`createServerSupabaseClient()` 在自托管分支会先读身份会话(数据库查询),连接错误从这里抛出。
|
||||
- 根因:16 处 `catch` 把任意创建失败一律翻译成「服务尚未配置」。该文案只对应 `SupabaseConfigurationError`(缺环境变量)。
|
||||
- 修复:共享 `jsonForSupabaseSetupFailure`:仅配置错误返回「服务尚未配置」;其它异常返回 `503 {"error":"服务暂时不可用,请稍后重试","code":"service_unavailable"}`,并 `console.error` 路由名与 `error.name`(不打印堆栈/连接串)。不改客户端抛错语义、不加重试、不改 health。
|
||||
- 验证:`npx tsx --test tests/api-service-unavailable-20260904.test.ts` 4/4。mock `createServerSupabaseClient` 抛配置错误 → 503「服务尚未配置」;抛 `Error("connection refused")` → `code=service_unavailable`,文案不含「尚未配置」。覆盖 `POST /api/rectification/agent` 与 `GET /api/rectification/cases/[caseId]`。`grep -rn "服务尚未配置" frontend/src` 只命中 helper。`tsc --noEmit` 0 错;改动文件 eslint `--quiet` 0 error。
|
||||
- 防复发:新路由创建数据库客户端失败不得手写「服务尚未配置」;必须走 helper。发布门模块 mock 继续用 Node 22 的 `namedExports`(BUG-514)。
|
||||
- 相关记录:BUG-010、BUG-514
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-543 | staging 镜像构建因 Google Fonts 拉不到 Inter 失败
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# PROGRESS · 部署窗口 503 误标为「服务尚未配置」(2026-09-04)
|
||||
|
||||
工作树:`.worktrees/api-not-configured-mislabel-20260904`
|
||||
分支:`codex/api-not-configured-mislabel-20260904`
|
||||
基线:开工时 `origin/staging` @ `d2a209d3`(任务书写的是 `a75929c1`;staging 已含后续提交)。未改 `.gitea/workflows/**`、未动迁移、未升级依赖、未 bump Skill、未改 `createServerSupabaseClient` / `createAdminSupabaseClient` 抛错语义。
|
||||
|
||||
| 任务 | 状态 | BUG |
|
||||
| --- | --- | --- |
|
||||
| 共享 helper + 16 处替换 | 完成 | BUG-542 |
|
||||
| 回归测试(agent + case 刷新) | 完成 | BUG-542 |
|
||||
| BUG_HISTORY / CHANGELOG / 本文件 | 完成 | BUG-542 |
|
||||
|
||||
## 编号
|
||||
|
||||
开工时最大号 BUG-550。本单使用预留的 **BUG-542**。未占用 BUG-551 及之后。
|
||||
|
||||
## 实现要点
|
||||
|
||||
- `frontend/src/lib/api/service-unavailable.ts`:`jsonForSupabaseSetupFailure(error, routeName)`。仅 `isSupabaseConfigurationError` 返回「服务尚未配置」+「请先配置数据库环境变量。」;其它异常 `503` + `code=service_unavailable`,`console.error(routeName, error.name)`,不把 Error 对象打进日志。
|
||||
- 16 处创建客户端的 catch 改为调用 helper(含 `consult/status` 的 server/admin 两段)。`birth-time-guide` / `birth-time-journey` 原先配置错误才 503、其它直接 throw,现与其余路由一样返回 `service_unavailable`。
|
||||
- 未登录仍是 401。未改 health、未加重试。
|
||||
|
||||
## 测试
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `npx tsx --test tests/api-service-unavailable-20260904.test.ts` | 4 项;pass 4;fail 0(约 28s,agent 路由 mock 导入较重) |
|
||||
| `npx tsx --test tests/self-hosted-error-copy.test.ts` | 1/1 |
|
||||
| `grep -rn "服务尚未配置" frontend/src` | 只命中 `src/lib/api/service-unavailable.ts` |
|
||||
| `npx tsc --noEmit` | 0 错 |
|
||||
| 改动文件 eslint `--quiet` | 0 error |
|
||||
|
||||
Node 22 mock 契约:子进程 `mock.module(..., { namedExports })`(BUG-514)。本地 Node 24 会警告 `namedExports` 已弃用,不可改成 `exports`。
|
||||
|
||||
## 既有断言 / 文案变更(原值 / 新值 / 原因)
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 16 处 catch 对非配置异常 | `503`「服务尚未配置」或直接 throw | `503` `code=service_unavailable`「服务暂时不可用,请稍后重试」 | 决策记录 1:只有配置错误能说尚未配置 |
|
||||
| `GET /api/models` 目录为空 | `error: "模型服务尚未配置"` | `error: "没有可用的咨询模型"` | 该句包含「服务尚未配置」子串,会撞验收 grep;这不是配置错误,是目录为空 |
|
||||
|
||||
未改既有测试断言。无既有测试把非配置异常锁成「服务尚未配置」。
|
||||
|
||||
## 遗留
|
||||
|
||||
未做浏览器真人走查。部署窗口的短暂 503 仍会出现,只是说法不再误导成环境变量丢失。未跑全量 `npm run lint` / `npm test`(任务书验收是 tsc + lint 0 error + 本单测试;改动文件 eslint 已 0 error)。
|
||||
@@ -60,7 +60,7 @@
|
||||
| `TASK-settings-dialog-and-billing-pane-20260906.md` | `PROGRESS-settings-dialog-and-billing-pane-20260906.md` | 设置弹窗尺寸随分区跳变、星盘资料格无条件铺开整张添加表单(改为固定尺寸四分区、列表→详情);「账户与点数」成为弹窗分区,删除 `/membership` 与 `/membership/orders` 页面并重定向,七处入口改回调 | 待执行(串行:在会话列表单之后) | `codex/settings-dialog-and-billing-pane-20260906`(BUG-554) |
|
||||
| `TASK-consultation-context-and-cache-20260906.md` | `PROGRESS-consultation-context-and-cache-20260906.md` | 普通对话历史只取每条前 4,000 字(报告结论被砍、无标记)、历史窗口不看模型 `context_window`、无溢出识别;改为检查点式会话摘要 + append-only 尾巴 + 按模型预算 + 一次降级重试;Anthropic 历史断点;共享方法段进系统块;后台用量页显示缓存命中率 | 待执行(串行:在会话列表单之后;动表需迁移) | `codex/consultation-context-and-cache-20260906`(BUG-555/556) |
|
||||
| `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`(2026-09-05;staging 部署仍停在 `afd14948`,`deploy-staging` 自 `c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`(BUG-550) |
|
||||
| `TASK-api-not-configured-mislabel-20260904.md` | — | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待执行 | `codex/api-not-configured-mislabel-20260904`(BUG-542 起) |
|
||||
| `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待验收 | `codex/api-not-configured-mislabel-20260904`(BUG-542) |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
createSupabaseBirthTimeJourneyStore,
|
||||
} from "@/lib/birth-time-journey-store";
|
||||
import { StaleJourneyTurnError } from "@/lib/birth-time-journey-turn-persistence";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { getBirthTimeGuideAgent } from "@/mastra";
|
||||
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
|
||||
@@ -40,13 +40,7 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/birth-time-guide");
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
|
||||
@@ -14,8 +14,8 @@ import { StaleJourneyTurnError } from "@/lib/birth-time-journey-turn-persistence
|
||||
import { CandidateConfirmationError } from "@/lib/birth-time-evidence";
|
||||
import { BirthTimeEvidenceContextError, EvidenceRectificationCaseNotFoundError, GuidedJourneyLegacyMutationError, StaleCandidateConfirmationError } from "@/lib/birth-time-evidence-service";
|
||||
import { createSupabaseBirthTimeJourneyStore, BirthTimeJourneyStoreError } from "@/lib/birth-time-journey-store";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { BirthTimeScoringJobError } from "@/lib/birth-time-scoring-job";
|
||||
import { GuidedCandidateActionError } from "@/lib/birth-time-guided-candidate";
|
||||
@@ -62,13 +62,7 @@ export async function POST(request: Request) {
|
||||
supabase = await createServerSupabaseClient();
|
||||
journeyStoreClient = createAdminSupabaseClient();
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/birth-time-journey");
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { runCreditRpc } from "@/lib/consultation-billing";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
@@ -16,11 +17,8 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/consult/cancel");
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
|
||||
@@ -33,6 +33,7 @@ import { cachedSystemMessage, mergePromptCacheUsage, promptCacheUsage } from "@/
|
||||
import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricing";
|
||||
import { reserveConsultationModel } from "@/lib/consultation-model-selection";
|
||||
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { streamTextResponse } from "@/lib/stream-text-response";
|
||||
@@ -250,11 +251,8 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/consult");
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { runCreditRpc } from "@/lib/consultation-billing";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
@@ -19,8 +20,8 @@ export async function GET(request: Request) {
|
||||
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/consult/status");
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
@@ -45,8 +46,8 @@ export async function GET(request: Request) {
|
||||
let accounting: ReturnType<typeof createAdminSupabaseClient>;
|
||||
try {
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/consult/status");
|
||||
}
|
||||
|
||||
let query = accounting
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
|
||||
|
||||
@@ -9,13 +10,7 @@ export async function GET() {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/models");
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
@@ -33,7 +28,7 @@ export async function GET() {
|
||||
};
|
||||
if (!catalog.defaultModelId || catalog.models.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "模型服务尚未配置", message: "当前没有可用的咨询模型。" },
|
||||
{ error: "没有可用的咨询模型", message: "当前没有可用的咨询模型。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricin
|
||||
import { loadRuntimeFeatureFlags } from "@/lib/feature-flags";
|
||||
import { isProductEnabled } from "@/lib/product-access";
|
||||
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
|
||||
@@ -147,11 +148,8 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/agent");
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { adoptLegacyRectificationSkill, mapRectificationRpcError, RectificationCaseServiceError } from "@/lib/rectification-agentic/v9/case-service";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
@@ -27,8 +28,8 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/adopt-skill");
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isProductEnabled } from "@/lib/product-access";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
@@ -58,8 +59,8 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/candidates/accept");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
@@ -29,8 +30,8 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/close");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
@@ -40,8 +41,8 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/rectification/cases/[caseId]");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/lib/rectification-agentic/v9/tool-service";
|
||||
import { resolveExactSkillPackage } from "@/lib/skill-package-registry";
|
||||
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { getRectificationV9RegenerationAgent } from "@/mastra/agentic-rectification";
|
||||
@@ -63,8 +64,8 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/turns/[turnId]/regenerate");
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
@@ -30,8 +31,8 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/[caseId]/upgrade-skill");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
@@ -20,8 +21,8 @@ export async function GET() {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "GET /api/rectification/cases/entry-summary");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isProductEnabled } from "@/lib/product-access";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
@@ -37,8 +38,8 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "服务尚未配置" }, { status: 503 });
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/rectification/cases/open");
|
||||
}
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
|
||||
export const SERVICE_NOT_CONFIGURED_ERROR = "服务尚未配置";
|
||||
export const SERVICE_NOT_CONFIGURED_MESSAGE = "请先配置数据库环境变量。";
|
||||
|
||||
export function jsonForSupabaseSetupFailure(error: unknown, routeName: string) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: SERVICE_NOT_CONFIGURED_ERROR,
|
||||
message: SERVICE_NOT_CONFIGURED_MESSAGE,
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
const errorName = error instanceof Error ? error.name : "UnknownError";
|
||||
console.error(routeName, errorName);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "服务暂时不可用,请稍后重试",
|
||||
code: "service_unavailable",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsonForSupabaseSetupFailure } from "./api/service-unavailable.ts";
|
||||
import {
|
||||
createOnboardingCacheIdentity,
|
||||
decideOnboardingCache,
|
||||
@@ -146,11 +147,8 @@ export function createOnboardingPost(dependencies: OnboardingPostDependencies):
|
||||
let session: OnboardingSession;
|
||||
try {
|
||||
session = await dependencies.openSession();
|
||||
} catch { // no-excuse-ok: catch -- route boundary converts missing configuration.
|
||||
return Response.json(
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonForSupabaseSetupFailure(error, "POST /api/onboarding");
|
||||
}
|
||||
|
||||
if (session.authError || !session.userId) {
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const frontendRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const srcRoot = join(frontendRoot, "src");
|
||||
|
||||
const NOT_CONFIGURED = "服务尚未配置";
|
||||
|
||||
const ROUTES_USING_HELPER = [
|
||||
"src/app/api/rectification/agent/route.ts",
|
||||
"src/app/api/rectification/cases/open/route.ts",
|
||||
"src/app/api/rectification/cases/entry-summary/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/close/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/adopt-skill/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/upgrade-skill/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts",
|
||||
"src/app/api/rectification/cases/[caseId]/turns/[turnId]/regenerate/route.ts",
|
||||
"src/app/api/consult/route.ts",
|
||||
"src/app/api/consult/status/route.ts",
|
||||
"src/app/api/consult/cancel/route.ts",
|
||||
"src/app/api/models/route.ts",
|
||||
"src/app/api/birth-time-journey/route.ts",
|
||||
"src/app/api/birth-time-guide/route.ts",
|
||||
"src/lib/onboarding-post.ts",
|
||||
] as const;
|
||||
|
||||
function walk(directory: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const name of readdirSync(directory)) {
|
||||
const path = join(directory, name);
|
||||
if (statSync(path).isDirectory()) {
|
||||
files.push(...walk(path));
|
||||
continue;
|
||||
}
|
||||
if (path.endsWith(".ts") || path.endsWith(".tsx")) files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
type RouteProbe = {
|
||||
configStatus: number;
|
||||
configBody: Record<string, unknown>;
|
||||
unavailableStatus: number;
|
||||
unavailableBody: Record<string, unknown>;
|
||||
unavailableLogs: string[][];
|
||||
};
|
||||
|
||||
function executeRouteProbe(kind: "agent" | "cases"): RouteProbe {
|
||||
const script = String.raw`
|
||||
import { mock } from "node:test";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { SupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
|
||||
const kind = ${JSON.stringify(kind)};
|
||||
const moduleUrl = (path) => pathToFileURL(process.cwd() + "/" + path).href;
|
||||
mock.module("server-only", { namedExports: {} });
|
||||
|
||||
let calls = 0;
|
||||
mock.module("@/lib/supabase/server", { namedExports: {
|
||||
createServerSupabaseClient: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) throw new SupabaseConfigurationError(["APP_DATABASE_URL"]);
|
||||
throw new Error("connection refused");
|
||||
},
|
||||
}});
|
||||
mock.module("@/lib/supabase/admin", { namedExports: {
|
||||
createAdminSupabaseClient: () => { throw new Error("admin unused in this unit test"); },
|
||||
}});
|
||||
|
||||
const unavailableLogs = [];
|
||||
const originalError = console.error;
|
||||
console.error = (...args) => {
|
||||
unavailableLogs.push(args.map((value) => {
|
||||
if (typeof value === "string") return value;
|
||||
if (value instanceof Error) return value.stack ?? value.message;
|
||||
return String(value);
|
||||
}));
|
||||
};
|
||||
|
||||
async function invoke() {
|
||||
if (kind === "agent") {
|
||||
const { POST } = await import(moduleUrl("src/app/api/rectification/agent/route.ts"));
|
||||
return POST(new Request("https://staging.jyotisha.chat/api/rectification/agent", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{}",
|
||||
}));
|
||||
}
|
||||
const { GET } = await import(moduleUrl("src/app/api/rectification/cases/[caseId]/route.ts"));
|
||||
return GET(
|
||||
new Request("https://staging.jyotisha.chat/api/rectification/cases/123e4567-e89b-12d3-a456-426614174000?sessionId=123e4567-e89b-12d3-a456-426614174001"),
|
||||
{ params: Promise.resolve({ caseId: "123e4567-e89b-12d3-a456-426614174000" }) },
|
||||
);
|
||||
}
|
||||
|
||||
const configResponse = await invoke();
|
||||
const configBody = await configResponse.json();
|
||||
const unavailableResponse = await invoke();
|
||||
const unavailableBody = await unavailableResponse.json();
|
||||
console.error = originalError;
|
||||
console.log("AYANAM_RESULT " + JSON.stringify({
|
||||
configStatus: configResponse.status,
|
||||
configBody,
|
||||
unavailableStatus: unavailableResponse.status,
|
||||
unavailableBody,
|
||||
unavailableLogs,
|
||||
}));
|
||||
`;
|
||||
const result = spawnSync(process.execPath, [
|
||||
"--experimental-test-module-mocks",
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
], {
|
||||
cwd: frontendRoot,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const line = result.stdout
|
||||
.split("\n")
|
||||
.map((row) => row.trim())
|
||||
.find((row) => row.startsWith("AYANAM_RESULT "));
|
||||
assert.ok(line, result.stdout);
|
||||
return JSON.parse(line.slice("AYANAM_RESULT ".length)) as RouteProbe;
|
||||
}
|
||||
|
||||
function assertConfigMislabel(body: Record<string, unknown>) {
|
||||
assert.equal(body.error, NOT_CONFIGURED);
|
||||
assert.equal(body.code, undefined);
|
||||
}
|
||||
|
||||
function assertUnavailable(body: Record<string, unknown>, logs: string[][]) {
|
||||
assert.equal(body.error, "服务暂时不可用,请稍后重试");
|
||||
assert.equal(body.code, "service_unavailable");
|
||||
assert.equal(JSON.stringify(body).includes("尚未配置"), false);
|
||||
assert.ok(logs.some((entry) => entry.includes("Error")));
|
||||
assert.equal(JSON.stringify(logs).includes("connection refused"), false);
|
||||
assert.equal(JSON.stringify(logs).includes("APP_DATABASE_URL"), false);
|
||||
}
|
||||
|
||||
test("shared helper classifies configuration errors and connection failures", async () => {
|
||||
const { jsonForSupabaseSetupFailure } = await import("../src/lib/api/service-unavailable.ts");
|
||||
const { SupabaseConfigurationError } = await import("../src/lib/supabase/config.ts");
|
||||
const logged: unknown[][] = [];
|
||||
const originalError = console.error;
|
||||
console.error = (...args: unknown[]) => {
|
||||
logged.push(args);
|
||||
};
|
||||
try {
|
||||
const missing = await jsonForSupabaseSetupFailure(
|
||||
new SupabaseConfigurationError(["APP_DATABASE_URL"]),
|
||||
"POST /api/rectification/agent",
|
||||
);
|
||||
assert.equal(missing.status, 503);
|
||||
assert.deepEqual(await missing.json(), {
|
||||
error: NOT_CONFIGURED,
|
||||
message: "请先配置数据库环境变量。",
|
||||
});
|
||||
assert.deepEqual(logged, []);
|
||||
|
||||
const unavailable = await jsonForSupabaseSetupFailure(
|
||||
new Error("connection refused"),
|
||||
"GET /api/rectification/cases/[caseId]",
|
||||
);
|
||||
assert.equal(unavailable.status, 503);
|
||||
assert.deepEqual(await unavailable.json(), {
|
||||
error: "服务暂时不可用,请稍后重试",
|
||||
code: "service_unavailable",
|
||||
});
|
||||
assert.deepEqual(logged, [["GET /api/rectification/cases/[caseId]", "Error"]]);
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
});
|
||||
|
||||
test("rectification agent maps setup failures without calling the rest of the handler", () => {
|
||||
const probe = executeRouteProbe("agent");
|
||||
assert.equal(probe.configStatus, 503);
|
||||
assertConfigMislabel(probe.configBody);
|
||||
assert.equal(probe.unavailableStatus, 503);
|
||||
assertUnavailable(probe.unavailableBody, probe.unavailableLogs);
|
||||
assert.ok(probe.unavailableLogs.some((entry) =>
|
||||
entry.some((value) => value.includes("/api/rectification/agent")),
|
||||
));
|
||||
});
|
||||
|
||||
test("rectification case refresh maps setup failures without calling the rest of the handler", () => {
|
||||
const probe = executeRouteProbe("cases");
|
||||
assert.equal(probe.configStatus, 503);
|
||||
assertConfigMislabel(probe.configBody);
|
||||
assert.equal(probe.unavailableStatus, 503);
|
||||
assertUnavailable(probe.unavailableBody, probe.unavailableLogs);
|
||||
assert.ok(probe.unavailableLogs.some((entry) =>
|
||||
entry.some((value) => value.includes("/api/rectification/cases/")),
|
||||
));
|
||||
});
|
||||
|
||||
test("not-configured copy lives only in the shared helper, and the 16 routes call it", () => {
|
||||
const hits = walk(srcRoot)
|
||||
.filter((path) => readFileSync(path, "utf8").includes(NOT_CONFIGURED))
|
||||
.map((path) => path.slice(srcRoot.length + 1).split("\\").join("/"));
|
||||
assert.deepEqual(hits, ["lib/api/service-unavailable.ts"]);
|
||||
for (const relative of ROUTES_USING_HELPER) {
|
||||
const source = readFileSync(join(frontendRoot, relative), "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/jsonForSupabaseSetupFailure/,
|
||||
`${relative} must use jsonForSupabaseSetupFailure`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user