本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
/**
|
||
* Token 估算工具 — 跨 Provider 通用
|
||
*
|
||
* 策略:智能字符估算,区分中文字符与 ASCII 字符
|
||
* - 中文字符(含全角标点、日韩文):1 字符 ≈ 1.5 token
|
||
* - ASCII 字符(英文、数字、半角符号):4 字符 ≈ 1 token
|
||
* - 其他 Unicode(emoji 等):1 字符 ≈ 1 token
|
||
*
|
||
* 对比旧的 `length / 2` 方案:
|
||
* - 中文场景:估算准确度从 ~50% 提升到 ~90%
|
||
* - 英文场景:从偏低变为接近真实
|
||
* - 混合场景:更贴近实际 token 消耗
|
||
*
|
||
* 仍为估算值(无 tiktoken 依赖),但留了 80% 触发阈值的缓冲。
|
||
*/
|
||
|
||
// 中日韩统一表意文字 + 全角标点 + 日文假名 + 韩文谚文
|
||
const CJK_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/;
|
||
|
||
/**
|
||
* L-17 修复: 提取魔法系数为命名常量,便于统一调整
|
||
* @see project_memory.md — Token estimation coefficients
|
||
*/
|
||
const CJK_TOKEN_RATIO = 1.5; // 中文字符(含全角标点、日韩文):1 字符 ≈ 1.5 token
|
||
const ASCII_TOKEN_RATIO = 0.25; // ASCII 字符(英文、数字、半角符号):4 字符 ≈ 1 token
|
||
const OTHER_TOKEN_RATIO = 1; // 其他 Unicode(emoji 等):1 字符 ≈ 1 token
|
||
const MSG_OVERHEAD_TOKENS = 4; // 每条消息的结构性开销(role、分隔符,参考 OpenAI 规范)
|
||
|
||
/**
|
||
* 估算字符串的 token 数
|
||
* @param text 待估算的字符串(可为 null/undefined,视为 0 token)
|
||
* @returns 估算的 token 数
|
||
*/
|
||
export function estimateStringTokens(text: string | null | undefined): number {
|
||
if (!text || text.length === 0) return 0;
|
||
|
||
let cjkCount = 0;
|
||
let asciiCount = 0;
|
||
let otherCount = 0;
|
||
|
||
for (const ch of text) {
|
||
if (CJK_REGEX.test(ch)) {
|
||
cjkCount++;
|
||
} else if (ch.charCodeAt(0) < 128) {
|
||
asciiCount++;
|
||
} else {
|
||
otherCount++;
|
||
}
|
||
}
|
||
|
||
// L-17 修复: 使用命名常量替代魔法数字
|
||
return Math.ceil(cjkCount * CJK_TOKEN_RATIO + asciiCount * ASCII_TOKEN_RATIO + otherCount * OTHER_TOKEN_RATIO);
|
||
}
|
||
|
||
/**
|
||
* 估算多条消息的总 token 数
|
||
*
|
||
* 每条消息额外加 4 token 的结构性开销(role、分隔符等,参考 OpenAI 规范)
|
||
*
|
||
* @param messages 消息列表(content 可为 null,对应仅有 tool_calls 的 assistant 消息)
|
||
* @returns 估算的 token 数
|
||
*/
|
||
export function estimateMessagesTokens(messages: Array<{
|
||
content: string | null;
|
||
reasoningContent?: string;
|
||
toolCalls?: Array<{ args: Record<string, unknown> }>;
|
||
}>): number {
|
||
let total = 0;
|
||
for (const msg of messages) {
|
||
total += estimateStringTokens(msg.content);
|
||
if (msg.reasoningContent) total += estimateStringTokens(msg.reasoningContent);
|
||
if (msg.toolCalls) {
|
||
for (const tc of msg.toolCalls) {
|
||
total += estimateStringTokens(JSON.stringify(tc.args));
|
||
}
|
||
}
|
||
// L-17 修复: 使用命名常量替代魔法数字
|
||
total += MSG_OVERHEAD_TOKENS;
|
||
}
|
||
return total;
|
||
}
|