devmate 2.3.0__tar.gz → 2.4.0__tar.gz

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.
Files changed (54) hide show
  1. {devmate-2.3.0 → devmate-2.4.0}/PKG-INFO +1 -1
  2. {devmate-2.3.0 → devmate-2.4.0}/pyproject.toml +1 -1
  3. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/__init__.py +1 -1
  4. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/app/cli.py +534 -3
  5. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/app/web/__init__.py +355 -24
  6. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/rag/__init__.py +68 -0
  7. devmate-2.4.0/src/devmate/workflow/__init__.py +865 -0
  8. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/PKG-INFO +1 -1
  9. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_agent/__init__.py +298 -7
  10. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_monitor/__init__.py +164 -0
  11. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_toolkit/__init__.py +82 -0
  12. devmate-2.3.0/src/devmate/workflow/__init__.py +0 -315
  13. {devmate-2.3.0 → devmate-2.4.0}/README.md +0 -0
  14. {devmate-2.3.0 → devmate-2.4.0}/setup.cfg +0 -0
  15. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/__main__.py +0 -0
  16. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/ai_config/__init__.py +0 -0
  17. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/ai_scenarios/__init__.py +0 -0
  18. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/app/__init__.py +0 -0
  19. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/app/tui.py +0 -0
  20. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/app/web/cli_docs.py +0 -0
  21. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/bus/__init__.py +0 -0
  22. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/bus/bus.py +0 -0
  23. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/bus/command.py +0 -0
  24. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/core/__init__.py +0 -0
  25. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/core/config.py +0 -0
  26. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/core/logging.py +0 -0
  27. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/core/security.py +0 -0
  28. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/events/__init__.py +0 -0
  29. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/events/bus.py +0 -0
  30. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/events/domain_event.py +0 -0
  31. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/mcp/__init__.py +0 -0
  32. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/module/__init__.py +0 -0
  33. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/module/registry.py +0 -0
  34. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/plugin/__init__.py +0 -0
  35. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/plugin_market/__init__.py +0 -0
  36. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/plugin_market/ratings.py +0 -0
  37. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/plugin_market/remote.py +0 -0
  38. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/security/audit.py +0 -0
  39. {devmate-2.3.0 → devmate-2.4.0}/src/devmate/security/vault.py +0 -0
  40. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/SOURCES.txt +0 -0
  41. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/dependency_links.txt +0 -0
  42. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/entry_points.txt +0 -0
  43. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/requires.txt +0 -0
  44. {devmate-2.3.0 → devmate-2.4.0}/src/devmate.egg-info/top_level.txt +0 -0
  45. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_apidev/__init__.py +0 -0
  46. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_apihub/__init__.py +0 -0
  47. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_dbadmin/__init__.py +0 -0
  48. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_gitflow/__init__.py +0 -0
  49. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_notekeeper/__init__.py +0 -0
  50. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_publisher/__init__.py +0 -0
  51. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_regexlab/__init__.py +0 -0
  52. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_reporter/__init__.py +0 -0
  53. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_scaffold/__init__.py +0 -0
  54. {devmate-2.3.0 → devmate-2.4.0}/src/devmate_sshman/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmate
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: 程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定
5
5
  Author-email: DevMate Team <devmate@example.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devmate"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,6 +1,6 @@
1
1
  """
2
2
  DevMate — 程序员终端瑞士军刀
3
3
  """
4
- __version__ = "2.3.0"
4
+ __version__ = "2.4.0"
5
5
  __app_name__ = "DevMate"
6
6
  __description__ = "程序员终端瑞士军刀——替代 5 个独立工具,一个 CLI 搞定"
@@ -276,6 +276,53 @@ def tk_dup(path, min_size):
276
276
  console.print(f" [dim]{f}[/dim]")
277
277
 
278
278
 
