@ghenya/clinn 0.8.8 → 0.9.1
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/Logos/StartLogo.txt +1 -1
- package/Mem/history.js +326 -24
- package/Mem/index.js +123 -14
- package/README.md +76 -164
- package/Src/agent.cjs +74 -11
- package/Src/index.js +81 -8
- package/Src/index.jsx +366 -108
- package/Tools/browser.js +1 -1
- package/Tools/extended_tools.js +83 -8
- package/Tools/index.js +1 -1
- package/Tools/search_tools.js +55 -1
- package/config.json +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,49 +1,35 @@
|
|
|
1
1
|
# Clinn — 终端原生 AI 编程助手
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Self-Evolving Terminal AI · Ink TUI · 50+ Tools · Session Memory
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @ghenya/clinn
|
|
7
|
+
clinn
|
|
8
|
+
```
|
|
8
9
|
|
|
9
10
|
```
|
|
10
|
-
██████
|
|
11
|
+
██████ ██ ██ ███ ██ ███ ██
|
|
11
12
|
██ ██ ██ ████ ██ ████ ██
|
|
12
13
|
██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
13
14
|
██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
14
|
-
██████ ███████ ██ ██ ████ ██ █ 0.
|
|
15
|
+
██████ ███████ ██ ██ ████ ██ █ 0.9
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
## 安装
|
|
20
21
|
|
|
21
|
-
### macOS / Linux / WSL
|
|
22
|
-
|
|
23
22
|
```bash
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
bash install.sh
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
安装后全局可用:终端输入 `clinn` 即可启动。
|
|
23
|
+
# npm 全局安装
|
|
24
|
+
npm install -g @ghenya/clinn
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```bat
|
|
26
|
+
# 或从 GitHub
|
|
34
27
|
git clone https://github.com/PillowBots/clinn.git
|
|
35
|
-
cd clinn
|
|
36
|
-
install.bat
|
|
28
|
+
cd clinn && bash install.sh
|
|
37
29
|
```
|
|
38
30
|
|
|
39
|
-
或直接双击 `clinn.bat`。
|
|
40
|
-
|
|
41
|
-
### 前置条件
|
|
42
|
-
|
|
43
31
|
- Node.js >= 18
|
|
44
|
-
- DeepSeek API Key
|
|
45
|
-
|
|
46
|
-
首次运行时会提示输入 API Key。
|
|
32
|
+
- DeepSeek API Key ([获取](https://platform.deepseek.com/))
|
|
47
33
|
|
|
48
34
|
---
|
|
49
35
|
|
|
@@ -51,190 +37,116 @@ install.bat
|
|
|
51
37
|
|
|
52
38
|
| 类别 | 工具 |
|
|
53
39
|
|---|---|
|
|
54
|
-
| **文件操作** | read, write, delete_file, move_file, copy_file, search_replace, edit_lines
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| **工具管理** | save_tool, delete_tool_file, list_saved_tools — AI 自己写的工具持久化保存 |
|
|
63
|
-
| **诊断** | get_diagnostics |
|
|
64
|
-
| **预览** | open_preview |
|
|
40
|
+
| **文件操作** | read, write, delete_file, move_file, copy_file, search_replace, edit_lines |
|
|
41
|
+
| **搜索** | grep (ripgrep优先), glob, search_in_files, find_files |
|
|
42
|
+
| **终端** | exec_console, wait_command |
|
|
43
|
+
| **网络** | web_search (Bing), web_fetch, browse_page (Puppeteer) |
|
|
44
|
+
| **记忆** | mem_rom (永久), mem_ram (会话), search_memory, list_memory, delete_memory |
|
|
45
|
+
| **对话** | forget_conversation, restart_session, compress_context |
|
|
46
|
+
| **Session** | list_sessions, view_session, search_sessions — 全局历史搜索 |
|
|
47
|
+
| **工具管理** | save_tool, delete_tool_file, list_saved_tools — AI 自写工具持久化 |
|
|
65
48
|
|
|
66
49
|
---
|
|
67
50
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
启动后直接输入自然语言:
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
> 帮我分析这个项目的代码结构
|
|
74
|
-
> 搜索项目里所有 TODO 注释
|
|
75
|
-
> 把 README.md 里的英文翻译成中文
|
|
76
|
-
> 液氮超频的最高纪录是多少?
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 内置命令
|
|
51
|
+
## 内置命令
|
|
80
52
|
|
|
81
53
|
| 命令 | 说明 |
|
|
82
54
|
|---|---|
|
|
83
|
-
| `/help` |
|
|
84
|
-
| `/
|
|
85
|
-
| `/
|
|
86
|
-
| `/
|
|
87
|
-
| `/
|
|
88
|
-
| `/
|
|
89
|
-
| `/
|
|
90
|
-
| `/
|
|
91
|
-
| `/
|
|
92
|
-
| `/
|
|
93
|
-
| `/
|
|
94
|
-
| `/
|
|
55
|
+
| `/help` | 查看帮助 |
|
|
56
|
+
| `/reset` | 重置对话 (新 session) |
|
|
57
|
+
| `/clear` | 清除消息 |
|
|
58
|
+
| `/ctx` | 上下文使用量 |
|
|
59
|
+
| `/status` | 当前状态 |
|
|
60
|
+
| `/tools` | 工具列表 |
|
|
61
|
+
| `/history [n]` | 最近历史 |
|
|
62
|
+
| `/history search <q>` | 搜索历史 |
|
|
63
|
+
| `/sessions` | 列出 session |
|
|
64
|
+
| `/session <id>` | 查看 session |
|
|
65
|
+
| `/session_search <q>` | 全局搜索 |
|
|
66
|
+
| `/memory` | 记忆统计 |
|
|
67
|
+
| `/memory_list [n]` | 记忆列表 |
|
|
68
|
+
| `/memory_search <q>` | 搜索记忆 |
|
|
69
|
+
| `/compress` | 压缩上下文 |
|
|
70
|
+
| `/trust <name>` | 信任工具 |
|
|
71
|
+
| `/api` | API 配置 |
|
|
95
72
|
| `/exit` | 退出 |
|
|
96
73
|
|
|
97
74
|
---
|
|
98
75
|
|
|
99
76
|
## 核心特性
|
|
100
77
|
|
|
101
|
-
###
|
|
78
|
+
### Ink 全屏 TUI (v0.9)
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
全屏终端界面,左侧 LOGO + 右侧欢迎框,消息折叠、工具调用折叠/展开、Ctrl+C 中断、消息队列排队执行。
|
|
104
81
|
|
|
105
|
-
###
|
|
82
|
+
### 双级记忆系统 (v0.9)
|
|
106
83
|
|
|
107
|
-
|
|
84
|
+
- **mem_rom**:永久记忆,落盘 `~/.clinn/mem/persistent-memory.json`,重启不丢
|
|
85
|
+
- **mem_ram**:会话记忆,仅本次窗口,关闭即丢
|
|
86
|
+
- 搜索同时覆盖 ROM + RAM,带 `[ROM]`/`[RAM]` 标签
|
|
108
87
|
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
AI 可以在对话中生成工具代码,通过 `save_tool` 持久化到 `Tools/custom/` 目录,下次启动自动加载。
|
|
112
|
-
|
|
113
|
-
### 上下文实时监控
|
|
114
|
-
|
|
115
|
-
- 90%:黄色提示 "建议 /clear 清除对话"
|
|
116
|
-
- 95%:二次警告 "请尽快 /clear"
|
|
117
|
-
- 不会再自动压缩上下文——由用户自己决定何时 `/clear`
|
|
88
|
+
### Session 回顾与全局搜索 (v0.9)
|
|
118
89
|
|
|
119
|
-
|
|
90
|
+
- 每次 `/reset` 或重启创建新 session
|
|
91
|
+
- `/sessions` 列出所有历史会话
|
|
92
|
+
- `/session_search` 全文搜索全部历史
|
|
120
93
|
|
|
121
|
-
|
|
94
|
+
### 虚拟浏览器
|
|
122
95
|
|
|
123
|
-
|
|
96
|
+
`browse_page` 基于 puppeteer-core + Chrome Headless,可绕过大部分反爬机制。
|
|
124
97
|
|
|
125
|
-
|
|
98
|
+
### 自定义工具持久化
|
|
126
99
|
|
|
127
|
-
|
|
128
|
-
Clinn/
|
|
129
|
-
├── Src/
|
|
130
|
-
│ ├── index.js # CLI 入口、UI 渲染、斜杠命令
|
|
131
|
-
│ ├── agent.js # Agent 主逻辑、工具循环、记忆管理
|
|
132
|
-
│ └── llm.js # DeepSeek API 客户端、流式解析
|
|
133
|
-
├── Tools/
|
|
134
|
-
│ ├── index.js # 工具注册、权限、自定义工具加载
|
|
135
|
-
│ ├── file_tools.js # 文件读写操作
|
|
136
|
-
│ ├── edit_tools.js # search_replace、edit_lines
|
|
137
|
-
│ ├── search_tools.js # grep、find_files、web_fetch
|
|
138
|
-
│ ├── extended_tools.js # web_search (Bing)、exec_console、todo_write 等
|
|
139
|
-
│ ├── browser.js # 虚拟浏览器 (puppeteer-core)
|
|
140
|
-
│ ├── tokenizer.js # Token 估算
|
|
141
|
-
│ └── custom/ # 用户自定义工具持久化目录
|
|
142
|
-
├── Mem/
|
|
143
|
-
│ ├── index.js # ConversationMemory
|
|
144
|
-
│ ├── history.js # 历史文件读写、搜索
|
|
145
|
-
│ └── history-*.json # 按日期存储的对话历史
|
|
146
|
-
├── Logos/
|
|
147
|
-
│ └── StartLogo.txt # 启动 Logo
|
|
148
|
-
├── config.json # 配置文件
|
|
149
|
-
├── install.sh # macOS/Linux 安装脚本
|
|
150
|
-
├── install.bat # Windows 安装脚本
|
|
151
|
-
├── clinn.bat # Windows 快捷启动
|
|
152
|
-
├── package.json
|
|
153
|
-
└── public/ # 发布版本
|
|
154
|
-
└── clinn-v0.7.1/
|
|
155
|
-
```
|
|
100
|
+
AI 在对话中生成工具代码,通过 `save_tool` 持久化到 `~/.clinn/Tools/custom/`,下次启动自动加载。
|
|
156
101
|
|
|
157
102
|
---
|
|
158
103
|
|
|
159
104
|
## 配置
|
|
160
105
|
|
|
161
|
-
编辑
|
|
106
|
+
编辑 `~/.clinn/config.json` 或对话内 `/api` 命令:
|
|
162
107
|
|
|
163
108
|
```json
|
|
164
109
|
{
|
|
165
110
|
"llm": {
|
|
166
|
-
"
|
|
167
|
-
"apiKey": "YOUR_DEEPSEEK_API_KEY_HERE",
|
|
111
|
+
"apiKey": "YOUR_KEY",
|
|
168
112
|
"baseURL": "https://api.deepseek.com/v1",
|
|
169
113
|
"model": "deepseek-v4-pro",
|
|
170
|
-
"
|
|
114
|
+
"contextWindow": 131072,
|
|
115
|
+
"maxTokens": 16384,
|
|
171
116
|
"temperature": 0.7
|
|
172
|
-
},
|
|
173
|
-
"memory": {
|
|
174
|
-
"maxHistory": 100,
|
|
175
|
-
"maxEntries": 800
|
|
176
|
-
},
|
|
177
|
-
"ui": {
|
|
178
|
-
"showLogo": true,
|
|
179
|
-
"dividerChar": "-"
|
|
180
117
|
}
|
|
181
118
|
}
|
|
182
119
|
```
|
|
183
120
|
|
|
184
121
|
---
|
|
185
122
|
|
|
186
|
-
## 更新日志
|
|
187
|
-
|
|
188
|
-
### v0.7.9 — 版本查询
|
|
189
|
-
|
|
190
|
-
- **`--version` / `-v`**:终端直接 `clinn --version` 查看版本
|
|
191
|
-
- **`/version`**:对话内 `/version` 查看版本
|
|
192
|
-
|
|
193
|
-
### v0.7.8 — Windows 兼容 & 权限修复
|
|
123
|
+
## 更新日志
|
|
194
124
|
|
|
195
|
-
|
|
196
|
-
- **修复双重权限弹窗**:危险工具只弹一次 Y/N 确认
|
|
125
|
+
### v0.9.0 — Ink TUI · 双级记忆 · Session 搜索
|
|
197
126
|
|
|
198
|
-
|
|
127
|
+
- **Ink 7 全屏 TUI**:React 19 + Ink 7.0.5,左右分栏启动页,消息折叠,工具调用折叠/展开,颜色标记(红绿黄)
|
|
128
|
+
- **双级记忆**:mem_rom(永久落盘)+ mem_ram(会话级),重启不丢
|
|
129
|
+
- **Session 系统**:会话自动分组,全局全文搜索,历史回顾
|
|
130
|
+
- **消息队列**:AI 忙时消息自动排队,可视队列
|
|
131
|
+
- **Ctrl+C 中断**:不退出进程,中断执行或清空输入
|
|
132
|
+
- **输入框字符计数**:长文本自动缩略显示末尾
|
|
133
|
+
- **grep 升级**:ripgrep 优先,支持上下文行/files_only/count
|
|
134
|
+
- **分隔线终端宽度对齐**,不再刷屏
|
|
199
135
|
|
|
200
|
-
|
|
201
|
-
- `/api` — 查看当前 API 设置(Key 脱敏显示)
|
|
202
|
-
- `/api key <KEY>` — 直接设置 API Key
|
|
203
|
-
- `/api url <URL>` — 切换 API 地址(兼容其他兼容接口)
|
|
204
|
-
- `/api model <MODEL>` — 切换模型
|
|
205
|
-
- **npm 发布**:`npm install -g @ghenya/clinn` 全球安装,配置自动存 `~/.clinn/`
|
|
206
|
-
- **历史/Mem/自定义工具路径统一到 `~/.clinn/`**
|
|
136
|
+
### v0.8.0 — 上下文修复 · 语法校验 · 颜文字
|
|
207
137
|
|
|
208
|
-
|
|
138
|
+
- 上下文窗口 6K→105K (×17),模型感知检测
|
|
139
|
+
- Token 估算修正 (CJK ×0.8 + ASCII ×0.3)
|
|
140
|
+
- 写文件后自动 node/py/json 语法校验
|
|
141
|
+
- 动态颜文字引擎:关键词匹配,不再随机
|
|
142
|
+
- Glob 纯 Node.js 跨平台,模板引擎 + 5 技能
|
|
209
143
|
|
|
210
|
-
|
|
211
|
-
- **工具执行静默化**:不再刷屏动画,只显示 `✓ tool_name` 完成标记
|
|
212
|
-
- **思考动画**:braille spinner `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` 120ms 旋转指示推理状态
|
|
213
|
-
- **修复死循环误判**:循环检测改为工具名+参数逐字比对,只有 300 次完全相同才拦截
|
|
214
|
-
- **修复上下文百分比漂移**:统一用持久记忆计算,排除临时网页正文干扰
|
|
215
|
-
- **全量对话历史**:`history-*.json` 保存完整工具调用链路
|
|
216
|
-
- **web_search 切换 Bing**:cn.bing.com HTML 解析,国内可用
|
|
217
|
-
- **虚拟浏览器**:browse_page 基于 puppeteer-core,反反爬
|
|
144
|
+
### v0.7.0 — 交互重构 · npm 发布
|
|
218
145
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
-
|
|
222
|
-
- `/history` 命令 + less 分页器浏览
|
|
223
|
-
- 历史搜索功能
|
|
224
|
-
- 表格结构转纯文本输出
|
|
225
|
-
- 宁缺毋滥行宽控制
|
|
226
|
-
|
|
227
|
-
### v0.5.0 — 首发
|
|
228
|
-
|
|
229
|
-
- DeepSeek API 集成
|
|
230
|
-
- 50+ 工具体系
|
|
231
|
-
- 工具权限控制
|
|
232
|
-
- 自定义工具持久化
|
|
233
|
-
- install.sh / install.bat 一键安装
|
|
234
|
-
- 流式响应输出
|
|
146
|
+
- npm 发布:`npm install -g @ghenya/clinn`
|
|
147
|
+
- `/api` 命令链式配置,上下文三级预警
|
|
148
|
+
- web_search 切换 Bing,虚拟浏览器
|
|
235
149
|
|
|
236
150
|
---
|
|
237
151
|
|
|
238
|
-
## 许可
|
|
239
|
-
|
|
240
152
|
MIT License
|
package/Src/agent.cjs
CHANGED
|
@@ -2,7 +2,12 @@ const os = require("os");
|
|
|
2
2
|
const LLMClient = require("./llm.cjs");
|
|
3
3
|
const Tools = require("../Tools");
|
|
4
4
|
const { ConversationMemory } = require("../Mem");
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
saveTurn, searchHistory, getFileList, loadFileTurns,
|
|
7
|
+
startSession, endSession, listSessions, getSession,
|
|
8
|
+
loadSessionTurns, searchSessions, globalSearch,
|
|
9
|
+
migrateLegacySessions,
|
|
10
|
+
} = require("../Mem/history");
|
|
6
11
|
|
|
7
12
|
const MAX_ITERATIONS = 500;
|
|
8
13
|
const MAX_TOOL_RESULT_CHARS = 3000;
|
|
@@ -84,6 +89,10 @@ class Agent {
|
|
|
84
89
|
this.maxContextTokens = (config.llm?.contextWindow || getContextWindow(config.llm?.model)) * 0.8;
|
|
85
90
|
this._lastToolCalls = [];
|
|
86
91
|
|
|
92
|
+
// session management
|
|
93
|
+
migrateLegacySessions();
|
|
94
|
+
this.sessionId = startSession();
|
|
95
|
+
|
|
87
96
|
Tools.setTrusted(config.tools?.trustedTools || []);
|
|
88
97
|
Tools.setPermissionCallback(async (name, args) => {
|
|
89
98
|
if (callbacks.onPermission) return callbacks.onPermission(name, args);
|
|
@@ -98,10 +107,12 @@ class Agent {
|
|
|
98
107
|
const self = this;
|
|
99
108
|
const memTools = [
|
|
100
109
|
"search_memory", "save_memory", "list_memory", "delete_memory",
|
|
110
|
+
"mem_rom", "mem_ram",
|
|
101
111
|
"compress_context", "agent_self_invoke", "set_timer",
|
|
102
112
|
"save_tool", "delete_tool_file", "list_saved_tools",
|
|
103
113
|
"forget_conversation", "restart_session",
|
|
104
114
|
"search_history", "list_history_files",
|
|
115
|
+
"list_sessions", "view_session", "search_sessions",
|
|
105
116
|
];
|
|
106
117
|
for (const name of memTools) {
|
|
107
118
|
const tool = Tools.getTool(name);
|
|
@@ -114,10 +125,16 @@ class Agent {
|
|
|
114
125
|
switch (name) {
|
|
115
126
|
case "search_memory":
|
|
116
127
|
return this._fmtEntries(this.memory.searchEntries(args.query, args.limit || 5));
|
|
117
|
-
case "save_memory":
|
|
128
|
+
case "save_memory":
|
|
129
|
+
case "mem_rom": {
|
|
130
|
+
const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
|
|
131
|
+
const entry = this.memory.addRomEntry(args.content, tags);
|
|
132
|
+
return entry ? `[ROM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
|
|
133
|
+
}
|
|
134
|
+
case "mem_ram": {
|
|
118
135
|
const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
|
|
119
|
-
const entry = this.memory.
|
|
120
|
-
return entry ? `[
|
|
136
|
+
const entry = this.memory.addRamEntry(args.content, tags);
|
|
137
|
+
return entry ? `[RAM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
|
|
121
138
|
}
|
|
122
139
|
case "list_memory": {
|
|
123
140
|
const all = this.memory.getAllEntries().slice(-(args.limit || 20));
|
|
@@ -131,7 +148,7 @@ class Agent {
|
|
|
131
148
|
const compressed = this.memory.compressHistory();
|
|
132
149
|
if (!compressed) return "[跳过] 对话太短";
|
|
133
150
|
const summary = await this._summarize(compressed);
|
|
134
|
-
this.memory.
|
|
151
|
+
this.memory.addRamEntry(summary, ["auto-summary"]);
|
|
135
152
|
this.memory.clear();
|
|
136
153
|
return `[OK] 上下文已压缩, 摘要存入记忆: ${summary}`;
|
|
137
154
|
}
|
|
@@ -174,16 +191,18 @@ class Agent {
|
|
|
174
191
|
if (compressed) summary = await this._summarize(compressed);
|
|
175
192
|
}
|
|
176
193
|
this.memory.clear();
|
|
177
|
-
if (summary) { this.memory.
|
|
194
|
+
if (summary) { this.memory.addRamEntry(summary, ["auto-summary"]); }
|
|
178
195
|
return summary
|
|
179
196
|
? `[OK] 对话已遗忘, 摘要已保存: ${summary}`
|
|
180
197
|
: "[OK] 对话历史已清空, 像全新对话一样";
|
|
181
198
|
}
|
|
182
199
|
case "restart_session": {
|
|
200
|
+
endSession(this.sessionId);
|
|
183
201
|
this.memory.clear();
|
|
184
|
-
this.memory.
|
|
202
|
+
this.memory.clearRamEntries();
|
|
185
203
|
this.llm.resetUsage();
|
|
186
|
-
|
|
204
|
+
this.sessionId = startSession();
|
|
205
|
+
return "[OK] 会话已完全重启: 历史+记忆+token计数均已重置, 新session已创建. 可以开始全新任务.";
|
|
187
206
|
}
|
|
188
207
|
case "search_history": {
|
|
189
208
|
const q = args.query || "";
|
|
@@ -199,6 +218,47 @@ class Agent {
|
|
|
199
218
|
if (files.length === 0) return "(暂无历史对话文件)";
|
|
200
219
|
return files.map((f) => `${f.file} | ${f.turns}轮 | ${f.size}KB | ${f.created.slice(0, 10)}`).join("\n");
|
|
201
220
|
}
|
|
221
|
+
case "list_sessions": {
|
|
222
|
+
const sessions = listSessions(args.limit || 20);
|
|
223
|
+
if (sessions.length === 0) return "(暂无对话 session)";
|
|
224
|
+
const now = this.sessionId;
|
|
225
|
+
return sessions.map((s, i) => {
|
|
226
|
+
const marker = s.id === now ? " ◀ 当前" : "";
|
|
227
|
+
const status = s.status === "active" ? "●" : "○";
|
|
228
|
+
const date = s.started ? s.started.slice(0, 16) : "?";
|
|
229
|
+
return `${i + 1}. ${status} [${date}] ${s.title || "(无标题)"} | ${s.turnCount}轮${marker}`;
|
|
230
|
+
}).join("\n");
|
|
231
|
+
}
|
|
232
|
+
case "view_session": {
|
|
233
|
+
const sid = args.session_id || "";
|
|
234
|
+
if (!sid) return "[错误] 请提供 session_id (用 list_sessions 获取)";
|
|
235
|
+
const session = getSession(sid);
|
|
236
|
+
if (!session) return `[未找到] session ${sid}`;
|
|
237
|
+
const turns = loadSessionTurns(sid, args.limit || 50);
|
|
238
|
+
if (turns.length === 0) return `[空] session ${sid} 中没有对话轮次`;
|
|
239
|
+
const header = [
|
|
240
|
+
`=== Session: ${session.title || "(无标题)"} ===`,
|
|
241
|
+
`ID: ${session.id} | 开始: ${session.started?.slice(0, 16) || "?"} | 共 ${session.turnCount} 轮`,
|
|
242
|
+
``,
|
|
243
|
+
];
|
|
244
|
+
const body = turns.map((t, i) => {
|
|
245
|
+
return `[${i + 1}] ${t.time?.slice(0, 16) || "?"}\n` +
|
|
246
|
+
` > 用户: ${t.user ? t.user.slice(0, 200) : ""}\n` +
|
|
247
|
+
` < 回复: ${t.assistant ? t.assistant.slice(0, 300) : ""}`;
|
|
248
|
+
});
|
|
249
|
+
return header.join("\n") + "\n" + body.join("\n\n");
|
|
250
|
+
}
|
|
251
|
+
case "search_sessions": {
|
|
252
|
+
const q = args.query || "";
|
|
253
|
+
if (!q.trim()) return "[错误] 请提供搜索关键词";
|
|
254
|
+
const results = globalSearch(q, args.limit || 10);
|
|
255
|
+
if (results.length === 0) return `[无匹配] 在所有历史 session 中未找到 "${q}"`;
|
|
256
|
+
return results.map((r, i) => {
|
|
257
|
+
const date = r.sessionStarted ? r.sessionStarted.slice(0, 16) : "?";
|
|
258
|
+
const previews = r.topMatches.map((m) => ` · ${m.user.slice(0, 80)}`).join("\n");
|
|
259
|
+
return `${i + 1}. [${date}] ${r.sessionTitle} | 匹配 ${r.matchCount} 处 | 得分 ${r.totalScore}\n${previews}`;
|
|
260
|
+
}).join("\n\n");
|
|
261
|
+
}
|
|
202
262
|
default:
|
|
203
263
|
return `[未知内部工具] ${name}`;
|
|
204
264
|
}
|
|
@@ -219,7 +279,10 @@ class Agent {
|
|
|
219
279
|
|
|
220
280
|
_fmtEntries(entries) {
|
|
221
281
|
if (!entries || entries.length === 0) return "(无记忆条目)";
|
|
222
|
-
return entries.map((e) =>
|
|
282
|
+
return entries.map((e) => {
|
|
283
|
+
const tag = (e._type === "ram") ? "[RAM]" : "[ROM]";
|
|
284
|
+
return `${tag} #${e.id} [${e.tags?.join(",") || "-"}] ${e.text}`;
|
|
285
|
+
}).join("\n");
|
|
223
286
|
}
|
|
224
287
|
|
|
225
288
|
refreshTools() {
|
|
@@ -326,7 +389,7 @@ class Agent {
|
|
|
326
389
|
this.memory.addAssistant(finalResponse);
|
|
327
390
|
|
|
328
391
|
try {
|
|
329
|
-
saveTurn(userMessage, finalResponse, process.cwd(), allMsgs);
|
|
392
|
+
saveTurn(userMessage, finalResponse, process.cwd(), allMsgs, this.sessionId);
|
|
330
393
|
} catch (_) {}
|
|
331
394
|
|
|
332
395
|
if (opts._noAutoSave) return finalResponse;
|
|
@@ -341,7 +404,7 @@ class Agent {
|
|
|
341
404
|
];
|
|
342
405
|
const res = await this.llm.chat(msgs);
|
|
343
406
|
const habit = res.choices?.[0]?.message?.content?.trim().slice(0, 100);
|
|
344
|
-
if (habit) this.memory.
|
|
407
|
+
if (habit) this.memory.addRamEntry(habit, ["habit"]);
|
|
345
408
|
}
|
|
346
409
|
} catch (_) {}
|
|
347
410
|
});
|
package/Src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const readline = require("readline");
|
|
|
4
4
|
const { spawn } = require("child_process");
|
|
5
5
|
const Agent = require("./agent");
|
|
6
6
|
const Tools = require("../Tools");
|
|
7
|
-
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
7
|
+
const { listRecentTurns, searchHistory, getFileList, loadFileTurns, listSessions, getSession, loadSessionTurns, globalSearch, endSession, startSession } = require("../Mem/history");
|
|
8
8
|
|
|
9
9
|
const C = {
|
|
10
10
|
reset: "\x1b[0m",
|
|
@@ -345,6 +345,8 @@ function showHelp() {
|
|
|
345
345
|
["/memory_clear", "清空所有记忆"], ["/compress", "手动压缩上下文"],
|
|
346
346
|
["/history [n]", "查看最近n条历史对话"], ["/history files", "查看历史文件列表"],
|
|
347
347
|
["/history search <q>", "搜索历史对话"], ["/history read <f>", "读取文件含完整工具调用"],
|
|
348
|
+
["/sessions [n]", "列出对话session"], ["/session <id>", "查看某个session"],
|
|
349
|
+
["/session_search <q>", "全局搜索session"],
|
|
348
350
|
["/tool_save <name>", "持久化保存工具"], ["/tool_list_saved", "列出持久化工具"],
|
|
349
351
|
["/tool_del_saved <name>", "删除持久化工具"],
|
|
350
352
|
["/trusted", "查看受信任工具"], ["/trust <name>", "永久信任工具"],
|
|
@@ -369,7 +371,7 @@ function showStatus() {
|
|
|
369
371
|
console.log(` 模型: ${C.bold}${config.llm.model}${C.reset} 温度: ${config.llm.temperature}`);
|
|
370
372
|
console.log(` Tokens 累计: ${emoji("tokenIn")}${C.cyan}${usage.prompt}${C.reset} ${emoji("tokenOut")}${C.magenta}${usage.completion}${C.reset}`);
|
|
371
373
|
console.log(` 上下文使用: ${ctxBar(ctxPct)}`);
|
|
372
|
-
console.log(` 记忆: ${mem.entries || 0}
|
|
374
|
+
console.log(` 记忆: ROM${mem.romEntries || mem.entries || 0} RAM${mem.ramEntries || 0} · 历史 ${mem.historyMessages || 0} 条`);
|
|
373
375
|
const tools = Tools.listToolNames();
|
|
374
376
|
console.log(` 工具: ${tools.length} 个 — ${tools.slice(0, 8).join(", ")}${tools.length > 8 ? " ..." : ""}`);
|
|
375
377
|
console.log(div("="));
|
|
@@ -436,8 +438,10 @@ async function handleSlashCommand(input) {
|
|
|
436
438
|
console.log(C.dim + `再见~ ${emoji("done")}` + C.reset);
|
|
437
439
|
process.exit(0);
|
|
438
440
|
case "reset":
|
|
441
|
+
endSession(agent.sessionId);
|
|
439
442
|
agent.reset();
|
|
440
|
-
|
|
443
|
+
agent.sessionId = startSession();
|
|
444
|
+
console.log(`${C.green}对话已重置, 新 session 已创建${C.reset}`);
|
|
441
445
|
break;
|
|
442
446
|
case "tools": {
|
|
443
447
|
const names = Tools.listToolNames();
|
|
@@ -563,7 +567,7 @@ async function handleSlashCommand(input) {
|
|
|
563
567
|
case "memory": {
|
|
564
568
|
const s = agent.memory.stats();
|
|
565
569
|
console.log(div());
|
|
566
|
-
console.log(` 条目: ${s.entries}
|
|
570
|
+
console.log(` 条目: ROM${s.romEntries || s.entries} RAM${s.ramEntries || 0} 历史消息: ${s.historyMessages}`);
|
|
567
571
|
console.log(div());
|
|
568
572
|
break;
|
|
569
573
|
}
|
|
@@ -572,7 +576,10 @@ async function handleSlashCommand(input) {
|
|
|
572
576
|
const entries = agent.memory.getAllEntries().slice(-n);
|
|
573
577
|
if (entries.length === 0) { console.log("(无记忆条目)"); break; }
|
|
574
578
|
console.log(div());
|
|
575
|
-
for (const e of entries)
|
|
579
|
+
for (const e of entries) {
|
|
580
|
+
const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
|
|
581
|
+
console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${C.dim}[${e.tags?.join(",") || "-"}]${C.reset} ${e.text}`);
|
|
582
|
+
}
|
|
576
583
|
console.log(div());
|
|
577
584
|
break;
|
|
578
585
|
}
|
|
@@ -581,7 +588,10 @@ async function handleSlashCommand(input) {
|
|
|
581
588
|
const results = agent.memory.searchEntries(rest, 10);
|
|
582
589
|
if (results.length === 0) { console.log(`无匹配: ${rest}`); break; }
|
|
583
590
|
console.log(div());
|
|
584
|
-
for (const e of results)
|
|
591
|
+
for (const e of results) {
|
|
592
|
+
const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
|
|
593
|
+
console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${e.text}`);
|
|
594
|
+
}
|
|
585
595
|
console.log(div());
|
|
586
596
|
break;
|
|
587
597
|
}
|
|
@@ -593,8 +603,8 @@ async function handleSlashCommand(input) {
|
|
|
593
603
|
break;
|
|
594
604
|
}
|
|
595
605
|
case "memory_clear":
|
|
596
|
-
agent.memory.
|
|
597
|
-
console.log(`${C.green}
|
|
606
|
+
agent.memory.clearAllEntries();
|
|
607
|
+
console.log(`${C.green}所有记忆条目已清空 (ROM+RAM)${C.reset}`);
|
|
598
608
|
break;
|
|
599
609
|
case "history": {
|
|
600
610
|
const subCmd = rest ? rest.split(/\s+/)[0] : "";
|
|
@@ -662,6 +672,68 @@ async function handleSlashCommand(input) {
|
|
|
662
672
|
await pipeToPager(wrapped);
|
|
663
673
|
break;
|
|
664
674
|
}
|
|
675
|
+
case "sessions": {
|
|
676
|
+
const limit = rest ? parseInt(rest, 10) || 20 : 20;
|
|
677
|
+
const sessions = listSessions(limit);
|
|
678
|
+
if (sessions.length === 0) { console.log("(暂无对话 session)"); break; }
|
|
679
|
+
let out = `${C.bold + C.cyan}对话 Sessions${C.reset} (${sessions.length})\n` + div() + "\n";
|
|
680
|
+
for (const [i, s] of sessions.entries()) {
|
|
681
|
+
const marker = s.status === "active" ? `${C.green}●${C.reset}` : `${C.dim}○${C.reset}`;
|
|
682
|
+
const active = s.status === "active" ? ` ${C.green}◀ 当前${C.reset}` : "";
|
|
683
|
+
out += ` ${String(i + 1).padStart(2)}. ${marker} [${(s.started || "?").slice(0, 16)}] ${s.title || "(无标题)"} | ${s.turnCount}轮${active}\n`;
|
|
684
|
+
}
|
|
685
|
+
out += "\n" + div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情, ${C.yellow}/session_search <关键词>${C.reset} 搜索`;
|
|
686
|
+
console.log(out);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "session": {
|
|
690
|
+
if (!rest) { console.log(`用法: /session <session_id 或列表序号>\n请先用 /sessions 查看列表`); break; }
|
|
691
|
+
const sessions = listSessions(999);
|
|
692
|
+
let sid = rest.trim();
|
|
693
|
+
if (/^\d+$/.test(sid)) {
|
|
694
|
+
const idx = parseInt(sid) - 1;
|
|
695
|
+
if (idx < 0 || idx >= sessions.length) { console.log(`序号超出范围 (1-${sessions.length})`); break; }
|
|
696
|
+
sid = sessions[idx].id;
|
|
697
|
+
}
|
|
698
|
+
const session = getSession(sid);
|
|
699
|
+
if (!session) { console.log(`未找到 session: ${sid}`); break; }
|
|
700
|
+
const turns = loadSessionTurns(sid, 30);
|
|
701
|
+
let out = `${C.bold + C.cyan}=== ${session.title || "(无标题)"} ===${C.reset}\n`;
|
|
702
|
+
out += `ID: ${C.dim}${session.id}${C.reset} | ${(session.started || "?").slice(0, 16)} | ${session.turnCount}轮\n` + div() + "\n";
|
|
703
|
+
if (turns.length === 0) { out += "(这个 session 暂无对话轮次)\n"; }
|
|
704
|
+
else {
|
|
705
|
+
for (const [i, t] of turns.entries()) {
|
|
706
|
+
out += `${C.yellow}[${i + 1}] ${(t.time || "?").slice(0, 16)}${C.reset}\n`;
|
|
707
|
+
if (t.cwd) out += ` ${C.dim}${t.cwd}${C.reset}\n`;
|
|
708
|
+
out += ` ${C.cyan}▸${C.reset} ${(t.user || "").slice(0, 400)}\n`;
|
|
709
|
+
out += ` ${C.green}◂${C.reset} ${(t.assistant || "").slice(0, 400)}\n\n`;
|
|
710
|
+
}
|
|
711
|
+
out += `(共 ${turns.length} 轮, 显示最近 30 轮)`;
|
|
712
|
+
}
|
|
713
|
+
out += "\n" + div();
|
|
714
|
+
await pipeToPager(out);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "session_search": {
|
|
718
|
+
if (!rest) { console.log("用法: /session_search <关键词> [数量]\n在整个历史中搜索相关 session"); break; }
|
|
719
|
+
const parts = rest.trim().split(/\s+/);
|
|
720
|
+
const maybeNum = parseInt(parts[parts.length - 1], 10);
|
|
721
|
+
const limit = !isNaN(maybeNum) ? maybeNum : 10;
|
|
722
|
+
const query = !isNaN(maybeNum) ? parts.slice(0, -1).join(" ") : rest.trim();
|
|
723
|
+
const results = globalSearch(query, limit);
|
|
724
|
+
if (results.length === 0) { console.log(`全史搜索未找到 "${query}"`); break; }
|
|
725
|
+
let out = `${C.bold + C.yellow}搜索: "${query}"${C.reset} — ${results.length} 个 session\n` + div() + "\n";
|
|
726
|
+
for (const [i, r] of results.entries()) {
|
|
727
|
+
out += `${C.yellow}${i + 1}.${C.reset} [${(r.sessionStarted || "?").slice(0, 16)}] ${r.sessionTitle} | ${r.matchCount}处 | 得分${r.totalScore}\n`;
|
|
728
|
+
for (const m of r.topMatches) {
|
|
729
|
+
out += ` ${C.dim}·${C.reset} ${m.user.slice(0, 100)}\n`;
|
|
730
|
+
}
|
|
731
|
+
out += "\n";
|
|
732
|
+
}
|
|
733
|
+
out += div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情`;
|
|
734
|
+
console.log(out);
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
665
737
|
case "compress": {
|
|
666
738
|
console.log(`${C.yellow}正在压缩上下文...${C.reset}`);
|
|
667
739
|
const result = await agent._handleAgentTool("compress_context", {});
|
|
@@ -963,6 +1035,7 @@ async function main() {
|
|
|
963
1035
|
`${C.cyan}tools${C.reset}`, `${C.cyan}status${C.reset}`, `${C.cyan}ctx${C.reset}`,
|
|
964
1036
|
`${C.cyan}temp${C.reset}`, `${C.cyan}token${C.reset}`,
|
|
965
1037
|
`${C.cyan}compress${C.reset}`, `${C.cyan}memory${C.reset}`, `${C.cyan}history${C.reset}`,
|
|
1038
|
+
`${C.cyan}sessions${C.reset}`, `${C.cyan}session_search${C.reset}`,
|
|
966
1039
|
`${C.cyan}tool_save${C.reset}`, `${C.cyan}tool_del${C.reset}`,
|
|
967
1040
|
];
|
|
968
1041
|
process.stdout.write("\n" + C.dim + menu.join(" ") + C.reset + "\n");
|