jarvis-ai-assistant 0.5.1__py3-none-any.whl → 0.6.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.
Files changed (29) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +15 -4
  3. jarvis/jarvis_agent/agent_manager.py +3 -0
  4. jarvis/jarvis_agent/jarvis.py +44 -14
  5. jarvis/jarvis_agent/run_loop.py +6 -1
  6. jarvis/jarvis_agent/task_planner.py +1 -0
  7. jarvis/jarvis_c2rust/__init__.py +13 -0
  8. jarvis/jarvis_c2rust/cli.py +405 -0
  9. jarvis/jarvis_c2rust/collector.py +209 -0
  10. jarvis/jarvis_c2rust/library_replacer.py +933 -0
  11. jarvis/jarvis_c2rust/llm_module_agent.py +1265 -0
  12. jarvis/jarvis_c2rust/scanner.py +1671 -0
  13. jarvis/jarvis_c2rust/transpiler.py +1236 -0
  14. jarvis/jarvis_code_agent/code_agent.py +144 -18
  15. jarvis/jarvis_data/config_schema.json +8 -3
  16. jarvis/jarvis_tools/cli/main.py +1 -0
  17. jarvis/jarvis_tools/execute_script.py +1 -1
  18. jarvis/jarvis_tools/read_code.py +11 -1
  19. jarvis/jarvis_tools/read_symbols.py +129 -0
  20. jarvis/jarvis_tools/registry.py +9 -1
  21. jarvis/jarvis_utils/config.py +14 -4
  22. jarvis/jarvis_utils/git_utils.py +39 -0
  23. jarvis/jarvis_utils/utils.py +13 -5
  24. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/METADATA +13 -1
  25. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/RECORD +29 -21
  26. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/entry_points.txt +2 -0
  27. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/WHEEL +0 -0
  28. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/licenses/LICENSE +0 -0
  29. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/top_level.txt +0 -0
jarvis/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.5.1"
4
+ __version__ = "0.6.0"
@@ -77,6 +77,7 @@ from jarvis.jarvis_utils.config import (
77
77
  get_tool_filter_threshold,
78
78
  get_after_tool_call_cb_dirs,
79
79
  get_plan_max_depth,
80
+ is_plan_enabled,
80
81
  )
81
82
  from jarvis.jarvis_utils.embedding import get_context_token_count
