@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { Session } from "../core/session.js";
|
|
5
|
+
import { buildSystemPrompt } from "../core/prompt.js";
|
|
6
|
+
import { createI18n } from "../i18n/index.js";
|
|
7
|
+
import { NullLogger } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
test("estimatedTokens 约 4 字符/token", () => {
|
|
10
|
+
const s = new Session({ sessions: os.tmpdir() }, new NullLogger(), {});
|
|
11
|
+
s.messages.push({ role: "user", content: "a".repeat(40) });
|
|
12
|
+
assert.equal(s.estimatedTokens(), 10);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("系统提示词在 CLI 渠道下提示终端格式", () => {
|
|
16
|
+
const sys = buildSystemPrompt({
|
|
17
|
+
memory: { snapshot: () => "" },
|
|
18
|
+
skills: [],
|
|
19
|
+
summary: "",
|
|
20
|
+
workspaceRoot: "/x",
|
|
21
|
+
channel: "cli",
|
|
22
|
+
i18n: createI18n("zh-CN"),
|
|
23
|
+
});
|
|
24
|
+
assert.match(sys, /命令行|CLI/);
|
|
25
|
+
assert.match(sys, /通讯方式/);
|
|
26
|
+
assert.match(sys, /终端能力|TERM=/);
|
|
27
|
+
assert.match(sys, /运行环境/);
|
|
28
|
+
assert.match(sys, /Node\.js/);
|
|
29
|
+
assert.match(sys, /操作系统/);
|
|
30
|
+
assert.match(sys, /UTC[+-]\d{2}:\d{2}/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("系统提示词注入 Vanor 实例上下文", () => {
|
|
34
|
+
const sys = buildSystemPrompt({
|
|
35
|
+
memory: { snapshot: () => "" },
|
|
36
|
+
skills: [],
|
|
37
|
+
summary: "",
|
|
38
|
+
workspaceRoot: "/workspace",
|
|
39
|
+
channel: "cli",
|
|
40
|
+
i18n: createI18n("zh-CN"),
|
|
41
|
+
instance: {
|
|
42
|
+
model: "wanyou/deepseek/deepseek-v4-pro",
|
|
43
|
+
configPath: "/home/u/.vanor/config.json",
|
|
44
|
+
paths: {
|
|
45
|
+
root: "/home/u/.vanor",
|
|
46
|
+
sessions: "/home/u/.vanor/sessions",
|
|
47
|
+
memory: "/home/u/.vanor/memory",
|
|
48
|
+
skills: "/home/u/.vanor/skills",
|
|
49
|
+
logs: "/home/u/.vanor/logs",
|
|
50
|
+
},
|
|
51
|
+
sessionId: "sess_1",
|
|
52
|
+
sessionRestored: true,
|
|
53
|
+
approval: "ask",
|
|
54
|
+
skillDirs: ["/home/u/.vanor/skills", "/workspace/.skills"],
|
|
55
|
+
skillsCount: 2,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
assert.match(sys, /Vanor 实例上下文/);
|
|
59
|
+
assert.match(sys, /当前模型:wanyou\/deepseek\/deepseek-v4-pro/);
|
|
60
|
+
assert.match(sys, /当前 provider:wanyou/);
|
|
61
|
+
assert.match(sys, /配置文件:\/home\/u\/\.vanor\/config\.json/);
|
|
62
|
+
assert.match(sys, /当前会话:sess_1(从历史恢复)/);
|
|
63
|
+
assert.match(sys, /工具审批模式:ask/);
|
|
64
|
+
assert.match(sys, /已加载技能数:2/);
|
|
65
|
+
assert.match(sys, /不要输出 API Key/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("系统提示词在 Intl 缺失时仍可构造时区信息", () => {
|
|
69
|
+
const desc = Object.getOwnPropertyDescriptor(globalThis, "Intl");
|
|
70
|
+
try {
|
|
71
|
+
Object.defineProperty(globalThis, "Intl", { value: undefined, configurable: true });
|
|
72
|
+
const sys = buildSystemPrompt({
|
|
73
|
+
memory: { snapshot: () => "" },
|
|
74
|
+
skills: [],
|
|
75
|
+
summary: "",
|
|
76
|
+
workspaceRoot: "/x",
|
|
77
|
+
channel: "cli",
|
|
78
|
+
i18n: createI18n("zh-CN"),
|
|
79
|
+
});
|
|
80
|
+
assert.match(sys, /当前时间/);
|
|
81
|
+
assert.match(sys, /UTC[+-]\d{2}:\d{2}/);
|
|
82
|
+
} finally {
|
|
83
|
+
if (desc) Object.defineProperty(globalThis, "Intl", desc);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("CLI 渠道注入具体终端能力(宽度/类型)", () => {
|
|
88
|
+
const sys = buildSystemPrompt({
|
|
89
|
+
memory: { snapshot: () => "" },
|
|
90
|
+
skills: [],
|
|
91
|
+
summary: "",
|
|
92
|
+
workspaceRoot: "/x",
|
|
93
|
+
channel: "cli",
|
|
94
|
+
i18n: createI18n("zh-CN"),
|
|
95
|
+
terminal: { cols: 120, rows: 30, term: "xterm-256color", color: true, trueColor: true },
|
|
96
|
+
});
|
|
97
|
+
assert.match(sys, /TERM=xterm-256color/);
|
|
98
|
+
assert.match(sys, /120 列/);
|
|
99
|
+
assert.match(sys, /真彩色/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("无颜色终端不显示真彩色标注", () => {
|
|
103
|
+
const sys = buildSystemPrompt({
|
|
104
|
+
memory: { snapshot: () => "" },
|
|
105
|
+
skills: [],
|
|
106
|
+
summary: "",
|
|
107
|
+
workspaceRoot: "/x",
|
|
108
|
+
channel: "cli",
|
|
109
|
+
i18n: createI18n("zh-CN"),
|
|
110
|
+
terminal: { cols: 80, rows: 24, term: "dumb", color: false, trueColor: true },
|
|
111
|
+
});
|
|
112
|
+
assert.match(sys, /不支持颜色/);
|
|
113
|
+
assert.doesNotMatch(sys, /真彩色/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("非 CLI 渠道不注入终端能力", () => {
|
|
117
|
+
const sys = buildSystemPrompt({
|
|
118
|
+
memory: { snapshot: () => "" },
|
|
119
|
+
skills: [],
|
|
120
|
+
summary: "",
|
|
121
|
+
workspaceRoot: "/x",
|
|
122
|
+
channel: "im",
|
|
123
|
+
i18n: createI18n("zh-CN"),
|
|
124
|
+
});
|
|
125
|
+
assert.doesNotMatch(sys, /TERM=/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("非 CLI 渠道使用通用格式说明", () => {
|
|
129
|
+
const sys = buildSystemPrompt({
|
|
130
|
+
memory: { snapshot: () => "" },
|
|
131
|
+
skills: [],
|
|
132
|
+
summary: "",
|
|
133
|
+
workspaceRoot: "/x",
|
|
134
|
+
channel: "im",
|
|
135
|
+
i18n: createI18n("zh-CN"),
|
|
136
|
+
});
|
|
137
|
+
assert.match(sys, /通讯渠道/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("系统提示词支持英文语言", () => {
|
|
141
|
+
const sys = buildSystemPrompt({
|
|
142
|
+
memory: { snapshot: () => "" },
|
|
143
|
+
skills: [],
|
|
144
|
+
summary: "",
|
|
145
|
+
workspaceRoot: "/x",
|
|
146
|
+
channel: "cli",
|
|
147
|
+
i18n: createI18n("en"),
|
|
148
|
+
terminal: { cols: 100, rows: 30, term: "xterm", color: false, trueColor: false },
|
|
149
|
+
});
|
|
150
|
+
assert.match(sys, /Runtime environment/);
|
|
151
|
+
assert.match(sys, /Default response language: English/);
|
|
152
|
+
assert.match(sys, /Terminal capabilities/);
|
|
153
|
+
assert.doesNotMatch(sys, /运行环境/);
|
|
154
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createI18n, detectLanguage, normalizeLanguage } from "../i18n/index.js";
|
|
4
|
+
|
|
5
|
+
test("normalizeLanguage 归一化常见系统语言格式", () => {
|
|
6
|
+
assert.equal(normalizeLanguage("zh_CN.UTF-8"), "zh-CN");
|
|
7
|
+
assert.equal(normalizeLanguage("zh-Hans"), "zh-CN");
|
|
8
|
+
assert.equal(normalizeLanguage("en_US.UTF-8"), "en");
|
|
9
|
+
assert.equal(normalizeLanguage("fr_FR.UTF-8"), "en");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("detectLanguage 根据环境变量检测语言", () => {
|
|
13
|
+
assert.equal(detectLanguage({ ui: { language: "auto" } }, { LANG: "zh_CN.UTF-8" }).language, "zh-CN");
|
|
14
|
+
assert.equal(detectLanguage({ ui: { language: "auto" } }, { LANG: "en_US.UTF-8" }).language, "en");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("config.ui.language 优先于系统语言", () => {
|
|
18
|
+
const detected = detectLanguage({ ui: { language: "en" } }, { LANG: "zh_CN.UTF-8" });
|
|
19
|
+
assert.equal(detected.language, "en");
|
|
20
|
+
assert.equal(detected.source, "config");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("无法确认语言时回退英文", () => {
|
|
24
|
+
const detected = detectLanguage({ ui: { language: "auto" } }, {}, null);
|
|
25
|
+
assert.equal(detected.language, "en");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("缺失翻译 key 回退英文或 key 本身", () => {
|
|
29
|
+
const zh = createI18n("zh-CN");
|
|
30
|
+
assert.equal(zh.t("cli.banner"), "Vanor 万佑");
|
|
31
|
+
assert.equal(zh.t("missing.key"), "missing.key");
|
|
32
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// 集成冒烟:mock LLM 跑最小 loop,验证 LLM ↔ 工具 全链路。
|
|
2
|
+
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createSecurity } from "../security/index.js";
|
|
9
|
+
import { defaultRegistry, createToolContext } from "../tools/index.js";
|
|
10
|
+
import { Session } from "../core/session.js";
|
|
11
|
+
import { runLoop } from "../core/loop.js";
|
|
12
|
+
import { NullLogger } from "../logger.js";
|
|
13
|
+
|
|
14
|
+
test("最小 loop:工具调用 → 结果 → 文本结束", async () => {
|
|
15
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), "vanor-loop-"));
|
|
16
|
+
fs.writeFileSync(path.join(ws, "hello.txt"), "HELLO");
|
|
17
|
+
const sessionsDir = path.join(ws, "sessions");
|
|
18
|
+
|
|
19
|
+
const security = createSecurity({ security: { approval: "auto", workspaceRoot: ws } });
|
|
20
|
+
const registry = defaultRegistry();
|
|
21
|
+
const toolCtx = createToolContext({ security, logger: new NullLogger(), confirm: async () => true });
|
|
22
|
+
const session = new Session({ sessions: sessionsDir }, new NullLogger(), { model: "mock/m" });
|
|
23
|
+
session.start();
|
|
24
|
+
session.addMessage({ id: "u1", role: "user", content: "读取 hello.txt", meta: {} });
|
|
25
|
+
|
|
26
|
+
// mock LLM:第一轮发起读取,第二轮基于结果输出文本
|
|
27
|
+
let calls = 0;
|
|
28
|
+
const llm = {
|
|
29
|
+
async *stream() {
|
|
30
|
+
calls++;
|
|
31
|
+
if (calls === 1) {
|
|
32
|
+
yield { type: "tool_call", call: { id: "c1", name: "read_file", arguments: { path: "hello.txt" } } };
|
|
33
|
+
yield { type: "done", finishReason: "tool_calls" };
|
|
34
|
+
} else {
|
|
35
|
+
yield { type: "text", delta: "文件内容是 HELLO" };
|
|
36
|
+
yield { type: "done", finishReason: "stop" };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async gather() {
|
|
40
|
+
return { text: "", toolCalls: [], usage: null, finishReason: "stop" };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const deps = {
|
|
45
|
+
llm,
|
|
46
|
+
registry,
|
|
47
|
+
toolCtx,
|
|
48
|
+
config: { agent: { maxIterations: 5 }, memory: {} },
|
|
49
|
+
model: "mock/m",
|
|
50
|
+
tools: registry.schemas(),
|
|
51
|
+
buildSystem: () => "system",
|
|
52
|
+
logger: new NullLogger(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const result = await runLoop(session, deps, {});
|
|
56
|
+
assert.ok(result.assistant);
|
|
57
|
+
assert.equal(result.assistant.content, "文件内容是 HELLO");
|
|
58
|
+
|
|
59
|
+
// 会话中应包含一条 read_file 的 tool 结果,内容为文件内容
|
|
60
|
+
const toolMsg = session.messages.find((m) => m.role === "tool");
|
|
61
|
+
assert.ok(toolMsg);
|
|
62
|
+
assert.equal(toolMsg.content, "HELLO");
|
|
63
|
+
assert.equal(calls, 2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("达到最大迭代次数会停止", async () => {
|
|
67
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), "vanor-loop2-"));
|
|
68
|
+
const security = createSecurity({ security: { approval: "auto", workspaceRoot: ws } });
|
|
69
|
+
const registry = defaultRegistry();
|
|
70
|
+
const toolCtx = createToolContext({ security, logger: new NullLogger(), confirm: async () => true });
|
|
71
|
+
const session = new Session({ sessions: path.join(ws, "s") }, new NullLogger(), {});
|
|
72
|
+
session.start();
|
|
73
|
+
session.addMessage({ id: "u1", role: "user", content: "loop", meta: {} });
|
|
74
|
+
|
|
75
|
+
// 永远返回工具调用 → 触发上限
|
|
76
|
+
const llm = {
|
|
77
|
+
async *stream() {
|
|
78
|
+
yield { type: "tool_call", call: { id: "c", name: "list_dir", arguments: { path: "." } } };
|
|
79
|
+
yield { type: "done", finishReason: "tool_calls" };
|
|
80
|
+
},
|
|
81
|
+
async gather() {
|
|
82
|
+
return { text: "" };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const deps = {
|
|
86
|
+
llm,
|
|
87
|
+
registry,
|
|
88
|
+
toolCtx,
|
|
89
|
+
config: { agent: { maxIterations: 3 }, memory: {} },
|
|
90
|
+
model: "mock/m",
|
|
91
|
+
tools: registry.schemas(),
|
|
92
|
+
buildSystem: () => "system",
|
|
93
|
+
logger: new NullLogger(),
|
|
94
|
+
};
|
|
95
|
+
const result = await runLoop(session, deps, {});
|
|
96
|
+
assert.equal(result.maxIterations, true);
|
|
97
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createMemory } from "../memory/index.js";
|
|
7
|
+
import { NullLogger } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
function tempPaths() {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vanor-mem-"));
|
|
11
|
+
return { memory: path.join(root, "memory"), working: path.join(root, "memory", "working") };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const config = { memory: { memoryMaxChars: 2200, userMaxChars: 1375 } };
|
|
15
|
+
|
|
16
|
+
test("add 后 snapshot 含条目", () => {
|
|
17
|
+
const mem = createMemory(tempPaths(), config, new NullLogger());
|
|
18
|
+
mem.apply({ action: "add", target: "memory", content: "用户使用 macOS" });
|
|
19
|
+
assert.match(mem.snapshot(), /macOS/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("replace 唯一匹配", () => {
|
|
23
|
+
const mem = createMemory(tempPaths(), config, new NullLogger());
|
|
24
|
+
mem.apply({ action: "add", target: "user", content: "偏好深色模式" });
|
|
25
|
+
const r = mem.apply({ action: "replace", target: "user", old_text: "深色", content: "偏好浅色模式" });
|
|
26
|
+
assert.ok(!r.isError);
|
|
27
|
+
assert.match(mem.snapshot(), /浅色/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("remove 删除条目", () => {
|
|
31
|
+
const mem = createMemory(tempPaths(), config, new NullLogger());
|
|
32
|
+
mem.apply({ action: "add", target: "memory", content: "临时事实" });
|
|
33
|
+
mem.apply({ action: "remove", target: "memory", old_text: "临时事实" });
|
|
34
|
+
assert.doesNotMatch(mem.snapshot(), /临时事实/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("search 关键词命中", () => {
|
|
38
|
+
const mem = createMemory(tempPaths(), config, new NullLogger());
|
|
39
|
+
mem.apply({ action: "add", target: "memory", content: "项目使用 Node.js 与 SQLite" });
|
|
40
|
+
const hits = mem.search("sqlite");
|
|
41
|
+
assert.equal(hits.length, 1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("空记忆 snapshot 为空串", () => {
|
|
45
|
+
const mem = createMemory(tempPaths(), config, new NullLogger());
|
|
46
|
+
assert.equal(mem.snapshot(), "");
|
|
47
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
userMessage,
|
|
5
|
+
assistantMessage,
|
|
6
|
+
toolMessage,
|
|
7
|
+
validateAlternation,
|
|
8
|
+
} from "../transport/message.js";
|
|
9
|
+
|
|
10
|
+
test("消息构造带 id 与时间戳", () => {
|
|
11
|
+
const m = userMessage("hi");
|
|
12
|
+
assert.equal(m.role, "user");
|
|
13
|
+
assert.equal(m.content, "hi");
|
|
14
|
+
assert.ok(m.id.startsWith("msg_"));
|
|
15
|
+
assert.ok(m.meta.timestamp);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("assistant 带 toolCalls", () => {
|
|
19
|
+
const m = assistantMessage("", [{ id: "c1", name: "read_file", arguments: {} }]);
|
|
20
|
+
assert.equal(m.toolCalls.length, 1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("toolMessage 标记错误", () => {
|
|
24
|
+
const m = toolMessage({ toolCallId: "c1", name: "x", content: "boom", isError: true });
|
|
25
|
+
assert.equal(m.role, "tool");
|
|
26
|
+
assert.equal(m.toolCallId, "c1");
|
|
27
|
+
assert.equal(m.meta.isError, true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("validateAlternation 检测连续 user", () => {
|
|
31
|
+
const errs = validateAlternation([userMessage("a"), userMessage("b")]);
|
|
32
|
+
assert.ok(errs.length >= 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("validateAlternation 合法序列通过", () => {
|
|
36
|
+
const errs = validateAlternation([userMessage("a"), assistantMessage("b")]);
|
|
37
|
+
assert.deepEqual(errs, []);
|
|
38
|
+
});
|