@cup319/mmpl 2.5.0
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/README.md +187 -0
- package/dist/auto-context-NUQFETSY.js +248 -0
- package/dist/auto-context-NUQFETSY.js.map +1 -0
- package/dist/checkpoint-ABRNCBCA.js +68 -0
- package/dist/checkpoint-ABRNCBCA.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-CHVFXR24.js +19 -0
- package/dist/chunk-CHVFXR24.js.map +1 -0
- package/dist/chunk-JL66S7P7.js +151 -0
- package/dist/chunk-JL66S7P7.js.map +1 -0
- package/dist/chunk-SSDURI3I.js +46 -0
- package/dist/chunk-SSDURI3I.js.map +1 -0
- package/dist/index.js +21841 -0
- package/dist/index.js.map +1 -0
- package/dist/mermaid-injector-ROQAAM7B.js +171 -0
- package/dist/mermaid-injector-ROQAAM7B.js.map +1 -0
- package/dist/observability-6AMQYVCF.js +100 -0
- package/dist/observability-6AMQYVCF.js.map +1 -0
- package/dist/offload-engine-BF5IXXBH.js +10 -0
- package/dist/offload-engine-BF5IXXBH.js.map +1 -0
- package/dist/persona-generator-NZQVIVDA.js +228 -0
- package/dist/persona-generator-NZQVIVDA.js.map +1 -0
- package/dist/pipeline-manager-IJ2JLUMK.js +485 -0
- package/dist/pipeline-manager-IJ2JLUMK.js.map +1 -0
- package/dist/scene-builder-TLVG23BJ.js +296 -0
- package/dist/scene-builder-TLVG23BJ.js.map +1 -0
- package/dist/scene-index-SU6GMVDL.js +178 -0
- package/dist/scene-index-SU6GMVDL.js.map +1 -0
- package/dist/symbolic-A4XAGBBB.js +502 -0
- package/dist/symbolic-A4XAGBBB.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# MMPL MCP
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/mmpl)
|
|
4
|
+
[](https://github.com/cnb-t/mmpl/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
基于 DuckDB 的向量记忆系统,具备完整的记忆生命周期管理,以常驻 HTTP MCP Server 形式运行(v2.5.0+,多 client 共享单进程,避免 DuckDB 文件锁冲突)���
|
|
7
|
+
|
|
8
|
+
## 核心特性
|
|
9
|
+
|
|
10
|
+
- **8 个精简工具**:write、search、manage、evolve、compress、palace、symbol、stats
|
|
11
|
+
- **模型自动下载**:首次启动自动从 HuggingFace 下载 ONNX 模型(默认 all-MiniLM-L6-v2,90MB,384维)
|
|
12
|
+
- **跨平台支持**:Windows + Linux,自动适配路径和信号处理
|
|
13
|
+
- **记忆生命周期**:Active → Infrequent → Archived → Permanent(含衰减、强化、SM-2 间隔复习)
|
|
14
|
+
- **混��检索**:BM25(FTS5)+ 向量(HNSW)+ 场景 + 用户画像
|
|
15
|
+
- **L0-L3 分层管道**:原始记录 → 原子事实 → 场景块 → 用户画像
|
|
16
|
+
- **符号压缩**:任务工具调用对 → Mermaid 符号图 + 钻取回放
|
|
17
|
+
- **可选 LLM 分析**:重要性评分、类型分类、实体抽取、情感分析
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @cup319/mmpl
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
安装后在 `node_modules/mmpl/dist/index.js`。作为 MCP Server 使用时,在配置中指向这个路径即可。
|
|
26
|
+
|
|
27
|
+
或从 Git 源码构建:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://cnb.cool/cnb-t/mmpl.git
|
|
31
|
+
cd mmpl
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 配置
|
|
37
|
+
|
|
38
|
+
### 部署:常驻 HTTP MCP Server(推荐 — 多 client 共享)
|
|
39
|
+
|
|
40
|
+
v2.5.0 起,mmpl 默认作为 **Streamable HTTP MCP server** 运行:一个常驻进程独占 DuckDB,被多个 MCP client(ZCode / Claude Desktop / IDE 等)共享。这从根本上避免了 stdio 模式下多 client 各自 spawn 进程、争抢同一个 `.duckdb` 文件导致的 `Connection closed` 错误。
|
|
41
|
+
|
|
42
|
+
**第一步:启动 server(一次)**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node ./node_modules/mmpl/dist/index.js
|
|
46
|
+
# MMPL MCP Server v2.5.0 listening on http://127.0.0.1:7680/mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
启动时通过环境变量配置 embedding / DB / LLM:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
EMBEDDING_PROVIDER=onnx \
|
|
53
|
+
DB_PATH=./data/memory.duckdb \
|
|
54
|
+
ONNX_MODEL_PATH=./models/all-MiniLM-L6-v2.onnx \
|
|
55
|
+
ANALYZER_API_URL=https://api.z.ai/api/paas/v4/chat/completions \
|
|
56
|
+
ANALYZER_API_KEY=<your-api-key> \
|
|
57
|
+
ANALYZER_MODEL=glm-4-flash \
|
|
58
|
+
node ./node_modules/mmpl/dist/index.js
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**第二步:client 配置(每个 client 一份,只填 URL)**
|
|
62
|
+
|
|
63
|
+
ZCode (`~/.zcode/cli/config.json`):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcp": {
|
|
68
|
+
"servers": {
|
|
69
|
+
"mmpl": {
|
|
70
|
+
"type": "http",
|
|
71
|
+
"url": "http://127.0.0.1:7680/mcp"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Claude Desktop / Claude Code (`claude_desktop_config.json` 或 `~/.claude.json`):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"mmpl": {
|
|
84
|
+
"type": "http",
|
|
85
|
+
"url": "http://127.0.0.1:7680/mcp"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Health check(确认 server 活着)**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
curl http://127.0.0.1:7680/
|
|
95
|
+
# {"status":"ok","server":"mmpl","sessions":3}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**常驻进程管理(可选)**
|
|
99
|
+
|
|
100
|
+
HTTP server 启动后不自我守护,靠外部保活:
|
|
101
|
+
|
|
102
|
+
- **Windows**: `nssm install MemoryPalace node D:\path\to\dist\index.js`,或用 Task Scheduler
|
|
103
|
+
- **Linux/macOS**: systemd unit 或 `pm2 start dist/index.js --name mmpl`
|
|
104
|
+
|
|
105
|
+
> server 默认只绑 `127.0.0.1`(本机访问)。多 client 共享时**不要**起第二个 server 进程——DuckDB 文件是单进程独占的,第二个进程会启动失败;多个 client 都连同一个 HTTP URL 即可。
|
|
106
|
+
|
|
107
|
+
### 遗留:stdio 模式
|
|
108
|
+
|
|
109
|
+
v2.5.0 之前默认走 stdio,每个 client 各 spawn 一个进程。该模式已在 v2.5.0 移除(多 client 场景下 DuckDB 文件锁无法解决)。如确需单 client stdio,可 git checkout v2.4.4 标签。
|
|
110
|
+
|
|
111
|
+
## 环境变量
|
|
112
|
+
|
|
113
|
+
**Server 启动时(HTTP 进程的 env)**
|
|
114
|
+
|
|
115
|
+
| 变量 | 默认值 | 说明 |
|
|
116
|
+
|------|--------|------|
|
|
117
|
+
| `EMBEDDING_PROVIDER` | `onnx` | `onnx` / `api` / `tfidf` |
|
|
118
|
+
| `ONNX_MODEL_PATH` | `./models/all-MiniLM-L6-v2.onnx` | ONNX 模型路径 |
|
|
119
|
+
| `DB_PATH` | `./data/memory.duckdb` | DuckDB 数据库路径 |
|
|
120
|
+
| `ANALYZER_API_URL` | — | LLM API 地址(智谱/OpenAI) |
|
|
121
|
+
| `ANALYZER_API_KEY` | — | LLM API 密钥 |
|
|
122
|
+
| `ANALYZER_MODEL` | `glm-4-flash` | 分析用模型 |
|
|
123
|
+
| `TFIDF_DICT_PATH` | `./data/tfidf-dict.json` | TF-IDF 字典路径(可选,仅 tfidf provider 使用,运行时自动生成) |
|
|
124
|
+
| `MCP_HTTP_HOST` | `127.0.0.1` | HTTP server 绑定地址(`0.0.0.0` 开放外网,需自行加防火墙/鉴权) |
|
|
125
|
+
| `MCP_HTTP_PORT` | `7680` | HTTP server 端口 |
|
|
126
|
+
| `MCP_HTTP_PATH` | `/mcp` | MCP 端点路径 |
|
|
127
|
+
|
|
128
|
+
## 工具一览
|
|
129
|
+
|
|
130
|
+
| 工具 | 说明 |
|
|
131
|
+
|------|------|
|
|
132
|
+
| `memory_write` | 写入记忆(encode 编码 + capture 捕获) |
|
|
133
|
+
| `memory_search` | 检索记忆(quick/deep/hybrid/keyword + drill 钻取) |
|
|
134
|
+
| `memory_manage` | 管理生命周期(forget/reinforce/consolidate/reclaim/reembed) |
|
|
135
|
+
| `memory_evolve` | 自动进化(extract 提取 + persona 画像更新) |
|
|
136
|
+
| `memory_compress` | 压缩记忆(compress/decompress/offload) |
|
|
137
|
+
| `memory_palace` | 记忆宫殿(build/explore/visualize/scenes) |
|
|
138
|
+
| `memory_symbol` | 符号记忆(create/search/export) |
|
|
139
|
+
| `memory_stats` | 统计监控(summary/strength/review/pipeline/metrics) |
|
|
140
|
+
|
|
141
|
+
## ONNX 模型
|
|
142
|
+
|
|
143
|
+
首次启动时自动从 HuggingFace 下载模型及 tokenizer 文件(~90MB),支持断点续传。
|
|
144
|
+
|
|
145
|
+
默认使用 `all-MiniLM-L6-v2`(384维,英文优化)。如需多语言支持,设置 `ONNX_MODEL_PATH` 指向 `paraphrase-multilingual-MiniLM-L12-v2`(384维,50+语言,~470MB)。
|
|
146
|
+
|
|
147
|
+
自动下载失败时,控制台会打印平台适配的手动下载命令。
|
|
148
|
+
|
|
149
|
+
### 国内网络环境(镜像加速)
|
|
150
|
+
|
|
151
|
+
HuggingFace 官方源(`huggingface.co`)在国内访问经常超时。MMPL 内置**自动镜像切换**:
|
|
152
|
+
|
|
153
|
+
- 启动时探测 `huggingface.co`(3 秒超时)
|
|
154
|
+
- 可达 → 用官方源
|
|
155
|
+
- 不可达 → 自动切到国内镜像 `hf-mirror.com`
|
|
156
|
+
|
|
157
|
+
控制台会打印实际选用的源,例如:
|
|
158
|
+
```
|
|
159
|
+
[ONNX] HuggingFace 官方源不可达 (3000ms 超时),切换国内镜像 https://hf-mirror.com
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**手动指定镜像源**(覆盖自动探测):设置 `HF_ENDPOINT` 环境变量(HuggingFace 官方约定):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Windows
|
|
166
|
+
set HF_ENDPOINT=https://hf-mirror.com
|
|
167
|
+
|
|
168
|
+
# Linux/macOS
|
|
169
|
+
export HF_ENDPOINT=https://hf-mirror.com
|
|
170
|
+
|
|
171
|
+
# 或者用自定义镜像
|
|
172
|
+
export HF_ENDPOINT=https://your-mirror.example.com
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`HF_ENDPOINT` 优先级最高,设置后会跳过自动探测直接使用。
|
|
176
|
+
|
|
177
|
+
## 开发
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm test # 单元测试(75 用例)
|
|
181
|
+
npm run lint # 类型检查
|
|
182
|
+
npm run dev # 开发模式
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 许可证
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
OffloadEngine
|
|
4
|
+
} from "./chunk-JL66S7P7.js";
|
|
5
|
+
import {
|
|
6
|
+
estimateTokens
|
|
7
|
+
} from "./chunk-CHVFXR24.js";
|
|
8
|
+
import "./chunk-2H7UOFLK.js";
|
|
9
|
+
|
|
10
|
+
// src/auto-context.ts
|
|
11
|
+
var EMPTY_RESULT = {
|
|
12
|
+
contextText: "",
|
|
13
|
+
tokenCount: 0,
|
|
14
|
+
memoryCount: 0,
|
|
15
|
+
hasPersona: false,
|
|
16
|
+
sceneCount: 0,
|
|
17
|
+
offloadStrategy: "none",
|
|
18
|
+
pendingL0: 0
|
|
19
|
+
};
|
|
20
|
+
var AutoContextGenerator = class {
|
|
21
|
+
opts;
|
|
22
|
+
offloadEngine;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
this.offloadEngine = opts.offloadEngine ?? new OffloadEngine({ tokenBudget: opts.tokenBudget ?? 2e3 });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 根据用户查询生成完整的记忆上下文。
|
|
29
|
+
*
|
|
30
|
+
* 全程 try-catch,任何子步骤失败都会被吞掉并跳过;整体失败返回空上下文。
|
|
31
|
+
*/
|
|
32
|
+
async generate(query) {
|
|
33
|
+
if (!query || !query.trim()) {
|
|
34
|
+
return { ...EMPTY_RESULT };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const topK = this.opts.topK ?? 5;
|
|
38
|
+
const relevanceThreshold = this.opts.relevanceThreshold ?? 0.5;
|
|
39
|
+
const scanLimit = this.opts.scanLimit ?? 50;
|
|
40
|
+
const personaTokenLimit = this.opts.personaTokenLimit ?? 500;
|
|
41
|
+
const snippetLen = this.opts.memorySnippetLength ?? 120;
|
|
42
|
+
const entries = [];
|
|
43
|
+
await this.collectVectorHits(
|
|
44
|
+
query,
|
|
45
|
+
entries,
|
|
46
|
+
relevanceThreshold,
|
|
47
|
+
scanLimit
|
|
48
|
+
);
|
|
49
|
+
await this.collectKeywordHits(query, entries, topK);
|
|
50
|
+
await this.collectSceneHits(query, entries);
|
|
51
|
+
const offloadResult = this.offloadEngine.compress(entries);
|
|
52
|
+
let contextText = "";
|
|
53
|
+
if (offloadResult.retained.length > 0) {
|
|
54
|
+
contextText += "## \u76F8\u5173\u8BB0\u5FC6\n";
|
|
55
|
+
for (const e of offloadResult.retained) {
|
|
56
|
+
const snippet = e.content.length > snippetLen ? e.content.substring(0, snippetLen) + "\u2026" : e.content;
|
|
57
|
+
contextText += `- ${snippet}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
if (offloadResult.summary) {
|
|
61
|
+
contextText += `
|
|
62
|
+
> ${offloadResult.summary}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const personaOut = await this.injectPersona(
|
|
67
|
+
contextText,
|
|
68
|
+
personaTokenLimit
|
|
69
|
+
);
|
|
70
|
+
contextText = personaOut.text;
|
|
71
|
+
const hasPersona = personaOut.injected;
|
|
72
|
+
let hasMermaid = false;
|
|
73
|
+
if (this.opts.mermaidInjector && this.opts.sessionId) {
|
|
74
|
+
try {
|
|
75
|
+
const mmdResult = await this.opts.mermaidInjector.injectExisting(this.opts.sessionId);
|
|
76
|
+
if (mmdResult.injectText) {
|
|
77
|
+
contextText = mmdResult.injectText + "\n\n" + contextText;
|
|
78
|
+
hasMermaid = true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let pendingL0 = 0;
|
|
84
|
+
try {
|
|
85
|
+
const raw = await this.opts.storage.getUnconsumedRaw(100);
|
|
86
|
+
pendingL0 = Array.isArray(raw) ? raw.length : 0;
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
const tokenCount = estimateTokens(contextText);
|
|
90
|
+
return {
|
|
91
|
+
contextText,
|
|
92
|
+
tokenCount,
|
|
93
|
+
memoryCount: offloadResult.retained.filter((e) => e.layer === 1).length,
|
|
94
|
+
hasPersona,
|
|
95
|
+
sceneCount: offloadResult.retained.filter((e) => e.layer === 2).length,
|
|
96
|
+
offloadStrategy: offloadResult.strategy,
|
|
97
|
+
pendingL0
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(
|
|
101
|
+
"[auto-context] generate failed:",
|
|
102
|
+
err instanceof Error ? err.message : err
|
|
103
|
+
);
|
|
104
|
+
return { ...EMPTY_RESULT };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 步骤 1: 向量检索 L1 active memories。
|
|
109
|
+
* 用 cosine 相似度过滤,命中阈值则加入 entries。
|
|
110
|
+
*/
|
|
111
|
+
async collectVectorHits(query, entries, relevanceThreshold, scanLimit) {
|
|
112
|
+
try {
|
|
113
|
+
if (!this.opts.embeddingService?.available) return;
|
|
114
|
+
const emb = await this.opts.embeddingService.embed(query);
|
|
115
|
+
if (!emb || emb.length === 0) return;
|
|
116
|
+
const memories = await this.opts.storage.getActiveMemories();
|
|
117
|
+
if (!Array.isArray(memories)) return;
|
|
118
|
+
for (const m of memories.slice(0, scanLimit)) {
|
|
119
|
+
let mEmb = m.embedding;
|
|
120
|
+
if (typeof mEmb === "string") {
|
|
121
|
+
try {
|
|
122
|
+
mEmb = JSON.parse(mEmb);
|
|
123
|
+
} catch {
|
|
124
|
+
mEmb = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(mEmb) || mEmb.length === 0) continue;
|
|
128
|
+
const sim = this.cosineSim(emb, mEmb);
|
|
129
|
+
if (sim >= relevanceThreshold) {
|
|
130
|
+
entries.push({
|
|
131
|
+
id: m.id,
|
|
132
|
+
content: m.content,
|
|
133
|
+
score: sim,
|
|
134
|
+
layer: 1,
|
|
135
|
+
importance: typeof m.importance === "number" ? m.importance : 0.5,
|
|
136
|
+
strength: typeof m.strength === "number" ? m.strength : 0.5,
|
|
137
|
+
source: "vector"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 步骤 2: BM25 关键词检索补充 L1。
|
|
146
|
+
* 只加入尚未被向量召回命中的记忆。
|
|
147
|
+
*/
|
|
148
|
+
async collectKeywordHits(query, entries, topK) {
|
|
149
|
+
try {
|
|
150
|
+
const results = await this.opts.storage.searchByKeywords(
|
|
151
|
+
query,
|
|
152
|
+
topK,
|
|
153
|
+
"active"
|
|
154
|
+
);
|
|
155
|
+
if (!Array.isArray(results)) return;
|
|
156
|
+
for (const m of results) {
|
|
157
|
+
if (entries.find((e) => e.id === m.id)) continue;
|
|
158
|
+
entries.push({
|
|
159
|
+
id: m.id,
|
|
160
|
+
content: m.content,
|
|
161
|
+
score: 0.7,
|
|
162
|
+
layer: 1,
|
|
163
|
+
importance: typeof m.importance === "number" ? m.importance : 0.5,
|
|
164
|
+
strength: typeof m.strength === "number" ? m.strength : 0.5,
|
|
165
|
+
source: "bm25"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 步骤 3: 场景匹配 L2。
|
|
173
|
+
* 用 label / summary 子串匹配(大小写不敏感)。
|
|
174
|
+
*/
|
|
175
|
+
async collectSceneHits(query, entries) {
|
|
176
|
+
try {
|
|
177
|
+
const scenes = await this.opts.storage.getAllScenes();
|
|
178
|
+
if (!Array.isArray(scenes)) return;
|
|
179
|
+
const qLower = query.toLowerCase();
|
|
180
|
+
for (const s of scenes) {
|
|
181
|
+
const label = (s.label ?? "").toLowerCase();
|
|
182
|
+
const summary = (s.summary ?? "").toLowerCase();
|
|
183
|
+
if (label.includes(qLower) || summary.includes(qLower)) {
|
|
184
|
+
entries.push({
|
|
185
|
+
id: s.id,
|
|
186
|
+
content: `[\u573A\u666F] ${s.label ?? s.id}: ${(s.summary ?? "").substring(
|
|
187
|
+
0,
|
|
188
|
+
80
|
|
189
|
+
)}`,
|
|
190
|
+
score: 0.6,
|
|
191
|
+
layer: 2,
|
|
192
|
+
importance: 0.7,
|
|
193
|
+
strength: 1,
|
|
194
|
+
source: "scene"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 步骤 6: 注入 L3 用户画像。
|
|
203
|
+
* 画像内容不参与 offload 压缩,但有自己的 token 上限保护。
|
|
204
|
+
* 返回更新后的文本和是否注入标志。
|
|
205
|
+
*/
|
|
206
|
+
async injectPersona(contextText, personaTokenLimit) {
|
|
207
|
+
try {
|
|
208
|
+
const persona = await this.opts.storage.getLatestPersona();
|
|
209
|
+
if (!persona || !persona.content) {
|
|
210
|
+
return { text: contextText, injected: false };
|
|
211
|
+
}
|
|
212
|
+
const personaTokens = estimateTokens(persona.content);
|
|
213
|
+
if (personaTokens >= personaTokenLimit) {
|
|
214
|
+
return { text: contextText, injected: false };
|
|
215
|
+
}
|
|
216
|
+
const text = contextText + `
|
|
217
|
+
## \u7528\u6237\u753B\u50CF
|
|
218
|
+
${persona.content.substring(0, 300)}
|
|
219
|
+
`;
|
|
220
|
+
return { text, injected: true };
|
|
221
|
+
} catch {
|
|
222
|
+
return { text: contextText, injected: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 余弦相似度(内联实现,不依赖 retrieval.ts)
|
|
227
|
+
* 不等长或空向量返回 0。
|
|
228
|
+
*/
|
|
229
|
+
cosineSim(a, b) {
|
|
230
|
+
if (!a || !b || a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
let dot = 0;
|
|
234
|
+
let magA = 0;
|
|
235
|
+
let magB = 0;
|
|
236
|
+
for (let i = 0; i < a.length; i++) {
|
|
237
|
+
dot += a[i] * b[i];
|
|
238
|
+
magA += a[i] * a[i];
|
|
239
|
+
magB += b[i] * b[i];
|
|
240
|
+
}
|
|
241
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
242
|
+
return denom > 0 ? dot / denom : 0;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
export {
|
|
246
|
+
AutoContextGenerator
|
|
247
|
+
};
|
|
248
|
+
//# sourceMappingURL=auto-context-NUQFETSY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auto-context.ts"],"sourcesContent":["/**\n * 自动召回上下文生成器 (Auto-Context Generator)\n *\n * 背景:\n * MCP Server 无法 Hook agent 的生命周期(不像 OpenClaw 这类插件框架可以\n * 拦截 before_prompt_build),因此这里实现一个\"一站式\"工具: agent 在每轮\n * 对话开始时调用一次 generate(query),即可拿到该轮所需的全部记忆上下文。\n *\n * 这相当于 TencentDB AgentMemory 的 before_prompt_build Hook 的 MCP 版本。\n *\n * 流程:\n * 1. 向量检索 L1 active memories(语义相似)\n * 2. BM25 关键词检索 L1 补充(覆盖向量召回遗漏)\n * 3. 场景匹配 L2(label / summary 子串匹配)\n * 4. L3 用户画像固定注入(不参与压缩)\n * 5. 检查 L0 待处理计数(提示 agent 是否需要 consolidate)\n * 6. 用 OffloadEngine 把 L1+L2 压缩到 token 预算内\n * 7. 格式化为可读 Markdown 文本\n *\n * 全程 try-catch,任何子步骤失败都不会抛出,最坏返回空上下文。\n * 不依赖任何外部库。\n */\n\nimport { OffloadEngine, OffloadEntry } from \"./offload-engine.js\";\nimport { estimateTokens } from \"./token-counter.js\";\n\n/** AutoContextGenerator 配置选项 */\nexport interface AutoContextOptions {\n /** 存储层(鸭子类型,提供 getActiveMemories / getAllScenes / getLatestPersona / searchByKeywords / getUnconsumedRaw 等) */\n storage: any;\n /** Embedding 服务(提供 available / embed) */\n embeddingService: any;\n /** 可选的 OffloadEngine,不传则按 tokenBudget 自动创建 */\n offloadEngine?: OffloadEngine;\n /** 可选的 MermaidInjector,提供时自动注入任务符号图 */\n mermaidInjector?: any;\n /** 可选的 session_id,用于注入已有 Mermaid 会话图 */\n sessionId?: string;\n /** 上下文 token 预算,默认 2000 */\n tokenBudget?: number;\n /** 每层取 top-k,默认 5 */\n topK?: number;\n /** 相关性阈值,默认 0.5 */\n relevanceThreshold?: number;\n /** 向量扫描上限(性能保护),默认 50 */\n scanLimit?: number;\n /** 画像内容 token 上限,超过则不注入,默认 500 */\n personaTokenLimit?: number;\n /** 单条记忆输出截断长度,默认 120 */\n memorySnippetLength?: number;\n}\n\n/** generate() 返回结果 */\nexport interface AutoContextResult {\n /** 注入到 prompt 前面的记忆上下文文本 */\n contextText: string;\n /** 上下文 token 数 */\n tokenCount: number;\n /** 命中的记忆条数(L1 retained) */\n memoryCount: number;\n /** 是否包含用户画像 */\n hasPersona: boolean;\n /** 命中的场景数(L2 retained) */\n sceneCount: number;\n /** 压缩策略名称 */\n offloadStrategy: string;\n /** 未消费的 L0 计数 */\n pendingL0: number;\n}\n\n/** 空结果常量,失败时统一返回 */\nconst EMPTY_RESULT: AutoContextResult = {\n contextText: \"\",\n tokenCount: 0,\n memoryCount: 0,\n hasPersona: false,\n sceneCount: 0,\n offloadStrategy: \"none\",\n pendingL0: 0,\n};\n\n/**\n * 自动召回上下文生成器\n */\nexport class AutoContextGenerator {\n private opts: AutoContextOptions;\n private offloadEngine: OffloadEngine;\n\n constructor(opts: AutoContextOptions) {\n this.opts = opts;\n // 若未提供 offloadEngine,按 tokenBudget 自动创建一个\n this.offloadEngine =\n opts.offloadEngine ??\n new OffloadEngine({ tokenBudget: opts.tokenBudget ?? 2000 });\n }\n\n /**\n * 根据用户查询生成完整的记忆上下文。\n *\n * 全程 try-catch,任何子步骤失败都会被吞掉并跳过;整体失败返回空上下文。\n */\n async generate(query: string): Promise<AutoContextResult> {\n // 防御:空 query 直接返回空上下文\n if (!query || !query.trim()) {\n return { ...EMPTY_RESULT };\n }\n\n try {\n const topK = this.opts.topK ?? 5;\n const relevanceThreshold = this.opts.relevanceThreshold ?? 0.5;\n const scanLimit = this.opts.scanLimit ?? 50;\n const personaTokenLimit = this.opts.personaTokenLimit ?? 500;\n const snippetLen = this.opts.memorySnippetLength ?? 120;\n\n const entries: OffloadEntry[] = [];\n\n // ===== 1. 向量检索 L1 active memories =====\n await this.collectVectorHits(\n query,\n entries,\n relevanceThreshold,\n scanLimit\n );\n\n // ===== 2. BM25 关键词检索补充 L1 =====\n await this.collectKeywordHits(query, entries, topK);\n\n // ===== 3. 场景匹配 L2 =====\n await this.collectSceneHits(query, entries);\n\n // ===== 4. 用 OffloadEngine 压缩到 token 预算 =====\n const offloadResult = this.offloadEngine.compress(entries);\n\n // ===== 5. 格式化记忆 + 场景文本 =====\n let contextText = \"\";\n if (offloadResult.retained.length > 0) {\n contextText += \"## 相关记忆\\n\";\n for (const e of offloadResult.retained) {\n const snippet =\n e.content.length > snippetLen\n ? e.content.substring(0, snippetLen) + \"…\"\n : e.content;\n contextText += `- ${snippet}\\n`;\n }\n if (offloadResult.summary) {\n contextText += `\\n> ${offloadResult.summary}\\n`;\n }\n }\n\n // ===== 6. L3 用户画像(固定注入,不参与压缩) =====\n const personaOut = await this.injectPersona(\n contextText,\n personaTokenLimit\n );\n contextText = personaOut.text;\n const hasPersona = personaOut.injected;\n\n // ===== 6.5 Mermaid 任务符号图注入 =====\n let hasMermaid = false;\n if (this.opts.mermaidInjector && this.opts.sessionId) {\n try {\n const mmdResult = await this.opts.mermaidInjector.injectExisting(this.opts.sessionId);\n if (mmdResult.injectText) {\n contextText = mmdResult.injectText + \"\\n\\n\" + contextText;\n hasMermaid = true;\n }\n } catch {\n /* ignore mermaid injection failure */\n }\n }\n\n // ===== 7. L0 待处理计数 =====\n let pendingL0 = 0;\n try {\n const raw = await this.opts.storage.getUnconsumedRaw(100);\n pendingL0 = Array.isArray(raw) ? raw.length : 0;\n } catch {\n /* ignore */\n }\n\n const tokenCount = estimateTokens(contextText);\n\n return {\n contextText,\n tokenCount,\n memoryCount: offloadResult.retained.filter((e) => e.layer === 1).length,\n hasPersona,\n sceneCount: offloadResult.retained.filter((e) => e.layer === 2).length,\n offloadStrategy: offloadResult.strategy,\n pendingL0,\n };\n } catch (err) {\n // 任何意外错误都返回空上下文,绝不抛出\n console.error(\n \"[auto-context] generate failed:\",\n err instanceof Error ? err.message : err\n );\n return { ...EMPTY_RESULT };\n }\n }\n\n /**\n * 步骤 1: 向量检索 L1 active memories。\n * 用 cosine 相似度过滤,命中阈值则加入 entries。\n */\n private async collectVectorHits(\n query: string,\n entries: OffloadEntry[],\n relevanceThreshold: number,\n scanLimit: number\n ): Promise<void> {\n try {\n if (!this.opts.embeddingService?.available) return;\n const emb = await this.opts.embeddingService.embed(query);\n if (!emb || emb.length === 0) return;\n\n const memories = await this.opts.storage.getActiveMemories();\n if (!Array.isArray(memories)) return;\n\n for (const m of memories.slice(0, scanLimit)) {\n // embedding 可能存成 JSON 字符串\n let mEmb: any = m.embedding;\n if (typeof mEmb === \"string\") {\n try {\n mEmb = JSON.parse(mEmb);\n } catch {\n mEmb = null;\n }\n }\n if (!Array.isArray(mEmb) || mEmb.length === 0) continue;\n\n const sim = this.cosineSim(emb, mEmb);\n if (sim >= relevanceThreshold) {\n entries.push({\n id: m.id,\n content: m.content,\n score: sim,\n layer: 1,\n importance: typeof m.importance === \"number\" ? m.importance : 0.5,\n strength: typeof m.strength === \"number\" ? m.strength : 0.5,\n source: \"vector\",\n });\n }\n }\n } catch {\n /* ignore — 向量检索失败不影响后续步骤 */\n }\n }\n\n /**\n * 步骤 2: BM25 关键词检索补充 L1。\n * 只加入尚未被向量召回命中的记忆。\n */\n private async collectKeywordHits(\n query: string,\n entries: OffloadEntry[],\n topK: number\n ): Promise<void> {\n try {\n const results = await this.opts.storage.searchByKeywords(\n query,\n topK,\n \"active\"\n );\n if (!Array.isArray(results)) return;\n for (const m of results) {\n if (entries.find((e) => e.id === m.id)) continue;\n entries.push({\n id: m.id,\n content: m.content,\n score: 0.7,\n layer: 1,\n importance: typeof m.importance === \"number\" ? m.importance : 0.5,\n strength: typeof m.strength === \"number\" ? m.strength : 0.5,\n source: \"bm25\",\n });\n }\n } catch {\n /* ignore */\n }\n }\n\n /**\n * 步骤 3: 场景匹配 L2。\n * 用 label / summary 子串匹配(大小写不敏感)。\n */\n private async collectSceneHits(\n query: string,\n entries: OffloadEntry[]\n ): Promise<void> {\n try {\n const scenes = await this.opts.storage.getAllScenes();\n if (!Array.isArray(scenes)) return;\n const qLower = query.toLowerCase();\n for (const s of scenes) {\n const label = (s.label ?? \"\").toLowerCase();\n const summary = (s.summary ?? \"\").toLowerCase();\n if (label.includes(qLower) || summary.includes(qLower)) {\n entries.push({\n id: s.id,\n content: `[场景] ${s.label ?? s.id}: ${(s.summary ?? \"\").substring(\n 0,\n 80\n )}`,\n score: 0.6,\n layer: 2,\n importance: 0.7,\n strength: 1.0,\n source: \"scene\",\n });\n }\n }\n } catch {\n /* ignore */\n }\n }\n\n /**\n * 步骤 6: 注入 L3 用户画像。\n * 画像内容不参与 offload 压缩,但有自己的 token 上限保护。\n * 返回更新后的文本和是否注入标志。\n */\n private async injectPersona(\n contextText: string,\n personaTokenLimit: number\n ): Promise<{ text: string; injected: boolean }> {\n try {\n const persona = await this.opts.storage.getLatestPersona();\n if (!persona || !persona.content) {\n return { text: contextText, injected: false };\n }\n const personaTokens = estimateTokens(persona.content);\n if (personaTokens >= personaTokenLimit) {\n return { text: contextText, injected: false };\n }\n const text =\n contextText +\n `\\n## 用户画像\\n${persona.content.substring(0, 300)}\\n`;\n return { text, injected: true };\n } catch {\n return { text: contextText, injected: false };\n }\n }\n\n /**\n * 余弦相似度(内联实现,不依赖 retrieval.ts)\n * 不等长或空向量返回 0。\n */\n private cosineSim(a: number[], b: number[]): number {\n if (\n !a ||\n !b ||\n a.length === 0 ||\n b.length === 0 ||\n a.length !== b.length\n ) {\n return 0;\n }\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n magA += a[i] * a[i];\n magB += b[i] * b[i];\n }\n const denom = Math.sqrt(magA) * Math.sqrt(magB);\n return denom > 0 ? dot / denom : 0;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-2H7UOFLK.js";
|
|
3
|
+
|
|
4
|
+
// src/checkpoint.ts
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var CheckpointManager = class {
|
|
8
|
+
checkpointPath;
|
|
9
|
+
checkpoint;
|
|
10
|
+
dirty = false;
|
|
11
|
+
constructor(dataDir) {
|
|
12
|
+
this.checkpointPath = path.join(dataDir, ".metadata", "pipeline_checkpoint.json");
|
|
13
|
+
this.checkpoint = {
|
|
14
|
+
lastL1ProcessedRawId: null,
|
|
15
|
+
lastL1Time: 0,
|
|
16
|
+
lastL2Time: 0,
|
|
17
|
+
lastL3Time: 0,
|
|
18
|
+
totalL0Processed: 0,
|
|
19
|
+
totalL1Extracted: 0,
|
|
20
|
+
totalL2Built: 0,
|
|
21
|
+
totalL3Versions: 0,
|
|
22
|
+
updatedAt: 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(this.checkpointPath, "utf-8");
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
this.checkpoint = { ...this.checkpoint, ...parsed };
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
recordL1(rawId, extracted) {
|
|
34
|
+
this.checkpoint.lastL1ProcessedRawId = rawId;
|
|
35
|
+
this.checkpoint.lastL1Time = Date.now();
|
|
36
|
+
this.checkpoint.totalL0Processed++;
|
|
37
|
+
this.checkpoint.totalL1Extracted += extracted;
|
|
38
|
+
this.dirty = true;
|
|
39
|
+
}
|
|
40
|
+
recordL2(sceneCount) {
|
|
41
|
+
this.checkpoint.lastL2Time = Date.now();
|
|
42
|
+
this.checkpoint.totalL2Built += sceneCount;
|
|
43
|
+
this.dirty = true;
|
|
44
|
+
}
|
|
45
|
+
recordL3() {
|
|
46
|
+
this.checkpoint.lastL3Time = Date.now();
|
|
47
|
+
this.checkpoint.totalL3Versions++;
|
|
48
|
+
this.dirty = true;
|
|
49
|
+
}
|
|
50
|
+
getCheckpoint() {
|
|
51
|
+
return { ...this.checkpoint };
|
|
52
|
+
}
|
|
53
|
+
/** 持久化到磁盘(原子写入:tmp + rename) */
|
|
54
|
+
async flush() {
|
|
55
|
+
if (!this.dirty) return;
|
|
56
|
+
this.checkpoint.updatedAt = Date.now();
|
|
57
|
+
const dir = path.dirname(this.checkpointPath);
|
|
58
|
+
await fs.mkdir(dir, { recursive: true });
|
|
59
|
+
const tmpPath = this.checkpointPath + ".tmp";
|
|
60
|
+
await fs.writeFile(tmpPath, JSON.stringify(this.checkpoint, null, 2), "utf-8");
|
|
61
|
+
await fs.rename(tmpPath, this.checkpointPath);
|
|
62
|
+
this.dirty = false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export {
|
|
66
|
+
CheckpointManager
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=checkpoint-ABRNCBCA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checkpoint.ts"],"sourcesContent":["import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * 管道检查点 — 追踪记忆处理进度,支持崩溃恢复\n *\n * 灵感: TencentDB Agent Memory 的 checkpoint.ts\n */\n\nexport interface PipelineCheckpoint {\n /** 最后一次 L1 提取处理的 L0 记录 ID */\n lastL1ProcessedRawId: string | null;\n /** 最后一次 L1 提取时间 */\n lastL1Time: number;\n /** 最后一次 L2 场景构建时间 */\n lastL2Time: number;\n /** 最后一次 L3 画像生成时间 */\n lastL3Time: number;\n /** 已处理的 L0 记录总数 */\n totalL0Processed: number;\n /** 已提取的 L1 记忆总数 */\n totalL1Extracted: number;\n /** 已构建的 L2 场景总数 */\n totalL2Built: number;\n /** 已生成的 L3 画像版本数 */\n totalL3Versions: number;\n /** 检查点最后更新时间 */\n updatedAt: number;\n}\n\nexport class CheckpointManager {\n private checkpointPath: string;\n private checkpoint: PipelineCheckpoint;\n private dirty: boolean = false;\n\n constructor(dataDir: string) {\n this.checkpointPath = path.join(dataDir, \".metadata\", \"pipeline_checkpoint.json\");\n this.checkpoint = {\n lastL1ProcessedRawId: null,\n lastL1Time: 0, lastL2Time: 0, lastL3Time: 0,\n totalL0Processed: 0, totalL1Extracted: 0, totalL2Built: 0, totalL3Versions: 0,\n updatedAt: 0,\n };\n }\n\n async init(): Promise<void> {\n try {\n const raw = await fs.readFile(this.checkpointPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n this.checkpoint = { ...this.checkpoint, ...parsed };\n } catch { /* first run */ }\n }\n\n recordL1(rawId: string, extracted: number): void {\n this.checkpoint.lastL1ProcessedRawId = rawId;\n this.checkpoint.lastL1Time = Date.now();\n this.checkpoint.totalL0Processed++;\n this.checkpoint.totalL1Extracted += extracted;\n this.dirty = true;\n }\n\n recordL2(sceneCount: number): void {\n this.checkpoint.lastL2Time = Date.now();\n this.checkpoint.totalL2Built += sceneCount;\n this.dirty = true;\n }\n\n recordL3(): void {\n this.checkpoint.lastL3Time = Date.now();\n this.checkpoint.totalL3Versions++;\n this.dirty = true;\n }\n\n getCheckpoint(): PipelineCheckpoint { return { ...this.checkpoint }; }\n\n /** 持久化到磁盘(原子写入:tmp + rename) */\n async flush(): Promise<void> {\n if (!this.dirty) return;\n this.checkpoint.updatedAt = Date.now();\n const dir = path.dirname(this.checkpointPath);\n await fs.mkdir(dir, { recursive: true });\n const tmpPath = this.checkpointPath + \".tmp\";\n await fs.writeFile(tmpPath, JSON.stringify(this.checkpoint, null, 2), \"utf-8\");\n await fs.rename(tmpPath, this.checkpointPath);\n this.dirty = false;\n }\n}\n"],"mappings":";;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AA6BV,IAAM,oBAAN,MAAwB;AAAA,EACrB;AAAA,EACA;AAAA,EACA,QAAiB;AAAA,EAEzB,YAAY,SAAiB;AAC3B,SAAK,iBAAiB,KAAK,KAAK,SAAS,aAAa,0BAA0B;AAChF,SAAK,aAAa;AAAA,MAChB,sBAAsB;AAAA,MACtB,YAAY;AAAA,MAAG,YAAY;AAAA,MAAG,YAAY;AAAA,MAC1C,kBAAkB;AAAA,MAAG,kBAAkB;AAAA,MAAG,cAAc;AAAA,MAAG,iBAAiB;AAAA,MAC5E,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI;AACF,YAAM,MAAM,MAAM,GAAG,SAAS,KAAK,gBAAgB,OAAO;AAC1D,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAK,aAAa,EAAE,GAAG,KAAK,YAAY,GAAG,OAAO;AAAA,IACpD,QAAQ;AAAA,IAAkB;AAAA,EAC5B;AAAA,EAEA,SAAS,OAAe,WAAyB;AAC/C,SAAK,WAAW,uBAAuB;AACvC,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW;AAChB,SAAK,WAAW,oBAAoB;AACpC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,SAAS,YAA0B;AACjC,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW,gBAAgB;AAChC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,WAAiB;AACf,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW;AAChB,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,gBAAoC;AAAE,WAAO,EAAE,GAAG,KAAK,WAAW;AAAA,EAAG;AAAA;AAAA,EAGrE,MAAM,QAAuB;AAC3B,QAAI,CAAC,KAAK,MAAO;AACjB,SAAK,WAAW,YAAY,KAAK,IAAI;AACrC,UAAM,MAAM,KAAK,QAAQ,KAAK,cAAc;AAC5C,UAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,UAAM,UAAU,KAAK,iBAAiB;AACtC,UAAM,GAAG,UAAU,SAAS,KAAK,UAAU,KAAK,YAAY,MAAM,CAAC,GAAG,OAAO;AAC7E,UAAM,GAAG,OAAO,SAAS,KAAK,cAAc;AAC5C,SAAK,QAAQ;AAAA,EACf;AACF;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__export
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-2H7UOFLK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/token-counter.ts
|
|
4
|
+
function estimateTokens(text) {
|
|
5
|
+
if (!text) return 0;
|
|
6
|
+
let count = 0;
|
|
7
|
+
for (const char of text) {
|
|
8
|
+
if (/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(char)) count += 1;
|
|
9
|
+
else if (/[a-zA-Z0-9]/.test(char)) count += 0.25;
|
|
10
|
+
else if (/\s/.test(char)) count += 0.1;
|
|
11
|
+
else count += 0.5;
|
|
12
|
+
}
|
|
13
|
+
return Math.ceil(count);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
estimateTokens
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=chunk-CHVFXR24.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/token-counter.ts"],"sourcesContent":["/**\n * 轻量 token 估算器 — 无需 tiktoken 依赖\n *\n * 估算公式:\n * 中文/CJK ~1 token/字\n * 英文字母数字 ~0.25 token/字符 (≈0.75 token/单词)\n * 空格 ~0.1 token\n * 标点符号 ~0.5 token\n *\n * 用于 Context Offload 压缩引擎的 token 预算计算。\n * 纯字符级估算,无外部依赖,开销极低。\n */\n\n/**\n * 估算文本的 token 数\n *\n * @param text 待估算的文本\n * @returns 估算的 token 数(向上取整)\n */\nexport function estimateTokens(text: string): number {\n if (!text) return 0;\n let count = 0;\n for (const char of text) {\n // CJK 字符(中日韩): ~1 token each\n if (/[\\u4e00-\\u9fff\\u3040-\\u30ff\\uac00-\\ud7af]/.test(char)) count += 1;\n // ASCII 字母数字: ~0.25 token (4 个字符约 1 token)\n else if (/[a-zA-Z0-9]/.test(char)) count += 0.25;\n // 空白字符: 0.1 token\n else if (/\\s/.test(char)) count += 0.1;\n // 标点及其他符号: 0.5 token\n else count += 0.5;\n }\n return Math.ceil(count);\n}\n\n/**\n * 估算消息数组的总 token 数(含结构开销)\n *\n * @param messages 消息列表,每条含 role 和 content\n * @returns 估算的总 token 数\n */\nexport function estimateMessagesTokens(\n messages: Array<{ role: string; content: string }>\n): number {\n let total = 0;\n for (const msg of messages) {\n total += estimateTokens(msg.content);\n total += 4; // role + structural overhead per message\n }\n return total + 2; // primer overhead\n}\n"],"mappings":";;;AAmBO,SAAS,eAAe,MAAsB;AACnD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,QAAQ;AACZ,aAAW,QAAQ,MAAM;AAEvB,QAAI,4CAA4C,KAAK,IAAI,EAAG,UAAS;AAAA,aAE5D,cAAc,KAAK,IAAI,EAAG,UAAS;AAAA,aAEnC,KAAK,KAAK,IAAI,EAAG,UAAS;AAAA,QAE9B,UAAS;AAAA,EAChB;AACA,SAAO,KAAK,KAAK,KAAK;AACxB;","names":[]}
|