Files
UHD_Media_Tool/packages/contracts/src/common.ts
T
rootandClaude Opus 5.5 fa344a7184 feat(web): token 数以 K 输入显示,已存凭证显示掩码占位(FIX-8)
- 新增 UnitInput 与 kind=tokens:上下文预算、单次 token 上限、自定义模型上下文窗口以 K 输入(允许小数),存储仍为原始 token 数;校验与提示用 K 表述;列表/详情 token 数统一 K/M 简写(formatTokens)
- 新增 SecretInput 与 kind=secret:已保存时显示「已设置 ••••••••」(有服务端掩码时带末 4 位),聚焦直接输入替换,留空保持,「清除」确认后保存生效;替换模型服务、TMDB、网页搜索、下载器、媒体服务器、浏览器服务、站点账号、Passkey、本机 Token 各处
- 统一只写凭证服务端语义:省略或空串 = 不变,null = 清除(contracts secretInput);下载器/媒体服务器新增清除;站点凭证同类更新可留空保持密码/Cookie/API Key/TOTP,TOTP 可 null 清除,不再误删 TOTP 与 UA
- contextBudget 最小值 4096→4000、contextWindow 1024→1000,便于按 K 填写
- 测试:换算与校验、SecretInput 渲染与状态、各接口留空不变/显式清除

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 00:10:46 +08:00

141 lines
4.5 KiB
TypeScript

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>;
/**
* 只写凭证输入(API Key、密码、Cookie、令牌)。全站统一语义:
* 省略或空串(含纯空白)= 保持原值;null = 清除(只在允许清除的更新接口上);非空 = 替换。
* 明文只在请求里出现,服务端加密为 secret 后丢弃,响应只给是否已设置(和个别接口的掩码)。
*/
export const WriteOnlySecret = (max = 4096) => z.string().max(max);
/** 归一只写凭证输入:空串 → undefined(不改);null 原样(清除);其余去首尾空白 */
export function secretInput(v: string | null | undefined): string | null | undefined {
if (v === null) return null;
if (v === undefined) return undefined;
const s = v.trim();
return s === "" ? undefined : s;
}
/** 游标分页请求: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",
"browser_reconnect_failed",
"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,
/** 浏览器服务重启后自动重连失败(BR-3):浏览器服务暂不可用,稍后重试 */
browser_reconnect_failed: 503,
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>;
/** 日期(无时区),如 air_date:YYYY-MM-DD */
export const DateOnly = z.iso.date();
export type DateOnly = z.infer<typeof DateOnly>;
/** JSONB 对象列(证据、规则参数、原文快照等)的 API 表示 */
export const JsonObject = z.record(z.string(), z.unknown());
export type JsonObject = z.infer<typeof JsonObject>;
/** 字节数、计数等非负整数(DB 为 bigint,客户端按 number 解析,上限 2^53) */
export const NonNegInt = z.number().int().nonnegative();
/** 操作者类型:与 DB 中 actor_type 列一致 */
export const ActorType = z.enum(["user", "agent", "system", "api_key"]);
export type ActorType = z.infer<typeof ActorType>;
/** 实体行的公共字段 */
export const EntityBase = z.object({
id: Id,
createdAt: Timestamp,
updatedAt: Timestamp,
});
/** 所有实体 Create 输入都要剔除的服务端字段 */
export const SERVER_MANAGED = { id: true, createdAt: true, updatedAt: true } as const;
/** 路径参数 `{id}` */
export const IdParam = z.object({ id: Id });
export type IdParam = z.infer<typeof IdParam>;