202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
// @ts-nocheck
|
|
|
|
|
|
const DEFAULT_THRESHOLD = 24000;
|
|
const DEFAULT_KEEP_RECENT = 6;
|
|
const TOOL_CONTENT_SOFT = 1800;
|
|
const TOOL_CONTENT_HARD = 800;
|
|
|
|
export function estimateTokens(messages, toolsCount = 0) {
|
|
let chars = 0;
|
|
for (const m of messages) {
|
|
if (typeof m.content === 'string') {
|
|
chars += m.content.length;
|
|
}
|
|
if (m.toolCalls) {
|
|
for (const tc of m.toolCalls) {
|
|
chars += JSON.stringify(tc).length;
|
|
}
|
|
}
|
|
chars += (m.role || '').length + 10;
|
|
}
|
|
chars += toolsCount * 2100;
|
|
return Math.ceil(chars / 3.5);
|
|
}
|
|
|
|
export function compressIfNeeded(messages, opts = {}) {
|
|
const threshold = opts.threshold || DEFAULT_THRESHOLD;
|
|
const keepRecent = opts.keepRecent || DEFAULT_KEEP_RECENT;
|
|
const toolsCount = opts.toolsCount || 0;
|
|
|
|
const originalTokens = estimateTokens(messages, toolsCount);
|
|
|
|
if (originalTokens <= threshold) {
|
|
return {
|
|
messages,
|
|
compressed: false,
|
|
originalTokens,
|
|
finalTokens: originalTokens,
|
|
hiddenCount: 0,
|
|
};
|
|
}
|
|
|
|
|
|
let working = slimToolMessages(messages, TOOL_CONTENT_SOFT);
|
|
let tokens = estimateTokens(working, toolsCount);
|
|
if (tokens <= threshold) {
|
|
return {
|
|
messages: working,
|
|
compressed: true,
|
|
originalTokens,
|
|
finalTokens: tokens,
|
|
hiddenCount: 0,
|
|
};
|
|
}
|
|
|
|
|
|
working = slimToolMessages(messages, TOOL_CONTENT_HARD);
|
|
tokens = estimateTokens(working, toolsCount);
|
|
if (tokens <= threshold) {
|
|
return {
|
|
messages: working,
|
|
compressed: true,
|
|
originalTokens,
|
|
finalTokens: tokens,
|
|
hiddenCount: 0,
|
|
};
|
|
}
|
|
|
|
|
|
const keepStart = findSafeKeepStart(working, keepRecent);
|
|
const last = working.slice(keepStart);
|
|
const lastUserIdx = (() => {
|
|
for (let i = working.length - 1; i >= 0; i--) {
|
|
if (working[i].role === 'user') return i;
|
|
}
|
|
return -1;
|
|
})();
|
|
|
|
const compressed = [];
|
|
if (lastUserIdx >= 0 && lastUserIdx < keepStart) {
|
|
compressed.push(working[lastUserIdx]);
|
|
}
|
|
|
|
const middleCount = lastUserIdx >= 0 && lastUserIdx < keepStart
|
|
? Math.max(0, keepStart - lastUserIdx - 1)
|
|
: Math.max(0, keepStart);
|
|
|
|
if (middleCount > 0) {
|
|
compressed.push({
|
|
role: 'system',
|
|
content: `[上下文已压缩] 已隐藏中间 ${middleCount} 条历史消息,只保留完整的最近工具调用组。请根据仍保留的工具结果回答用户;若信息不足请如实说明。`,
|
|
_compressed: true,
|
|
});
|
|
}
|
|
|
|
compressed.push(...last);
|
|
|
|
const finalTokens = estimateTokens(compressed, toolsCount);
|
|
|
|
return {
|
|
messages: compressed,
|
|
compressed: true,
|
|
originalTokens,
|
|
finalTokens,
|
|
hiddenCount: middleCount,
|
|
};
|
|
}
|
|
|
|
function findSafeKeepStart(messages, minKeep) {
|
|
if (!messages.length) return 0;
|
|
let start = Math.max(0, messages.length - minKeep);
|
|
|
|
|
|
while (start > 0 && messages[start].role === 'tool') {
|
|
start--;
|
|
}
|
|
|
|
|
|
if (messages[start]?.role === 'tool') {
|
|
while (start > 0 && messages[start].role === 'tool') start--;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return start;
|
|
}
|
|
|
|
function slimToolMessages(messages, maxLen) {
|
|
return messages.map(m => {
|
|
if (m.role !== 'tool') return m;
|
|
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
if (content.length <= maxLen) return m;
|
|
return {
|
|
...m,
|
|
content: slimToolContent(content, maxLen),
|
|
};
|
|
});
|
|
}
|
|
|
|
function slimToolContent(content, maxLen) {
|
|
try {
|
|
const parsed = JSON.parse(content);
|
|
const slim = slimToolObject(parsed);
|
|
const out = JSON.stringify(slim);
|
|
if (out.length <= maxLen) return out;
|
|
return out.slice(0, maxLen) + '…[已截断]';
|
|
} catch (_) {
|
|
return content.slice(0, maxLen) + '…[已截断]';
|
|
}
|
|
}
|
|
|
|
function slimToolObject(obj) {
|
|
if (!obj || typeof obj !== 'object') return obj;
|
|
if (Array.isArray(obj)) {
|
|
return obj.length > 3
|
|
? [...obj.slice(0, 3), `…另有 ${obj.length - 3} 条已省略`]
|
|
: obj;
|
|
}
|
|
|
|
const out = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
|
|
if (k === '_call' || k === 'guidance' || k === 'assistantGuidance' || k === 'ok' || k === 'error') {
|
|
out[k] = v;
|
|
continue;
|
|
}
|
|
if (k === 'result' && v && typeof v === 'object') {
|
|
out[k] = slimToolObject(v);
|
|
continue;
|
|
}
|
|
if (k === 'overview' || k === 'summary' || k === 'currentEntity' || k === 'lastQueryType') {
|
|
out[k] = typeof v === 'object' ? slimToolObject(v) : v;
|
|
continue;
|
|
}
|
|
if (Array.isArray(v)) {
|
|
if (v.length === 0) {
|
|
out[k] = v;
|
|
} else if (typeof v[0] !== 'object') {
|
|
out[k] = v.length > 8 ? [...v.slice(0, 8), `…+${v.length - 8}`] : v;
|
|
} else {
|
|
out[k] = {
|
|
_omitted: true,
|
|
count: v.length,
|
|
sample: slimToolObject(v[0]),
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
if (v && typeof v === 'object') {
|
|
out[k] = slimToolObject(v);
|
|
continue;
|
|
}
|
|
out[k] = v;
|
|
}
|
|
return out;
|
|
}
|