@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.
- package/package.json +1 -1
- package/runtime/index.json +6 -6
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
- package/runtime/plugins/agent-pack/plugin.json +1 -1
- package/runtime/plugins/agent-pack/references/local-only.md +126 -0
- package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
- package/runtime/plugins/agent-pack/references/nudge.md +58 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
- package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
- package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
- package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
- package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
- package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
- package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agents-init/content.json +17 -2
- package/runtime/plugins/agents-init/plugin.json +1 -1
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
- package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
- package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
- package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
- package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
- package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
- package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
- package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
- package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/content.json +30 -3
- package/runtime/plugins/project-orchestrator/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
- package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
- package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
- package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
- package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
- package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
- package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
- package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
- package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
- package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
- package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
- 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",
|
|
40
|
-
return "unavailable",
|
|
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",
|
|
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] + ([
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
"
|
|
149
|
-
|
|
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,
|
|
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
|
-
"
|
|
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(
|
|
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(
|
|
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(
|
|
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":
|
|
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":
|
|
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":
|
|
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(
|
|
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(
|
|
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 = ([
|
|
277
|
-
([
|
|
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":
|
|
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":
|
|
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(
|
|
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(
|
|
319
|
+
raise EntryError(_("项目没有声明这个工具:{0}").format(tool_id), "not-found")
|
|
317
320
|
if tool["state"] != "available":
|
|
318
|
-
raise EntryError(
|
|
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(
|
|
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(
|
|
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":
|
|
332
|
+
"message": _("已运行:{0}").format(' '.join(tool['command'])) + (f"\n{output[-1200:]}" if output else "")}
|