Files
UHD_Media_Tool/apps/server/src/modules/search/dispatch.ts
T
rootandClaude Opus 5.5 52402864d6 style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing,
  full-width parens around Chinese, single ellipsis, 「」 quotes, period rules)
- scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX
  text (not comments, types, keys, match/compare strings); --fix, --strict,
  --by-file; wired into deno task ci as check:copy in warn mode
- --fix applied to web, server and contracts; files other lanes were editing
  (subscriptions, sites, browser, agent, auth/account, inventory) left for them
- tests updated for the changed messages

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 20:09:29 +08:00

76 lines
2.9 KiB
TypeScript

import type { Context } from "hono";
import { Hono } from "hono";
import type { AuthEnv } from "../../http/auth.ts";
import { ApiException } from "../../http/errors.ts";
import type { ModuleApp } from "../types.ts";
// 进程内转调其它模块的 HTTP 端点(库存叠加 GET /inventory、下载 POST /acquisition-plans):
// 走对方真实的路由(scope 校验 / 入参校验 / 审计 / 业务逻辑都不复制),调用者身份沿用本次请求已通过鉴权的 principal。
// 所有模块路由注册完后第一次调用时才组装(route() 按调用时刻复制路由表)。
interface Origin {
principal: AuthEnv["Variables"]["principal"];
actor: AuthEnv["Variables"]["actor"];
session: AuthEnv["Variables"]["session"];
requestId: string | undefined;
}
export class InternalDispatcher {
private inner: Hono<AuthEnv> | null = null;
private readonly origins = new WeakMap<Request, Origin>();
private readonly errors = new WeakMap<Request, unknown>();
constructor(private readonly api: ModuleApp) {}
private app(): Hono<AuthEnv> {
if (this.inner) return this.inner;
const inner = new Hono<AuthEnv>();
inner.use("*", async (c, next) => {
const o = this.origins.get(c.req.raw);
if (!o) throw new ApiException("unauthorized", "内部调用缺少调用者");
c.set("principal", o.principal);
c.set("actor", o.actor);
c.set("session", o.session);
if (o.requestId) c.set("requestId" as never, o.requestId as never);
await next();
});
inner.route("/", this.api);
// 原样抛回原始错误(ApiException 由外层 app.onError 转信封)
inner.onError((err, c) => {
this.errors.set(c.req.raw, err);
return c.body(null, 500);
});
inner.notFound((c) => {
this.errors.set(c.req.raw, new ApiException("internal", `内部端点不存在:${c.req.method} ${c.req.path}`));
return c.body(null, 404);
});
this.inner = inner;
return inner;
}
async call<T>(from: Context<AuthEnv>, method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
const req = new Request(`http://internal${path}`, {
method,
signal: from.req.raw.signal,
...(body !== undefined ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}),
});
this.origins.set(req, {
principal: from.get("principal"),
actor: from.get("actor"),
session: from.get("session"),
requestId: from.get("requestId" as never) as string | undefined,
});
const res = await this.app().fetch(req);
const err = this.errors.get(req);
if (err) throw err;
const json = (await res.json()) as { ok: boolean; data?: T; error?: { code: string; message: string } };
if (!json.ok) {
throw new ApiException(
(json.error?.code ?? "internal") as never,
json.error?.message ?? `内部调用失败(${res.status})`,
);
}
return json.data as T;
}
}