279
+ @tk_group.command("dedup", help="清理重复文件(默认预览)")
280
+ @click.argument("path", default=".")
281
+ @click.option("--apply", is_flag=True, help="实际删除(默认只预览)")
282
+ @click.option("--min-size", default=1, type=int, help="最小文件大小(字节)")
283
+ @click.option("--keep", type=click.Choice(["first", "last"]), default="first",
284
+ help="每组保留哪个:first=最早(默认) last=最新")
285
+ def tk_dedup(path, apply, min_size, keep):
286
+ """清理重复文件——安全模式
287
+
288
+ 默认只预览(列出将删除的文件,不实际删除)。
289
+ 确认无误后加 --apply 才真正删除。每组保留最早(first)或最新(last)的副本。
290
+ """
291
+ from rich.table import Table
292
+
293
+ from devmate_toolkit import clean_duplicates
294
+
295
+ result = clean_duplicates(path, dry_run=not apply,
296
+ min_size=min_size, keep=keep)
297
+ if not result.success:
298
+ console.print(f"[red]❌ {result.error}[/red]")
299
+ return
300
+
301
+ data = result.data
302
+ mode_style = "yellow" if data["mode"] == "dry_run" else "green"
303
+ console.print(f"\n[{mode_style}]{data['message']}[/{mode_style}]\n")
304
+
305
+ if data["to_delete"]:
306
+ table = Table(title=f"待删除文件({data['to_delete_count']} 个,共 {data['freed']})")
307
+ table.add_column("#", style="dim", width=4)
308
+ table.add_column("文件路径", style="cyan")
309
+ for i, f in enumerate(data["to_delete"], 1):
310
+ table.add_row(str(i), f)
311
+ console.print(table)
312
+
313
+ if data["kept"]:
314
+ console.print("[bold]保留的副本:[/bold]")
315
+ for k in data["kept"][:10]:
316
+ console.print(f" [green]✓[/green] [dim]{k}[/dim]")
317
+
318
+ if data["mode"] == "dry_run":
319
+ console.print("\n[dim]提示: 确认无误后加 [yellow]--apply[/yellow] 实际删除[/dim]")
320
+ elif data.get("errors"):
321
+ console.print(f"\n[red]⚠️ {len(data['errors'])} 个文件删除失败:[/red]")
322
+ for e in data["errors"][:5]:
323
+ console.print(f" [red]{e}[/red]")
324
+
325
+
279
326
  @tk_group.command("json", help="JSON 格式化/校验/压缩")
280
327
  @click.option("--path", type=click.Path(exists=False), help="JSON 文件路径")
281
328
  @click.option("--text", "-t", help="JSON 字符串")
@@ -1068,8 +1115,16 @@ def agent_group():
1068
1115
  @agent_group.command("chat", help="与 Agent 对话,完成任务")
1069
1116
  @click.argument("message")
1070
1117
  @click.option("--system", "-s", help="自定义系统提示词")
1071
- def agent_chat(message, system):
1072
- """向 Agent 发送任务,Agent 自动调用 LLM + 11 个工具完成"""
1118
+ @click.option("--dry-run", is_flag=True, help="预览模式:只展示执行计划,不执行")
1119
+ @click.option("--confirm", is_flag=True, help="执行前确认(先预览计划,确认后执行)")
1120
+ @click.option("--mcp", is_flag=True, help="连接已配置的外部 MCP 工具")
1121
+ def agent_chat(message, system, dry_run, confirm, mcp):
1122
+ """向 Agent 发送任务,Agent 自动调用 LLM + 11 个工具完成
1123
+
1124
+ --dry-run 仅生成执行计划不执行(信任机制)
1125
+ --confirm 先展示计划,确认后执行(信任机制)
1126
+ --mcp 动态加载已连接的外部 MCP Server 工具(双向兼容)
1127
+ """
1073
1128
  import uuid
1074
1129
 
1075
1130
  from rich.table import Table
@@ -1081,8 +1136,41 @@ def agent_chat(message, system):
1081
1136
  if debug:
1082
1137
  console.print("[dim]🔍 调试模式 (DEVMATE_AGENT_DEBUG=1)[/dim]")
1083
1138
 
