@huyooo/ai-chat-core 0.2.44 → 0.3.2
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/dist/adapter/index.d.ts +11 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/model-adapter.d.ts +25 -0
- package/dist/adapter/model-adapter.d.ts.map +1 -0
- package/dist/adapter/model-options.d.ts +53 -0
- package/dist/adapter/model-options.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +28 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/chat-runtime.d.ts +96 -0
- package/dist/chat-runtime.d.ts.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/events.d.ts +605 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +1 -1
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/types.d.ts +46 -0
- package/dist/extension/types.d.ts.map +1 -0
- package/dist/families/index.d.ts +11 -0
- package/dist/families/index.d.ts.map +1 -0
- package/dist/families/presets.d.ts +31 -0
- package/dist/families/presets.d.ts.map +1 -0
- package/dist/families/resolver.d.ts +11 -0
- package/dist/families/resolver.d.ts.map +1 -0
- package/dist/families/types.d.ts +29 -0
- package/dist/families/types.d.ts.map +1 -0
- package/dist/governance/command-safety.d.ts +34 -0
- package/dist/governance/command-safety.d.ts.map +1 -0
- package/dist/governance/governance.d.ts +19 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/index.d.ts +12 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/types.d.ts +29 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/index.d.ts +72 -804
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -1
- package/dist/internal/management-args.d.ts +13 -0
- package/dist/internal/management-args.d.ts.map +1 -0
- package/dist/internal/management-results.d.ts +21 -0
- package/dist/internal/management-results.d.ts.map +1 -0
- package/dist/llm-config.d.ts +108 -0
- package/dist/llm-config.d.ts.map +1 -0
- package/dist/logger/core.d.ts +31 -0
- package/dist/logger/core.d.ts.map +1 -0
- package/dist/logger/index.d.ts +9 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/orchestrator/compression-handler.d.ts +29 -0
- package/dist/orchestrator/compression-handler.d.ts.map +1 -0
- package/dist/orchestrator/context-compressor.d.ts +51 -0
- package/dist/orchestrator/context-compressor.d.ts.map +1 -0
- package/dist/orchestrator/context-summarizer.d.ts +41 -0
- package/dist/orchestrator/context-summarizer.d.ts.map +1 -0
- package/dist/orchestrator/index.d.ts +12 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +46 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/types.d.ts +58 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/parts/index.d.ts +13 -0
- package/dist/parts/index.d.ts.map +1 -0
- package/dist/parts/registry.d.ts +11 -0
- package/dist/parts/registry.d.ts.map +1 -0
- package/dist/parts/summaries.d.ts +9 -0
- package/dist/parts/summaries.d.ts.map +1 -0
- package/dist/parts/types.d.ts +61 -0
- package/dist/parts/types.d.ts.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +1 -0
- package/dist/protocols/anthropic.d.ts +20 -0
- package/dist/protocols/anthropic.d.ts.map +1 -0
- package/dist/protocols/ark.d.ts +36 -0
- package/dist/protocols/ark.d.ts.map +1 -0
- package/dist/protocols/deepseek.d.ts +24 -0
- package/dist/protocols/deepseek.d.ts.map +1 -0
- package/dist/protocols/error-utils.d.ts +14 -0
- package/dist/protocols/error-utils.d.ts.map +1 -0
- package/dist/protocols/gemini.d.ts +24 -0
- package/dist/protocols/gemini.d.ts.map +1 -0
- package/dist/protocols/glm.d.ts +20 -0
- package/dist/protocols/glm.d.ts.map +1 -0
- package/dist/protocols/grok.d.ts +20 -0
- package/dist/protocols/grok.d.ts.map +1 -0
- package/dist/protocols/index.d.ts +31 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/minimax.d.ts +38 -0
- package/dist/protocols/minimax.d.ts.map +1 -0
- package/dist/protocols/moonshot.d.ts +20 -0
- package/dist/protocols/moonshot.d.ts.map +1 -0
- package/dist/protocols/openai-sse.d.ts +33 -0
- package/dist/protocols/openai-sse.d.ts.map +1 -0
- package/dist/protocols/openai.d.ts +19 -0
- package/dist/protocols/openai.d.ts.map +1 -0
- package/dist/protocols/qwen.d.ts +26 -0
- package/dist/protocols/qwen.d.ts.map +1 -0
- package/dist/protocols/responses-sse.d.ts +30 -0
- package/dist/protocols/responses-sse.d.ts.map +1 -0
- package/dist/protocols/sse-reader.d.ts +23 -0
- package/dist/protocols/sse-reader.d.ts.map +1 -0
- package/dist/protocols/tool-arguments.d.ts +8 -0
- package/dist/protocols/tool-arguments.d.ts.map +1 -0
- package/dist/protocols/types.d.ts +148 -0
- package/dist/protocols/types.d.ts.map +1 -0
- package/dist/protocols/vercel-gateway.d.ts +15 -0
- package/dist/protocols/vercel-gateway.d.ts.map +1 -0
- package/dist/runtime.d.ts +151 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1 -0
- package/dist/skills/index.d.ts +14 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/management/admin.d.ts +10 -0
- package/dist/skills/management/admin.d.ts.map +1 -0
- package/dist/skills/management/index.d.ts +11 -0
- package/dist/skills/management/index.d.ts.map +1 -0
- package/dist/skills/management/inputs.d.ts +44 -0
- package/dist/skills/management/inputs.d.ts.map +1 -0
- package/dist/skills/management/operations.d.ts +78 -0
- package/dist/skills/management/operations.d.ts.map +1 -0
- package/dist/skills/management/types.d.ts +70 -0
- package/dist/skills/management/types.d.ts.map +1 -0
- package/dist/skills/registry.d.ts +37 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/summaries.d.ts +9 -0
- package/dist/skills/summaries.d.ts.map +1 -0
- package/dist/skills/types.d.ts +61 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/test-utils/mock-sse.d.ts +13 -0
- package/dist/test-utils/mock-sse.d.ts.map +1 -0
- package/dist/tool-manager/define-tool.d.ts +35 -0
- package/dist/tool-manager/define-tool.d.ts.map +1 -0
- package/dist/tool-manager/formats.d.ts +46 -0
- package/dist/tool-manager/formats.d.ts.map +1 -0
- package/dist/tool-manager/identity.d.ts +18 -0
- package/dist/tool-manager/identity.d.ts.map +1 -0
- package/dist/tool-manager/in-process-provider.d.ts +15 -0
- package/dist/tool-manager/in-process-provider.d.ts.map +1 -0
- package/dist/tool-manager/index.d.ts +18 -0
- package/dist/tool-manager/index.d.ts.map +1 -0
- package/dist/tool-manager/manager.d.ts +18 -0
- package/dist/tool-manager/manager.d.ts.map +1 -0
- package/dist/tool-manager/mcp-provider.d.ts +21 -0
- package/dist/tool-manager/mcp-provider.d.ts.map +1 -0
- package/dist/tool-manager/summaries.d.ts +39 -0
- package/dist/tool-manager/summaries.d.ts.map +1 -0
- package/dist/tool-manager/types.d.ts +314 -0
- package/dist/tool-manager/types.d.ts.map +1 -0
- package/dist/types.d.ts +663 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +26 -15
- package/src/adapter/index.ts +25 -0
- package/src/adapter/model-adapter.ts +196 -0
- package/src/adapter/model-options.ts +143 -0
- package/src/adapter/types.ts +41 -0
- package/src/chat-runtime.ts +515 -0
- package/src/constants.ts +9 -102
- package/src/events.ts +364 -150
- package/src/extension/index.ts +24 -0
- package/src/extension/types.ts +49 -0
- package/src/families/index.ts +28 -0
- package/src/families/presets.ts +124 -0
- package/src/families/resolver.ts +22 -0
- package/src/families/types.ts +55 -0
- package/src/governance/command-safety.ts +224 -0
- package/src/governance/governance.ts +125 -0
- package/src/governance/index.ts +38 -0
- package/src/governance/types.ts +44 -0
- package/src/index.ts +250 -145
- package/src/internal/management-args.ts +39 -0
- package/src/internal/management-results.ts +60 -0
- package/src/llm-config.ts +137 -0
- package/src/logger/core.ts +96 -0
- package/src/logger/index.ts +8 -0
- package/src/orchestrator/compression-handler.ts +137 -0
- package/src/{providers → orchestrator}/context-compressor.ts +79 -47
- package/src/orchestrator/context-summarizer.ts +123 -0
- package/src/orchestrator/index.ts +20 -0
- package/src/orchestrator/orchestrator.ts +1002 -0
- package/src/orchestrator/types.ts +70 -0
- package/src/parts/index.ts +20 -0
- package/src/parts/registry.ts +95 -0
- package/src/parts/summaries.ts +40 -0
- package/src/parts/types.ts +63 -0
- package/src/platform.ts +73 -0
- package/src/protocols/anthropic.ts +377 -0
- package/src/protocols/ark.ts +300 -0
- package/src/protocols/deepseek.ts +192 -0
- package/src/{providers/protocols → protocols}/error-utils.ts +17 -20
- package/src/protocols/gemini.ts +352 -0
- package/src/protocols/glm.ts +212 -0
- package/src/protocols/grok.ts +98 -0
- package/src/protocols/index.ts +48 -0
- package/src/protocols/minimax.ts +308 -0
- package/src/protocols/moonshot.ts +186 -0
- package/src/protocols/openai-sse.ts +156 -0
- package/src/protocols/openai.ts +97 -0
- package/src/protocols/qwen.ts +358 -0
- package/src/protocols/responses-sse.ts +224 -0
- package/src/protocols/sse-reader.ts +54 -0
- package/src/protocols/tool-arguments.ts +32 -0
- package/src/{providers/protocols → protocols}/types.ts +46 -37
- package/src/protocols/vercel-gateway.ts +391 -0
- package/src/runtime.ts +167 -0
- package/src/skills/index.ts +29 -0
- package/src/skills/management/admin.ts +170 -0
- package/src/skills/management/index.ts +27 -0
- package/src/skills/management/inputs.ts +79 -0
- package/src/skills/management/operations.ts +256 -0
- package/src/skills/management/types.ts +57 -0
- package/src/skills/registry.ts +120 -0
- package/src/skills/summaries.ts +48 -0
- package/src/skills/types.ts +65 -0
- package/src/test-utils/mock-sse.ts +3 -3
- package/src/tool-manager/define-tool.ts +201 -0
- package/src/tool-manager/formats.ts +146 -0
- package/src/tool-manager/identity.ts +80 -0
- package/src/tool-manager/in-process-provider.ts +164 -0
- package/src/tool-manager/index.ts +63 -0
- package/src/tool-manager/manager.ts +562 -0
- package/src/tool-manager/mcp-provider.ts +509 -0
- package/src/tool-manager/summaries.ts +136 -0
- package/src/tool-manager/types.ts +389 -0
- package/src/types.ts +750 -191
- package/dist/events-CU5D5ray.d.ts +0 -1128
- package/src/agent.ts +0 -409
- package/src/internal/update-plan.ts +0 -2
- package/src/internal/web-search.ts +0 -77
- package/src/mcp/client-manager.ts +0 -302
- package/src/mcp/index.ts +0 -2
- package/src/mcp/types.ts +0 -43
- package/src/providers/context-summarizer.ts +0 -70
- package/src/providers/index.ts +0 -125
- package/src/providers/model-registry.ts +0 -466
- package/src/providers/orchestrator.ts +0 -839
- package/src/providers/protocols/anthropic.ts +0 -406
- package/src/providers/protocols/ark.ts +0 -362
- package/src/providers/protocols/deepseek.ts +0 -344
- package/src/providers/protocols/gemini.ts +0 -350
- package/src/providers/protocols/index.ts +0 -36
- package/src/providers/protocols/openai.ts +0 -420
- package/src/providers/protocols/qwen.ts +0 -315
- package/src/providers/types.ts +0 -264
- package/src/providers/unified-adapter.ts +0 -367
- package/src/router.ts +0 -72
- package/src/tools.ts +0 -162
- package/src/utils.ts +0 -86
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 配置(Model 中心)
|
|
3
|
+
*
|
|
4
|
+
* LLMConfig.models 是模型的唯一真相源:
|
|
5
|
+
* - key = 模型 ID(自定义字符串)
|
|
6
|
+
* - value = ModelConfig(路由 + 元数据)
|
|
7
|
+
* - 前端只展示这里配置的模型
|
|
8
|
+
* - 每个模型必须显式指定 family,不做任何隐式推导
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ProtocolFactory, Protocol, ProtocolConfig } from './protocols';
|
|
12
|
+
import type { ModelFamilyConfig, ModelFamilyId } from './families';
|
|
13
|
+
|
|
14
|
+
/** 模型定价(每百万 tokens) */
|
|
15
|
+
export interface ModelPricing {
|
|
16
|
+
currency: 'CNY' | 'USD';
|
|
17
|
+
/** 输入价格(每百万 tokens) */
|
|
18
|
+
input: number;
|
|
19
|
+
/** 输出价格,单值或 [最低, 最高] 区间(如思考/非思考差价) */
|
|
20
|
+
output: number | [number, number];
|
|
21
|
+
/** 缓存命中价格(每百万 tokens,部分模型支持) */
|
|
22
|
+
cached?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 单条模型访问路径 */
|
|
26
|
+
export interface ModelRoute {
|
|
27
|
+
/** 基础 URL */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** 访问 key(代理 token 或厂商 key) */
|
|
30
|
+
accessKey: string;
|
|
31
|
+
/** 可选,透传给厂商的 key(BYOK,优先于 accessKey) */
|
|
32
|
+
vendorKey?: string;
|
|
33
|
+
/** 协议类型(内置或自定义 protocolId) */
|
|
34
|
+
protocol: string;
|
|
35
|
+
/** 该供应商 API 要求的 model 名 */
|
|
36
|
+
providerModelId: string;
|
|
37
|
+
/** 协议子路径,直连 '',代理 '/openai' 等 */
|
|
38
|
+
path?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 模型配置(路由 + 元数据) */
|
|
42
|
+
export interface ModelConfig {
|
|
43
|
+
/** 路由链(数组顺序 = 降级优先级) */
|
|
44
|
+
routes: ModelRoute[];
|
|
45
|
+
/** 显示名称(不传则从预设库补全,再 fallback 到 modelId) */
|
|
46
|
+
displayName?: string;
|
|
47
|
+
/**
|
|
48
|
+
* 所属家族(决定 thinking/toolCall 行为)——必填,不做任何隐式推导
|
|
49
|
+
* - 字符串:引用内置 ModelFamilyId 或 LLMConfig.families 中的自定义 familyId
|
|
50
|
+
* - 对象:内联自定义家族配置
|
|
51
|
+
*/
|
|
52
|
+
family: ModelFamilyId | ModelFamilyConfig;
|
|
53
|
+
/** 是否在前端模型选择器中显示(默认 true) */
|
|
54
|
+
visible?: boolean;
|
|
55
|
+
/** 是否支持深度思考(必填,不做隐式推导) */
|
|
56
|
+
supportsThinking: boolean;
|
|
57
|
+
/** 是否支持图片理解(必填,不做隐式推导) */
|
|
58
|
+
supportsVision: boolean;
|
|
59
|
+
/** 上下文窗口 token 数(必填,压缩器依赖此值计算预算,前端展示自动推导为 "256K" 格式) */
|
|
60
|
+
contextWindowTokens: number;
|
|
61
|
+
/** 定价信息(结构化,展示时自动格式化) */
|
|
62
|
+
pricing?: ModelPricing;
|
|
63
|
+
/** 最大输出 token(必填,每个模型限制不同,不做隐式推导) */
|
|
64
|
+
maxOutputTokens: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 压缩模型配置(可选,不配则用默认值)
|
|
69
|
+
*
|
|
70
|
+
* 历史压缩直接复用当前对话模型,零配置。
|
|
71
|
+
* 仅单条超长消息(如用户粘贴巨量文本)需要长上下文模型。
|
|
72
|
+
*/
|
|
73
|
+
export interface CompressionConfig {
|
|
74
|
+
/** 超长内容压缩模型(默认 qwen-long) */
|
|
75
|
+
longModel?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 完整 LLM 配置 */
|
|
79
|
+
export interface LLMConfig {
|
|
80
|
+
/**
|
|
81
|
+
* 模型配置(唯一真相源)
|
|
82
|
+
*
|
|
83
|
+
* key = 模型 ID,value = 路由 + 元数据。
|
|
84
|
+
* 前端模型选择器只展示这里 visible !== false 的模型。
|
|
85
|
+
*/
|
|
86
|
+
models: Record<string, ModelConfig>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 自定义协议工厂(可插拔)
|
|
90
|
+
*
|
|
91
|
+
* key = 自定义 protocolId,value = Protocol 工厂函数。
|
|
92
|
+
* ModelRoute.protocol 引用此处的 key 或内置 protocolId。
|
|
93
|
+
*/
|
|
94
|
+
protocols?: Record<string, ProtocolFactory>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 自定义家族配置(可插拔)
|
|
98
|
+
*
|
|
99
|
+
* key = 自定义 familyId,value = ModelFamilyConfig。
|
|
100
|
+
* ModelConfig.family 引用此处的 key 或内置 familyId。
|
|
101
|
+
*/
|
|
102
|
+
families?: Record<string, ModelFamilyConfig>;
|
|
103
|
+
|
|
104
|
+
/** 压缩模型配置(可选) */
|
|
105
|
+
compression?: CompressionConfig;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 解析 route 的完整 API URL */
|
|
109
|
+
export function resolveRouteUrl(route: ModelRoute): string {
|
|
110
|
+
const p = route.path ?? '';
|
|
111
|
+
return p ? `${route.baseUrl.replace(/\/$/, '')}${p}` : route.baseUrl;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取模型的 route 链(按优先级)
|
|
116
|
+
*/
|
|
117
|
+
export function getRouteChain(
|
|
118
|
+
config: LLMConfig,
|
|
119
|
+
modelId: string,
|
|
120
|
+
): ModelRoute[] {
|
|
121
|
+
return config.models?.[modelId]?.routes ?? [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 查找第一个支持指定 protocol 的 route
|
|
126
|
+
*/
|
|
127
|
+
export function findRouteByProtocol(
|
|
128
|
+
config: LLMConfig,
|
|
129
|
+
protocolId: string,
|
|
130
|
+
): ModelRoute | undefined {
|
|
131
|
+
for (const modelConfig of Object.values(config.models)) {
|
|
132
|
+
if (!modelConfig?.routes) continue;
|
|
133
|
+
const found = modelConfig.routes.find(r => r.protocol === protocolId);
|
|
134
|
+
if (found) return found;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pino 日志核心(与 SuperX 架构一致)
|
|
3
|
+
*
|
|
4
|
+
* 使用 pino.multistream + pino.destination,避免 pino.transport(打包后 worker 不可用)。
|
|
5
|
+
* - 开发:pino-pretty 输出到 stdout
|
|
6
|
+
* - 生产:JSON 输出 + 可选文件持久化
|
|
7
|
+
* - 模块:child({ module: 'Name' }) 创建带前缀的 logger
|
|
8
|
+
*/
|
|
9
|
+
import pino, { type Logger } from 'pino';
|
|
10
|
+
import pinoPretty from 'pino-pretty';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
|
|
14
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
15
|
+
|
|
16
|
+
export interface LoggerConfig {
|
|
17
|
+
/** 日志级别 */
|
|
18
|
+
level?: LogLevel;
|
|
19
|
+
/** 用户数据目录(用于 logs/ai-chat.log) */
|
|
20
|
+
baseDir?: string;
|
|
21
|
+
/** 是否开发模式(使用 pino-pretty) */
|
|
22
|
+
isDev?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let rootLogger: Logger | null = null;
|
|
26
|
+
|
|
27
|
+
function isDev(): boolean {
|
|
28
|
+
return process.env.NODE_ENV === 'development';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getLogLevel(): LogLevel {
|
|
32
|
+
const v = process.env.LOG_LEVEL?.toLowerCase();
|
|
33
|
+
if (v && ['debug', 'info', 'warn', 'error'].includes(v)) return v as LogLevel;
|
|
34
|
+
return isDev() ? 'debug' : 'info';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 创建默认 logger(无文件,仅 stdout)
|
|
39
|
+
*/
|
|
40
|
+
function createDefaultLogger(): Logger {
|
|
41
|
+
const level = getLogLevel();
|
|
42
|
+
if (isDev()) {
|
|
43
|
+
const stream = pinoPretty({ colorize: true, translateTime: 'SYS:standard' });
|
|
44
|
+
return pino({ level }, stream);
|
|
45
|
+
}
|
|
46
|
+
return pino({ level }, pino.destination(1));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 创建完整配置的 logger
|
|
51
|
+
* 使用 multistream + destination,避免 pino.transport(Electron 打包后不可用)
|
|
52
|
+
*/
|
|
53
|
+
function createFullLogger(config: LoggerConfig): Logger {
|
|
54
|
+
const level = config.level ?? (config.isDev ? 'debug' : 'info');
|
|
55
|
+
const streams: Parameters<typeof pino.multistream>[0] = [];
|
|
56
|
+
|
|
57
|
+
if (config.isDev) {
|
|
58
|
+
streams.push({ stream: pinoPretty({ colorize: true, translateTime: 'SYS:standard' }), level });
|
|
59
|
+
} else {
|
|
60
|
+
streams.push({ stream: pino.destination(1), level });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (config.baseDir) {
|
|
64
|
+
const logsDir = path.join(config.baseDir, 'logs');
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
67
|
+
} catch {
|
|
68
|
+
// 忽略
|
|
69
|
+
}
|
|
70
|
+
const logPath = path.join(logsDir, 'ai-chat.log');
|
|
71
|
+
streams.push({ stream: pino.destination(logPath), level });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return pino({ level }, pino.multistream(streams));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 初始化 logger(应在 app.whenReady 后调用)
|
|
79
|
+
*/
|
|
80
|
+
export function initLogger(config: LoggerConfig): void {
|
|
81
|
+
rootLogger = createFullLogger(config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取 root logger,未初始化时返回默认 logger
|
|
86
|
+
*/
|
|
87
|
+
export function getLogger(): Logger {
|
|
88
|
+
return rootLogger ?? createDefaultLogger();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 创建模块专用 logger(替代原 DebugLogger.module)
|
|
93
|
+
*/
|
|
94
|
+
export function createModuleLogger(moduleName: string): Logger {
|
|
95
|
+
return getLogger().child({ module: moduleName });
|
|
96
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 压缩处理器
|
|
3
|
+
*
|
|
4
|
+
* 从 orchestrator 提取的两个独立压缩逻辑:
|
|
5
|
+
* 1. compactIfNeeded — 历史消息整体压缩
|
|
6
|
+
* 2. compressSingleMessageIfNeeded — 单条超长 user 消息压缩
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ProtocolMessage } from '../protocols';
|
|
10
|
+
import type {
|
|
11
|
+
CompactStartEvent,
|
|
12
|
+
CompactEndEvent,
|
|
13
|
+
ContentCompressStartEvent,
|
|
14
|
+
ContentCompressEndEvent,
|
|
15
|
+
} from '../events';
|
|
16
|
+
import {
|
|
17
|
+
createCompactStart,
|
|
18
|
+
createCompactEnd,
|
|
19
|
+
createContentCompressStart,
|
|
20
|
+
createContentCompressEnd,
|
|
21
|
+
} from '../events';
|
|
22
|
+
import {
|
|
23
|
+
needsCompaction,
|
|
24
|
+
estimateTotalTokens,
|
|
25
|
+
estimateMessageTokens,
|
|
26
|
+
getPromptBudget,
|
|
27
|
+
needsSingleMessageCompression,
|
|
28
|
+
type CompactConfig,
|
|
29
|
+
} from './context-compressor';
|
|
30
|
+
import { summarizeHistory, compressSingleMessage } from './context-summarizer';
|
|
31
|
+
import { createModuleLogger } from '../logger';
|
|
32
|
+
import type { CompressionConfig } from '../llm-config';
|
|
33
|
+
import type { SummarizeFn } from './context-summarizer';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_LONG_MODEL = 'qwen-long';
|
|
36
|
+
|
|
37
|
+
const logger = createModuleLogger('Compression');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 历史消息整体压缩
|
|
41
|
+
*
|
|
42
|
+
* 当消息总 token 超出 context window 预算时,
|
|
43
|
+
* 直接用当前对话模型压缩历史(无需额外配置)。
|
|
44
|
+
*
|
|
45
|
+
* 历史压缩触发点 = context window budget,远小于超长阈值(800K),
|
|
46
|
+
* 所以不会出现需要切长上下文模型的场景。
|
|
47
|
+
*/
|
|
48
|
+
export async function* compactIfNeeded(
|
|
49
|
+
messages: ProtocolMessage[],
|
|
50
|
+
compactConfig: CompactConfig,
|
|
51
|
+
summarizeFn?: SummarizeFn,
|
|
52
|
+
currentModel?: string,
|
|
53
|
+
): AsyncGenerator<CompactStartEvent | CompactEndEvent, ProtocolMessage[]> {
|
|
54
|
+
if (!needsCompaction(messages, compactConfig)) {
|
|
55
|
+
return messages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!summarizeFn || !currentModel) {
|
|
59
|
+
logger.warn('上下文超限但未配置 summarize 回调或模型,跳过压缩');
|
|
60
|
+
return messages;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const model = currentModel;
|
|
64
|
+
|
|
65
|
+
const compactStartedAt = Date.now();
|
|
66
|
+
const tokensBeforeCompact = estimateTotalTokens(messages);
|
|
67
|
+
const budget = getPromptBudget(compactConfig);
|
|
68
|
+
const originalCount = messages.length;
|
|
69
|
+
|
|
70
|
+
yield createCompactStart(tokensBeforeCompact, budget);
|
|
71
|
+
|
|
72
|
+
const { messages: result, success, summaryContent } = await summarizeHistory(
|
|
73
|
+
summarizeFn, messages, compactConfig, model,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
yield createCompactEnd(
|
|
77
|
+
success,
|
|
78
|
+
tokensBeforeCompact,
|
|
79
|
+
estimateTotalTokens(result),
|
|
80
|
+
originalCount,
|
|
81
|
+
result.length,
|
|
82
|
+
compactStartedAt,
|
|
83
|
+
summaryContent,
|
|
84
|
+
model,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 单条超长 user 消息压缩
|
|
92
|
+
*
|
|
93
|
+
* 检测最后一条 user 消息是否超长,如果超长则用 qwen-long 压缩。
|
|
94
|
+
*/
|
|
95
|
+
export async function* compressSingleMessageIfNeeded(
|
|
96
|
+
messages: ProtocolMessage[],
|
|
97
|
+
compactConfig: CompactConfig,
|
|
98
|
+
summarizeFn?: SummarizeFn,
|
|
99
|
+
compressionConfig?: CompressionConfig,
|
|
100
|
+
): AsyncGenerator<ContentCompressStartEvent | ContentCompressEndEvent, ProtocolMessage[]> {
|
|
101
|
+
const lastMsg = messages[messages.length - 1];
|
|
102
|
+
if (!lastMsg || lastMsg.role !== 'user') return messages;
|
|
103
|
+
if (!needsSingleMessageCompression(lastMsg, compactConfig)) return messages;
|
|
104
|
+
|
|
105
|
+
if (!summarizeFn) {
|
|
106
|
+
logger.warn('单条消息超长但未配置 summarize 回调,跳过压缩');
|
|
107
|
+
return messages;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const originalTokens = estimateMessageTokens(lastMsg);
|
|
111
|
+
const budget = getPromptBudget(compactConfig);
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
|
|
114
|
+
// 单条超长消息始终用长上下文模型
|
|
115
|
+
const model = compressionConfig?.longModel ?? DEFAULT_LONG_MODEL;
|
|
116
|
+
|
|
117
|
+
yield createContentCompressStart(originalTokens, budget);
|
|
118
|
+
|
|
119
|
+
const result = await compressSingleMessage(summarizeFn, lastMsg, model);
|
|
120
|
+
|
|
121
|
+
if (result.success) {
|
|
122
|
+
const newMessages = [...messages];
|
|
123
|
+
newMessages[newMessages.length - 1] = {
|
|
124
|
+
...lastMsg,
|
|
125
|
+
content: result.summary,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
yield createContentCompressEnd(
|
|
129
|
+
true, result.originalTokens, result.compressedTokens, startedAt, result.summary, model,
|
|
130
|
+
);
|
|
131
|
+
return newMessages;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 压缩失败,保持原文
|
|
135
|
+
yield createContentCompressEnd(false, result.originalTokens, 0, startedAt);
|
|
136
|
+
return messages;
|
|
137
|
+
}
|
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
* 不机械截断,而是让 AI 生成高质量摘要,保留关键决策和上下文。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
11
|
+
import type { ProtocolMessage } from '../protocols';
|
|
12
|
+
import { createModuleLogger } from '../logger';
|
|
13
13
|
|
|
14
|
-
const logger =
|
|
14
|
+
const logger = createModuleLogger('ContextCompressor');
|
|
15
15
|
|
|
16
16
|
// ==================== Token 估算 ====================
|
|
17
17
|
|
|
18
18
|
const CHARS_PER_TOKEN = 3.2;
|
|
19
19
|
const MESSAGE_OVERHEAD_TOKENS = 4;
|
|
20
20
|
|
|
21
|
-
function estimateStringTokens(s: string): number {
|
|
21
|
+
export function estimateStringTokens(s: string): number {
|
|
22
22
|
if (!s) return 0;
|
|
23
23
|
return Math.ceil(s.length / CHARS_PER_TOKEN);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function estimateMessageTokens(msg:
|
|
26
|
+
export function estimateMessageTokens(msg: ProtocolMessage): number {
|
|
27
27
|
let tokens = MESSAGE_OVERHEAD_TOKENS;
|
|
28
28
|
tokens += estimateStringTokens(msg.content);
|
|
29
29
|
|
|
@@ -42,7 +42,7 @@ export function estimateMessageTokens(msg: StandardMessage): number {
|
|
|
42
42
|
return tokens;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function estimateTotalTokens(messages:
|
|
45
|
+
export function estimateTotalTokens(messages: ProtocolMessage[]): number {
|
|
46
46
|
let total = 3;
|
|
47
47
|
for (const msg of messages) {
|
|
48
48
|
total += estimateMessageTokens(msg);
|
|
@@ -50,6 +50,7 @@ export function estimateTotalTokens(messages: StandardMessage[]): number {
|
|
|
50
50
|
return total;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
|
|
53
54
|
// ==================== 配置 ====================
|
|
54
55
|
|
|
55
56
|
export interface CompactConfig {
|
|
@@ -73,7 +74,7 @@ export function getPromptBudget(config: CompactConfig): number {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/** 检测是否需要压缩 */
|
|
76
|
-
export function needsCompaction(messages:
|
|
77
|
+
export function needsCompaction(messages: ProtocolMessage[], config: CompactConfig): boolean {
|
|
77
78
|
return estimateTotalTokens(messages) > getPromptBudget(config);
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -84,15 +85,14 @@ export function needsCompaction(messages: StandardMessage[], config: CompactConf
|
|
|
84
85
|
* 总结完成后调用 applySummary 组装新的消息列表。
|
|
85
86
|
*/
|
|
86
87
|
export function buildSummarizeRequest(
|
|
87
|
-
messages:
|
|
88
|
+
messages: ProtocolMessage[],
|
|
88
89
|
config: CompactConfig,
|
|
89
|
-
): { summarizeMessages:
|
|
90
|
+
): { summarizeMessages: ProtocolMessage[]; keepMessages: ProtocolMessage[] } {
|
|
90
91
|
const keepRecent = config.keepRecentMessages ?? DEFAULT_KEEP_RECENT;
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
let systemEnd = 0;
|
|
94
|
-
if (messages[0]?.role === 'system') systemEnd = 1;
|
|
93
|
+
const systemEnd = messages[0]?.role === 'system' ? 1 : 0;
|
|
95
94
|
|
|
95
|
+
// 第一条 user 消息由 applySummary 原样保留,不参与总结
|
|
96
96
|
let firstUserEnd = systemEnd;
|
|
97
97
|
for (let i = systemEnd; i < messages.length; i++) {
|
|
98
98
|
if (messages[i].role === 'user') {
|
|
@@ -101,32 +101,24 @@ export function buildSummarizeRequest(
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// 要保留的最近消息
|
|
105
104
|
const recentStart = Math.max(firstUserEnd, messages.length - keepRecent);
|
|
106
105
|
const keepMessages = messages.slice(recentStart);
|
|
107
106
|
|
|
108
|
-
//
|
|
107
|
+
// 中间历史:从 firstUser 之后到 recentStart
|
|
108
|
+
// 多轮压缩时包含旧摘要(role=system),因为 applySummary 把摘要放在 firstUser 之后
|
|
109
109
|
const middleMessages = messages.slice(firstUserEnd, recentStart);
|
|
110
110
|
|
|
111
111
|
if (middleMessages.length < 2) {
|
|
112
|
-
// 中间太短,没什么可总结的
|
|
113
112
|
return { summarizeMessages: [], keepMessages: messages.slice(systemEnd) };
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
const estimatedTokens = estimateTotalTokens(messages);
|
|
117
116
|
const budget = getPromptBudget(config);
|
|
118
|
-
logger.info(`准备 AI 总结: ~${estimatedTokens} tokens > budget ${budget}, 总结 ${middleMessages.length} 条中间消息, 保留最近 ${keepMessages.length} 条`);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
role: 'system',
|
|
124
|
-
content: SUMMARIZE_SYSTEM_PROMPT,
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
role: 'user',
|
|
128
|
-
content: formatMessagesForSummary(middleMessages),
|
|
129
|
-
},
|
|
117
|
+
logger.info({ estimatedTokens, budget, middleCount: middleMessages.length, keepCount: keepMessages.length }, `准备 AI 总结: ~${estimatedTokens} tokens > budget ${budget}, 总结 ${middleMessages.length} 条中间消息, 保留最近 ${keepMessages.length} 条`);
|
|
118
|
+
|
|
119
|
+
const summarizeMessages: ProtocolMessage[] = [
|
|
120
|
+
{ role: 'system', content: SUMMARIZE_SYSTEM_PROMPT },
|
|
121
|
+
{ role: 'user', content: formatMessagesForSummary(middleMessages) },
|
|
130
122
|
];
|
|
131
123
|
|
|
132
124
|
return { summarizeMessages, keepMessages };
|
|
@@ -136,16 +128,15 @@ export function buildSummarizeRequest(
|
|
|
136
128
|
* 用 AI 返回的摘要组装新的消息列表
|
|
137
129
|
*/
|
|
138
130
|
export function applySummary(
|
|
139
|
-
originalMessages:
|
|
131
|
+
originalMessages: ProtocolMessage[],
|
|
140
132
|
summary: string,
|
|
141
|
-
keepMessages:
|
|
142
|
-
):
|
|
143
|
-
// 取原始的 system prompt
|
|
133
|
+
keepMessages: ProtocolMessage[],
|
|
134
|
+
): ProtocolMessage[] {
|
|
144
135
|
const systemPrompt = originalMessages[0]?.role === 'system' ? originalMessages[0] : null;
|
|
145
136
|
|
|
146
|
-
//
|
|
137
|
+
// 保留第一条 user 消息原文(初始需求/意图,AI 总结可能丢失细节)
|
|
147
138
|
const startIdx = systemPrompt ? 1 : 0;
|
|
148
|
-
let firstUser:
|
|
139
|
+
let firstUser: ProtocolMessage | null = null;
|
|
149
140
|
for (let i = startIdx; i < originalMessages.length; i++) {
|
|
150
141
|
if (originalMessages[i].role === 'user') {
|
|
151
142
|
firstUser = originalMessages[i];
|
|
@@ -153,25 +144,64 @@ export function applySummary(
|
|
|
153
144
|
}
|
|
154
145
|
}
|
|
155
146
|
|
|
156
|
-
const result:
|
|
147
|
+
const result: ProtocolMessage[] = [];
|
|
157
148
|
if (systemPrompt) result.push(systemPrompt);
|
|
158
149
|
if (firstUser) result.push(firstUser);
|
|
159
150
|
|
|
160
|
-
// 插入 AI 生成的摘要
|
|
161
151
|
result.push({
|
|
162
152
|
role: 'system',
|
|
163
153
|
content: `[对话历史摘要]\n${summary}`,
|
|
164
154
|
});
|
|
165
155
|
|
|
166
|
-
// 拼上最近保留的消息
|
|
167
156
|
result.push(...keepMessages);
|
|
168
157
|
|
|
169
158
|
const tokens = estimateTotalTokens(result);
|
|
170
|
-
logger.info(`AI 总结应用完成: ${originalMessages.length} → ${result.length} 条消息, ~${tokens} tokens`);
|
|
159
|
+
logger.info({ originalCount: originalMessages.length, resultCount: result.length, tokens }, `AI 总结应用完成: ${originalMessages.length} → ${result.length} 条消息, ~${tokens} tokens`);
|
|
171
160
|
|
|
172
161
|
return result;
|
|
173
162
|
}
|
|
174
163
|
|
|
164
|
+
// ==================== 单条消息内容压缩 ====================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 检测单条用户消息是否需要压缩
|
|
168
|
+
* 阈值:单条消息 token > contextWindowTokens * 0.8 - maxOutputTokens
|
|
169
|
+
*/
|
|
170
|
+
export function needsSingleMessageCompression(
|
|
171
|
+
message: ProtocolMessage,
|
|
172
|
+
config: CompactConfig,
|
|
173
|
+
): boolean {
|
|
174
|
+
if (message.role !== 'user') return false;
|
|
175
|
+
const msgTokens = estimateMessageTokens(message);
|
|
176
|
+
const budget = getPromptBudget(config);
|
|
177
|
+
return msgTokens > budget;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 构建单条消息的总结请求
|
|
182
|
+
*/
|
|
183
|
+
export function buildSingleMessageSummarizeRequest(
|
|
184
|
+
message: ProtocolMessage,
|
|
185
|
+
): { summarizeMessages: ProtocolMessage[] } {
|
|
186
|
+
const summarizeMessages: ProtocolMessage[] = [
|
|
187
|
+
{ role: 'system', content: CONTENT_COMPRESS_SYSTEM_PROMPT },
|
|
188
|
+
{ role: 'user', content: message.content },
|
|
189
|
+
];
|
|
190
|
+
return { summarizeMessages };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const CONTENT_COMPRESS_SYSTEM_PROMPT = `你是一个文档压缩助手。用户将发送一段很长的文本(可能包含提问/指令 + 超长文档内容)。
|
|
194
|
+
|
|
195
|
+
你的任务:
|
|
196
|
+
1. 准确识别并保留用户的提问或指令(原文保留,不要改写)
|
|
197
|
+
2. 将超长文档内容压缩为结构化要点,保留所有关键信息
|
|
198
|
+
3. 输出格式:先输出用户的原始提问/指令,然后输出「文档要点:」+ 编号列表
|
|
199
|
+
4. 如果用户没有明确的提问(只是贴了文档),则直接输出文档要点
|
|
200
|
+
5. 要点应覆盖:核心主题、关键论点/数据、方法论、结论、重要细节
|
|
201
|
+
6. 不要遗漏可能影响后续分析的信息
|
|
202
|
+
|
|
203
|
+
直接输出结果,不要说"以下是摘要"之类的开头。`;
|
|
204
|
+
|
|
175
205
|
// ==================== 内部 ====================
|
|
176
206
|
|
|
177
207
|
const SUMMARIZE_SYSTEM_PROMPT = `你是一个对话历史压缩助手。请总结以下对话历史,保留所有关键信息:
|
|
@@ -187,27 +217,29 @@ const SUMMARIZE_SYSTEM_PROMPT = `你是一个对话历史压缩助手。请总
|
|
|
187
217
|
直接输出摘要,不要开头说"以下是摘要"之类的话。`;
|
|
188
218
|
|
|
189
219
|
/** 把消息列表格式化为可读文本,供总结用 */
|
|
190
|
-
function formatMessagesForSummary(messages:
|
|
220
|
+
function formatMessagesForSummary(messages: ProtocolMessage[]): string {
|
|
191
221
|
const parts: string[] = [];
|
|
192
222
|
|
|
193
223
|
for (const msg of messages) {
|
|
224
|
+
if (msg.role === 'system') {
|
|
225
|
+
// 上一轮的历史摘要,去掉标记前缀后直接当上下文
|
|
226
|
+
const content = msg.content.startsWith('[对话历史摘要]\n')
|
|
227
|
+
? msg.content.slice('[对话历史摘要]\n'.length)
|
|
228
|
+
: msg.content;
|
|
229
|
+
if (content) parts.push(`[上一轮摘要]\n${content}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
194
233
|
const role = msg.role === 'assistant' ? 'AI' : msg.role === 'user' ? '用户' : '工具';
|
|
195
234
|
|
|
196
235
|
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
197
236
|
const calls = msg.toolCalls.map(tc => {
|
|
198
|
-
|
|
199
|
-
? tc.arguments.slice(0, 200) + '...'
|
|
200
|
-
: tc.arguments;
|
|
201
|
-
return ` 调用 ${tc.name}(${argsPreview})`;
|
|
237
|
+
return ` 调用 ${tc.name}(${tc.arguments})`;
|
|
202
238
|
}).join('\n');
|
|
203
239
|
const text = msg.content ? `${msg.content}\n${calls}` : calls;
|
|
204
240
|
parts.push(`[${role}]\n${text}`);
|
|
205
241
|
} else if (msg.role === 'tool') {
|
|
206
|
-
|
|
207
|
-
const content = msg.content.length > 1000
|
|
208
|
-
? msg.content.slice(0, 1000) + `... (共 ${msg.content.length} 字符)`
|
|
209
|
-
: msg.content;
|
|
210
|
-
parts.push(`[${role}: ${msg.toolName ?? 'unknown'}]\n${content}`);
|
|
242
|
+
parts.push(`[${role}: ${msg.toolName ?? 'unknown'}]\n${msg.content}`);
|
|
211
243
|
} else if (msg.content) {
|
|
212
244
|
parts.push(`[${role}]\n${msg.content}`);
|
|
213
245
|
}
|