Make English the repository working language (#5)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-20 17:45:02 +08:00
committed by GitHub
parent e80cde0ea6
commit 0eaaa54e25
136 changed files with 1515 additions and 1352 deletions
+11 -7
View File
@@ -41,13 +41,17 @@ export class AdminService {
async createUser(userId: string, password: string): Promise<UserInfo> {
if (!USERNAME_PATTERN.test(userId)) {
throw new HttpError(400, "invalid_user_id", `用户名须为 2~32 位:${SEMANTIC_ID_RULE}。`);
throw new HttpError(
400,
"invalid_user_id",
`Username must be 2–32 characters: ${SEMANTIC_ID_RULE}.`,
);
}
if (password.length < MIN_PASSWORD_LENGTH) {
throw new HttpError(400, "invalid_password", "密码至少 8 个字符。");
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
}
if (this.deps.users.findById(userId)) {
throw new HttpError(409, "user_exists", `用户已存在:${userId}。`);
throw new HttpError(409, "user_exists", `User already exists: ${userId}.`);
}
const user: UserRow = {
userId,
@@ -70,10 +74,10 @@ export class AdminService {
/** Reset another user's password: flags it as initial and clears all their sessions (prompts a password change on next login). */
async resetPassword(userId: string, password: string): Promise<void> {
if (!this.deps.users.findById(userId)) {
throw new HttpError(404, "user_not_found", `用户不存在:${userId}。`);
throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
}
if (password.length < MIN_PASSWORD_LENGTH) {
throw new HttpError(400, "invalid_password", "密码至少 8 个字符。");
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
}
this.deps.users.updatePassword(userId, await hashPassword(password), true);
this.deps.authSessions.deleteByUser(userId);
@@ -83,10 +87,10 @@ export class AdminService {
async deleteUser(userId: string): Promise<void> {
const target = this.deps.users.findById(userId);
if (!target) {
throw new HttpError(404, "user_not_found", `用户不存在:${userId}。`);
throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
}
if (target.isAdmin) {
throw new HttpError(409, "cannot_delete_admin", "内置管理员不能删除。");
throw new HttpError(409, "cannot_delete_admin", "The built-in admin cannot be deleted.");
}
for (const project of this.deps.projects.listByOwner(userId)) {
await this.deps.projectService.destroyProject(project.projectId);
@@ -68,7 +68,7 @@ export class AgentConfigService {
async requireExists(projectId: string, agentId: string): Promise<void> {
if (!(await this.exists(projectId, agentId))) {
throw new HttpError(404, "agent_not_found", "Agent 不存在。");
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
}
}
@@ -269,11 +269,11 @@ export class AgentConfigService {
for (const entry of req.entries) {
if (!isValidVaultKey(entry.key)) {
throw badRequest(
`vault 键名不合法:${entry.key}(仅字母、数字与下划线,且不能以数字开头)。`,
`Invalid vault key name: ${entry.key} (letters, digits and underscores only, and must not start with a digit).`,
);
}
if (seen.has(entry.key)) {
throw badRequest(`entries 中存在重复的键名:${entry.key}。`);
throw badRequest(`entries contains a duplicate key: ${entry.key}.`);
}
seen.add(entry.key);
const prevValue = prev[entry.key];
@@ -281,13 +281,15 @@ export class AgentConfigService {
// Values are injected into the child process environment: an oversized value would
// make exec spawn fail (E2BIG), so we reject it on write (same limit as core).
if (entry.value.length > VAULT_VALUE_MAX_LENGTH) {
throw badRequest(`vault 值过长:${entry.key}(上限 ${VAULT_VALUE_MAX_LENGTH} 字符)。`);
throw badRequest(
`Vault value too long: ${entry.key} (limit ${VAULT_VALUE_MAX_LENGTH} characters).`,
);
}
nextVault[entry.key] = entry.value;
} else if (prevValue !== undefined) {
nextVault[entry.key] = prevValue;
} else {
throw badRequest(`新增键 ${entry.key} 必须提供 value。`);
throw badRequest(`New key ${entry.key} must provide a value.`);
}
}
@@ -297,20 +299,20 @@ export class AgentConfigService {
}
function validateToolsBuiltin(value: unknown): ToolDefinitionConfig[] {
if (!Array.isArray(value)) throw badRequest("toolsBuiltin 必须是数组。");
if (!Array.isArray(value)) throw badRequest("toolsBuiltin must be an array.");
return value.map((item, i) => {
const t = asRecord(item);
if (typeof t.name !== "string" || t.name.length === 0) {
throw badRequest(`toolsBuiltin[${i}].name 必须是非空字符串。`);
throw badRequest(`toolsBuiltin[${i}].name must be a non-empty string.`);
}
if (typeof t.description !== "string") {
throw badRequest(`toolsBuiltin[${i}].description 必须是字符串。`);
throw badRequest(`toolsBuiltin[${i}].description must be a string.`);
}
if (t.permission !== undefined && t.permission !== "r" && t.permission !== "rw") {
throw badRequest(`toolsBuiltin[${i}].permission 必须是 r / rw 之一。`);
throw badRequest(`toolsBuiltin[${i}].permission must be one of r / rw.`);
}
if (t.forModel !== undefined && t.forModel !== "vision" && t.forModel !== "text-only") {
throw badRequest(`toolsBuiltin[${i}].forModel 必须是 vision / text-only 之一。`);
throw badRequest(`toolsBuiltin[${i}].forModel must be one of vision / text-only.`);
}
optionalNumber(t, "timeoutMs", {
integer: true,
@@ -327,14 +329,14 @@ function validateToolsBuiltin(value: unknown): ToolDefinitionConfig[] {
}
function validateMcpServers(value: unknown): MCPServerConfig[] {
if (!Array.isArray(value)) throw badRequest("mcpServers 必须是数组。");
if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
return value.map((item, i) => {
const s = asRecord(item);
if (typeof s.name !== "string" || s.name.length === 0) {
throw badRequest(`mcpServers[${i}].name 必须是非空字符串。`);
throw badRequest(`mcpServers[${i}].name must be a non-empty string.`);
}
if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
throw badRequest(`mcpServers[${i}].config 必须是对象。`);
throw badRequest(`mcpServers[${i}].config must be an object.`);
}
return s as unknown as MCPServerConfig;
});
@@ -155,7 +155,7 @@ export class AgentService {
throw new HttpError(
409,
"cannot_delete_builtin_agent",
"内置 Agent(default_agent)随 Project 供给,不能从 Web 删除。",
"Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web.",
);
}
await fs.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
@@ -175,7 +175,11 @@ export class AgentService {
description?: string,
): Promise<AgentListItem> {
if (!SEMANTIC_ID_PATTERN.test(agentId)) {
throw new HttpError(400, "invalid_agent_id", `Agent id 须为 2~64 位:${SEMANTIC_ID_RULE}。`);
throw new HttpError(
400,
"invalid_agent_id",
`Agent id must be 2–64 characters: ${SEMANTIC_ID_RULE}.`,
);
}
const taken =
this.agents.exists(projectId, agentId) ||
@@ -184,7 +188,7 @@ export class AgentService {
() => false,
));
if (taken) {
throw new HttpError(409, "agent_exists", `Agent id 已被占用:${agentId}。`);
throw new HttpError(409, "agent_exists", `Agent id is already taken: ${agentId}.`);
}
const displayName = name ?? agentId;
await coreCreateAgent({ root: this.root, projectId, agentId });
+2 -1
View File
@@ -31,4 +31,5 @@ export const PROJECT_SUFFIX_PATTERN = /^[a-z0-9_]+$/;
export const PROJECT_ID_MAX_LENGTH = 64;
/** Human-readable description of the rule (reused in error messages). */
export const SEMANTIC_ID_RULE = "小写字母开头,仅小写字母、数字与下划线";
export const SEMANTIC_ID_RULE =
"starts with a lowercase letter; lowercase letters, digits and underscores only";
@@ -364,7 +364,7 @@ export class ProjectConfigService {
const key = refKey(entry.provider, entry.modelId);
if (seen.has(key)) {
throw badRequest(
`models 中存在重复的模型引用:${showRef(entry.provider, entry.modelId)}。`,
`models contains a duplicate model reference: ${showRef(entry.provider, entry.modelId)}.`,
);
}
seen.add(key);
@@ -436,7 +436,7 @@ export class ProjectConfigService {
if (req.defaultModel !== undefined) {
if (!seen.has(refKey(req.defaultModel.provider, req.defaultModel.modelId))) {
throw badRequest(
`defaultModel 必须包含在 models 内:${showRef(req.defaultModel.provider, req.defaultModel.modelId)}。`,
`defaultModel must be included in models: ${showRef(req.defaultModel.provider, req.defaultModel.modelId)}.`,
);
}
defaultModel = req.defaultModel;
@@ -459,12 +459,12 @@ export class ProjectConfigService {
if (req.visionModel !== undefined) {
if (!seen.has(refKey(req.visionModel.provider, req.visionModel.modelId))) {
throw badRequest(
`visionModel 必须包含在 models 内:${showRef(req.visionModel.provider, req.visionModel.modelId)}。`,
`visionModel must be included in models: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`,
);
}
if (targetOf(req.visionModel)?.vision === false) {
throw badRequest(
`visionModel 不能指向标注为不支持图片的模型:${showRef(req.visionModel.provider, req.visionModel.modelId)}。`,
`visionModel must not point to a model annotated as not supporting images: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`,
);
}
visionModel = req.visionModel;
+28 -12
View File
@@ -84,7 +84,11 @@ export class ProjectService {
requireProjectAccess(userId: string, projectId: string): ProjectRow & { role: ProjectRole } {
const row = this.resolveAccess(userId, projectId);
if (!row) {
throw new HttpError(404, "project_not_found", "Project 不存在或无权访问。");
throw new HttpError(
404,
"project_not_found",
"Project does not exist or you do not have access.",
);
}
return row;
}
@@ -103,7 +107,11 @@ export class ProjectService {
requireProjectOwner(userId: string, projectId: string): ProjectRow {
const row = this.requireProjectAccess(userId, projectId);
if (row.role !== "owner") {
throw new HttpError(403, "owner_required", "该操作仅 Project owner 可执行。");
throw new HttpError(
403,
"owner_required",
"Only the Project owner can perform this operation.",
);
}
return row;
}
@@ -149,7 +157,7 @@ export class ProjectService {
throw new HttpError(
400,
"invalid_project_id",
`Project id 须为 2~64 位:${SEMANTIC_ID_RULE}(连字符保留作用户命名空间分隔)。`,
`Project id must be 2–64 characters: ${SEMANTIC_ID_RULE} (the hyphen is reserved as the user namespace separator).`,
);
}
} else {
@@ -159,7 +167,7 @@ export class ProjectService {
throw new HttpError(
400,
"project_id_prefix_required",
`Project id 须以 ${prefix} 开头,后接小写字母、数字或下划线(总长不超过 ${PROJECT_ID_MAX_LENGTH})。`,
`Project id must start with ${prefix}, followed by lowercase letters, digits or underscores (at most ${PROJECT_ID_MAX_LENGTH} in total).`,
);
}
}
@@ -167,7 +175,7 @@ export class ProjectService {
this.deps.projects.findById(projectId) !== null ||
(await dirExists(projectDir(this.deps.root, projectId)))
) {
throw new HttpError(409, "project_exists", `Project id 已被占用:${projectId}。`);
throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
}
const displayName = name ?? projectId;
const createdAt = new Date().toISOString();
@@ -179,7 +187,7 @@ export class ProjectService {
this.deps.projects.insert({ projectId, ownerUserId: owner.userId, createdAt });
} catch (err) {
if (err instanceof Error && err.message.includes("UNIQUE")) {
throw new HttpError(409, "project_exists", `Project id 已被占用:${projectId}。`);
throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
}
throw err;
}
@@ -248,14 +256,14 @@ export class ProjectService {
throw new HttpError(
409,
"cannot_delete_default_project",
"default_project 与 CLI 共用,不能从 Web 删除。",
"default_project is shared with the CLI and cannot be deleted from the web.",
);
}
if (this.deps.projects.listAccessible(userId).length <= 1) {
throw new HttpError(
409,
"cannot_delete_last_project",
"这是当前账号最后一个 Project,删除后将无 Project 可用;请先创建新的 Project。",
"This is the account's last Project; deleting it would leave no Project available. Create a new Project first.",
);
}
await this.destroyProject(projectId);
@@ -306,13 +314,21 @@ export class ProjectService {
const project = this.requireProjectOwner(userId, projectId);
const target = this.deps.users.findById(targetUserId);
if (!target) {
throw new HttpError(404, "user_not_found", `用户不存在:${targetUserId}。`);
throw new HttpError(404, "user_not_found", `User does not exist: ${targetUserId}.`);
}
if (target.userId === project.ownerUserId) {
throw new HttpError(409, "already_owner", "owner 无需授权给自己。");
throw new HttpError(
409,
"already_owner",
"The owner does not need to grant access to themselves.",
);
}
if (this.deps.members.isMember(projectId, target.userId)) {
throw new HttpError(409, "already_member", `${targetUserId} 已是该 Project 的成员。`);
throw new HttpError(
409,
"already_member",
`${targetUserId} is already a member of this Project.`,
);
}
const createdAt = new Date().toISOString();
this.deps.members.insert({ projectId, userId: target.userId, createdAt });
@@ -323,7 +339,7 @@ export class ProjectService {
removeMember(userId: string, projectId: string, targetUserId: string): void {
this.requireProjectOwner(userId, projectId);
if (!this.deps.members.isMember(projectId, targetUserId)) {
throw new HttpError(404, "member_not_found", `该 Project 没有成员:${targetUserId}。`);
throw new HttpError(404, "member_not_found", `This Project has no member: ${targetUserId}.`);
}
this.deps.members.delete(projectId, targetUserId);
}
@@ -168,7 +168,7 @@ export class SessionService {
throw new HttpError(
400,
"no_default_model",
"该 Project 尚未配置默认模型,请先在「模型」页添加模型并设为默认。",
"This Project has no default model yet. Add a model on the Models page and set it as the default first.",
);
}
modelId = def.model_id;
@@ -42,7 +42,7 @@ export class SnapshotService {
try {
raw = await fs.readFile(systemConfigPath(this.root, projectId, agentId), "utf8");
} catch {
throw new HttpError(404, "agent_not_found", "Agent 不存在。");
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
}
const parsed = parseYaml(raw) as { version?: unknown } | null;
return agentStateVersion({
@@ -117,7 +117,7 @@ export class SnapshotService {
filter: (p) => !isVaultEntry(p),
});
} catch {
throw badRequest("导入失败:不是合法的 tar.gz 快照包。");
throw badRequest("Import failed: not a valid tar.gz snapshot package.");
}
// Validation: the package must contain agent_state/system_config.yaml, and version must be valid.
@@ -126,21 +126,21 @@ export class SnapshotService {
try {
parsed = parseYaml(await fs.readFile(configPath, "utf8"));
} catch {
throw badRequest("导入失败:包内缺少 agent_state/system_config.yaml。");
throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
}
if (
parsed === null ||
typeof parsed !== "object" ||
typeof (parsed as { system_prompt?: unknown }).system_prompt !== "string"
) {
throw badRequest("导入失败:包内 system_config.yaml 非法。");
throw badRequest("Import failed: the package's system_config.yaml is invalid.");
}
const incomingRaw = (parsed as { version?: unknown }).version;
if (
incomingRaw !== undefined &&
(!Number.isInteger(incomingRaw) || (incomingRaw as number) < 1)
) {
throw badRequest("导入失败:包内 version 非法。");
throw badRequest("Import failed: the package's version is invalid.");
}
const incoming = agentStateVersion({
version: typeof incomingRaw === "number" ? incomingRaw : undefined,
@@ -149,7 +149,7 @@ export class SnapshotService {
throw new HttpError(
409,
"version_conflict",
`包版本 v${incoming} 不高于当前 v${current},需要确认后导入。`,
`Package version v${incoming} is not higher than the current v${current}; confirmation is required to import.`,
);
}
@@ -675,7 +675,7 @@ export class TraceService {
throw new HttpError(
404,
"trace_not_found",
`该 Session 没有 index 为 ${index} 的 Trace 文件。`,
`This Session has no Trace file with index ${index}.`,
);
}
return readTraceTolerant(file.path);
@@ -56,7 +56,7 @@ export class WorkspaceFilesService {
try {
return await fs.realpath(path.resolve(workspace));
} catch {
throw new HttpError(404, "workspace_missing", "该 Session 的 Workspace 已不存在。");
throw new HttpError(404, "workspace_missing", "This Session's Workspace no longer exists.");
}
}
@@ -74,17 +74,17 @@ export class WorkspaceFilesService {
/** Lexical prefix check (a relative path, once resolved, must still be inside the Workspace); returns the absolute target path. */
private lexicalTarget(base: string, rel: string): string {
if (rel.includes("\0")) throw badRequest("path 非法。");
if (rel.includes("\0")) throw badRequest("path is invalid.");
const target = path.resolve(base, rel === "" ? "." : rel);
if (!this.isInside(target, base)) {
throw badRequest("path 必须位于 Workspace 内。");
throw badRequest("path must be inside the Workspace.");
}
return target;
}
private assertInside(real: string, realBase: string): void {
if (!this.isInside(real, realBase)) {
throw badRequest("path 必须位于 Workspace 内。");
throw badRequest("path must be inside the Workspace.");
}
}
@@ -103,7 +103,7 @@ export class WorkspaceFilesService {
canonical = await fs.realpath(target);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
throw new HttpError(404, "path_not_found", "文件不存在。");
throw new HttpError(404, "path_not_found", "File does not exist.");
}
throw err;
}
@@ -132,7 +132,7 @@ export class WorkspaceFilesService {
const realBase = await this.realBase(workspace);
const target = this.lexicalTarget(path.resolve(workspace), rel);
const name = path.basename(target);
if (name === "" || name === "." || name === "..") throw badRequest("path 必须是文件路径。");
if (name === "" || name === "." || name === "..") throw badRequest("path must be a file path.");
const parent = path.dirname(target);
let canonicalParent: string;
try {
@@ -147,7 +147,7 @@ export class WorkspaceFilesService {
} catch (probeErr) {
if ((probeErr as NodeJS.ErrnoException).code !== "ENOENT") throw probeErr;
const up = path.dirname(probe);
if (up === probe) throw badRequest("path 非法。");
if (up === probe) throw badRequest("path is invalid.");
probe = up;
}
}
@@ -189,10 +189,10 @@ export class WorkspaceFilesService {
dirents = await fs.readdir(dir, { withFileTypes: true });
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
throw new HttpError(404, "path_not_found", "目录不存在。");
throw new HttpError(404, "path_not_found", "Directory does not exist.");
}
if ((err as NodeJS.ErrnoException).code === "ENOTDIR") {
throw badRequest("path 不是目录。");
throw badRequest("path is not a directory.");
}
throw err;
}
@@ -231,11 +231,11 @@ export class WorkspaceFilesService {
try {
stat = await fs.stat(file);
} catch {
throw new HttpError(404, "path_not_found", "文件不存在。");
throw new HttpError(404, "path_not_found", "File does not exist.");
}
if (stat.isDirectory()) throw badRequest("path 是目录。");
if (stat.isDirectory()) throw badRequest("path is a directory.");
if (stat.size > MAX_READ_BYTES) {
throw new HttpError(413, "file_too_large", "文件超过 50MB 读取上限。");
throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
}
const data = await fs.readFile(file);
const ext = path.extname(file).toLowerCase();
@@ -256,9 +256,9 @@ export class WorkspaceFilesService {
* sandbox escapes).
*/
async write(workspace: string, rel: string, data: Buffer): Promise<void> {
if (rel === "" || rel.endsWith("/")) throw badRequest("path 必须是文件路径。");
if (rel === "" || rel.endsWith("/")) throw badRequest("path must be a file path.");
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.");
}
const { dir, name } = await this.resolveWriteParent(workspace, rel);
const file = path.join(dir, name);
@@ -269,9 +269,10 @@ export class WorkspaceFilesService {
handle = await fs.open(file, flags, 0o644);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "ELOOP") throw badRequest("path 不能是符号链接。");
if (code === "ENOENT") throw new HttpError(404, "path_not_found", "父目录不存在。");
if (code === "EISDIR") throw badRequest("path 是目录。");
if (code === "ELOOP") throw badRequest("path must not be a symlink.");
if (code === "ENOENT")
throw new HttpError(404, "path_not_found", "Parent directory does not exist.");
if (code === "EISDIR") throw badRequest("path is a directory.");
throw err;
}
try {
@@ -26,12 +26,16 @@ export async function assertWorkspaceAllowed(args: { workspace: string }): Promi
throw new HttpError(
400,
"workspace_not_found",
`Workspace 不存在或不可访问:${args.workspace}。请指定一个已存在的目录,或留空以使用临时目录。`,
`Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary directory.`,
);
}
const stat = await fs.stat(ws);
if (!stat.isDirectory()) {
throw new HttpError(400, "workspace_not_found", `Workspace 不是目录:${args.workspace}。`);
throw new HttpError(
400,
"workspace_not_found",
`Workspace is not a directory: ${args.workspace}.`,
);
}
return ws;
}