@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1

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 (86) hide show
  1. package/package.json +1 -1
  2. package/runtime/index.json +6 -6
  3. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
  4. package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
  5. package/runtime/plugins/agent-pack/plugin.json +1 -1
  6. package/runtime/plugins/agent-pack/references/local-only.md +126 -0
  7. package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
  8. package/runtime/plugins/agent-pack/references/nudge.md +58 -0
  9. package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
  10. package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
  12. package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
  13. package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
  14. package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
  15. package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
  16. package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
  17. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
  18. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
  19. package/runtime/plugins/agents-init/content.json +17 -2
  20. package/runtime/plugins/agents-init/plugin.json +1 -1
  21. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
  23. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
  24. package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
  25. package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
  26. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
  27. package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
  28. package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
  29. package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
  30. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
  31. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
  32. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
  33. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
  34. package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
  35. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
  36. package/runtime/plugins/opinion-manager/plugin.json +1 -1
  37. package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
  38. package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
  39. package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
  40. package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
  41. package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
  42. package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
  43. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
  44. package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
  45. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
  46. package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
  47. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
  48. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
  49. package/runtime/plugins/project-orchestrator/content.json +30 -3
  50. package/runtime/plugins/project-orchestrator/plugin.json +1 -1
  51. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
  52. package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
  53. package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
  54. package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
  55. package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
  56. package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
  57. package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
  58. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
  59. package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
  60. package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
  61. package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
  62. package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
  63. package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
  64. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
  65. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
  66. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
  67. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
  68. package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
  69. package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
  70. package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
  71. package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
  72. package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
  73. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
  74. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
  75. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
  76. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
  77. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
  78. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
  79. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
  80. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
  81. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
  82. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
  83. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
  84. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
  85. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
  86. package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
@@ -24,6 +24,7 @@ from typing import Any
24
24
 
25
25
  import case as case_tool
26
26
  from project_base import DEFAULT_TOOLS, EntryError, inside
27
+ from i18n_orchestrator import _ # noqa: E402
27
28
 
28
29
  TOOL_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
29
30
  TOOL_TIMEOUT_SECONDS = 600
@@ -36,12 +37,12 @@ def record_capability(provider: Any) -> tuple[str, str]:
36
37
  adapter = provider.entry["items"]
37
38
  if adapter:
38
39
  if adapter.get("write"):
39
- return "available", f"经项目工具 {' '.join(adapter['command'])} 写入"
40
- return "unavailable", f"项目事项工具 {' '.join(adapter['command'])} 没有声明写接口(entry.items.write)"
40
+ return "available", _("经项目工具 {0} 写入").format(' '.join(adapter['command']))
41
+ return "unavailable", _("项目事项工具 {0} 没有声明写接口(entry.items.write)").format(' '.join(adapter['command']))
41
42
  if provider.case_command:
42
- return "unavailable", f"项目声明了自己的 Case 工具({provider.case_command}),本入口不绕过它写入"
43
+ return "unavailable", _("项目声明了自己的 Case 工具({0}),本入口不绕过它写入").format(provider.case_command)
43
44
  if provider.work_mode != "case-workspace":
44
- return "unavailable", "项目没有使用 Case 工作目录"
45
+ return "unavailable", _("项目没有使用 Case 工作目录")
45
46
  return "available", ""
46
47
 
47
48
 
@@ -49,14 +50,14 @@ def _gap(provider: Any) -> EntryError:
49
50
  state, reason = record_capability(provider)
50
51
  steps = [reason]
51
52
  if provider.entry["items"] or provider.case_command:
52
- steps.append("需要时请直接用项目自己的工具;或请项目维护者按 case-workspaces.md「项目入口适配」补上写接口")
53
- return EntryError("这个项目不能在本入口记录事项", "unavailable", steps)
53
+ steps.append(_("需要时请直接用项目自己的工具;或请项目维护者按 case-workspaces.md「项目入口适配」补上写接口"))
54
+ return EntryError(_("这个项目不能在本入口记录事项"), "unavailable", steps)
54
55
 
55
56
 
56
57
  def _diff(path: str, current: str, new: str) -> list[str]:
57
58
  lines = [line.rstrip("\n") for line in difflib.unified_diff(
58
59
  current.splitlines(keepends=True), new.splitlines(keepends=True), lineterm="", n=1)][2:]