82
83
  from jarvis.jarvis_utils.globals import (
@@ -317,11 +318,12 @@ class Agent:
317
318
  use_methodology: Optional[bool] = None,
318
319
  use_analysis: Optional[bool] = None,
319
320
  force_save_memory: Optional[bool] = None,
321
+ disable_file_edit: bool = False,
320
322
  files: Optional[List[str]] = None,
321
323
  confirm_callback: Optional[Callable[[str, bool], bool]] = None,
322
324
  non_interactive: Optional[bool] = None,
323
325
  in_multi_agent: Optional[bool] = None,
324
- plan: bool = False,
326
+ plan: Optional[bool] = None,
325
327
  plan_max_depth: Optional[int] = None,
326
328
  plan_depth: int = 0,
327
329
  agent_type: str = "normal",
@@ -344,7 +346,7 @@ class Agent:
344
346
  force_save_memory: 是否强制保存记忆
345
347
  confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
346
348
  non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
347
- plan: 是否启用任务规划与子任务拆分(默认 False;启用后在进入主循环前评估是否需要将任务拆分为 <SUB_TASK> 列表,逐一由子Agent执行并汇总结果)
349
+ plan: 是否启用任务规划与子任务拆分(默认从配置加载;启用后在进入主循环前评估是否需要将任务拆分为 <SUB_TASK> 列表,逐一由子Agent执行并汇总结果)
348
350
  plan_max_depth: 任务规划的最大层数(默认3,可通过配置 JARVIS_PLAN_MAX_DEPTH 或入参覆盖)
349
351
  plan_depth: 当前规划层数(内部用于递归控制,子Agent会在父基础上+1)
350
352
  """
@@ -363,6 +365,7 @@ class Agent:
363
365
  self.execute_tool_confirm = execute_tool_confirm
364
366
  self.summary_prompt = summary_prompt
365
367
  self.force_save_memory = force_save_memory
368
+ self.disable_file_edit = bool(disable_file_edit)
366
369
  # 资源与环境
367
370
  self.model_group = model_group
368
371
  self.files = files or []
@@ -370,7 +373,8 @@ class Agent:
370
373
  self.non_interactive = non_interactive
371
374
  # 多智能体运行标志:用于控制非交互模式下的自动完成行为
372
375
  self.in_multi_agent = bool(in_multi_agent)
373
- self.plan = bool(plan)
376
+ # 任务规划:优先使用入参,否则回退到配置
377
+ self.plan = bool(plan) if plan is not None else is_plan_enabled()
374
378
  # 规划深度与上限
375
379
  try:
376
380
  self.plan_max_depth = (
@@ -511,7 +515,13 @@ class Agent:
511
515
  use_tools: List[str],
512
516
  ):
513
517
  """初始化各种处理器"""
514
- self.output_handler = output_handler or [ToolRegistry(), EditFileHandler(), RewriteFileHandler()]
518
+ default_handlers = [ToolRegistry()]
519
+ if not getattr(self, "disable_file_edit", False):
520
+ default_handlers.extend([EditFileHandler(), RewriteFileHandler()])
521
+ handlers = output_handler or default_handlers
522
+ if getattr(self, "disable_file_edit", False):
523
+ handlers = [h for h in handlers if not isinstance(h, (EditFileHandler, RewriteFileHandler))]
524
+ self.output_handler = handlers
515
525
  self.set_use_tools(use_tools)
516
526
  self.input_handler = [
517
527
  builtin_input_handler,
@@ -1238,6 +1248,7 @@ class Agent:
1238
1248
  "use_methodology": self.use_methodology,
1239
1249
  "use_analysis": self.use_analysis,
1240
1250
  "force_save_memory": self.force_save_memory,
1251
+ "disable_file_edit": self.disable_file_edit,
1241
1252
  "files": self.files,
1242
1253
  "confirm_callback": self.confirm_callback,
1243
1254
  "non_interactive": True,
@@ -32,6 +32,7 @@ class AgentManager:
32
32
  multiline_inputer: Optional[Callable[[str], str]] = None,
33
33
  confirm_callback: Optional[Callable[[str, bool], bool]] = None,
34
34
  non_interactive: Optional[bool] = None,
35
+ plan: Optional[bool] = None,
35
36
  ):
36
37
  self.model_group = model_group
37
38
  self.tool_group = tool_group
@@ -43,6 +44,7 @@ class AgentManager:
43
44
  self.multiline_inputer = multiline_inputer
44
45
  self.confirm_callback = confirm_callback
45
46
  self.non_interactive = non_interactive
47
+ self.plan = plan
46
48
 
47
49
  def initialize(self) -> Agent:
48
50
  """初始化Agent"""
@@ -61,6 +63,7 @@ class AgentManager:
61
63
  multiline_inputer=self.multiline_inputer,
62
64
  confirm_callback=self.confirm_callback,
63
65
  non_interactive=self.non_interactive,
66
+ plan=self.plan,
64
67
  )
65
68
 
66
69
  # 尝试恢复会话
@@ -370,10 +370,30 @@ def handle_builtin_config_selector(
370
370
  """在进入默认通用代理前,列出内置配置供选择(agent/multi_agent/roles)。"""
371
371
  if is_enable_builtin_config_selector():
372
372
  try:
373
- # 优先使用项目内置目录,若不存在则回退到指定的绝对路径
374
- builtin_root = Path(__file__).resolve().parents[3] / "builtin"
375
- if not builtin_root.exists():
376
- builtin_root = Path("/home/skyfire/code/Jarvis/builtin")
373
+ # 查找可用的 builtin 目录(支持多候选)
374
+ builtin_dirs: List[Path] = []
375
+ try:
376
+ ancestors = list(Path(__file__).resolve().parents)
377
+ for anc in ancestors[:8]:
378
+ p = anc / "builtin"
379
+ if p.exists():
380
+ builtin_dirs.append(p)
381
+ except Exception:
382
+ pass
383
+ # 去重,保留顺序
384
+ _seen = set()
385
+ _unique: List[Path] = []
386
+ for d in builtin_dirs:
387
+ try:
388
+ key = str(d.resolve())
389
+ except Exception:
390
+ key = str(d)
391
+ if key not in _seen:
392
+ _seen.add(key)
393
+ _unique.append(d)
394
+ builtin_dirs = _unique
395
+ # 向后兼容:保留第一个候选作为 builtin_root
396
+ builtin_root = builtin_dirs[0] if builtin_dirs else None # type: ignore[assignment]
377
397
 
378
398
  categories = [
379
399
  ("agent", "jarvis-agent", "*.yaml"),
@@ -414,8 +434,14 @@ def handle_builtin_config_selector(
414
434
  # 忽略配置读取异常
415
435
  pass
416
436
 
417
- # 追加内置目录
418
- search_dirs.append(builtin_root / cat)
437
+ # 追加内置目录(支持多个候选)
438
+ try:
439
+ candidates = builtin_dirs if isinstance(builtin_dirs, list) and builtin_dirs else ([builtin_root] if builtin_root else [])
440
+ except Exception:
441
+ candidates = ([builtin_root] if builtin_root else [])
442
+ for _bd in candidates:
443
+ if _bd:
444
+ search_dirs.append(Path(_bd) / cat)
419
445
 
420
446
  # 去重并保留顺序
421
447
  unique_dirs = []
@@ -429,11 +455,14 @@ def handle_builtin_config_selector(
429
455
  seen.add(key)
430
456
  unique_dirs.append(Path(d))
431
457
 
432
- # 每日自动更新配置目录(如目录为Git仓库则执行git pull,每日仅一次)
458
+ # 可选调试输出:查看每类的搜索目录
433
459
  try:
434
- jutils.daily_check_git_updates([str(p) for p in unique_dirs], cat)
460
+ if os.environ.get("JARVIS_DEBUG_BUILTIN_SELECTOR") == "1":
461
+ PrettyOutput.print(
462
+ f"DEBUG: category={cat} search_dirs=" + ", ".join(str(p) for p in unique_dirs),
463
+ OutputType.INFO,
464
+ )
435
465
  except Exception:
436
- # 忽略更新过程中的所有异常,避免影响主流程
437
466
  pass
438
467
 
439
468
  for dir_path in unique_dirs:
@@ -681,7 +710,7 @@ def run_cli(
681
710
  non_interactive: bool = typer.Option(
682
711
  False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
683
712
  ),
684
- plan: bool = typer.Option(False, "--plan/--no-plan", help="启用或禁用任务规划(拆分子任务并汇总执行结果)"),
713
+ plan: Optional[bool] = typer.Option(None, "--plan/--no-plan", help="启用或禁用任务规划(不指定则从配置加载)"),
685
714
  web: bool = typer.Option(False, "--web", help="以 Web 模式启动,通过浏览器 WebSocket 交互"),
686
715
  web_host: str = typer.Option("127.0.0.1", "--web-host", help="Web 服务主机"),
687
716
  web_port: int = typer.Option(8765, "--web-port", help="Web 服务端口"),
@@ -1024,10 +1053,11 @@ def run_cli(
1024
1053
  )
1025
1054
  agent = agent_manager.initialize()
1026
1055
  # CLI 开关:启用/禁用规划(不依赖 AgentManager 支持,直接设置 Agent 属性)
1027
- try:
1028
- agent.plan = bool(plan)
1029
- except Exception:
1030
- pass
1056
+ if plan is not None:
1057
+ try:
1058
+ agent.plan = bool(plan)
1059
+ except Exception:
1060
+ pass
1031
1061
 
1032
1062
  if web:
1033
1063
  try:
@@ -125,7 +125,12 @@ class AgentRunLoop:
125
125
 
126
126
  # 检查自动完成
127
127
  if ag.auto_complete and is_auto_complete(current_response):
128
- return ag._complete_task(auto_completed=True)
128
+ # 先运行_complete_task,触发记忆整理/事件等副作用,再决定返回值
129
+ result = ag._complete_task(auto_completed=True)
130
+ # 若不需要summary,则将最后一条LLM输出作为返回值
131
+ if not getattr(ag, "need_summary", True):
132
+ return current_response
133
+ return result
129
134
 
130
135
  # 获取下一步用户输入
131
136
  next_action = ag._get_next_user_action()
@@ -75,6 +75,7 @@ class TaskPlanner:
75
75
  "<PLAN>\n- 子任务1\n- 子任务2\n</PLAN>\n"
76
76
  "示例:\n"
77
77
  "<PLAN>\n- 分析当前任务,提取需要修改的文件列表\n- 修改配置默认值并更新相关 schema\n- 更新文档中对该默认值的描述\n</PLAN>\n"
78
+ "注意:不要将步骤拆分太细,一般不要超过4个步骤。\n"
78
79
  "要求:<PLAN> 内必须是有效 YAML 列表,仅包含字符串项;禁止输出任何额外解释。\n"
79
80
  "当不需要拆分时,仅输出:\n<DONT_NEED/>\n"
80
81
  "禁止输出任何额外解释。"
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Jarvis C2Rust 工具集。
4
+
5
+ 模块:
6
+ - scanner: C/C++ 函数扫描器和调用图提取器,将结果存储在
7
+ <scan_root>/.jarvis/c2rust/functions.jsonl 和 types.jsonl 的 JSONL 文件中。
8
+
9
+ 用法:
10
+ python -m jarvis.jarvis_c2rust.scanner --root /path/to/src
11
+ """
12
+
13
+ __all__ = ["scanner"]
@@ -0,0 +1,405 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ C2Rust 独立命令行入口。
4
+
5
+ 提供分组式 CLI,将扫描能力作为子命令 scan 暴露:
6
+ - jarvis-c2rust scan --root <path> [--dot ...] [--only-dot] [--subgraphs-dir ...] [--only-subgraphs] [--png]
7
+
8
+ 实现策略:
9
+ - 复用 scanner.cli 的核心逻辑,避免重复代码。
10
+ - 使用 Typer 分组式结构,便于后续扩展更多子命令(如 analyze/export 等)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Optional, List
17
+
18
+ import typer
19
+ from jarvis.jarvis_c2rust.scanner import run_scan as _run_scan
20
+ from jarvis.jarvis_c2rust.scanner import (
21
+ compute_translation_order_jsonl as _compute_order,
22
+ )
23
+ from jarvis.jarvis_c2rust.library_replacer import (
24
+ apply_library_replacement as _apply_library_replacement,
25
+ )
26
+ from jarvis.jarvis_utils.utils import init_env
27
+ from jarvis.jarvis_c2rust.llm_module_agent import (
28
+ execute_llm_plan as _execute_llm_plan,
29
+ entries_to_yaml as _entries_to_yaml,
30
+ )
31
+
32
+ app = typer.Typer(help="C2Rust 命令行工具")
33
+
34
+ # 显式定义根回调,确保为命令组而非单函数入口
35
+ @app.callback()
36
+ def _root():
37
+ """
38
+ C2Rust 命令行工具
39
+ """
40
+ # 不做任何处理,仅作为命令组的占位,使 'scan' 作为子命令出现
41
+ init_env("欢迎使用 Jarvis C2Rust 工具")
42
+ pass
43
+
44
+
45
+ @app.command("scan")
46
+ def scan(
47
+ dot: Optional[Path] = typer.Option(
48
+ None,
49
+ "--dot",
50
+ help="扫描后将引用依赖图写入 DOT 文件(或与 --only-dot 一起使用)",
51
+ ),
52
+ only_dot: bool = typer.Option(
53
+ False,
54
+ "--only-dot",
55
+ help="不重新扫描。读取现有数据 (JSONL) 并仅生成 DOT(需要 --dot)",
56
+ ),
57
+ subgraphs_dir: Optional[Path] = typer.Option(
58
+ None,
59
+ "--subgraphs-dir",
60
+ help="用于写入每个根函数引用子图 DOT 文件的目录(每个根函数一个文件)",
61
+ ),
62
+ only_subgraphs: bool = typer.Option(
63
+ False,
64
+ "--only-subgraphs",
65
+ help="不重新扫描。仅生成每个根函数的引用子图 DOT 文件(需要 --subgraphs-dir)",
66
+ ),
67
+ ) -> None:
68
+ """
69
+ 进行 C/C++ 函数扫描并生成引用关系 DOT 图;PNG 渲染默认启用(无需参数)。
70
+ """
71
+ _run_scan(
72
+ dot=dot,
73
+ only_dot=only_dot,
74
+ subgraphs_dir=subgraphs_dir,
75
+ only_subgraphs=only_subgraphs,
76
+ png=True,
77
+ )
78
+
79
+ @app.command("prepare")
80
+ def prepare(
81
+ llm_group: Optional[str] = typer.Option(
82
+ None, "-g", "--llm-group", help="指定用于规划的 LLM 模型组(仅影响本次运行)"
83
+ ),
84
+ ) -> None:
85
+ """
86
+ 使用 LLM Agent 基于根函数子图规划 Rust crate 模块结构并直接应用到磁盘。
87
+ 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl)
88
+ 默认使用当前目录作为项目根,并从 <root>/.jarvis/c2rust/symbols.jsonl 读取数据
89
+ """
90
+ try:
91
+ _execute_llm_plan(apply=True, llm_group=llm_group)
92
+ except Exception as e:
93
+ typer.secho(f"[c2rust-llm-planner] 错误: {e}", fg=typer.colors.RED, err=True)
94
+ raise typer.Exit(code=1)
95
+
96
+
97
+ @app.command("transpile")
98
+ def transpile(
99
+ llm_group: Optional[str] = typer.Option(
100
+ None, "-g", "--llm-group", help="指定用于翻译的 LLM 模型组"
101
+ ),
102
+ only: Optional[str] = typer.Option(
103
+ None, "--only", help="仅翻译指定的函数(名称或限定名称),以逗号分隔"
104
+ ),
105
+ ) -> None:
106
+ """
107
+ 使用转译器按扫描顺序逐个函数转译并构建修复。
108
+ 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl 与 translation_order.jsonl)
109
+ 默认使用当前目录作为项目根,并从 <root>/.jarvis/c2rust/symbols.jsonl 读取数据。
110
+ 未指定目标 crate 时,使用默认 <cwd>/<cwd.name>_rs。
111
+ """
112
+ try:
113
+ # Lazy import to avoid hard dependency if not used
114
+ from jarvis.jarvis_c2rust.transpiler import run_transpile as _run_transpile
115
+ only_list = [s.strip() for s in str(only).split(",") if s.strip()] if only else None
116
+ _run_transpile(
117
+ project_root=Path("."),
118
+ crate_dir=None,
119
+ llm_group=llm_group,
120
+ only=only_list,
121
+ )
122
+ except Exception as e:
123
+ typer.secho(f"[c2rust-transpiler] 错误: {e}", fg=typer.colors.RED, err=True)
124
+ raise typer.Exit(code=1)
125
+
126
+
127
+ @app.command("lib-replace")
128
+ def lib_replace(
129
+ llm_group: Optional[str] = typer.Option(
130
+ None, "-g", "--llm-group", help="用于评估的 LLM 模型组"
131
+ ),
132
+ root_list_file: Optional[Path] = typer.Option(
133
+ None, "--root-list-file", help="根列表文件:按行列出要参与评估的根符号名称或限定名(忽略空行与以#开头的注释)"
134
+ ),
135
+ root_list_syms: Optional[str] = typer.Option(
136
+ None, "--root-list-syms", help="根列表内联:以逗号分隔的符号名称或限定名(仅评估这些根)"
137
+ ),
138
+ disabled_libs: Optional[str] = typer.Option(
139
+ None, "--disabled-libs", help="禁用库列表:逗号分隔的库名(评估时禁止使用这些库)"
140
+ ),
141
+ ) -> None:
142
+ """
143
+ Root-list 评估模式(必须走 LLM 评估):
144
+ - 必须提供根列表(--root-list-file 或 --root-list-syms,至少一种)
145
+ - 仅对根列表中的符号作为评估根执行 LLM 子树评估
146
+ - 若可替代:替换该根的 ref 为库占位,并剪除其所有子孙函数(根本身保留)
147
+ - 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl)
148
+ - 默认库: std(仅用于对后续流程保持一致的默认上下文)
149
+ - 可选:--disabled-libs 指定评估时禁止使用的库列表(逗号分隔)
150
+ """
151
+ try:
152
+ data_dir = Path(".") / ".jarvis" / "c2rust"
153
+ curated_symbols = data_dir / "symbols.jsonl"
154
+ raw_symbols = data_dir / "symbols_raw.jsonl"
155
+ if not curated_symbols.exists() and not raw_symbols.exists():
156
+ typer.secho("[c2rust-lib-replace] 未找到符号数据(symbols.jsonl 或 symbols_raw.jsonl),正在执行扫描以生成数据...", fg=typer.colors.YELLOW)
157
+ _run_scan(dot=None, only_dot=False, subgraphs_dir=None, only_subgraphs=False, png=False)
158
+ if not curated_symbols.exists() and not raw_symbols.exists():
159
+ raise FileNotFoundError(f"未找到符号数据: {curated_symbols} 或 {raw_symbols}")
160
+
161
+ # 使用默认库: std
162
+ library = "std"
163
+
164
+ # 读取根列表(必填,至少提供一种来源)
165
+ root_names: List[str] = []
166
+ # 文件来源
167
+ if root_list_file is not None:
168
+ try:
169
+ txt = root_list_file.read_text(encoding="utf-8")
170
+ root_names.extend([ln.strip() for ln in txt.splitlines() if ln.strip() and not ln.strip().startswith("#")])
171
+ except Exception as _e:
172
+ typer.secho(f"[c2rust-lib-replace] 读取根列表失败: {root_list_file}: {_e}", fg=typer.colors.RED, err=True)
173
+ raise typer.Exit(code=1)
174
+ # 内联来源
175
+ if isinstance(root_list_syms, str) and root_list_syms.strip():
176
+ parts = [s.strip() for s in root_list_syms.replace("\n", ",").split(",") if s.strip()]
177
+ root_names.extend(parts)
178
+ # 去重
179
+ try:
180
+ root_names = list(dict.fromkeys(root_names))
181
+ except Exception:
182
+ root_names = sorted(list(set(root_names)))
183
+ if not root_names:
184
+ typer.secho("[c2rust-lib-replace] 错误:必须提供根列表(--root-list-file 或 --root-list-syms)。", fg=typer.colors.RED, err=True)
185
+ raise typer.Exit(code=2)
186
+
187
+ # 解析禁用库列表(可选)
188
+ disabled_list: Optional[List[str]] = None
189
+ if isinstance(disabled_libs, str) and disabled_libs.strip():
190
+ disabled_list = [s.strip() for s in disabled_libs.replace("\n", ",").split(",") if s.strip()]
191
+ if disabled_list:
192
+ typer.secho(f"[c2rust-lib-replace] 禁用库: {', '.join(disabled_list)}", fg=typer.colors.YELLOW)
193
+
194
+ # 必须走 LLM 评估:仅评估提供的根(candidates),不启用强制剪枝模式
195
+ ret = _apply_library_replacement(
196
+ db_path=Path("."),
197
+ library_name=library,
198
+ llm_group=llm_group,
199
+ candidates=root_names, # 仅评估这些根
200
+ out_symbols_path=None,
201
+ out_mapping_path=None,
202
+ max_funcs=None,
203
+ disabled_libraries=disabled_list,
204
+ )
205
+ # 输出简要结果摘要(底层已写出新的符号表与可选转译顺序)
206
+ try:
207
+ order_msg = f"\n[c2rust-lib-replace] 转译顺序: {ret['order']}" if 'order' in ret else ""
208
+ typer.secho(
209
+ f"[c2rust-lib-replace] 替代映射: {ret['mapping']}\n"
210
+ f"[c2rust-lib-replace] 新符号表: {ret['symbols']}"
211
+ + order_msg,
212
+ fg=typer.colors.GREEN,
213
+ )
214
+ except Exception as _e:
215
+ typer.secho(f"[c2rust-lib-replace] 结果输出时发生非致命错误: {_e}", fg=typer.colors.YELLOW, err=True)
216
+ except Exception as e:
217
+ typer.secho(f"[c2rust-lib-replace] 错误: {e}", fg=typer.colors.RED, err=True)
218
+ raise typer.Exit(code=1)
219
+
220
+
221
+
222
+ @app.command("collect")
223
+ def collect(
224
+ files: List[Path] = typer.Argument(..., help="一个或多个 C/C++ 头文件路径(.h/.hh/.hpp/.hxx)"),
225
+ out: Path = typer.Option(..., "-o", "--out", help="输出文件路径(写入唯一函数名,每行一个)"),
226
+ ) -> None:
227
+ """
228
+ 收集指定头文件中的函数名(使用 libclang 解析),并写入指定输出文件(每行一个)。
229
+ 示例:
230
+ jarvis-c2rust collect a.h b.hpp -o funcs.txt
231
+ 说明:
232
+ 非头文件会被跳过(仅支持 .h/.hh/.hpp/.hxx)。
233
+ """
234
+ try:
235
+ from jarvis.jarvis_c2rust.collector import collect_function_names as _collect_fn_names
236
+ _collect_fn_names(files=files, out_path=out)
237
+ typer.secho(f"[c2rust-collect] 函数名已写入: {out}", fg=typer.colors.GREEN)
238
+ except Exception as e:
239
+ typer.secho(f"[c2rust-collect] 错误: {e}", fg=typer.colors.RED, err=True)
240
+ raise typer.Exit(code=1)
241
+
242
+ @app.command("run")
243
+ def run(
244
+ files: Optional[List[Path]] = typer.Option(
245
+ None,
246
+ "--files",
247
+ help="用于 collect 阶段的头文件列表(.h/.hh/.hpp/.hxx);提供则先执行 collect",
248
+ ),
249
+ out: Optional[Path] = typer.Option(
250
+ None,
251
+ "-o",
252
+ "--out",
253
+ help="collect 输出函数名文件;若未提供且指定 --files 则默认为 <root>/.jarvis/c2rust/roots.txt",
254
+ ),
255
+ llm_group: Optional[str] = typer.Option(
256
+ None,
257
+ "-g",
258
+ "--llm-group",
259
+ help="用于 LLM 相关阶段(lib-replace/prepare/transpile)的模型组",
260
+ ),
261
+ root_list_file: Optional[Path] = typer.Option(
262
+ None,
263
+ "--root-list-file",
264
+ help="兼容占位:run 会使用 collect 的 --out 作为 lib-replace 的输入;当提供 --files 时本参数将被忽略;未提供 --files 时,本命令要求使用 --root-list-syms",
265
+ ),
266
+ root_list_syms: Optional[str] = typer.Option(
267
+ None,
268
+ "--root-list-syms",
269
+ help="lib-replace 的根列表内联(逗号分隔)。未提供 --files 时该参数为必填",
270
+ ),
271
+ disabled_libs: Optional[str] = typer.Option(
272
+ None,
273
+ "--disabled-libs",
274
+ help="lib-replace 禁用库列表(逗号分隔)",
275
+ ),
276
+ ) -> None:
277
+ """
278
+ 依次执行流水线:collect -> scan -> lib-replace -> prepare -> transpile
279
+
280
+ 约束:
281
+ - collect 的输出文件就是 lib-replace 的输入文件;
282
+ 当提供 --files 时,lib-replace 将固定读取 --out(或默认值)作为根列表文件,忽略 --root-list-file
283
+ - 未提供 --files 时,必须通过 --root-list-syms 提供根列表
284
+ - scan 始终执行以确保数据完整
285
+ - prepare/transpile 会使用 --llm-group 指定的模型组
286
+ """
287
+ try:
288
+ data_dir = Path(".") / ".jarvis" / "c2rust"
289
+ default_roots = data_dir / "roots.txt"
290
+
291
+ # Step 1: collect(可选)
292
+ roots_path: Optional[Path] = None
293
+ if files:
294
+ try:
295
+ if out is None:
296
+ out = default_roots
297
+ out.parent.mkdir(parents=True, exist_ok=True)
298
+ from jarvis.jarvis_c2rust.collector import (
299
+ collect_function_names as _collect_fn_names,
300
+ )
301
+ _collect_fn_names(files=files, out_path=out)
302
+ typer.secho(f"[c2rust-run] collect: 函数名已写入: {out}", fg=typer.colors.GREEN)
303
+ roots_path = out
304
+ except Exception as _e:
305
+ typer.secho(f"[c2rust-run] collect: 错误: {_e}", fg=typer.colors.RED, err=True)
306
+ raise
307
+
308
+ # Step 2: scan(始终执行)
309
+ typer.secho("[c2rust-run] scan: 开始", fg=typer.colors.BLUE)
310
+ _run_scan(dot=None, only_dot=False, subgraphs_dir=None, only_subgraphs=False, png=False)
311
+ typer.secho("[c2rust-run] scan: 完成", fg=typer.colors.GREEN)
312
+
313
+ # Step 3: lib-replace(强制执行,依据约束获取根列表)
314
+ root_names: List[str] = []
315
+
316
+ if files:
317
+ # 约束:collect 的输出文件作为唯一文件来源
318
+ if not roots_path or not roots_path.exists():
319
+ typer.secho("[c2rust-run] lib-replace: 未找到 collect 输出文件,无法继续", fg=typer.colors.RED, err=True)
320
+ raise typer.Exit(code=2)
321
+ try:
322
+ txt = roots_path.read_text(encoding="utf-8")
323
+ root_names.extend([ln.strip() for ln in txt.splitlines() if ln.strip() and not ln.strip().startswith("#")])
324
+ typer.secho(f"[c2rust-run] lib-replace: 使用根列表文件: {roots_path}", fg=typer.colors.BLUE)
325
+ except Exception as _e:
326
+ typer.secho(f"[c2rust-run] lib-replace: 读取根列表失败: {roots_path}: {_e}", fg=typer.colors.RED, err=True)
327
+ raise
328
+ # 兼容参数提示
329
+ if root_list_file is not None:
330
+ typer.secho("[c2rust-run] 提示: --root-list-file 已被忽略(run 会固定使用 collect 的 --out 作为输入)", fg=typer.colors.YELLOW)
331
+ else:
332
+ # 约束:未传递 files 必须提供 --root-list-syms
333
+ if not (isinstance(root_list_syms, str) and root_list_syms.strip()):
334
+ typer.secho("[c2rust-run] 错误:未提供 --files 时,必须通过 --root-list-syms 指定根列表(逗号分隔)", fg=typer.colors.RED, err=True)
335
+ raise typer.Exit(code=2)
336
+ parts = [s.strip() for s in root_list_syms.replace("\n", ",").split(",") if s.strip()]
337
+ root_names.extend(parts)
338
+
339
+ # 去重并校验非空
340
+ try:
341
+ root_names = list(dict.fromkeys(root_names))
342
+ except Exception:
343
+ root_names = sorted(list(set(root_names)))
344
+ if not root_names:
345
+ typer.secho("[c2rust-run] lib-replace: 根列表为空,无法继续", fg=typer.colors.RED, err=True)
346
+ raise typer.Exit(code=2)
347
+
348
+ # 可选禁用库列表
349
+ disabled_list: Optional[List[str]] = None
350
+ if isinstance(disabled_libs, str) and disabled_libs.strip():
351
+ disabled_list = [s.strip() for s in disabled_libs.replace("\n", ",").split(",") if s.strip()]
352
+ if disabled_list:
353
+ typer.secho(f"[c2rust-run] lib-replace: 禁用库: {', '.join(disabled_list)}", fg=typer.colors.YELLOW)
354
+
355
+ # 执行 lib-replace(默认库 std)
356
+ library = "std"
357
+ typer.secho(f"[c2rust-run] lib-replace: 开始(库: {library},根数: {len(root_names)})", fg=typer.colors.BLUE)
358
+ ret = _apply_library_replacement(
359
+ db_path=Path("."),
360
+ library_name=library,
361
+ llm_group=llm_group,
362
+ candidates=root_names,
363
+ out_symbols_path=None,
364
+ out_mapping_path=None,
365
+ max_funcs=None,
366
+ disabled_libraries=disabled_list,
367
+ )
368
+ try:
369
+ order_msg = f"\n[c2rust-run] lib-replace: 转译顺序: {ret['order']}" if 'order' in ret else ""
370
+ typer.secho(
371
+ f"[c2rust-run] lib-replace: 替代映射: {ret['mapping']}\n"
372
+ f"[c2rust-run] lib-replace: 新符号表: {ret['symbols']}"
373
+ + order_msg,
374
+ fg=typer.colors.GREEN,
375
+ )
376
+ except Exception as _e:
377
+ typer.secho(f"[c2rust-run] lib-replace: 结果输出时发生非致命错误: {_e}", fg=typer.colors.YELLOW, err=True)
378
+
379
+ # Step 4: prepare
380
+ typer.secho("[c2rust-run] prepare: 开始", fg=typer.colors.BLUE)
381
+ _execute_llm_plan(apply=True, llm_group=llm_group)
382
+ typer.secho("[c2rust-run] prepare: 完成", fg=typer.colors.GREEN)
383
+
384
+ # Step 5: transpile
385
+ typer.secho("[c2rust-run] transpile: 开始", fg=typer.colors.BLUE)
386
+ from jarvis.jarvis_c2rust.transpiler import run_transpile as _run_transpile
387
+ _run_transpile(
388
+ project_root=Path("."),
389
+ crate_dir=None,
390
+ llm_group=llm_group,
391
+ only=None,
392
+ )
393
+ typer.secho("[c2rust-run] transpile: 完成", fg=typer.colors.GREEN)
394
+ except Exception as e:
395
+ typer.secho(f"[c2rust-run] 错误: {e}", fg=typer.colors.RED, err=True)
396
+ raise typer.Exit(code=1)
397
+
398
+
399
+ def main() -> None:
400
+ """主入口"""
401
+ app()
402
+
403
+
404
+ if __name__ == "__main__":
405
+ main()