Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/agents/error-hint-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ runtime/create-cli.ts: 每次调用创建 reporter,统一捕获初始化/解
↓
runtime/error-handler.ts:
- 服务端错误(BailianError(GENERAL)) → text 直接打 message
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 errorHint(只 AUTH 还有增强)
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 errorHint(AUTH 增强;另可对已知可恢复服务端错误追加 hint)
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
- Node fs errno → 翻成 GENERAL + errno hint
- reporter:JSON 错误、warning、debug 各占 stderr 一行,无空行;stdout 仅保留最终业务结果
Expand Down Expand Up @@ -104,8 +104,13 @@ process.exit(err.exitCode)

### B. runtime 增强(`errorHint`)

- [ ] `packages/runtime/src/error-handler.ts:errorHint`:**当前只为 internal AUTH 增强**(因为 resolver / authStage 等内部位置会发 AUTH)
- [ ] `packages/runtime/src/error-handler.ts:errorHint`:**主要为 internal AUTH 增强**(resolver / authStage 等内部位置会发 AUTH)
- [ ] **允许的有限扩展**:对已知可恢复的服务端错误,**追加**恢复 hint(保留原 message / exitCode)。当前仅 `AllocationQuota.FreeTierOnly`(及同义英文 message 短语)。message / apiCode 匹配**只用于追加**,不参与分类、归并或替换 message(勿与下方反面 case 3 的关键词归并混淆)
- [ ] 追加时若已有 hint(如 video task id),拼接并对**完整恢复文本**去重;不要 `hint` 非空就跳过,也不要用 `"usage freetier"` 子串匹配
- [ ] 命令层不得自行拼 `usage free` / `usage freetier` 恢复指引;该指引只由 runtime `errorHint` 生成
- [ ] 渲染产品命令前须经能力门控(`hasCommand(path)`),不要对 `kscli` 等缺命令的产品硬拼 `usage freetier`
- [ ] 命令名使用 `commandPrefix`,不要硬编码 `bl`
- [ ] 恢复命令经 `recoveryCommand` 渲染;Profile 名用 `resolveRecoveryConfigName` / `Settings.recoveryConfigName`(显式 `--config default` 保留,隐式 default 不加)。API Key capability fallback 到 `default` 时,runtime 须把恢复 hint 的 profile 改写为 `default`(当前命令自身的控制台会话 hint `consoleRecoveryHint` 不改)。理由:恢复命令要跟随实际报错的 API Key 所属 Profile——此时报错(如 `AllocationQuota.FreeTierOnly`)来自 `default` 的 API Key 所属账号,免费额度恢复虽是 Console 命令,也须钉 `default`;钉回原命名 Profile 可能用另一账号的 Console token 静默改错账号的计费设置。`default` 缺 Console token 时报 AUTH 并给出 `auth login --console --config default`,可恢复且账号正确。不要拆成 api-key / console 两套 Profile 名
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`

### C. command / runtime 直接抛错
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/output-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ OpenAgentPack SDK 没有 logger 注入接口:仅在 SDK 调用上下文内,
- [ ] JSON 诊断每条一行;stdout 为单个业务 JSON 文档,stderr 单独解析
- [ ] speech synthesize 的原始音频仅在 text 流式模式下直接写 stdout;JSON 流式需 --out 文件并返回元数据,未指定文件时在请求和文件操作前报用法错误;dry-run 仍允许 JSON 预览。--list-voices 的 JSON 返回音色条目与已有文档链接,内置列表未知时 voices/total 为 null
- [ ] speech recognize 的同步 JSON 返回完整服务响应;异步完成后返回原始任务响应并附加 transcriptions 数组,保留失败子任务和 request_id。text 保留转写、说话人和批量分隔;--out 仍保存原有转写文件结构,下载及写盘失败不提前输出最终结果;部分子任务失败不改变原有退出码
- [ ] usage freetier 的启用 JSON 保留原始响应数组;停用 JSON 返回 `{ processed: [{ model, result }], skipped: [model] }`,跳过项仍不发起停用请求。失败与超时用 warning 提示,保留原有退出策略,不把部分失败丢出结果。
- [ ] usage freetier 的启用 JSON 保留原始响应数组(含零模型时的 `[]`);停用 JSON 返回 `{ processed: [{ model, result }], skipped: [model] }`(含空对象),跳过项仍不发起停用请求。按模型串行修改:业务失败 / 超时 / 未在 successModels(或 successCount)确认成功时 fail-fast,不再请求后续模型;HTTP/网络异常先输出已确认进度并用 warning 记录进度,再原样抛出原始错误(不二次包装,保留 cause.code / ERROR_STACK)。失败与超时先用 warning 提示并保留业务结果,再抛摘要:仅轮询超时 → `TIMEOUT(5)`(文案用「超时未确认」);服务端拒绝 / 未确认成功 → `GENERAL(1)`;成功文案(Enabled / Disabled)仅在该模型操作成功时输出
- [ ] Token Plan 创建密钥的完整响应和一次性明文仍属于业务结果,quiet 不裁剪;不得通过 warning/debug 泄露明文。
- [ ] 原始流与原生交互仅限上表;Command Pack API 1 由宿主缓存并提交一次结果,不再直接转发 stdout
- [ ] 每个调用使用独立 reporter,不共享结果状态、不替换全局 stdout/stderr;SDK console 桥接只能读取调用上下文,不缓存上一个 host
Expand Down
7 changes: 6 additions & 1 deletion packages/commands/src/commands/knowledge/chunk-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ export default defineCommand({
deletedCount += batchIds.length;
completedBatches.push(batchIds);
} catch (error) {
const original = normalizeError(error, formatCommand(ctx.identity), ctx.localize);
const original = normalizeError(
error,
formatCommand(ctx.identity),
ctx.localize,
ctx.settings.recoveryConfigName,
);
const failedBatch = completedBatches.length + 1;
const progress = [
ctx.localize({
Expand Down
191 changes: 163 additions & 28 deletions packages/commands/src/commands/usage/freetier.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineCommand, unwrapResponse } from "bailian-cli-core";
import { BailianError, ExitCode, defineCommand, unwrapResponse } from "bailian-cli-core";
import {
FREE_TIER_ONLY_STATUS_API,
extractFreeTierOnlyStatuses,
Expand All @@ -14,13 +14,41 @@ interface BatchResultFailure {
errorCode: string;
}

/** Require a positive success signal; empty successModels must not count as success. */
function isModelOperationConfirmed(
resultData: Record<string, unknown>,
modelName: string,
): boolean {
const failureModels = (resultData.failureModels as BatchResultFailure[] | undefined) ?? [];
if (failureModels.length > 0) return false;
const successModels = resultData.successModels;
if (Array.isArray(successModels)) {
return successModels.includes(modelName);
}
if (typeof resultData.successCount === "number") {
return resultData.successCount > 0;
}
// Legacy responses without a success list still use the boolean flag.
return resultData.success === true;
}

export default defineCommand({
description: {
"en-US":
"Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable",
"zh-CN": "启用或停用免费额度模型自动停止。默认启用,使用 --off 停用",
},
auth: "console",
risk: {
level: "high",
reason: "billing",
message: {
"en-US":
"This changes free-tier auto-stop. Enabling it makes calls fail once free quota is exhausted; disabling it allows pay-as-you-go charges after free quota is exhausted.",
"zh-CN":
"此操作会变更免费额度自动停止。启用后,免费额度用尽时调用将失败;停用后,免费额度用尽将继续按量计费调用。",
},
},
usageArgs: "<--model <model>[,model2,...] | --all> [--off] [flags]",
flags: {
model: {
Expand Down Expand Up @@ -48,12 +76,19 @@ export default defineCommand({
},
},
exampleArgs: [
"--model qwen3-max",
"--model qwen3-max,qwen-turbo",
"--all",
"--on --model qwen3-max",
"--off --model qwen3-max",
"--off --all",
"--model qwen3-max --yes",
"--model qwen3-max,qwen-turbo --yes",
"--all --yes",
"--on --model qwen3-max --yes",
"--off --model qwen3-max --yes",
"--off --all --yes",
],
notes: [
{
"en-US":
"Requires `--yes` (exit code 7 / requires_confirmation without it). Applies to both enabling and disabling auto-stop.",
"zh-CN": "需要 `--yes`(缺少时退出码 7 / requires_confirmation)。启用与停用自动停止均适用。",
},
],
validate: (flags) =>
!flags.model && !flags.all ? "Provide --model <model>[,model2,...] or --all." : undefined,
Expand All @@ -62,6 +97,8 @@ export default defineCommand({
const modelFlag = flags.model || undefined;
const off = Boolean(flags.off);
const format = settings.output;
const action = off ? "disable" : "enable";
const actionZh = off ? "停用" : "启用";

let models: string[];
if (modelFlag) {
Expand Down Expand Up @@ -96,6 +133,9 @@ export default defineCommand({

const skipped: string[] = [];
const processed: { model: string; result: unknown }[] = [];
const confirmed: string[] = [];
const failed: string[] = [];
const timedOut: string[] = [];
const textLines: string[] = [];
let stopMap = new Map<string, boolean>();
if (off) {
Expand All @@ -106,6 +146,38 @@ export default defineCommand({
stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));
}

let resultWritten = false;
/** Empty JSON is a valid success; only skip empty output when catching with no progress. */
const writeBusinessResult = (options?: { requireProgress?: boolean }) => {
if (resultWritten) return;
if (format === "text" && textLines.length === 0) return;
if (
options?.requireProgress &&
processed.length === 0 &&
skipped.length === 0 &&
textLines.length === 0
) {
return;
}
ctx.output.result(
off ? { processed, skipped } : processed.map((item) => item.result),
format === "text" ? { text: textLines.join("\n") } : undefined,
);
resultWritten = true;
};

const progressMessage = (failedModel: string) =>
ctx.localize({
"en-US": [
`Confirmed ${action} auto-stop for ${confirmed.length} model(s)${confirmed.length > 0 ? `: ${confirmed.join(", ")}` : ""}.`,
`Model "${failedModel}" failed; its outcome is unconfirmed. Later models were not executed.`,
].join("\n"),
"zh-CN": [
`已确认${actionZh}自动停止 ${confirmed.length} 个模型${confirmed.length > 0 ? `:${confirmed.join("、")}` : ""}。`,
`模型 "${failedModel}" 失败,结果未确认。后续模型未执行。`,
].join("\n"),
});

for (const name of models) {
if (off && stopMap.get(name) === false) {
skipped.push(name);
Expand All @@ -115,11 +187,35 @@ export default defineCommand({
});
continue;
}
const result = await pollFreeTierBatch(ctx.client, api, requestKey, [name]);

let result: unknown;
try {
result = await pollFreeTierBatch(ctx.client, api, requestKey, [name]);
} catch (error) {
// Keep stdout progress, but do not wrap the original error (preserves cause.code /
// ERROR_STACK). Surface confirmed work via warning instead.
writeBusinessResult({ requireProgress: true });
if (confirmed.length > 0 || processed.length > 0 || skipped.length > 0) {
ctx.output.warning({
code: "FREETIER_PARTIAL_PROGRESS",
message: progressMessage(name),
details: {
confirmed,
failed_model: name,
processed_count: processed.length,
skipped_count: skipped.length,
},
});
}
throw error;
}

processed.push({ model: name, result });
const resultData = result ? unwrapResponse(result as Record<string, unknown>) : undefined;
const failureModels = (resultData?.failureModels as BatchResultFailure[] | undefined) ?? [];

if (!result) {
timedOut.push(name);
ctx.output.warning({
code: "FREETIER_OPERATION_TIMEOUT",
message: {
Expand All @@ -128,7 +224,11 @@ export default defineCommand({
},
details: { model: name },
});
} else if (failureModels.length > 0) {
break;
}

if (failureModels.length > 0) {
failed.push(name);
const errorCode = failureModels[0].errorCode;
ctx.output.warning({
code: "FREETIER_OPERATION_FAILED",
Expand All @@ -143,29 +243,64 @@ export default defineCommand({
},
details: { model: name, error_code: errorCode },
});
break;
}
// Keep the existing disable response policy; the JSON report retains the raw response.
if (off || (result && failureModels.length === 0)) {
textLines.push(
ctx.localize(
off
? {
"en-US": `Disabled auto-stop for "${name}".`,
"zh-CN": `已为模型 "${name}" 停用自动停止。`,
}
: {
"en-US": `Enabled auto-stop for "${name}".`,
"zh-CN": `已为模型 "${name}" 启用自动停止。`,
},
),
);

if (!resultData || !isModelOperationConfirmed(resultData, name)) {
failed.push(name);
ctx.output.warning({
code: "FREETIER_OPERATION_FAILED",
message: {
"en-US": `Auto-stop ${action} for "${name}" was not confirmed by the server.`,
"zh-CN": `服务端未确认模型 "${name}" 的自动停止${actionZh}。`,
},
details: { model: name },
});
break;
}

confirmed.push(name);
textLines.push(
ctx.localize(
off
? {
"en-US": `Disabled auto-stop for "${name}".`,
"zh-CN": `已为模型 "${name}" 停用自动停止。`,
}
: {
"en-US": `Enabled auto-stop for "${name}".`,
"zh-CN": `已为模型 "${name}" 启用自动停止。`,
},
),
);
}
if (format === "json" || textLines.length > 0) {
ctx.output.result(
off ? { processed, skipped } : processed.map((item) => item.result),
format === "text" ? { text: textLines.join("\n") } : undefined,

writeBusinessResult();
if (failed.length === 0 && timedOut.length === 0) return;

// Client poll timeout is internal TIMEOUT; server rejection stays GENERAL.
const summaryPartsEn: string[] = [];
const summaryPartsZh: string[] = [];
if (failed.length > 0) {
summaryPartsEn.push(
`Failed to ${action} auto-stop for ${failed.length} model(s): ${failed.join(", ")}.`,
);
summaryPartsZh.push(`${failed.length} 个模型${actionZh}自动停止失败:${failed.join("、")}。`);
}
if (timedOut.length > 0) {
summaryPartsEn.push(
`Timed out waiting to confirm ${action} auto-stop for ${timedOut.length} model(s): ${timedOut.join(", ")}.`,
);
summaryPartsZh.push(
`${timedOut.length} 个模型${actionZh}自动停止超时未确认:${timedOut.join("、")}。`,
);
}
throw new BailianError(
ctx.localize({
"en-US": summaryPartsEn.join(" "),
"zh-CN": summaryPartsZh.join(""),
}),
failed.length > 0 ? ExitCode.GENERAL : ExitCode.TIMEOUT,
);
},
});
Loading
Loading