59
- return lines[:DIFF_LIMIT] + ([f"…另有 {len(lines) - DIFF_LIMIT} 行"] if len(lines) > DIFF_LIMIT else [])
60
+ return lines[:DIFF_LIMIT] + ([_("…另有 {0} 行").format(len(lines) - DIFF_LIMIT)] if len(lines) > DIFF_LIMIT else [])
60
61
 
61
62
 
62
63
  def _slug(change: dict) -> str:
@@ -98,34 +99,34 @@ def _case_relative(root: Path, case_dir: Path, value: str) -> str:
98
99
 
99
100
 
100
101
  def preview(provider: Any, target: str | None, change: dict) -> dict:
101
- state, _ = record_capability(provider)
102
+ state, _detail = record_capability(provider)
102
103
  if state != "available":
103
104
  raise _gap(provider)
104
105
  change = {**change, "add_deliverable": [_project_relative(provider.root, value)
105
106
  for value in change.get("add_deliverable") or []]}
106
107
  for path in change["add_deliverable"]:
107
- inside(provider.root, path, "交付物路径")
108
+ inside(provider.root, path, _("交付物路径"))
108
109
  if not (provider.root / path).exists():
109
- raise EntryError(f"要登记的文件不存在:{path}", "usage", ["路径相对于项目根目录"])
110
+ raise EntryError(_("要登记的文件不存在:{0}").format(path), "usage", [_("路径相对于项目根目录")])
110
111
  if provider.entry["items"]:
111
112
  return _tool_preview(provider, target, change)
112
113
  try:
113
114
  project = case_tool.Project(provider.root)
114
115
  if target is None:
115
116
  if not (change.get("title") and change.get("goal")):
116
- raise EntryError("新建事项需要标题和目标", "usage")
117
+ raise EntryError(_("新建事项需要标题和目标"), "usage")
117
118
  case_id, text = case_tool.plan_create(project, _new_args(change))
118
119
  path = f"{project.relative(project.case_root)}/{case_id}/CASE.md"
119
120
  front = text.split("\n---\n", 1)[0].splitlines()[1:]
120
- lines = [f"+{line}" for line in front] + [" (正文按 Case 模板生成:背景、下一动作、工作记录等小节)"]
121
+ lines = [f"+{line}" for line in front] + [_(" (正文按 Case 模板生成:背景、下一动作、工作记录等小节)")]
121
122
  return {"kind": "new", "target": None, "path": path, "lines": lines,
122
123
  "expect_updated_at": None, "expect_sha256": None, "change": change, "simulated": False,
123
- "note": "编号以写入时为准(同一天有人先建了事项时顺延)"}
124
+ "note": _("编号以写入时为准(同一天有人先建了事项时顺延)")}
124
125
  item = provider.item(target)
125
126
  case_dir = (provider.root / item["path"]).parent
126
127
  change = {**change, "add_deliverable": [_case_relative(provider.root, case_dir, value)
127
128
  for value in change.get("add_deliverable") or []]}
128
- path, current, text, _ = case_tool.plan_update(project, _case_args(change, case=item["id"]))
129
+ path, current, text, _extra = case_tool.plan_update(project, _case_args(change, case=item["id"]))
129
130
  except case_tool.CaseError as exc:
130
131
  raise EntryError(str(exc), "conflict" if exc.code == "conflict" else "usage") from exc
131
132
  return {"kind": "update", "target": item["id"], "path": project.relative(path),
@@ -142,23 +143,25 @@ def apply(provider: Any, planned: dict) -> dict:
142
143
  project = case_tool.Project(provider.root)
143
144
  if planned["kind"] == "new":
144
145
  result = case_tool.create(project, _new_args(change))
145
- data, _ = case_tool.read_case(provider.root / result["path"])
146
+ data, _body = case_tool.read_case(provider.root / result["path"])
146
147
  verified = data.get("title") == change.get("title")
147
148
  return {"path": result["path"], "id": result["id"], "simulated": False,
148
- "verify": ("写入后重新读取:已建立 " if verified else "写入后读回不一致:") + result["path"]}
149
- before, _ = case_tool.read_case(provider.root / planned["path"])
149
+ "sop_events": result.get("sop_events", []),
150
+ "verify": (_("写入后重新读取:已建立 ") if verified else _("写入后读回不一致:")) + result["path"]}
151
+ before, _body = case_tool.read_case(provider.root / planned["path"])
150
152
  result = case_tool.update(project, _case_args(change, case=planned["target"],
151
153
  expect_updated_at=planned["expect_updated_at"],
152
154
  expect_sha256=planned["expect_sha256"]))
