@jpssff/vanor 0.1.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-cn.md +166 -0
- package/README.md +120 -0
- package/base/config.js +162 -0
- package/base/core/compaction.js +58 -0
- package/base/core/harness.js +246 -0
- package/base/core/loop.js +72 -0
- package/base/core/prompt.js +126 -0
- package/base/core/session.js +255 -0
- package/base/events.js +54 -0
- package/base/i18n/index.js +80 -0
- package/base/i18n/locales/en.js +254 -0
- package/base/i18n/locales/zh-CN.js +252 -0
- package/base/llm/index.js +119 -0
- package/base/llm/providers/anthropic.js +147 -0
- package/base/llm/providers/openai.js +155 -0
- package/base/llm/sse.js +27 -0
- package/base/llm/trace.js +64 -0
- package/base/logger.js +57 -0
- package/base/memory/index.js +139 -0
- package/base/security/index.js +77 -0
- package/base/skills/loader.js +297 -0
- package/base/test/cli.test.js +91 -0
- package/base/test/config.test.js +63 -0
- package/base/test/core.test.js +154 -0
- package/base/test/i18n.test.js +32 -0
- package/base/test/loop.test.js +97 -0
- package/base/test/memory.test.js +47 -0
- package/base/test/message.test.js +38 -0
- package/base/test/session.test.js +324 -0
- package/base/test/skills.test.js +236 -0
- package/base/test/statusbar.test.js +143 -0
- package/base/test/tools.test.js +127 -0
- package/base/test/trace.test.js +62 -0
- package/base/test/tui.test.js +242 -0
- package/base/test/utils.test.js +35 -0
- package/base/tools/builtin.js +221 -0
- package/base/tools/index.js +157 -0
- package/base/transport/cli.js +417 -0
- package/base/transport/message.js +81 -0
- package/base/transport/statusbar.js +117 -0
- package/base/transport/tui.js +397 -0
- package/base/utils.js +150 -0
- package/docs/TECH_DESIGN.md +544 -0
- package/index.js +175 -0
- package/package.json +33 -0
package/README-cn.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Vanor(万佑)
|
|
2
|
+
|
|
3
|
+
万佑智算通用 CLI 智能体。Vanor 由大模型驱动,可在本地工作区内理解任务、规划步骤、读写文件、执行命令,并通过会话、记忆、技能和安全审批机制持续协助开发者工作。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @jpssff/vanor
|
|
9
|
+
vanor config
|
|
10
|
+
vanor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
要求 Node.js `>=18`。当前版本保持零第三方运行时依赖,仅使用 Node.js 原生模块。
|
|
14
|
+
|
|
15
|
+
配置文件位于 `~/.vanor/config.json`。`apiKey` 推荐使用 `env:VAR_NAME` 引用环境变量,避免明文写入配置:
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
{
|
|
19
|
+
"llm": {
|
|
20
|
+
"defaultModel": "wanyou/deepseek/deepseek-v4-pro",
|
|
21
|
+
"providers": {
|
|
22
|
+
"wanyou": {
|
|
23
|
+
"type": "openai",
|
|
24
|
+
"baseURL": "https://wanyouzhisuan.com/api/v1",
|
|
25
|
+
"apiKey": "env:VANOR_API_KEY",
|
|
26
|
+
"models": ["deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"fallback": []
|
|
30
|
+
},
|
|
31
|
+
"security": {
|
|
32
|
+
"approval": "ask",
|
|
33
|
+
"workspaceRoot": "."
|
|
34
|
+
},
|
|
35
|
+
"ui": {
|
|
36
|
+
"language": "auto"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`ui.language` 支持 `auto`、`en`、`zh-CN`。`auto` 会根据操作系统 / 终端语言自动切换,无法确认时默认英文;交互中可用 `/language` 查看或切换语言。
|
|
42
|
+
|
|
43
|
+
## 核心能力
|
|
44
|
+
|
|
45
|
+
- **交互式 CLI**:彩色流式输出、固定输入行、底部状态栏、上下文用量、模型、工作区和运行状态动画。
|
|
46
|
+
- **Agent Loop**:LLM ↔ 工具迭代,支持文件读写、命令执行、记忆和技能工具。
|
|
47
|
+
- **安全执行**:默认危险操作需确认;支持 allowlist / denylist;`/auto-run` 只在当前会话临时减少确认,denylist 仍生效。
|
|
48
|
+
- **会话恢复**:会话以 JSONL 存储;`~/.vanor/state.json` 按 `workspaceRoot` 记录最近 session,切换工作区后可恢复各自上下文。
|
|
49
|
+
- **上下文压缩**:接近上下文窗口时自动压缩;也可用 `/compact` 手动压缩。
|
|
50
|
+
- **配置热重载**:交互过程中修改 `config.json` 后,下一轮自动重载;也可执行 `/reload`。
|
|
51
|
+
- **模型切换**:`/model <关键词>` 会搜索当前模型、默认模型、fallback 和 provider `models` 列表,多个命中时编号选择。
|
|
52
|
+
- **多语言**:自动检测系统语言,支持英文和简体中文;可用 `/language [auto|en|zh-CN]` 即时切换并写回配置。
|
|
53
|
+
- **技能系统**:自动扫描本机和工作区 skills,注入请求上下文;支持读取、创建、修改、删除本地 `SKILL.md`。
|
|
54
|
+
- **可观测日志**:JSONL 事件日志;可选开启完整 LLM 请求 / 响应 trace,并默认脱敏敏感 header。
|
|
55
|
+
|
|
56
|
+
## CLI 命令
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
vanor # 交互对话
|
|
60
|
+
vanor config # 配置 LLM provider 与默认模型
|
|
61
|
+
vanor doctor # 检查配置
|
|
62
|
+
vanor sessions # 查看历史会话
|
|
63
|
+
vanor resume [id] # 恢复指定或最近会话
|
|
64
|
+
vanor skills # 列出已加载技能
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
交互内 slash 命令:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
/help 显示帮助
|
|
71
|
+
/new 开启新会话
|
|
72
|
+
/messages 查看当前会话的历史用户消息
|
|
73
|
+
/compact 立即压缩上下文
|
|
74
|
+
/retry 重发上一条消息
|
|
75
|
+
/model [关键词] 查看或搜索切换模型
|
|
76
|
+
/language [语言] 查看或切换语言(auto | en | zh-CN)
|
|
77
|
+
/usage 查看本会话 token 用量
|
|
78
|
+
/skills 列出已加载技能
|
|
79
|
+
/reload 立即重载配置文件
|
|
80
|
+
/auto-run [off] 本次对话自动执行操作,不再逐次确认(off 恢复)
|
|
81
|
+
/exit 退出
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 目录结构
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
vanor/ # npm 包(只读)
|
|
88
|
+
├── index.js # CLI 入口
|
|
89
|
+
├── base/
|
|
90
|
+
│ ├── core/ # harness、loop、session、prompt、compaction
|
|
91
|
+
│ ├── llm/ # provider 抽象与调用
|
|
92
|
+
│ ├── tools/ # 工具注册、审批、执行
|
|
93
|
+
│ ├── skills/loader.js # 技能加载与管理
|
|
94
|
+
│ ├── memory/ # 长期 / 工作记忆
|
|
95
|
+
│ ├── transport/ # CLI 与统一 Message 协议
|
|
96
|
+
│ ├── security/ # 工作区边界、allowlist、denylist
|
|
97
|
+
│ ├── events.js
|
|
98
|
+
│ ├── logger.js
|
|
99
|
+
│ ├── config.js
|
|
100
|
+
│ └── test/ # node:test 契约测试
|
|
101
|
+
└── docs/
|
|
102
|
+
|
|
103
|
+
~/.vanor/ # 运行时根目录(可写)
|
|
104
|
+
├── config.json # 配置
|
|
105
|
+
├── state.json # 工作区最近 session 索引
|
|
106
|
+
├── skills/ # 用户技能
|
|
107
|
+
├── memory/
|
|
108
|
+
│ ├── MEMORY.md # 长期:环境事实
|
|
109
|
+
│ ├── USER.md # 长期:用户偏好
|
|
110
|
+
│ └── working/ # 工作记忆
|
|
111
|
+
├── sessions/ # 会话 JSONL,每会话一文件
|
|
112
|
+
├── cache/
|
|
113
|
+
└── logs/ # JSONL 日志
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 技能加载
|
|
117
|
+
|
|
118
|
+
Vanor 会从以下位置加载技能,工作区技能优先:
|
|
119
|
+
|
|
120
|
+
- 当前工作区 `.skills/`
|
|
121
|
+
- 当前工作区 `skills/`
|
|
122
|
+
- `~/.vanor/skills`
|
|
123
|
+
- `~/.cursor/skills-cursor`
|
|
124
|
+
- `~/.claude/skills`
|
|
125
|
+
- `~/.agents/skills`
|
|
126
|
+
- `~/.codex/skills`
|
|
127
|
+
- `~/.codex/skills/.archive`
|
|
128
|
+
|
|
129
|
+
每个技能通常是一个包含 frontmatter 的 `SKILL.md`。Vanor 能解析常见 YAML description 写法,包括单行、folded block(`>-`)和 literal block(`|`)。
|
|
130
|
+
|
|
131
|
+
## 记忆与会话
|
|
132
|
+
|
|
133
|
+
Vanor 使用三层记忆:
|
|
134
|
+
|
|
135
|
+
| 层级 | 说明 |
|
|
136
|
+
|------|------|
|
|
137
|
+
| 短期 | 当前会话消息,存于 `sessions/`;压缩后内存只保留摘要与最近消息,但磁盘保留完整历史 |
|
|
138
|
+
| 长期 | `memory/MEMORY.md` 与 `memory/USER.md`,记录环境事实和用户偏好 |
|
|
139
|
+
| 工作 | `memory/working/`,保存任务中的计划和中间状态 |
|
|
140
|
+
|
|
141
|
+
会话恢复优先读取 `state.json` 中当前工作区的最近 session id,避免 session 文件过多时启动扫描变慢。若索引失效,再回退扫描 `sessions/`。
|
|
142
|
+
|
|
143
|
+
## 日志与 LLM Trace
|
|
144
|
+
|
|
145
|
+
默认只记录结构化事件日志。需要排查 provider 请求时,可开启完整 LLM trace:
|
|
146
|
+
|
|
147
|
+
```jsonc
|
|
148
|
+
{
|
|
149
|
+
"logging": {
|
|
150
|
+
"level": "info",
|
|
151
|
+
"llmTrace": true,
|
|
152
|
+
"llmTraceRedact": true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
开启后会记录请求 URL、headers、body、响应内容等详细信息;`llmTraceRedact` 默认开启,会脱敏 `Authorization`、`api-key` 等敏感 header。
|
|
158
|
+
|
|
159
|
+
## 设计原则
|
|
160
|
+
|
|
161
|
+
- 零运行时依赖,优先使用 Node.js 原生能力。
|
|
162
|
+
- 模块职责清晰:Transport / Core / Loop / Tools / Skills / Memory / Security 解耦。
|
|
163
|
+
- 默认安全:工作区边界、危险命令确认、denylist 拦截。
|
|
164
|
+
- 所有关键状态可恢复、可观测、可测试。
|
|
165
|
+
|
|
166
|
+
详细技术设计见 [docs/TECH_DESIGN.md](docs/TECH_DESIGN.md)。
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Vanor
|
|
2
|
+
|
|
3
|
+
Vanor is a zero-dependency CLI agent for developers. It uses your LLM provider to understand tasks, edit files, run commands, keep session history, and work safely inside your local workspace.
|
|
4
|
+
|
|
5
|
+
中文文档:[README-cn.md](README-cn.md)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g @jpssff/vanor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js `>=18`.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Configure your model provider:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
vanor config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Start chatting in your project:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd your-project
|
|
27
|
+
vanor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Vanor stores config and runtime data under `~/.vanor/`.
|
|
31
|
+
|
|
32
|
+
## Manual Config
|
|
33
|
+
|
|
34
|
+
You can edit `~/.vanor/config.json` directly. Use `env:VAR_NAME` for API keys so secrets are not stored in plain text.
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
{
|
|
38
|
+
"llm": {
|
|
39
|
+
"defaultModel": "wanyou/deepseek/deepseek-v4-pro",
|
|
40
|
+
"providers": {
|
|
41
|
+
"wanyou": {
|
|
42
|
+
"type": "openai",
|
|
43
|
+
"baseURL": "https://wanyouzhisuan.com/api/v1",
|
|
44
|
+
"apiKey": "env:VANOR_API_KEY",
|
|
45
|
+
"models": ["deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash"]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"fallback": []
|
|
49
|
+
},
|
|
50
|
+
"security": {
|
|
51
|
+
"approval": "ask",
|
|
52
|
+
"workspaceRoot": "."
|
|
53
|
+
},
|
|
54
|
+
"ui": {
|
|
55
|
+
"language": "auto"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Config changes are hot-reloaded before the next turn. You can also run `/reload` inside Vanor.
|
|
61
|
+
`ui.language` can be `auto`, `en`, or `zh-CN`. In `auto` mode, Vanor detects your OS / terminal language and falls back to English when unsure. You can switch it inside the chat with `/language`.
|
|
62
|
+
|
|
63
|
+
## Common Commands
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
vanor # start interactive chat
|
|
67
|
+
vanor config # configure provider and model
|
|
68
|
+
vanor doctor # check configuration
|
|
69
|
+
vanor sessions # list sessions
|
|
70
|
+
vanor resume [id] # resume a session
|
|
71
|
+
vanor skills # list loaded skills
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Inside the chat:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
/help show help
|
|
78
|
+
/new start a new session
|
|
79
|
+
/messages list user messages in the current session
|
|
80
|
+
/compact compact context now
|
|
81
|
+
/retry retry the last message
|
|
82
|
+
/model [query] show or search and switch models
|
|
83
|
+
/language [lang] show or switch language
|
|
84
|
+
/usage show token usage
|
|
85
|
+
/skills list loaded skills
|
|
86
|
+
/reload reload config
|
|
87
|
+
/auto-run [off] auto-approve actions for this session
|
|
88
|
+
/exit quit
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Highlights
|
|
92
|
+
|
|
93
|
+
- Interactive terminal UI with streaming output, a fixed input line, and a live status bar.
|
|
94
|
+
- Workspace-aware session restore, so each project resumes its own latest session.
|
|
95
|
+
- File and command tools with confirmation, allowlist / denylist, and workspace boundaries.
|
|
96
|
+
- Skills are loaded automatically from `~/.vanor/skills`, common local skill folders, and workspace `.skills/` or `skills/`.
|
|
97
|
+
- Optional full LLM request / response tracing for debugging, with sensitive headers redacted by default.
|
|
98
|
+
|
|
99
|
+
## Safety
|
|
100
|
+
|
|
101
|
+
Vanor asks before potentially risky tool actions by default. `/auto-run` only changes approval behavior for the current session, and denylisted commands are still blocked.
|
|
102
|
+
|
|
103
|
+
## Data Locations
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
~/.vanor/config.json provider, model, security, logging config
|
|
107
|
+
~/.vanor/state.json latest session index per workspace
|
|
108
|
+
~/.vanor/sessions/ JSONL session history
|
|
109
|
+
~/.vanor/memory/ long-term and working memory
|
|
110
|
+
~/.vanor/skills/ user skills
|
|
111
|
+
~/.vanor/logs/ JSONL logs
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm test
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Design details: [docs/TECH_DESIGN.md](docs/TECH_DESIGN.md).
|
package/base/config.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// 配置加载与校验。顺序:内置默认 → ~/.vanor/config.json → 环境变量覆盖。
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { createI18n, isSupportedLanguageSetting } from "./i18n/index.js";
|
|
7
|
+
import { deepMerge, ensureDir, expandHome, isPlainObject, readJsonSafe } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
const fallbackT = createI18n("en").t;
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CONFIG = {
|
|
12
|
+
llm: {
|
|
13
|
+
defaultModel: "",
|
|
14
|
+
providers: {},
|
|
15
|
+
fallback: [],
|
|
16
|
+
},
|
|
17
|
+
agent: {
|
|
18
|
+
maxIterations: 25,
|
|
19
|
+
thinking: "off",
|
|
20
|
+
contextWindow: 256000,
|
|
21
|
+
},
|
|
22
|
+
memory: {
|
|
23
|
+
memoryMaxChars: 2200,
|
|
24
|
+
userMaxChars: 1375,
|
|
25
|
+
compactThreshold: 0.75,
|
|
26
|
+
},
|
|
27
|
+
security: {
|
|
28
|
+
approval: "ask", // ask | auto | deny
|
|
29
|
+
allowlist: [],
|
|
30
|
+
denylist: ["rm -rf *", "sudo *", "shutdown *", "mkfs *"],
|
|
31
|
+
workspaceRoot: ".",
|
|
32
|
+
allowOutsideWorkspace: false,
|
|
33
|
+
},
|
|
34
|
+
session: {
|
|
35
|
+
restore: "last", // last | none
|
|
36
|
+
reset: { idleMinutes: 0, daily: false },
|
|
37
|
+
dmScope: "main",
|
|
38
|
+
},
|
|
39
|
+
logging: {
|
|
40
|
+
level: "info",
|
|
41
|
+
llmTrace: false, // 记录每次 LLM 请求/响应的完整详情(url、headers、body、返回内容)
|
|
42
|
+
llmTraceRedact: true, // 记录时对 Authorization / api-key 等敏感 header 脱敏
|
|
43
|
+
},
|
|
44
|
+
ui: {
|
|
45
|
+
color: true,
|
|
46
|
+
stream: true,
|
|
47
|
+
language: "auto", // auto | en | zh-CN
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** 返回 ~/.vanor 下的标准路径集合。 */
|
|
52
|
+
export function getPaths(root) {
|
|
53
|
+
const base = root || path.join(homedir(), ".vanor");
|
|
54
|
+
return {
|
|
55
|
+
root: base,
|
|
56
|
+
config: path.join(base, "config.json"),
|
|
57
|
+
state: path.join(base, "state.json"),
|
|
58
|
+
skills: path.join(base, "skills"),
|
|
59
|
+
memory: path.join(base, "memory"),
|
|
60
|
+
working: path.join(base, "memory", "working"),
|
|
61
|
+
sessions: path.join(base, "sessions"),
|
|
62
|
+
cache: path.join(base, "cache"),
|
|
63
|
+
logs: path.join(base, "logs"),
|
|
64
|
+
baseUser: path.join(base, "base-user"),
|
|
65
|
+
backups: path.join(base, "backups"),
|
|
66
|
+
plugins: path.join(base, "plugins"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 确保运行时目录存在。 */
|
|
71
|
+
export function ensurePaths(paths) {
|
|
72
|
+
for (const key of ["root", "skills", "memory", "working", "sessions", "cache", "logs"]) {
|
|
73
|
+
ensureDir(paths[key]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 递归解析 "env:VAR" 形式的字符串为环境变量值。 */
|
|
78
|
+
function resolveEnv(value) {
|
|
79
|
+
if (typeof value === "string") {
|
|
80
|
+
return value.startsWith("env:") ? process.env[value.slice(4)] || "" : value;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) return value.map(resolveEnv);
|
|
83
|
+
if (isPlainObject(value)) {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [k, v] of Object.entries(value)) out[k] = resolveEnv(v);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 加载配置。
|
|
93
|
+
* @param {string} [configPath] 配置文件路径,默认 ~/.vanor/config.json
|
|
94
|
+
* @returns {{ config: object, exists: boolean, path: string }}
|
|
95
|
+
*/
|
|
96
|
+
export function loadConfig(configPath) {
|
|
97
|
+
const file = configPath || getPaths().config;
|
|
98
|
+
const exists = fs.existsSync(file);
|
|
99
|
+
const fileConfig = exists ? readJsonSafe(file, {}) : {};
|
|
100
|
+
const merged = deepMerge(DEFAULT_CONFIG, fileConfig || {});
|
|
101
|
+
const config = resolveEnv(merged);
|
|
102
|
+
// workspaceRoot 归一为绝对路径(仅作用于运行时 config;raw 保留原始值供回写)
|
|
103
|
+
config.security.workspaceRoot = path.resolve(expandHome(config.security.workspaceRoot || "."));
|
|
104
|
+
// raw:未解析 env、未绝对化的合并结果,供配置向导回写,避免破坏 env: 引用
|
|
105
|
+
return { config, raw: merged, exists, path: file };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 写入配置文件(保留 env: 占位,不写入解析后的密钥)。 */
|
|
109
|
+
export function saveConfig(configPath, config) {
|
|
110
|
+
const file = configPath || getPaths().config;
|
|
111
|
+
ensureDir(path.dirname(file));
|
|
112
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
113
|
+
return file;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 仅写回 ui.language,保留 raw config 中的 env: 引用。 */
|
|
117
|
+
export function saveUiLanguage(configPath, language) {
|
|
118
|
+
const file = configPath || getPaths().config;
|
|
119
|
+
const raw = fs.existsSync(file) ? readJsonSafe(file, {}) || {} : {};
|
|
120
|
+
raw.ui = raw.ui || {};
|
|
121
|
+
raw.ui.language = language;
|
|
122
|
+
return saveConfig(file, raw);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 基本合法性校验,返回问题列表(供 vanor doctor 使用)。 */
|
|
126
|
+
export function validateConfig(config, t = fallbackT) {
|
|
127
|
+
const issues = [];
|
|
128
|
+
if (!config.llm?.defaultModel) issues.push(t("config.errors.noDefaultModel"));
|
|
129
|
+
const providers = config.llm?.providers || {};
|
|
130
|
+
if (Object.keys(providers).length === 0) issues.push(t("config.errors.noProviders"));
|
|
131
|
+
for (const [name, p] of Object.entries(providers)) {
|
|
132
|
+
if (!p.baseURL && p.type !== "anthropic") issues.push(t("config.errors.providerMissingBaseURL", { name }));
|
|
133
|
+
if (!p.apiKey) issues.push(t("config.errors.providerMissingApiKey", { name }));
|
|
134
|
+
}
|
|
135
|
+
if (!["ask", "auto", "deny"].includes(config.security?.approval)) {
|
|
136
|
+
issues.push(t("config.errors.approvalInvalid"));
|
|
137
|
+
}
|
|
138
|
+
if (!isSupportedLanguageSetting(config.ui?.language || "auto")) {
|
|
139
|
+
issues.push(t("config.errors.languageInvalid"));
|
|
140
|
+
}
|
|
141
|
+
return issues;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 仅校验启动所必需的项:默认模型及其引用的 provider。
|
|
146
|
+
* 不波及其它未使用的 provider(避免残留/备用配置阻塞启动)。
|
|
147
|
+
*/
|
|
148
|
+
export function startupIssues(config, t = fallbackT) {
|
|
149
|
+
const model = config.llm?.defaultModel;
|
|
150
|
+
if (!model) return [t("config.errors.noDefaultModel")];
|
|
151
|
+
const slash = model.indexOf("/");
|
|
152
|
+
if (slash < 0) return [t("config.errors.defaultModelFormat", { model })];
|
|
153
|
+
const name = model.slice(0, slash);
|
|
154
|
+
const p = config.llm?.providers?.[name];
|
|
155
|
+
if (!p) return [t("config.errors.defaultProviderMissing", { name })];
|
|
156
|
+
const issues = [];
|
|
157
|
+
if (!p.apiKey) issues.push(t("config.errors.providerMissingApiKey", { name }));
|
|
158
|
+
if ((p.type || "openai") !== "anthropic" && !p.baseURL) {
|
|
159
|
+
issues.push(t("config.errors.providerMissingBaseURL", { name }));
|
|
160
|
+
}
|
|
161
|
+
return issues;
|
|
162
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// 上下文压缩:历史超阈值时摘要旧消息,保留最近若干轮。
|
|
2
|
+
|
|
3
|
+
import { EVENTS } from "../events.js";
|
|
4
|
+
import { systemMessage, userMessage } from "../transport/message.js";
|
|
5
|
+
|
|
6
|
+
const KEEP_RECENT = 6;
|
|
7
|
+
|
|
8
|
+
/** 估算上下文是否超过压缩阈值。 */
|
|
9
|
+
export function shouldCompact(session, config) {
|
|
10
|
+
const window = config.agent?.contextWindow ?? 256000;
|
|
11
|
+
const threshold = config.memory?.compactThreshold ?? 0.75;
|
|
12
|
+
return session.estimatedTokens() > window * threshold;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 压缩较旧的消息为摘要,保留最近的对话。
|
|
17
|
+
* recent 从最近的一条 user 消息开始,保证发往 provider 的序列合法。
|
|
18
|
+
*/
|
|
19
|
+
export async function compact(session, deps) {
|
|
20
|
+
const msgs = session.messages;
|
|
21
|
+
if (msgs.length <= KEEP_RECENT + 2) return;
|
|
22
|
+
|
|
23
|
+
let idx = msgs.length - KEEP_RECENT;
|
|
24
|
+
while (idx > 0 && msgs[idx].role !== "user") idx--;
|
|
25
|
+
if (idx <= 0) return; // 找不到安全切点,放弃本次压缩
|
|
26
|
+
|
|
27
|
+
const older = msgs.slice(0, idx);
|
|
28
|
+
const recent = msgs.slice(idx);
|
|
29
|
+
|
|
30
|
+
const convo = older
|
|
31
|
+
.map((m) => {
|
|
32
|
+
const tools = m.toolCalls?.length ? ` [调用:${m.toolCalls.map((t) => t.name).join(",")}]` : "";
|
|
33
|
+
return `${m.role}: ${(m.content || "").slice(0, 2000)}${tools}`;
|
|
34
|
+
})
|
|
35
|
+
.join("\n");
|
|
36
|
+
|
|
37
|
+
let summaryText = "";
|
|
38
|
+
try {
|
|
39
|
+
const res = await deps.llm.gather({
|
|
40
|
+
model: deps.model,
|
|
41
|
+
messages: [
|
|
42
|
+
systemMessage(
|
|
43
|
+
"你是对话压缩器。用简洁中文总结以下历史,保留关键事实、文件路径、已完成项与待办,省略寒暄与冗余。",
|
|
44
|
+
),
|
|
45
|
+
userMessage(convo),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
summaryText = res.text || "";
|
|
49
|
+
} catch (e) {
|
|
50
|
+
deps.logger?.warn(EVENTS.memory.compress, { error: e.message, skipped: true });
|
|
51
|
+
return; // 压缩失败则保持原样
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
session.summary = session.summary ? `${session.summary}\n${summaryText}` : summaryText;
|
|
55
|
+
session.replaceRecent(recent);
|
|
56
|
+
session.addCompaction({ summary: summaryText, replaced: older.length });
|
|
57
|
+
deps.logger?.info(EVENTS.memory.compress, { replaced: older.length });
|
|
58
|
+
}
|