Make English the repository working language (#5)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Unified HTTP error: `{error: {code, message}}` response body,
|
||||
* with message in Chinese.
|
||||
* with message in English.
|
||||
*
|
||||
* Routes and services express business errors via throw HttpError; app-level onError
|
||||
* uniformly converges these into a JSON response, with unknown errors converged to 500
|
||||
@@ -40,7 +40,7 @@ export function modelCredentialMissing(modelId: string): HttpError {
|
||||
return new HttpError(
|
||||
400,
|
||||
"model_credential_missing",
|
||||
`模型 ${modelId} 还没有可用的 API key,请先在「模型」页为它配置。`,
|
||||
`Model ${modelId} has no API key yet. Configure it on the Models page first.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,6 @@ export function handleError(err: Error, c: Context): Response {
|
||||
return c.json(errorBody(err.code, err.message), err.status as 400);
|
||||
}
|
||||
// Unknown error: print the stack for diagnosis, but never expose details externally.
|
||||
console.error(`[server] 未处理异常: ${err.stack ?? err.message}`);
|
||||
return c.json(errorBody("internal", "服务器内部错误。"), 500);
|
||||
console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);
|
||||
return c.json(errorBody("internal", "Internal server error."), 500);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function adminUsersRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
if (!c.var.user.isAdmin) {
|
||||
throw new HttpError(403, "admin_required", "该操作仅管理员可执行。");
|
||||
throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ export function agentConfigRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
if (agentsMd !== undefined) req.agentsMd = agentsMd;
|
||||
if (body.config !== undefined) {
|
||||
if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
|
||||
throw badRequest("config 必须是对象。");
|
||||
throw badRequest("config must be an object.");
|
||||
}
|
||||
req.config = body.config as AgentConfigUpdateRequest["config"];
|
||||
}
|
||||
|
||||
@@ -42,10 +42,11 @@ export function agentTransferRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
try {
|
||||
archive = Buffer.from(dataBase64, "base64");
|
||||
} catch {
|
||||
throw badRequest("dataBase64 不是合法的 base64。");
|
||||
throw badRequest("dataBase64 is not valid base64.");
|
||||
}
|
||||
if (archive.byteLength === 0) throw badRequest("导入包为空。");
|
||||
if (archive.byteLength > MAX_ARCHIVE_BYTES) throw badRequest("导入包超过 14MB 上限。");
|
||||
if (archive.byteLength === 0) throw badRequest("Import package is empty.");
|
||||
if (archive.byteLength > MAX_ARCHIVE_BYTES)
|
||||
throw badRequest("Import package exceeds the 14MB limit.");
|
||||
const { version } = await deps.snapshots.importArchive(projectId, agentId, archive, confirm);
|
||||
const res: AgentImportResponse = { version };
|
||||
return c.json(res);
|
||||
|
||||
@@ -31,18 +31,22 @@ export function dirsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const raw = c.req.query("path");
|
||||
const target = raw && raw.trim() ? raw.trim() : os.homedir();
|
||||
if (!path.isAbsolute(target)) {
|
||||
throw new HttpError(400, "dir_not_absolute", "目录必须是绝对路径。");
|
||||
throw new HttpError(400, "dir_not_absolute", "Directory must be an absolute path.");
|
||||
}
|
||||
|
||||
let real: string;
|
||||
try {
|
||||
real = await fs.realpath(target);
|
||||
} catch {
|
||||
throw new HttpError(404, "dir_not_found", `目录不存在或不可访问:${target}。`);
|
||||
throw new HttpError(
|
||||
404,
|
||||
"dir_not_found",
|
||||
`Directory does not exist or is inaccessible: ${target}.`,
|
||||
);
|
||||
}
|
||||
const stat = await fs.stat(real);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new HttpError(400, "not_a_dir", "不是目录。");
|
||||
throw new HttpError(400, "not_a_dir", "Not a directory.");
|
||||
}
|
||||
|
||||
let dirents: import("node:fs").Dirent[] = [];
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { AppDeps } from "../../app.js";
|
||||
/** Validate a paired reference object ({ provider, modelId }); shape mismatch throws 400. */
|
||||
function parseRef(value: unknown, label: string): ModelRefDto {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw badRequest(`${label} 必须是 { provider, modelId } 对象。`);
|
||||
throw badRequest(`${label} must be a { provider, modelId } object.`);
|
||||
}
|
||||
const r = value as Record<string, unknown>;
|
||||
return {
|
||||
@@ -29,10 +29,10 @@ function parseRef(value: unknown, label: string): ModelRefDto {
|
||||
|
||||
/** Validate the PUT request body and shape it into a ModelsUpdateRequest (rejects any shape errors). */
|
||||
function parseModelsUpdate(body: Record<string, unknown>): ModelsUpdateRequest {
|
||||
if (!Array.isArray(body.models)) throw badRequest("models 必须是数组。");
|
||||
if (!Array.isArray(body.models)) throw badRequest("models must be an array.");
|
||||
const models: ModelUpdateEntry[] = body.models.map((item, i) => {
|
||||
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
||||
throw badRequest(`models[${i}] 必须是对象。`);
|
||||
throw badRequest(`models[${i}] must be an object.`);
|
||||
}
|
||||
const m = item as Record<string, unknown>;
|
||||
const entry: ModelUpdateEntry = {
|
||||
@@ -49,7 +49,7 @@ function parseModelsUpdate(body: Record<string, unknown>): ModelsUpdateRequest {
|
||||
};
|
||||
if (m.displayName !== undefined) {
|
||||
if (typeof m.displayName !== "string" || m.displayName.length > 100) {
|
||||
throw badRequest(`models[${i}].displayName 必须是长度不超过 100 的字符串。`);
|
||||
throw badRequest(`models[${i}].displayName must be a string of at most 100 characters.`);
|
||||
}
|
||||
if (m.displayName) entry.displayName = m.displayName;
|
||||
}
|
||||
@@ -59,32 +59,32 @@ function parseModelsUpdate(body: Record<string, unknown>): ModelsUpdateRequest {
|
||||
}
|
||||
if (m.contextWindow !== undefined) {
|
||||
if (typeof m.contextWindow !== "number" || !(m.contextWindow > 0)) {
|
||||
throw badRequest(`models[${i}].contextWindow 必须是正数。`);
|
||||
throw badRequest(`models[${i}].contextWindow must be a positive number.`);
|
||||
}
|
||||
entry.contextWindow = m.contextWindow;
|
||||
}
|
||||
if (m.clientType !== undefined) {
|
||||
if (typeof m.clientType !== "string" || m.clientType.length > 64) {
|
||||
throw badRequest(`models[${i}].clientType 必须是长度不超过 64 的字符串。`);
|
||||
throw badRequest(`models[${i}].clientType must be a string of at most 64 characters.`);
|
||||
}
|
||||
// An empty string is treated as "unspecified", leaving AgentHub to infer it from modelId.
|
||||
if (m.clientType) entry.clientType = m.clientType;
|
||||
}
|
||||
if (m.vision !== undefined) {
|
||||
if (typeof m.vision !== "boolean") {
|
||||
throw badRequest(`models[${i}].vision 必须是布尔值。`);
|
||||
throw badRequest(`models[${i}].vision must be a boolean.`);
|
||||
}
|
||||
entry.vision = m.vision;
|
||||
}
|
||||
if (m.pricing !== undefined) {
|
||||
const p = m.pricing as Record<string, unknown>;
|
||||
if (p === null || typeof p !== "object" || Array.isArray(p)) {
|
||||
throw badRequest(`models[${i}].pricing 必须是对象。`);
|
||||
throw badRequest(`models[${i}].pricing must be an object.`);
|
||||
}
|
||||
for (const key of ["cacheRead", "cacheWrite", "output"] as const) {
|
||||
const v = p[key];
|
||||
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
||||
throw badRequest(`models[${i}].pricing.${key} 必须是非负数字。`);
|
||||
throw badRequest(`models[${i}].pricing.${key} must be a non-negative number.`);
|
||||
}
|
||||
}
|
||||
entry.pricing = {
|
||||
@@ -95,19 +95,19 @@ function parseModelsUpdate(body: Record<string, unknown>): ModelsUpdateRequest {
|
||||
}
|
||||
if (m.apiKey !== undefined) {
|
||||
if (typeof m.apiKey !== "string" || m.apiKey.length === 0) {
|
||||
throw badRequest(`models[${i}].apiKey 必须是非空字符串。`);
|
||||
throw badRequest(`models[${i}].apiKey must be a non-empty string.`);
|
||||
}
|
||||
entry.apiKey = m.apiKey;
|
||||
}
|
||||
if (m.clearApiKey !== undefined) {
|
||||
if (typeof m.clearApiKey !== "boolean") {
|
||||
throw badRequest(`models[${i}].clearApiKey 必须是布尔值。`);
|
||||
throw badRequest(`models[${i}].clearApiKey must be a boolean.`);
|
||||
}
|
||||
entry.clearApiKey = m.clearApiKey;
|
||||
}
|
||||
if (m.baseUrl !== undefined) {
|
||||
if (m.baseUrl !== null && typeof m.baseUrl !== "string") {
|
||||
throw badRequest(`models[${i}].baseUrl 必须是字符串或 null。`);
|
||||
throw badRequest(`models[${i}].baseUrl must be a string or null.`);
|
||||
}
|
||||
entry.baseUrl = m.baseUrl as string | null;
|
||||
}
|
||||
@@ -153,22 +153,22 @@ export function modelsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
modelId: requireString(body, "modelId", { minLen: 1, maxLen: 200 }),
|
||||
};
|
||||
if (body.apiKey !== undefined) {
|
||||
if (typeof body.apiKey !== "string") throw badRequest("apiKey 必须是字符串。");
|
||||
if (typeof body.apiKey !== "string") throw badRequest("apiKey must be a string.");
|
||||
if (body.apiKey) req.apiKey = body.apiKey;
|
||||
}
|
||||
if (body.clearApiKey !== undefined) {
|
||||
if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey 必须是布尔值。");
|
||||
if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey must be a boolean.");
|
||||
req.clearApiKey = body.clearApiKey;
|
||||
}
|
||||
// null = explicit clear (test against the draft, don't fall back to the stored value); empty string is treated as null.
|
||||
if (body.baseUrl !== undefined) {
|
||||
if (body.baseUrl !== null && typeof body.baseUrl !== "string") {
|
||||
throw badRequest("baseUrl 必须是字符串或 null。");
|
||||
throw badRequest("baseUrl must be a string or null.");
|
||||
}
|
||||
req.baseUrl = body.baseUrl ? body.baseUrl : null;
|
||||
}
|
||||
if (body.clientType !== undefined) {
|
||||
if (typeof body.clientType !== "string") throw badRequest("clientType 必须是字符串。");
|
||||
if (typeof body.clientType !== "string") throw badRequest("clientType must be a string.");
|
||||
if (body.clientType) req.clientType = body.clientType;
|
||||
}
|
||||
return c.json(await deps.projectConfigService.testModel(projectId, req));
|
||||
|
||||
@@ -48,7 +48,7 @@ function parseUpsertBody(body: Record<string, unknown>): {
|
||||
modelId?: string;
|
||||
provider?: string;
|
||||
} {
|
||||
if (typeof body.enabled !== "boolean") throw badRequest("enabled 必须是布尔值。");
|
||||
if (typeof body.enabled !== "boolean") throw badRequest("enabled must be a boolean.");
|
||||
const prompt = requireString(body, "prompt", { minLen: 1, maxLen: 100_000 });
|
||||
const startAt = requireString(body, "startAt", { minLen: 1, maxLen: 100 });
|
||||
const period = optionalString(body, "period", { minLen: 1, maxLen: 20 });
|
||||
@@ -128,7 +128,7 @@ function toItem(
|
||||
|
||||
/** Schedule name in the path: same character rules as directories/files, validated before any path construction. */
|
||||
function requireScheduleName(raw: string | undefined): string {
|
||||
if (!raw || !isValidId(raw)) throw badRequest("定时任务名非法。");
|
||||
if (!raw || !isValidId(raw)) throw badRequest("Invalid schedule name.");
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function scheduleRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const body = await readJson(c);
|
||||
const name = requireScheduleName(requireString(body, "name", { minLen: 1, maxLen: 100 }));
|
||||
if (await readScheduleFile(deps.config.root, projectId, agentId, name)) {
|
||||
throw new HttpError(409, "schedule_exists", `定时任务已存在:${name}`);
|
||||
throw new HttpError(409, "schedule_exists", `Schedule already exists: ${name}`);
|
||||
}
|
||||
await upsert(deps, c.var.user.userId, projectId, agentId, name, body);
|
||||
return c.json(await readItem(deps, projectId, agentId, name), 201);
|
||||
@@ -178,7 +178,7 @@ export function scheduleRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
deps.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
||||
const name = requireScheduleName(c.req.param("name"));
|
||||
if (!(await readScheduleFile(deps.config.root, projectId, agentId, name))) {
|
||||
throw new HttpError(404, "schedule_not_found", `定时任务不存在:${name}`);
|
||||
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
||||
}
|
||||
const body = await readJson(c);
|
||||
await upsert(deps, c.var.user.userId, projectId, agentId, name, body);
|
||||
@@ -191,7 +191,8 @@ export function scheduleRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
deps.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
||||
const name = requireScheduleName(c.req.param("name"));
|
||||
const removed = await deleteScheduleFile(deps.config.root, projectId, agentId, name);
|
||||
if (!removed) throw new HttpError(404, "schedule_not_found", `定时任务不存在:${name}`);
|
||||
if (!removed)
|
||||
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
||||
deps.scheduler.dropEntry(projectId, agentId, name);
|
||||
return c.body(null, 204);
|
||||
});
|
||||
@@ -211,10 +212,10 @@ async function upsert(
|
||||
const fields = parseUpsertBody(body);
|
||||
const raw = serializeSchedule(fields);
|
||||
const parsed = parseScheduleFile(name, raw);
|
||||
if (!parsed.ok) throw badRequest(`定时任务配置非法:${parsed.error}`);
|
||||
if (!parsed.ok) throw badRequest(`Invalid schedule configuration: ${parsed.error}`);
|
||||
// At save time, verify the model reference resolves (resolveModelRef semantics; same rules as reconciliation) so we never persist a broken file.
|
||||
const refError = await validateScheduleModelRef(deps.config.root, projectId, parsed.def);
|
||||
if (refError !== null) throw badRequest(`定时任务配置非法:${refError}`);
|
||||
if (refError !== null) throw badRequest(`Invalid schedule configuration: ${refError}`);
|
||||
await writeScheduleFile(deps.config.root, projectId, agentId, name, raw);
|
||||
// Creator attribution: the API writer is the creator (falls back to the Project owner only for hand-edited files).
|
||||
deps.schedulesRepo.registerOrSync({
|
||||
@@ -238,6 +239,6 @@ async function readItem(
|
||||
const entry = entries.find((e) => e.def.name === name);
|
||||
if (entry) return toItem(entry.def, entry.state, entry.queued, Date.now());
|
||||
const bad = invalid.find((i) => i.name === name);
|
||||
if (bad) throw badRequest(`定时任务文件非法:${bad.error}`);
|
||||
throw new HttpError(404, "schedule_not_found", `定时任务不存在:${name}`);
|
||||
if (bad) throw badRequest(`Invalid schedule file: ${bad.error}`);
|
||||
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
||||
}
|
||||
|
||||
@@ -59,16 +59,16 @@ const APPROVAL_MODES: readonly ApprovalMode[] = [
|
||||
function parseTaskInput(body: Record<string, unknown>): OmniMessage[] {
|
||||
const input = body.input;
|
||||
if (!Array.isArray(input) || input.length === 0) {
|
||||
throw badRequest("input 必须是至少包含一项的数组。");
|
||||
throw badRequest("input must be an array with at least one item.");
|
||||
}
|
||||
return input.map((item, i) => {
|
||||
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
||||
throw badRequest(`input[${i}] 必须是对象。`);
|
||||
throw badRequest(`input[${i}] must be an object.`);
|
||||
}
|
||||
const part = item as Record<string, unknown>;
|
||||
if (part.type === "text") {
|
||||
if (typeof part.text !== "string" || part.text.length === 0) {
|
||||
throw badRequest(`input[${i}].text 必须是非空字符串。`);
|
||||
throw badRequest(`input[${i}].text must be a non-empty string.`);
|
||||
}
|
||||
return userText(part.text);
|
||||
}
|
||||
@@ -78,11 +78,11 @@ function parseTaskInput(body: Record<string, unknown>): OmniMessage[] {
|
||||
typeof url !== "string" ||
|
||||
!(url.startsWith("data:") || url.startsWith("http://") || url.startsWith("https://"))
|
||||
) {
|
||||
throw badRequest(`input[${i}].imageUrl 仅支持 data: 或 http(s) URL。`);
|
||||
throw badRequest(`input[${i}].imageUrl only supports data: or http(s) URLs.`);
|
||||
}
|
||||
return imageUrlMessage(url);
|
||||
}
|
||||
throw badRequest(`input[${i}].type 必须是 text / image_url 之一。`);
|
||||
throw badRequest(`input[${i}].type must be one of text / image_url.`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +110,9 @@ export function agentSessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const provider = optionalString(body, "provider", { minLen: 1, label: "provider" });
|
||||
// Model reference is submitted as a pair: provider can't appear without modelId (core does the same validation; this catches it early).
|
||||
if (provider !== undefined && modelId === undefined) {
|
||||
throw badRequest("指定了 provider 却未指定 modelId:模型引用须成对给出。");
|
||||
throw badRequest(
|
||||
"provider is specified but modelId is not: a model reference must be given as a pair.",
|
||||
);
|
||||
}
|
||||
const approvalMode = optionalEnum(body, "approvalMode", APPROVAL_MODES);
|
||||
let workspace = optionalString(body, "workspace", { minLen: 1, label: "workspace" });
|
||||
@@ -141,12 +143,20 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const sessionId = c.req.param("sessionId");
|
||||
const row = sessionId ? deps.sessionsRepo.findById(sessionId) : null;
|
||||
if (!row) {
|
||||
throw new HttpError(404, "session_not_found", "Session 不存在或无权访问。");
|
||||
throw new HttpError(
|
||||
404,
|
||||
"session_not_found",
|
||||
"Session does not exist or you do not have access.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, row.projectId);
|
||||
} catch {
|
||||
throw new HttpError(404, "session_not_found", "Session 不存在或无权访问。");
|
||||
throw new HttpError(
|
||||
404,
|
||||
"session_not_found",
|
||||
"Session does not exist or you do not have access.",
|
||||
);
|
||||
}
|
||||
return row;
|
||||
};
|
||||
@@ -167,15 +177,23 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
let title: string | undefined;
|
||||
if (titleRaw !== undefined) {
|
||||
if (typeof titleRaw !== "string") {
|
||||
throw new HttpError(400, "invalid_title", "title 必须是字符串。");
|
||||
throw new HttpError(400, "invalid_title", "title must be a string.");
|
||||
}
|
||||
title = titleRaw.trim();
|
||||
if (!title || title.length > SESSION_TITLE_MAX) {
|
||||
throw new HttpError(400, "invalid_title", `title 需为 1–${SESSION_TITLE_MAX} 个字符。`);
|
||||
throw new HttpError(
|
||||
400,
|
||||
"invalid_title",
|
||||
`title must be 1–${SESSION_TITLE_MAX} characters.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (approvalMode === undefined && archived === undefined && title === undefined) {
|
||||
throw new HttpError(400, "no_update", "缺少可更新字段(approvalMode / archived / title)。");
|
||||
throw new HttpError(
|
||||
400,
|
||||
"no_update",
|
||||
"No updatable field provided (approvalMode / archived / title).",
|
||||
);
|
||||
}
|
||||
let updated: SessionRow = { ...row };
|
||||
if (title !== undefined) {
|
||||
@@ -239,7 +257,7 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const row = resolveSession(c);
|
||||
const fileName = c.req.param("fileName") ?? "";
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(fileName) || fileName.includes("..")) {
|
||||
throw new HttpError(404, "file_not_found", "文件不存在。");
|
||||
throw new HttpError(404, "file_not_found", "File does not exist.");
|
||||
}
|
||||
const filePath = path.join(
|
||||
scratchpadDir(deps.config.root, row.projectId, row.agentId),
|
||||
@@ -250,7 +268,7 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
try {
|
||||
bytes = await fs.readFile(filePath);
|
||||
} catch {
|
||||
throw new HttpError(404, "file_not_found", "文件不存在。");
|
||||
throw new HttpError(404, "file_not_found", "File does not exist.");
|
||||
}
|
||||
const MIME_BY_EXT: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
@@ -309,7 +327,11 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const decision = requireEnum(body, "decision", ["allow", "deny"] as const);
|
||||
const ok = deps.manager.decideApproval(row.sessionId, pathParam(c, "toolCallId"), decision);
|
||||
if (!ok) {
|
||||
throw new HttpError(404, "approval_not_found", "该审批不存在或已被决定。");
|
||||
throw new HttpError(
|
||||
404,
|
||||
"approval_not_found",
|
||||
"Approval does not exist or has already been decided.",
|
||||
);
|
||||
}
|
||||
return c.body(null, 204);
|
||||
});
|
||||
@@ -374,7 +396,7 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
!paths.every((p) => typeof p === "string" && p.length <= STAT_MAX_PATH_LEN)
|
||||
) {
|
||||
throw badRequest(
|
||||
`paths 必须是字符串数组(≤${STAT_MAX_PATHS} 项,每项 ≤${STAT_MAX_PATH_LEN} 字符)。`,
|
||||
`paths must be an array of strings (≤${STAT_MAX_PATHS} items, each ≤${STAT_MAX_PATH_LEN} characters).`,
|
||||
);
|
||||
}
|
||||
const existing = await deps.workspaceFiles.statExisting(row.workspace, paths as string[]);
|
||||
@@ -386,11 +408,11 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const rel = c.req.query("path") ?? "";
|
||||
const body = await readJson(c);
|
||||
if (typeof body.dataBase64 !== "string") {
|
||||
throw badRequest("dataBase64 必须是 base64 字符串。");
|
||||
throw badRequest("dataBase64 must be a base64 string.");
|
||||
}
|
||||
const data = Buffer.from(body.dataBase64, "base64");
|
||||
if (data.length > MAX_UPLOAD_BYTES) {
|
||||
throw new HttpError(413, "file_too_large", "上传文件超过 14MB 上限。");
|
||||
throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
|
||||
}
|
||||
await deps.workspaceFiles.write(row.workspace, rel, data);
|
||||
return c.body(null, 204);
|
||||
|
||||
@@ -64,11 +64,11 @@ function libraryResponse(): SkillLibraryResponse {
|
||||
/** Validate the POST request body: names must be a non-empty array of strings. */
|
||||
function parseInstallNames(body: Record<string, unknown>): string[] {
|
||||
if (!Array.isArray(body.names) || body.names.length === 0) {
|
||||
throw badRequest("names 必须是非空数组。");
|
||||
throw badRequest("names must be a non-empty array.");
|
||||
}
|
||||
return body.names.map((v, i) => {
|
||||
if (typeof v !== "string" || v.length === 0) {
|
||||
throw badRequest(`names[${i}] 必须是非空字符串。`);
|
||||
throw badRequest(`names[${i}] must be a non-empty string.`);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
@@ -110,7 +110,7 @@ export function agentSkillsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
// Verify all names up front before writing anything: if any name isn't in the library, reject the whole request rather than leaving a half-installed state.
|
||||
const skills: LibrarySkill[] = names.map((name) => {
|
||||
const skill = librarySkill(name);
|
||||
if (!skill) throw new HttpError(404, "unknown_skill", `Skill 库中不存在:${name}`);
|
||||
if (!skill) throw new HttpError(404, "unknown_skill", `Skill is not in the library: ${name}`);
|
||||
return skill;
|
||||
});
|
||||
for (const skill of skills) {
|
||||
@@ -129,7 +129,7 @@ export function agentSkillsRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
try {
|
||||
await fs.access(file);
|
||||
} catch {
|
||||
throw new HttpError(404, "not_found", `Skill 未安装:${name}`);
|
||||
throw new HttpError(404, "not_found", `Skill is not installed: ${name}`);
|
||||
}
|
||||
await removeSkill(deps.config.root, projectId, agentId, name);
|
||||
return c.body(null, 204);
|
||||
|
||||
@@ -20,7 +20,7 @@ export function usageRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
||||
const groupByRaw = c.req.query("groupBy") ?? "date";
|
||||
if (!(GROUP_BYS as readonly string[]).includes(groupByRaw)) {
|
||||
throw badRequest(`groupBy 必须是 ${GROUP_BYS.join(" / ")} 之一。`);
|
||||
throw badRequest(`groupBy must be one of ${GROUP_BYS.join(" / ")}.`);
|
||||
}
|
||||
const from = optionalDateParam(c.req.query("from"), "from");
|
||||
const to = optionalDateParam(c.req.query("to"), "to");
|
||||
|
||||
@@ -11,10 +11,10 @@ import type { AppDeps } from "../../app.js";
|
||||
|
||||
/** Validate the PUT request body and shape it into a VaultUpdateRequest (semantic checks like key-name rules live in the service layer). */
|
||||
function parseVaultUpdate(body: Record<string, unknown>): VaultUpdateRequest {
|
||||
if (!Array.isArray(body.entries)) throw badRequest("entries 必须是数组。");
|
||||
if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
|
||||
const entries: VaultEntryUpdate[] = body.entries.map((item, i) => {
|
||||
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
||||
throw badRequest(`entries[${i}] 必须是对象。`);
|
||||
throw badRequest(`entries[${i}] must be an object.`);
|
||||
}
|
||||
const e = item as Record<string, unknown>;
|
||||
const entry: VaultEntryUpdate = {
|
||||
@@ -22,7 +22,7 @@ function parseVaultUpdate(body: Record<string, unknown>): VaultUpdateRequest {
|
||||
};
|
||||
if (e.value !== undefined) {
|
||||
if (typeof e.value !== "string" || e.value.length === 0) {
|
||||
throw badRequest(`entries[${i}].value 必须是非空字符串。`);
|
||||
throw badRequest(`entries[${i}].value must be a non-empty string.`);
|
||||
}
|
||||
entry.value = e.value;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function badRequest(message: string): HttpError {
|
||||
export function pathParam(c: Context, name: string): string {
|
||||
const v = c.req.param(name);
|
||||
if (v === undefined || v === "") {
|
||||
throw new HttpError(404, "not_found", "路径参数缺失。");
|
||||
throw new HttpError(404, "not_found", "Missing path parameter.");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function pathParam(c: Context, name: string): string {
|
||||
export function requireValidId(c: Context, name: string): string {
|
||||
const v = pathParam(c, name);
|
||||
if (!isValidId(v)) {
|
||||
throw new HttpError(404, "not_found", "资源不存在或无权访问。");
|
||||
throw new HttpError(404, "not_found", "Resource does not exist or you do not have access.");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function requireValidId(c: Context, name: string): string {
|
||||
/** Parse a positive-integer path parameter (e.g. Trace file index). */
|
||||
export function positiveIntParam(c: Context, name: string): number {
|
||||
const v = Number.parseInt(pathParam(c, name), 10);
|
||||
if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} 必须是正整数。`);
|
||||
if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,10 @@ export function positiveIntParam(c: Context, name: string): number {
|
||||
export function paginationQuery(c: Context): { offset: number; limit: number } {
|
||||
const offset = Number.parseInt(c.req.query("offset") ?? "0", 10);
|
||||
const limit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
||||
if (!Number.isInteger(offset) || offset < 0) throw badRequest("offset 必须是非负整数。");
|
||||
if (!Number.isInteger(offset) || offset < 0)
|
||||
throw badRequest("offset must be a non-negative integer.");
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
|
||||
throw badRequest("limit 必须是 1~1000 的整数。");
|
||||
throw badRequest("limit must be an integer between 1 and 1000.");
|
||||
}
|
||||
return { offset, limit };
|
||||
}
|
||||
@@ -64,10 +65,10 @@ export async function readJson(c: Context): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
throw badRequest("请求体必须是合法 JSON。");
|
||||
throw badRequest("Request body must be valid JSON.");
|
||||
}
|
||||
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
||||
throw badRequest("请求体必须是 JSON 对象。");
|
||||
throw badRequest("Request body must be a JSON object.");
|
||||
}
|
||||
return body as Record<string, unknown>;
|
||||
}
|
||||
@@ -87,15 +88,15 @@ export function requireString(
|
||||
): string {
|
||||
const v = obj[key];
|
||||
const label = rule.label ?? key;
|
||||
if (typeof v !== "string") throw badRequest(`${label} 必须是字符串。`);
|
||||
if (typeof v !== "string") throw badRequest(`${label} must be a string.`);
|
||||
if (rule.minLen !== undefined && v.length < rule.minLen) {
|
||||
throw badRequest(`${label} 长度至少 ${rule.minLen} 个字符。`);
|
||||
throw badRequest(`${label} must be at least ${rule.minLen} characters.`);
|
||||
}
|
||||
if (rule.maxLen !== undefined && v.length > rule.maxLen) {
|
||||
throw badRequest(`${label} 长度不能超过 ${rule.maxLen} 个字符。`);
|
||||
throw badRequest(`${label} must be at most ${rule.maxLen} characters.`);
|
||||
}
|
||||
if (rule.pattern !== undefined && !rule.pattern.test(v)) {
|
||||
throw badRequest(`${label} 格式不合法。`);
|
||||
throw badRequest(`${label} has an invalid format.`);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export function requireEnum<T extends string>(
|
||||
): T {
|
||||
const v = obj[key];
|
||||
if (typeof v !== "string" || !(values as readonly string[]).includes(v)) {
|
||||
throw badRequest(`${label} 必须是 ${values.join(" / ")} 之一。`);
|
||||
throw badRequest(`${label} must be one of ${values.join(" / ")}.`);
|
||||
}
|
||||
return v as T;
|
||||
}
|
||||
@@ -150,12 +151,12 @@ export function optionalNumber(
|
||||
const v = obj[key];
|
||||
if (v === undefined) return undefined;
|
||||
const label = rule.label ?? key;
|
||||
if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} 必须是数字。`);
|
||||
if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} 必须是整数。`);
|
||||
if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} must be a number.`);
|
||||
if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} must be an integer.`);
|
||||
if (rule.positiveOrMinusOne && !(v > 0 || v === -1)) {
|
||||
throw badRequest(`${label} 必须大于 0 或为 -1。`);
|
||||
throw badRequest(`${label} must be greater than 0 or equal to -1.`);
|
||||
}
|
||||
if (rule.nonNegative && v < 0) throw badRequest(`${label} 不能为负数。`);
|
||||
if (rule.nonNegative && v < 0) throw badRequest(`${label} must not be negative.`);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ export function optionalNumber(
|
||||
export function optionalDateParam(value: string | undefined, label: string): string | undefined {
|
||||
if (value === undefined || value === "") return undefined;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
throw badRequest(`${label} 必须是 YYYY-MM-DD 格式。`);
|
||||
throw badRequest(`${label} must be in YYYY-MM-DD format.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user