1139
+ runtime = AgentRuntime(system_prompt=system or "", connect_mcp=mcp)
1140
+ if mcp and runtime.external_servers:
1141
+ console.print(f"[dim]🔌 已连接 {len(runtime.external_servers)} 个外部 MCP Server,"
1142
+ f"Agent 工具总数: {len(runtime.tools.list_names())}[/dim]")
1143
+ elif mcp:
1144
+ console.print("[dim]🔌 未配置外部 MCP Server(devmate mcp connect <url> 添加)[/dim]")
1145
+
1146
+ # 信任机制:先预览计划
1147
+ if dry_run or confirm:
1148
+ console.print("[dim]🤖 生成执行计划中…(预览模式,不执行)[/dim]\n")
1149
+ result = runtime.run(message, dry_run=True)
1150
+ data = result.data
1151
+ save_trace(session_id, message, result)
1152
+
1153
+ if data["output"]:
1154
+ console.print(f"[cyan]{data['output']}[/cyan]\n")
1155
+
1156
+ if data.get("planned_tools"):
1157
+ console.print("[yellow]📋 将执行以下工具操作:[/yellow]")
1158
+ for i, t in enumerate(data["planned_tools"], 1):
1159
+ console.print(f" {i}. [cyan]{t}[/cyan]")
1160
+
1161
+ if dry_run:
1162
+ console.print("\n[dim]提示: 确认无误后去掉 --dry-run 实际执行[/dim]")
1163
+ console.print(f"\n[dim]Trace ID: {session_id}[/dim]")
1164
+ return
1165
+
1166
+ # confirm 模式:询问确认
1167
+ if not click.confirm("\n是否执行以上操作?", default=True):
1168
+ console.print("[yellow]已取消执行[/yellow]")
1169
+ console.print(f"\n[dim]Trace ID: {session_id}[/dim]")
1170
+ return
1171
+ console.print("\n[dim]🤖 开始执行…[/dim]\n")
1172
+
1084
1173
  console.print("[dim]🤖 Agent v2.0 思考中…(LLM + 工具调用)[/dim]\n")
1085
- runtime = AgentRuntime(system_prompt=system or "")
1086
1174
  result = runtime.run(message)
1087
1175
  data = result.data
1088
1176
  save_trace(session_id, message, result)
@@ -1108,9 +1196,85 @@ def agent_chat(message, system):
1108
1196
  table.add_row(str(s["step"]), s.get("type", ""), tool_name, status, result_text)
1109
1197
  console.print(table)
1110
1198
 
1199
+ # 变更回滚提示
1200
+ snapshots = runtime.list_snapshots()
1201
+ if snapshots.success and snapshots.data["total"] > 0:
1202
+ console.print(f"\n[dim]💾 本次执行有 {snapshots.data['total']} 处文件变更,"
1203
+ f"可用 [yellow]devmate agent rollback[/yellow] 回滚[/dim]")
1204
+
1111
1205
  console.print(f"\n[dim]Trace ID: {session_id}[/dim]")
1112
1206
 
1113
1207
 
1208
+ @agent_group.command("rollback", help="回滚 Agent 文件变更")
1209
+ @click.option("--index", type=int, help="回滚第几次变更(默认最后一次)")
1210
+ def agent_rollback(index):
1211
+ """回滚 Agent 执行产生的文件变更(信任机制)"""
1212
+ from devmate_agent import AgentRuntime
1213
+
1214
+ runtime = AgentRuntime()
1215
+ result = runtime.rollback(index)
1216
+ if result.success:
1217
+ console.print(f"[green]{result.data['message']}[/green]")
1218
+ else:
1219
+ console.print(f"[red]❌ {result.error}[/red]")
1220
+
1221
+
1222
+ @agent_group.command("snapshots", help="查看 Agent 变更快照")
1223
+ def agent_snapshots():
1224
+ """列出 Agent 执行的变更快照(可回滚)"""
1225
+ from rich.table import Table
1226
+
1227
+ from devmate_agent import AgentRuntime
1228
+
1229
+ runtime = AgentRuntime()
1230
+ result = runtime.list_snapshots()
1231
+ if not result.success:
1232
+ console.print(f"[red]❌ {result.error}[/red]")
1233
+ return
1234
+ data = result.data
1235
+ if data["total"] == 0:
1236
+ console.print("[yellow]暂无变更快照[/yellow]")
1237
+ return
1238
+ table = Table(title=f"变更快照({data['total']} 处,从新到旧)")
1239
+ table.add_column("时间", style="dim")
1240
+ table.add_column("文件", style="cyan")
1241
+ table.add_column("大小", style="yellow")
1242
+ for s in data["snapshots"]:
1243
+ table.add_row(s["time"], s["path"], f"{s['size']}B")
1244
+ console.print(table)
1245
+ console.print("[dim]回滚: devmate agent rollback [--index N][/dim]")
1246
+
1247
+
1248
+ @agent_group.command("audit", help="查看 Agent 操作审计")
1249
+ @click.option("--limit", default=20, help="显示条数")
1250
+ def agent_audit(limit):
1251
+ """查看 Agent 每次工具调用的审计记录(信任机制)"""
1252
+ from rich.table import Table
1253
+
1254
+ from devmate_agent import AgentRuntime
1255
+
1256
+ runtime = AgentRuntime()
1257
+ result = runtime.audit_log(limit=limit)
1258
+ if not result.success:
1259
+ console.print(f"[red]❌ {result.error}[/red]")
1260
+ return
1261
+ data = result.data
1262
+ if data["total"] == 0:
1263
+ console.print("[yellow]暂无审计记录[/yellow]")
1264
+ return
1265
+ table = Table(title=f"Agent 操作审计({data['total']} 条)")
1266
+ table.add_column("时间", style="dim")
1267
+ table.add_column("模式", style="cyan")
1268
+ table.add_column("工具", style="yellow")
1269
+ table.add_column("参数", style="white")
1270
+ table.add_column("耗时", style="dim", justify="right")
1271
+ for e in data["entries"]:
1272
+ args = json.dumps(e.get("args", {}), ensure_ascii=False)[:40] # noqa: F821
1273
+ table.add_row(e["time"], e["action"], e["tool"], args,
1274
+ f"{e['duration_ms']}ms")
1275
+ console.print(table)
1276
+
1277
+
1114
1278
  @agent_group.command("tools", help="列出 Agent 可用工具")
