superagent-ai-agent 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/.env.example +27 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/README.zh-CN.md +147 -0
- package/agent-config.json +4 -0
- package/agent-persona.md +67 -0
- package/bin/postinstall.js +26 -0
- package/bin/superagent.js +283 -0
- package/package.json +43 -0
- package/src/agent.js +114 -0
- package/src/config.js +103 -0
- package/src/public/index.html +1889 -0
- package/src/query.js +239 -0
- package/src/server.js +303 -0
- package/src/web-tool.js +174 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bin/superagent.js — CLI 入口
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* superagent # 启动 Agent 服务
|
|
8
|
+
* superagent init # 在当前目录初始化配置文件
|
|
9
|
+
* superagent config # 交互式配置 API 密钥和网关
|
|
10
|
+
* superagent --port 8080 # 指定端口
|
|
11
|
+
* superagent --help # 显示帮助
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import readline from 'node:readline';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
23
|
+
|
|
24
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
25
|
+
|
|
26
|
+
// ─── 帮助信息 ───
|
|
27
|
+
const HELP = `
|
|
28
|
+
SuperAgent v${VERSION}
|
|
29
|
+
A local AI Agent with Plan-Execute-Reflect architecture.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
superagent [options] Start the agent server
|
|
33
|
+
superagent init Initialize config files in current directory
|
|
34
|
+
superagent config Configure API credentials interactively
|
|
35
|
+
superagent config --url <url> --token <token> Configure via flags
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--port, -p <port> Server port (default: 3000, or PORT env)
|
|
39
|
+
--url <url> Set ANTHROPIC_BASE_URL
|
|
40
|
+
--token <token> Set ANTHROPIC_AUTH_TOKEN
|
|
41
|
+
--model <model> Set ANTHROPIC_MODEL
|
|
42
|
+
--help, -h Show this help message
|
|
43
|
+
--version, -v Show version number
|
|
44
|
+
|
|
45
|
+
Environment Variables:
|
|
46
|
+
ANTHROPIC_BASE_URL API gateway URL (required)
|
|
47
|
+
ANTHROPIC_AUTH_TOKEN API auth token (required)
|
|
48
|
+
ANTHROPIC_MODEL Text model name
|
|
49
|
+
ANTHROPIC_MODEL_VISION Vision model name
|
|
50
|
+
PORT Server port (default: 3000)
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
superagent Start with default settings
|
|
54
|
+
superagent --port 8080 Start on port 8080
|
|
55
|
+
superagent init Create .env and config files
|
|
56
|
+
superagent config Interactive API setup
|
|
57
|
+
superagent config --url https://api.anthropic.com --token sk-xxx
|
|
58
|
+
`.trim();
|
|
59
|
+
|
|
60
|
+
// ─── 解析参数 ───
|
|
61
|
+
const args = process.argv.slice(2);
|
|
62
|
+
|
|
63
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
64
|
+
console.log(HELP);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
69
|
+
console.log(VERSION);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── 工具函数:获取 flag 值 ───
|
|
74
|
+
function getFlag(arr, flag) {
|
|
75
|
+
const idx = arr.indexOf(flag);
|
|
76
|
+
return idx !== -1 && arr[idx + 1] ? arr[idx + 1] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── 工具函数:读取 .env 为对象 ───
|
|
80
|
+
function readEnv(filePath) {
|
|
81
|
+
if (!fs.existsSync(filePath)) return {};
|
|
82
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
const env = {};
|
|
84
|
+
for (const line of content.split('\n')) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
87
|
+
const eqIdx = trimmed.indexOf('=');
|
|
88
|
+
if (eqIdx === -1) continue;
|
|
89
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
90
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
91
|
+
env[key] = val;
|
|
92
|
+
}
|
|
93
|
+
return env;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── 工具函数:写入 .env ───
|
|
97
|
+
function writeEnv(filePath, envObj) {
|
|
98
|
+
// 如果已有文件,就在原文件基础上更新
|
|
99
|
+
if (fs.existsSync(filePath)) {
|
|
100
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
101
|
+
for (const [key, val] of Object.entries(envObj)) {
|
|
102
|
+
// 匹配 KEY=value 或 # KEY=value 形式
|
|
103
|
+
const re = new RegExp(`^(#\\s*)?${key}\\s*=.*$`, 'm');
|
|
104
|
+
if (re.test(content)) {
|
|
105
|
+
content = content.replace(re, `${key}=${val}`);
|
|
106
|
+
} else {
|
|
107
|
+
content += `\n${key}=${val}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
111
|
+
} else {
|
|
112
|
+
const lines = Object.entries(envObj).map(([k, v]) => `${k}=${v}`);
|
|
113
|
+
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf8');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── 工具函数:交互式询问 ───
|
|
118
|
+
function ask(question, defaultVal = '') {
|
|
119
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
120
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : '';
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
123
|
+
rl.close();
|
|
124
|
+
resolve(answer.trim() || defaultVal);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── config 子命令 ───
|
|
130
|
+
if (args[0] === 'config') {
|
|
131
|
+
const cwd = process.cwd();
|
|
132
|
+
const envPath = path.join(cwd, '.env');
|
|
133
|
+
|
|
134
|
+
// 解析 --url / --token / --model 标志位
|
|
135
|
+
const flagUrl = getFlag(args, '--url');
|
|
136
|
+
const flagToken = getFlag(args, '--token');
|
|
137
|
+
const flagModel = getFlag(args, '--model');
|
|
138
|
+
|
|
139
|
+
if (flagUrl || flagToken || flagModel) {
|
|
140
|
+
// 非交互模式:通过命令行参数直接设置
|
|
141
|
+
const updates = {};
|
|
142
|
+
if (flagUrl) updates.ANTHROPIC_BASE_URL = flagUrl;
|
|
143
|
+
if (flagToken) updates.ANTHROPIC_AUTH_TOKEN = flagToken;
|
|
144
|
+
if (flagModel) updates.ANTHROPIC_MODEL = flagModel;
|
|
145
|
+
|
|
146
|
+
// 如果 .env 不存在,先从模板复制
|
|
147
|
+
if (!fs.existsSync(envPath)) {
|
|
148
|
+
const src = path.join(PKG_ROOT, '.env.example');
|
|
149
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, envPath);
|
|
150
|
+
}
|
|
151
|
+
writeEnv(envPath, updates);
|
|
152
|
+
console.log('✅ Configuration saved to .env');
|
|
153
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
154
|
+
const display = k.includes('TOKEN') ? v.slice(0, 8) + '...' : v;
|
|
155
|
+
console.log(` ${k}=${display}`);
|
|
156
|
+
}
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 交互模式
|
|
161
|
+
console.log('\n🔧 SuperAgent Configuration\n');
|
|
162
|
+
const currentEnv = readEnv(envPath);
|
|
163
|
+
|
|
164
|
+
const url = await ask(
|
|
165
|
+
' API Gateway URL (ANTHROPIC_BASE_URL)',
|
|
166
|
+
currentEnv.ANTHROPIC_BASE_URL || 'https://api.anthropic.com'
|
|
167
|
+
);
|
|
168
|
+
const token = await ask(
|
|
169
|
+
' API Token (ANTHROPIC_AUTH_TOKEN)',
|
|
170
|
+
currentEnv.ANTHROPIC_AUTH_TOKEN || ''
|
|
171
|
+
);
|
|
172
|
+
const model = await ask(
|
|
173
|
+
' Model name (ANTHROPIC_MODEL, press Enter to skip)',
|
|
174
|
+
currentEnv.ANTHROPIC_MODEL || ''
|
|
175
|
+
);
|
|
176
|
+
const portInput = await ask(
|
|
177
|
+
' Server port (PORT)',
|
|
178
|
+
currentEnv.PORT || '3000'
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (!url || !token) {
|
|
182
|
+
console.error('\n❌ URL and Token are required.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const updates = {
|
|
187
|
+
ANTHROPIC_BASE_URL: url,
|
|
188
|
+
ANTHROPIC_AUTH_TOKEN: token,
|
|
189
|
+
};
|
|
190
|
+
if (model) updates.ANTHROPIC_MODEL = model;
|
|
191
|
+
if (portInput && portInput !== '3000') updates.PORT = portInput;
|
|
192
|
+
|
|
193
|
+
// 如果 .env 不存在,先从模板复制
|
|
194
|
+
if (!fs.existsSync(envPath)) {
|
|
195
|
+
const src = path.join(PKG_ROOT, '.env.example');
|
|
196
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, envPath);
|
|
197
|
+
}
|
|
198
|
+
writeEnv(envPath, updates);
|
|
199
|
+
|
|
200
|
+
console.log('\n✅ Configuration saved to .env');
|
|
201
|
+
console.log(` ANTHROPIC_BASE_URL=${url}`);
|
|
202
|
+
console.log(` ANTHROPIC_AUTH_TOKEN=${token.slice(0, 8)}...`);
|
|
203
|
+
if (model) console.log(` ANTHROPIC_MODEL=${model}`);
|
|
204
|
+
console.log('\n Run "superagent" to start the server.\n');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── init 子命令 ───
|
|
209
|
+
if (args[0] === 'init') {
|
|
210
|
+
const cwd = process.cwd();
|
|
211
|
+
const files = [
|
|
212
|
+
{ name: '.env', source: '.env.example' },
|
|
213
|
+
{ name: 'agent-config.json', source: 'agent-config.json' },
|
|
214
|
+
{ name: 'agent-persona.md', source: 'agent-persona.md' },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
console.log('🚀 Initializing SuperAgent config files...\n');
|
|
218
|
+
|
|
219
|
+
for (const { name, source } of files) {
|
|
220
|
+
const dest = path.join(cwd, name);
|
|
221
|
+
if (fs.existsSync(dest)) {
|
|
222
|
+
console.log(` ⏭ ${name} (already exists, skipped)`);
|
|
223
|
+
} else {
|
|
224
|
+
const src = path.join(PKG_ROOT, source);
|
|
225
|
+
fs.copyFileSync(src, dest);
|
|
226
|
+
console.log(` ✅ ${name} (created)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`
|
|
231
|
+
Done! Next steps:
|
|
232
|
+
1. Run "superagent config" to set up API credentials
|
|
233
|
+
2. (Optional) Edit agent-config.json to customize agent name
|
|
234
|
+
3. (Optional) Edit agent-persona.md to customize personality
|
|
235
|
+
4. Run: superagent
|
|
236
|
+
`);
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── 解析 --port ───
|
|
241
|
+
let port = process.env.PORT || '3000';
|
|
242
|
+
const portIdx = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
|
243
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
244
|
+
port = args[portIdx + 1];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── 查找 .env 文件 ───
|
|
248
|
+
const envFile = path.resolve(process.cwd(), '.env');
|
|
249
|
+
const envArgs = fs.existsSync(envFile) ? ['--env-file', envFile] : [];
|
|
250
|
+
|
|
251
|
+
if (!fs.existsSync(envFile)) {
|
|
252
|
+
// 检查环境变量是否已设置
|
|
253
|
+
if (!process.env.ANTHROPIC_BASE_URL || !process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
254
|
+
console.error(`
|
|
255
|
+
❌ Missing configuration!
|
|
256
|
+
|
|
257
|
+
Either create a .env file or set environment variables:
|
|
258
|
+
ANTHROPIC_BASE_URL=<your-api-url>
|
|
259
|
+
ANTHROPIC_AUTH_TOKEN=<your-token>
|
|
260
|
+
|
|
261
|
+
Run 'superagent init' to create config files.
|
|
262
|
+
`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── 启动服务 ───
|
|
268
|
+
const serverPath = path.join(PKG_ROOT, 'src', 'server.js');
|
|
269
|
+
|
|
270
|
+
const child = spawn(process.execPath, [...envArgs, serverPath], {
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
stdio: 'inherit',
|
|
273
|
+
env: { ...process.env, PORT: port },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
child.on('exit', (code) => {
|
|
277
|
+
process.exit(code ?? 0);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 转发信号
|
|
281
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
282
|
+
process.on(sig, () => child.kill(sig));
|
|
283
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "superagent-ai-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local AI Agent powered by Claude SDK with Plan-Execute-Reflect architecture. Zero-build, browser-ready.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"superagent": "bin/superagent.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/",
|
|
16
|
+
"agent-config.json",
|
|
17
|
+
"agent-persona.md",
|
|
18
|
+
".env.example"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node --env-file=.env src/server.js",
|
|
22
|
+
"postinstall": "node bin/postinstall.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"claude",
|
|
27
|
+
"local-agent",
|
|
28
|
+
"plan-execute-reflect",
|
|
29
|
+
"ai-assistant",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+ssh://git@github.com/nodermachine/superagent.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/nodermachine/superagent",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.159",
|
|
39
|
+
"express": "^4.19.2",
|
|
40
|
+
"marked": "^18.0.4",
|
|
41
|
+
"playwright": "^1.60.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/agent.js — Plan-Execute-Reflect 编排器
|
|
3
|
+
*
|
|
4
|
+
* 对外唯一接口:runAgent({ prompt, sessionId, images, abortController })
|
|
5
|
+
*
|
|
6
|
+
* 三段式管线:
|
|
7
|
+
* 1. Planner — 规划(只读探查 + 输出结构化 plan)
|
|
8
|
+
* 2. Executor — 逐步执行(每步独立 session,防上下文雪崩)
|
|
9
|
+
* 3. Reflector — 失败反思(诊断 + 修复 + 验证,仅一次机会)
|
|
10
|
+
*
|
|
11
|
+
* 快捷路径:
|
|
12
|
+
* - LUNA_PLAN=0 → 关闭 plan,退化为单次 query
|
|
13
|
+
* - 简单请求(短 prompt + 匹配 SIMPLE_RE)→ 跳过 Planner 直接执行
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
MODEL_TEXT, EXECUTOR_TOOLS, PLANNER_TOOLS,
|
|
18
|
+
PLANNER_ROLE, REFLECTOR_ROLE,
|
|
19
|
+
PLAN_ENABLED, SIMPLE_RE,
|
|
20
|
+
} from './config.js';
|
|
21
|
+
import { withFallback, phaseModel, parsePlan } from './query.js';
|
|
22
|
+
|
|
23
|
+
export function runAgent({ prompt, sessionId, images = [], abortController }) {
|
|
24
|
+
// ─── 退化路径:关闭 plan,走原单次行为 ───
|
|
25
|
+
if (!PLAN_ENABLED) {
|
|
26
|
+
return (async function* () {
|
|
27
|
+
yield* withFallback({
|
|
28
|
+
prompt, images, model: phaseModel(images),
|
|
29
|
+
sessionId, abortController, tools: EXECUTOR_TOOLS, roleAppend: '',
|
|
30
|
+
});
|
|
31
|
+
})();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── 主管线 ───
|
|
35
|
+
return (async function* () {
|
|
36
|
+
let currentSessionId = sessionId ?? null;
|
|
37
|
+
|
|
38
|
+
// ─── 快捷路径:简单请求跳过 Planner ───
|
|
39
|
+
const isSimple = !images.length && prompt.length < 60 && SIMPLE_RE.test(prompt.trim());
|
|
40
|
+
if (isSimple) {
|
|
41
|
+
yield* withFallback({
|
|
42
|
+
prompt, images, model: MODEL_TEXT,
|
|
43
|
+
sessionId: currentSessionId, abortController, tools: EXECUTOR_TOOLS, roleAppend: '',
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Phase 1: Planner ───
|
|
49
|
+
const plannerState = yield* withFallback({
|
|
50
|
+
prompt, images, model: phaseModel(images),
|
|
51
|
+
sessionId: currentSessionId, abortController,
|
|
52
|
+
tools: PLANNER_TOOLS, roleAppend: PLANNER_ROLE,
|
|
53
|
+
});
|
|
54
|
+
const planText = plannerState.text;
|
|
55
|
+
currentSessionId = plannerState.sessionId ?? currentSessionId;
|
|
56
|
+
|
|
57
|
+
// 解析 plan JSON
|
|
58
|
+
const plan = parsePlan(planText);
|
|
59
|
+
const steps = Array.isArray(plan?.steps)
|
|
60
|
+
? plan.steps
|
|
61
|
+
.filter((s) => s && s.goal)
|
|
62
|
+
.map((s, i) => ({ id: s.id ?? i + 1, goal: String(s.goal), verifyHint: s.verifyHint ? String(s.verifyHint) : '' }))
|
|
63
|
+
: [];
|
|
64
|
+
const effectivePlan = plan?.needPlan === true && steps.length > 0;
|
|
65
|
+
|
|
66
|
+
// 通知前端 plan 结果
|
|
67
|
+
yield { type: 'plan', needPlan: effectivePlan, steps, reason: plan?.reason ?? '' };
|
|
68
|
+
|
|
69
|
+
// ─── Phase 2a: 无需 plan,单次直接回答 ───
|
|
70
|
+
if (!effectivePlan) {
|
|
71
|
+
yield* withFallback({
|
|
72
|
+
prompt, images, model: phaseModel(images),
|
|
73
|
+
sessionId: currentSessionId, abortController, tools: EXECUTOR_TOOLS, roleAppend: '',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Phase 2b: 逐步执行(独立 session 防上下文雪崩) ───
|
|
79
|
+
const total = steps.length;
|
|
80
|
+
const planSummary = steps.map((s) => `${s.id}. ${s.goal}`).join('\n');
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < total; i++) {
|
|
83
|
+
const step = steps[i];
|
|
84
|
+
const stepPrompt = `【执行计划】(${i + 1}/${total})\n${planSummary}\n\n` +
|
|
85
|
+
`当前执行第 ${step.id} 步:\n目标: ${step.goal}\n` +
|
|
86
|
+
(step.verifyHint ? `验证: ${step.verifyHint}\n` : '') +
|
|
87
|
+
`\n只执行这一步。若已在前面顺带完成,直接说明已完成即可。完成后用一句话总结结果。`;
|
|
88
|
+
|
|
89
|
+
// 每步独立 session + 固定 TEXT 模型
|
|
90
|
+
const exState = yield* withFallback({
|
|
91
|
+
prompt: stepPrompt, images: [], model: MODEL_TEXT,
|
|
92
|
+
sessionId: null, abortController, tools: EXECUTOR_TOOLS, roleAppend: '',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── Phase 3: Reflector(该步失败时触发,最多 1 次) ───
|
|
96
|
+
if (exState.failures.length > 0) {
|
|
97
|
+
const failureText = exState.failures.join('\n---\n').slice(0, 4000);
|
|
98
|
+
yield { type: 'reflection', phase: 'start', step: step.id, goal: step.goal, failure: failureText };
|
|
99
|
+
|
|
100
|
+
const refPrompt = `执行计划第 ${step.id} 步「${step.goal}」时出现失败:\n\n${failureText}\n\n` +
|
|
101
|
+
`诊断根因并修复。` +
|
|
102
|
+
(step.verifyHint ? `修完后必须执行验证: ${step.verifyHint},确认通过再结束。` : '') +
|
|
103
|
+
`这是唯一一次反思机会,修完说明修了什么。`;
|
|
104
|
+
|
|
105
|
+
// Reflector 使用 executor 的 sessionId(看到失败的工具调用上下文)
|
|
106
|
+
yield* withFallback({
|
|
107
|
+
prompt: refPrompt, images: [], model: MODEL_TEXT,
|
|
108
|
+
sessionId: exState.sessionId, abortController, tools: EXECUTOR_TOOLS, roleAppend: REFLECTOR_ROLE,
|
|
109
|
+
});
|
|
110
|
+
yield { type: 'reflection', phase: 'end', step: step.id };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/config.js — Luna Agent 配置中心
|
|
3
|
+
*
|
|
4
|
+
* 所有可调参数、工具列表、角色提示词模板集中在这里。
|
|
5
|
+
* 修改模型名称、增减工具、调整 prompt 只需改这一个文件。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════
|
|
11
|
+
// 模型配置
|
|
12
|
+
// ═══════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
/** 纯文本对话使用的主模型 */
|
|
15
|
+
export const MODEL_TEXT = process.env.ANTHROPIC_MODEL ?? 'glm-5.2';
|
|
16
|
+
|
|
17
|
+
/** 多模态(带图片)请求使用的模型 */
|
|
18
|
+
export const MODEL_VISION = process.env.ANTHROPIC_MODEL_VISION ?? MODEL_TEXT;
|
|
19
|
+
|
|
20
|
+
/** 主模型命中内容审查时自动切换的兜底模型 */
|
|
21
|
+
export const MODEL_FALLBACK = process.env.ANTHROPIC_MODEL_FALLBACK ?? 'qwen3.7-plus';
|
|
22
|
+
|
|
23
|
+
// ═══════════════════════════════════════════════
|
|
24
|
+
// 工具列表
|
|
25
|
+
// ═══════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
/** Executor 拥有的全量工具(读写 + shell + web) */
|
|
28
|
+
export const EXECUTOR_TOOLS = [
|
|
29
|
+
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
|
30
|
+
'mcp__web__WebSearch', 'mcp__web__WebFetch',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Planner 拥有的工具(与 Executor 相同,但通过 prompt 约束其只做规划) */
|
|
34
|
+
export const PLANNER_TOOLS = [
|
|
35
|
+
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
|
36
|
+
'mcp__web__WebSearch', 'mcp__web__WebFetch',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════
|
|
40
|
+
// 开关 & 正则
|
|
41
|
+
// ═══════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
/** Plan-Execute 总开关:LUNA_PLAN=0 退化为原单次 query 行为(应急逃生口) */
|
|
44
|
+
export const PLAN_ENABLED = process.env.LUNA_PLAN !== '0';
|
|
45
|
+
|
|
46
|
+
/** 命中内容安全审查的特征正则(智谱网关会把上游 data_inspection_failed 包成 InvalidParameter) */
|
|
47
|
+
export const INSPECTION_RE = /data_inspection_failed|Input text data may contain inappropriate content/i;
|
|
48
|
+
|
|
49
|
+
/** 简单请求判定:命中则跳过 Planner 直接执行,减少延迟 */
|
|
50
|
+
export const SIMPLE_RE = /^(你好|hi|hello|hey|谢谢|thanks|帮我|能不能|什么|是什么|怎么).{0,30}$/i;
|
|
51
|
+
|
|
52
|
+
// ═══════════════════════════════════════════════
|
|
53
|
+
// 路径
|
|
54
|
+
// ═══════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
/** 人设文件路径(每次 query 重新读取,改完即生效) */
|
|
57
|
+
export const PERSONA_PATH = path.resolve(process.cwd(), 'agent-persona.md');
|
|
58
|
+
|
|
59
|
+
/** 项目级 skills 目录 */
|
|
60
|
+
export const PROJECT_SKILLS_DIR = path.resolve(process.cwd(), '.claude', 'skills');
|
|
61
|
+
|
|
62
|
+
// ═══════════════════════════════════════════════
|
|
63
|
+
// 角色提示词模板(append 到主 persona 之后)
|
|
64
|
+
// ═══════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
/** Planner 角色约束 — 让模型只输出结构化计划,不执行 */
|
|
67
|
+
export const PLANNER_ROLE = `
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
## 当前阶段:Planner(规划者)
|
|
71
|
+
|
|
72
|
+
你现在是 **Planner**,负责把主人的请求拆成可执行计划。**你没有写文件 / 跑命令的权限**,严禁执行任何修改性操作,只能用只读工具探查现状。
|
|
73
|
+
|
|
74
|
+
先按需 Read/Grep/Glob 了解现状,然后输出**唯一一个**计划块,格式严格如下(\`\`\`plan 包裹):
|
|
75
|
+
|
|
76
|
+
\`\`\`plan
|
|
77
|
+
{
|
|
78
|
+
"needPlan": <true|false>,
|
|
79
|
+
"reason": "<一句话:为什么需要/不需要拆步>",
|
|
80
|
+
"steps": [
|
|
81
|
+
{"id": 1, "goal": "<这步达成什么>", "verifyHint": "<如何验证成功,如:跑 xxx 测试 / 检查文件存在 / 编译通过>"}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
判定:
|
|
87
|
+
- 简单问答、纯解释、单步即可完成 → \`needPlan: false\`,steps 留空
|
|
88
|
+
- 多步、有依赖、涉及多文件 / 多次工具调用 → \`needPlan: true\`,steps 写清每步目标和验证方式
|
|
89
|
+
|
|
90
|
+
输出计划后**立即停止**,不执行步骤,不额外寒暄。`;
|
|
91
|
+
|
|
92
|
+
/** Reflector 角色约束 — 诊断失败并修复,只有一次机会 */
|
|
93
|
+
export const REFLECTOR_ROLE = `
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
## 当前阶段:Reflector(反思者)
|
|
97
|
+
|
|
98
|
+
你现在是 **Reflector**。执行器在某个子任务上失败了,你的职责:
|
|
99
|
+
1. 诊断根因(看 tool_result 的错误)
|
|
100
|
+
2. **优先用客观验证**(跑测试 / 编译 / lint)判断,别凭主观
|
|
101
|
+
3. 给出具体修复并执行
|
|
102
|
+
|
|
103
|
+
这是**唯一一次**反思机会,修完说明修了什么,不要无限重试。`;
|