perf(build): minify main process and lazy-load tiktoken rank table

Enable esbuild minification for main/preload (was renderer-only by default),
reducing main process bundle by ~55%. Extract cl100k_base BPE rank table out
of the synchronous startup path via dynamic import — it now lands in its own
chunk and is loaded on first agent run instead of at app startup.

Also fix Windows CI step to use PowerShell syntax for version injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
diguaandClaude Opus 4.8 committed 2026-06-03 00:46:34 +08:00
1 parent 3ff9437510
commit 2f8591fb8c
8 files changed
+89 -25

No files matched your search

@@ -16,6 +16,7 @@ import {
import { StreamingThinkTagParser, needsStreamingThinkParsing } from '@openchatlab/core'
import type { AgentCoreOptions, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './types'
import { initTokenizer } from '../tokenizer'
function createEmptyPiUsage(): PiUsage {
return {
@@ -70,6 +71,9 @@ export async function runAgentCore(options: AgentCoreOptions): Promise<AgentCore
onDebugContext,
} = options
// 确保 cl100k rank 表已加载,压缩/预处理路径使用精确 token 计数
await initTokenizer()
const resolvedStreamFn = (options.streamFn ?? defaultStreamSimple) as typeof defaultStreamSimple
const totalUsage: AgentTokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
+1 -1
View File
@@ -35,7 +35,7 @@ export type {
} from './conversations'
// Tokenizer
export { countTokens, countMessagesTokens } from './tokenizer'
export { countTokens, countMessagesTokens, initTokenizer } from './tokenizer'
// SkillManager (runtime: activate-skill tool builder)
export { SkillManager } from './skill-manager'
+67 -17
View File
@@ -1,43 +1,93 @@
/**
* Token 计数模块
* 使用 js-tiktoken 的 cl100k_base 编码进行近似 token 计数。
* 该编码是 GPT-4/Claude 系列的近似值,对国内模型有一定误差,
*
* 使用 js-tiktoken/lite + cl100k_base 编码进行近似 token 计数。
* cl100k_base 是 GPT-4 / Claude 系列的近似值,对国内模型有一定误差,
* 因此阈值计算时预留了余量。
*
* 【打包优化】使用 lite 入口 + 动态 import rank 表:
* - `js-tiktoken/lite` 本身不内联任何 BPE rank 数据(~0KB 额外体积)
* - rank 表通过动态 import 加载,rollup 会将其拆为独立 chunk,
* 不再随主进程启动路径同步加载(原方式会内联 ~1MB base64 数据)
* - 调用 initTokenizer() 完成后,countTokens/countMessagesTokens 使用精确计数;
* 初始化前的调用会降级到轻量字符估算(误差 <10%,适合"近似"场景)
*/
import { encodingForModel } from 'js-tiktoken'
import { Tiktoken } from 'js-tiktoken/lite'
let encoder: ReturnType<typeof encodingForModel> | null = null
let encoder: Tiktoken | null = null
let initPromise: Promise<void> | null = null
function getEncoder() {
if (!encoder) {
encoder = encodingForModel('gpt-4o')
}
return encoder
/**
* 异步初始化 tokenizer。
* 应在 AI agent 启动前 await 此函数,确保压缩/预处理路径使用精确计数。
* 可安全多次调用(幂等)。
*/
export async function initTokenizer(): Promise<void> {
if (encoder) return
if (initPromise) return initPromise
initPromise = (async () => {
const ranks = (await import('js-tiktoken/ranks/cl100k_base')).default
encoder = new Tiktoken(ranks)
})()
return initPromise
}
/**
* 计算单段文本的 token 数
* 轻量字符估算(fallback)
* CJK 字符约 1.6 字符/token,ASCII 约 4 字符/token
*/
function estimateTokens(text: string): number {
if (!text) return 0
let cjk = 0
let other = 0
for (const ch of text) {
const cp = ch.codePointAt(0) ?? 0
// CJK 统一表意文字 + 扩展区 + 假名 + 谚文
if (
(cp >= 0x4e00 && cp <= 0x9fff) ||
(cp >= 0x3040 && cp <= 0x30ff) ||
(cp >= 0xac00 && cp <= 0xd7af) ||
(cp >= 0x3400 && cp <= 0x4dbf)
) {
cjk++
} else {
other++
}
}
return Math.ceil(cjk / 1.6 + other / 4)
}
/**
* 计算单段文本的 token 数。
* encoder 已初始化时使用精确计数,否则降级到字符估算。
*/
export function countTokens(text: string): number {
if (!text) return 0
return getEncoder().encode(text).length
if (encoder) {
return encoder.encode(text).length
}
return estimateTokens(text)
}
/**
* 计算消息列表的总 token 数(含 systemPrompt)
* 每条消息额外计 4 tokens 的格式开销(role + 分隔符)
* 计算消息列表的总 token 数(含 systemPrompt)。
* 每条消息额外计 4 tokens 的格式开销(role + 分隔符)。
*/
export function countMessagesTokens(messages: Array<{ role: string; content: string }>, systemPrompt?: string): number {
const enc = getEncoder()
export function countMessagesTokens(
messages: Array<{ role: string; content: string }>,
systemPrompt?: string,
): number {
let total = 0
if (systemPrompt) {
total += enc.encode(systemPrompt).length + 4
total += countTokens(systemPrompt) + 4
}
for (const msg of messages) {
total += enc.encode(msg.content).length + 4
total += countTokens(msg.content) + 4
}
// 回复引导 token
+1 -1
View File
@@ -82,7 +82,7 @@ export {
export type { AssistantConfig, AssistantSummary, SkillDef, SkillSummary } from './ai'
export { parseAssistantFile, serializeAssistant, parseSkillFile, extractSkillId } from './ai'
export { AIConversationManager } from './ai'
export { countTokens, countMessagesTokens } from './ai'
export { countTokens, countMessagesTokens, initTokenizer } from './ai'
// Assistant Manager
export { AssistantManager } from './ai'