merco 0.1.0__py3-none-any.whl
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.
- cli/__init__.py +5 -0
- cli/commands.py +3 -0
- cli/main.py +539 -0
- cli/tui.py +10 -0
- merco/__init__.py +4 -0
- merco/core/__init__.py +21 -0
- merco/core/agent.py +554 -0
- merco/core/config.py +152 -0
- merco/core/context.py +63 -0
- merco/core/llm.py +200 -0
- merco/core/message.py +45 -0
- merco/core/pipeline.py +568 -0
- merco/core/self_healing.py +216 -0
- merco/core/session.py +46 -0
- merco/gateway/__init__.py +5 -0
- merco/gateway/base.py +38 -0
- merco/gateway/discord.py +24 -0
- merco/gateway/telegram.py +25 -0
- merco/hooks/__init__.py +5 -0
- merco/hooks/chat_hooks.py +22 -0
- merco/hooks/lifecycle.py +23 -0
- merco/hooks/registry.py +40 -0
- merco/hooks/tool_hooks.py +22 -0
- merco/memory/__init__.py +6 -0
- merco/memory/compressor.py +168 -0
- merco/memory/recall.py +38 -0
- merco/memory/search.py +35 -0
- merco/memory/store.py +69 -0
- merco/observability/__init__.py +6 -0
- merco/observability/audit.py +39 -0
- merco/observability/logger.py +34 -0
- merco/observability/metrics.py +57 -0
- merco/observability/tracing.py +67 -0
- merco/sandbox/__init__.py +5 -0
- merco/sandbox/isolation.py +36 -0
- merco/sandbox/permissions.py +46 -0
- merco/sandbox/security.py +34 -0
- merco/scheduler/__init__.py +5 -0
- merco/scheduler/cron.py +100 -0
- merco/scheduler/delivery.py +30 -0
- merco/scheduler/jobs.py +65 -0
- merco/skills/__init__.py +6 -0
- merco/skills/builtin/__init__.py +1 -0
- merco/skills/loader.py +69 -0
- merco/skills/registry.py +48 -0
- merco/tools/__init__.py +60 -0
- merco/tools/base.py +57 -0
- merco/tools/bash_tools.py +51 -0
- merco/tools/file_tools.py +137 -0
- merco/tools/mcp_tools.py +55 -0
- merco/tools/registry.py +78 -0
- merco/tools/skill_tools.py +73 -0
- merco/tools/task_tools.py +37 -0
- merco/tools/web_tools.py +67 -0
- merco/utils/__init__.py +1 -0
- merco/utils/helpers.py +40 -0
- merco-0.1.0.dist-info/METADATA +101 -0
- merco-0.1.0.dist-info/RECORD +62 -0
- merco-0.1.0.dist-info/WHEEL +4 -0
- merco-0.1.0.dist-info/entry_points.txt +3 -0
- web/__init__.py +5 -0
- web/app.py +27 -0
cli/__init__.py
ADDED
cli/commands.py
ADDED
cli/main.py
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""CLI 主入口"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import readline
|
|
7
|
+
import signal
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="merco",
|
|
17
|
+
help="Mercury Code — AI 驱动的自改进软件开发平台",
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── 启动首页 Dashboard ──────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
import merco
|
|
26
|
+
|
|
27
|
+
class DashboardSection(ABC):
|
|
28
|
+
"""首页展示区块基类。新增条目:继承 + 实现 render() + dashboard.use()"""
|
|
29
|
+
name: str = ""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def render(self, agent, **ctx) -> str | None:
|
|
33
|
+
"""返回一行 Rich 标记文本,None 则跳过"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WelcomeSection(DashboardSection):
|
|
38
|
+
name = "welcome"
|
|
39
|
+
def render(self, agent, **ctx) -> str:
|
|
40
|
+
return f"[bold green]Mercury Code v{merco.__version__}[/bold green]"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ModelSection(DashboardSection):
|
|
44
|
+
name = "model"
|
|
45
|
+
def render(self, agent, **ctx) -> str:
|
|
46
|
+
return f"模型: {agent.config.model.provider}/{agent.config.model.model}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ToolsSection(DashboardSection):
|
|
50
|
+
name = "tools"
|
|
51
|
+
|
|
52
|
+
def __init__(self, max_display: int = 5):
|
|
53
|
+
self.max_display = max_display
|
|
54
|
+
|
|
55
|
+
def render(self, agent, **ctx) -> str:
|
|
56
|
+
tools = agent.tool_registry.list_tools() if agent.tool_registry else []
|
|
57
|
+
active = [t.name for t in tools if t.check()]
|
|
58
|
+
if not active:
|
|
59
|
+
return "工具: [dim]无[/dim]"
|
|
60
|
+
shown = active[:self.max_display]
|
|
61
|
+
line = f"工具: [bold]{', '.join(shown)}[/bold]"
|
|
62
|
+
if len(active) > self.max_display:
|
|
63
|
+
line += f" [dim]等 {len(active)} 个[/dim]"
|
|
64
|
+
return line
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SkillsSection(DashboardSection):
|
|
68
|
+
name = "skills"
|
|
69
|
+
|
|
70
|
+
def __init__(self, max_display: int = 3):
|
|
71
|
+
self.max_display = max_display
|
|
72
|
+
|
|
73
|
+
def render(self, agent, **ctx) -> str:
|
|
74
|
+
registry = getattr(agent, "skill_registry", None)
|
|
75
|
+
if not registry:
|
|
76
|
+
return "技能: [dim]无[/dim]"
|
|
77
|
+
skills = registry.list_skills()
|
|
78
|
+
if not skills:
|
|
79
|
+
return "技能: [dim]无[/dim]"
|
|
80
|
+
names = [s["name"] for s in skills[:self.max_display]]
|
|
81
|
+
line = f"技能: [bold]{', '.join(names)}[/bold]"
|
|
82
|
+
if len(skills) > self.max_display:
|
|
83
|
+
line += f" [dim]等 {len(skills)} 个[/dim]"
|
|
84
|
+
return line
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ConfigSection(DashboardSection):
|
|
88
|
+
name = "config"
|
|
89
|
+
|
|
90
|
+
def render(self, agent, **ctx) -> str:
|
|
91
|
+
return f"配置: [dim]{ctx.get('config_source', '默认值')}[/dim]"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class HintSection(DashboardSection):
|
|
95
|
+
name = "hint"
|
|
96
|
+
|
|
97
|
+
def render(self, agent, **ctx) -> str:
|
|
98
|
+
return "[dim]输入消息开始对话,/help 查看命令,/exit 退出[/dim]"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Dashboard:
|
|
102
|
+
"""首页渲染器。按 use() 顺序渲染各区块。"""
|
|
103
|
+
def __init__(self):
|
|
104
|
+
self._sections: list[DashboardSection] = []
|
|
105
|
+
|
|
106
|
+
def use(self, section: DashboardSection) -> "Dashboard":
|
|
107
|
+
self._sections.append(section)
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def render(self, agent, **ctx) -> str:
|
|
111
|
+
parts = []
|
|
112
|
+
for s in self._sections:
|
|
113
|
+
try:
|
|
114
|
+
line = s.render(agent, **ctx)
|
|
115
|
+
if line:
|
|
116
|
+
parts.append(line)
|
|
117
|
+
except Exception:
|
|
118
|
+
parts.append(f"[dim]({s.name}: 渲染失败)[/dim]")
|
|
119
|
+
return "\n".join(parts)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── 共享的 Agent 启动逻辑 ────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def _setup_agent(config_path: str | None, model: str | None, api_key: str | None, debug: bool):
|
|
125
|
+
from merco.core.config import MercoConfig
|
|
126
|
+
from merco.core.agent import Agent
|
|
127
|
+
import merco
|
|
128
|
+
from merco.tools import discover_tools, tool_registry
|
|
129
|
+
discover_tools()
|
|
130
|
+
|
|
131
|
+
if debug:
|
|
132
|
+
logging.basicConfig(
|
|
133
|
+
level=logging.WARNING, # 全局默认 WARNING,不污染第三方库
|
|
134
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
135
|
+
datefmt="%H:%M:%S",
|
|
136
|
+
)
|
|
137
|
+
logging.getLogger("merco").setLevel(logging.DEBUG)
|
|
138
|
+
console.print("[yellow]🔍 调试模式已开启[/yellow]")
|
|
139
|
+
else:
|
|
140
|
+
logging.basicConfig(level=logging.WARNING)
|
|
141
|
+
|
|
142
|
+
cfg = MercoConfig.load(config_path)
|
|
143
|
+
if model:
|
|
144
|
+
cfg.model.model = model
|
|
145
|
+
if api_key:
|
|
146
|
+
cfg.model.api_key = api_key
|
|
147
|
+
|
|
148
|
+
if not cfg.model.api_key:
|
|
149
|
+
env_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
|
|
150
|
+
if env_key:
|
|
151
|
+
cfg.model.api_key = env_key
|
|
152
|
+
else:
|
|
153
|
+
candidates = ["./merco.json", "~/.config/merco/config.json"]
|
|
154
|
+
searched = "\n".join(f" • {c}" for c in candidates)
|
|
155
|
+
console.print(Panel(
|
|
156
|
+
f"[red]未找到 API Key。请设置:\n"
|
|
157
|
+
f"1. 在 merco.json 中设置 model.api_key\n"
|
|
158
|
+
f"2. 或设置环境变量(OPENAI_API_KEY / OPENROUTER_API_KEY 等)\n"
|
|
159
|
+
f"3. 或使用 merco -k sk-... 启动\n\n"
|
|
160
|
+
f"[dim]已搜索配置文件:[/dim]\n{searched}[/red]",
|
|
161
|
+
title="配置错误",
|
|
162
|
+
))
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
|
|
165
|
+
# tools auto-registered via discover_tools()
|
|
166
|
+
|
|
167
|
+
# ── 技能注册 ──
|
|
168
|
+
from merco.skills.registry import SkillRegistry
|
|
169
|
+
skill_registry = SkillRegistry()
|
|
170
|
+
if cfg.skills_paths:
|
|
171
|
+
skill_registry.load_from_paths(cfg.skills_paths)
|
|
172
|
+
|
|
173
|
+
# 注入 skill_registry 给 SkillViewTool(动态描述 + 可用性检查)
|
|
174
|
+
sv = tool_registry.get("skill_view")
|
|
175
|
+
if sv and hasattr(sv, "set_skill_registry"):
|
|
176
|
+
sv.set_skill_registry(skill_registry)
|
|
177
|
+
|
|
178
|
+
agent = Agent(config=cfg, tool_registry=tool_registry,
|
|
179
|
+
skill_registry=skill_registry)
|
|
180
|
+
|
|
181
|
+
# 显示加载的配置来源
|
|
182
|
+
config_source = "默认值"
|
|
183
|
+
for candidate in ["./merco.json", "./.merco/merco.json",
|
|
184
|
+
os.path.expanduser("~/.config/merco/config.json")]:
|
|
185
|
+
if os.path.exists(candidate):
|
|
186
|
+
config_source = candidate
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
dashboard = (Dashboard()
|
|
190
|
+
.use(WelcomeSection())
|
|
191
|
+
.use(ModelSection())
|
|
192
|
+
.use(ToolsSection(max_display=5))
|
|
193
|
+
.use(SkillsSection(max_display=3))
|
|
194
|
+
.use(ConfigSection())
|
|
195
|
+
.use(HintSection()))
|
|
196
|
+
|
|
197
|
+
console.print(Panel(
|
|
198
|
+
dashboard.render(agent, config_source=config_source),
|
|
199
|
+
title="🚀 Mercury Code",
|
|
200
|
+
))
|
|
201
|
+
return agent
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── 输入区 PromptDecorator ─────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
class PromptDecorator(ABC):
|
|
207
|
+
"""输入区装饰器基类。新增:继承 + 实现 render() + prompt_area.use()"""
|
|
208
|
+
name: str = ""
|
|
209
|
+
|
|
210
|
+
def render(self, agent) -> str | None:
|
|
211
|
+
"""返回输入上方的展示文本,None 跳过"""
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def get_prompt(self) -> str:
|
|
215
|
+
"""返回输入提示符"""
|
|
216
|
+
return "> "
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ContextBar(PromptDecorator):
|
|
220
|
+
"""上下文用量进度条 — 半高薄款"""
|
|
221
|
+
name = "context_bar"
|
|
222
|
+
_W = 16
|
|
223
|
+
|
|
224
|
+
def render(self, agent) -> str:
|
|
225
|
+
stats = agent.get_context_stats()
|
|
226
|
+
thresh_p = int(stats["threshold"] * self._W)
|
|
227
|
+
filled_n = int(stats["ratio"] * self._W)
|
|
228
|
+
bar = "▐"
|
|
229
|
+
for i in range(self._W):
|
|
230
|
+
if i == thresh_p:
|
|
231
|
+
bar += "│"
|
|
232
|
+
elif i < filled_n:
|
|
233
|
+
bar += "█"
|
|
234
|
+
else:
|
|
235
|
+
bar += "░"
|
|
236
|
+
bar += "▌"
|
|
237
|
+
|
|
238
|
+
color = "dim"
|
|
239
|
+
if stats["ratio"] > stats["threshold"]:
|
|
240
|
+
color = "yellow"
|
|
241
|
+
if stats["ratio"] > 0.95:
|
|
242
|
+
color = "red"
|
|
243
|
+
est = "~" if stats["is_estimate"] else ""
|
|
244
|
+
cur = stats["current"]; mx = stats["max"]
|
|
245
|
+
def _f(n): return str(n) if n < 1024 else f"{n/1024:.1f}K"
|
|
246
|
+
return f" [{color}]{bar}[/{color}] {est}{_f(cur)}/{_f(mx)}"
|
|
247
|
+
|
|
248
|
+
def get_prompt(self) -> str:
|
|
249
|
+
return "▸ "
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class PromptArea:
|
|
253
|
+
"""输入区渲染器。按 use() 顺序渲染各装饰器。"""
|
|
254
|
+
def __init__(self):
|
|
255
|
+
self._decorators: list[PromptDecorator] = []
|
|
256
|
+
|
|
257
|
+
def use(self, d: PromptDecorator) -> "PromptArea":
|
|
258
|
+
self._decorators.append(d)
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def render(self, agent) -> tuple[str, str]:
|
|
262
|
+
pre_parts = []
|
|
263
|
+
prompt = "> "
|
|
264
|
+
for d in self._decorators:
|
|
265
|
+
try:
|
|
266
|
+
line = d.render(agent)
|
|
267
|
+
if line:
|
|
268
|
+
pre_parts.append(line)
|
|
269
|
+
prompt = d.get_prompt()
|
|
270
|
+
except Exception:
|
|
271
|
+
pre_parts.append(f"[dim]({d.name}: render failed)[/dim]")
|
|
272
|
+
return "\n".join(pre_parts), prompt
|
|
273
|
+
|
|
274
|
+
def _render_context_bar(stats: dict) -> str:
|
|
275
|
+
"""渲染 token 用量进度条 — 阈值标记在中间"""
|
|
276
|
+
w = 10
|
|
277
|
+
thresh_p = int(stats["threshold"] * w)
|
|
278
|
+
filled_n = int(stats["ratio"] * w)
|
|
279
|
+
bar = "▕"
|
|
280
|
+
for i in range(w):
|
|
281
|
+
if i == thresh_p:
|
|
282
|
+
bar += "│"
|
|
283
|
+
elif i < filled_n:
|
|
284
|
+
bar += "█"
|
|
285
|
+
else:
|
|
286
|
+
bar += "░"
|
|
287
|
+
bar += "▏"
|
|
288
|
+
|
|
289
|
+
color = "dim"
|
|
290
|
+
if stats["ratio"] > stats["threshold"]:
|
|
291
|
+
color = "yellow"
|
|
292
|
+
if stats["ratio"] > 0.95:
|
|
293
|
+
color = "red"
|
|
294
|
+
est = "~" if stats["is_estimate"] else ""
|
|
295
|
+
tool_info = f"🔧 {stats['tool_count']}/{stats['max_tool_calls']}"
|
|
296
|
+
cur = stats["current"]; mx = stats["max"]
|
|
297
|
+
def _f(n): return str(n) if n < 1024 else f"{n/1024:.1f}K"
|
|
298
|
+
return f" [{color}]{bar}[/{color}] {est}{_f(cur)}/{_f(mx)} {tool_info}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── REPL 交互循环 ────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def run_repl(agent):
|
|
304
|
+
import termios
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
old_tc = termios.tcgetattr(0)
|
|
308
|
+
except termios.error:
|
|
309
|
+
old_tc = None
|
|
310
|
+
|
|
311
|
+
if old_tc is not None:
|
|
312
|
+
new_tc = termios.tcgetattr(0)
|
|
313
|
+
new_tc[3] = new_tc[3] & ~termios.ECHOCTL
|
|
314
|
+
try:
|
|
315
|
+
termios.tcsetattr(0, termios.TCSADRAIN, new_tc)
|
|
316
|
+
except termios.error:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
_exit_hooks = []
|
|
320
|
+
|
|
321
|
+
def _on_exit(fn):
|
|
322
|
+
_exit_hooks.append(fn)
|
|
323
|
+
|
|
324
|
+
def _run_exit_hooks():
|
|
325
|
+
for hook in reversed(_exit_hooks):
|
|
326
|
+
try:
|
|
327
|
+
hook()
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
if old_tc is not None:
|
|
332
|
+
_on_exit(lambda: termios.tcsetattr(0, termios.TCSADRAIN, old_tc))
|
|
333
|
+
|
|
334
|
+
async def repl():
|
|
335
|
+
loop = asyncio.get_running_loop()
|
|
336
|
+
current_task: asyncio.Task | None = None
|
|
337
|
+
exit_count = 0
|
|
338
|
+
|
|
339
|
+
def handle_interrupt():
|
|
340
|
+
nonlocal current_task, exit_count
|
|
341
|
+
if current_task and not current_task.done():
|
|
342
|
+
current_task.cancel()
|
|
343
|
+
else:
|
|
344
|
+
exit_count += 1
|
|
345
|
+
if exit_count == 1:
|
|
346
|
+
console.print("\n[yellow]再按 Ctrl+C 退出,或输入 /exit。[/yellow]")
|
|
347
|
+
else:
|
|
348
|
+
console.print("\n[dim]再见![/dim]")
|
|
349
|
+
try:
|
|
350
|
+
loop.remove_signal_handler(signal.SIGINT)
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
_run_exit_hooks()
|
|
354
|
+
os._exit(0)
|
|
355
|
+
|
|
356
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
357
|
+
loop.add_signal_handler(sig, handle_interrupt)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
while True:
|
|
361
|
+
try:
|
|
362
|
+
prompt_area = (PromptArea()
|
|
363
|
+
.use(ContextBar()))
|
|
364
|
+
pre_text, prompt = prompt_area.render(agent)
|
|
365
|
+
console.print(f"\n{pre_text}")
|
|
366
|
+
user_input = await asyncio.to_thread(input, prompt)
|
|
367
|
+
user_input = user_input.strip()
|
|
368
|
+
exit_count = 0 # 正常输入,重置计数
|
|
369
|
+
|
|
370
|
+
if not user_input:
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
if user_input.startswith("/"):
|
|
374
|
+
if await handle_command(user_input, agent):
|
|
375
|
+
continue
|
|
376
|
+
else:
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
console.rule("[bold]Agent[/bold]", style="dim")
|
|
380
|
+
current_task = asyncio.current_task()
|
|
381
|
+
response = await agent.run(user_input)
|
|
382
|
+
current_task = None
|
|
383
|
+
|
|
384
|
+
console.print(Panel(Markdown(response), border_style="dim"))
|
|
385
|
+
console.rule(style="dim")
|
|
386
|
+
|
|
387
|
+
except asyncio.CancelledError:
|
|
388
|
+
console.rule(style="dim")
|
|
389
|
+
console.print("\n[dim]操作已取消。再按一次 Ctrl+C 退出。[/dim]")
|
|
390
|
+
current_task = None
|
|
391
|
+
except EOFError:
|
|
392
|
+
console.print("\n[dim]再见![/dim]")
|
|
393
|
+
break
|
|
394
|
+
except KeyboardInterrupt:
|
|
395
|
+
console.print("\n[dim]再见![/dim]")
|
|
396
|
+
break
|
|
397
|
+
except Exception as e:
|
|
398
|
+
current_task = None
|
|
399
|
+
console.print(f"[red]错误: {e}[/red]")
|
|
400
|
+
finally:
|
|
401
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
402
|
+
try:
|
|
403
|
+
loop.remove_signal_handler(sig)
|
|
404
|
+
except (NotImplementedError, RuntimeError):
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
asyncio.run(repl())
|
|
409
|
+
except KeyboardInterrupt:
|
|
410
|
+
pass
|
|
411
|
+
finally:
|
|
412
|
+
_run_exit_hooks()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ── 回调:无子命令时进入交互模式 ─────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
@app.callback(invoke_without_command=True)
|
|
418
|
+
def main_callback(
|
|
419
|
+
ctx: typer.Context,
|
|
420
|
+
config: str = typer.Option(None, "--config", "-c", help="配置文件路径"),
|
|
421
|
+
model: str = typer.Option(None, "--model", "-m", help="指定模型"),
|
|
422
|
+
api_key: str = typer.Option(None, "--api-key", "-k", help="API Key"),
|
|
423
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="开启调试日志"),
|
|
424
|
+
):
|
|
425
|
+
if ctx.invoked_subcommand is not None:
|
|
426
|
+
return
|
|
427
|
+
agent = _setup_agent(config, model, api_key, debug)
|
|
428
|
+
run_repl(agent)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ── 子命令 ────────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
@app.command("run")
|
|
434
|
+
def run_cmd(
|
|
435
|
+
config: str = typer.Option(None, "--config", "-c", help="配置文件路径"),
|
|
436
|
+
model: str = typer.Option(None, "--model", "-m", help="指定模型"),
|
|
437
|
+
api_key: str = typer.Option(None, "--api-key", "-k", help="API Key"),
|
|
438
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="开启调试日志"),
|
|
439
|
+
):
|
|
440
|
+
agent = _setup_agent(config, model, api_key, debug)
|
|
441
|
+
run_repl(agent)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@app.command("init")
|
|
445
|
+
def init_cmd(path: str = typer.Argument(".", help="项目路径")):
|
|
446
|
+
from pathlib import Path
|
|
447
|
+
from merco.core.config import MercoConfig
|
|
448
|
+
|
|
449
|
+
config_path = Path(path) / "merco.json"
|
|
450
|
+
if config_path.exists():
|
|
451
|
+
console.print(f"[yellow]配置已存在: {config_path}[/yellow]")
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
cfg = MercoConfig()
|
|
455
|
+
cfg.save(str(config_path))
|
|
456
|
+
console.print(f"[green]已创建配置: {config_path}[/green]")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@app.command("skills")
|
|
460
|
+
def skills_cmd(
|
|
461
|
+
list: bool = typer.Option(False, "--list", "-l", help="列出已加载技能"),
|
|
462
|
+
path: str = typer.Option(None, "--path", "-p", help="技能目录路径"),
|
|
463
|
+
):
|
|
464
|
+
from merco.skills.loader import SkillLoader
|
|
465
|
+
from merco.skills.registry import SkillRegistry
|
|
466
|
+
|
|
467
|
+
if list:
|
|
468
|
+
registry = SkillRegistry()
|
|
469
|
+
if path:
|
|
470
|
+
registry.load_from_paths([path])
|
|
471
|
+
else:
|
|
472
|
+
registry.load_from_paths(["./.merco/skills", "~/.config/merco/skills"])
|
|
473
|
+
|
|
474
|
+
skills = registry.list_skills()
|
|
475
|
+
if skills:
|
|
476
|
+
console.print(f"[bold]已加载 {len(skills)} 个技能:[/bold]")
|
|
477
|
+
for skill in skills:
|
|
478
|
+
console.print(f" - {skill['name']}: {skill['description']}")
|
|
479
|
+
else:
|
|
480
|
+
console.print("未加载任何技能")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ── 命令处理 ──────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
async def handle_command(cmd: str, agent) -> bool:
|
|
486
|
+
parts = cmd.split(maxsplit=1)
|
|
487
|
+
command = parts[0].lower()
|
|
488
|
+
|
|
489
|
+
if command in ("/exit", "/quit", "/q"):
|
|
490
|
+
console.print("[dim]再见![/dim]")
|
|
491
|
+
return False
|
|
492
|
+
|
|
493
|
+
elif command == "/help":
|
|
494
|
+
console.print(Panel(
|
|
495
|
+
"[bold]可用命令[/bold]\n\n"
|
|
496
|
+
"/help - 显示此帮助\n"
|
|
497
|
+
"/exit - 退出\n"
|
|
498
|
+
"/new - 新会话\n"
|
|
499
|
+
"/model - 显示当前模型\n"
|
|
500
|
+
"/tools - 列出可用工具\n"
|
|
501
|
+
"/context - 上下文用量\n"
|
|
502
|
+
"/skills - 列出已加载技能",
|
|
503
|
+
title="帮助",
|
|
504
|
+
))
|
|
505
|
+
return True
|
|
506
|
+
|
|
507
|
+
elif command == "/new":
|
|
508
|
+
agent.reset()
|
|
509
|
+
console.print("[dim]已开启新会话[/dim]")
|
|
510
|
+
return True
|
|
511
|
+
|
|
512
|
+
elif command == "/model":
|
|
513
|
+
console.print(f"当前模型: {agent.config.model.provider}/{agent.config.model.model}")
|
|
514
|
+
return True
|
|
515
|
+
|
|
516
|
+
elif command == "/context":
|
|
517
|
+
stats = agent.get_context_stats()
|
|
518
|
+
bar = ContextBar()
|
|
519
|
+
console.print(bar.render(agent))
|
|
520
|
+
console.print(f" 阈值: {int(stats['threshold']*100)}% | 模型推算: {'是' if stats['is_estimate'] else '否(API 实测)'}")
|
|
521
|
+
return True
|
|
522
|
+
|
|
523
|
+
elif command == "/tools":
|
|
524
|
+
tools = agent.tool_registry.list_tools() if agent.tool_registry else []
|
|
525
|
+
if tools:
|
|
526
|
+
console.print("[bold]可用工具:[/bold]")
|
|
527
|
+
for tool in tools:
|
|
528
|
+
console.print(f" - {tool.name}: {tool.description}")
|
|
529
|
+
else:
|
|
530
|
+
console.print("无可用工具")
|
|
531
|
+
return True
|
|
532
|
+
|
|
533
|
+
else:
|
|
534
|
+
console.print(f"[dim]未知命令: {command},输入 /help 查看帮助[/dim]")
|
|
535
|
+
return True
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
if __name__ == "__main__":
|
|
539
|
+
app()
|
cli/tui.py
ADDED
merco/__init__.py
ADDED
merco/core/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Mercury Code 核心引擎"""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent, AgentLoop
|
|
4
|
+
from .config import MercoConfig, ModelConfig
|
|
5
|
+
from .session import Session, SessionStore
|
|
6
|
+
from .message import Message, MessageRole
|
|
7
|
+
from .context import ContextManager
|
|
8
|
+
from .llm import LLMClient
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Agent",
|
|
12
|
+
"AgentLoop",
|
|
13
|
+
"MercoConfig",
|
|
14
|
+
"ModelConfig",
|
|
15
|
+
"Session",
|
|
16
|
+
"SessionStore",
|
|
17
|
+
"Message",
|
|
18
|
+
"MessageRole",
|
|
19
|
+
"ContextManager",
|
|
20
|
+
"LLMClient",
|
|
21
|
+
]
|