foliko 2.0.26 → 2.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/chat.js +225 -161
- package/src/common/constants.js +2 -2
- package/src/llm/tokens.js +107 -42
package/package.json
CHANGED
package/src/agent/chat.js
CHANGED
|
@@ -718,44 +718,31 @@ class AgentChatHandler extends EventEmitter {
|
|
|
718
718
|
async *chatStream(message, options = {}) {
|
|
719
719
|
const sessionId = options.sessionId || this.getDefaultSessionId();
|
|
720
720
|
const framework = this.agent.framework;
|
|
721
|
-
// ★ 提到外层作用域,catch 中要回滚 userMessage 时需要访问
|
|
722
721
|
let messageStore = null;
|
|
723
722
|
let messages = null;
|
|
724
723
|
let userMessage = null;
|
|
724
|
+
let didRetryWithCompression = false;
|
|
725
725
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
messages = prepared.messages;
|
|
730
|
-
userMessage = prepared.userMessage;
|
|
731
|
-
|
|
732
|
-
if (!this._aiClient) {
|
|
733
|
-
throw new Error('AI 客户端未配置');
|
|
734
|
-
}
|
|
735
|
-
const systemPrompt = framework.prompts.build();
|
|
736
|
-
//await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
|
|
737
|
-
const tools = this._getToolMap();
|
|
726
|
+
// 内部异步函数执行真正的流式处理,便于压缩重试时重启
|
|
727
|
+
const doStream = async function* (self, sessionId, message, messageStore, messages, userMessage) {
|
|
728
|
+
const tools = self._getToolMap();
|
|
738
729
|
const toolLoop = new ToolLoop({
|
|
739
730
|
tools,
|
|
740
|
-
maxSteps:
|
|
741
|
-
prepareStep:
|
|
742
|
-
repairToolCall:
|
|
731
|
+
maxSteps: self._maxSteps || 1000,
|
|
732
|
+
prepareStep: self._createPrepareStep(),
|
|
733
|
+
repairToolCall: self._repairToolCall.bind(self),
|
|
743
734
|
abortSignal: framework.getAbortSignal?.(),
|
|
744
|
-
thinkingMode:
|
|
735
|
+
thinkingMode: self._thinkingMode,
|
|
745
736
|
});
|
|
746
737
|
|
|
747
|
-
// DeepSeek thinking mode: 收集 reasoning_content
|
|
748
|
-
const isThinking = this._thinkingMode;
|
|
749
738
|
let reasoningContent = '';
|
|
750
|
-
|
|
751
739
|
const stream = toolLoop.stream({
|
|
752
740
|
messages,
|
|
753
|
-
systemPrompt,
|
|
754
|
-
model:
|
|
755
|
-
client:
|
|
756
|
-
...
|
|
741
|
+
systemPrompt: framework.prompts.build(),
|
|
742
|
+
model: self.model,
|
|
743
|
+
client: self._aiClient,
|
|
744
|
+
...self.providerOptions,
|
|
757
745
|
});
|
|
758
|
-
// 遍历流式响应
|
|
759
746
|
let fullText = '';
|
|
760
747
|
let usageRecv = {};
|
|
761
748
|
|
|
@@ -778,67 +765,94 @@ class AgentChatHandler extends EventEmitter {
|
|
|
778
765
|
yield { type: 'usage', inputTokens: usageRecv.inputTokens, outputTokens: usageRecv.outputTokens };
|
|
779
766
|
}
|
|
780
767
|
}
|
|
768
|
+
|
|
781
769
|
// 持久化用量
|
|
782
770
|
messageStore.usage = usageRecv;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (this.agent && this.agent.framework) {
|
|
786
|
-
this.agent.framework.emit('agent:usage', {
|
|
787
|
-
sessionId,
|
|
788
|
-
usage: usageRecv,
|
|
789
|
-
});
|
|
771
|
+
if (self.agent && self.agent.framework) {
|
|
772
|
+
self.agent.framework.emit('agent:usage', { sessionId, usage: usageRecv });
|
|
790
773
|
}
|
|
791
774
|
|
|
792
|
-
// yield usage 数据,让 UI 层能直接获取
|
|
793
|
-
// (usage 已在 stream 循环中 yield,无需重复)
|
|
794
|
-
|
|
795
775
|
const userMsg = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
796
|
-
|
|
776
|
+
self.emit('message', { content: fullText, sessionId, userMessage: userMsg });
|
|
797
777
|
|
|
798
|
-
// ★ 修复:成功路径才持久化。AI 失败时若 finally 也 save,
|
|
799
|
-
// 会把刚 push 的 userMessage 写盘 → 下次请求 loadHistory 再次
|
|
800
|
-
// load 到,_prepareSession 再 push 一份 → 连续两条相同 user
|
|
801
|
-
// → AI SDK 抛 "Invalid prompt: ModelMessage[] schema"
|
|
802
778
|
try {
|
|
803
779
|
await messageStore.save();
|
|
804
780
|
} catch (err) {
|
|
805
781
|
logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);
|
|
806
782
|
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
783
|
+
}.bind(null, this);
|
|
784
|
+
|
|
785
|
+
// 压缩重试循环
|
|
786
|
+
for (let attempt = 0; attempt <= 1; attempt++) {
|
|
787
|
+
try {
|
|
788
|
+
// 首次尝试前做 prepareSession;压缩重试时跳过
|
|
789
|
+
if (attempt === 0) {
|
|
790
|
+
const prepared = await this._prepareSession(message, sessionId);
|
|
791
|
+
messageStore = prepared.messageStore;
|
|
792
|
+
messages = prepared.messages;
|
|
793
|
+
userMessage = prepared.userMessage;
|
|
819
794
|
} else {
|
|
820
|
-
|
|
795
|
+
// 重试前重新 build 系统提示词
|
|
796
|
+
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
797
|
+
this._invalidateMessageTokensCache(messageStore);
|
|
821
798
|
}
|
|
822
|
-
}
|
|
823
799
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
800
|
+
if (!this._aiClient) {
|
|
801
|
+
throw new Error('AI 客户端未配置');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// 执行流式生成
|
|
805
|
+
for await (const chunk of doStream(this, sessionId, message, messageStore, messages, userMessage)) {
|
|
806
|
+
yield chunk;
|
|
807
|
+
}
|
|
808
|
+
return; // 成功完成
|
|
809
|
+
} catch (err) {
|
|
810
|
+
// ★ 上下文超限:压缩后重试一次
|
|
811
|
+
if (!didRetryWithCompression && this._isContextOverflowError(err)) {
|
|
812
|
+
logger.warn(`[AgentChat] chatStream context overflow (attempt ${attempt + 1}), compressing and retrying...`);
|
|
813
|
+
try {
|
|
814
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
815
|
+
this._invalidateMessageTokensCache(messageStore);
|
|
816
|
+
logger.info(`[AgentChat] Compression done, retrying stream...`);
|
|
817
|
+
didRetryWithCompression = true;
|
|
818
|
+
continue; // 重试
|
|
819
|
+
} catch (compressErr) {
|
|
820
|
+
logger.error(`[AgentChat] Compression failed: ${compressErr.message}`);
|
|
830
821
|
}
|
|
831
822
|
}
|
|
832
|
-
} catch { /* ignore */ }
|
|
833
823
|
|
|
834
|
-
|
|
835
|
-
|
|
824
|
+
// 打印错误
|
|
825
|
+
logger.error('[AgentChat] chatStream error:', err.message);
|
|
826
|
+
if (err.cause) {
|
|
827
|
+
const cause = err.cause;
|
|
828
|
+
if (cause.issues) {
|
|
829
|
+
for (const issue of cause.issues.slice(0, 5)) {
|
|
830
|
+
logger.error(
|
|
831
|
+
`[AgentChat] schema issue: path=${JSON.stringify(issue.path)} code=${issue.code} message=${issue.message}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
logger.error(`[AgentChat] error cause: ${cause.message || String(cause)}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
836
838
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
839
|
+
// 回滚 userMessage
|
|
840
|
+
try {
|
|
841
|
+
if (messages && userMessage) {
|
|
842
|
+
const last = messages[messages.length - 1];
|
|
843
|
+
if (last && last.role === 'user' && last === userMessage) {
|
|
844
|
+
messages.pop();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} catch { /* ignore */ }
|
|
848
|
+
|
|
849
|
+
const friendlyMessage = err?.message?.split('\n')[0] || '未知错误';
|
|
850
|
+
if (err.name !== 'AbortError') {
|
|
851
|
+
this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });
|
|
852
|
+
}
|
|
853
|
+
yield { type: 'error', error: friendlyMessage };
|
|
854
|
+
return; // 失败返回
|
|
840
855
|
}
|
|
841
|
-
yield { type: 'error', error: friendlyMessage };
|
|
842
856
|
}
|
|
843
857
|
}
|
|
844
858
|
|
|
@@ -891,109 +905,132 @@ class AgentChatHandler extends EventEmitter {
|
|
|
891
905
|
let messageStore = null;
|
|
892
906
|
let messages = null;
|
|
893
907
|
let userMessage = null;
|
|
908
|
+
let didRetryWithCompression = false;
|
|
894
909
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
messageStore = prepared.messageStore;
|
|
898
|
-
messages = prepared.messages;
|
|
899
|
-
userMessage = prepared.userMessage;
|
|
910
|
+
// 最多压缩重试 1 次
|
|
911
|
+
const maxCompressionRetries = 1;
|
|
900
912
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
913
|
+
for (let attempt = 0; attempt <= maxCompressionRetries; attempt++) {
|
|
914
|
+
try {
|
|
915
|
+
// 首次尝试前做 prepareSession;压缩重试时跳过(已压缩过)
|
|
916
|
+
if (attempt === 0) {
|
|
917
|
+
const prepared = await this._prepareSession(message, sessionId);
|
|
918
|
+
messageStore = prepared.messageStore;
|
|
919
|
+
messages = prepared.messages;
|
|
920
|
+
userMessage = prepared.userMessage;
|
|
921
|
+
} else {
|
|
922
|
+
// 压缩重试前重新 build 系统提示词(压缩后 messages 变了)
|
|
923
|
+
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
924
|
+
this._invalidateMessageTokensCache(messageStore);
|
|
925
|
+
}
|
|
906
926
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
927
|
+
if (!this._aiClient) {
|
|
928
|
+
throw new Error('AI 客户端未配置');
|
|
929
|
+
}
|
|
930
|
+
const systemPrompt = framework.prompts.build();
|
|
931
|
+
const tools = this._getToolMap();
|
|
912
932
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
abortSignal: framework.getAbortSignal?.(),
|
|
919
|
-
thinkingMode: this._thinkingMode,
|
|
920
|
-
});
|
|
933
|
+
// DeepSeek thinking mode: 处理参数
|
|
934
|
+
const apiOptions = { ...this.providerOptions };
|
|
935
|
+
if (this._thinkingMode) {
|
|
936
|
+
delete apiOptions.temperature;
|
|
937
|
+
}
|
|
921
938
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
939
|
+
const toolLoop = new ToolLoop({
|
|
940
|
+
tools,
|
|
941
|
+
maxSteps: this._maxSteps || 1000,
|
|
942
|
+
prepareStep: this._createPrepareStep(),
|
|
943
|
+
repairToolCall: this._repairToolCall.bind(this),
|
|
944
|
+
abortSignal: framework.getAbortSignal?.(),
|
|
945
|
+
thinkingMode: this._thinkingMode,
|
|
946
|
+
});
|
|
930
947
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
948
|
+
const result = await toolLoop.generate({
|
|
949
|
+
messages,
|
|
950
|
+
systemPrompt,
|
|
951
|
+
model: this.model,
|
|
952
|
+
client: this._aiClient,
|
|
953
|
+
...apiOptions,
|
|
954
|
+
});
|
|
955
|
+
// result = { text, response: { messages }, steps, usage, reasoningText }
|
|
938
956
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
// ★ 修复:AI 调用成功后才持久化(之前 finally 总是 save,
|
|
947
|
-
// 会把刚 push 的 userMessage 写盘;下次请求 loadHistory 再次 load 到,
|
|
948
|
-
// _prepareSession 又 push 一份 → 连续两条相同 user 消息 →
|
|
949
|
-
// AI SDK 抛 "Invalid prompt: ModelMessage[] schema")
|
|
950
|
-
try {
|
|
951
|
-
await messageStore.save();
|
|
952
|
-
} catch (err) {
|
|
953
|
-
logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);
|
|
954
|
-
}
|
|
957
|
+
if (result.usage && (result.usage.promptTokens > 0 || result.usage.completionTokens > 0)) {
|
|
958
|
+
this._updateMessageStoreUsage(messageStore, result.usage);
|
|
959
|
+
} else {
|
|
960
|
+
const inputTokens = this._tokenCounter.countMessages(messages);
|
|
961
|
+
const outputTokens = this._tokenCounter.countText(result.text || '');
|
|
962
|
+
messageStore.usage = { promptTokens: inputTokens, completionTokens: outputTokens };
|
|
963
|
+
}
|
|
955
964
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
965
|
+
// ToolLoop 已经直接在 messages 末尾添加了新消息,不需要再 push
|
|
966
|
+
// 计算 original 的 user message
|
|
967
|
+
const userMsg = result.text
|
|
968
|
+
? messages.slice(-1)[0]
|
|
969
|
+
: messages[messages.length - 1];
|
|
970
|
+
this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
|
|
971
|
+
|
|
972
|
+
// ★ 修复:AI 调用成功后才持久化
|
|
973
|
+
try {
|
|
974
|
+
await messageStore.save();
|
|
975
|
+
} catch (err) {
|
|
976
|
+
logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
success: true,
|
|
981
|
+
message: cleanResponse(result.text || ''),
|
|
982
|
+
stepCount: result.stepCount || 1,
|
|
983
|
+
};
|
|
984
|
+
} catch (err) {
|
|
985
|
+
// ★ 上下文超限:压缩后重试一次
|
|
986
|
+
if (!didRetryWithCompression && this._isContextOverflowError(err)) {
|
|
987
|
+
logger.warn(`[AgentChat] Context overflow (attempt ${attempt + 1}), compressing and retrying...`);
|
|
988
|
+
try {
|
|
989
|
+
// 强制压缩(忽略阈值检查)
|
|
990
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
991
|
+
this._invalidateMessageTokensCache(messageStore);
|
|
992
|
+
logger.info(`[AgentChat] Compression done, retrying...`);
|
|
993
|
+
didRetryWithCompression = true;
|
|
994
|
+
continue; // 重试
|
|
995
|
+
} catch (compressErr) {
|
|
996
|
+
logger.error(`[AgentChat] Compression failed: ${compressErr.message}`);
|
|
997
|
+
// 压缩失败,继续走下方错误处理
|
|
973
998
|
}
|
|
974
|
-
} else {
|
|
975
|
-
logger.error(`[AgentChat] error cause: ${cause.message || String(cause)}`);
|
|
976
999
|
}
|
|
977
|
-
}
|
|
978
|
-
const errorMsg = err.message || String(err);
|
|
979
1000
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1001
|
+
// 打印错误信息
|
|
1002
|
+
logger.error('[AgentChat] _processMessage error:', err.message);
|
|
1003
|
+
if (err.cause) {
|
|
1004
|
+
const cause = err.cause;
|
|
1005
|
+
if (cause.issues) {
|
|
1006
|
+
for (const issue of cause.issues.slice(0, 5)) {
|
|
1007
|
+
logger.error(
|
|
1008
|
+
`[AgentChat] schema issue: path=${JSON.stringify(issue.path)} code=${issue.code} message=${issue.message}`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
logger.error(`[AgentChat] error cause: ${cause.message || String(cause)}`);
|
|
987
1013
|
}
|
|
988
1014
|
}
|
|
989
|
-
|
|
1015
|
+
const errorMsg = err.message || String(err);
|
|
990
1016
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1017
|
+
// ★ 失败时 ROLLBACK 掉本轮 push 的 userMessage
|
|
1018
|
+
try {
|
|
1019
|
+
if (messages && userMessage) {
|
|
1020
|
+
const last = messages[messages.length - 1];
|
|
1021
|
+
if (last && last.role === 'user' && last === userMessage) {
|
|
1022
|
+
messages.pop();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch { /* ignore */ }
|
|
1026
|
+
|
|
1027
|
+
return {
|
|
1028
|
+
success: false,
|
|
1029
|
+
message: 'AI 服务暂时不可用,请稍后重试。',
|
|
1030
|
+
error: errorMsg,
|
|
1031
|
+
stepCount: 0,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
997
1034
|
}
|
|
998
1035
|
}
|
|
999
1036
|
|
|
@@ -1028,16 +1065,16 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1028
1065
|
const totalTokens = cachedMsgTokens + toolsTokens + systemPromptTokens;
|
|
1029
1066
|
const limit = this._maxContextTokens * SIMPLE_COMPRESS_TOKEN_RATIO;
|
|
1030
1067
|
|
|
1031
|
-
|
|
1068
|
+
logger.info(`[_prepareSession] BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}, threshold=${this._compressionMessageThreshold}`);
|
|
1032
1069
|
|
|
1033
1070
|
if (totalTokens > limit || messages.length > this._compressionMessageThreshold) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1071
|
+
logger.info(
|
|
1072
|
+
`Context large (${messages.length} msgs, ${totalTokens}/${this._maxContextTokens} tokens), compressing...`
|
|
1073
|
+
);
|
|
1037
1074
|
await this._compressContext(sessionId, messages, messageStore);
|
|
1038
1075
|
// 压缩后消息被截断/替换,缓存已失效,下次 _getCachedMessageTokens 会重新计算
|
|
1039
1076
|
this._invalidateMessageTokensCache(messageStore);
|
|
1040
|
-
logger.
|
|
1077
|
+
logger.info(`[_prepareSession] AFTER compress: messages=${messages.length}`);
|
|
1041
1078
|
}
|
|
1042
1079
|
|
|
1043
1080
|
// 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
|
|
@@ -1500,6 +1537,33 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1500
1537
|
return this._tokenCounter.countTools(this._toolExecutor.getAllTools());
|
|
1501
1538
|
}
|
|
1502
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* 判断是否为上下文超限错误
|
|
1542
|
+
* @param {Error} err
|
|
1543
|
+
* @returns {boolean}
|
|
1544
|
+
* @private
|
|
1545
|
+
*/
|
|
1546
|
+
_isContextOverflowError(err) {
|
|
1547
|
+
if (!err) return false;
|
|
1548
|
+
const msg = err.message || '';
|
|
1549
|
+
const lowerMsg = msg.toLowerCase();
|
|
1550
|
+
// DeepSeek / OpenAI 兼容的上下文超限错误关键词
|
|
1551
|
+
const keywords = [
|
|
1552
|
+
'context_length',
|
|
1553
|
+
'context length',
|
|
1554
|
+
'maximum context',
|
|
1555
|
+
'max context',
|
|
1556
|
+
'too many tokens',
|
|
1557
|
+
'token limit',
|
|
1558
|
+
'tokens exceed',
|
|
1559
|
+
'exceed maximum',
|
|
1560
|
+
'maximum tokens',
|
|
1561
|
+
'context_exceeded',
|
|
1562
|
+
'context overflow',
|
|
1563
|
+
];
|
|
1564
|
+
return keywords.some(k => lowerMsg.includes(k));
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1503
1567
|
/**
|
|
1504
1568
|
* 获取缓存的工具 token 数(工具列表不变时复用)
|
|
1505
1569
|
* @private
|
package/src/common/constants.js
CHANGED
|
@@ -51,10 +51,10 @@ const DEFAULT_MAX_MESSAGES_PER_SESSION = 1000;
|
|
|
51
51
|
// ============================================================================
|
|
52
52
|
|
|
53
53
|
/** 保留最近的 N 条消息 */
|
|
54
|
-
const KEEP_RECENT_MESSAGES =
|
|
54
|
+
const KEEP_RECENT_MESSAGES = 20;
|
|
55
55
|
|
|
56
56
|
/** 压缩消息数量阈值 */
|
|
57
|
-
const COMPRESSION_MESSAGE_THRESHOLD =
|
|
57
|
+
const COMPRESSION_MESSAGE_THRESHOLD = 80;
|
|
58
58
|
|
|
59
59
|
/** 智能压缩启用 */
|
|
60
60
|
const ENABLE_SMART_COMPRESS = true;
|
package/src/llm/tokens.js
CHANGED
|
@@ -5,7 +5,70 @@
|
|
|
5
5
|
* Ported from pi's compaction/compaction.ts with enhancements
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// CJK 字符 Unicode 范围
|
|
9
|
+
const CJK_RANGES = [
|
|
10
|
+
[0x4e00, 0x9fff], // CJK Unified Ideographs (中文简体/繁体)
|
|
11
|
+
[0x3400, 0x4dbf], // CJK Unified Ideographs Extension A
|
|
12
|
+
[0x20000, 0x2a6df], // CJK Unified Ideographs Extension B
|
|
13
|
+
[0x3040, 0x309f], // Hiragana (日文平假名)
|
|
14
|
+
[0x30a0, 0x30ff], // Katakana (日文片假名)
|
|
15
|
+
[0xac00, 0xd7af], // Hangul Syllables (韩文)
|
|
16
|
+
[0x1100, 0x11ff], // Hangul Jamo
|
|
17
|
+
[0x3000, 0x303f], // CJK Symbols and Punctuation
|
|
18
|
+
[0xff00, 0xffef], // Halfwidth and Fullwidth Forms
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function isCJKChar(char) {
|
|
22
|
+
const code = char.codePointAt(0);
|
|
23
|
+
if (!code) return false;
|
|
24
|
+
for (const [start, end] of CJK_RANGES) {
|
|
25
|
+
if (code >= start && code <= end) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 改进的 Token 估算
|
|
32
|
+
* 根据文本语言类型采用不同估算系数
|
|
33
|
+
*
|
|
34
|
+
* @param {string} text - 输入文本
|
|
35
|
+
* @returns {number} 估算的 token 数
|
|
36
|
+
*/
|
|
37
|
+
function encode(text) {
|
|
38
|
+
if (!text || typeof text !== 'string') return 0;
|
|
39
|
+
|
|
40
|
+
let cjkChars = 0;
|
|
41
|
+
let asciiChars = 0;
|
|
42
|
+
let otherChars = 0;
|
|
43
|
+
|
|
44
|
+
// 逐字符统计
|
|
45
|
+
for (const char of text) {
|
|
46
|
+
if (isCJKChar(char)) {
|
|
47
|
+
cjkChars++;
|
|
48
|
+
} else if (char.charCodeAt(0) < 128) {
|
|
49
|
+
// ASCII 字符 (英文、数字、标点)
|
|
50
|
+
asciiChars++;
|
|
51
|
+
} else {
|
|
52
|
+
otherChars++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 更精确的估算系数 (基于 tiktoken 统计数据):
|
|
57
|
+
// - ASCII/英文: ~0.25 tokens/char (4 chars per token)
|
|
58
|
+
// - 中文: ~0.5 tokens/char (2 chars per token, 中文字符是 3 字节 UTF-8 但 tokenizer 输出约 0.5)
|
|
59
|
+
// - 其他 (emoji, 特殊符号): ~0.25 tokens/char
|
|
60
|
+
const asciiTokens = asciiChars * 0.25;
|
|
61
|
+
const cjkTokens = cjkChars * 0.5;
|
|
62
|
+
const otherTokens = otherChars * 0.25;
|
|
63
|
+
|
|
64
|
+
return Math.ceil(asciiTokens + cjkTokens + otherTokens);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 旧版 encode(兼容)
|
|
69
|
+
* @deprecated 使用新版 encode 替代
|
|
70
|
+
*/
|
|
71
|
+
function encodeOld(text, bytesPerToken = 4) {
|
|
9
72
|
if (!text) return 0;
|
|
10
73
|
const bytes = Buffer.byteLength(String(text), 'utf8');
|
|
11
74
|
return Math.ceil(bytes / bytesPerToken);
|
|
@@ -25,64 +88,66 @@ function safeJsonStringify(value) {
|
|
|
25
88
|
}
|
|
26
89
|
}
|
|
27
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 估算单条消息的 token 数(使用改进的编码方法)
|
|
93
|
+
*/
|
|
28
94
|
function estimateTokens(message) {
|
|
29
|
-
|
|
95
|
+
if (!message) return 0;
|
|
96
|
+
|
|
97
|
+
let total = 4; // message overhead
|
|
30
98
|
|
|
31
99
|
switch (message.role) {
|
|
32
|
-
case 'user':
|
|
100
|
+
case 'user':
|
|
101
|
+
case 'assistant':
|
|
102
|
+
case 'custom':
|
|
103
|
+
case 'toolResult': {
|
|
33
104
|
const content = message.content;
|
|
34
105
|
if (typeof content === 'string') {
|
|
35
|
-
|
|
106
|
+
total += encode(content);
|
|
36
107
|
} else if (Array.isArray(content)) {
|
|
37
108
|
for (const block of content) {
|
|
38
109
|
if (block.type === 'text' && block.text) {
|
|
39
|
-
|
|
110
|
+
total += encode(block.text);
|
|
111
|
+
} else if (block.type === 'tool-call' || block.type === 'toolCall') {
|
|
112
|
+
total += encode(block.name || '') + encode(safeJsonStringify(block.arguments || {}));
|
|
113
|
+
} else if (block.type === 'tool-result' || block.type === 'toolResult') {
|
|
114
|
+
total += encode(typeof block.content === 'string' ? block.content : safeJsonStringify(block.content));
|
|
115
|
+
} else if (block.type === 'image') {
|
|
116
|
+
total += 85;
|
|
117
|
+
} else if (block.type === 'thinking') {
|
|
118
|
+
total += encode(block.thinking || '');
|
|
40
119
|
}
|
|
41
120
|
}
|
|
42
121
|
}
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
case 'assistant': {
|
|
46
|
-
const assistant = message;
|
|
47
|
-
for (const block of assistant.content || []) {
|
|
48
|
-
if (block.type === 'text') {
|
|
49
|
-
chars += block.text.length;
|
|
50
|
-
} else if (block.type === 'thinking') {
|
|
51
|
-
chars += block.thinking.length;
|
|
52
|
-
} else if (block.type === 'toolCall') {
|
|
53
|
-
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return Math.ceil(chars / 4);
|
|
57
|
-
}
|
|
58
|
-
case 'custom':
|
|
59
|
-
case 'toolResult': {
|
|
60
|
-
if (typeof message.content === 'string') {
|
|
61
|
-
chars = message.content.length;
|
|
62
|
-
} else {
|
|
63
|
-
for (const block of message.content || []) {
|
|
64
|
-
if (block.type === 'text' && block.text) {
|
|
65
|
-
chars += block.text.length;
|
|
66
|
-
}
|
|
67
|
-
if (block.type === 'image') {
|
|
68
|
-
chars += 4800;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return Math.ceil(chars / 4);
|
|
122
|
+
break;
|
|
73
123
|
}
|
|
74
124
|
case 'bashExecution': {
|
|
75
|
-
|
|
76
|
-
|
|
125
|
+
total += encode(message.command || '');
|
|
126
|
+
total += encode(message.output || '');
|
|
127
|
+
break;
|
|
77
128
|
}
|
|
78
129
|
case 'branchSummary':
|
|
79
130
|
case 'compactionSummary': {
|
|
80
|
-
|
|
81
|
-
|
|
131
|
+
total += encode(message.summary || '');
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// tool_calls overhead
|
|
137
|
+
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
|
138
|
+
total += 15; // tool_calls overhead
|
|
139
|
+
for (const tc of message.tool_calls) {
|
|
140
|
+
if (tc.function) {
|
|
141
|
+
total += encode(tc.function.name || '') + encode(safeJsonStringify(tc.function.arguments || {}));
|
|
142
|
+
}
|
|
82
143
|
}
|
|
83
144
|
}
|
|
84
145
|
|
|
85
|
-
|
|
146
|
+
if (message.tool_call_id) {
|
|
147
|
+
total += 15;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return total;
|
|
86
151
|
}
|
|
87
152
|
|
|
88
153
|
function calculateContextTokens(usage) {
|
|
@@ -157,8 +222,8 @@ class TokenCounter {
|
|
|
157
222
|
this.toolSchema = config.toolSchema || null;
|
|
158
223
|
}
|
|
159
224
|
|
|
160
|
-
countText(text
|
|
161
|
-
return encode(text
|
|
225
|
+
countText(text) {
|
|
226
|
+
return encode(text);
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
countMessages(messages) {
|