Make English the repository working language (#5)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,30 +82,31 @@ export function parseScheduleFile(name: string, raw: string): ScheduleParseResul
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `TOML 解析失败:${err instanceof Error ? err.message : String(err)}`,
|
||||
error: `Failed to parse TOML: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object")
|
||||
return { ok: false, error: "内容不是 TOML 表" };
|
||||
return { ok: false, error: "Content is not a TOML table" };
|
||||
const t = parsed as Record<string, unknown>;
|
||||
|
||||
const prompt = t["prompt"];
|
||||
if (typeof prompt !== "string" || prompt.trim() === "") {
|
||||
return { ok: false, error: "缺少必填字段 prompt" };
|
||||
return { ok: false, error: "Missing required field prompt" };
|
||||
}
|
||||
const enabled = t["enabled"] === undefined ? false : t["enabled"];
|
||||
if (typeof enabled !== "boolean") return { ok: false, error: "enabled 必须是布尔值" };
|
||||
if (typeof enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
|
||||
|
||||
const startAt = parseInstant(t["start_at"]);
|
||||
if (startAt === null) return { ok: false, error: "start_at 缺失或不是合法的 ISO 8601 时刻" };
|
||||
if (startAt === null)
|
||||
return { ok: false, error: "start_at is missing or not a valid ISO 8601 instant" };
|
||||
|
||||
let period: string | undefined;
|
||||
let periodMs: number | undefined;
|
||||
if (t["period"] !== undefined) {
|
||||
if (typeof t["period"] !== "string") return { ok: false, error: "period 必须是字符串" };
|
||||
if (typeof t["period"] !== "string") return { ok: false, error: "period must be a string" };
|
||||
const ms = parsePeriod(t["period"]);
|
||||
if (ms === null) return { ok: false, error: "period 必须形如 30m / 12h / 7d" };
|
||||
if (ms < MIN_PERIOD_MS) return { ok: false, error: "period 低于下限 5m" };
|
||||
if (ms === null) return { ok: false, error: "period must look like 30m / 12h / 7d" };
|
||||
if (ms < MIN_PERIOD_MS) return { ok: false, error: "period is below the 5m minimum" };
|
||||
period = t["period"].trim();
|
||||
periodMs = ms;
|
||||
}
|
||||
@@ -113,41 +114,46 @@ export function parseScheduleFile(name: string, raw: string): ScheduleParseResul
|
||||
let endAt: { ms: number; raw: string } | undefined;
|
||||
if (t["end_at"] !== undefined) {
|
||||
const parsedEnd = parseInstant(t["end_at"]);
|
||||
if (parsedEnd === null) return { ok: false, error: "end_at 不是合法的 ISO 8601 时刻" };
|
||||
if (parsedEnd.ms <= startAt.ms) return { ok: false, error: "end_at 必须晚于 start_at" };
|
||||
if (parsedEnd === null) return { ok: false, error: "end_at is not a valid ISO 8601 instant" };
|
||||
if (parsedEnd.ms <= startAt.ms)
|
||||
return { ok: false, error: "end_at must be later than start_at" };
|
||||
endAt = parsedEnd;
|
||||
}
|
||||
|
||||
let sessionId: string | undefined;
|
||||
if (t["session_id"] !== undefined) {
|
||||
if (typeof t["session_id"] !== "string" || t["session_id"] === "") {
|
||||
return { ok: false, error: "session_id 必须是非空字符串" };
|
||||
return { ok: false, error: "session_id must be a non-empty string" };
|
||||
}
|
||||
sessionId = t["session_id"];
|
||||
}
|
||||
let workspace: string | undefined;
|
||||
if (t["workspace"] !== undefined) {
|
||||
if (typeof t["workspace"] !== "string" || t["workspace"] === "") {
|
||||
return { ok: false, error: "workspace 必须是非空字符串" };
|
||||
return { ok: false, error: "workspace must be a non-empty string" };
|
||||
}
|
||||
workspace = t["workspace"];
|
||||
}
|
||||
let modelId: string | undefined;
|
||||
if (t["model_id"] !== undefined) {
|
||||
if (typeof t["model_id"] !== "string" || t["model_id"] === "") {
|
||||
return { ok: false, error: "model_id 必须是非空字符串" };
|
||||
return { ok: false, error: "model_id must be a non-empty string" };
|
||||
}
|
||||
modelId = t["model_id"];
|
||||
}
|
||||
let provider: string | undefined;
|
||||
if (t["provider"] !== undefined) {
|
||||
if (typeof t["provider"] !== "string" || t["provider"] === "") {
|
||||
return { ok: false, error: "provider 必须是非空字符串" };
|
||||
return { ok: false, error: "provider must be a non-empty string" };
|
||||
}
|
||||
provider = t["provider"];
|
||||
}
|
||||
if (provider !== undefined && modelId === undefined) {
|
||||
return { ok: false, error: "provider 仅与 model_id 成对使用(模型引用须成对给出)" };
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"provider is only used together with model_id (a model reference must be given as a pair)",
|
||||
};
|
||||
}
|
||||
if (
|
||||
sessionId !== undefined &&
|
||||
@@ -155,7 +161,7 @@ export function parseScheduleFile(name: string, raw: string): ScheduleParseResul
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "目标二选一:workspace 与 provider / model_id 仅用于新建 Session 模式",
|
||||
error: "Pick one target: workspace and provider / model_id are only for new-Session mode",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,10 @@ import type { UsageContext } from "./usage-recorder.js";
|
||||
/** 409 for when there's nothing to compact: give the specific reason rather than a one-size-fits-none message. */
|
||||
function compactUnavailable(why: Exclude<CompactAvailability, "ok">): HttpError {
|
||||
const messages: Record<typeof why, string> = {
|
||||
unsupported: "该 Agent 未配置上下文压缩能力。",
|
||||
empty: "当前上下文没有可压缩的内容(尚无已完成的对话轮次)。",
|
||||
just_compacted: "上下文刚压缩过,此后还没有新的对话,无需再次压缩。",
|
||||
unsupported: "This Agent does not have context compaction configured.",
|
||||
empty: "The current context has nothing to compact (no completed conversation turns yet).",
|
||||
just_compacted:
|
||||
"The context was just compacted and there is no new conversation since; no need to compact again.",
|
||||
};
|
||||
return new HttpError(409, "nothing_to_compact", messages[why]);
|
||||
}
|
||||
@@ -129,7 +130,7 @@ export function createCoreSessionLoader(root: string): SessionLoader {
|
||||
throw new HttpError(
|
||||
409,
|
||||
"workspace_missing",
|
||||
`该 Session 的 Workspace 已不存在:${row.workspace},无法继续。请新建 Session。`,
|
||||
`This Session's Workspace no longer exists: ${row.workspace}, so it cannot continue. Create a new Session.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -533,7 +534,11 @@ export class SessionManager {
|
||||
|
||||
private assertOpen(): void {
|
||||
if (this.closed) {
|
||||
throw new HttpError(503, "shutting_down", "服务端正在关停,暂不接受新任务。");
|
||||
throw new HttpError(
|
||||
503,
|
||||
"shutting_down",
|
||||
"Server is shutting down; not accepting new Tasks.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,23 +546,35 @@ export class SessionManager {
|
||||
private assertAgentNotDeleting(sessionId: string): void {
|
||||
const row = this.deps.sessions.findById(sessionId);
|
||||
if (row && this.deletingAgents.has(agentKey(row.projectId, row.agentId))) {
|
||||
throw new HttpError(409, "agent_deleting", "该 Agent 正在删除,暂不接受新任务。");
|
||||
throw new HttpError(
|
||||
409,
|
||||
"agent_deleting",
|
||||
"This Agent is being deleted; not accepting new Tasks.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
|
||||
private assertSessionNotDeleting(sessionId: string): void {
|
||||
if (this.deletingSessions.has(sessionId)) {
|
||||
throw new HttpError(409, "session_deleting", "该 Session 正在删除,暂不接受新任务。");
|
||||
throw new HttpError(
|
||||
409,
|
||||
"session_deleting",
|
||||
"This Session is being deleted; not accepting new Tasks.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private assertIdle(entry: RuntimeEntry): void {
|
||||
if (entry.status === "running") {
|
||||
throw new HttpError(409, "task_in_progress", "该 Session 已有进行中的 Task。");
|
||||
throw new HttpError(409, "task_in_progress", "This Session already has a Task in progress.");
|
||||
}
|
||||
if (entry.status === "compacting") {
|
||||
throw new HttpError(409, "compacting", "该 Session 正在压缩上下文,暂不接受新输入。");
|
||||
throw new HttpError(
|
||||
409,
|
||||
"compacting",
|
||||
"This Session is compacting its context; not accepting new input.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +584,11 @@ export class SessionManager {
|
||||
if (existing) return existing;
|
||||
const row = this.deps.sessions.findById(sessionId);
|
||||
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.",
|
||||
);
|
||||
}
|
||||
const session = await this.deps.loader.load(row);
|
||||
// The Session/Agent was marked for deletion while loading: discard the load result,
|
||||
@@ -676,7 +697,7 @@ export class SessionManager {
|
||||
}
|
||||
} catch (err) {
|
||||
this.log(
|
||||
`[subagent] 子会话登记失败: ${err instanceof Error ? err.message : String(err)}`,
|
||||
`[subagent] Failed to register child session: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
this.deps.errors?.record({
|
||||
source: "subagent",
|
||||
@@ -702,7 +723,7 @@ export class SessionManager {
|
||||
try {
|
||||
await this.deps.recorder.record(ctx, msg);
|
||||
} catch (err) {
|
||||
this.log(`[usage] 落库失败: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.log(`[usage] Insert failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.deps.errors?.record({ source: "usage", err, ctx, code: "usage_insert_failed" });
|
||||
}
|
||||
}
|
||||
@@ -710,7 +731,7 @@ export class SessionManager {
|
||||
// The SDK doesn't normally throw (errors are converged into the message stream);
|
||||
// this is a defensive record here to avoid crashing the runtime.
|
||||
this.log(
|
||||
`[session] 运行异常: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`,
|
||||
`[session] Run failed: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`,
|
||||
);
|
||||
this.deps.errors?.record({ source: "session", err, ctx, code: "session_run_failed" });
|
||||
} finally {
|
||||
|
||||
@@ -75,12 +75,16 @@ type LlmFailure = "failed" | "timeout" | "malformed";
|
||||
|
||||
/** LLM failure state → error code, classification, and fallback message (used when the abort reason isn't available). */
|
||||
const LLM_FAILURES: Record<LlmFailure, { code: string; kind: ErrorKind; text: string }> = {
|
||||
failed: { code: "llm_failed", kind: "unexpected", text: "LLM 请求失败(不可重试)。" },
|
||||
timeout: { code: "llm_timeout", kind: "expected", text: "LLM 请求超时(引擎重连重试)。" },
|
||||
failed: { code: "llm_failed", kind: "unexpected", text: "LLM request failed (not retryable)." },
|
||||
timeout: {
|
||||
code: "llm_timeout",
|
||||
kind: "expected",
|
||||
text: "LLM request timed out (the engine reconnects and retries).",
|
||||
},
|
||||
malformed: {
|
||||
code: "llm_malformed",
|
||||
kind: "expected",
|
||||
text: "LLM 响应无法解析(引擎重连重试)。",
|
||||
text: "LLM response could not be parsed (the engine reconnects and retries).",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -113,7 +117,7 @@ function originKey(msg: OmniMessage): string {
|
||||
* drop the reason.
|
||||
*/
|
||||
function toolFailureText(output: string): string {
|
||||
if (!output) return "工具执行失败(无输出)。";
|
||||
if (!output) return "Tool execution failed (no output).";
|
||||
if (output.length <= MESSAGE_MAX) return output;
|
||||
return `…${output.slice(output.length - (MESSAGE_MAX - 1))}`;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class TitleGenerator implements TitleNotifier {
|
||||
this.inflight.add(ctx.sessionId);
|
||||
void this.generate(ctx, session, req)
|
||||
.catch((err: unknown) => {
|
||||
this.log(`[title] 生成失败: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.log(`[title] Generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.deps.errors?.record({ source: "title", err, ctx, code: "title_failed" });
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -97,7 +97,9 @@ export class TitleGenerator implements TitleNotifier {
|
||||
try {
|
||||
await this.deps.recorder.record(ctx, tokenUsage(emptyTokenCounts(), res.usage));
|
||||
} catch (err) {
|
||||
this.log(`[title] 用量落库失败: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.log(
|
||||
`[title] Usage insert failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
this.deps.errors?.record({
|
||||
source: "title",
|
||||
err,
|
||||
@@ -109,7 +111,7 @@ export class TitleGenerator implements TitleNotifier {
|
||||
} catch (err) {
|
||||
// A model request error (rate limit / timeout / network, etc.) shouldn't leave the
|
||||
// title permanently missing: log it and fall through to the fallback.
|
||||
this.log(`[title] 模型请求失败: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.log(`[title] Model request failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
this.deps.errors?.record({ source: "title", err, ctx, code: "title_llm_failed" });
|
||||
}
|
||||
// When the LLM produces no usable title (failure / empty result), truncate the fallback material's first line — this guarantees a title is always generated.
|
||||
|
||||
Reference in New Issue
Block a user