153
- after, _ = case_tool.read_case(provider.root / planned["path"])
155
+ after, _body = case_tool.read_case(provider.root / planned["path"])
154
156
  except case_tool.CaseError as exc:
155
157
  if exc.code == "conflict":
156
- raise EntryError("这个事项在你预览之后被修改过,没有写入", "conflict",
157
- [str(exc), "重新打开预览,看过对方的改动后再提交"]) from exc
158
+ raise EntryError(_("这个事项在你预览之后被修改过,没有写入"), "conflict",
159
+ [str(exc), _("重新打开预览,看过对方的改动后再提交")]) from exc
158
160
  raise EntryError(str(exc), "usage") from exc
159
161
  kept = [key for key in before if key in after]
160
162
  return {"path": planned["path"], "id": result["id"], "simulated": False, "changed": result["changed"],
161
- "verify": f"写入后重新读取:已更新 {'、'.join(result['changed'])};原有 {len(kept)} 个字段与其他小节保留"}
163
+ "sop_events": result.get("sop_events", []),
164
+ "verify": _("写入后重新读取:已更新 {0};原有 {1} 个字段与其他小节保留").format(_('、').join(result['changed']), len(kept))}
162
165
 
163
166
 
164
167
  def _tool_flags(change: dict) -> list[str]:
@@ -178,7 +181,7 @@ def _run_items_tool(provider: Any, arguments: list[str]) -> tuple[int, dict]:
178
181
  completed = subprocess.run(command, cwd=provider.root, capture_output=True, text=True, check=False,
179
182
  timeout=120)
180
183
  except (OSError, subprocess.TimeoutExpired) as exc:
181
- raise EntryError(f"项目事项工具无法运行:{exc}", "failed", [" ".join(command)]) from exc
184
+ raise EntryError(_("项目事项工具无法运行:{0}").format(exc), "failed", [" ".join(command)]) from exc
182
185
  try:
183
186
  payload = json.loads(completed.stdout or "{}")
184
187
  except ValueError:
@@ -195,8 +198,8 @@ def _tool_preview(provider: Any, target: str | None, change: dict) -> dict:
195
198
  arguments = ["update", item["id"], *_tool_flags(change), "--dry-run"]
196
199
  code, payload = _run_items_tool(provider, arguments)
197
200
  if code != 0:
198
- raise EntryError(f"项目事项工具拒绝了这次修改:{payload.get('error') or payload}", "usage")
199
- lines = payload.get("preview") or (payload.get("diff") or "").splitlines() or ["(项目工具没有给出预览内容)"]
201
+ raise EntryError(_("项目事项工具拒绝了这次修改:{0}").format(payload.get('error') or payload), "usage")
202
+ lines = payload.get("preview") or (payload.get("diff") or "").splitlines() or [_("(项目工具没有给出预览内容)")]
200
203
  return {"kind": "new" if item is None else "update", "target": item["id"] if item else None,
201
204
  "path": payload.get("path") or (item["path"] if item else ""), "lines": [str(line) for line in lines],
202
205
  "expect_updated_at": item["updated_at"] if item else None, "expect_sha256": None, "change": change,
@@ -214,13 +217,13 @@ def _tool_apply(provider: Any, planned: dict) -> dict:
214
217
  code, payload = _run_items_tool(provider, arguments)
215
218
  if code != 0:
216
219
  if payload.get("code") == "conflict":
217
- raise EntryError("这个事项在你预览之后被修改过,项目工具没有写入", "conflict",
218
- ["重新打开预览,看过对方的改动后再提交"])
219
- raise EntryError(f"项目事项工具写入失败:{payload.get('error') or payload}", "failed")
220
+ raise EntryError(_("这个事项在你预览之后被修改过,项目工具没有写入"), "conflict",
221
+ [_("重新打开预览,看过对方的改动后再提交")])
222
+ raise EntryError(_("项目事项工具写入失败:{0}").format(payload.get('error') or payload), "failed")
220
223
  provider._items = None
221
224
  return {"path": payload.get("path") or planned["path"], "simulated": False,
222
- "verify": "项目工具已写入;重新读取列表:" + ("找到该事项" if _still_listed(provider, planned, payload)
223
- else "列表里没有找到,请用项目工具核对")}
225
+ "verify": _("项目工具已写入;重新读取列表:") + (_("找到该事项") if _still_listed(provider, planned, payload)
226
+ else _("列表里没有找到,请用项目工具核对"))}
224
227
 
