chore: scaffold monorepo and migrate uhd_web design system (W0-A)

- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
umtandClaude Fable 5.1 committed 2026-09-27 17:36:26 +08:00
commit b22c440139
138 files changed
+14455

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# @umt/contracts
所有 API、SSE 事件与能力(capability)输入输出的唯一定义(zod 4 schema + 推导出的 TS 类型)。web 与 server 共用,任何一端都不得手写重复 DTO;新增接口先在这里加 schema,再由 server 用 `@hono/zod-openapi` 生成 OpenAPI。
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@umt/contracts",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc -p tsconfig.json",
"test": "vitest run"
},
"dependencies": {
"zod": "^4.3.6"
}
}
+45
View File
@@ -0,0 +1,45 @@
import { z } from "zod";
/**
* 能力调用上下文(REQ §10.3)。产品自有接口,不是 Pi SDK 原生 API。
* signal 由任务取消传播而来。
*/
export interface CapabilityContext {
actorId: string;
taskId: string;
scopeIds: string[];
signal: AbortSignal;
}
/** 变更计划:由业务端生成与保存,审批绑定 id + payloadHash + expectedRevision */
export const MutationPlan = z.object({
id: z.string(),
operation: z.string(),
targetIds: z.array(z.string()),
expectedRevision: z.number().int().nonnegative(),
evidenceIds: z.array(z.string()),
payloadHash: z.string(),
requiresApproval: z.boolean(),
});
export type MutationPlan = z.infer<typeof MutationPlan>;
/** 能力风险级别 */
export const CapabilityRisk = z.enum(["read", "preview", "write", "destructive"]);
export type CapabilityRisk = z.infer<typeof CapabilityRisk>;
/** 能力 ID:`域.动作`,如 download.submit */
export const CapabilityId = z.string().regex(/^[a-z][a-z0-9]*\.[a-z][a-z0-9_]*$/);
export type CapabilityId = z.infer<typeof CapabilityId>;
/** 能力定义(注册中心元数据;处理器在 server 侧挂载) */
export interface CapabilityDefinition<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
id: CapabilityId;
version: number;
description: string;
input: I;
output: O;
scopes: string[];
risk: CapabilityRisk;
idempotent: boolean;
supportsPreview: boolean;
}
+93
View File
@@ -0,0 +1,93 @@
import { z } from "zod";
/** 本地实体 ID 一律 UUID */
export const Id = z.uuid();
export type Id = z.infer<typeof Id>;
/** ISO 8601 时间串 */
export const Timestamp = z.iso.datetime({ offset: true });
export type Timestamp = z.infer<typeof Timestamp>;
/** 游标分页请求:cursor 为不透明字符串,由服务端编码 */
export const CursorQuery = z.object({
cursor: z.string().min(1).optional(),
limit: z.coerce.number().int().min(1).max(200).default(50),
});
export type CursorQuery = z.infer<typeof CursorQuery>;
/** 游标分页结果:nextCursor 为 null 表示已到末尾 */
export const cursorPage = <T extends z.ZodType>(item: T) =>
z.object({
items: z.array(item),
nextCursor: z.string().nullable(),
});
export type CursorPage<T> = { items: T[]; nextCursor: string | null };
/** 错误码表:新增错误码只在这里加 */
export const ApiErrorCode = z.enum([
"bad_request",
"validation_failed",
"unauthorized",
"forbidden",
"not_found",
"conflict",
"precondition_failed",
"rate_limited",
"automation_disabled",
"upstream_unavailable",
"internal",
]);
export type ApiErrorCode = z.infer<typeof ApiErrorCode>;
/** 错误码对应的 HTTP 状态 */
export const API_ERROR_STATUS = {
bad_request: 400,
validation_failed: 422,
unauthorized: 401,
forbidden: 403,
not_found: 404,
conflict: 409,
precondition_failed: 412,
rate_limited: 429,
automation_disabled: 423,
upstream_unavailable: 502,
internal: 500,
} as const satisfies Record<ApiErrorCode, number>;
export const ApiError = z.object({
code: ApiErrorCode,
message: z.string(),
});
export type ApiError = z.infer<typeof ApiError>;
/** 统一响应信封 */
export const apiResponse = <T extends z.ZodType>(data: T) =>
z.object({
ok: z.boolean(),
data: data.optional(),
error: ApiError.optional(),
});
export type ApiResponse<T> =
| { ok: true; data: T; error?: undefined }
| { ok: false; data?: undefined; error: ApiError };
/** 没有 data 的失败响应 schema(OpenAPI 错误分支用) */
export const ApiErrorResponse = z.object({
ok: z.literal(false),
error: ApiError,
});
/** API Key 权限范围 */
export const ApiScope = z.enum(["read", "organize.read", "organize.execute", "admin"]);
export type ApiScope = z.infer<typeof ApiScope>;
/** 健康检查 */
export const HealthStatus = z.object({
status: z.enum(["ok", "degraded"]),
version: z.string(),
role: z.enum(["api", "worker", "all"]),
uptimeSec: z.number(),
db: z.enum(["ok", "down", "unknown"]),
time: Timestamp,
});
export type HealthStatus = z.infer<typeof HealthStatus>;
+28
View File
@@ -0,0 +1,28 @@
import { z } from "zod";
import { Timestamp } from "./common";
/** SSE 事件信封:seq 在进程内单调递增,客户端断线后用 ?after=<seq> 补读 */
export const EventEnvelope = z.object({
seq: z.number().int().nonnegative(),
type: z.string().min(1),
at: Timestamp,
payload: z.unknown(),
});
export type EventEnvelope<P = unknown> = {
seq: number;
type: string;
at: string;
payload: P;
};
/** 已知事件类型;业务事件后续按 `域.动作` 追加 */
export const EventType = {
heartbeat: "system.heartbeat",
resync: "system.resync",
} as const;
export type EventType = (typeof EventType)[keyof typeof EventType];
/** 心跳 payload */
export const HeartbeatPayload = z.object({
role: z.string(),
});
+3
View File
@@ -0,0 +1,3 @@
export * from "./capability";
export * from "./common";
export * from "./events";
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { API_ERROR_STATUS, apiResponse, CapabilityId, CursorQuery, EventEnvelope, Id } from "../src";
describe("contracts", () => {
it("Id 只接受 uuid", () => {
expect(Id.safeParse("7d3f7f5e-8a8c-4b8e-9f0a-1b2c3d4e5f60").success).toBe(true);
expect(Id.safeParse("abc").success).toBe(false);
});
it("CursorQuery 默认 limit 50 且限制上限", () => {
expect(CursorQuery.parse({}).limit).toBe(50);
expect(CursorQuery.safeParse({ limit: 1000 }).success).toBe(false);
});
it("ApiResponse 信封", () => {
const s = apiResponse(z.object({ n: z.number() }));
expect(s.parse({ ok: true, data: { n: 1 } }).data?.n).toBe(1);
expect(s.safeParse({ ok: false, error: { code: "nope", message: "" } }).success).toBe(false);
});
it("错误码都有 HTTP 状态", () => {
expect(API_ERROR_STATUS.not_found).toBe(404);
});
it("事件信封与能力 ID", () => {
expect(
EventEnvelope.safeParse({ seq: 1, type: "system.heartbeat", at: new Date().toISOString(), payload: {} }).success,
).toBe(true);
expect(CapabilityId.safeParse("download.submit").success).toBe(true);
expect(CapabilityId.safeParse("Download").success).toBe(false);
});
});
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src", "test"]
}