1115
1279
  def agent_tools():
1116
1280
  """列出 Agent 可调用的所有工具"""
@@ -2065,6 +2229,124 @@ def top_port(port):
2065
2229
  console.print(table)
2066
2230
 
2067
2231
 
2232
+ @top_group.command("net", help="网络流量实时监控")
2233
+ @click.option("--interval", default=1.0, type=float, help="采样间隔秒数")
2234
+ def top_net(interval):
2235
+ """实时网络流量(收/发速率 + 总流量)"""
2236
+ from rich.table import Table
2237
+
2238
+ from devmate_monitor import network_usage
2239
+
2240
+ console.print(f"[dim]采样中({interval}s)…[/dim]")
2241
+ result = network_usage(interval=interval)
2242
+ if not result.success:
2243
+ console.print(f"[red]❌ {result.error}[/red]")
2244
+ return
2245
+
2246
+ data = result.data
2247
+ table = Table(title="🌐 网络流量")
2248
+ table.add_column("方向", style="cyan", width=10)
2249
+ table.add_column("速率", style="yellow")
2250
+ table.add_column("总流量", style="green")
2251
+ table.add_row("📥 接收", data["recv_speed"], data["total_recv"])
2252
+ table.add_row("📤 发送", data["sent_speed"], data["total_sent"])
2253
+ console.print(table)
2254
+
2255
+
2256
+ @top_group.command("disk", help="所有磁盘分区用量")
2257
+ def top_disk():
2258
+ """列出所有磁盘分区/挂载点用量(按使用率排序)"""
2259
+ from rich.table import Table
2260
+
2261
+ from devmate_monitor import disk_usage_all
2262
+
2263
+ result = disk_usage_all()
2264
+ if not result.success:
2265
+ console.print(f"[red]❌ {result.error}[/red]")
2266
+ return
2267
+
2268
+ partitions = result.data["partitions"]
2269
+ if not partitions:
2270
+ console.print("[yellow]未检测到磁盘分区[/yellow]")
2271
+ return
2272
+
2273
+ table = Table(title=f"💾 磁盘分区({result.data['total']} 个,按使用率排序)")
2274
+ table.add_column("挂载点", style="cyan")
2275
+ table.add_column("设备", style="dim")
2276
+ table.add_column("已用", style="yellow")
2277
+ table.add_column("可用", style="green")
2278
+ table.add_column("总容量", style="white")
2279
+ table.add_column("使用率", style="bold", justify="right")
2280
+ for p in partitions:
2281
+ percent = p["percent"]
2282
+ color = "red" if percent >= 90 else ("yellow" if percent >= 75 else "green")
2283
+ table.add_row(p["mountpoint"], p["device"], p["used"],
2284
+ p["free"], p["total"], f"[{color}]{percent:.1f}%[/{color}]")
2285
+ console.print(table)
2286
+
2287
+
2288
+ @top_group.command("alert", help="资源阈值告警检查")
2289
+ @click.option("--cpu", default=80, type=int, help="CPU 阈值 %")
2290
+ @click.option("--mem", default=80, type=int, help="内存阈值 %")
2291
+ @click.option("--disk", default=90, type=int, help="磁盘阈值 %")
2292
+ def top_alert(cpu, mem, disk):
2293
+ """检查 CPU/内存/磁盘是否超过阈值"""
2294
+ from rich.table import Table
2295
+
2296
+ from devmate_monitor import alert_check
2297
+
2298
+ result = alert_check(cpu_limit=cpu, mem_limit=mem, disk_limit=disk)
2299
+ if not result.success:
2300
+ console.print(f"[red]❌ {result.error}[/red]")
2301
+ return
2302
+
2303
+ data = result.data
2304
+ status_icon = "[red]🚨 告警[/red]" if data["status"] == "ALERT" else "[green]✅ 正常[/green]"
2305
+ console.print(f"\n{status_icon}({data['alert_count']} 项告警)\n")
2306
+
2307
+ table = Table(title=f"阈值: CPU>{cpu}% 内存>{mem}% 磁盘>{disk}%")
2308
+ table.add_column("指标", style="cyan")
2309
+ table.add_column("当前值", style="white")
2310
+ table.add_column("阈值", style="dim")
2311
+ table.add_column("状态", style="bold")
2312
+ for name, info in data["summary"].items():
2313
+ icon = "[red]🚨[/red]" if info["alert"] else "[green]✅[/green]"
2314
+ table.add_row(name, f"{info['value']:.1f}{info['unit']}",
2315
+ f"{info['limit']}{info['unit']}", icon)
2316
+ console.print(table)
2317
+
2318
+ if data["alerts"]:
2319
+ console.print("\n[bold red]告警详情:[/bold red]")
2320
+ for a in data["alerts"]:
2321
+ console.print(f" [red]⚠️ {a}[/red]")
2322
+
2323
+
2324
+ @top_group.command("battery", help="电池状态")
2325
+ def top_battery():
2326
+ """电池状态(笔记本;台式机可能不可用)"""
2327
+ from devmate_monitor import battery_status
2328
+
2329
+ result = battery_status()
2330
+ if not result.success:
2331
+ console.print(f"[red]❌ {result.error}[/red]")
2332
+ return
2333
+
2334
+ data = result.data
2335
+ if not data.get("available"):
2336
+ console.print(f"[yellow]🔋 {data.get('message', '电池不可用')}[/yellow]")
2337
+ return
2338
+
2339
+ percent = data["percent"]
2340
+ bar = "█" * (percent // 5) + "░" * (20 - percent // 5)
2341
+ color = "green" if percent > 50 else ("yellow" if percent > 20 else "red")
2342
+ console.print(f"\n🔋 [bold]电池: {percent}%[/bold] [{color}]{bar}[/{color}]")
2343
+ console.print(f"状态: {data['status']}")
2344
+ secs = data.get("remaining_sec")
2345
+ if isinstance(secs, int) and secs >= 0:
2346
+ mins, _ = divmod(secs, 60)
2347
+ console.print(f"剩余时间: ~{mins} 分钟")
2348
+
2349
+
2068
2350
  @cli.group("report", help="日报/周报自动生成")
2069
2351
  def report_group():
2070
2352
  """reporter — 报告生成"""
@@ -2919,6 +3201,58 @@ def wf_rm_step(name, index):
2919
3201
  else f"[red]❌ {result.error}[/red]")
