diff --git a/apps/desktop/plugins/static-i18n-plugin.ts b/apps/desktop/plugins/static-i18n-plugin.ts index 0623dc5..0fc23f3 100644 --- a/apps/desktop/plugins/static-i18n-plugin.ts +++ b/apps/desktop/plugins/static-i18n-plugin.ts @@ -9,7 +9,7 @@ import { normalizePath, type Plugin } from "vite" const traverse = traverseModule.default ?? traverseModule const SOURCE_LOCALE = "zh-CN" -const SUPPORTED_LOCALES = ["zh-CN", "en-US"] +const SUPPORTED_LOCALES = ["zh-CN", "en-US", "pt-BR"] const CHINESE_SOURCE_PATTERN = /[\u3400-\u9fff]/u const PLACEHOLDER_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g const AUTO_IMPORT_NAME = "__staticI18nT" diff --git a/apps/desktop/src/features/home/metrics/HomeMetrics.tsx b/apps/desktop/src/features/home/metrics/HomeMetrics.tsx index 995c405..4cf32bd 100644 --- a/apps/desktop/src/features/home/metrics/HomeMetrics.tsx +++ b/apps/desktop/src/features/home/metrics/HomeMetrics.tsx @@ -1,4 +1,5 @@ import { formatCompactInteger, formatInteger } from "../../../shared/utils/numberFormat"; +import { useAppStore } from "../../../shared/store/appStore"; import { Icon } from "../../../shared/ui/Icon"; import { useTooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip"; import { informationOutlineIcon } from "../../../shared/ui/icons"; @@ -15,13 +16,6 @@ export type HomeMetricsData = { cacheWriteTokens: number; }; -const TOKEN_PRICE_PER_MILLION = { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, -} as const; - function formatMetricValue(value: number) { const full = formatInteger(value); const compact = formatCompactInteger(value); @@ -66,6 +60,7 @@ function InfoTooltip({ content }: { content: string }) { } export function HomeMetrics({ data, refreshVersion = 0 }: { data: HomeMetricsData; refreshVersion?: number }) { + const { pricing } = useAppStore(); const inputTokens = Math.max(0, data.promptTokens - data.cacheReadTokens - data.cacheWriteTokens); const outputTokens = Math.max(0, data.tokenUsage - data.promptTokens); const defaultCacheHitRate = calculateRate(data.cacheReadTokens, data.cacheReadTokens + inputTokens); @@ -75,10 +70,10 @@ export function HomeMetrics({ data, refreshVersion = 0 }: { data: HomeMetricsDat ); const successfulCallRate = calculateRate(data.successfulCalls, data.llmCalls); const costs = { - input: priceTokens(inputTokens, TOKEN_PRICE_PER_MILLION.input), - output: priceTokens(outputTokens, TOKEN_PRICE_PER_MILLION.output), - cacheRead: priceTokens(data.cacheReadTokens, TOKEN_PRICE_PER_MILLION.cacheRead), - cacheWrite: priceTokens(data.cacheWriteTokens, TOKEN_PRICE_PER_MILLION.cacheWrite), + input: priceTokens(inputTokens, pricing.input_per_million), + output: priceTokens(outputTokens, pricing.output_per_million), + cacheRead: priceTokens(data.cacheReadTokens, pricing.cache_read_per_million), + cacheWrite: priceTokens(data.cacheWriteTokens, pricing.cache_write_per_million), }; const totalCost = costs.input + costs.output + costs.cacheRead + costs.cacheWrite; const cacheCost = costs.cacheRead + costs.cacheWrite; @@ -111,27 +106,27 @@ export function HomeMetrics({ data, refreshVersion = 0 }: { data: HomeMetricsDat t("缓存读写已计入提示词侧统计。"), ].join("\n"); const costTooltip = [ - t("按 Claude Opus 4.7 价格估算。"), + t("按配置的 Token 价格估算。"), t("缓存统计策略:默认口径({rate})", { rate: formatRate(defaultCacheHitRate) }), "", t("普通输入:{tokens} × ${price}/1M = {cost}", { tokens: formatMetricValue(inputTokens), - price: TOKEN_PRICE_PER_MILLION.input, + price: pricing.input_per_million, cost: formatUSD(costs.input), }), t("模型输出:{tokens} × ${price}/1M = {cost}", { tokens: formatMetricValue(outputTokens), - price: TOKEN_PRICE_PER_MILLION.output, + price: pricing.output_per_million, cost: formatUSD(costs.output), }), t("缓存读取:{tokens} × ${price}/1M = {cost}", { tokens: formatMetricValue(data.cacheReadTokens), - price: TOKEN_PRICE_PER_MILLION.cacheRead, + price: pricing.cache_read_per_million, cost: formatUSD(costs.cacheRead), }), t("缓存写入:{tokens} × ${price}/1M = {cost}", { tokens: formatMetricValue(data.cacheWriteTokens), - price: TOKEN_PRICE_PER_MILLION.cacheWrite, + price: pricing.cache_write_per_million, cost: formatUSD(costs.cacheWrite), }), "", diff --git a/apps/desktop/src/features/settings/PricingSettingsCard.tsx b/apps/desktop/src/features/settings/PricingSettingsCard.tsx new file mode 100644 index 0000000..2ae5cde --- /dev/null +++ b/apps/desktop/src/features/settings/PricingSettingsCard.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from "react"; +import type { TokenPricingSettings } from "../../shared/api"; +import { appStore, DEFAULT_TOKEN_PRICING, useAppStore } from "../../shared/store/appStore"; +import { Button } from "../../shared/ui/Button"; +import { FormField, TextInput } from "../../shared/ui/FormControls"; +import { TitledCard } from "../../shared/ui/TitledCard"; +import { useMessage } from "../../shared/ui/message"; +import styles from "./ProxySettingsCard.module.scss"; + +type PricingDraft = { + input_per_million: string; + output_per_million: string; + cache_read_per_million: string; + cache_write_per_million: string; +}; + +function toDraft(pricing: TokenPricingSettings): PricingDraft { + return { + input_per_million: String(pricing.input_per_million), + output_per_million: String(pricing.output_per_million), + cache_read_per_million: String(pricing.cache_read_per_million), + cache_write_per_million: String(pricing.cache_write_per_million), + }; +} + +function formatPrice(value: number) { + return `$${value}`; +} + +function parsePrice(value: string, label: string) { + const price = Number(value); + if (!Number.isFinite(price) || price < 0) { + throw new Error(t("{label}必须是非负数", { label })); + } + return price; +} + +function toSettings(draft: PricingDraft): TokenPricingSettings { + return { + input_per_million: parsePrice(draft.input_per_million, t("输入价格")), + output_per_million: parsePrice(draft.output_per_million, t("输出价格")), + cache_read_per_million: parsePrice(draft.cache_read_per_million, t("缓存读取价格")), + cache_write_per_million: parsePrice(draft.cache_write_per_million, t("缓存写入价格")), + }; +} + +export function PricingSettingsCard() { + const { pricing } = useAppStore(); + const message = useMessage(); + const [draft, setDraft] = useState(() => toDraft(pricing)); + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!editing) setDraft(toDraft(pricing)); + }, [pricing, editing]); + + const edit = () => { + setDraft(toDraft(pricing)); + setEditing(true); + }; + + const cancel = () => { + setDraft(toDraft(pricing)); + setEditing(false); + }; + + const save = async () => { + try { + setSaving(true); + const next = toSettings(draft); + if (await appStore.updatePricingSettings(next)) { + setEditing(false); + message(t("定价设置已保存")); + } + } catch (cause) { + message(cause instanceof Error ? cause.message : String(cause)); + } finally { + setSaving(false); + } + }; + + const restoreDefault = () => { + setDraft(toDraft(DEFAULT_TOKEN_PRICING)); + }; + + const action = editing ? ( +
+ + + +
+ ) : ( + + ); + + return ( + +
+ {t("用于首页价值估算的 Token 单价,单位:美元 / 百万 Token。")} + {editing ? ( +
+ + setDraft({ ...draft, input_per_million: event.target.value })} + /> + + + setDraft({ ...draft, output_per_million: event.target.value })} + /> + + + setDraft({ ...draft, cache_read_per_million: event.target.value })} + /> + + + setDraft({ ...draft, cache_write_per_million: event.target.value })} + /> + +
+ ) : ( + <> +
+ {t("输入价格($/1M)")} + {formatPrice(pricing.input_per_million)} +
+
+ {t("输出价格($/1M)")} + {formatPrice(pricing.output_per_million)} +
+
+ {t("缓存读取价格($/1M)")} + {formatPrice(pricing.cache_read_per_million)} +
+
+ {t("缓存写入价格($/1M)")} + {formatPrice(pricing.cache_write_per_million)} +
+ + )} +
+
+ ); +} diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx index 4f05143..700c416 100644 --- a/apps/desktop/src/features/settings/SettingsPage.tsx +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -4,6 +4,7 @@ import { PageContent } from "../../shell/layout/PageContent"; import { LegacyModelImport } from "../models/LegacyModelImport"; import { AppLifecycleSettingsCard } from "./AppLifecycleSettingsCard"; import { CommitSettingsCard } from "./CommitSettingsCard"; +import { PricingSettingsCard } from "./PricingSettingsCard"; import { ProxySettingsCard } from "./ProxySettingsCard"; import { TabSettingsCard } from "./TabSettingsCard"; import { Button } from "../../shared/ui/Button"; @@ -231,6 +232,7 @@ export function SettingsPage() { void saveProxy()} /> void saveTab()} /> + {({ busy, previewing, open }) =>
@@ -247,7 +249,7 @@ export function SettingsPage() {
{t("界面语言")} - {t("默认跟随操作系统;不支持的系统语言使用英文。当前:{language}", { language: locale === "zh-CN" ? "简体中文" : "English" })} + {t("默认跟随操作系统;不支持的系统语言使用英文。当前:{language}", { language: locale === "zh-CN" ? "简体中文" : locale === "pt-BR" ? "Português (Brasil)" : "English" })}