Compare commits
3
Commits
4de13ad353
...
v0.7.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36fc9c50a8 | ||
|
|
1a11cfbaaf | ||
|
|
81699e757d |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimiswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.12",
|
"version": "0.7.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# KimiSwitch v0.7.13
|
||||||
|
|
||||||
|
## 适配 kimi-code 0.41.0
|
||||||
|
|
||||||
|
- **实验 flag 清单回至 9 个**:上游 0.41.0 移除了 `file_history` 实验 flag(轮级文件历史转为恒开),KimiSwitch 镜像全链路同步删除;用户配置中残留的该键会被上游静默忽略,无副作用
|
||||||
|
- **危险命令守卫文案更新**:Auto(永不询问)模式自 0.41.0 起不再拦截危险命令与无法静态分析的命令,仅 Manual/YOLO 模式强制询问——设置页描述文案(中英)同步修正
|
||||||
|
|
||||||
|
## 账单查询改进
|
||||||
|
|
||||||
|
- **CodingPlan.site 自动识别**:`codingplan.site` 系供应商自动识别为 NewAPI 余额查询,无需手动选模板
|
||||||
|
- **修复凭据缺失误报「网络异常」**:NewAPI 模板未配置 Access Token/User ID 时,现在显示明确的本地化配置提示,而非误导性的网络错误
|
||||||
|
- **金额显示统一**:配置弹窗与卡片底部走同一格式化(两位小数);NewAPI 站点未配置货币符号时的 `¤` 占位符不再显示
|
||||||
|
|
||||||
|
## 其它
|
||||||
|
|
||||||
|
- models-dev 快照随 0.7.12 线更新;新增 usage 错误本地化单元测试
|
||||||
Generated
+1
-1
@@ -1978,7 +1978,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.12"
|
version = "0.7.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.12"
|
version = "0.7.13"
|
||||||
description = "Kimi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["codingplan.site"]
|
authors = ["codingplan.site"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -690,8 +690,8 @@ pub async fn query_provider_usage(
|
|||||||
|
|
||||||
let base_url = resolve_base_url(provider);
|
let base_url = resolve_base_url(provider);
|
||||||
|
|
||||||
// NewAPI template: query the gateway's own /api/user/self with the
|
// NewAPI template: query the gateway's own usage endpoints with its
|
||||||
// web-console access token, bypassing usage_kinds entirely.
|
// dedicated credentials, bypassing usage_kinds entirely.
|
||||||
if let Some(cfg) = &provider.usage_config {
|
if let Some(cfg) = &provider.usage_config {
|
||||||
if cfg.template_type == UsageConfig::TEMPLATE_NEWAPI {
|
if cfg.template_type == UsageConfig::TEMPLATE_NEWAPI {
|
||||||
// Err = transient (network) → propagate for retry, same semantics
|
// Err = transient (network) → propagate for retry, same semantics
|
||||||
@@ -1355,7 +1355,7 @@ pub async fn kimi_oauth_poll(
|
|||||||
/// the raw values are returned and the truthy check happens on the frontend.
|
/// the raw values are returned and the truthy check happens on the frontend.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_experimental_env_status() -> HashMap<String, String> {
|
pub fn get_experimental_env_status() -> HashMap<String, String> {
|
||||||
const VARS: [(&str, &str); 12] = [
|
const VARS: [(&str, &str); 11] = [
|
||||||
("secondary-model", "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL"),
|
("secondary-model", "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL"),
|
||||||
("tool-select", "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT"),
|
("tool-select", "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT"),
|
||||||
(
|
(
|
||||||
@@ -1368,7 +1368,6 @@ pub fn get_experimental_env_status() -> HashMap<String, String> {
|
|||||||
("auto_session_title", "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE"),
|
("auto_session_title", "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE"),
|
||||||
("remote-control", "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL"),
|
("remote-control", "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL"),
|
||||||
("search_worker", "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER"),
|
("search_worker", "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER"),
|
||||||
("file_history", "KIMI_CODE_EXPERIMENTAL_FILE_HISTORY"),
|
|
||||||
// Non-flag probes consumed by the frontend:
|
// Non-flag probes consumed by the frontend:
|
||||||
("master", "KIMI_CODE_EXPERIMENTAL_FLAG"),
|
("master", "KIMI_CODE_EXPERIMENTAL_FLAG"),
|
||||||
("legacy", "KIMI_CODE_LEGACY_FLAG"),
|
("legacy", "KIMI_CODE_LEGACY_FLAG"),
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ pub fn detect_provider(base_url: &str) -> Vec<UsageKind> {
|
|||||||
if url.contains("openrouter.ai") {
|
if url.contains("openrouter.ai") {
|
||||||
kinds.push(UsageKind::BalanceOpenrouter);
|
kinds.push(UsageKind::BalanceOpenrouter);
|
||||||
}
|
}
|
||||||
|
// codingplan.site 检测为 NewAPI 实例(本工具仅支持 NewAPI 查询);
|
||||||
|
// 家族匹配意味着 ai. 子域也自然命中该规则。
|
||||||
|
if url.contains("codingplan.site") {
|
||||||
|
kinds.push(UsageKind::BalanceNewapi);
|
||||||
|
}
|
||||||
if url.contains("api.stepfun.com") {
|
if url.contains("api.stepfun.com") {
|
||||||
kinds.push(UsageKind::BalanceStepfun);
|
kinds.push(UsageKind::BalanceStepfun);
|
||||||
}
|
}
|
||||||
@@ -126,6 +131,19 @@ pub fn detect_provider(base_url: &str) -> Vec<UsageKind> {
|
|||||||
kinds
|
kinds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// BalanceNewapi 模板所需凭据(web 控制台 access token + user id)。
|
||||||
|
/// 两者都必须存在且非空;返回 None 表示缺少凭据(确定性配置错误)。
|
||||||
|
fn newapi_creds(
|
||||||
|
usage_config: Option<&crate::models::UsageConfig>,
|
||||||
|
) -> Option<(&str, &str)> {
|
||||||
|
usage_config.and_then(|c| {
|
||||||
|
c.access_token
|
||||||
|
.as_deref()
|
||||||
|
.zip(c.user_id.as_deref())
|
||||||
|
.filter(|(t, u)| !t.is_empty() && !u.is_empty())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 按 kind 路由到对应查询实现。`base_url` 用于消歧同一家供应商的
|
/// 按 kind 路由到对应查询实现。`base_url` 用于消歧同一家供应商的
|
||||||
/// 国内/海外站(SiliconFlow .cn/.com、MiniMax .com/.io、智谱 bigmodel/z.ai)。
|
/// 国内/海外站(SiliconFlow .cn/.com、MiniMax .com/.io、智谱 bigmodel/z.ai)。
|
||||||
/// `usage_config` 仅 BalanceNewapi 分支读取(access_token / user_id),
|
/// `usage_config` 仅 BalanceNewapi 分支读取(access_token / user_id),
|
||||||
@@ -155,16 +173,13 @@ pub async fn query_kind(
|
|||||||
balance::query_kimi(api_key, !lower.contains("moonshot.ai"), timeout).await
|
balance::query_kimi(api_key, !lower.contains("moonshot.ai"), timeout).await
|
||||||
}
|
}
|
||||||
UsageKind::BalanceNewapi => {
|
UsageKind::BalanceNewapi => {
|
||||||
let (token, uid) = usage_config
|
// 缺凭据是确定性配置错误 → Ok(failure)(前端按前缀本地化提示),
|
||||||
.and_then(|c| {
|
// 而非 Err(Err 语义 = 瞬时网络错误,见 commands.rs newapi 分支)。
|
||||||
c.access_token
|
let Some((token, uid)) = newapi_creds(usage_config) else {
|
||||||
.as_deref()
|
return Ok(UsageResult::failure(
|
||||||
.zip(c.user_id.as_deref())
|
"newapi template requires accessToken and userId".to_string(),
|
||||||
.filter(|(t, u)| !t.is_empty() && !u.is_empty())
|
));
|
||||||
})
|
};
|
||||||
.ok_or_else(|| {
|
|
||||||
"newapi template requires accessToken and userId".to_string()
|
|
||||||
})?;
|
|
||||||
let url = usage_config
|
let url = usage_config
|
||||||
.and_then(|c| c.base_url.as_deref())
|
.and_then(|c| c.base_url.as_deref())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
@@ -262,6 +277,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_provider_codingplan_site_family() {
|
||||||
|
// codingplan.site 是 NewAPI 实例(本工具仅支持 NewAPI 查询):
|
||||||
|
// 主域与 ai. 子域都命中家族规则,且只映射到 BalanceNewapi。
|
||||||
|
assert_eq!(
|
||||||
|
detect_provider("https://codingplan.site"),
|
||||||
|
vec![UsageKind::BalanceNewapi]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_provider("https://codingplan.site/v1"),
|
||||||
|
vec![UsageKind::BalanceNewapi]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_provider("https://ai.codingplan.site"),
|
||||||
|
vec![UsageKind::BalanceNewapi]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_provider("https://ai.codingplan.site/v1"),
|
||||||
|
vec![UsageKind::BalanceNewapi]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usage_kind_string_roundtrip() {
|
fn usage_kind_string_roundtrip() {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -272,4 +309,30 @@ mod tests {
|
|||||||
assert!(UsageKind::from_str("balance:unknown").is_err());
|
assert!(UsageKind::from_str("balance:unknown").is_err());
|
||||||
assert!(UsageKind::from_str("").is_err());
|
assert!(UsageKind::from_str("").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newapi_creds_requires_both_and_nonempty() {
|
||||||
|
use crate::models::UsageConfig;
|
||||||
|
let cfg = |token: Option<&str>, uid: Option<&str>| UsageConfig {
|
||||||
|
enabled: true,
|
||||||
|
template_type: "newapi".to_string(),
|
||||||
|
base_url: None,
|
||||||
|
access_token: token.map(str::to_string),
|
||||||
|
user_id: uid.map(str::to_string),
|
||||||
|
auto_query_interval_minutes: None,
|
||||||
|
timeout_seconds: None,
|
||||||
|
};
|
||||||
|
// 缺 config / 缺任一项 / 空字符串 → None(确定性配置错误)
|
||||||
|
assert_eq!(newapi_creds(None), None);
|
||||||
|
assert_eq!(newapi_creds(Some(&cfg(None, None))), None);
|
||||||
|
assert_eq!(newapi_creds(Some(&cfg(Some("tok"), None))), None);
|
||||||
|
assert_eq!(newapi_creds(Some(&cfg(None, Some("uid")))), None);
|
||||||
|
assert_eq!(newapi_creds(Some(&cfg(Some(""), Some("uid")))), None);
|
||||||
|
assert_eq!(newapi_creds(Some(&cfg(Some("tok"), Some("")))), None);
|
||||||
|
// 两项齐全 → Some
|
||||||
|
assert_eq!(
|
||||||
|
newapi_creds(Some(&cfg(Some("tok"), Some("uid")))),
|
||||||
|
Some(("tok", "uid"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Kimi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.7.12",
|
"version": "0.7.13",
|
||||||
"identifier": "com.kimiswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -157,10 +157,13 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title={t("permissionRules")}>
|
<Card title={t("permissionRules")}>
|
||||||
{/* kimi-code 0.40.1 `[permission] dangerous_command_guard`. The env
|
{/* kimi-code `[permission] dangerous_command_guard`. The env
|
||||||
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
|
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
|
||||||
outranks this config at runtime; no env-lock UI here (deliberate
|
outranks this config at runtime; no env-lock UI here (deliberate
|
||||||
minimal scope). Absent key = upstream default on. */}
|
minimal scope). Absent key = upstream default on. Since
|
||||||
|
kimi-code 0.41.0 the guard only gates Manual/YOLO modes —
|
||||||
|
Auto (never-ask) mode no longer intercepts dangerous
|
||||||
|
commands. */}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("permissionDangerousGuard")}
|
label={t("permissionDangerousGuard")}
|
||||||
checked={settings.permission?.dangerous_command_guard ?? true}
|
checked={settings.permission?.dangerous_command_guard ?? true}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }
|
|||||||
},
|
},
|
||||||
"remote-control": { name: "flagRemoteControl", desc: "flagRemoteControlDesc" },
|
"remote-control": { name: "flagRemoteControl", desc: "flagRemoteControlDesc" },
|
||||||
search_worker: { name: "flagSearchWorker", desc: "flagSearchWorkerDesc" },
|
search_worker: { name: "flagSearchWorker", desc: "flagSearchWorkerDesc" },
|
||||||
file_history: { name: "flagFileHistory", desc: "flagFileHistoryDesc" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
|
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { ArrowLeft, Eye, EyeOff, Loader2, Play } from "lucide-react";
|
import { ArrowLeft, Eye, EyeOff, Loader2, Play } from "lucide-react";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
import type { TranslationKey } from "../i18n/zh";
|
import type { TranslationKey } from "../i18n/zh";
|
||||||
import { localizeUsageError, planLabel } from "../lib/usage-display";
|
import { formatAmount, localizeUsageError, planLabel } from "../lib/usage-display";
|
||||||
import type { Agent, Provider, UsageConfig } from "../types";
|
import type { Agent, Provider, UsageConfig } from "../types";
|
||||||
|
|
||||||
interface UsageResult {
|
interface UsageResult {
|
||||||
@@ -134,7 +134,7 @@ export function UsageConfigModal({ open, agent, provider, onClose, onSave }: Usa
|
|||||||
.map((d) => {
|
.map((d) => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (d.planName) parts.push(planLabel(d.planName, t));
|
if (d.planName) parts.push(planLabel(d.planName, t));
|
||||||
if (d.remaining != null) parts.push(`${t("usageRemaining")} ${d.remaining}${d.unit ?? ""}`);
|
if (d.remaining != null) parts.push(`${t("usageRemaining")} ${formatAmount(d.remaining, d.unit)}`);
|
||||||
if (d.used != null && d.total != null && d.total > 0)
|
if (d.used != null && d.total != null && d.total > 0)
|
||||||
parts.push(`${Math.round((d.used / d.total) * 100)}%`);
|
parts.push(`${Math.round((d.used / d.total) * 100)}%`);
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Loader2, RefreshCw } from "lucide-react";
|
import { Loader2, RefreshCw } from "lucide-react";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
import { localizeUsageError, planLabel } from "../lib/usage-display";
|
import { formatAmount, localizeUsageError, planLabel } from "../lib/usage-display";
|
||||||
import type { UsageData, UsageQueryState } from "../hooks/useUsageQuery";
|
import type { UsageData, UsageQueryState } from "../hooks/useUsageQuery";
|
||||||
|
|
||||||
interface UsageFooterProps {
|
interface UsageFooterProps {
|
||||||
@@ -36,17 +36,6 @@ export function UsageFooter({ usage, variant = "detail" }: UsageFooterProps) {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAmount = (
|
|
||||||
value: number | null | undefined,
|
|
||||||
unit?: string | null
|
|
||||||
): string => {
|
|
||||||
if (value == null) return "—";
|
|
||||||
const symbol =
|
|
||||||
unit === "CNY" ? "¥" : unit === "USD" ? "$" : unit ? `${unit} ` : "";
|
|
||||||
const num = Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
||||||
return `${symbol}${num}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatReset = (resetsAt?: string | null): string | null => {
|
const formatReset = (resetsAt?: string | null): string | null => {
|
||||||
if (!resetsAt) return null;
|
if (!resetsAt) return null;
|
||||||
const ms = new Date(resetsAt).getTime() - now;
|
const ms = new Date(resetsAt).getTime() - now;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type UsageKind =
|
|||||||
| "balance:stepfun"
|
| "balance:stepfun"
|
||||||
| "balance:novita"
|
| "balance:novita"
|
||||||
| "balance:kimi"
|
| "balance:kimi"
|
||||||
|
| "balance:newapi"
|
||||||
| "plan:kimi_coding"
|
| "plan:kimi_coding"
|
||||||
| "plan:zhipu"
|
| "plan:zhipu"
|
||||||
| "plan:minimax"
|
| "plan:minimax"
|
||||||
@@ -30,6 +31,7 @@ export const SUPPORTED_USAGE_KINDS: ReadonlySet<string> = new Set<UsageKind>([
|
|||||||
"balance:stepfun",
|
"balance:stepfun",
|
||||||
"balance:novita",
|
"balance:novita",
|
||||||
"balance:kimi",
|
"balance:kimi",
|
||||||
|
"balance:newapi",
|
||||||
"plan:kimi_coding",
|
"plan:kimi_coding",
|
||||||
"plan:zhipu",
|
"plan:zhipu",
|
||||||
"plan:minimax",
|
"plan:minimax",
|
||||||
|
|||||||
+1
-4
@@ -153,7 +153,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
permissionAsk: "Ask",
|
permissionAsk: "Ask",
|
||||||
permissionDangerousGuard: "Dangerous Command Guard",
|
permissionDangerousGuard: "Dangerous Command Guard",
|
||||||
permissionDangerousGuardDesc:
|
permissionDangerousGuardDesc:
|
||||||
"When on (default): Auto mode refuses dangerous commands (rm -rf, shutdown, dd of=, ...) and other modes always prompt; turning it off restores the previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD overrides this config",
|
"When on (default): Manual/YOLO modes force a prompt for dangerous commands (rm -rf, shutdown, dd of=, ...) and commands that cannot be statically analyzed; Auto (never-ask) mode no longer intercepts them since kimi-code 0.41.0. Turning it off restores the previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD overrides this config",
|
||||||
addRule: "+ Add rule",
|
addRule: "+ Add rule",
|
||||||
addCommonRules: "Add common rules",
|
addCommonRules: "Add common rules",
|
||||||
hooks: "Lifecycle Hooks",
|
hooks: "Lifecycle Hooks",
|
||||||
@@ -482,9 +482,6 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
flagRemoteControlDesc: "Enable the experimental remote control feature",
|
flagRemoteControlDesc: "Enable the experimental remote control feature",
|
||||||
flagSearchWorker: "Search Worker",
|
flagSearchWorker: "Search Worker",
|
||||||
flagSearchWorkerDesc: "Run the kap-server global search backend in a background worker host",
|
flagSearchWorkerDesc: "Run the kap-server global search backend in a background worker host",
|
||||||
flagFileHistory: "File History",
|
|
||||||
flagFileHistoryDesc:
|
|
||||||
"Snapshot edited files before/after each turn (last 5 turns across 30 sessions, stored in the session data dir) for real whole-file diffs",
|
|
||||||
flagDefaultOn: "On by default",
|
flagDefaultOn: "On by default",
|
||||||
lockedByEnv: "Locked by env var",
|
lockedByEnv: "Locked by env var",
|
||||||
masterEnvOnHint:
|
masterEnvOnHint:
|
||||||
|
|||||||
+1
-4
@@ -151,7 +151,7 @@ export const zhTranslations = {
|
|||||||
permissionAsk: "询问",
|
permissionAsk: "询问",
|
||||||
permissionDangerousGuard: "危险命令防护",
|
permissionDangerousGuard: "危险命令防护",
|
||||||
permissionDangerousGuardDesc:
|
permissionDangerousGuardDesc:
|
||||||
"开启(默认)时:Auto 模式直接拒绝危险命令(rm -rf、shutdown、dd of= 等),其它模式强制询问;关闭后回归旧行为。环境变量 KIMI_CODE_DANGEROUS_COMMAND_GUARD 会覆盖本配置",
|
"开启(默认)时:Manual/YOLO 模式下,危险命令(rm -rf、shutdown、dd of= 等)和无法静态分析的命令会强制询问;自动(永不询问)模式自 kimi-code 0.41.0 起不再拦截。关闭后回归旧行为。环境变量 KIMI_CODE_DANGEROUS_COMMAND_GUARD 会覆盖本配置",
|
||||||
addRule: "+ 添加规则",
|
addRule: "+ 添加规则",
|
||||||
addCommonRules: "添加常用规则",
|
addCommonRules: "添加常用规则",
|
||||||
hooks: "生命周期钩子",
|
hooks: "生命周期钩子",
|
||||||
@@ -477,9 +477,6 @@ export const zhTranslations = {
|
|||||||
flagRemoteControlDesc: "启用实验性远程控制功能",
|
flagRemoteControlDesc: "启用实验性远程控制功能",
|
||||||
flagSearchWorker: "搜索 Worker",
|
flagSearchWorker: "搜索 Worker",
|
||||||
flagSearchWorkerDesc: "kap-server 全局搜索的后台 worker 宿主服务",
|
flagSearchWorkerDesc: "kap-server 全局搜索的后台 worker 宿主服务",
|
||||||
flagFileHistory: "文件历史",
|
|
||||||
flagFileHistoryDesc:
|
|
||||||
"记录每轮编辑文件的前后快照(最近 30 个会话×最近 5 轮,存于会话数据目录),用于真实的整文件 diff",
|
|
||||||
flagDefaultOn: "默认开启",
|
flagDefaultOn: "默认开启",
|
||||||
lockedByEnv: "被环境变量锁定",
|
lockedByEnv: "被环境变量锁定",
|
||||||
masterEnvOnHint:
|
masterEnvOnHint:
|
||||||
|
|||||||
@@ -455,11 +455,11 @@ describe("validateSubagentPool", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// [experimental] — flag registry (kimi-code 0.40.1) + write semantics
|
// [experimental] — flag registry (kimi-code 0.41.0) + write semantics
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe("experimental flag registry", () => {
|
describe("experimental flag registry", () => {
|
||||||
it("mirrors the 0.40.1 v2 registry (10 flags incl. file_history)", () => {
|
it("mirrors the 0.41.0 v2 registry (9 flags, file_history removed)", () => {
|
||||||
expect(EXPERIMENTAL_FLAGS.map((f) => f.id)).toEqual([
|
expect(EXPERIMENTAL_FLAGS.map((f) => f.id)).toEqual([
|
||||||
"secondary-model",
|
"secondary-model",
|
||||||
"tool-select",
|
"tool-select",
|
||||||
@@ -470,11 +470,10 @@ describe("experimental flag registry", () => {
|
|||||||
"auto_session_title",
|
"auto_session_title",
|
||||||
"remote-control",
|
"remote-control",
|
||||||
"search_worker",
|
"search_worker",
|
||||||
"file_history",
|
|
||||||
]);
|
]);
|
||||||
const fh = EXPERIMENTAL_FLAGS.find((f) => f.id === "file_history");
|
// Upstream removed the flag in 0.41.0 (turn-level file history is
|
||||||
expect(fh?.envVar).toBe("KIMI_CODE_EXPERIMENTAL_FILE_HISTORY");
|
// always on), so the adapter must not mirror it either.
|
||||||
expect(fh?.defaultEnabled).toBeUndefined();
|
expect(EXPERIMENTAL_FLAGS.some((f) => f.id === "file_history")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("secondary-model is on by default since 0.40.1", () => {
|
it("secondary-model is on by default since 0.40.1", () => {
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ export interface ExperimentalFlagDef {
|
|||||||
* (the per-feature flag.ts files under packages/agent-core-v2/src:
|
* (the per-feature flag.ts files under packages/agent-core-v2/src:
|
||||||
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
|
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
|
||||||
* subagent_fork, wait_for, auto_session_title, remote-control,
|
* subagent_fork, wait_for, auto_session_title, remote-control,
|
||||||
* search_worker, file_history). `acp-v2` was removed upstream and is
|
* search_worker). `acp-v2` was removed upstream and is dropped here;
|
||||||
* dropped here. Keep the env-var list in sync with
|
* `file_history` was removed in kimi-code 0.41.0 (turn-level file
|
||||||
|
* history is always on, no flag). Keep the env-var list in sync with
|
||||||
* `get_experimental_env_status` in src-tauri/src/commands.rs. */
|
* `get_experimental_env_status` in src-tauri/src/commands.rs. */
|
||||||
export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
||||||
{
|
{
|
||||||
@@ -69,7 +70,6 @@ export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
|
|||||||
envVar: "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER",
|
envVar: "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER",
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{ id: "file_history", envVar: "KIMI_CODE_EXPERIMENTAL_FILE_HISTORY" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatAmount, localizeUsageError, planLabel } from "./usage-display";
|
||||||
|
|
||||||
|
// Minimal TranslateFn: return the key itself so assertions read the key.
|
||||||
|
const t = (key: string) => key;
|
||||||
|
|
||||||
|
describe("localizeUsageError", () => {
|
||||||
|
it("maps known Rust error prefixes to their localization keys", () => {
|
||||||
|
expect(
|
||||||
|
localizeUsageError("unsupported provider: no usage query available", t)
|
||||||
|
).toBe("usageUnsupportedProvider");
|
||||||
|
expect(localizeUsageError("no API key configured", t)).toBe("usageErrNoKey");
|
||||||
|
expect(localizeUsageError("usage query disabled in config panel", t)).toBe(
|
||||||
|
"usageErrDisabled"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
localizeUsageError("no Kimi Code OAuth credentials found; run `kimi login` first", t)
|
||||||
|
).toBe("usageErrNoOauth");
|
||||||
|
expect(
|
||||||
|
localizeUsageError("newapi template requires accessToken and userId", t)
|
||||||
|
).toBe("usageErrNewapiCreds");
|
||||||
|
expect(localizeUsageError("Authentication failed: 401 Unauthorized", t)).toBe(
|
||||||
|
"usageInvalidKey"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("login expired / refresh failed match case-insensitively anywhere", () => {
|
||||||
|
expect(localizeUsageError("LOGIN expired at 12:00", t)).toBe("usageErrLoginExpired");
|
||||||
|
expect(localizeUsageError("oauth refresh failed: 500", t)).toBe("usageErrLoginExpired");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips the kinds-loop `{kind}: ` wrapper before matching", () => {
|
||||||
|
// commands.rs 的 kinds 循环把错误包成 "balance:newapi: ..." —— 必须仍命中。
|
||||||
|
expect(
|
||||||
|
localizeUsageError(
|
||||||
|
"balance:newapi: newapi template requires accessToken and userId",
|
||||||
|
t
|
||||||
|
)
|
||||||
|
).toBe("usageErrNewapiCreds");
|
||||||
|
expect(localizeUsageError("plan:zhipu: Authentication failed: 401", t)).toBe(
|
||||||
|
"usageInvalidKey"
|
||||||
|
);
|
||||||
|
expect(localizeUsageError("balance:deepseek: no API key configured", t)).toBe(
|
||||||
|
"usageErrNoKey"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes unknown errors through unchanged (kind prefix retained)", () => {
|
||||||
|
const raw = "balance:openrouter: HTTP 429 too many requests";
|
||||||
|
expect(localizeUsageError(raw, t)).toBe(raw);
|
||||||
|
expect(localizeUsageError("HTTP 500 Something broke", t)).toBe(
|
||||||
|
"HTTP 500 Something broke"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("planLabel", () => {
|
||||||
|
it("maps known tier ids and passes unknown names through", () => {
|
||||||
|
expect(planLabel("five_hour", t)).toBe("usageTier5h");
|
||||||
|
expect(planLabel("weekly_limit", t)).toBe("usageTierWeekly");
|
||||||
|
expect(planLabel("monthly_limit", t)).toBe("usageTierMonthly");
|
||||||
|
expect(planLabel("NewAPI", t)).toBe("NewAPI");
|
||||||
|
expect(planLabel("¥", t)).toBe("¥");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatAmount", () => {
|
||||||
|
it("maps CNY and USD to ¥/$ with two decimals", () => {
|
||||||
|
expect(formatAmount(1.5, "CNY")).toBe("¥1.50");
|
||||||
|
expect(formatAmount(1.5, "USD")).toBe("$1.50");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats ¤ (U+00A4) as no configured currency symbol", () => {
|
||||||
|
expect(formatAmount(136907472.01013, "¤")).toBe("136907472.01");
|
||||||
|
expect(formatAmount(136907472.01013, "\u{00A4}")).toBe("136907472.01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefixes arbitrary units without a symbol", () => {
|
||||||
|
// Non-integer so the two-decimal rule applies; rounds to 3.00.
|
||||||
|
expect(formatAmount(3.004, "credit")).toBe("credit 3.00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders null/undefined values as —", () => {
|
||||||
|
expect(formatAmount(null, "¤")).toBe("—");
|
||||||
|
expect(formatAmount(undefined, "CNY")).toBe("—");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps integers without decimals", () => {
|
||||||
|
expect(formatAmount(42, "CNY")).toBe("¥42");
|
||||||
|
expect(formatAmount(100, "USD")).toBe("$100");
|
||||||
|
expect(formatAmount(7, "")).toBe("7");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,12 +35,32 @@ export function planLabel(name: string, t: TranslateFn): string {
|
|||||||
* src-tauri/src/commands.rs and src-tauri/src/services/*.
|
* src-tauri/src/commands.rs and src-tauri/src/services/*.
|
||||||
*/
|
*/
|
||||||
export function localizeUsageError(error: string, t: TranslateFn): string {
|
export function localizeUsageError(error: string, t: TranslateFn): string {
|
||||||
if (error.startsWith("unsupported provider")) return t("usageUnsupportedProvider");
|
// commands.rs 的 kinds 循环把单条错误包成 `{kind}: {msg}`(kind 含数字,
|
||||||
if (error.startsWith("no API key configured")) return t("usageErrNoKey");
|
// 如 plan:zhipu),先剥离再按前缀匹配;未知错误仍回退按原文展示(保留 kind
|
||||||
if (error.startsWith("usage query disabled")) return t("usageErrDisabled");
|
// 前缀,属诊断细节)。
|
||||||
if (/login expired|refresh failed/i.test(error)) return t("usageErrLoginExpired");
|
const stripped = error.replace(/^(balance|plan):[a-z0-9_]+:\s*/, "");
|
||||||
if (error.startsWith("no Kimi Code OAuth credentials")) return t("usageErrNoOauth");
|
if (stripped.startsWith("unsupported provider")) return t("usageUnsupportedProvider");
|
||||||
if (error.startsWith("newapi template requires")) return t("usageErrNewapiCreds");
|
if (stripped.startsWith("no API key configured")) return t("usageErrNoKey");
|
||||||
if (error.startsWith("Authentication failed")) return t("usageInvalidKey");
|
if (stripped.startsWith("usage query disabled")) return t("usageErrDisabled");
|
||||||
|
if (/login expired|refresh failed/i.test(stripped)) return t("usageErrLoginExpired");
|
||||||
|
if (stripped.startsWith("no Kimi Code OAuth credentials")) return t("usageErrNoOauth");
|
||||||
|
if (stripped.startsWith("newapi template requires")) return t("usageErrNewapiCreds");
|
||||||
|
if (stripped.startsWith("Authentication failed")) return t("usageInvalidKey");
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a usage amount with its unit. `¤` (U+00A4) is NewAPI's placeholder
|
||||||
|
* when the site has no custom currency symbol configured — treat it as "no
|
||||||
|
* symbol" and render only the number.
|
||||||
|
*/
|
||||||
|
export function formatAmount(
|
||||||
|
value: number | null | undefined,
|
||||||
|
unit?: string | null
|
||||||
|
): string {
|
||||||
|
if (value == null) return "—";
|
||||||
|
const symbol =
|
||||||
|
unit === "CNY" ? "¥" : unit === "USD" ? "$" : unit && unit.trim() !== "¤" ? `${unit} ` : "";
|
||||||
|
const num = Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||||
|
return `${symbol}${num}`;
|
||||||
|
}
|
||||||
+5
-3
@@ -151,9 +151,11 @@ export interface AgentSettings {
|
|||||||
background?: BackgroundConfig;
|
background?: BackgroundConfig;
|
||||||
permission?: {
|
permission?: {
|
||||||
rules?: PermissionRule[];
|
rules?: PermissionRule[];
|
||||||
/** kimi-code 0.40.1 `[permission]` key. Default true when the key is
|
/** kimi-code `[permission]` key. Default true when the key is
|
||||||
* absent: Auto mode refuses dangerous commands (rm -rf, shutdown,
|
* absent: since 0.41.0 the guard only gates Manual/YOLO modes —
|
||||||
* dd of=, …) and other modes always prompt; false restores the
|
* dangerous commands (rm -rf, shutdown, dd of=, …) and commands
|
||||||
|
* that cannot be statically analyzed force a prompt, while Auto
|
||||||
|
* (never-ask) mode no longer intercepts them; false restores the
|
||||||
* previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal
|
* previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal
|
||||||
* "true"/"false" only) outranks this config. */
|
* "true"/"false" only) outranks this config. */
|
||||||
dangerous_command_guard?: boolean;
|
dangerous_command_guard?: boolean;
|
||||||
|
|||||||
Reference in new issue
Block a user