225
228
 
226
229
  def _still_listed(provider: Any, planned: dict, payload: dict) -> bool:
@@ -242,30 +245,30 @@ def load_tools(provider: Any) -> dict:
242
245
  except EntryError as exc:
243
246
  return {"tools": [], "error": str(exc), "path": relative}
244
247
  if not path.is_file():
245
- return {"tools": [], "error": f"entry.tools 指向的文件不存在:{relative}" if declared else "",
248
+ return {"tools": [], "error": _("entry.tools 指向的文件不存在:{0}").format(relative) if declared else "",
246
249
  "path": relative}
247
250
  try:
248
251
  data = json.loads(path.read_text(encoding="utf-8"))
249
252
  raw = data["tools"]
250
253
  if not isinstance(raw, list):
251
- raise TypeError("tools 必须是列表")
254
+ raise TypeError(_("tools 必须是列表"))
252
255
  tools = [_tool(provider, item) for item in raw]
253
256
  except (OSError, UnicodeDecodeError, ValueError, KeyError, TypeError) as exc:
254
- return {"tools": [], "error": f"{relative} 格式不对:{exc}", "path": relative}
257
+ return {"tools": [], "error": _("{0} 格式不对:{1}").format(relative, exc), "path": relative}
255
258
  ids = [tool["id"] for tool in tools]
256
259
  if len(ids) != len(set(ids)):
257
- return {"tools": [], "error": f"{relative} 里有重复的工具 id", "path": relative}
260
+ return {"tools": [], "error": _("{0} 里有重复的工具 id").format(relative), "path": relative}
258
261
  return {"tools": tools, "error": "", "path": relative}
259
262
 
260
263
 
261
264
  def _tool(provider: Any, item: Any) -> dict:
262
265
  if not isinstance(item, dict):
263
- raise TypeError("每个工具必须是对象")
266
+ raise TypeError(_("每个工具必须是对象"))
264
267
  tool_id, command = item.get("id"), item.get("command")
265
268
  if not isinstance(tool_id, str) or not TOOL_ID_RE.fullmatch(tool_id):
266
- raise ValueError(f"工具 id 只能用小写字母、数字和连字符:{tool_id!r}")
269
+ raise ValueError(_("工具 id 只能用小写字母、数字和连字符:{0!r}").format(tool_id))
267
270
  if not (isinstance(command, list) and command and all(isinstance(part, str) and part for part in command)):
268
- raise ValueError(f"{tool_id}: command 必须是非空参数列表(不经过 shell)")
271
+ raise ValueError(_("{0}: command 必须是非空参数列表(不经过 shell)").format(tool_id))
269
272
  requires = item.get("requires") or {}
270
273
  commands = [str(value) for value in requires.get("commands", [command[0]])]
271
274
  env_names = [str(value) for value in requires.get("env", [])]
@@ -273,13 +276,13 @@ def _tool(provider: Any, item: Any) -> dict:
273
276
  network = bool(item.get("network", False))
274
277
  missing = [name for name in commands if not _command_available(provider.root, name)]
275
278
  unset = [name for name in env_names if not os.environ.get(name)]
276
- reasons = ([f"缺少命令 {'、'.join(missing)}"] if missing else []) + \
277
- ([f"缺少环境变量 {'、'.join(unset)}"] if unset else []) + \
278
- (["当前离线"] if network and provider.offline else [])
279
+ reasons = ([_("缺少命令 {0}").format(_('、').join(missing))] if missing else []) + \
280
+ ([_("缺少环境变量 {0}").format(_('、').join(unset))] if unset else []) + \
281
+ ([_("当前离线")] if network and provider.offline else [])
279
282
  return {"id": tool_id, "title": str(item.get("title") or tool_id), "description": str(item.get("description") or ""),
280
- "command": command, "network": network, "account": "、".join(env_names) or None, "writes": writes,
283
+ "command": command, "network": network, "account": _("、").join(env_names) or None, "writes": writes,
281
284
  "requires": commands + env_names, "state": "unavailable" if reasons else "available",
282
- "reason": ";".join(reasons)}
285
+ "reason": _(";").join(reasons)}
283
286
 
284
287
 
285
288
  def _command_available(root: Path, name: str) -> bool:
@@ -287,7 +290,7 @@ def _command_available(root: Path, name: str) -> bool:
287
290
  return Path(name).is_file() and os.access(name, os.X_OK)
