- 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]>
76 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|