feat: 阈值预警/加力钱包/api_key_env/版本跟踪器/一键体检 — bump to v0.7.20
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
This commit is contained in:
1 parent
35581103ed
commit
fcd08eae32
35 files changed
+2567
-91
No files matched your search
@@ -134,10 +134,104 @@ describe("loop_control — v1/v2 key handling", () => {
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// [permission] dangerous_command_guard (kimi-code 0.40.1) — absent key means
|
||||
// upstream default ON; an explicit false must survive even with no rules
|
||||
// loop_control.compaction_max_attempts (kimi-code 0.43.0) — read via the loop
|
||||
// spread, written only when set, and 0/empty removes the key (upstream
|
||||
// default 5 applies)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("loop_control.compaction_max_attempts", () => {
|
||||
it("reads an existing value through the loop spread", () => {
|
||||
expect(
|
||||
getAgentSettings({ loop_control: { compaction_max_attempts: 7 } })
|
||||
.loop_control?.compaction_max_attempts
|
||||
).toBe(7);
|
||||
});
|
||||
|
||||
it("reads as undefined when the key is absent (UI falls back to 5)", () => {
|
||||
expect(
|
||||
getAgentSettings({ loop_control: {} }).loop_control?.compaction_max_attempts
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("writes the value when the patch sets it", () => {
|
||||
const next = setAgentSettings({}, {
|
||||
loop_control: { compaction_max_attempts: 8 },
|
||||
});
|
||||
expect(loopOf(next).compaction_max_attempts).toBe(8);
|
||||
});
|
||||
|
||||
it("drops the key for 0 and for a cleared (undefined) field", () => {
|
||||
const raw = { loop_control: { compaction_max_attempts: 6 } };
|
||||
expect(loopOf(setAgentSettings(raw, {
|
||||
loop_control: { compaction_max_attempts: 0 },
|
||||
}))).not.toHaveProperty("compaction_max_attempts");
|
||||
expect(loopOf(setAgentSettings(raw, {
|
||||
loop_control: { compaction_max_attempts: undefined },
|
||||
}))).not.toHaveProperty("compaction_max_attempts");
|
||||
});
|
||||
|
||||
it("does not materialize the upstream default on an unrelated save", () => {
|
||||
// An absent key must stay absent — a plain save must not pin `5`.
|
||||
const next = setAgentSettings({ thinking: { effort: "high" } }, {});
|
||||
expect(loopOf(next)).not.toHaveProperty("compaction_max_attempts");
|
||||
// An existing value survives a save that does not touch it.
|
||||
const kept = setAgentSettings(
|
||||
{ loop_control: { compaction_max_attempts: 4 } },
|
||||
{ thinking: { effort: "low" } }
|
||||
);
|
||||
expect(loopOf(kept).compaction_max_attempts).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// [watch] enabled (kimi-code 2.0.1+) — top-level section, off by default since
|
||||
// 2.0.2; mirrors the [background] handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function watchOf(raw: unknown): Record<string, unknown> {
|
||||
const w = (raw as { watch?: unknown })?.watch;
|
||||
return w && typeof w === "object" && !Array.isArray(w)
|
||||
? (w as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
describe("[watch] enabled", () => {
|
||||
it("reads as false when the section or key is absent (2.0.2 default)", () => {
|
||||
expect(getAgentSettings({}).watch?.enabled).toBe(false);
|
||||
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("reads an explicit value", () => {
|
||||
expect(getAgentSettings({ watch: { enabled: true } }).watch?.enabled).toBe(true);
|
||||
expect(getAgentSettings({ watch: { enabled: false } }).watch?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("writes the top-level [watch] section", () => {
|
||||
const next = setAgentSettings({}, { watch: { enabled: true } }) as {
|
||||
watch?: Record<string, unknown>;
|
||||
};
|
||||
expect(next.watch).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("keeps the section on saves that do not touch it", () => {
|
||||
const next = setAgentSettings(
|
||||
{ watch: { enabled: true } },
|
||||
{ thinking: { effort: "high" } }
|
||||
) as { watch?: Record<string, unknown> };
|
||||
expect(next.watch).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("does not disturb the other managed sections", () => {
|
||||
const next = setAgentSettings(
|
||||
{ background: { max_running_tasks: 2 } },
|
||||
{ watch: { enabled: true } }
|
||||
) as { background?: Record<string, unknown> };
|
||||
expect(next.background?.max_running_tasks).toBe(2);
|
||||
expect(watchOf(next).enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("permission.dangerous_command_guard", () => {
|
||||
it("reads as undefined when the key is absent (default on)", () => {
|
||||
expect(
|
||||
|
||||
@@ -13,6 +13,11 @@ const DEFAULT_SETTINGS: AgentSettings = {
|
||||
background: {
|
||||
keep_alive_on_exit: false,
|
||||
},
|
||||
// `[watch] enabled` — kimi-code 2.0.1 added the key, 2.0.2 flipped the
|
||||
// default to off, so an absent key reads as "off".
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
permission: { rules: [] },
|
||||
hooks: [],
|
||||
};
|
||||
@@ -76,6 +81,10 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
|
||||
...DEFAULT_SETTINGS.background,
|
||||
...getSection<AgentSettings["background"]>(rawOther, "background"),
|
||||
},
|
||||
watch: {
|
||||
...DEFAULT_SETTINGS.watch,
|
||||
...getSection<AgentSettings["watch"]>(rawOther, "watch"),
|
||||
},
|
||||
permission: {
|
||||
rules: sectionPermission?.rules ?? [],
|
||||
// `dangerous_command_guard` stays undefined unless the config carries
|
||||
@@ -111,6 +120,7 @@ export function setAgentSettings(
|
||||
thinking: { ...current.thinking, ...patch.thinking },
|
||||
loop_control: { ...current.loop_control, ...patch.loop_control },
|
||||
background: { ...current.background, ...patch.background },
|
||||
watch: { ...current.watch, ...patch.watch },
|
||||
permission,
|
||||
hooks: patch.hooks ?? current.hooks ?? [],
|
||||
};
|
||||
@@ -129,9 +139,16 @@ export function setAgentSettings(
|
||||
if (!opts?.legacyV1) {
|
||||
delete next.loop_control?.max_retries_per_step;
|
||||
}
|
||||
// `compaction_max_attempts` (kimi-code 0.43.0+): 0 / undefined means "leave
|
||||
// the key out" — the config then falls back to the upstream default (5)
|
||||
// instead of pinning a literal 0.
|
||||
if (!next.loop_control?.compaction_max_attempts) {
|
||||
delete next.loop_control?.compaction_max_attempts;
|
||||
}
|
||||
if (next.thinking) root.thinking = next.thinking;
|
||||
if (next.loop_control) root.loop_control = next.loop_control;
|
||||
if (next.background) root.background = next.background;
|
||||
if (next.watch) root.watch = next.watch;
|
||||
// Keep `[permission]` when it carries rules *or* an explicit
|
||||
// dangerous_command_guard — dropping the section would silently lose a
|
||||
// guard=false write (absent key = upstream default on). An empty rules
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("planLabel", () => {
|
||||
expect(planLabel("five_hour", t)).toBe("usageTier5h");
|
||||
expect(planLabel("weekly_limit", t)).toBe("usageTierWeekly");
|
||||
expect(planLabel("monthly_limit", t)).toBe("usageTierMonthly");
|
||||
expect(planLabel("booster_wallet", t)).toBe("usageTierBoosterWallet");
|
||||
expect(planLabel("NewAPI", t)).toBe("NewAPI");
|
||||
expect(planLabel("¥", t)).toBe("¥");
|
||||
});
|
||||
|
||||
@@ -5,6 +5,13 @@ type TranslateFn = (
|
||||
vars?: Record<string, string | number>
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Kimi quota 的加力钱包行 id(src-tauri/src/services/coding_plan.rs 的
|
||||
* PLAN_BOOSTER_WALLET)。它是金额行而非配额百分比行,planLabel() 与
|
||||
* UsageFooter 的金额行判定共用此常量,避免字面量散落。
|
||||
*/
|
||||
export const BOOSTER_WALLET_PLAN = "booster_wallet";
|
||||
|
||||
/**
|
||||
* Map a raw Rust plan/tier name to a localized label. Unknown names (proper
|
||||
* nouns like "OpenRouter", "NewAPI", currency codes) pass through unchanged.
|
||||
@@ -28,6 +35,8 @@ export function planLabel(name: string, t: TranslateFn): string {
|
||||
return t("usageTierMonthly");
|
||||
case "month_code":
|
||||
return t("usageTierMonthCode");
|
||||
case BOOSTER_WALLET_PLAN:
|
||||
return t("usageTierBoosterWallet");
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ export function validateProviders(
|
||||
const reasons: string[] = [];
|
||||
|
||||
const apiKey = provider.api_key?.trim();
|
||||
if (!apiKey && !provider.managed) {
|
||||
// kimi-code 2.0.0+: the credential may come from an environment variable
|
||||
// instead of a stored key — a provider configured that way is complete.
|
||||
const apiKeyEnv = provider.api_key_env?.trim();
|
||||
if (!apiKey && !apiKeyEnv && !provider.managed) {
|
||||
reasons.push(t("validationNoApiKey"));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user