- 新增 UnitInput 与 kind=tokens:上下文预算、单次 token 上限、自定义模型上下文窗口以 K 输入(允许小数),存储仍为原始 token 数;校验与提示用 K 表述;列表/详情 token 数统一 K/M 简写(formatTokens) - 新增 SecretInput 与 kind=secret:已保存时显示「已设置 ••••••••」(有服务端掩码时带末 4 位),聚焦直接输入替换,留空保持,「清除」确认后保存生效;替换模型服务、TMDB、网页搜索、下载器、媒体服务器、浏览器服务、站点账号、Passkey、本机 Token 各处 - 统一只写凭证服务端语义:省略或空串 = 不变,null = 清除(contracts secretInput);下载器/媒体服务器新增清除;站点凭证同类更新可留空保持密码/Cookie/API Key/TOTP,TOTP 可 null 清除,不再误删 TOTP 与 UA - contextBudget 最小值 4096→4000、contextWindow 1024→1000,便于按 K 填写 - 测试:换算与校验、SecretInput 渲染与状态、各接口留空不变/显式清除 Co-Authored-By: Claude Opus 5.5 <[email protected]>
59 lines
2.6 KiB
TypeScript
59 lines
2.6 KiB
TypeScript
// token 数在界面上以 K(千)为单位输入与显示;存储与 API 仍是原始 token 数,只在前端换算。
|
||
// 1K = 1000 token(模型目录的上下文窗口也是按 1000 计,如 128K = 128000)。
|
||
|
||
export const TOKENS_PER_K = 1000;
|
||
|
||
/** 去掉小数末尾的 0:1.50 → "1.5",2.00 → "2" */
|
||
const trim = (n: number, digits: number) => String(Number(n.toFixed(digits)));
|
||
|
||
/** 列表 / 详情里的 token 数:<1000 原样,千级 "128K",百万级 "1.5M" */
|
||
export function formatTokens(n: number | null | undefined): string {
|
||
if (n === null || n === undefined || !Number.isFinite(n)) return "—";
|
||
const abs = Math.abs(n);
|
||
if (abs < TOKENS_PER_K) return String(Math.round(n));
|
||
if (abs < 1_000_000) {
|
||
const k = n / TOKENS_PER_K;
|
||
// 999.95K 以上四舍五入会变成 1000K,直接进位到 M
|
||
if (Math.abs(Number(k.toFixed(1))) < 1000) return `${trim(k, Math.abs(k) < 10 ? 2 : 1)}K`;
|
||
}
|
||
return `${trim(n / 1_000_000, 2)}M`;
|
||
}
|
||
|
||
/** 原始 token 数 → 输入框里的 K 值字符串(null → 空串);最多 3 位小数,正好换回原值 */
|
||
export function tokensToK(n: number | null | undefined): string {
|
||
if (n === null || n === undefined || !Number.isFinite(n)) return "";
|
||
return trim(n / TOKENS_PER_K, 3);
|
||
}
|
||
|
||
/**
|
||
* 输入框里的 K 值 → 原始 token 数。
|
||
* 空串 → undefined;不是数字 → NaN(交给校验报错);其余四舍五入到整数 token。
|
||
*/
|
||
export function kToTokens(input: string): number | undefined {
|
||
const s = input.trim().replace(/[kK]$/, "").trim();
|
||
if (s === "") return undefined;
|
||
if (!/^\d+(\.\d+)?$|^\.\d+$/.test(s)) return Number.NaN;
|
||
return Math.round(Number(s) * TOKENS_PER_K);
|
||
}
|
||
|
||
export type TokenRange = { min?: number; max?: number; required?: boolean };
|
||
|
||
/** 范围说明:{min:4000,max:2000000} → "4K–2000K" */
|
||
export function tokenRangeText(r: TokenRange): string | null {
|
||
if (r.min !== undefined && r.max !== undefined) return `${tokensToK(r.min)}K–${tokensToK(r.max)}K`;
|
||
if (r.min !== undefined) return `${tokensToK(r.min)}K 以上`;
|
||
if (r.max !== undefined) return `不超过 ${tokensToK(r.max)}K`;
|
||
return null;
|
||
}
|
||
|
||
/** 校验 K 值输入;通过返回 null,否则返回一句错误 */
|
||
export function tokenInputError(input: string, r: TokenRange = {}): string | null {
|
||
const t = kToTokens(input);
|
||
if (t === undefined) return r.required ? "填一个数,单位 K" : null;
|
||
const range = tokenRangeText(r);
|
||
if (Number.isNaN(t)) return "只填数字,单位 K,如 128 或 1.5";
|
||
if (t <= 0 || (r.min !== undefined && t < r.min) || (r.max !== undefined && t > r.max))
|
||
return range ? `填 ${range}` : "填大于 0 的数";
|
||
return null;
|
||
}
|