288
291
  if "/" in name:
289
292
  try:
290
- candidate = inside(root, name, "工具命令")
293
+ candidate = inside(root, name, _("工具命令"))
291
294
  except EntryError:
292
295
  return False
293
296
  return candidate.is_file()
@@ -297,33 +300,33 @@ def _command_available(root: Path, name: str) -> bool:
297
300
  def check_tools(provider: Any, findings: list[dict], ok: list[str]) -> None:
298
301
  loaded = load_tools(provider)
299
302
  if loaded["error"]:
300
- findings.append({"id": "tools-invalid", "level": "suggest", "area": "项目工具", "title": "项目工具声明有误",
301
- "reason": loaded["error"], "impact": "项目工具暂不可用,其他功能不受影响",
302
- "next": [{"label": f"修正 {loaded['path']}", "action": ""}]})
303
+ findings.append({"id": "tools-invalid", "level": "suggest", "area": _("项目工具"), "title": _("项目工具声明有误"),
304
+ "reason": loaded["error"], "impact": _("项目工具暂不可用,其他功能不受影响"),
305
+ "next": [{"label": _("修正 {0}").format(loaded['path']), "action": ""}]})
303
306
  return
304
307
  for tool in loaded["tools"]:
305
308
  if tool["state"] != "available":
306
- findings.append({"id": f"tool-{tool['id']}", "level": "unavailable", "area": "项目工具",
307
- "title": f"“{tool['title']}”当前不可用", "reason": tool["reason"],
308
- "impact": "只影响这一项工具", "next": [{"label": "补齐后重新检查", "action": ""}]})
309
+ findings.append({"id": f"tool-{tool['id']}", "level": "unavailable", "area": _("项目工具"),
310
+ "title": _("“{0}”当前不可用").format(tool['title']), "reason": tool["reason"],
311
+ "impact": _("只影响这一项工具"), "next": [{"label": _("补齐后重新检查"), "action": ""}]})
309
312
  if loaded["tools"]:
310
- ok.append(f"项目工具({sum(tool['state'] == 'available' for tool in loaded['tools'])}/{len(loaded['tools'])} 可用)")
313
+ ok.append(_("项目工具({0}/{1} 可用)").format(sum(tool['state'] == 'available' for tool in loaded['tools']), len(loaded['tools'])))
311
314
 
312
315
 
313
316
  def run_tool(provider: Any, tool_id: str, *, capture: bool = True) -> dict:
314
317
  tool = next((tool for tool in load_tools(provider)["tools"] if tool["id"] == tool_id), None)
315
318
  if tool is None:
316
- raise EntryError(f"项目没有声明这个工具:{tool_id}", "not-found")
319
+ raise EntryError(_("项目没有声明这个工具:{0}").format(tool_id), "not-found")
317
320
  if tool["state"] != "available":
318
- raise EntryError(f"“{tool['title']}”当前不可用", "unavailable", [tool["reason"]])
321
+ raise EntryError(_("“{0}”当前不可用").format(tool['title']), "unavailable", [tool["reason"]])
319
322
  try:
320
323
  completed = subprocess.run(tool["command"], cwd=provider.root, check=False, timeout=TOOL_TIMEOUT_SECONDS,
321
324
  capture_output=capture, text=True)
322
325
  except (OSError, subprocess.TimeoutExpired) as exc:
323
- raise EntryError(f"“{tool['title']}”无法运行:{exc}", "failed") from exc
326
+ raise EntryError(_("“{0}”无法运行:{1}").format(tool['title'], exc), "failed") from exc
324
327
  output = ((completed.stdout or "") + (completed.stderr or "")).strip() if capture else ""
325
328
  if completed.returncode != 0:
326
- raise EntryError(f"“{tool['title']}”运行失败(退出码 {completed.returncode})", "failed",
327
- [line for line in output.splitlines()[-5:]] or ["没有输出"])
329
+ raise EntryError(_("“{0}”运行失败(退出码 {1})").format(tool['title'], completed.returncode), "failed",
330
+ [line for line in output.splitlines()[-5:]] or [_("没有输出")])
328
331
  return {"id": tool_id, "simulated": False, "exit_code": completed.returncode, "output": output[-2000:],
329
- "message": f"已运行:{' '.join(tool['command'])}" + (f"\n{output[-1200:]}" if output else "")}
332
+ "message": _("已运行:{0}").format(' '.join(tool['command'])) + (f"\n{output[-1200:]}" if output else "")}