@baimingtao/lbs-agent 1.0.0 → 1.2.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/bin/lbs.js +123 -44
- package/config/state_root.py +20 -2
- package/interfaces/cli/_impl/commands/chat_repl.py +31 -22
- package/interfaces/cli/_impl/commands/setup.py +519 -0
- package/interfaces/cli/_impl/main.py +8 -0
- package/package.json +1 -1
- package/scripts/init.js +123 -72
package/bin/lbs.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lbs — CLI wrapper for Leaving Blank Space Agent.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* 架构 (对标 Hermes):
|
|
6
|
+
* 包代码 (只读) ← npm install -g 安装的目录
|
|
7
|
+
* state root (可写) ← %LOCALAPPDATA%/lbs-agent 或 ~/.lbs-agent
|
|
8
|
+
* ├── venv/ ← Python 虚拟环境
|
|
9
|
+
* ├── config.yaml ← 用户配置
|
|
10
|
+
* ├── .env ← API keys
|
|
11
|
+
* ├── sessions/ ← 会话数据
|
|
12
|
+
* └── ...
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1. 检查 Python 虚拟环境是否已创建
|
|
10
|
-
* 2. 如果没有,自动运行 init 脚本
|
|
11
|
-
* 3. 用虚拟环境的 Python 执行 lbs.py
|
|
14
|
+
* venv 不再建在包目录里,升级/重装 npm 包不会丢失运行时状态。
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
const { spawn, spawnSync } = require('child_process');
|
|
@@ -16,92 +19,168 @@ const path = require('path');
|
|
|
16
19
|
const fs = require('fs');
|
|
17
20
|
const os = require('os');
|
|
18
21
|
|
|
19
|
-
// 解析项目根目录(npm install 后 bin 的上级是包根目录)
|
|
20
22
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
23
|
+
|
|
24
|
+
// ── State Root 解析 (对标 Hermes get_hermes_home) ──────────────
|
|
25
|
+
|
|
26
|
+
function getStateRoot() {
|
|
27
|
+
// 1. 环境变量 LBS_HOME 优先
|
|
28
|
+
const envHome = process.env.LBS_HOME;
|
|
29
|
+
if (envHome) return path.resolve(envHome);
|
|
30
|
+
|
|
31
|
+
// 2. 平台默认: Windows → %LOCALAPPDATA%/lbs-agent, 其他 → ~/.lbs-agent
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
34
|
+
return path.join(localAppData, 'lbs-agent');
|
|
35
|
+
}
|
|
36
|
+
return path.join(os.homedir(), '.lbs-agent');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATE_ROOT = getStateRoot();
|
|
40
|
+
const VENV_DIR = path.join(STATE_ROOT, 'venv');
|
|
41
|
+
const VENV_PYTHON = process.platform === 'win32'
|
|
42
|
+
? path.join(VENV_DIR, 'Scripts', 'python.exe')
|
|
43
|
+
: path.join(VENV_DIR, 'bin', 'python');
|
|
44
|
+
const INIT_FLAG = path.join(STATE_ROOT, '.lbs-initialized');
|
|
27
45
|
const INIT_SCRIPT = path.join(PKG_ROOT, 'scripts', 'init.js');
|
|
28
46
|
|
|
29
|
-
//
|
|
47
|
+
// ── 颜色与图标 (对标 Hermes banner/colors) ─────────────────────
|
|
48
|
+
|
|
49
|
+
const C = {
|
|
50
|
+
reset: '\x1b[0m',
|
|
51
|
+
bold: '\x1b[1m',
|
|
52
|
+
dim: '\x1b[2m',
|
|
53
|
+
red: '\x1b[31m',
|
|
54
|
+
green: '\x1b[32m',
|
|
55
|
+
yellow: '\x1b[33m',
|
|
56
|
+
blue: '\x1b[34m',
|
|
57
|
+
magenta: '\x1b[35m',
|
|
58
|
+
cyan: '\x1b[36m',
|
|
59
|
+
gold: '\x1b[1;38;2;255;191;0m',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function supportsColor() {
|
|
63
|
+
if (process.env.NO_COLOR) return false;
|
|
64
|
+
if (process.env.TERM === 'dumb') return false;
|
|
65
|
+
return process.stdout.isTTY;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function paint(text, ...colors) {
|
|
69
|
+
if (!supportsColor()) return text;
|
|
70
|
+
return colors.join('') + text + C.reset;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── ASCII Logo ─────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const LOGO = [
|
|
76
|
+
'',
|
|
77
|
+
' ' + paint('╔══╗', C.gold) + ' ' + paint('╔═══╗', C.cyan) + ' ' + paint('╔═════╗', C.cyan),
|
|
78
|
+
' ' + paint('║ ║', C.gold) + ' ' + paint('║ ║', C.cyan) + ' ' + paint('║ ╔═╝', C.cyan),
|
|
79
|
+
' ' + paint('╠══╣', C.gold) + ' ' + paint('╠═══╣', C.cyan) + ' ' + paint('║ ╚═╗', C.cyan),
|
|
80
|
+
' ' + paint('║ ║', C.gold) + ' ' + paint('║ ║', C.cyan) + ' ' + paint('╚═════╝', C.cyan),
|
|
81
|
+
' ' + paint('╚══╝', C.gold) + ' ' + paint('╚═══╝', C.cyan) + ' ' + paint('AGENT', C.dim),
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
84
|
+
|
|
85
|
+
function printBanner() {
|
|
86
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
87
|
+
console.log(LOGO);
|
|
88
|
+
console.log(paint(' v' + pkg.version, C.bold, C.gold) + paint(' Leaving Blank Space Agent', C.dim));
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 初始化检查 ─────────────────────────────────────────────────
|
|
93
|
+
|
|
30
94
|
function isInitialized() {
|
|
31
|
-
return fs.existsSync(INIT_FLAG) && fs.existsSync(
|
|
95
|
+
return fs.existsSync(INIT_FLAG) && fs.existsSync(VENV_PYTHON);
|
|
32
96
|
}
|
|
33
97
|
|
|
34
|
-
// 首次运行自动初始化
|
|
35
98
|
function ensureInitialized() {
|
|
36
99
|
if (isInitialized()) return true;
|
|
37
100
|
|
|
38
|
-
console.log('
|
|
39
|
-
console.log('
|
|
101
|
+
console.log(paint('⚙ ', C.gold) + paint('首次运行,正在初始化 LBS Agent...', C.bold));
|
|
102
|
+
console.log(paint(' 状态目录: ', C.dim) + paint(STATE_ROOT, C.cyan));
|
|
103
|
+
console.log(paint(' 创建 Python 虚拟环境 + 安装依赖(约 1-2 分钟)', C.dim));
|
|
104
|
+
console.log();
|
|
40
105
|
|
|
41
106
|
if (!fs.existsSync(INIT_SCRIPT)) {
|
|
42
|
-
console.error('
|
|
43
|
-
console.error(' 请手动运行: python -m venv venv && venv/bin/pip install -r requirements.txt');
|
|
107
|
+
console.error(paint('✖ ', C.red) + '初始化脚本不存在: ' + INIT_SCRIPT);
|
|
44
108
|
return false;
|
|
45
109
|
}
|
|
46
110
|
|
|
47
111
|
const result = spawnSync('node', [INIT_SCRIPT], {
|
|
48
112
|
cwd: PKG_ROOT,
|
|
49
113
|
stdio: 'inherit',
|
|
50
|
-
env: { ...process.env, LBS_PKG_ROOT: PKG_ROOT },
|
|
114
|
+
env: { ...process.env, LBS_STATE_ROOT: STATE_ROOT, LBS_PKG_ROOT: PKG_ROOT },
|
|
51
115
|
});
|
|
52
116
|
|
|
53
117
|
if (result.status !== 0) {
|
|
54
|
-
console.error('\n
|
|
55
|
-
console.error(' python -m venv venv');
|
|
56
|
-
console.error(' ' + (process.platform === 'win32' ? 'venv\\Scripts\\pip' : 'venv/bin/pip') + ' install -r requirements.txt');
|
|
118
|
+
console.error(paint('\n✖ ', C.red) + '初始化失败');
|
|
57
119
|
return false;
|
|
58
120
|
}
|
|
59
121
|
|
|
60
|
-
console.log('\n
|
|
122
|
+
console.log(paint('\n✓ ', C.green) + paint('初始化完成!', C.bold, C.green));
|
|
123
|
+
console.log();
|
|
61
124
|
return true;
|
|
62
125
|
}
|
|
63
126
|
|
|
64
|
-
// 主逻辑
|
|
127
|
+
// ── 主逻辑 ─────────────────────────────────────────────────────
|
|
128
|
+
|
|
65
129
|
const args = process.argv.slice(2);
|
|
130
|
+
const subcmd = args[0];
|
|
66
131
|
|
|
67
|
-
//
|
|
68
|
-
if (
|
|
132
|
+
// 不需要初始化的命令
|
|
133
|
+
if (subcmd === 'init' || subcmd === '--init') {
|
|
134
|
+
printBanner();
|
|
69
135
|
if (fs.existsSync(INIT_SCRIPT)) {
|
|
70
|
-
const result = spawnSync('node', [INIT_SCRIPT], {
|
|
136
|
+
const result = spawnSync('node', [INIT_SCRIPT], {
|
|
137
|
+
cwd: PKG_ROOT,
|
|
138
|
+
stdio: 'inherit',
|
|
139
|
+
env: { ...process.env, LBS_STATE_ROOT: STATE_ROOT, LBS_PKG_ROOT: PKG_ROOT },
|
|
140
|
+
});
|
|
71
141
|
process.exit(result.status || 0);
|
|
72
142
|
}
|
|
73
|
-
console.error('初始化脚本不存在');
|
|
143
|
+
console.error(paint('✖ ', C.red) + '初始化脚本不存在');
|
|
74
144
|
process.exit(1);
|
|
75
145
|
}
|
|
76
146
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
147
|
+
if (subcmd === 'version' || subcmd === '--version' || subcmd === '-v') {
|
|
148
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
149
|
+
console.log(`lbs ${pkg.version}`);
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (subcmd === 'doctor' || subcmd === '--doctor') {
|
|
154
|
+
// doctor 用系统 Python,不需要完整初始化
|
|
155
|
+
const sysPython = process.env.LBS_PYTHON || (process.platform === 'win32' ? 'python' : 'python3');
|
|
82
156
|
const result = spawnSync(sysPython, [path.join(PKG_ROOT, 'lbs.py'), 'doctor'], {
|
|
83
|
-
cwd:
|
|
157
|
+
cwd: process.cwd(),
|
|
84
158
|
stdio: 'inherit',
|
|
159
|
+
env: { ...process.env, LBS_HOME: STATE_ROOT },
|
|
85
160
|
});
|
|
86
161
|
process.exit(result.status || 0);
|
|
87
162
|
}
|
|
88
163
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
164
|
+
if (subcmd === 'setup' || subcmd === '--setup') {
|
|
165
|
+
// setup 需要初始化
|
|
166
|
+
if (!ensureInitialized()) process.exit(1);
|
|
167
|
+
const result = spawnSync(VENV_PYTHON, [path.join(PKG_ROOT, 'lbs.py'), 'setup', ...args.slice(1)], {
|
|
168
|
+
cwd: process.cwd(),
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
env: { ...process.env, LBS_HOME: STATE_ROOT },
|
|
171
|
+
});
|
|
172
|
+
process.exit(result.status || 0);
|
|
93
173
|
}
|
|
94
174
|
|
|
95
|
-
//
|
|
175
|
+
// 默认:确保初始化后用 venv Python 执行
|
|
96
176
|
if (!ensureInitialized()) {
|
|
97
177
|
process.exit(1);
|
|
98
178
|
}
|
|
99
179
|
|
|
100
|
-
|
|
101
|
-
const child = spawn(PYTHON, [path.join(PKG_ROOT, 'lbs.py'), ...args], {
|
|
180
|
+
const child = spawn(VENV_PYTHON, [path.join(PKG_ROOT, 'lbs.py'), ...args], {
|
|
102
181
|
cwd: process.cwd(),
|
|
103
182
|
stdio: 'inherit',
|
|
104
|
-
env: process.env,
|
|
183
|
+
env: { ...process.env, LBS_HOME: STATE_ROOT },
|
|
105
184
|
});
|
|
106
185
|
|
|
107
186
|
child.on('close', (code) => {
|
package/config/state_root.py
CHANGED
|
@@ -2,20 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
提供统一的 resolve_state_root() 函数,
|
|
4
4
|
被多个模块用于定位 session/memory/cron 等运行时数据的存储位置。
|
|
5
|
+
|
|
6
|
+
架构对标 Hermes:
|
|
7
|
+
包代码 (只读) ← pip/npm 安装的目录
|
|
8
|
+
state root (可写) ← %LOCALAPPDATA%/lbs-agent (Windows) 或 ~/.lbs-agent (其他)
|
|
9
|
+
|
|
10
|
+
这样 venv/config/sessions 全在 state root,
|
|
11
|
+
升级/重装包不会丢失运行时状态。
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from __future__ import annotations
|
|
8
15
|
|
|
9
16
|
import os
|
|
17
|
+
import sys
|
|
10
18
|
from pathlib import Path
|
|
11
19
|
|
|
12
20
|
|
|
21
|
+
def _platform_default_state_root() -> Path:
|
|
22
|
+
"""返回平台默认的 state root 路径 (对标 Hermes _get_platform_default_hermes_home)。"""
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
local_appdata = os.environ.get("LOCALAPPDATA", "").strip()
|
|
25
|
+
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
|
|
26
|
+
return base / "lbs-agent"
|
|
27
|
+
return Path.home() / ".lbs-agent"
|
|
28
|
+
|
|
29
|
+
|
|
13
30
|
def resolve_state_root(project_root: str | Path | None = None) -> Path:
|
|
14
31
|
"""解析状态根目录。优先级:
|
|
15
32
|
|
|
16
33
|
1. LBS_HOME 环境变量
|
|
17
34
|
2. project_root / .lbs-agent(如果传了 project_root)
|
|
18
|
-
3.
|
|
35
|
+
3. 平台默认:Windows → %LOCALAPPDATA%/lbs-agent, 其他 → ~/.lbs-agent
|
|
36
|
+
|
|
19
37
|
返回 Path 对象,调用者可直接使用 .exists() / / 等 Path 操作。
|
|
20
38
|
"""
|
|
21
39
|
env = os.environ.get("LBS_HOME")
|
|
@@ -25,4 +43,4 @@ def resolve_state_root(project_root: str | Path | None = None) -> Path:
|
|
|
25
43
|
if project_root:
|
|
26
44
|
return Path(str(project_root)).resolve() / ".lbs-agent"
|
|
27
45
|
|
|
28
|
-
return
|
|
46
|
+
return _platform_default_state_root()
|
|
@@ -25,12 +25,15 @@ from config.config import AppConfig
|
|
|
25
25
|
from interfaces.cli._impl.commands.common import project_root
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
BANNER = """\033[1;
|
|
29
|
-
|
|
30
|
-
║
|
|
31
|
-
|
|
32
|
-
║
|
|
33
|
-
|
|
28
|
+
BANNER = """\033[1;38;2;255;191;0m
|
|
29
|
+
\033[1;36m╔══╗\033[0m \033[1;36m╔═══╗\033[0m \033[1;36m╔═════╗\033[0m
|
|
30
|
+
\033[1;36m║ ║\033[0m \033[1;36m║ ║\033[0m \033[1;36m║ ╔═╝\033[0m
|
|
31
|
+
\033[1;36m╠══╣\033[0m \033[1;36m╠═══╣\033[0m \033[1;36m║ ╚═╗\033[0m
|
|
32
|
+
\033[1;36m║ ║\033[0m \033[1;36m║ ║\033[0m \033[1;36m╚═════╝\033[0m
|
|
33
|
+
\033[1;36m╚══╝\033[0m \033[1;36m╚═══╝\033[0m \033[2mAGENT\033[0m
|
|
34
|
+
\033[0m
|
|
35
|
+
\033[2mInteractive Chat · /help for commands · /exit to quit\033[0m
|
|
36
|
+
"""
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
@dataclass
|
|
@@ -62,7 +65,9 @@ def _resolve_agent(config: AppConfig) -> str:
|
|
|
62
65
|
|
|
63
66
|
def _print_turn_header(state: ChatState) -> None:
|
|
64
67
|
"""每次 turn 的 header(agent + turn 编号)。"""
|
|
65
|
-
|
|
68
|
+
agent_label = f"\033[1;36m{state.agent_name}\033[0m"
|
|
69
|
+
turn_label = f"\033[2mturn {state.turn_count}\033[0m"
|
|
70
|
+
print(f"\n{agent_label} {turn_label} \033[2m›\033[0m ", end="", flush=True)
|
|
66
71
|
|
|
67
72
|
|
|
68
73
|
def _print_help() -> None:
|
|
@@ -126,15 +131,18 @@ def _cmd_history(state: ChatState, n: int = 5) -> None:
|
|
|
126
131
|
|
|
127
132
|
def _cmd_status(state: ChatState) -> None:
|
|
128
133
|
elapsed = int(time.time() - state.started_at)
|
|
129
|
-
|
|
130
|
-
\033[
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
rows = [
|
|
135
|
+
("session", state.session_id or "\033[2m(unsaved)\033[0m"),
|
|
136
|
+
("agent", state.agent_name),
|
|
137
|
+
("provider", state.provider_name or "\033[2m(default)\033[0m"),
|
|
138
|
+
("model", state.model_name or "\033[2m(default)\033[0m"),
|
|
139
|
+
("turns", str(state.turn_count)),
|
|
140
|
+
("elapsed", f"{elapsed}s"),
|
|
141
|
+
]
|
|
142
|
+
print()
|
|
143
|
+
for label, val in rows:
|
|
144
|
+
print(f" \033[2m{label:9}\033[0m │ {val}")
|
|
145
|
+
print()
|
|
138
146
|
|
|
139
147
|
|
|
140
148
|
def _handle_command(state: ChatState, line: str) -> bool:
|
|
@@ -207,20 +215,21 @@ def _run_turn(state: ChatState, user_input: str) -> None:
|
|
|
207
215
|
# 显示结果
|
|
208
216
|
if result.success:
|
|
209
217
|
content = (result.content or "").strip()
|
|
210
|
-
print(f"\n\033[1;
|
|
218
|
+
print(f"\n\033[1;37m{content}\033[0m")
|
|
211
219
|
else:
|
|
212
220
|
err = result.error or "(无错误信息)"
|
|
213
|
-
print(f"\n\033[1;31m✗
|
|
214
|
-
|
|
221
|
+
print(f"\n\033[1;31m✗ {err[:300]}\033[0m")
|
|
222
|
+
# 状态行
|
|
223
|
+
print(f"\033[2m {elapsed:.1f}s │ iter {result.iterations} │ tools {result.total_tool_calls}\033[0m")
|
|
215
224
|
# 保存 session_id(Orchestrator 内部会创建/复用)
|
|
216
225
|
if not state.session_id:
|
|
217
226
|
state.session_id = result.session_id if hasattr(result, "session_id") and result.session_id else f"chat-{int(state.started_at)}"
|
|
218
227
|
except KeyboardInterrupt:
|
|
219
|
-
print("\n\033[1;33m
|
|
228
|
+
print("\n\033[1;33m ⏸ 已中断\033[0m")
|
|
220
229
|
except Exception as e:
|
|
221
230
|
elapsed = time.time() - t0
|
|
222
|
-
print(f"\n\033[1;31m✗
|
|
223
|
-
print(f"\033[2m
|
|
231
|
+
print(f"\n\033[1;31m ✗ {type(e).__name__}: {str(e)[:200]}\033[0m")
|
|
232
|
+
print(f"\033[2m {elapsed:.1f}s\033[0m")
|
|
224
233
|
|
|
225
234
|
|
|
226
235
|
def cmd_chat(
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""LBS Agent 交互式配置向导 (对标 Hermes setup wizard)。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
lbs setup — 完整向导
|
|
5
|
+
lbs setup model — 只配置模型/供应商
|
|
6
|
+
lbs setup agent — 只配置 Agent 参数
|
|
7
|
+
lbs setup keys — 只配置 API Keys
|
|
8
|
+
|
|
9
|
+
配置写入 state_root (LBS_HOME),不修改包目录里的出厂 models.yaml/agents.yaml。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import yaml
|
|
21
|
+
except ImportError:
|
|
22
|
+
yaml = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ═══════════════════════════════════════════════════════════════
|
|
26
|
+
# State Root 解析 (不 import config.state_root, 架构边界要求)
|
|
27
|
+
# ═══════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
def _resolve_state_root() -> Path:
|
|
30
|
+
"""解析状态根目录。优先级: LBS_HOME 环境变量 → 平台默认。"""
|
|
31
|
+
env = os.environ.get("LBS_HOME")
|
|
32
|
+
if env:
|
|
33
|
+
return Path(env).expanduser().resolve()
|
|
34
|
+
if sys.platform == "win32":
|
|
35
|
+
local_appdata = os.environ.get("LOCALAPPDATA", "").strip()
|
|
36
|
+
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
|
|
37
|
+
return base / "lbs-agent"
|
|
38
|
+
return Path.home() / ".lbs-agent"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════
|
|
42
|
+
# 颜色与 UI 工具 (对标 Hermes colors.py)
|
|
43
|
+
# ═══════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
class _C:
|
|
46
|
+
"""ANSI 颜色码。"""
|
|
47
|
+
RESET = "\033[0m"
|
|
48
|
+
BOLD = "\033[1m"
|
|
49
|
+
DIM = "\033[2m"
|
|
50
|
+
RED = "\033[31m"
|
|
51
|
+
GREEN = "\033[32m"
|
|
52
|
+
YELLOW = "\033[33m"
|
|
53
|
+
BLUE = "\033[34m"
|
|
54
|
+
MAGENTA = "\033[35m"
|
|
55
|
+
CYAN = "\033[36m"
|
|
56
|
+
GOLD = "\033[1;38;2;255;191;0m"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _use_color() -> bool:
|
|
60
|
+
if os.environ.get("NO_COLOR"):
|
|
61
|
+
return False
|
|
62
|
+
if os.environ.get("TERM") == "dumb":
|
|
63
|
+
return False
|
|
64
|
+
return sys.stdout.isatty()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _paint(text: str, *codes: str) -> str:
|
|
68
|
+
if not _use_color():
|
|
69
|
+
return text
|
|
70
|
+
return "".join(codes) + text + _C.RESET
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _header(title: str) -> None:
|
|
74
|
+
"""打印章节标题 (带边框)。"""
|
|
75
|
+
print()
|
|
76
|
+
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.MAGENTA))
|
|
77
|
+
print(_paint(" │ ", _C.MAGENTA) + _paint(f"⚙ LBS Setup — {title}", _C.BOLD, _C.GOLD)
|
|
78
|
+
+ _paint(" " * max(0, 47 - len(title) - 14) + "│", _C.MAGENTA))
|
|
79
|
+
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.MAGENTA))
|
|
80
|
+
print()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _info(msg: str) -> None:
|
|
84
|
+
print(_paint(" ℹ ", _C.BLUE) + msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _success(msg: str) -> None:
|
|
88
|
+
print(_paint(" ✓ ", _C.GREEN) + _paint(msg, _C.GREEN))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _warn(msg: str) -> None:
|
|
92
|
+
print(_paint(" ⚠ ", _C.YELLOW) + _paint(msg, _C.YELLOW))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _error(msg: str) -> None:
|
|
96
|
+
print(_paint(" ✖ ", _C.RED) + _paint(msg, _C.RED))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _prompt(label: str, default: str = "") -> str:
|
|
100
|
+
"""带默认值的输入提示。"""
|
|
101
|
+
suffix = f" [{default}]" if default else ""
|
|
102
|
+
try:
|
|
103
|
+
val = input(_paint(f" ▸ {label}{suffix}: ", _C.CYAN)).strip()
|
|
104
|
+
except (EOFError, KeyboardInterrupt):
|
|
105
|
+
print()
|
|
106
|
+
return default
|
|
107
|
+
return val or default
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _prompt_choice(label: str, options: list[str], default: str = "") -> str:
|
|
111
|
+
"""带选项列表的输入提示。"""
|
|
112
|
+
print(_paint(f" ▸ {label}", _C.CYAN))
|
|
113
|
+
for i, opt in enumerate(options, 1):
|
|
114
|
+
marker = _paint("→", _C.GREEN) if opt == default else " "
|
|
115
|
+
print(f" {marker} {i}. {opt}")
|
|
116
|
+
val = _prompt("选择编号或直接输入", default)
|
|
117
|
+
if val.isdigit():
|
|
118
|
+
idx = int(val) - 1
|
|
119
|
+
if 0 <= idx < len(options):
|
|
120
|
+
return options[idx]
|
|
121
|
+
return val
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _prompt_password(label: str) -> str:
|
|
125
|
+
"""密码输入(不回显)。"""
|
|
126
|
+
import getpass
|
|
127
|
+
try:
|
|
128
|
+
val = getpass.getpass(_paint(f" ▸ {label}: ", _C.CYAN))
|
|
129
|
+
except (EOFError, KeyboardInterrupt):
|
|
130
|
+
print()
|
|
131
|
+
return ""
|
|
132
|
+
return val.strip()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _prompt_yes_no(label: str, default: bool = True) -> bool:
|
|
136
|
+
"""是/否选择。"""
|
|
137
|
+
suffix = "Y/n" if default else "y/N"
|
|
138
|
+
val = _prompt(f"{label} ({suffix})", "y" if default else "n")
|
|
139
|
+
return val.lower().startswith("y")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ═══════════════════════════════════════════════════════════════
|
|
143
|
+
# 配置文件读写
|
|
144
|
+
# ═══════════════════════════════════════════════════════════════
|
|
145
|
+
|
|
146
|
+
def _state_root() -> Path:
|
|
147
|
+
return _resolve_state_root()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _models_yaml_path() -> Path:
|
|
151
|
+
"""用户级 models.yaml 覆盖文件。"""
|
|
152
|
+
return _state_root() / "models.override.yaml"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _agents_yaml_path() -> Path:
|
|
156
|
+
"""用户级 agents.yaml 覆盖文件。"""
|
|
157
|
+
return _state_root() / "agents.override.yaml"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _env_path() -> Path:
|
|
161
|
+
""".env 文件,存 API keys。"""
|
|
162
|
+
return _state_root() / ".env"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
166
|
+
if not yaml or not path.exists():
|
|
167
|
+
return {}
|
|
168
|
+
try:
|
|
169
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
170
|
+
return yaml.safe_load(f) or {}
|
|
171
|
+
except Exception:
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _save_yaml(path: Path, data: dict[str, Any]) -> None:
|
|
176
|
+
if not yaml:
|
|
177
|
+
_error("PyYAML 未安装,无法保存配置")
|
|
178
|
+
return
|
|
179
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
181
|
+
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _load_env() -> dict[str, str]:
|
|
185
|
+
env: dict[str, str] = {}
|
|
186
|
+
path = _env_path()
|
|
187
|
+
if path.exists():
|
|
188
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
189
|
+
line = line.strip()
|
|
190
|
+
if line and not line.startswith("#") and "=" in line:
|
|
191
|
+
key, _, val = line.partition("=")
|
|
192
|
+
env[key.strip()] = val.strip()
|
|
193
|
+
return env
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _save_env(env: dict[str, str]) -> None:
|
|
197
|
+
path = _env_path()
|
|
198
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
lines = [f"{k}={v}" for k, v in sorted(env.items())]
|
|
200
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ═══════════════════════════════════════════════════════════════
|
|
204
|
+
# 内置供应商目录
|
|
205
|
+
# ═══════════════════════════════════════════════════════════════
|
|
206
|
+
|
|
207
|
+
_PROVIDERS = {
|
|
208
|
+
"openai": {
|
|
209
|
+
"display": "OpenAI",
|
|
210
|
+
"env_key": "OPENAI_API_KEY",
|
|
211
|
+
"base_url": "https://api.openai.com/v1",
|
|
212
|
+
"api_mode": "chat_completions",
|
|
213
|
+
"models": ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-4.1", "o3-mini"],
|
|
214
|
+
},
|
|
215
|
+
"anthropic": {
|
|
216
|
+
"display": "Anthropic",
|
|
217
|
+
"env_key": "ANTHROPIC_API_KEY",
|
|
218
|
+
"base_url": "https://api.anthropic.com",
|
|
219
|
+
"api_mode": "anthropic_messages",
|
|
220
|
+
"models": ["claude-opus-4.6", "claude-sonnet-4.6", "claude-sonnet-4.5", "claude-haiku-4.5"],
|
|
221
|
+
},
|
|
222
|
+
"deepseek": {
|
|
223
|
+
"display": "DeepSeek",
|
|
224
|
+
"env_key": "DEEPSEEK_API_KEY",
|
|
225
|
+
"base_url": "https://api.deepseek.com/v1",
|
|
226
|
+
"api_mode": "chat_completions",
|
|
227
|
+
"models": ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-chat", "deepseek-reasoner"],
|
|
228
|
+
},
|
|
229
|
+
"openrouter": {
|
|
230
|
+
"display": "OpenRouter (聚合 300+ 模型)",
|
|
231
|
+
"env_key": "OPENROUTER_API_KEY",
|
|
232
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
233
|
+
"api_mode": "chat_completions",
|
|
234
|
+
"models": ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3-pro-preview"],
|
|
235
|
+
},
|
|
236
|
+
"qwen": {
|
|
237
|
+
"display": "通义千问 (阿里云)",
|
|
238
|
+
"env_key": "QWEN_API_KEY",
|
|
239
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
240
|
+
"api_mode": "chat_completions",
|
|
241
|
+
"models": ["qwen3.7-max", "qwen3.6-plus", "qwen3.5-plus", "qwen-max", "qwen-plus"],
|
|
242
|
+
},
|
|
243
|
+
"zai": {
|
|
244
|
+
"display": "智谱 AI (GLM)",
|
|
245
|
+
"env_key": "ZAI_API_KEY",
|
|
246
|
+
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
247
|
+
"api_mode": "chat_completions",
|
|
248
|
+
"models": ["glm-5.2", "glm-5.1", "glm-5", "glm-4.7", "glm-4-plus", "glm-4-flash"],
|
|
249
|
+
},
|
|
250
|
+
"gemini": {
|
|
251
|
+
"display": "Google Gemini",
|
|
252
|
+
"env_key": "GEMINI_API_KEY",
|
|
253
|
+
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
254
|
+
"api_mode": "chat_completions",
|
|
255
|
+
"models": ["gemini-3-pro-preview", "gemini-3-flash-preview", "gemini-2.5-pro"],
|
|
256
|
+
},
|
|
257
|
+
"kimi": {
|
|
258
|
+
"display": "Kimi (月之暗面)",
|
|
259
|
+
"env_key": "KIMI_API_KEY",
|
|
260
|
+
"base_url": "https://api.moonshot.cn/v1",
|
|
261
|
+
"api_mode": "chat_completions",
|
|
262
|
+
"models": ["kimi-k2.6", "kimi-k2.5", "moonshot-v1-128k"],
|
|
263
|
+
},
|
|
264
|
+
"custom": {
|
|
265
|
+
"display": "自定义供应商 (兼容 OpenAI/Anthropic API)",
|
|
266
|
+
"env_key": "",
|
|
267
|
+
"base_url": "",
|
|
268
|
+
"api_mode": "chat_completions",
|
|
269
|
+
"models": [],
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ═══════════════════════════════════════════════════════════════
|
|
275
|
+
# Step 1: 模型 & 供应商配置
|
|
276
|
+
# ═══════════════════════════════════════════════════════════════
|
|
277
|
+
|
|
278
|
+
def setup_model_provider() -> None:
|
|
279
|
+
"""配置推理供应商和默认模型。"""
|
|
280
|
+
_header("模型 & 供应商")
|
|
281
|
+
|
|
282
|
+
provider_keys = list(_PROVIDERS.keys())
|
|
283
|
+
provider_displays = [f"{_PROVIDERS[k]['display']}" for k in provider_keys]
|
|
284
|
+
|
|
285
|
+
_info("选择你的 LLM 供应商(决定 API 端点和认证方式)")
|
|
286
|
+
_info(f" 文档: https://lbs-agent.nousresearch.com/docs/providers")
|
|
287
|
+
print()
|
|
288
|
+
|
|
289
|
+
choice = _prompt_choice("选择供应商", provider_displays, default=provider_displays[0])
|
|
290
|
+
idx = provider_displays.index(choice) if choice in provider_displays else 0
|
|
291
|
+
provider_key = provider_keys[idx]
|
|
292
|
+
provider = _PROVIDERS[provider_key]
|
|
293
|
+
|
|
294
|
+
print()
|
|
295
|
+
_info(f"已选: {provider['display']}")
|
|
296
|
+
|
|
297
|
+
# 输入 API Key
|
|
298
|
+
env = _load_env()
|
|
299
|
+
existing_key = env.get(provider["env_key"], "") if provider["env_key"] else ""
|
|
300
|
+
if provider["env_key"]:
|
|
301
|
+
print()
|
|
302
|
+
if existing_key:
|
|
303
|
+
_info(f"已有 API Key: {existing_key[:8]}...{existing_key[-4:]}")
|
|
304
|
+
if not _prompt_yes_no("是否更换 Key?", default=False):
|
|
305
|
+
pass
|
|
306
|
+
else:
|
|
307
|
+
key = _prompt_password(f"输入 {provider['display']} API Key")
|
|
308
|
+
if key:
|
|
309
|
+
env[provider["env_key"]] = key
|
|
310
|
+
else:
|
|
311
|
+
key = _prompt_password(f"输入 {provider['display']} API Key")
|
|
312
|
+
if key:
|
|
313
|
+
env[provider["env_key"]] = key
|
|
314
|
+
_success("API Key 已保存")
|
|
315
|
+
|
|
316
|
+
# 自定义供应商需要额外信息
|
|
317
|
+
if provider_key == "custom":
|
|
318
|
+
print()
|
|
319
|
+
custom_name = _prompt("供应商名称 (如 myapi)", "custom")
|
|
320
|
+
custom_base = _prompt("API Base URL", "")
|
|
321
|
+
custom_mode = _prompt_choice("API 模式", ["chat_completions", "anthropic_messages"], "chat_completions")
|
|
322
|
+
custom_key_env = f"{custom_name.upper()}_API_KEY"
|
|
323
|
+
key = _prompt_password(f"输入 API Key")
|
|
324
|
+
if key:
|
|
325
|
+
env[custom_key_env] = key
|
|
326
|
+
|
|
327
|
+
provider = {
|
|
328
|
+
"display": custom_name,
|
|
329
|
+
"env_key": custom_key_env,
|
|
330
|
+
"base_url": custom_base,
|
|
331
|
+
"api_mode": custom_mode,
|
|
332
|
+
"models": [],
|
|
333
|
+
}
|
|
334
|
+
provider_key = custom_name
|
|
335
|
+
|
|
336
|
+
# 选择模型
|
|
337
|
+
print()
|
|
338
|
+
models = provider["models"]
|
|
339
|
+
if models:
|
|
340
|
+
_info(f"可选模型:")
|
|
341
|
+
model = _prompt_choice("选择默认模型", models, default=models[0])
|
|
342
|
+
else:
|
|
343
|
+
model = _prompt("输入模型名称", "")
|
|
344
|
+
|
|
345
|
+
# 写入 override YAML
|
|
346
|
+
override = _load_yaml(_models_yaml_path())
|
|
347
|
+
|
|
348
|
+
# 写入 default
|
|
349
|
+
override["default"] = {
|
|
350
|
+
"provider": provider_key,
|
|
351
|
+
"model": model,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# 写入 builtin/custom 供应商
|
|
355
|
+
if provider_key == "custom" and provider_key not in _PROVIDERS:
|
|
356
|
+
custom_section = override.setdefault("custom", {})
|
|
357
|
+
custom_section[provider_key] = {
|
|
358
|
+
"display_name": provider["display"],
|
|
359
|
+
"base_url": provider["base_url"],
|
|
360
|
+
"api_mode": provider["api_mode"],
|
|
361
|
+
"api_key": f"${{{provider['env_key']}}}" if provider["env_key"] else "",
|
|
362
|
+
"models": [model] if model else [],
|
|
363
|
+
}
|
|
364
|
+
else:
|
|
365
|
+
builtin_section = override.setdefault("builtin", {})
|
|
366
|
+
if provider_key not in builtin_section:
|
|
367
|
+
builtin_section[provider_key] = {
|
|
368
|
+
"api_key": f"${{{provider['env_key']}}}" if provider["env_key"] else "",
|
|
369
|
+
"base_url": provider["base_url"],
|
|
370
|
+
"api_mode": provider["api_mode"],
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_save_yaml(_models_yaml_path(), override)
|
|
374
|
+
_save_env(env)
|
|
375
|
+
|
|
376
|
+
print()
|
|
377
|
+
_success(f"模型配置已保存: {_models_yaml_path()}")
|
|
378
|
+
_success(f"默认模型: {provider_key} / {model}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ═══════════════════════════════════════════════════════════════
|
|
382
|
+
# Step 2: Agent 参数
|
|
383
|
+
# ═══════════════════════════════════════════════════════════════
|
|
384
|
+
|
|
385
|
+
def setup_agent_settings() -> None:
|
|
386
|
+
"""配置 Agent 运行参数。"""
|
|
387
|
+
_header("Agent 设置")
|
|
388
|
+
|
|
389
|
+
override = _load_yaml(_agents_yaml_path())
|
|
390
|
+
global_cfg = override.get("global", {})
|
|
391
|
+
|
|
392
|
+
_info("配置 Agent 全局参数")
|
|
393
|
+
print()
|
|
394
|
+
|
|
395
|
+
max_concurrent = int(_prompt("最大并发 Agent 数", str(global_cfg.get("max_concurrent", 3))))
|
|
396
|
+
max_depth = int(_prompt("最大嵌套深度", str(global_cfg.get("max_spawn_depth", 1))))
|
|
397
|
+
default_timeout = int(_prompt("默认超时 (秒)", str(global_cfg.get("default_timeout", 600))))
|
|
398
|
+
auto_approve = _prompt_yes_no("自动批准危险操作 (不推荐)", default=global_cfg.get("auto_approve_dangerous", False))
|
|
399
|
+
|
|
400
|
+
override["global"] = {
|
|
401
|
+
"max_concurrent": max_concurrent,
|
|
402
|
+
"max_spawn_depth": max_depth,
|
|
403
|
+
"default_timeout": default_timeout,
|
|
404
|
+
"auto_approve_dangerous": auto_approve,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_save_yaml(_agents_yaml_path(), override)
|
|
408
|
+
print()
|
|
409
|
+
_success(f"Agent 配置已保存: {_agents_yaml_path()}")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ═══════════════════════════════════════════════════════════════
|
|
413
|
+
# Step 3: API Keys 管理
|
|
414
|
+
# ═══════════════════════════════════════════════════════════════
|
|
415
|
+
|
|
416
|
+
def setup_keys() -> None:
|
|
417
|
+
"""配置所有 API Keys。"""
|
|
418
|
+
_header("API Keys")
|
|
419
|
+
|
|
420
|
+
env = _load_env()
|
|
421
|
+
|
|
422
|
+
known_keys = [
|
|
423
|
+
("OPENAI_API_KEY", "OpenAI"),
|
|
424
|
+
("ANTHROPIC_API_KEY", "Anthropic"),
|
|
425
|
+
("DEEPSEEK_API_KEY", "DeepSeek"),
|
|
426
|
+
("OPENROUTER_API_KEY", "OpenRouter"),
|
|
427
|
+
("QWEN_API_KEY", "通义千问"),
|
|
428
|
+
("ZAI_API_KEY", "智谱 AI"),
|
|
429
|
+
("GEMINI_API_KEY", "Google Gemini"),
|
|
430
|
+
("KIMI_API_KEY", "Kimi"),
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
for env_key, display_name in known_keys:
|
|
434
|
+
existing = env.get(env_key, "")
|
|
435
|
+
if existing:
|
|
436
|
+
_info(f"{display_name}: {existing[:8]}...{existing[-4:]}")
|
|
437
|
+
if not _prompt_yes_no(f" 更换?", default=False):
|
|
438
|
+
continue
|
|
439
|
+
else:
|
|
440
|
+
if not _prompt_yes_no(f"配置 {display_name} Key?", default=False):
|
|
441
|
+
continue
|
|
442
|
+
key = _prompt_password(f"输入 {display_name} API Key")
|
|
443
|
+
if key:
|
|
444
|
+
env[env_key] = key
|
|
445
|
+
_success(f"{display_name} Key 已保存")
|
|
446
|
+
print()
|
|
447
|
+
|
|
448
|
+
_save_env(env)
|
|
449
|
+
_success(f"Keys 已保存到: {_env_path()}")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ═══════════════════════════════════════════════════════════════
|
|
453
|
+
# 主入口
|
|
454
|
+
# ═══════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
SECTIONS = [
|
|
457
|
+
("model", "模型 & 供应商", setup_model_provider),
|
|
458
|
+
("agent", "Agent 设置", setup_agent_settings),
|
|
459
|
+
("keys", "API Keys", setup_keys),
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def run_setup(section: str | None = None) -> int:
|
|
464
|
+
"""运行配置向导。"""
|
|
465
|
+
# 确保 state root 存在
|
|
466
|
+
root = _state_root()
|
|
467
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
468
|
+
|
|
469
|
+
print()
|
|
470
|
+
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.GOLD))
|
|
471
|
+
print(_paint(" │ ", _C.GOLD) + _paint("⚙ LBS Agent Setup Wizard", _C.BOLD, _C.GOLD)
|
|
472
|
+
+ _paint(" │", _C.GOLD))
|
|
473
|
+
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.GOLD))
|
|
474
|
+
print()
|
|
475
|
+
_info(f"配置目录: {root}")
|
|
476
|
+
print()
|
|
477
|
+
|
|
478
|
+
if section:
|
|
479
|
+
# 只运行指定 section
|
|
480
|
+
for key, label, func in SECTIONS:
|
|
481
|
+
if key == section:
|
|
482
|
+
func()
|
|
483
|
+
_print_done()
|
|
484
|
+
return 0
|
|
485
|
+
_error(f"未知配置项: {section}")
|
|
486
|
+
_info(f"可用项: {', '.join(k for k, _, _ in SECTIONS)}")
|
|
487
|
+
return 1
|
|
488
|
+
|
|
489
|
+
# 完整向导
|
|
490
|
+
for key, label, func in SECTIONS:
|
|
491
|
+
func()
|
|
492
|
+
|
|
493
|
+
_print_done()
|
|
494
|
+
return 0
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _print_done() -> None:
|
|
498
|
+
print()
|
|
499
|
+
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.GREEN))
|
|
500
|
+
print(_paint(" │ ", _C.GREEN) + _paint("✅ 配置完成!", _C.BOLD, _C.GREEN)
|
|
501
|
+
+ _paint(" │", _C.GREEN))
|
|
502
|
+
print(_paint(" │ │", _C.GREEN))
|
|
503
|
+
print(_paint(" │ ", _C.GREEN) + _paint("配置文件:", _C.BOLD) + paint_path(_models_yaml_path()) + _paint(" │", _C.GREEN))
|
|
504
|
+
print(_paint(" │ ", _C.GREEN) + _paint("密钥文件:", _C.BOLD) + paint_path(_env_path()) + _paint(" │", _C.GREEN))
|
|
505
|
+
print(_paint(" │ │", _C.GREEN))
|
|
506
|
+
print(_paint(" │ ", _C.GREEN) + _paint("现在可以运行:", _C.BOLD) + paint_cmd(" lbs status") + _paint(" │", _C.GREEN))
|
|
507
|
+
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.GREEN))
|
|
508
|
+
print()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def paint_path(p: Path) -> str:
|
|
512
|
+
s = str(p)
|
|
513
|
+
if len(s) > 40:
|
|
514
|
+
s = "..." + s[-37:]
|
|
515
|
+
return _paint(s, _C.CYAN)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def paint_cmd(cmd: str) -> str:
|
|
519
|
+
return _paint(cmd, _C.CYAN, _C.BOLD)
|
|
@@ -50,6 +50,7 @@ from interfaces.cli._impl.commands.cron import (
|
|
|
50
50
|
)
|
|
51
51
|
from interfaces.cli._impl.commands.soul import cmd_soul_list, cmd_soul_switch, cmd_soul_show
|
|
52
52
|
from interfaces.cli._impl.commands.smoke_test import main as smoke_main
|
|
53
|
+
from interfaces.cli._impl.commands.setup import run_setup
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
# --- 命令注册表 ---
|
|
@@ -657,6 +658,12 @@ def _cmd_doctor_handler(config: Any) -> int:
|
|
|
657
658
|
return 1 if report.get("failed", 0) else 0
|
|
658
659
|
|
|
659
660
|
|
|
661
|
+
def _cmd_setup_handler(_config: Any) -> int:
|
|
662
|
+
"""交互式配置向导。"""
|
|
663
|
+
section = sys.argv[2] if len(sys.argv) > 2 else None
|
|
664
|
+
return run_setup(section)
|
|
665
|
+
|
|
666
|
+
|
|
660
667
|
# --- 注册表实例 ---
|
|
661
668
|
|
|
662
669
|
_REGISTRY = CommandRegistry()
|
|
@@ -689,6 +696,7 @@ def _build_registry() -> None:
|
|
|
689
696
|
_REGISTRY.register("gateway", _cmd_gateway_handler, help="网关操作")
|
|
690
697
|
_REGISTRY.register("doctor", _cmd_doctor_handler, help="诊断配置问题")
|
|
691
698
|
_REGISTRY.register("smoke-test", lambda _config: smoke_main(), aliases=["smoke"], help="快速冒烟测试")
|
|
699
|
+
_REGISTRY.register("setup", _cmd_setup_handler, help="交互式配置向导(model/agent/keys)")
|
|
692
700
|
|
|
693
701
|
|
|
694
702
|
# --- 入口 ---
|
package/package.json
CHANGED
package/scripts/init.js
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* LBS Agent 初始化脚本 — 跨平台 (Windows / macOS / Linux)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 3. 安装 requirements.txt 依赖
|
|
9
|
-
* 4. 创建 .lbs-initialized 标记文件
|
|
5
|
+
* 架构 (对标 Hermes):
|
|
6
|
+
* venv 建在 state root (%LOCALAPPDATA%/lbs-agent/venv), 不在包目录
|
|
7
|
+
* config/sessions 也在 state root, npm 升级不丢数据
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* LBS_PKG_ROOT
|
|
9
|
+
* 环境变量:
|
|
10
|
+
* LBS_STATE_ROOT ← bin/lbs.js 传入的 state root 路径
|
|
11
|
+
* LBS_PKG_ROOT ← bin/lbs.js 传入的包根路径
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
const { spawnSync } = require('child_process');
|
|
@@ -19,31 +17,48 @@ const fs = require('fs');
|
|
|
19
17
|
const os = require('os');
|
|
20
18
|
|
|
21
19
|
const PKG_ROOT = process.env.LBS_PKG_ROOT || path.resolve(__dirname, '..');
|
|
22
|
-
const
|
|
23
|
-
const
|
|
20
|
+
const STATE_ROOT = process.env.LBS_STATE_ROOT || path.join(os.homedir(), '.lbs-agent');
|
|
21
|
+
const VENV_DIR = path.join(STATE_ROOT, 'venv');
|
|
22
|
+
const INIT_FLAG = path.join(STATE_ROOT, '.lbs-initialized');
|
|
24
23
|
const REQUIREMENTS = path.join(PKG_ROOT, 'requirements.txt');
|
|
25
24
|
|
|
26
25
|
const isWin = process.platform === 'win32';
|
|
27
26
|
const PYTHON = isWin ? path.join(VENV_DIR, 'Scripts', 'python.exe') : path.join(VENV_DIR, 'bin', 'python');
|
|
28
27
|
const PIP = isWin ? path.join(VENV_DIR, 'Scripts', 'pip.exe') : path.join(VENV_DIR, 'bin', 'pip');
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// ── 颜色 ───────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const C = {
|
|
32
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
33
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
34
|
+
blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m',
|
|
35
|
+
gold: '\x1b[1;38;2;255;191;0m',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function paint(text, ...colors) {
|
|
39
|
+
if (process.env.NO_COLOR || process.env.TERM === 'dumb' || !process.stdout.isTTY) return text;
|
|
40
|
+
return colors.join('') + text + C.reset;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 工具函数 ───────────────────────────────────────────────────
|
|
32
44
|
|
|
33
45
|
function run(cmd, args, opts = {}) {
|
|
34
|
-
|
|
46
|
+
return spawnSync(cmd, args, {
|
|
35
47
|
stdio: opts.silent ? 'pipe' : 'inherit',
|
|
36
48
|
encoding: 'utf-8',
|
|
37
49
|
shell: isWin,
|
|
38
50
|
...opts,
|
|
39
51
|
});
|
|
40
|
-
return result;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
function ensureDir(dir) {
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Step 1: 检测系统 Python ────────────────────────────────────
|
|
44
59
|
|
|
45
60
|
function findSystemPython() {
|
|
46
|
-
log('🔍 检测系统 Python...');
|
|
61
|
+
console.log(paint(' 🔍 ', C.gold) + '检测系统 Python...');
|
|
47
62
|
|
|
48
63
|
const candidates = isWin
|
|
49
64
|
? ['python', 'python3', 'py -3']
|
|
@@ -53,102 +68,138 @@ function findSystemPython() {
|
|
|
53
68
|
const result = run(cmd, ['--version'], { silent: true, shell: isWin });
|
|
54
69
|
if (result.status === 0) {
|
|
55
70
|
const version = (result.stdout || result.stderr || '').trim();
|
|
56
|
-
log(
|
|
71
|
+
console.log(paint(' ✓ ', C.green) + `${cmd}: ${version}`);
|
|
57
72
|
|
|
58
|
-
// 检查版本 >= 3.10
|
|
59
73
|
const match = version.match(/(\d+)\.(\d+)/);
|
|
60
74
|
if (match) {
|
|
61
75
|
const major = parseInt(match[1]);
|
|
62
76
|
const minor = parseInt(match[2]);
|
|
63
|
-
if (major >= 3 && minor >= 10)
|
|
64
|
-
return cmd;
|
|
65
|
-
}
|
|
77
|
+
if (major >= 3 && minor >= 10) return cmd;
|
|
66
78
|
}
|
|
67
|
-
log(
|
|
79
|
+
console.log(paint(' ⚠ ', C.yellow) + '版本低于 3.10,继续寻找...');
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
err(' Linux: sudo apt install python3 python3-venv');
|
|
83
|
+
console.error(paint('\n ✖ ', C.red) + '未找到 Python 3.10+');
|
|
84
|
+
console.error(paint(' ', C.dim) + 'Windows: https://www.python.org/downloads/');
|
|
85
|
+
console.error(paint(' ', C.dim) + 'macOS: brew install python@3.12');
|
|
86
|
+
console.error(paint(' ', C.dim) + 'Linux: sudo apt install python3 python3-venv');
|
|
76
87
|
return null;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
// ── Step 2:
|
|
90
|
+
// ── Step 2: 创建 state root + venv ─────────────────────────────
|
|
80
91
|
|
|
81
92
|
function createVenv(sysPython) {
|
|
82
93
|
if (fs.existsSync(PYTHON)) {
|
|
83
|
-
log('✓ 虚拟环境已存在,跳过创建');
|
|
94
|
+
console.log(paint(' ✓ ', C.green) + '虚拟环境已存在,跳过创建');
|
|
84
95
|
return true;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
|
|
98
|
+
// 确保 state root 存在
|
|
99
|
+
ensureDir(STATE_ROOT);
|
|
100
|
+
console.log(paint(' 📦 ', C.gold) + `创建 Python 虚拟环境 → ${paint(STATE_ROOT + '/venv', C.cyan)}`);
|
|
101
|
+
|
|
88
102
|
const result = run(sysPython, ['-m', 'venv', VENV_DIR]);
|
|
89
103
|
if (result.status !== 0 || !fs.existsSync(PYTHON)) {
|
|
90
|
-
|
|
104
|
+
console.error(paint(' ✖ ', C.red) + '创建虚拟环境失败');
|
|
91
105
|
return false;
|
|
92
106
|
}
|
|
93
|
-
log(' ✓ venv
|
|
107
|
+
console.log(paint(' ✓ ', C.green) + 'venv 已创建');
|
|
94
108
|
return true;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
// ── Step 3: 安装依赖
|
|
111
|
+
// ── Step 3: 安装依赖 ───────────────────────────────────────────
|
|
98
112
|
|
|
99
113
|
function installDeps() {
|
|
100
114
|
if (!fs.existsSync(REQUIREMENTS)) {
|
|
101
|
-
|
|
115
|
+
console.error(paint(' ✖ ', C.red) + 'requirements.txt 不存在: ' + REQUIREMENTS);
|
|
102
116
|
return false;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
log('📥 安装 Python 依赖 (可能需要 1-2 分钟)...');
|
|
106
|
-
const
|
|
107
|
-
const result = run(pipCmd, ['install', '-r', REQUIREMENTS, '--disable-pip-version-check']);
|
|
119
|
+
console.log(paint(' 📥 ', C.gold) + '安装 Python 依赖 (可能需要 1-2 分钟)...');
|
|
120
|
+
const result = run(PIP, ['install', '-r', REQUIREMENTS, '--disable-pip-version-check']);
|
|
108
121
|
if (result.status !== 0) {
|
|
109
|
-
|
|
122
|
+
console.error(paint(' ✖ ', C.red) + '依赖安装失败');
|
|
110
123
|
return false;
|
|
111
124
|
}
|
|
112
|
-
log(' ✓ 依赖安装完成');
|
|
125
|
+
console.log(paint(' ✓ ', C.green) + '依赖安装完成');
|
|
113
126
|
return true;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
// ── Step 4:
|
|
129
|
+
// ── Step 4: 创建默认目录结构 ───────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function createDefaultDirs() {
|
|
132
|
+
const dirs = ['sessions', 'uploads', 'skills', 'skills/active', 'skills/local', 'approvals'];
|
|
133
|
+
for (const d of dirs) {
|
|
134
|
+
ensureDir(path.join(STATE_ROOT, d));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 默认 config.yaml (如果不存在)
|
|
138
|
+
const configPath = path.join(STATE_ROOT, 'config.yaml');
|
|
139
|
+
if (!fs.existsSync(configPath)) {
|
|
140
|
+
const defaultConfig = [
|
|
141
|
+
'# LBS Agent 配置文件',
|
|
142
|
+
'# 运行 lbs setup 进入交互式配置向导',
|
|
143
|
+
'',
|
|
144
|
+
'model:',
|
|
145
|
+
' provider: ""',
|
|
146
|
+
' name: ""',
|
|
147
|
+
'',
|
|
148
|
+
'agent:',
|
|
149
|
+
' max_iterations: 10',
|
|
150
|
+
' default_agent: chat',
|
|
151
|
+
'',
|
|
152
|
+
'# 自定义供应商列表',
|
|
153
|
+
'custom_providers: []',
|
|
154
|
+
'',
|
|
155
|
+
].join('\n');
|
|
156
|
+
fs.writeFileSync(configPath, defaultConfig, 'utf-8');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(paint(' ✓ ', C.green) + '目录结构已创建');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Step 5: 验证 ───────────────────────────────────────────────
|
|
117
163
|
|
|
118
164
|
function verifyInstall() {
|
|
119
|
-
log('🔍 验证安装...');
|
|
165
|
+
console.log(paint(' 🔍 ', C.gold) + '验证安装...');
|
|
120
166
|
const result = run(PYTHON, [path.join(PKG_ROOT, 'lbs.py'), 'status'], {
|
|
121
167
|
silent: true,
|
|
122
168
|
cwd: PKG_ROOT,
|
|
169
|
+
env: { ...process.env, LBS_HOME: STATE_ROOT },
|
|
123
170
|
});
|
|
124
171
|
if (result.status === 0) {
|
|
125
|
-
log(' ✓ lbs status 运行正常');
|
|
126
|
-
|
|
172
|
+
console.log(paint(' ✓ ', C.green) + 'lbs status 运行正常');
|
|
173
|
+
} else {
|
|
174
|
+
console.log(paint(' ⚠ ', C.yellow) + 'lbs status 运行异常(可能是配置问题,不影响初始化)');
|
|
127
175
|
}
|
|
128
|
-
|
|
129
|
-
return true; // 不阻断
|
|
176
|
+
return true;
|
|
130
177
|
}
|
|
131
178
|
|
|
132
|
-
// ── Step
|
|
179
|
+
// ── Step 6: 写入标记 ───────────────────────────────────────────
|
|
133
180
|
|
|
134
181
|
function writeFlag() {
|
|
135
182
|
fs.writeFileSync(INIT_FLAG, JSON.stringify({
|
|
136
183
|
initialized_at: new Date().toISOString(),
|
|
184
|
+
state_root: STATE_ROOT,
|
|
185
|
+
pkg_root: PKG_ROOT,
|
|
137
186
|
platform: os.platform(),
|
|
138
187
|
node_version: process.version,
|
|
139
188
|
}, null, 2));
|
|
140
|
-
log(' ✓ .lbs-initialized 已创建');
|
|
189
|
+
console.log(paint(' ✓ ', C.green) + '.lbs-initialized 已创建');
|
|
141
190
|
}
|
|
142
191
|
|
|
143
|
-
// ── 主流程
|
|
192
|
+
// ── 主流程 ─────────────────────────────────────────────────────
|
|
144
193
|
|
|
145
194
|
function main() {
|
|
146
|
-
console.log(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(paint(' ┌─────────────────────────────────────────────────────────┐', C.gold));
|
|
197
|
+
console.log(paint(' │ ', C.gold) + paint('⚙ LBS Agent — 初始化', C.bold, C.gold) + paint(' │', C.gold));
|
|
198
|
+
console.log(paint(' └─────────────────────────────────────────────────────────┘', C.gold));
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(paint(' 包目录 (只读): ', C.dim) + paint(PKG_ROOT, C.cyan));
|
|
201
|
+
console.log(paint(' 状态目录 (可写): ', C.dim) + paint(STATE_ROOT, C.cyan));
|
|
202
|
+
console.log();
|
|
152
203
|
|
|
153
204
|
// Step 1: Python
|
|
154
205
|
const sysPython = findSystemPython();
|
|
@@ -160,28 +211,28 @@ function main() {
|
|
|
160
211
|
// Step 3: deps
|
|
161
212
|
if (!installDeps()) process.exit(1);
|
|
162
213
|
|
|
163
|
-
// Step 4:
|
|
214
|
+
// Step 4: dirs
|
|
215
|
+
createDefaultDirs();
|
|
216
|
+
|
|
217
|
+
// Step 5: verify
|
|
164
218
|
verifyInstall();
|
|
165
219
|
|
|
166
|
-
// Step
|
|
220
|
+
// Step 6: flag
|
|
167
221
|
writeFlag();
|
|
168
222
|
|
|
169
|
-
console.log(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
║ node bin/lbs.js status ║
|
|
183
|
-
╚══════════════════════════════════════════════╝
|
|
184
|
-
`);
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(paint(' ┌─────────────────────────────────────────────────────────┐', C.green));
|
|
225
|
+
console.log(paint(' │ ', C.green) + paint('✅ 初始化完成!', C.bold, C.green) + paint(' │', C.green));
|
|
226
|
+
console.log(paint(' │ │', C.green));
|
|
227
|
+
console.log(paint(' │ ', C.green) + paint('下一步:', C.bold) + paint(' │', C.green));
|
|
228
|
+
console.log(paint(' │ ', C.green) + paint(' lbs setup 配置模型/供应商/Agent', C.cyan) + paint(' │', C.green));
|
|
229
|
+
console.log(paint(' │ ', C.green) + paint(' lbs status 查看系统状态', C.cyan) + paint(' │', C.green));
|
|
230
|
+
console.log(paint(' │ ', C.green) + paint(' lbs help 查看所有命令', C.cyan) + paint(' │', C.green));
|
|
231
|
+
console.log(paint(' │ ', C.green) + paint(' lbs web 启动 Web 界面', C.cyan) + paint(' │', C.green));
|
|
232
|
+
console.log(paint(' │ │', C.green));
|
|
233
|
+
console.log(paint(' │ ', C.green) + paint('状态目录: ', C.dim) + paint(STATE_ROOT, C.cyan) + paint(' │', C.green));
|
|
234
|
+
console.log(paint(' └─────────────────────────────────────────────────────────┘', C.green));
|
|
235
|
+
console.log();
|
|
185
236
|
}
|
|
186
237
|
|
|
187
238
|
main();
|