2920
3202
 
2921
3203
 
3204
+ @wf_group.command("template", help="管理工作流模板")
3205
+ @click.argument("action", type=click.Choice(["list", "new"]))
3206
+ @click.argument("name", required=False)
3207
+ @click.argument("template", required=False)
3208
+ @click.option("--param", "-p", multiple=True, help="模板参数 key=value")
3209
+ def wf_template(action, name, template, param):
3210
+ """列出内置模板 / 从模板创建工作流(闭环编排)"""
3211
+ from rich.table import Table
3212
+
3213
+ from devmate.workflow import create_from_template, list_templates
3214
+
3215
+ if action == "list":
3216
+ result = list_templates()
3217
+ if not result.success:
3218
+ console.print(f"[red]❌ {result.error}[/red]")
3219
+ return
3220
+ data = result.data
3221
+ table = Table(title=f"内置工作流模板({data['total']} 个)——跨模块闭环")
3222
+ table.add_column("模板", style="cyan")
3223
+ table.add_column("步骤", style="yellow", justify="right")
3224
+ table.add_column("工具链", style="green")
3225
+ table.add_column("描述", style="white")
3226
+ for t in data["templates"]:
3227
+ table.add_row(t["name"], str(t["step_count"]),
3228
+ " → ".join(t["tools"]), t["description"])
3229
+ console.print(table)
3230
+ console.print("\n[dim]使用: devmate wf template new <工作流名> <模板名> -p 参数=值[/dim]")
3231
+ console.print("[dim]示例: devmate wf template new 每日巡检 daily-check -p topic=数据库 -p output=~/reports[/dim]") # noqa: E501
3232
+ return
3233
+
3234
+ # action == "new"
3235
+ if not name or not template:
3236
+ console.print("[red]❌ new 需要 <工作流名> <模板名>[/red]")
3237
+ return
3238
+ params = {}
3239
+ for p in param:
3240
+ if "=" in p:
3241
+ k, v = p.split("=", 1)
3242
+ params[k.strip()] = v.strip()
3243
+ result = create_from_template(name, template, params)
3244
+ if not result.success:
3245
+ console.print(f"[red]❌ {result.error}[/red]")
3246
+ return
3247
+ data = result.data
3248
+ console.print(f"\n[green]{data['message']}[/green]")
3249
+ if data.get("unresolved_params"):
3250
+ console.print(f"[yellow]未提供参数(用默认值): {', '.join(data['unresolved_params'])}[/yellow]") # noqa: E501
3251
+ tool_chain = " → ".join(s["tool"] for s in data["steps"])
3252
+ console.print(f"[dim]步骤: {tool_chain}[/dim]")
3253
+ console.print("[dim]执行: devmate wf run <工作流名>[/dim]")
3254
+
3255
+
2922
3256
  @cli.group("knowledge", help="RAG 知识库管理")
