@baimingtao/lbs-agent 1.2.0 → 1.3.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 +25 -21
- package/interfaces/cli/_impl/cli_theme.py +200 -0
- package/interfaces/cli/_impl/commands/chat_repl.py +84 -72
- package/interfaces/cli/_impl/commands/setup.py +15 -15
- package/package.json +1 -1
- package/scripts/init.js +25 -20
package/bin/lbs.js
CHANGED
|
@@ -46,17 +46,21 @@ const INIT_SCRIPT = path.join(PKG_ROOT, 'scripts', 'init.js');
|
|
|
46
46
|
|
|
47
47
|
// ── 颜色与图标 (对标 Hermes banner/colors) ─────────────────────
|
|
48
48
|
|
|
49
|
+
// ── Sakura × Mint 配色 (对标 Web 端 tailwind.config.js) ───────
|
|
50
|
+
|
|
49
51
|
const C = {
|
|
50
|
-
reset: '\x1b[0m',
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
|
|
53
|
+
// 品牌色
|
|
54
|
+
sakura: '\x1b[38;2;255;183;197m', // #FFB7C5
|
|
55
|
+
sakuraDark: '\x1b[38;2;232;154;168m', // #E89AA8
|
|
56
|
+
mint: '\x1b[38;2;152;216;200m', // #98D8C8
|
|
57
|
+
mintDark: '\x1b[38;2;111;196;176m', // #6FC4B0
|
|
58
|
+
// 功能色
|
|
59
|
+
violet: '\x1b[38;2;110;90;168m', // #6E5AA8
|
|
60
|
+
honey: '\x1b[38;2;255;184;107m', // #FFB86B
|
|
61
|
+
green: '\x1b[38;2;69;214;163m', // #45D6A3
|
|
62
|
+
red: '\x1b[38;2;255;107;129m', // #FF6B81
|
|
63
|
+
blue: '\x1b[38;2;101;168;255m', // #65A8FF
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
function supportsColor() {
|
|
@@ -70,25 +74,25 @@ function paint(text, ...colors) {
|
|
|
70
74
|
return colors.join('') + text + C.reset;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
// ── ASCII Logo
|
|
77
|
+
// ── ASCII Logo — Sakura 主题 ───────────────────────────────────
|
|
74
78
|
|
|
75
79
|
const LOGO = [
|
|
76
80
|
'',
|
|
77
|
-
' ' + paint('╔══╗', C.
|
|
78
|
-
' ' + paint('║ ║', C.
|
|
79
|
-
' ' + paint('╠══╣', C.
|
|
80
|
-
' ' + paint('║ ║', C.
|
|
81
|
-
' ' + paint('╚══╝', C.
|
|
82
|
-
'',
|
|
81
|
+
' ' + paint('╔══╗', C.sakura) + ' ' + paint('╔═══╗', C.mint) + ' ' + paint('╔═════╗', C.mint),
|
|
82
|
+
' ' + paint('║ ║', C.sakura) + ' ' + paint('║ ║', C.mint) + ' ' + paint('║ ╔═╝', C.mint),
|
|
83
|
+
' ' + paint('╠══╣', C.sakura) + ' ' + paint('╠═══╣', C.mint) + ' ' + paint('║ ╚═╗', C.mint),
|
|
84
|
+
' ' + paint('║ ║', C.sakura) + ' ' + paint('║ ║', C.mint) + ' ' + paint('╚═════╝', C.mint),
|
|
85
|
+
' ' + paint('╚══╝', C.sakura) + ' ' + paint('╚═══╝', C.mint) + ' ' + paint('AGENT', C.dim),
|
|
83
86
|
].join('\n');
|
|
84
87
|
|
|
85
88
|
function printBanner() {
|
|
86
89
|
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
87
90
|
console.log(LOGO);
|
|
88
|
-
console.log(paint(' v' + pkg.version, C.bold, C.
|
|
91
|
+
console.log(paint(' v' + pkg.version, C.bold, C.sakuraDark) +
|
|
92
|
+
paint(' · ', C.sakura) +
|
|
93
|
+
paint('Leaving Blank Space Agent', C.dim));
|
|
89
94
|
console.log();
|
|
90
95
|
}
|
|
91
|
-
|
|
92
96
|
// ── 初始化检查 ─────────────────────────────────────────────────
|
|
93
97
|
|
|
94
98
|
function isInitialized() {
|
|
@@ -98,8 +102,8 @@ function isInitialized() {
|
|
|
98
102
|
function ensureInitialized() {
|
|
99
103
|
if (isInitialized()) return true;
|
|
100
104
|
|
|
101
|
-
console.log(paint('⚙ ', C.
|
|
102
|
-
console.log(paint(' 状态目录: ', C.dim) + paint(STATE_ROOT, C.
|
|
105
|
+
console.log(paint('⚙ ', C.sakura) + paint('首次运行,正在初始化 LBS Agent...', C.bold));
|
|
106
|
+
console.log(paint(' 状态目录: ', C.dim) + paint(STATE_ROOT, C.mint));
|
|
103
107
|
console.log(paint(' 创建 Python 虚拟环境 + 安装依赖(约 1-2 分钟)', C.dim));
|
|
104
108
|
console.log();
|
|
105
109
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""LBS CLI 主题系统 — Sakura × Mint (对标 Web 端 tailwind.config.js)。
|
|
2
|
+
|
|
3
|
+
Web 端配色 → ANSI True Color 映射:
|
|
4
|
+
sakura #FFB7C5 → 粉色 (品牌主色)
|
|
5
|
+
mint #98D8C8 → 薄荷绿 (辅助色)
|
|
6
|
+
berry #2B1828 → 深莓 (正文)
|
|
7
|
+
violet #6E5AA8 → 紫色 (Agent)
|
|
8
|
+
honey #FFB86B → 蜂蜜 (警告)
|
|
9
|
+
green #45D6A3 → 翠绿 (成功)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── ANSI True Color 码 (直接用 Web 端 hex) ─────────────────────
|
|
19
|
+
|
|
20
|
+
class Theme:
|
|
21
|
+
"""LBS CLI 色彩主题,映射 Web 端 tailwind config。"""
|
|
22
|
+
|
|
23
|
+
# 基础
|
|
24
|
+
RESET = "\033[0m"
|
|
25
|
+
BOLD = "\033[1m"
|
|
26
|
+
DIM = "\033[2m"
|
|
27
|
+
ITALIC = "\033[3m"
|
|
28
|
+
UNDERLINE = "\033[4m"
|
|
29
|
+
|
|
30
|
+
# 品牌色 (对标 tailwind)
|
|
31
|
+
SAKURA = "\033[38;2;255;183;197m" # #FFB7C5
|
|
32
|
+
SAKURA_LIGHT = "\033[38;2;255;212;222m" # #FFD4DE
|
|
33
|
+
SAKURA_DARK = "\033[38;2;232;154;168m" # #E89AA8
|
|
34
|
+
MINT = "\033[38;2;152;216;200m" # #98D8C8
|
|
35
|
+
MINT_LIGHT = "\033[38;2;184;232;220m" # #B8E8DC
|
|
36
|
+
MINT_DARK = "\033[38;2;111;196;176m" # #6FC4B0
|
|
37
|
+
|
|
38
|
+
# 功能色
|
|
39
|
+
BERRY = "\033[38;2;43;24;40m" # #2B1828 (深莓正文)
|
|
40
|
+
BERRY_LIGHT = "\033[38;2;74;47;69m" # #4A2F45
|
|
41
|
+
VIOLET = "\033[38;2;110;90;168m" # #6E5AA8
|
|
42
|
+
VIOLET_LIGHT = "\033[38;2;144;128;200m" # #9080C8
|
|
43
|
+
HONEY = "\033[38;2;255;184;107m" # #FFB86B
|
|
44
|
+
GREEN = "\033[38;2;69;214;163m" # #45D6A3
|
|
45
|
+
RED = "\033[38;2;255;107;129m" # #FF6B81
|
|
46
|
+
BLUE = "\033[38;2;101;168;255m" # #65A8FF
|
|
47
|
+
|
|
48
|
+
# 状态色 (终端兼容)
|
|
49
|
+
OK = "\033[1;38;2;69;214;163m" # green bold
|
|
50
|
+
ERR = "\033[1;38;2;255;107;129m" # red bold
|
|
51
|
+
WARN = "\033[1;38;2;255;184;107m" # honey bold
|
|
52
|
+
INFO = "\033[38;2;101;168;255m" # blue
|
|
53
|
+
DIM_TEXT = "\033[2m"
|
|
54
|
+
|
|
55
|
+
# 渐变 (用多段 ANSI 近似模拟)
|
|
56
|
+
GRADIENT_PINK = "\033[1;38;2;255;183;197m"
|
|
57
|
+
GRADIENT_MINT = "\033[1;38;2;152;216;200m"
|
|
58
|
+
GRADIENT_VIOLET = "\033[1;38;2;110;90;168m"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _use_color() -> bool:
|
|
62
|
+
"""检测是否使用彩色输出。"""
|
|
63
|
+
if os.environ.get("NO_COLOR"):
|
|
64
|
+
return False
|
|
65
|
+
if os.environ.get("TERM") == "dumb":
|
|
66
|
+
return False
|
|
67
|
+
return sys.stdout.isatty()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def paint(text: str, *codes: str) -> str:
|
|
71
|
+
"""给文本上色 (无 TTY 时自动降级)。"""
|
|
72
|
+
if not _use_color():
|
|
73
|
+
return text
|
|
74
|
+
return "".join(codes) + text + Theme.RESET
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── 高级组件 ───────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def sakura_label(text: str) -> str:
|
|
80
|
+
"""粉色标签 (用于品牌名)。"""
|
|
81
|
+
return paint(text, Theme.BOLD, Theme.SAKURA_DARK)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def mint_label(text: str) -> str:
|
|
85
|
+
"""薄荷标签 (用于成功状态)。"""
|
|
86
|
+
return paint(text, Theme.BOLD, Theme.MINT_DARK)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def dim(text: str) -> str:
|
|
90
|
+
"""灰色辅助文字。"""
|
|
91
|
+
return paint(text, Theme.DIM)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def agent_badge(name: str) -> str:
|
|
95
|
+
"""Agent 名称紫色胶囊。"""
|
|
96
|
+
return paint(f" {name} ", Theme.BOLD, Theme.VIOLET)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def model_pill(model: str) -> str:
|
|
100
|
+
"""模型名粉色文字。"""
|
|
101
|
+
return paint(model, Theme.SAKURA_DARK)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def status_dot(ok: bool = True) -> str:
|
|
105
|
+
"""状态圆点。"""
|
|
106
|
+
color = Theme.GREEN if ok else Theme.RED
|
|
107
|
+
return paint("●", color)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def divider(width: int = 56) -> str:
|
|
111
|
+
"""分隔线 — 樱花粉渐变薄荷。"""
|
|
112
|
+
return paint("─" * width, Theme.SAKURA, Theme.DIM)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def header_box(title: str, subtitle: str = "") -> str:
|
|
116
|
+
"""带圆角的标题框 (对标 Web 端 glass card)。"""
|
|
117
|
+
w = max(len(title) + 4, 44)
|
|
118
|
+
top = paint("╭" + "─" * (w - 2) + "╮", Theme.SAKURA)
|
|
119
|
+
bottom = paint("╰" + "─" * (w - 2) + "╯", Theme.SAKURA)
|
|
120
|
+
mid_title = paint(f"│ {title}", Theme.BOLD, Theme.SAKURA_DARK)
|
|
121
|
+
mid_title += " " * (w - 2 - len(title) - 1) + paint("│", Theme.SAKURA)
|
|
122
|
+
lines = [top, mid_title]
|
|
123
|
+
if subtitle:
|
|
124
|
+
mid_sub = paint(f"│ ", Theme.SAKURA) + paint(subtitle, Theme.DIM)
|
|
125
|
+
mid_sub += " " * (w - 2 - len(subtitle) - 1) + paint("│", Theme.SAKURA)
|
|
126
|
+
lines.append(mid_sub)
|
|
127
|
+
lines.append(bottom)
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def kv_row(label: str, value: str, label_width: int = 9) -> str:
|
|
132
|
+
"""键值行 (对标 Web 端 status dock)。"""
|
|
133
|
+
return f" {paint(label.ljust(label_width), Theme.DIM)} {paint('│', Theme.SAKURA, Theme.DIM)} {value}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def turn_prompt(agent: str, turn: int) -> str:
|
|
137
|
+
"""Chat turn 提示符。"""
|
|
138
|
+
return f"{agent_badge(agent)} {dim(f'turn {turn}')} {paint('›', Theme.SAKURA)} "
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def usage_bar(seconds: float, iterations: int, tools: int) -> str:
|
|
142
|
+
"""用量状态条 (对标 Web 端 status dock)。"""
|
|
143
|
+
parts = [
|
|
144
|
+
paint(f"{seconds:.1f}s", Theme.MINT_DARK),
|
|
145
|
+
paint(f"iter {iterations}", Theme.DIM),
|
|
146
|
+
paint(f"tools {tools}", Theme.DIM),
|
|
147
|
+
]
|
|
148
|
+
return " " + paint(" │ ", Theme.SAKURA, Theme.DIM).join(parts)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def react_header(agent_display: str, model_chain: list[str], tools: list[str]) -> str:
|
|
152
|
+
"""ReAct 迭代头 (对标 Web 端 ChatFlow 的任务头)。"""
|
|
153
|
+
model_str = paint(" → ", Theme.SAKURA).join(
|
|
154
|
+
paint(m, Theme.SAKURA_DARK) for m in model_chain
|
|
155
|
+
)
|
|
156
|
+
tools_str = paint(", ", Theme.DIM).join(
|
|
157
|
+
paint(t, Theme.MINT_DARK) for t in tools
|
|
158
|
+
)
|
|
159
|
+
lines = [
|
|
160
|
+
paint("━" * 56, Theme.SAKURA, Theme.DIM),
|
|
161
|
+
f" {paint('Agent', Theme.DIM)} {agent_badge(agent_display)}",
|
|
162
|
+
f" {paint('Model', Theme.DIM)} {model_str}",
|
|
163
|
+
f" {paint('Tools', Theme.DIM)} {tools_str}",
|
|
164
|
+
paint("━" * 56, Theme.SAKURA, Theme.DIM),
|
|
165
|
+
]
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def react_iteration(iteration: int, max_iterations: int, phase: str = "") -> str:
|
|
170
|
+
"""ReAct 迭代标记。"""
|
|
171
|
+
phase_str = f" {paint(phase, Theme.VIOLET)}" if phase else ""
|
|
172
|
+
return f"\n {paint('⟡', Theme.SAKURA)} {paint(f'iter {iteration}', Theme.BOLD, Theme.SAKURA_DARK)}{paint(f'/{max_iterations}', Theme.DIM)}{phase_str}"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def thinking_block(content: str) -> str:
|
|
176
|
+
"""思考过程块 (薄荷绿斜体)。"""
|
|
177
|
+
return paint(content, Theme.ITALIC, Theme.MINT_DARK)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def result_block(content: str) -> str:
|
|
181
|
+
"""最终结果块。"""
|
|
182
|
+
return paint(content, Theme.BOLD)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def error_block(content: str) -> str:
|
|
186
|
+
"""错误块。"""
|
|
187
|
+
return paint(f"✗ {content}", Theme.BOLD, Theme.RED)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def logo() -> str:
|
|
191
|
+
"""LBS Agent ASCII Logo — Sakura 主题。"""
|
|
192
|
+
lines = [
|
|
193
|
+
"",
|
|
194
|
+
f" {paint('╔══╗', Theme.SAKURA)} {paint('╔═══╗', Theme.MINT)} {paint('╔═════╗', Theme.MINT)}",
|
|
195
|
+
f" {paint('║ ║', Theme.SAKURA)} {paint('║ ║', Theme.MINT)} {paint('║ ╔═╝', Theme.MINT)}",
|
|
196
|
+
f" {paint('╠══╣', Theme.SAKURA)} {paint('╠═══╣', Theme.MINT)} {paint('║ ╚═╗', Theme.MINT)}",
|
|
197
|
+
f" {paint('║ ║', Theme.SAKURA)} {paint('║ ║', Theme.MINT)} {paint('╚═════╝', Theme.MINT)}",
|
|
198
|
+
f" {paint('╚══╝', Theme.SAKURA)} {paint('╚═══╝', Theme.MINT)} {paint('AGENT', Theme.DIM)}",
|
|
199
|
+
]
|
|
200
|
+
return "\n".join(lines)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""LBS Agent REPL
|
|
1
|
+
"""LBS Agent REPL 交互模式。
|
|
2
2
|
|
|
3
3
|
设计:
|
|
4
4
|
- 用户进入交互循环,每次输入一行
|
|
@@ -23,17 +23,12 @@ from typing import Optional
|
|
|
23
23
|
from application.container import AgentSystemContainer
|
|
24
24
|
from config.config import AppConfig
|
|
25
25
|
from interfaces.cli._impl.commands.common import project_root
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
"""
|
|
26
|
+
from interfaces.cli._impl.cli_theme import (
|
|
27
|
+
Theme, paint, dim, logo, agent_badge, model_pill, status_dot,
|
|
28
|
+
divider, header_box, kv_row, turn_prompt, usage_bar,
|
|
29
|
+
react_header, react_iteration, thinking_block, result_block,
|
|
30
|
+
error_block, sakura_label, mint_label,
|
|
31
|
+
)
|
|
37
32
|
|
|
38
33
|
|
|
39
34
|
@dataclass
|
|
@@ -63,85 +58,104 @@ def _resolve_agent(config: AppConfig) -> str:
|
|
|
63
58
|
return names[0]
|
|
64
59
|
|
|
65
60
|
|
|
61
|
+
def _print_banner(state: ChatState) -> None:
|
|
62
|
+
"""启动 banner — Sakura 主题。"""
|
|
63
|
+
print()
|
|
64
|
+
print(logo())
|
|
65
|
+
print(f" {paint('LBS', Theme.BOLD, Theme.SAKURA_DARK)} {dim('Agent')} "
|
|
66
|
+
f"{paint('·', Theme.SAKURA)} {dim('Interactive Chat')}")
|
|
67
|
+
print()
|
|
68
|
+
print(f" {dim('Type a message, or')} {sakura_label('/help')} {dim('for commands')}")
|
|
69
|
+
print()
|
|
70
|
+
|
|
71
|
+
|
|
66
72
|
def _print_turn_header(state: ChatState) -> None:
|
|
67
73
|
"""每次 turn 的 header(agent + turn 编号)。"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
print(f"\n{agent_label} {turn_label} \033[2m›\033[0m ", end="", flush=True)
|
|
74
|
+
print()
|
|
75
|
+
print(turn_prompt(state.agent_name, state.turn_count), end="", flush=True)
|
|
71
76
|
|
|
72
77
|
|
|
73
78
|
def _print_help() -> None:
|
|
74
|
-
print(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
""
|
|
79
|
+
print()
|
|
80
|
+
print(f" {header_box('Commands', '/ prefix to run')}")
|
|
81
|
+
print()
|
|
82
|
+
cmds = [
|
|
83
|
+
("/help", "显示本帮助"),
|
|
84
|
+
("/exit", "退出 chat"),
|
|
85
|
+
("/new", "开始新 session"),
|
|
86
|
+
("/status", "显示 session 状态"),
|
|
87
|
+
("/agent", "列出 / 切换 agent"),
|
|
88
|
+
("/model", "列出 / 切换 model"),
|
|
89
|
+
("/history", "显示最近对话"),
|
|
90
|
+
("/clear", "清屏"),
|
|
91
|
+
]
|
|
92
|
+
for cmd, desc in cmds:
|
|
93
|
+
print(f" {sakura_label(cmd.ljust(12))} {dim(desc)}")
|
|
94
|
+
print()
|
|
89
95
|
|
|
90
96
|
|
|
91
97
|
def _cmd_list_agents(state: ChatState) -> None:
|
|
92
98
|
agents = state.container.config.agents.agents
|
|
93
|
-
print(
|
|
99
|
+
print()
|
|
94
100
|
for name, a in agents.items():
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if name == state.agent_name:
|
|
102
|
+
marker = f" {status_dot(True)} {dim('current')}"
|
|
103
|
+
else:
|
|
104
|
+
marker = ""
|
|
105
|
+
print(f" {agent_badge(name)} {dim(a.display_name)}{marker}")
|
|
106
|
+
print()
|
|
97
107
|
|
|
98
108
|
|
|
99
109
|
def _cmd_list_models(state: ChatState) -> None:
|
|
100
110
|
config = state.container.config
|
|
101
111
|
provider_name = state.provider_name or config.models.default_provider
|
|
102
112
|
if provider_name not in config.models.providers:
|
|
103
|
-
print(f" 无 provider
|
|
113
|
+
print(f" {error_block(f'无 provider {provider_name!r}')}")
|
|
104
114
|
return
|
|
105
115
|
p = config.models.providers[provider_name]
|
|
106
|
-
print(
|
|
116
|
+
print()
|
|
117
|
+
print(f" {dim('provider')} {paint(provider_name, Theme.VIOLET)} {dim(f'({p.display_name or provider_name})')}")
|
|
107
118
|
for m in p.models:
|
|
108
|
-
marker = "
|
|
109
|
-
print(f"
|
|
119
|
+
marker = f" {status_dot(True)}" if m == state.model_name else ""
|
|
120
|
+
print(f" {model_pill(m)}{marker}")
|
|
121
|
+
print()
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
def _cmd_history(state: ChatState, n: int = 5) -> None:
|
|
113
125
|
if not state.session_id:
|
|
114
|
-
print(" 无 session")
|
|
126
|
+
print(f" {dim('无 session')}")
|
|
115
127
|
return
|
|
116
128
|
try:
|
|
117
129
|
messages = list(state.session_service.session_store.load_session_messages(state.session_id))
|
|
118
|
-
# 显示 user/assistant 配对
|
|
119
130
|
recent = []
|
|
120
131
|
for msg in messages[-n*2:]:
|
|
121
132
|
role = getattr(msg, "role", "?")
|
|
122
133
|
content = getattr(msg, "content", "")[:120]
|
|
123
134
|
if role in ("user", "assistant"):
|
|
124
135
|
recent.append((role, content))
|
|
125
|
-
print(
|
|
136
|
+
print()
|
|
137
|
+
print(f" {dim(f'最近 {len(recent)} 条消息')}")
|
|
126
138
|
for role, content in recent:
|
|
127
|
-
|
|
139
|
+
role_color = Theme.SAKURA_DARK if role == "user" else Theme.MINT_DARK
|
|
140
|
+
print(f" {paint(role.ljust(9), role_color)} {paint('│', Theme.SAKURA, Theme.DIM)} {dim(content)}")
|
|
141
|
+
print()
|
|
128
142
|
except Exception as e:
|
|
129
|
-
print(f"
|
|
143
|
+
print(f" {error_block(f'读取失败: {e}')}")
|
|
130
144
|
|
|
131
145
|
|
|
132
146
|
def _cmd_status(state: ChatState) -> None:
|
|
133
147
|
elapsed = int(time.time() - state.started_at)
|
|
134
148
|
rows = [
|
|
135
|
-
("session", state.session_id or "
|
|
136
|
-
("agent", state.agent_name),
|
|
137
|
-
("provider", state.provider_name or "
|
|
138
|
-
("model", state.model_name
|
|
149
|
+
("session", state.session_id or dim("(unsaved)")),
|
|
150
|
+
("agent", agent_badge(state.agent_name)),
|
|
151
|
+
("provider", state.provider_name or dim("(default)")),
|
|
152
|
+
("model", model_pill(state.model_name) if state.model_name else dim("(default)")),
|
|
139
153
|
("turns", str(state.turn_count)),
|
|
140
154
|
("elapsed", f"{elapsed}s"),
|
|
141
155
|
]
|
|
142
156
|
print()
|
|
143
157
|
for label, val in rows:
|
|
144
|
-
print(
|
|
158
|
+
print(kv_row(label, val))
|
|
145
159
|
print()
|
|
146
160
|
|
|
147
161
|
|
|
@@ -159,13 +173,13 @@ def _handle_command(state: ChatState, line: str) -> bool:
|
|
|
159
173
|
state.session_id = ""
|
|
160
174
|
state.turn_count = 0
|
|
161
175
|
state.started_at = time.time()
|
|
162
|
-
print("
|
|
176
|
+
print(f" {mint_label('→')} {dim('new session')}")
|
|
163
177
|
elif cmd == "/session":
|
|
164
178
|
if arg:
|
|
165
179
|
state.session_id = arg
|
|
166
|
-
print(f"
|
|
180
|
+
print(f" {mint_label('→')} {dim('switched to')} {paint(arg, Theme.SAKURA_DARK)}")
|
|
167
181
|
else:
|
|
168
|
-
print(f"
|
|
182
|
+
print(f" {dim('session:')} {state.session_id or dim('(none)')}")
|
|
169
183
|
elif cmd == "/status":
|
|
170
184
|
_cmd_status(state)
|
|
171
185
|
elif cmd == "/agent":
|
|
@@ -175,15 +189,15 @@ def _handle_command(state: ChatState, line: str) -> bool:
|
|
|
175
189
|
agents = state.container.config.agents.agents
|
|
176
190
|
if arg in agents:
|
|
177
191
|
state.agent_name = arg
|
|
178
|
-
print(f"
|
|
192
|
+
print(f" {mint_label('→')} {dim('agent')} {agent_badge(arg)}")
|
|
179
193
|
else:
|
|
180
|
-
print(f" 无 agent
|
|
194
|
+
print(f" {error_block(f'无 agent {arg!r}')}")
|
|
181
195
|
elif cmd == "/model":
|
|
182
196
|
if not arg:
|
|
183
197
|
_cmd_list_models(state)
|
|
184
198
|
else:
|
|
185
199
|
state.model_name = arg
|
|
186
|
-
print(f"
|
|
200
|
+
print(f" {mint_label('→')} {dim('model')} {model_pill(arg)}")
|
|
187
201
|
elif cmd == "/clear":
|
|
188
202
|
print("\033[2J\033[H", end="")
|
|
189
203
|
elif cmd == "/history":
|
|
@@ -191,12 +205,11 @@ def _handle_command(state: ChatState, line: str) -> bool:
|
|
|
191
205
|
n = int(arg) if arg else 5
|
|
192
206
|
_cmd_history(state, n)
|
|
193
207
|
except ValueError:
|
|
194
|
-
print(" /history [n]
|
|
208
|
+
print(f" {error_block('/history [n] — n 必须是整数')}")
|
|
195
209
|
elif cmd == "/save":
|
|
196
|
-
|
|
197
|
-
print(" session 已在每次 turn 自动保存")
|
|
210
|
+
print(f" {dim('session 自动保存在每次 turn')}")
|
|
198
211
|
else:
|
|
199
|
-
print(f"
|
|
212
|
+
print(f" {error_block(f'未知命令: {cmd} — 输入 /help')}")
|
|
200
213
|
return True
|
|
201
214
|
|
|
202
215
|
|
|
@@ -204,7 +217,6 @@ def _run_turn(state: ChatState, user_input: str) -> None:
|
|
|
204
217
|
"""跑一轮对话(同步阻塞)。"""
|
|
205
218
|
t0 = time.time()
|
|
206
219
|
try:
|
|
207
|
-
# 把 user_input 作为 task
|
|
208
220
|
result = state.orchestrator.run_agent_sync(
|
|
209
221
|
state.agent_name,
|
|
210
222
|
user_input,
|
|
@@ -215,21 +227,21 @@ def _run_turn(state: ChatState, user_input: str) -> None:
|
|
|
215
227
|
# 显示结果
|
|
216
228
|
if result.success:
|
|
217
229
|
content = (result.content or "").strip()
|
|
218
|
-
print(f"\n
|
|
230
|
+
print(f"\n{result_block(content)}")
|
|
219
231
|
else:
|
|
220
232
|
err = result.error or "(无错误信息)"
|
|
221
|
-
print(f"\n
|
|
222
|
-
#
|
|
223
|
-
print(
|
|
224
|
-
# 保存 session_id
|
|
233
|
+
print(f"\n {error_block(err[:300])}")
|
|
234
|
+
# 用量状态条
|
|
235
|
+
print(usage_bar(elapsed, result.iterations, result.total_tool_calls))
|
|
236
|
+
# 保存 session_id
|
|
225
237
|
if not state.session_id:
|
|
226
238
|
state.session_id = result.session_id if hasattr(result, "session_id") and result.session_id else f"chat-{int(state.started_at)}"
|
|
227
239
|
except KeyboardInterrupt:
|
|
228
|
-
print("\n
|
|
240
|
+
print(f"\n {paint('⏸', Theme.HONEY)} {dim('已中断')}")
|
|
229
241
|
except Exception as e:
|
|
230
242
|
elapsed = time.time() - t0
|
|
231
|
-
print(f"\n
|
|
232
|
-
print(f"
|
|
243
|
+
print(f"\n {error_block(f'{type(e).__name__}: {str(e)[:200]}')}")
|
|
244
|
+
print(f" {dim(f'{elapsed:.1f}s')}")
|
|
233
245
|
|
|
234
246
|
|
|
235
247
|
def cmd_chat(
|
|
@@ -249,7 +261,8 @@ def cmd_chat(
|
|
|
249
261
|
|
|
250
262
|
# 验证 agent 存在
|
|
251
263
|
if agent_name not in config.agents.agents:
|
|
252
|
-
|
|
264
|
+
msg = f"Agent '{agent_name}' 不存在"
|
|
265
|
+
print(f" {error_block(msg)}")
|
|
253
266
|
return 1
|
|
254
267
|
|
|
255
268
|
state = ChatState(
|
|
@@ -262,7 +275,7 @@ def cmd_chat(
|
|
|
262
275
|
session_id=session_id,
|
|
263
276
|
)
|
|
264
277
|
|
|
265
|
-
# 单次 query
|
|
278
|
+
# 单次 query 模式
|
|
266
279
|
if query:
|
|
267
280
|
state.turn_count = 1
|
|
268
281
|
_print_turn_header(state)
|
|
@@ -271,9 +284,8 @@ def cmd_chat(
|
|
|
271
284
|
return 0
|
|
272
285
|
|
|
273
286
|
# REPL 模式
|
|
274
|
-
|
|
287
|
+
_print_banner(state)
|
|
275
288
|
_cmd_status(state)
|
|
276
|
-
print("\n输入 /help 查看命令。Ctrl+C 中断当前 turn,/exit 退出。\n")
|
|
277
289
|
|
|
278
290
|
while True:
|
|
279
291
|
try:
|
|
@@ -283,7 +295,7 @@ def cmd_chat(
|
|
|
283
295
|
print("\n")
|
|
284
296
|
break
|
|
285
297
|
except KeyboardInterrupt:
|
|
286
|
-
print("\n
|
|
298
|
+
print(f"\n {paint('用 /exit 退出', Theme.HONEY)}")
|
|
287
299
|
continue
|
|
288
300
|
|
|
289
301
|
line = line.strip()
|
|
@@ -296,5 +308,5 @@ def cmd_chat(
|
|
|
296
308
|
state.turn_count += 1
|
|
297
309
|
_run_turn(state, line)
|
|
298
310
|
|
|
299
|
-
print("\n
|
|
311
|
+
print(f"\n {mint_label('✓')} {dim('Session saved. Bye.')}")
|
|
300
312
|
return 0
|
|
@@ -73,15 +73,15 @@ def _paint(text: str, *codes: str) -> str:
|
|
|
73
73
|
def _header(title: str) -> None:
|
|
74
74
|
"""打印章节标题 (带边框)。"""
|
|
75
75
|
print()
|
|
76
|
-
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.
|
|
77
|
-
print(_paint(" │ ", _C.
|
|
78
|
-
+ _paint(" " * max(0, 47 - len(title) - 14) + "│", _C.
|
|
79
|
-
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.
|
|
76
|
+
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.SAKURA))
|
|
77
|
+
print(_paint(" │ ", _C.SAKURA) + _paint(f"⚙ LBS Setup — {title}", _C.BOLD, _C.SAKURA)
|
|
78
|
+
+ _paint(" " * max(0, 47 - len(title) - 14) + "│", _C.SAKURA))
|
|
79
|
+
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.SAKURA))
|
|
80
80
|
print()
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def _info(msg: str) -> None:
|
|
84
|
-
print(_paint(" ℹ ", _C.
|
|
84
|
+
print(_paint(" ℹ ", _C.VIOLET) + msg)
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
def _success(msg: str) -> None:
|
|
@@ -89,7 +89,7 @@ def _success(msg: str) -> None:
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _warn(msg: str) -> None:
|
|
92
|
-
print(_paint(" ⚠ ", _C.
|
|
92
|
+
print(_paint(" ⚠ ", _C.HONEY) + _paint(msg, _C.HONEY))
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _error(msg: str) -> None:
|
|
@@ -100,7 +100,7 @@ def _prompt(label: str, default: str = "") -> str:
|
|
|
100
100
|
"""带默认值的输入提示。"""
|
|
101
101
|
suffix = f" [{default}]" if default else ""
|
|
102
102
|
try:
|
|
103
|
-
val = input(_paint(f" ▸ {label}{suffix}: ", _C.
|
|
103
|
+
val = input(_paint(f" ▸ {label}{suffix}: ", _C.MINT)).strip()
|
|
104
104
|
except (EOFError, KeyboardInterrupt):
|
|
105
105
|
print()
|
|
106
106
|
return default
|
|
@@ -109,7 +109,7 @@ def _prompt(label: str, default: str = "") -> str:
|
|
|
109
109
|
|
|
110
110
|
def _prompt_choice(label: str, options: list[str], default: str = "") -> str:
|
|
111
111
|
"""带选项列表的输入提示。"""
|
|
112
|
-
print(_paint(f" ▸ {label}", _C.
|
|
112
|
+
print(_paint(f" ▸ {label}", _C.MINT))
|
|
113
113
|
for i, opt in enumerate(options, 1):
|
|
114
114
|
marker = _paint("→", _C.GREEN) if opt == default else " "
|
|
115
115
|
print(f" {marker} {i}. {opt}")
|
|
@@ -125,7 +125,7 @@ def _prompt_password(label: str) -> str:
|
|
|
125
125
|
"""密码输入(不回显)。"""
|
|
126
126
|
import getpass
|
|
127
127
|
try:
|
|
128
|
-
val = getpass.getpass(_paint(f" ▸ {label}: ", _C.
|
|
128
|
+
val = getpass.getpass(_paint(f" ▸ {label}: ", _C.MINT))
|
|
129
129
|
except (EOFError, KeyboardInterrupt):
|
|
130
130
|
print()
|
|
131
131
|
return ""
|
|
@@ -467,10 +467,10 @@ def run_setup(section: str | None = None) -> int:
|
|
|
467
467
|
root.mkdir(parents=True, exist_ok=True)
|
|
468
468
|
|
|
469
469
|
print()
|
|
470
|
-
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.
|
|
471
|
-
print(_paint(" │ ", _C.
|
|
472
|
-
+ _paint(" │", _C.
|
|
473
|
-
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.
|
|
470
|
+
print(_paint(" ┌─────────────────────────────────────────────────────────┐", _C.SAKURA))
|
|
471
|
+
print(_paint(" │ ", _C.SAKURA) + _paint("⚙ LBS Agent Setup Wizard", _C.BOLD, _C.SAKURA)
|
|
472
|
+
+ _paint(" │", _C.SAKURA))
|
|
473
|
+
print(_paint(" └─────────────────────────────────────────────────────────┘", _C.SAKURA))
|
|
474
474
|
print()
|
|
475
475
|
_info(f"配置目录: {root}")
|
|
476
476
|
print()
|
|
@@ -512,8 +512,8 @@ def paint_path(p: Path) -> str:
|
|
|
512
512
|
s = str(p)
|
|
513
513
|
if len(s) > 40:
|
|
514
514
|
s = "..." + s[-37:]
|
|
515
|
-
return _paint(s, _C.
|
|
515
|
+
return _paint(s, _C.MINT)
|
|
516
516
|
|
|
517
517
|
|
|
518
518
|
def paint_cmd(cmd: str) -> str:
|
|
519
|
-
return _paint(cmd, _C.
|
|
519
|
+
return _paint(cmd, _C.MINT, _C.BOLD)
|
package/package.json
CHANGED
package/scripts/init.js
CHANGED
|
@@ -29,10 +29,15 @@ const PIP = isWin ? path.join(VENV_DIR, 'Scripts', 'pip.exe') : path.join(VENV_D
|
|
|
29
29
|
// ── 颜色 ───────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
const C = {
|
|
32
|
-
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
|
|
33
|
+
sakura: '\x1b[38;2;255;183;197m', // #FFB7C5
|
|
34
|
+
sakuraDark: '\x1b[38;2;232;154;168m', // #E89AA8
|
|
35
|
+
mint: '\x1b[38;2;152;216;200m', // #98D8C8
|
|
36
|
+
mintDark: '\x1b[38;2;111;196;176m', // #6FC4B0
|
|
37
|
+
violet: '\x1b[38;2;110;90;168m', // #6E5AA8
|
|
38
|
+
honey: '\x1b[38;2;255;184;107m', // #FFB86B
|
|
39
|
+
green: '\x1b[38;2;69;214;163m', // #45D6A3
|
|
40
|
+
red: '\x1b[38;2;255;107;129m', // #FF6B81
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
function paint(text, ...colors) {
|
|
@@ -58,7 +63,7 @@ function ensureDir(dir) {
|
|
|
58
63
|
// ── Step 1: 检测系统 Python ────────────────────────────────────
|
|
59
64
|
|
|
60
65
|
function findSystemPython() {
|
|
61
|
-
console.log(paint(' 🔍 ', C.
|
|
66
|
+
console.log(paint(' 🔍 ', C.sakura) + '检测系统 Python...');
|
|
62
67
|
|
|
63
68
|
const candidates = isWin
|
|
64
69
|
? ['python', 'python3', 'py -3']
|
|
@@ -76,7 +81,7 @@ function findSystemPython() {
|
|
|
76
81
|
const minor = parseInt(match[2]);
|
|
77
82
|
if (major >= 3 && minor >= 10) return cmd;
|
|
78
83
|
}
|
|
79
|
-
console.log(paint(' ⚠ ', C.
|
|
84
|
+
console.log(paint(' ⚠ ', C.honey) + '版本低于 3.10,继续寻找...');
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -97,7 +102,7 @@ function createVenv(sysPython) {
|
|
|
97
102
|
|
|
98
103
|
// 确保 state root 存在
|
|
99
104
|
ensureDir(STATE_ROOT);
|
|
100
|
-
console.log(paint(' 📦 ', C.
|
|
105
|
+
console.log(paint(' 📦 ', C.sakura) + `创建 Python 虚拟环境 → ${paint(STATE_ROOT + '/venv', C.mint)}`);
|
|
101
106
|
|
|
102
107
|
const result = run(sysPython, ['-m', 'venv', VENV_DIR]);
|
|
103
108
|
if (result.status !== 0 || !fs.existsSync(PYTHON)) {
|
|
@@ -116,7 +121,7 @@ function installDeps() {
|
|
|
116
121
|
return false;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
console.log(paint(' 📥 ', C.
|
|
124
|
+
console.log(paint(' 📥 ', C.sakura) + '安装 Python 依赖 (可能需要 1-2 分钟)...');
|
|
120
125
|
const result = run(PIP, ['install', '-r', REQUIREMENTS, '--disable-pip-version-check']);
|
|
121
126
|
if (result.status !== 0) {
|
|
122
127
|
console.error(paint(' ✖ ', C.red) + '依赖安装失败');
|
|
@@ -162,7 +167,7 @@ function createDefaultDirs() {
|
|
|
162
167
|
// ── Step 5: 验证 ───────────────────────────────────────────────
|
|
163
168
|
|
|
164
169
|
function verifyInstall() {
|
|
165
|
-
console.log(paint(' 🔍 ', C.
|
|
170
|
+
console.log(paint(' 🔍 ', C.sakura) + '验证安装...');
|
|
166
171
|
const result = run(PYTHON, [path.join(PKG_ROOT, 'lbs.py'), 'status'], {
|
|
167
172
|
silent: true,
|
|
168
173
|
cwd: PKG_ROOT,
|
|
@@ -171,7 +176,7 @@ function verifyInstall() {
|
|
|
171
176
|
if (result.status === 0) {
|
|
172
177
|
console.log(paint(' ✓ ', C.green) + 'lbs status 运行正常');
|
|
173
178
|
} else {
|
|
174
|
-
console.log(paint(' ⚠ ', C.
|
|
179
|
+
console.log(paint(' ⚠ ', C.honey) + 'lbs status 运行异常(可能是配置问题,不影响初始化)');
|
|
175
180
|
}
|
|
176
181
|
return true;
|
|
177
182
|
}
|
|
@@ -193,12 +198,12 @@ function writeFlag() {
|
|
|
193
198
|
|
|
194
199
|
function main() {
|
|
195
200
|
console.log();
|
|
196
|
-
console.log(paint(' ┌─────────────────────────────────────────────────────────┐', C.
|
|
197
|
-
console.log(paint(' │ ', C.
|
|
198
|
-
console.log(paint(' └─────────────────────────────────────────────────────────┘', C.
|
|
201
|
+
console.log(paint(' ┌─────────────────────────────────────────────────────────┐', C.sakura));
|
|
202
|
+
console.log(paint(' │ ', C.sakura) + paint('⚙ LBS Agent — 初始化', C.bold, C.sakura) + paint(' │', C.sakura));
|
|
203
|
+
console.log(paint(' └─────────────────────────────────────────────────────────┘', C.sakura));
|
|
199
204
|
console.log();
|
|
200
|
-
console.log(paint(' 包目录 (只读): ', C.dim) + paint(PKG_ROOT, C.
|
|
201
|
-
console.log(paint(' 状态目录 (可写): ', C.dim) + paint(STATE_ROOT, C.
|
|
205
|
+
console.log(paint(' 包目录 (只读): ', C.dim) + paint(PKG_ROOT, C.mint));
|
|
206
|
+
console.log(paint(' 状态目录 (可写): ', C.dim) + paint(STATE_ROOT, C.mint));
|
|
202
207
|
console.log();
|
|
203
208
|
|
|
204
209
|
// Step 1: Python
|
|
@@ -225,12 +230,12 @@ function main() {
|
|
|
225
230
|
console.log(paint(' │ ', C.green) + paint('✅ 初始化完成!', C.bold, C.green) + paint(' │', C.green));
|
|
226
231
|
console.log(paint(' │ │', C.green));
|
|
227
232
|
console.log(paint(' │ ', C.green) + paint('下一步:', C.bold) + paint(' │', C.green));
|
|
228
|
-
console.log(paint(' │ ', C.green) + paint(' lbs setup 配置模型/供应商/Agent', C.
|
|
229
|
-
console.log(paint(' │ ', C.green) + paint(' lbs status 查看系统状态', C.
|
|
230
|
-
console.log(paint(' │ ', C.green) + paint(' lbs help 查看所有命令', C.
|
|
231
|
-
console.log(paint(' │ ', C.green) + paint(' lbs web 启动 Web 界面', C.
|
|
233
|
+
console.log(paint(' │ ', C.green) + paint(' lbs setup 配置模型/供应商/Agent', C.mint) + paint(' │', C.green));
|
|
234
|
+
console.log(paint(' │ ', C.green) + paint(' lbs status 查看系统状态', C.mint) + paint(' │', C.green));
|
|
235
|
+
console.log(paint(' │ ', C.green) + paint(' lbs help 查看所有命令', C.mint) + paint(' │', C.green));
|
|
236
|
+
console.log(paint(' │ ', C.green) + paint(' lbs web 启动 Web 界面', C.mint) + paint(' │', C.green));
|
|
232
237
|
console.log(paint(' │ │', C.green));
|
|
233
|
-
console.log(paint(' │ ', C.green) + paint('状态目录: ', C.dim) + paint(STATE_ROOT, C.
|
|
238
|
+
console.log(paint(' │ ', C.green) + paint('状态目录: ', C.dim) + paint(STATE_ROOT, C.mint) + paint(' │', C.green));
|
|
234
239
|
console.log(paint(' └─────────────────────────────────────────────────────────┘', C.green));
|
|
235
240
|
console.log();
|
|
236
241
|
}
|