Files
AIclinicalresearch/backend/src/modules/pkb/services/tokenService.ts
HaHafeng 5a17d096a7 feat(pkb): Complete PKB module frontend migration with V3 design
Summary:
- Implement PKB Dashboard and Workspace pages based on V3 prototype
- Add single-layer header with integrated Tab navigation
- Implement 3 work modes: Full Text, Deep Read, Batch Processing
- Integrate Ant Design X Chat component for AI conversations
- Create BatchModeComplete with template selection and document processing
- Add compact work mode selector with dropdown design

Backend:
- Migrate PKB controllers and services to /modules/pkb structure
- Register v2 API routes at /api/v2/pkb/knowledge
- Maintain dual API routes for backward compatibility

Technical details:
- Use Zustand for state management
- Handle SSE streaming responses for AI chat
- Support document selection for Deep Read mode
- Implement batch processing with progress tracking

Known issues:
- Batch processing API integration pending
- Knowledge assets page navigation needs optimization

Status: Frontend functional, pending refinement
2026-01-06 22:15:42 +08:00

233 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { encoding_for_model, Tiktoken } from 'tiktoken';
/**
* Token计数服务
* 用于全文阅读模式的Token管理
*/
// Token限制配置
export const TOKEN_LIMITS = {
MAX_FILES: 50, // 最多50个文件
MAX_TOTAL_TOKENS: 980000, // 最多980K tokens为Qwen-Long 1M上下文留20K余量
CONTEXT_RESERVE: 20000, // 预留给系统提示词和用户查询的token
};
// 缓存编码器
let encoderCache: Tiktoken | null = null;
/**
* 获取编码器使用gpt-4作为Qwen的替代
*/
function getEncoder(): Tiktoken {
if (!encoderCache) {
// Qwen使用类似GPT-4的tokenizer
encoderCache = encoding_for_model('gpt-4');
}
return encoderCache;
}
/**
* 计算文本的Token数
*/
export function countTokens(text: string): number {
if (!text || text.trim().length === 0) {
return 0;
}
try {
const encoder = getEncoder();
const tokens = encoder.encode(text);
return tokens.length;
} catch (error) {
console.error('[TokenService] Failed to count tokens:', error);
// 降级粗略估算中文约1.5字符/token英文约4字符/token
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const totalChars = text.length;
const englishChars = totalChars - chineseChars;
return Math.ceil(chineseChars / 1.5 + englishChars / 4);
}
}
/**
* 批量计算多个文本的Token数
*/
export function countTokensBatch(texts: string[]): number[] {
return texts.map(text => countTokens(text));
}
/**
* 计算文档Token数基于提取的文本
*/
export interface DocumentTokenInfo {
documentId: string;
filename: string;
charCount: number;
estimatedTokens: number;
extractionMethod?: string;
}
/**
* 为文档列表计算Token数
*/
export function calculateDocumentTokens(
documents: Array<{
id: string;
filename: string;
extractedText?: string | null;
charCount?: number | null;
extractionMethod?: string | null;
}>
): DocumentTokenInfo[] {
return documents.map(doc => {
let estimatedTokens = 0;
if (doc.extractedText) {
// 使用提取的文本计算精确token数
estimatedTokens = countTokens(doc.extractedText);
} else if (doc.charCount) {
// 如果没有提取文本,使用字符数估算
// 假设中英文混合平均2.5字符/token
estimatedTokens = Math.ceil(doc.charCount / 2.5);
}
return {
documentId: doc.id,
filename: doc.filename,
charCount: doc.charCount || 0,
estimatedTokens,
extractionMethod: doc.extractionMethod || undefined,
};
});
}
/**
* 选择文档以满足Token限制
* 策略优先选择Token数少的文档直到达到限制
*/
export interface DocumentSelectionResult {
selectedDocuments: DocumentTokenInfo[];
totalTokens: number;
totalFiles: number;
excludedDocuments: DocumentTokenInfo[];
reason: 'all_included' | 'file_limit' | 'token_limit';
availableTokens: number;
}
export function selectDocumentsForFullText(
documents: DocumentTokenInfo[],
maxFiles: number = TOKEN_LIMITS.MAX_FILES,
maxTokens: number = TOKEN_LIMITS.MAX_TOTAL_TOKENS
): DocumentSelectionResult {
// 按Token数升序排序优先选择小文件
const sortedDocs = [...documents].sort(
(a, b) => a.estimatedTokens - b.estimatedTokens
);
const selected: DocumentTokenInfo[] = [];
const excluded: DocumentTokenInfo[] = [];
let totalTokens = 0;
for (const doc of sortedDocs) {
// 检查文件数限制
if (selected.length >= maxFiles) {
excluded.push(doc);
continue;
}
// 检查Token限制
if (totalTokens + doc.estimatedTokens > maxTokens) {
excluded.push(doc);
continue;
}
// 添加到选中列表
selected.push(doc);
totalTokens += doc.estimatedTokens;
}
// 判断限制原因
let reason: 'all_included' | 'file_limit' | 'token_limit' = 'all_included';
if (excluded.length > 0) {
if (selected.length >= maxFiles) {
reason = 'file_limit';
} else {
reason = 'token_limit';
}
}
return {
selectedDocuments: selected,
totalTokens,
totalFiles: selected.length,
excludedDocuments: excluded,
reason,
availableTokens: maxTokens - totalTokens,
};
}
/**
* 估算查询需要的Token数
*/
export function estimateQueryTokens(query: string, systemPrompt?: string): number {
let total = countTokens(query);
if (systemPrompt) {
total += countTokens(systemPrompt);
}
// 为响应预留空间
total += 2000; // 假设响应最多2000 tokens
return total;
}
/**
* 检查是否超过Token限制
*/
export function checkTokenLimit(
documentsTokens: number,
queryTokens: number,
maxTokens: number = TOKEN_LIMITS.MAX_TOTAL_TOKENS
): {
withinLimit: boolean;
totalTokens: number;
maxTokens: number;
remaining: number;
} {
const totalTokens = documentsTokens + queryTokens;
const remaining = maxTokens - totalTokens;
return {
withinLimit: remaining >= 0,
totalTokens,
maxTokens,
remaining,
};
}
/**
* 释放编码器(清理资源)
*/
export function cleanup() {
if (encoderCache) {
encoderCache.free();
encoderCache = null;
}
}
// 进程退出时清理
if (typeof process !== 'undefined') {
process.on('exit', cleanup);
process.on('SIGINT', () => {
cleanup();
process.exit();
});
}