2923
3257
  def knowledge_group():
2924
3258
  """RAG 知识库——向量检索笔记"""
@@ -3146,6 +3480,203 @@ def mcp_disconnect_cmd(server_url):
3146
3480
  console.print(f"[green]{result.data['message']}[/green]")
3147
3481
 
3148
3482
 
3483
+ @mcp_group.command("list", help="列出已连接的 MCP 服务器")
3484
+ def mcp_list_cmd():
3485
+ """查看所有已保存的外部 MCP 服务器连接"""
3486
+ import json as _json
3487
+ from pathlib import Path as _Path
3488
+
3489
+ from rich.table import Table
3490
+
3491
+ config_path = _Path.home() / ".devmate" / "mcp_servers.json"
3492
+ if not config_path.exists():
3493
+ console.print("[yellow]暂无已连接的 MCP 服务器[/yellow]")
3494
+ console.print("[dim]连接: devmate mcp connect <url>[/dim]")
3495
+ return
3496
+
3497
+ try:
3498
+ servers = _json.loads(config_path.read_text())
3499
+ except Exception:
3500
+ servers = []
3501
+ if not servers:
3502
+ console.print("[yellow]暂无已连接的 MCP 服务器[/yellow]")
3503
+ return
3504
+
3505
+ table = Table(title=f"已连接的 MCP 服务器({len(servers)} 个)")
3506
+ table.add_column("#", style="dim")
3507
+ table.add_column("服务器地址", style="cyan")
3508
+ for i, s in enumerate(servers, 1):
3509
+ table.add_row(str(i), s)
3510
+ console.print(table)
3511
+ console.print("\n[dim]Agent 集成: devmate agent chat \"...\" --mcp 可让 Agent 使用这些外部工具[/dim]") # noqa: E501
3512
+
3513
+
3514
+ @mcp_group.command("agent-tools", help="查看 Agent 可用的 MCP 工具")
3515
+ def mcp_agent_tools_cmd():
3516
+ """查看 Agent 动态注册的外部 MCP 工具(双向兼容)"""
3517
+ from rich.table import Table
3518
+
3519
+ from devmate_agent import AgentRuntime
3520
+
3521
+ runtime = AgentRuntime(connect_mcp=True)
3522
+ tools = runtime.list_available_tools()
3523
+ external = [t for t in tools if t["name"].startswith("external_")]
3524
+
3525
+ if not external:
3526
+ console.print("[yellow]当前没有外部 MCP 工具(先 devmate mcp connect <url>)[/yellow]")
3527
+ return
3528
+
3529
+ table = Table(title=f"Agent 已注册的外部 MCP 工具({len(external)} 个)")
3530
+ table.add_column("工具名", style="cyan")
3531
+ table.add_column("说明", style="white")
3532
+ for t in external:
3533
+ table.add_row(t["name"], t["description"].replace("[外部MCP] ", ""))
3534
+ console.print(table)
3535
+
3536
+
3537
+ # ═══════════════════════════════════════════════════
3538
+ # 3 分钟 Aha 场景(devmate demo)
3539
+ # ═══════════════════════════════════════════════════
3540
+
3541
+ _DEMO_SCENARIOS = [
3542
+ {
3543
+ "id": "daily",
3544
+ "title": "📊 每日巡检(3 个工具一次搞定)",
3545
+ "desc": "系统监控 → 检索笔记 → 自动生成日报",
3546
+ "tools": "top dashboard + note search + 报告生成",
3547
+ "workflow": "daily-check",
3548
+ "params": {"topic": "开发", "output": "~/.devmate/demo"},
3549
+ },
3550
+ {
3551
+ "id": "api",
3552
+ "title": "🌐 API 调试(替代 Postman)",
3553
+ "desc": "发送请求 → 保存到集合 → 复用执行",
3554
+ "tools": "api request + api collection + api env",
3555
+ "workflow": "api-health",
3556
+ "params": {"url": "https://httpbin.org/get", "url2": "https://httpbin.org/status/500",
3557
+ "output": "~/.devmate/demo"},
3558
+ },
3559
+ {
3560
+ "id": "files",
3561
+ "title": "📁 文件整理(替代手动拖拽)",
3562
+ "desc": "扫描目录 → 按类型整理 → 生成清理报告",
3563
+ "tools": "tk size + tk organize + 报告生成",
3564
+ "workflow": "disk-cleanup",
3565
+ "params": {"path": "~/Downloads", "output": "~/.devmate/demo"},
3566
+ },
3567
+ ]
3568
+
3569
+
3570
+ @cli.command("demo", help="3 分钟体验 DevMate(Aha 场景)")
3571
+ @click.option("--scenario", type=click.Choice(["daily", "api", "files"]),
3572
+ help="指定场景(默认交互选择)")
3573
+ @click.option("--list", "list_only", is_flag=True, help="列出可用场景")
3574
+ def demo_cmd(scenario, list_only):
3575
+ """3 分钟完成一个"原来要开 3 个工具"的真实任务闭环
3576
+
3577
+ 零配置、无需 LLM:每个场景自动调用 DevMate 模块完成真实操作
3578
+ """
3579
+ import time as _time
3580
+
3581
+ from rich.table import Table as _Table
3582
+
3583
+ if list_only:
3584
+ table = _Table(title="DevMate Aha 场景(3 分钟体验)")
3585
+ table.add_column("ID", style="cyan")
3586
+ table.add_column("场景", style="bold")
3587
+ table.add_column("说明", style="white")
3588
+ table.add_column("工具链", style="green")
3589
+ for s in _DEMO_SCENARIOS:
3590
+ table.add_row(s["id"], s["title"], s["desc"], s["tools"])
3591
+ console.print(table)
3592
+ console.print("\n[dim]体验: devmate demo --scenario daily | api | files[/dim]")
3593
+ return
3594
+
3595
+ # 选择场景
3596
+ selected = None
3597
+ if scenario:
3598
+ selected = next((s for s in _DEMO_SCENARIOS if s["id"] == scenario), None)
3599
+ else:
3600
+ console.print(Panel(
3601
+ "[bold green]⚡ 3 分钟体验 DevMate[/bold green]\n"
3602
+ "[dim]选一个场景,自动完成一个原来要开 3 个工具才能完成的任务[/dim]",
3603
+ border_style="green", padding=(1, 2)))
3604
+ table = _Table(show_header=False, box=None)
3605
+ table.add_column("#", style="dim", width=4)
3606
+ table.add_column("场景", style="bold cyan")
3607
+ table.add_column("说明", style="white")
3608
+ for i, s in enumerate(_DEMO_SCENARIOS, 1):
3609
+ table.add_row(str(i), s["title"], s["desc"])
3610
+ console.print(table)
3611
+ console.print()
3612
+ choice = click.prompt("选择场景编号", type=int, default=1)
3613
+ if choice < 1 or choice > len(_DEMO_SCENARIOS):
3614
+ console.print(f"[red]无效编号: {choice}[/red]")
3615
+ return
3616
+ selected = _DEMO_SCENARIOS[choice - 1]
3617
+
3618
+ wf_name = f"demo-{selected['id']}-{int(_time.time())}"
3619
+
3620
+ console.print(f"\n[bold cyan]{selected['title']}[/bold cyan]")
3621
+ console.print(f"[dim]{selected['desc']}[/dim]")
3622
+ console.print(f"[green]工具链: {selected['tools']}[/green]\n")
3623
+
3624
+ # 步骤 1: 从模板创建
3625
+ console.print("[dim]① 创建场景工作流…[/dim]")
3626
+ from devmate.workflow import create_from_template
3627
+ from devmate.workflow import run as run_workflow
3628
+ result = create_from_template(wf_name, selected["workflow"],
3629
+ params=selected["params"])
3630
+ if not result.success:
3631
+ console.print(f"[red]❌ {result.error}[/red]")
3632
+ return
3633
+
3634
+ # 步骤 2: 执行闭环
3635
+ console.print(f"[dim]② 执行 {len(result.data['steps'])} 步闭环任务…[/dim]\n")
3636
+ run_result = run_workflow(wf_name)
3637
+
3638
+ # 步骤 3: 展示结果
3639
+ if not run_result.success:
3640
+ console.print(f"[red]❌ 执行失败: {run_result.error}[/red]")
3641
+ return
3642
+ data = run_result.data
3643
+
3644
+ steps_table = _Table(title=f"✅ 任务完成({data['success_count']} 成功 / {data['fail_count']} 失败,{data['duration_ms']}ms)") # noqa: E501
3645
+ steps_table.add_column("#", style="dim")
3646
+ steps_table.add_column("步骤", style="cyan")
3647
+ steps_table.add_column("工具", style="yellow")
3648
+ steps_table.add_column("结果", style="white", width=55)
3649
+ icons = {"success": "[green]✅[/green]", "error": "[red]❌[/red]"}
3650
+ for i, s in enumerate(data["steps"], 1):
3651
+ icon = icons.get(s["status"], "[dim]⏭️[/dim]")
3652
+ text = s.get("error", "") or s.get("result", "")[:55]
3653
+ steps_table.add_row(str(i), s["step_id"], s["tool"], f"{icon} {text}")
3654
+ console.print(steps_table)
3655
+
3656
+ # 报告产物路径
3657
+ from pathlib import Path as _Path
3658
+ report_dir = _Path(selected["params"]["output"]).expanduser()
3659
+ if report_dir.exists():
3660
+ reports = sorted(report_dir.glob("*.md"))
3661
+ if reports:
3662
+ console.print("\n[bold green]📄 生成报告:[/bold green]")
3663
+ for rp in reports[-3:]:
3664
+ console.print(f" [cyan]{rp}[/cyan]")
3665
+ try:
3666
+ first_lines = rp.read_text()[:120]
3667
+ console.print(f" [dim]{first_lines.replace(chr(10), chr(10)+' ')}[/dim]")
3668
+ except Exception:
3669
+ pass
3670
+
3671
+ # 清理临时工作流
3672
+ from devmate.workflow import delete as delete_wf
3673
+ delete_wf(wf_name)
3674
+
3675
+ console.print("\n[bold green]🎉 3 分钟体验完成![/bold green]")
3676
+ console.print(f"[dim]真实工具链: {selected['tools']}[/dim]")
3677
+ console.print("[dim]继续探索: devmate --help | devmate wf template list[/dim]\n")
3678
+
3679
+
3149
3680
  @cli.command("web", help="启动 Web 面板")
3150
3681
  @click.option("--host", default="127.0.0.1", help="监听地址")
3151
3682
  @click.option("--port", default=8600, help="监听端口")