@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
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
- ``update-plugins``:各客户端里的插件代码(调用客户端自己的市场命令,需联网);
|
|
7
7
|
- ``update-content``:项目里由模板生成的文件,改过的文件逐个保留或采用,先备份;
|
|
8
8
|
- ``install-runtime`` / ``update-runtime``:用户级运行时,新版本装在旁边,旧版本保留;
|
|
9
|
-
- ``add-entry``:为已初始化的项目补上根目录启动器与 Profile 的 entry
|
|
9
|
+
- ``add-entry``:为已初始化的项目补上根目录启动器与 Profile 的 entry 段;
|
|
10
|
+
- ``add-skeleton`` / ``profile`` / ``repair-lock``:脚手架差异(缺少的骨架文件、Profile 字段、内容锁版本);
|
|
11
|
+
逐项可选 采用 / 永久跳过 / 稍后,跳过与稍后记入决定文件(见 agents-init 的 scaffold.py)。
|
|
10
12
|
|
|
11
13
|
个人规则(Opinion)的新版本只报告,从不在这里采纳:那由 opinion-manager 预览后经用户确认。
|
|
12
14
|
插件、模板与运行时的检查经 agent-pack(``HOST``)完成;没有它时这些项标为不可用。
|
|
@@ -23,8 +25,10 @@ from typing import Any
|
|
|
23
25
|
|
|
24
26
|
import assets
|
|
25
27
|
from project_base import DEFAULT_LAUNCHER, LAUNCHER_RE, EntryError
|
|
28
|
+
from i18n_orchestrator import _, N_ # noqa: E402
|
|
29
|
+
from i18n_orchestrator import _ # noqa: E402
|
|
26
30
|
|
|
27
|
-
CLIENT_LABEL = {"claude": "Claude Code", "codex": "Codex", "cursor": "Cursor", "legacy": "旧方式安装"}
|
|
31
|
+
CLIENT_LABEL = {"claude": "Claude Code", "codex": "Codex", "cursor": "Cursor", "legacy": N_("旧方式安装")}
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def _iso(seconds: float | None) -> str:
|
|
@@ -41,19 +45,82 @@ def runtime_module(host: Any) -> Any:
|
|
|
41
45
|
|
|
42
46
|
# ---------------------------------------------------------------- check
|
|
43
47
|
|
|
48
|
+
SCAFFOLD_NEXT = {"add-skeleton": N_("按模板补上(只新建,不改已有文件)"),
|
|
49
|
+
"profile": N_("把字段改为模板值(先备份,预览里有 diff)"),
|
|
50
|
+
"repair-lock": N_("按实际版本修复内容锁(不涉及你的文件)"),
|
|
51
|
+
"update-content": N_("逐文件选择保留或采用新模板"),
|
|
52
|
+
"case-naming": N_("查看改名计划与受影响的引用(只给计划,不移动目录)"),
|
|
53
|
+
"update-region": N_("更新受管区块:运行 init_project.py --mode region(改过的区块逐个 --keep / --accept)")}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _scaffold_findings(scaffold: dict | None, findings: list[dict], ok: list[str]) -> None:
|
|
57
|
+
if scaffold is None:
|
|
58
|
+
return
|
|
59
|
+
if scaffold.get("error"):
|
|
60
|
+
findings.append({"id": "scaffold-check", "level": "unavailable", "area": _("脚手架"), "title": _("无法检查项目脚手架"),
|
|
61
|
+
"reason": scaffold["error"], "impact": _("只影响脚手架一致性提示"), "next": []})
|
|
62
|
+
return
|
|
63
|
+
for item in scaffold["findings"]:
|
|
64
|
+
mark = _("(稍后)") if item.get("deferred") else _("(期望内容已变化)") if item.get("changed_since_decision") else ""
|
|
65
|
+
# 菜单只执行 FIXES 里的处理项;其余(如 update-region)只给出怎么处理的说明。
|
|
66
|
+
step = {"label": _(SCAFFOLD_NEXT.get(item["fix"], item["fix"])), "action": f"fix:{item['fix']}" if item["fix"] in FIXES else ""} \
|
|
67
|
+
if item["fix"] else {"label": _("这一项需要你自己处理"), "action": ""}
|
|
68
|
+
findings.append({"id": item["id"], "level": "suggest", "area": _("脚手架"), "title": item["title"] + mark,
|
|
69
|
+
"reason": item["reason"], "impact": _("脚手架和最新版不一致;不影响日常使用,"
|
|
70
|
+
"不想要的可以永久跳过(一次决定,所有机器生效)"), "next": [step],
|
|
71
|
+
"decision": {"id": item["id"], "fingerprint": item["fingerprint"], "scope": "project"}})
|
|
72
|
+
for message in scaffold["decisions"]["errors"]:
|
|
73
|
+
findings.append({"id": "scaffold-decisions", "level": "unavailable", "area": _("脚手架"),
|
|
74
|
+
"title": _("决定文件有问题"), "reason": message, "impact": _("已记录的决定暂时不生效"), "next": []})
|
|
75
|
+
if scaffold["decisions"]["conflict_copies"]:
|
|
76
|
+
findings.append({"id": "scaffold-decisions-conflict", "level": "suggest", "area": _("脚手架"),
|
|
77
|
+
"title": _("同步服务留下了决定文件的冲突副本"),
|
|
78
|
+
"reason": _("、").join(scaffold["decisions"]["conflict_copies"]),
|
|
79
|
+
"impact": _("另一台机器写入的决定可能没有生效"),
|
|
80
|
+
"next": [{"label": _("把其中的条目合并进 check-decisions.json 后删除副本"), "action": ""}]})
|
|
81
|
+
if not scaffold["findings"]:
|
|
82
|
+
ok.append(_("项目脚手架(与期望一致") + (_(",{0} 项已跳过").format(len(scaffold['skipped'])) if scaffold["skipped"] else "") + _(")"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _local_only_finding(item: dict) -> dict:
|
|
86
|
+
"""agent-pack 的 local-only 发现 -> 健康检查里的一项;选项里第一个带命令的对应 fix local-only。"""
|
|
87
|
+
steps, linked = [], False
|
|
88
|
+
for option in item.get("options") or []:
|
|
89
|
+
action = "fix:local-only" if option.get("command") and not linked else ""
|
|
90
|
+
linked = linked or bool(action)
|
|
91
|
+
steps.append({"label": option["label"], "action": action})
|
|
92
|
+
return {"id": item["id"], "level": "unavailable" if item.get("kind") == "error" else "suggest", "area": _("同步盘"),
|
|
93
|
+
"title": item["title"], "reason": item["reason"], "impact": item["impact"], "next": steps,
|
|
94
|
+
"decision": {"id": item["id"], "fingerprint": item["fingerprint"], "scope": "machine"}}
|
|
95
|
+
|
|
96
|
+
|
|
44
97
|
def check_updates(provider: Any, findings: list[dict], ok: list[str]) -> dict:
|
|
45
98
|
host = provider.host
|
|
46
99
|
if host is None:
|
|
47
100
|
return {"status": "unavailable", "cached_at": ""}
|
|
48
101
|
env = host.current_env()
|
|
102
|
+
# 脚手架与本机检查不依赖联网,先做;决定文件让已决定的项不再出现。
|
|
103
|
+
reprompt = getattr(provider, "reprompt", False)
|
|
104
|
+
scaffold = host.scaffold_report(env, provider.root, reprompt=reprompt) if hasattr(host, "scaffold_report") else None
|
|
105
|
+
machine = host.machine_findings(env, provider.root, reprompt=reprompt) if hasattr(host, "machine_findings") else None
|
|
106
|
+
provider._skipped = [*(scaffold["skipped"] if scaffold else []), *(machine["skipped"] if machine else [])]
|
|
107
|
+
_scaffold_findings(scaffold, findings, ok)
|
|
108
|
+
for item in (machine["findings"] if machine else []):
|
|
109
|
+
if item["id"] == "local:sync-folder":
|
|
110
|
+
findings.append({"id": "sync-folder", "level": "suggest", "area": _("环境"), "title": item["title"],
|
|
111
|
+
"reason": item["reason"], "impact": item["impact"],
|
|
112
|
+
"next": [{"label": _("查看同步盘使用指引(只给方案,不改文件、不删内容)"), "action": "fix:sync-folder"}],
|
|
113
|
+
"decision": {"id": item["id"], "fingerprint": item["fingerprint"], "scope": "machine"}})
|
|
114
|
+
elif item.get("fix") == "local-only" or item.get("kind") == "error":
|
|
115
|
+
findings.append(_local_only_finding(item))
|
|
49
116
|
try:
|
|
50
117
|
report = host.run_check(env, provider.root, offline=provider.offline)
|
|
51
118
|
except (host.CheckError, OSError, ValueError) as exc:
|
|
52
|
-
findings.append({"id": "update-check", "level": "unavailable", "area": "更新", "title": "无法检查更新",
|
|
53
|
-
"reason": str(exc), "impact": "不影响找资料、看进展等日常功能",
|
|
54
|
-
"next": [{"label": "联网后重新检查", "action": ""}]})
|
|
119
|
+
findings.append({"id": "update-check", "level": "unavailable", "area": _("更新"), "title": _("无法检查更新"),
|
|
120
|
+
"reason": str(exc), "impact": _("不影响找资料、看进展等日常功能"),
|
|
121
|
+
"next": [{"label": _("联网后重新检查"), "action": ""}]})
|
|
55
122
|
provider._report = None
|
|
56
|
-
_check_runtime(provider, env, None, findings, ok)
|
|
123
|
+
_check_runtime(provider, env, None, findings, ok, machine)
|
|
57
124
|
return {"status": "offline", "cached_at": ""}
|
|
58
125
|
provider._report = report
|
|
59
126
|
origin = report["latest"]["source"]
|
|
@@ -64,7 +131,7 @@ def check_updates(provider: Any, findings: list[dict], ok: list[str]) -> dict:
|
|
|
64
131
|
_plugin_findings(provider, env, report, findings, ok)
|
|
65
132
|
_content_findings(report, findings, ok)
|
|
66
133
|
_opinion_findings(report, findings, ok)
|
|
67
|
-
_check_runtime(provider, env, report["_latest"], findings, ok)
|
|
134
|
+
_check_runtime(provider, env, report["_latest"], findings, ok, machine)
|
|
68
135
|
return source
|
|
69
136
|
|
|
70
137
|
|
|
@@ -74,95 +141,114 @@ def _plugin_findings(provider: Any, env: Any, report: dict, findings: list[dict]
|
|
|
74
141
|
behind = [item for item in plugins if item["status"] == "behind"]
|
|
75
142
|
if behind:
|
|
76
143
|
breaking = [item for item in behind if item["breaking"]]
|
|
77
|
-
details = ";".join(
|
|
144
|
+
details = _(";").join(_("{0} {1} → {2}({3})").format(item['plugin'], item['version'], item['latest'], _(CLIENT_LABEL[item['client']]))
|
|
78
145
|
for item in behind)
|
|
79
|
-
findings.append({"id": "update-plugins", "level": "update", "area": "插件",
|
|
80
|
-
"title":
|
|
81
|
-
"reason": details, "impact": "只影响 AI 客户端里的插件;不更新也能继续使用本菜单",
|
|
82
|
-
"next": [{"label": "查看更新内容与迁移说明,再决定是否更新", "action": "fix:update-plugins"}]})
|
|
146
|
+
findings.append({"id": "update-plugins", "level": "update", "area": _("插件"),
|
|
147
|
+
"title": _("{0} 个插件有新版本").format(len(behind)) + (_("(含不兼容变更)") if breaking else ""),
|
|
148
|
+
"reason": details, "impact": _("只影响 AI 客户端里的插件;不更新也能继续使用本菜单"),
|
|
149
|
+
"next": [{"label": _("查看更新内容与迁移说明,再决定是否更新"), "action": "fix:update-plugins"}]})
|
|
83
150
|
elif plugins:
|
|
84
|
-
ok.append("客户端插件(最新)")
|
|
151
|
+
ok.append(_("客户端插件(最新)"))
|
|
85
152
|
if not plugins:
|
|
86
153
|
clients = host.detect_clients(env)
|
|
87
154
|
if clients:
|
|
88
|
-
findings.append({"id": "no-plugins", "level": "unavailable", "area": "插件",
|
|
89
|
-
"title": "AI 客户端里还没有安装 agenthelm 插件",
|
|
90
|
-
"reason": "检测到 " + "、".join(CLIENT_LABEL.get(c, c) for c in clients) + ",但没有市场安装",
|
|
91
|
-
"impact": "只影响 AI Agent 使用;本菜单不受影响",
|
|
92
|
-
"next": [{"label": "需要时运行 agenthelm install 预览安装", "action": ""}]})
|
|
155
|
+
findings.append({"id": "no-plugins", "level": "unavailable", "area": _("插件"),
|
|
156
|
+
"title": _("AI 客户端里还没有安装 agenthelm 插件"),
|
|
157
|
+
"reason": _("检测到 ") + _("、").join(_(CLIENT_LABEL.get(c, c)) for c in clients) + _(",但没有市场安装"),
|
|
158
|
+
"impact": _("只影响 AI Agent 使用;本菜单不受影响"),
|
|
159
|
+
"next": [{"label": _("需要时运行 agenthelm install 预览安装"), "action": ""}]})
|
|
93
160
|
else:
|
|
94
|
-
findings.append({"id": "no-ai-client", "level": "unavailable", "area": "环境", "title": "本机没有 AI 客户端",
|
|
95
|
-
"reason": "没有找到 Claude Code、Codex 或 Cursor",
|
|
96
|
-
"impact": "本菜单的日常功能全部不受影响;只有“安装插件到客户端”不可用",
|
|
97
|
-
"next": [{"label": "需要时安装任一客户端,再运行 agenthelm install", "action": ""}]})
|
|
161
|
+
findings.append({"id": "no-ai-client", "level": "unavailable", "area": _("环境"), "title": _("本机没有 AI 客户端"),
|
|
162
|
+
"reason": _("没有找到 Claude Code、Codex 或 Cursor"),
|
|
163
|
+
"impact": _("本菜单的日常功能全部不受影响;只有“安装插件到客户端”不可用"),
|
|
164
|
+
"next": [{"label": _("需要时安装任一客户端,再运行 agenthelm install"), "action": ""}]})
|
|
98
165
|
if report["summary"]["legacy_installs"]:
|
|
99
|
-
findings.append({"id": "legacy", "level": "suggest", "area": "插件", "title": "有旧方式安装待迁移",
|
|
100
|
-
"reason": "旧的复制 + 软链安装与市场安装并存会重复", "impact": "Agent 里可能看到重复的 Skill",
|
|
101
|
-
"next": [{"label": "运行 agenthelm migrate --install 预览迁移", "action": ""}]})
|
|
166
|
+
findings.append({"id": "legacy", "level": "suggest", "area": _("插件"), "title": _("有旧方式安装待迁移"),
|
|
167
|
+
"reason": _("旧的复制 + 软链安装与市场安装并存会重复"), "impact": _("Agent 里可能看到重复的 Skill"),
|
|
168
|
+
"next": [{"label": _("运行 agenthelm migrate --install 预览迁移"), "action": ""}]})
|
|
102
169
|
for issue in host.doctor(env, report)["issues"]:
|
|
103
170
|
if issue["issue"] in ("version-skew", "duplicate", "incomplete"):
|
|
104
|
-
findings.append({"id": f"doctor-{issue['issue']}", "level": "suggest", "area": "插件",
|
|
105
|
-
"title": {"version-skew": "各客户端插件版本不一致", "duplicate": "插件重复安装",
|
|
106
|
-
"incomplete": "插件没有装全"}[issue["issue"]],
|
|
107
|
-
"reason": issue["detail"], "impact": "只影响 AI Agent 使用",
|
|
108
|
-
"next": [{"label": "运行 agenthelm doctor 查看", "action": ""}]})
|
|
171
|
+
findings.append({"id": f"doctor-{issue['issue']}", "level": "suggest", "area": _("插件"),
|
|
172
|
+
"title": {"version-skew": _("各客户端插件版本不一致"), "duplicate": _("插件重复安装"),
|
|
173
|
+
"incomplete": _("插件没有装全")}[issue["issue"]],
|
|
174
|
+
"reason": issue["detail"], "impact": _("只影响 AI Agent 使用"),
|
|
175
|
+
"next": [{"label": _("运行 agenthelm doctor 查看"), "action": ""}]})
|
|
109
176
|
|
|
110
177
|
|
|
111
178
|
def _content_findings(report: dict, findings: list[dict], ok: list[str]) -> None:
|
|
112
179
|
updates = [item for item in report["content"] if item.get("status") in ("update-available", "conflict")]
|
|
113
180
|
for item in report["content"]:
|
|
114
181
|
if item.get("pack") is None:
|
|
115
|
-
findings.append({"id": "content-lock", "level": "suggest", "area": "项目模板", "title": "内容锁无法读取",
|
|
116
|
-
"reason": item.get("error", ""), "impact": "模板更新无法判断哪些文件改过",
|
|
117
|
-
"next": [{"label": "检查 .agents/moe.sakanano.agent-pack/content.lock.json", "action": ""}]})
|
|
182
|
+
findings.append({"id": "content-lock", "level": "suggest", "area": _("项目模板"), "title": _("内容锁无法读取"),
|
|
183
|
+
"reason": item.get("error", ""), "impact": _("模板更新无法判断哪些文件改过"),
|
|
184
|
+
"next": [{"label": _("检查 .agents/moe.sakanano.agent-pack/content.lock.json"), "action": ""}]})
|
|
118
185
|
if updates:
|
|
119
186
|
changed = sorted({path for item in updates for path in item["locally_changed"]})
|
|
120
|
-
findings.append({"id": "update-content", "level": "update", "area": "项目模板",
|
|
121
|
-
"title": "项目模板/样例有新版本",
|
|
122
|
-
"reason": ";".join(f"{item['pack']} {item['version']} → {item['latest']}" for item in updates)
|
|
123
|
-
+ (
|
|
124
|
-
"impact": "不更新不影响使用;更新时你改过的文件逐个选择保留或采用",
|
|
125
|
-
"next": [{"label": "查看影响并选择", "action": "fix:update-content"}]})
|
|
187
|
+
findings.append({"id": "update-content", "level": "update", "area": _("项目模板"),
|
|
188
|
+
"title": _("项目模板/样例有新版本"),
|
|
189
|
+
"reason": _(";").join(f"{item['pack']} {item['version']} → {item['latest']}" for item in updates)
|
|
190
|
+
+ (_(";你改过的:{0}").format(_('、').join(changed)) if changed else ""),
|
|
191
|
+
"impact": _("不更新不影响使用;更新时你改过的文件逐个选择保留或采用"),
|
|
192
|
+
"next": [{"label": _("查看影响并选择"), "action": "fix:update-content"}]})
|
|
126
193
|
elif report["content"]:
|
|
127
|
-
ok.append("项目模板(最新)")
|
|
194
|
+
ok.append(_("项目模板(最新)"))
|
|
128
195
|
|
|
129
196
|
|
|
130
197
|
def _opinion_findings(report: dict, findings: list[dict], ok: list[str]) -> None:
|
|
131
198
|
for item in report["opinion"]:
|
|
132
199
|
if item["status"] == "update-available":
|
|
133
|
-
refs = ";".join(f"{update['current']} → {update['available'][-1]}" for update in item["updates"])
|
|
134
|
-
findings.append({"id": "opinion-updates", "level": "update", "area": "个人规则",
|
|
135
|
-
"title": "行为规则模板有新版本", "reason": refs,
|
|
136
|
-
"impact": "工具升级不会自动采纳个人规则;当前规则继续有效",
|
|
137
|
-
"next": [{"label": "需要时在 Agent 会话中用 opinion-manager 预览后确认", "action": ""}]})
|
|
200
|
+
refs = _(";").join(f"{update['current']} → {update['available'][-1]}" for update in item["updates"])
|
|
201
|
+
findings.append({"id": "opinion-updates", "level": "update", "area": _("个人规则"),
|
|
202
|
+
"title": _("行为规则模板有新版本"), "reason": refs,
|
|
203
|
+
"impact": _("工具升级不会自动采纳个人规则;当前规则继续有效"),
|
|
204
|
+
"next": [{"label": _("需要时在 Agent 会话中用 opinion-manager 预览后确认"), "action": ""}]})
|
|
138
205
|
elif item["status"] in ("error", "unknown"):
|
|
139
|
-
findings.append({"id": "opinion-check", "level": "unavailable", "area": "个人规则",
|
|
140
|
-
"title": "行为规则版本无法检查", "reason": item.get("error", ""),
|
|
141
|
-
"impact": "只影响规则更新提示", "next": []})
|
|
206
|
+
findings.append({"id": "opinion-check", "level": "unavailable", "area": _("个人规则"),
|
|
207
|
+
"title": _("行为规则版本无法检查"), "reason": item.get("error", ""),
|
|
208
|
+
"impact": _("只影响规则更新提示"), "next": []})
|
|
142
209
|
else:
|
|
143
|
-
ok.append("行为规则(已是锁定的最新版本)")
|
|
210
|
+
ok.append(_("行为规则(已是锁定的最新版本)"))
|
|
144
211
|
|
|
145
212
|
|
|
146
|
-
def _check_runtime(provider: Any, env: Any, latest: dict | None, findings: list[dict], ok: list[str]
|
|
213
|
+
def _check_runtime(provider: Any, env: Any, latest: dict | None, findings: list[dict], ok: list[str],
|
|
214
|
+
machine: dict | None = None) -> None:
|
|
147
215
|
runtime = runtime_module(provider.host)
|
|
148
216
|
data = runtime.report(env, latest)
|
|
149
217
|
active = data["current"]
|
|
150
218
|
if active is None:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
if machine is not None and not machine["findings"]:
|
|
220
|
+
return # 用户已决定不提示这台机器的运行时(决定存在用户状态目录,不进项目)
|
|
221
|
+
text = runtime.missing_text(data, runtime.command_status()) if hasattr(runtime, "missing_text") else {
|
|
222
|
+
"reason": _("本次由 {0} 提供;装好运行时后,不依赖 AI 客户端与 npx 也能打开菜单").format(data['running_from']),
|
|
223
|
+
"impact": _("现在能用;换一台没有 AI 客户端的机器或客户端插件被移除时会打不开"),
|
|
224
|
+
"label": _("安装运行时(只写用户数据目录)")}
|
|
225
|
+
decision = ({"id": "local:runtime-missing", "fingerprint": "runtime-missing", "scope": "machine"}
|
|
226
|
+
if machine is not None else None)
|
|
227
|
+
findings.append({"id": "install-runtime", "level": "suggest", "area": _("运行时"), "title": _("这台机器还没有安装用户级运行时"),
|
|
228
|
+
"reason": text["reason"], "impact": text["impact"],
|
|
229
|
+
"next": [{"label": text["label"], "action": "fix:install-runtime"}],
|
|
230
|
+
**({"decision": decision} if decision else {})})
|
|
155
231
|
elif data["update_available"]:
|
|
156
|
-
findings.append({"id": "update-runtime", "level": "update", "area": "运行时", "title": "运行时有新版本",
|
|
157
|
-
"reason":
|
|
158
|
-
"impact": "不更新也能继续使用", "next": [{"label": "下载并切换(旧版本保留)", "action": "fix:update-runtime"}]})
|
|
232
|
+
findings.append({"id": "update-runtime", "level": "update", "area": _("运行时"), "title": _("运行时有新版本"),
|
|
233
|
+
"reason": _("当前 {0},最新 {1}").format(active['version'], data['latest_release']),
|
|
234
|
+
"impact": _("不更新也能继续使用"), "next": [{"label": _("下载并切换(旧版本保留)"), "action": "fix:update-runtime"}]})
|
|
159
235
|
else:
|
|
160
|
-
ok.append(
|
|
236
|
+
ok.append(_("运行时 {0}").format(active['version']))
|
|
161
237
|
|
|
162
238
|
|
|
163
239
|
# ---------------------------------------------------------------- plan
|
|
164
240
|
|
|
165
|
-
FIXES = ("rebuild-index", "update-plugins", "update-content", "install-runtime", "update-runtime", "add-entry"
|
|
241
|
+
FIXES = ("rebuild-index", "update-plugins", "update-content", "install-runtime", "update-runtime", "add-entry",
|
|
242
|
+
"add-skeleton", "profile", "repair-lock", "case-naming", "sync-folder", "local-only")
|
|
243
|
+
# 可以带目标的处理项:fix add-skeleton <路径…>、fix profile <字段…>
|
|
244
|
+
TARGETED = ("add-skeleton", "profile")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def split_targets(ids: list[str]) -> tuple[list[str], list[str]]:
|
|
248
|
+
"""``["add-skeleton", "a.md"]`` -> (["add-skeleton"], ["a.md"]); other ids are returned unchanged."""
|
|
249
|
+
if ids and ids[0] in TARGETED:
|
|
250
|
+
return ids[:1], ids[1:]
|
|
251
|
+
return ids, []
|
|
166
252
|
|
|
167
253
|
|
|
168
254
|
def available_ids(provider: Any) -> list[str]:
|
|
@@ -170,7 +256,7 @@ def available_ids(provider: Any) -> list[str]:
|
|
|
170
256
|
found = []
|
|
171
257
|
for finding in health["findings"]:
|
|
172
258
|
for step in finding["next"]:
|
|
173
|
-
verb,
|
|
259
|
+
verb, _sep, target = step["action"].partition(":")
|
|
174
260
|
if verb == "fix" and target in FIXES and target not in found:
|
|
175
261
|
found.append(target)
|
|
176
262
|
return found
|
|
@@ -181,7 +267,7 @@ def plan(provider: Any, ids: list[str], choices: dict[str, str]) -> dict:
|
|
|
181
267
|
chosen = ids or offered
|
|
182
268
|
unknown = [fix_id for fix_id in chosen if fix_id not in FIXES]
|
|
183
269
|
if unknown:
|
|
184
|
-
raise EntryError("没有这些处理项:" + "、".join(unknown), "not-found", ["可处理:" + ("、".join(offered) or "(无)")])
|
|
270
|
+
raise EntryError(_("没有这些处理项:") + _("、").join(unknown), "not-found", [_("可处理:") + (_("、").join(offered) or _("(无)"))])
|
|
185
271
|
steps = [_plan_step(provider, fix_id, choices) for fix_id in chosen]
|
|
186
272
|
return {"steps": steps, "simulated": False, "update_source": (provider._health or {}).get("update_source")}
|
|
187
273
|
|
|
@@ -195,18 +281,22 @@ def _step(fix_id: str, title: str, **values: Any) -> dict:
|
|
|
195
281
|
|
|
196
282
|
def _need_host(provider: Any, fix_id: str, title: str) -> dict | None:
|
|
197
283
|
if provider.host is None:
|
|
198
|
-
return _step(fix_id, title, blocked="当前运行方式没有携带 agent-pack:用 agenthelm 运行时打开后再处理")
|
|
284
|
+
return _step(fix_id, title, blocked=_("当前运行方式没有携带 agent-pack:用 agenthelm 运行时打开后再处理"))
|
|
199
285
|
return None
|
|
200
286
|
|
|
201
287
|
|
|
202
288
|
def _plan_step(provider: Any, fix_id: str, choices: dict[str, str]) -> dict:
|
|
203
289
|
if fix_id == "rebuild-index":
|
|
204
290
|
state_dir = provider.assets_project().state_dir()
|
|
205
|
-
return _step(fix_id, "重建资料索引", changes=["按当前文件重新登记资料、版本与引用"],
|
|
206
|
-
writes=[{"path": str(state_dir), "scope": "用户缓存(不改项目文件)"}],
|
|
207
|
-
recovery="不涉及项目文件;索引可随时再次重建", verify="重新读取索引状态")
|
|
291
|
+
return _step(fix_id, _("重建资料索引"), changes=[_("按当前文件重新登记资料、版本与引用")],
|
|
292
|
+
writes=[{"path": str(state_dir), "scope": _("用户缓存(不改项目文件)")}],
|
|
293
|
+
recovery=_("不涉及项目文件;索引可随时再次重建"), verify=_("重新读取索引状态"))
|
|
208
294
|
if fix_id == "add-entry":
|
|
209
295
|
return _plan_entry(provider, choices)
|
|
296
|
+
if fix_id in ("add-skeleton", "profile", "repair-lock"):
|
|
297
|
+
return _plan_scaffold(provider, fix_id, choices)
|
|
298
|
+
if fix_id in ("case-naming", "sync-folder", "local-only"):
|
|
299
|
+
return _plan_guidance(provider, fix_id)
|
|
210
300
|
blocked = _need_host(provider, fix_id, fix_id)
|
|
211
301
|
if blocked:
|
|
212
302
|
return blocked
|
|
@@ -215,44 +305,91 @@ def _plan_step(provider: Any, fix_id: str, choices: dict[str, str]) -> dict:
|
|
|
215
305
|
if fix_id in ("install-runtime", "update-runtime"):
|
|
216
306
|
return _plan_runtime(provider, env, fix_id)
|
|
217
307
|
report = getattr(provider, "_report", None)
|
|
218
|
-
if report is None:
|
|
219
|
-
return _step(fix_id, fix_id, blocked="版本信息不可用(离线且没有缓存):联网后重新检查")
|
|
308
|
+
if report is None and fix_id != "update-content":
|
|
309
|
+
return _step(fix_id, fix_id, blocked=_("版本信息不可用(离线且没有缓存):联网后重新检查"))
|
|
220
310
|
if fix_id == "update-plugins":
|
|
221
311
|
steps = host.update_steps(env, report, None, None, env.home, [], [], None, "plugins")
|
|
222
312
|
behind = [item for item in report["plugins"] if item["status"] == "behind"]
|
|
223
|
-
changes = [
|
|
313
|
+
changes = [_("{0} → {1}:{2}").format(item['plugin'], step['to'], step['summary']) for item in behind for step in item["steps"]]
|
|
224
314
|
actions = [action for item in behind for step in item["steps"] for action in step["actions"]]
|
|
225
315
|
blocked = next((step.blocked for step in steps if step.blocked), "") or \
|
|
226
|
-
("当前离线:需要联网,只能预览" if provider.offline else "")
|
|
227
|
-
return _step(fix_id, "更新客户端里的插件", network=True, changes=changes + [
|
|
228
|
-
writes=[{"path": step.description + (
|
|
229
|
-
"scope": "客户端插件目录"} for step in steps],
|
|
316
|
+
(_("当前离线:需要联网,只能预览") if provider.offline else "")
|
|
317
|
+
return _step(fix_id, _("更新客户端里的插件"), network=True, changes=changes + [_("需要的动作:{0}").format(a) for a in actions],
|
|
318
|
+
writes=[{"path": step.description + (_("({0})").format(' '.join(step.command)) if step.command else ""),
|
|
319
|
+
"scope": _("客户端插件目录")} for step in steps],
|
|
230
320
|
breaking=any(item["breaking"] for item in behind),
|
|
231
|
-
recovery="客户端保留旧版本缓存;需要时用客户端命令回到旧版本",
|
|
232
|
-
verify="重新检查各客户端已安装的版本", blocked=blocked)
|
|
321
|
+
recovery=_("客户端保留旧版本缓存;需要时用客户端命令回到旧版本"),
|
|
322
|
+
verify=_("重新检查各客户端已安装的版本"), blocked=blocked)
|
|
233
323
|
return _plan_content(provider, env, report, choices)
|
|
234
324
|
|
|
235
325
|
|
|
236
|
-
def
|
|
326
|
+
def _run_refresh(provider: Any, accept: list[str], keep: list[str], recovery: Path, *, apply: bool) -> dict:
|
|
327
|
+
"""agents-init 的 refresh:模板包版本没有更新、但有文件与当前模板不同(例如没有基线)时直接用它。"""
|
|
328
|
+
script = _init_script(provider)
|
|
329
|
+
if script is None:
|
|
330
|
+
raise EntryError(_("找不到支持刷新的 agents-init"), "unavailable", [_("更新插件或运行时后再试")])
|
|
331
|
+
command = [sys.executable, str(script), "--project", str(provider.root), "--mode", "refresh", "--output", "json"]
|
|
332
|
+
for path in accept:
|
|
333
|
+
command += ["--accept", path]
|
|
334
|
+
for path in keep:
|
|
335
|
+
command += ["--keep", path]
|
|
336
|
+
if apply or accept:
|
|
337
|
+
command += ["--recovery-dir", str(recovery)]
|
|
338
|
+
if apply:
|
|
339
|
+
command.append("--apply")
|
|
340
|
+
completed = subprocess.run(command, capture_output=True, text=True, check=False, timeout=120)
|
|
341
|
+
try:
|
|
342
|
+
result = json.loads(completed.stdout)
|
|
343
|
+
except ValueError as exc:
|
|
344
|
+
raise EntryError(_("agents-init 没有返回结果:") + (completed.stderr or "").strip()[:200], "failed") from exc
|
|
345
|
+
if "files" not in result:
|
|
346
|
+
raise EntryError(str(result.get("error") or _("agents-init 拒绝")), "failed")
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _content_preview(provider: Any, env: Any, report: dict | None, accept: list[str], keep: list[str],
|
|
351
|
+
recovery: Path) -> tuple[str, list[dict], str, bool]:
|
|
352
|
+
"""(说明, 文件计划, 阻塞原因, 是否走 refresh 直通路径)。"""
|
|
353
|
+
host = provider.host
|
|
354
|
+
steps = host.update_steps(env, report, provider.root, None, env.home, accept, keep, recovery, "content") \
|
|
355
|
+
if report is not None else []
|
|
356
|
+
if steps:
|
|
357
|
+
step = steps[0]
|
|
358
|
+
if step.blocked:
|
|
359
|
+
return step.description, [], step.blocked, False
|
|
360
|
+
preview = host.execute_steps([step], apply=False, recovery_root=None, child_env=env.child_environment())[0]
|
|
361
|
+
if preview.get("status") == "failed":
|
|
362
|
+
return step.description, [], preview.get("error", _("预览失败")), False
|
|
363
|
+
return step.description, (preview.get("result") or {}).get("files") or [], "", False
|
|
364
|
+
try:
|
|
365
|
+
result = _run_refresh(provider, accept, keep, recovery, apply=False)
|
|
366
|
+
except EntryError as exc:
|
|
367
|
+
return _("项目模板/样例"), [], str(exc), True
|
|
368
|
+
files = result.get("files") or []
|
|
369
|
+
if all(item["state"] in ("unchanged", "kept-by-decision") for item in files):
|
|
370
|
+
return _("项目模板/样例"), [], _("没有需要更新的模板"), True
|
|
371
|
+
return _("按当前模板对比项目契约文件"), files, "", True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _plan_content(provider: Any, env: Any, report: dict | None, choices: dict[str, str]) -> dict:
|
|
237
375
|
host = provider.host
|
|
238
376
|
accept = sorted(path for path, choice in choices.items() if choice == "accept")
|
|
239
377
|
keep = sorted(path for path, choice in choices.items() if choice == "keep")
|
|
240
378
|
recovery = Path(choices.get("_recovery") or host.recovery_dir_for(env, "content"))
|
|
241
|
-
|
|
242
|
-
if
|
|
243
|
-
return _step("update-content",
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
custom = [{"path": item["path"], "state": "你改过", "default": "keep"} for item in files
|
|
379
|
+
description, files, error, direct = _content_preview(provider, env, report, accept, keep, recovery)
|
|
380
|
+
if error:
|
|
381
|
+
return _step("update-content", description, blocked=error)
|
|
382
|
+
|
|
383
|
+
class _Step: # 保持下面沿用的 step.description 写法
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
step = _Step()
|
|
387
|
+
step.description = description
|
|
388
|
+
custom = [{"path": item["path"], "state": _("你改过"), "default": "keep"} for item in files
|
|
252
389
|
if item["state"] == "modified" or (item["path"] in choices and item["state"] in ("modified", "missing"))]
|
|
253
390
|
for item in files:
|
|
254
391
|
if item["state"] == "missing" and not any(entry["path"] == item["path"] for entry in custom):
|
|
255
|
-
custom.append({"path": item["path"], "state": "新文件(目前不存在)", "default": "keep"})
|
|
392
|
+
custom.append({"path": item["path"], "state": _("新文件(目前不存在)"), "default": "keep"})
|
|
256
393
|
effective = {item["path"]: choices.get(item["path"], item["default"]) for item in custom}
|
|
257
394
|
writes = []
|
|
258
395
|
for item in files:
|
|
@@ -262,18 +399,19 @@ def _plan_content(provider: Any, env: Any, report: dict, choices: dict[str, str]
|
|
|
262
399
|
if action == "skip":
|
|
263
400
|
action = "create" if effective.get(item["path"]) == "accept" else "keep"
|
|
264
401
|
if action in ("replace", "create"):
|
|
265
|
-
writes.append({"path": item["path"], "scope": "项目(先备份)" if action == "replace" else "项目(新建)"})
|
|
402
|
+
writes.append({"path": item["path"], "scope": _("项目(先备份)") if action == "replace" else _("项目(新建)")})
|
|
266
403
|
# 刷新总会把内容锁更新到新版本(保留的文件沿用旧基线),所以它总在写入范围里。
|
|
267
|
-
writes.append({"path": ".agents/moe.sakanano.agent-pack/content.lock.json", "scope": "项目(内容锁)"})
|
|
404
|
+
writes.append({"path": ".agents/moe.sakanano.agent-pack/content.lock.json", "scope": _("项目(内容锁)")})
|
|
268
405
|
return _step("update-content", step.description, changes=[_content_change(item, effective) for item in files],
|
|
269
406
|
writes=writes, custom=custom, recovery=str(recovery),
|
|
270
|
-
verify="重新检查模板版本;保留的文件内容不变"
|
|
407
|
+
verify=_("重新检查模板版本;保留的文件内容不变"),
|
|
408
|
+
payload={"recovery": str(recovery), "files": files, "direct": direct})
|
|
271
409
|
|
|
272
410
|
|
|
273
|
-
CONTENT_STATE = {"unchanged": "与新模板相同", "clean": "没改过", "modified": "你改过", "missing": "目前不存在",
|
|
274
|
-
"local-only": "只有你的改动,新模板没变"}
|
|
275
|
-
CONTENT_ACTION = {"keep": "保留", "replace": "换成新模板(先备份)", "create": "新建", "skip": "不新建",
|
|
276
|
-
"blocked": "需要你选择"}
|
|
411
|
+
CONTENT_STATE = {"unchanged": N_("与新模板相同"), "clean": N_("没改过"), "modified": N_("你改过"), "missing": N_("目前不存在"),
|
|
412
|
+
"local-only": N_("只有你的改动,新模板没变")}
|
|
413
|
+
CONTENT_ACTION = {"keep": N_("保留"), "replace": N_("换成新模板(先备份)"), "create": N_("新建"), "skip": N_("不新建"),
|
|
414
|
+
"blocked": N_("需要你选择")}
|
|
277
415
|
|
|
278
416
|
|
|
279
417
|
def _content_change(item: dict, effective: dict[str, str]) -> str:
|
|
@@ -282,34 +420,34 @@ def _content_change(item: dict, effective: dict[str, str]) -> str:
|
|
|
282
420
|
chosen = effective.get(item["path"], "keep")
|
|
283
421
|
action = ("replace" if action == "blocked" else "create") if chosen == "accept" else \
|
|
284
422
|
("keep" if action == "blocked" else "skip")
|
|
285
|
-
note = "(保留则不含新模板的改动)" if item["state"] == "modified" and action == "keep" else ""
|
|
286
|
-
return
|
|
423
|
+
note = _("(保留则不含新模板的改动)") if item["state"] == "modified" and action == "keep" else ""
|
|
424
|
+
return _("{0}:{1} → {2}{3}").format(item['path'], _(CONTENT_STATE.get(item['state'], item['state'])), _(CONTENT_ACTION.get(action, action)), note)
|
|
287
425
|
|
|
288
426
|
|
|
289
427
|
def _plan_runtime(provider: Any, env: Any, fix_id: str) -> dict:
|
|
290
428
|
runtime = runtime_module(provider.host)
|
|
291
429
|
host = provider.host
|
|
292
|
-
title = "安装用户级运行时" if fix_id == "install-runtime" else "更新用户级运行时"
|
|
430
|
+
title = _("安装用户级运行时") if fix_id == "install-runtime" else _("更新用户级运行时")
|
|
293
431
|
if fix_id == "install-runtime" and runtime.bundled_root() is not None:
|
|
294
|
-
root, origin = runtime.bundled_root(), "随附内容"
|
|
432
|
+
root, origin = runtime.bundled_root(), _("随附内容")
|
|
295
433
|
network = False
|
|
296
434
|
else:
|
|
297
435
|
if provider.offline:
|
|
298
|
-
return _step(fix_id, title, network=True, blocked="当前离线:需要下载发布包,只能预览;也可用 agenthelm runtime install --source <发布包>")
|
|
436
|
+
return _step(fix_id, title, network=True, blocked=_("当前离线:需要下载发布包,只能预览;也可用 agenthelm runtime install --source <发布包>"))
|
|
299
437
|
try:
|
|
300
|
-
latest,
|
|
438
|
+
latest, _origin = host.load_latest(env, None)
|
|
301
439
|
except (host.CheckError, OSError, ValueError) as exc:
|
|
302
|
-
return _step(fix_id, title, network=True, blocked=
|
|
440
|
+
return _step(fix_id, title, network=True, blocked=_("拿不到版本信息:{0}").format(exc))
|
|
303
441
|
version = runtime.release_version(latest)
|
|
304
|
-
return _step(fix_id, title, network=True, changes=[
|
|
305
|
-
writes=[{"path": str(runtime.runtimes_dir(env) / (version or "<最新版本>")), "scope": "用户数据目录(新版本)"},
|
|
306
|
-
{"path": str(runtime.current_link(env)), "scope": "切换当前版本"}],
|
|
307
|
-
recovery="旧版本保留;agenthelm runtime use <旧版本> --apply 切回", verify="重新读取当前运行时版本",
|
|
442
|
+
return _step(fix_id, title, network=True, changes=[_("下载发布包 {0} 并核对摘要").format(version or _('(最新)'))],
|
|
443
|
+
writes=[{"path": str(runtime.runtimes_dir(env) / (version or _("<最新版本>"))), "scope": _("用户数据目录(新版本)")},
|
|
444
|
+
{"path": str(runtime.current_link(env)), "scope": _("切换当前版本")}],
|
|
445
|
+
recovery=_("旧版本保留;agenthelm runtime use <旧版本> --apply 切回"), verify=_("重新读取当前运行时版本"),
|
|
308
446
|
payload={"from_release": True})
|
|
309
447
|
planned = runtime.plan(env, root, origin)
|
|
310
|
-
return _step(fix_id, title, network=network, changes=[
|
|
311
|
-
writes=planned["writes"], recovery="旧版本保留;agenthelm runtime use <旧版本> --apply 切回",
|
|
312
|
-
verify="重新读取当前运行时版本", payload={"from_release": False, "plan": planned})
|
|
448
|
+
return _step(fix_id, title, network=network, changes=[_("运行时 {0}({1})").format(planned['version'], origin)],
|
|
449
|
+
writes=planned["writes"], recovery=_("旧版本保留;agenthelm runtime use <旧版本> --apply 切回"),
|
|
450
|
+
verify=_("重新读取当前运行时版本"), payload={"from_release": False, "plan": planned})
|
|
313
451
|
|
|
314
452
|
|
|
315
453
|
def _init_script(provider: Any) -> Path | None:
|
|
@@ -327,7 +465,7 @@ def _init_script(provider: Any) -> Path | None:
|
|
|
327
465
|
def _run_init(provider: Any, name: str, choices: dict[str, str], recovery: str, *, apply: bool) -> dict:
|
|
328
466
|
script = _init_script(provider)
|
|
329
467
|
if script is None:
|
|
330
|
-
raise EntryError("找不到支持生成入口的 agents-init", "unavailable", ["更新插件或运行时后再试"])
|
|
468
|
+
raise EntryError(_("找不到支持生成入口的 agents-init"), "unavailable", [_("更新插件或运行时后再试")])
|
|
331
469
|
command = [sys.executable, str(script), "--project", str(provider.root), "--mode", "entry",
|
|
332
470
|
"--entry-name", name, "--output", "json"]
|
|
333
471
|
if choices.get(name) in ("keep", "accept"):
|
|
@@ -342,7 +480,7 @@ def _run_init(provider: Any, name: str, choices: dict[str, str], recovery: str,
|
|
|
342
480
|
try:
|
|
343
481
|
return json.loads(completed.stdout)
|
|
344
482
|
except ValueError as exc:
|
|
345
|
-
raise EntryError("agents-init 没有返回结果:" + (completed.stderr or "").strip()[:200], "failed") from exc
|
|
483
|
+
raise EntryError(_("agents-init 没有返回结果:") + (completed.stderr or "").strip()[:200], "failed") from exc
|
|
346
484
|
|
|
347
485
|
|
|
348
486
|
def _recovery_dir(provider: Any, label: str) -> str:
|
|
@@ -355,8 +493,8 @@ def _recovery_dir(provider: Any, label: str) -> str:
|
|
|
355
493
|
def _plan_entry(provider: Any, choices: dict[str, str]) -> dict:
|
|
356
494
|
name = choices.get("entry-name") or provider.launcher() or DEFAULT_LAUNCHER
|
|
357
495
|
if not LAUNCHER_RE.fullmatch(name):
|
|
358
|
-
raise EntryError(
|
|
359
|
-
title =
|
|
496
|
+
raise EntryError(_("启动器名称不合法:{0}").format(name), "usage", [_("只能用字母、数字、. _ -,例如 menu、go")])
|
|
497
|
+
title = _("生成根目录启动器 ./{0}").format(name) if provider.launcher() != name else _("完善入口 ./{0}").format(name)
|
|
360
498
|
recovery = choices.get("_recovery") or _recovery_dir(provider, "entry")
|
|
361
499
|
try:
|
|
362
500
|
result = _run_init(provider, name, choices, recovery, apply=False)
|
|
@@ -366,18 +504,236 @@ def _plan_entry(provider: Any, choices: dict[str, str]) -> dict:
|
|
|
366
504
|
except EntryError as exc:
|
|
367
505
|
return _step("add-entry", title, blocked=str(exc))
|
|
368
506
|
if result.get("code"):
|
|
369
|
-
return _step("add-entry", title, blocked=result.get("error", "agents-init 拒绝"))
|
|
507
|
+
return _step("add-entry", title, blocked=result.get("error", _("agents-init 拒绝")))
|
|
370
508
|
entry = result.get("entry") or {}
|
|
371
|
-
custom = [{"path": name, "state": "已有同名文件(你的)", "default": "keep"}] \
|
|
509
|
+
custom = [{"path": name, "state": _("已有同名文件(你的)"), "default": "keep"}] \
|
|
372
510
|
if entry.get("action") in ("keep", "replace", "blocked") else []
|
|
373
511
|
readme = provider.root / "README.md"
|
|
374
512
|
has_section = readme.is_file() and "<!-- agenthelm:entry -->" in readme.read_text(encoding="utf-8", errors="replace")
|
|
375
513
|
if not has_section:
|
|
376
|
-
custom.append({"path": "README.md", "state": "可追加「怎样使用」一节(带标记)", "default": "keep"})
|
|
377
|
-
writes = [{"path": path, "scope": "项目"} for path in entry.get("writes", [])]
|
|
514
|
+
custom.append({"path": "README.md", "state": _("可追加「怎样使用」一节(带标记)"), "default": "keep"})
|
|
515
|
+
writes = [{"path": path, "scope": _("项目")} for path in entry.get("writes", [])]
|
|
378
516
|
return _step("add-entry", title, changes=entry.get("changes", []), writes=writes, custom=custom,
|
|
379
|
-
recovery=recovery if choices.get(name) == "accept" else "新增文件可直接删除;Profile 改动前后见预览",
|
|
380
|
-
verify=
|
|
517
|
+
recovery=recovery if choices.get(name) == "accept" else _("新增文件可直接删除;Profile 改动前后见预览"),
|
|
518
|
+
verify=_("重新读取 Profile 的 entry 段与 ./{0}").format(name), payload={"name": name, "recovery": recovery})
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ---------------------------------------------------------------- scaffold fixes
|
|
522
|
+
|
|
523
|
+
SCAFFOLD_TITLE = {"add-skeleton": N_("补上缺少的骨架文件"), "profile": N_("修正 Profile 字段"), "repair-lock": N_("修复内容锁版本")}
|
|
524
|
+
DECISIONS_FILE = ".agents/moe.sakanano.agent-pack/check-decisions.json"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def scaffold_module(provider: Any) -> Any | None:
|
|
528
|
+
host = provider.host
|
|
529
|
+
return host.scaffold_module(host.current_env()) if host is not None and hasattr(host, "scaffold_module") else None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _preview_lines(text: str, limit: int = 30) -> str:
|
|
533
|
+
lines = text.splitlines()
|
|
534
|
+
shown = [f"+{line}" for line in lines[:limit]]
|
|
535
|
+
return "\n".join(shown + ([_("... 还有 {0} 行").format(len(lines) - limit)] if len(lines) > limit else [])) or _("(空文件)")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _plan_scaffold(provider: Any, fix_id: str, choices: dict[str, str]) -> dict:
|
|
539
|
+
title = _(SCAFFOLD_TITLE[fix_id])
|
|
540
|
+
module = scaffold_module(provider)
|
|
541
|
+
if module is None:
|
|
542
|
+
return _step(fix_id, title, blocked=_("需要带脚手架检查的 agents-init(更新插件或运行时后再试)"))
|
|
543
|
+
root = provider.root
|
|
544
|
+
state_dir = provider.host.current_env().state_dir
|
|
545
|
+
wanted = [item for item in (choices.get("_targets") or "").split("\n") if item]
|
|
546
|
+
try:
|
|
547
|
+
if fix_id == "repair-lock":
|
|
548
|
+
result = module.repair_lock(root)
|
|
549
|
+
if result["blocked"]:
|
|
550
|
+
return _step(fix_id, title, blocked=result["blocked"])
|
|
551
|
+
return _step(fix_id, title, changes=result["changes"] + [result["diff"]] if result["diff"] else result["changes"],
|
|
552
|
+
writes=[{"path": ".agents/moe.sakanano.agent-pack/content.lock.json", "scope": _("项目(内容锁)")}],
|
|
553
|
+
recovery=_("内容锁由工具生成;写入前再次核对内容未变,可用 git 或同步盘历史还原"),
|
|
554
|
+
verify=_("重新检查:内容锁不再显示 0.0.0"))
|
|
555
|
+
report = module.check(root, state_dir=state_dir, reprompt=True)
|
|
556
|
+
recovery = Path(choices.get("_recovery") or _recovery_dir(provider, fix_id))
|
|
557
|
+
if fix_id == "add-skeleton":
|
|
558
|
+
rows = [row["path"] for row in report["skeleton"] if row["state"] == "missing" and row["required"]]
|
|
559
|
+
rows = [p for p in rows if not wanted or p in wanted]
|
|
560
|
+
unknown = [p for p in wanted if p not in {r["path"] for r in report["skeleton"]}]
|
|
561
|
+
if unknown:
|
|
562
|
+
raise EntryError(_("不在期望骨架清单里:") + _("、").join(unknown), "usage")
|
|
563
|
+
if not rows:
|
|
564
|
+
return _step(fix_id, title, blocked=_("没有缺少的骨架文件"))
|
|
565
|
+
ctx = module.Context(root)
|
|
566
|
+
custom = [{"path": p, "state": _("缺少"), "default": "accept", "options": True} for p in rows]
|
|
567
|
+
decided = {p: choices.get(p, "accept") for p in rows}
|
|
568
|
+
accepted = [p for p, choice in decided.items() if choice == "accept"]
|
|
569
|
+
preview = module.add_skeleton(root, accepted, apply=False, state_dir=state_dir) if accepted else \
|
|
570
|
+
{"create": [], "unchanged": []}
|
|
571
|
+
writes = [{"path": item["path"], "scope": _("项目(新建)")} for item in preview["create"]]
|
|
572
|
+
other = {p: c for p, c in decided.items() if c != "accept"}
|
|
573
|
+
if other:
|
|
574
|
+
writes.append({"path": DECISIONS_FILE, "scope": _("项目(决定文件,随项目同步)")})
|
|
575
|
+
return _step(fix_id, title, changes=[_("{0}:{1}").format(p, {'accept': _('新建'), 'skip': _('永久跳过'), 'later': _('稍后')}[c])
|
|
576
|
+
for p, c in decided.items()],
|
|
577
|
+
writes=writes, custom=custom,
|
|
578
|
+
recovery=_("只新建文件,已有文件不会被改动;新建的可直接删除"),
|
|
579
|
+
verify=_("重新检查:这些文件不再显示为缺少"),
|
|
580
|
+
payload={"accepted": accepted, "decisions": other, "recovery": str(recovery),
|
|
581
|
+
"diffs": {p: _preview_lines(ctx.expected_text(p) or "") for p in rows}})
|
|
582
|
+
# profile
|
|
583
|
+
rows = {row["field"]: row for row in report["profile"] if row["state"] != "ok" and row["mode"] in ("default", "equal")}
|
|
584
|
+
unknown = [f for f in wanted if f not in rows]
|
|
585
|
+
if unknown:
|
|
586
|
+
raise EntryError(_("这些字段不需要或不能自动修复:") + _("、").join(unknown), "usage")
|
|
587
|
+
fields = [f for f in rows if not wanted or f in wanted]
|
|
588
|
+
if not fields:
|
|
589
|
+
return _step(fix_id, title, blocked=_("没有需要修正的 Profile 字段"))
|
|
590
|
+
decided = {f: choices.get(f, "accept") for f in fields}
|
|
591
|
+
accepted = [f for f, choice in decided.items() if choice == "accept"]
|
|
592
|
+
diffs = {f: module.fix_profile(root, [f], apply=False, state_dir=state_dir)["diff"] for f in fields}
|
|
593
|
+
writes = []
|
|
594
|
+
if accepted:
|
|
595
|
+
writes.append({"path": ".agents/moe.sakanano.agent-pack/project.json", "scope": _("项目(先备份)")})
|
|
596
|
+
other = {f: c for f, c in decided.items() if c != "accept"}
|
|
597
|
+
if other:
|
|
598
|
+
writes.append({"path": DECISIONS_FILE, "scope": _("项目(决定文件,随项目同步)")})
|
|
599
|
+
return _step(fix_id, title,
|
|
600
|
+
changes=[f"{f} → {json.dumps(rows[f]['expected'], ensure_ascii=False)}"
|
|
601
|
+
if decided[f] == "accept" else _("{0}:{1}").format(f, {'skip': _('永久跳过'), 'later': _('稍后')}[decided[f]])
|
|
602
|
+
for f in fields],
|
|
603
|
+
writes=writes, custom=[{"path": f, "state": _("缺少") if rows[f]["state"] == "missing" else _("不符合"),
|
|
604
|
+
"default": "accept", "options": True} for f in fields],
|
|
605
|
+
recovery=str(recovery), verify=_("重新检查:这些字段不再显示为缺少或不符合"),
|
|
606
|
+
payload={"accepted": accepted, "decisions": other, "recovery": str(recovery), "diffs": diffs})
|
|
607
|
+
except Exception as exc: # noqa: BLE001 - init errors come from a separately loaded module copy
|
|
608
|
+
if isinstance(exc, EntryError):
|
|
609
|
+
raise
|
|
610
|
+
return _step(fix_id, title, blocked=str(exc))
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
PLAN_ONLY = N_("只给方案,不会自动执行;按方案自己处理后重新检查")
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _plan_guidance(provider: Any, fix_id: str) -> dict:
|
|
617
|
+
"""Plan-only items: they explain what to do and change nothing (no directory moves, no deletions)."""
|
|
618
|
+
if fix_id == "case-naming":
|
|
619
|
+
title = _("Case 目录命名:改名计划")
|
|
620
|
+
module = scaffold_module(provider)
|
|
621
|
+
if module is None:
|
|
622
|
+
return _step(fix_id, title, blocked=_("需要带脚手架检查的 agents-init(更新插件或运行时后再试)"))
|
|
623
|
+
try:
|
|
624
|
+
plan = module.case_naming_plan(provider.root)
|
|
625
|
+
except Exception as exc: # noqa: BLE001
|
|
626
|
+
return _step(fix_id, title, blocked=str(_entry_error(exc)))
|
|
627
|
+
if plan["error"]:
|
|
628
|
+
return _step(fix_id, title, blocked=_("Profile 的 work.case.directory_naming 无法使用:") + plan["error"])
|
|
629
|
+
if not plan["renames"]:
|
|
630
|
+
return _step(fix_id, title, blocked=_("Case 目录命名与规则一致,没有需要改的"))
|
|
631
|
+
changes = []
|
|
632
|
+
for item in plan["renames"]:
|
|
633
|
+
line = f"{item['from']} → {item['to'] or _('(需要你决定新名字)')}"
|
|
634
|
+
if item["references"]:
|
|
635
|
+
line += _(";被这些文件提到,改名后要同步:{0}").format(_('、').join(item['references'][:5])) \
|
|
636
|
+
+ (_(" 等 {0} 个").format(len(item['references'])) if len(item["references"]) > 5 else "")
|
|
637
|
+
if item["note"]:
|
|
638
|
+
line += _("({0})").format(item['note'])
|
|
639
|
+
changes.append(line)
|
|
640
|
+
if plan["declaration_hint"]:
|
|
641
|
+
hint = json.dumps(plan["declaration_hint"]["if_this_is_your_convention"], ensure_ascii=False)
|
|
642
|
+
changes.append(_("如果这些名字正是你的约定,不用改名:在 Profile 里声明它,之后按声明检查 → ") + hint)
|
|
643
|
+
changes.append(_("每个 Case 的 CASE.md 里的 id 字段也要和新目录名保持一致"))
|
|
644
|
+
return _step(fix_id, title, changes=changes, recovery=_("本处理项不移动、不改写任何文件"),
|
|
645
|
+
verify=_("按计划改名并更新引用后,重新运行检查"), blocked=_(PLAN_ONLY) + _("(不会移动目录)"))
|
|
646
|
+
if fix_id == "local-only":
|
|
647
|
+
return _plan_local_only(provider)
|
|
648
|
+
title = _("同步盘使用指引")
|
|
649
|
+
host = provider.host
|
|
650
|
+
where = host.sync_folder(provider.root) if host is not None and hasattr(host, "sync_folder") else None
|
|
651
|
+
if where is None:
|
|
652
|
+
return _step(fix_id, title, blocked=_("项目不在同步盘里"))
|
|
653
|
+
changes = [
|
|
654
|
+
_("随项目同步的:Profile、内容锁、决定文件(check-decisions.json)、所有项目文件"),
|
|
655
|
+
_("每台机器各一份的:用户级运行时(~/.local/share/agenthelm)、版本缓存、wf 的资料台账缓存(按项目绝对路径命名)、本机决定"),
|
|
656
|
+
_("所以:两台机器的 wf 结果可能不同——台账各自重建,且取决于各自装的 project-orchestrator 版本;"
|
|
657
|
+
"用 agenthelm check --fingerprint 对比两台机器的脚手架是否一致"),
|
|
658
|
+
]
|
|
659
|
+
if where["rebuildable"]:
|
|
660
|
+
listed = _("、").join(where["rebuildable"][:8])
|
|
661
|
+
changes.append(_("项目里有可重建目录:{0}。它们不需要同步,可以:").format(listed))
|
|
662
|
+
changes.append(_(" · 推荐:agenthelm local-only check 逐项查看,agenthelm local-only fix 把 .venv、node_modules 等"
|
|
663
|
+
"外置到本机(~/.local/share/agenthelm/local-only/…)并在原位置建软链接;yarn 经典版、bun、uv 实测可用,"
|
|
664
|
+
"npm 会把链接换回真实目录、pnpm 会安装失败,同步目录里请尽量避免它们"))
|
|
665
|
+
changes.append(_(" · 写进 .gitignore(版本管理忽略它们,不影响同步盘)"))
|
|
666
|
+
changes.append(_(" · 或在 {0} 的设置里把这些目录设为不同步(选择性同步)").format(where['service']))
|
|
667
|
+
changes.append(_("本工具不会替你移动、改名或删除任何内容"))
|
|
668
|
+
return _step(fix_id, title, changes=changes, recovery=_("本处理项不改任何文件"), verify=_("不涉及"),
|
|
669
|
+
blocked=_(PLAN_ONLY))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _plan_local_only(provider: Any) -> dict:
|
|
673
|
+
"""只展示 agenthelm local-only fix 的预览:移动目录属于用户的同步盘内容,由它自带预览与回滚的命令执行。"""
|
|
674
|
+
title = _("本机专属目录:外置到本机")
|
|
675
|
+
host = provider.host
|
|
676
|
+
if host is None or not hasattr(host, "local_only_plan"):
|
|
677
|
+
return _step("local-only", title, blocked=_("需要带 local-only 的 agent-pack(更新插件或运行时后再试)"))
|
|
678
|
+
try:
|
|
679
|
+
plan = host.local_only_plan(host.current_env(), provider.root)
|
|
680
|
+
except Exception as exc: # noqa: BLE001 - LocalOnlyError 等:把原因原样给用户
|
|
681
|
+
return _step("local-only", title, blocked=str(exc))
|
|
682
|
+
changes = [step["detail"] for step in plan["steps"]] + [_("不处理:{0}").format(message) for message in plan["blocked"]]
|
|
683
|
+
if not plan["steps"]:
|
|
684
|
+
return _step("local-only", title, changes=changes,
|
|
685
|
+
blocked=_("没有可以自动处理的项;体积阈值等需要你选择,见 health 里各项给出的选项"))
|
|
686
|
+
changes.append(_("执行:agenthelm local-only fix --apply(先预览;移动失败整体撤销)"))
|
|
687
|
+
return _step("local-only", title, changes=changes, recovery=_("移动失败整体撤销;旧配置备份在 ~/.agent-pack/recovery/"),
|
|
688
|
+
verify=_("重新运行 health,或 agenthelm local-only check"), blocked=PLAN_ONLY + _("(这里只展示预览)"))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def record_decisions(provider: Any, items: dict[str, str], reason: str, *, prefix: str, apply: bool) -> list[str]:
|
|
692
|
+
"""Persist 永久跳过 / 稍后 choices (``{path_or_field: skip|later}``) made while fixing."""
|
|
693
|
+
module = scaffold_module(provider)
|
|
694
|
+
env = provider.host.current_env()
|
|
695
|
+
written = []
|
|
696
|
+
for key, choice in items.items():
|
|
697
|
+
module.decide(provider.root, f"{prefix}:{key}", choice, reason=reason if choice == "skip" else "",
|
|
698
|
+
apply=apply, state_dir=env.state_dir)
|
|
699
|
+
written.append(f"{prefix}:{key}")
|
|
700
|
+
return written
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _entry_error(exc: Exception) -> EntryError:
|
|
704
|
+
"""agents-init raises its own InitError (a separately loaded class): map its code to the entry's."""
|
|
705
|
+
if isinstance(exc, EntryError):
|
|
706
|
+
return exc
|
|
707
|
+
code = {"conflict": "conflict", "profile-invalid": "profile-invalid"}.get(getattr(exc, "code", ""), "usage")
|
|
708
|
+
return EntryError(str(exc), code)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def decide_ids(provider: Any, skip: list[str], unskip: list[str], reason: str, apply: bool) -> dict:
|
|
712
|
+
"""``fix --skip <id> --reason …`` / ``--unskip <id>``: only decisions are written, never project files."""
|
|
713
|
+
module = scaffold_module(provider)
|
|
714
|
+
if module is None:
|
|
715
|
+
raise EntryError(_("需要带脚手架检查的 agents-init(更新插件或运行时后再试)"), "unavailable")
|
|
716
|
+
health = provider._health or provider.health()
|
|
717
|
+
known = {f["decision"]["id"]: f["decision"] for f in health["findings"] if f.get("decision")}
|
|
718
|
+
env = provider.host.current_env()
|
|
719
|
+
results = []
|
|
720
|
+
for identifier in skip:
|
|
721
|
+
if identifier not in known:
|
|
722
|
+
raise EntryError(_("现在没有这个发现:{0}").format(identifier), "usage", [_("可跳过:") + (_("、").join(sorted(known)) or _("(无)"))])
|
|
723
|
+
item = known[identifier]
|
|
724
|
+
try:
|
|
725
|
+
results.append(module.decide(provider.root, identifier, "skip", reason=reason,
|
|
726
|
+
fingerprint=item["fingerprint"], scope=item["scope"], apply=apply,
|
|
727
|
+
state_dir=env.state_dir))
|
|
728
|
+
except Exception as exc: # noqa: BLE001
|
|
729
|
+
raise _entry_error(exc) from exc
|
|
730
|
+
for identifier in unskip:
|
|
731
|
+
try:
|
|
732
|
+
results.append(module.undecide(provider.root, identifier, apply=apply, state_dir=env.state_dir))
|
|
733
|
+
except Exception as exc: # noqa: BLE001
|
|
734
|
+
raise _entry_error(exc) from exc
|
|
735
|
+
provider._health = None
|
|
736
|
+
return {"decisions": results, "applied": apply}
|
|
381
737
|
|
|
382
738
|
|
|
383
739
|
# ---------------------------------------------------------------- apply
|
|
@@ -395,7 +751,7 @@ def apply(provider: Any, planned: dict, choices: dict[str, str]) -> dict:
|
|
|
395
751
|
fresh = _plan_step(provider, step["id"], effective)
|
|
396
752
|
if [w["path"] for w in fresh["writes"]] != [w["path"] for w in step["writes"]]:
|
|
397
753
|
results.append({"id": step["id"], "status": "skipped",
|
|
398
|
-
"detail": "预览之后情况有变化(写入范围不同),没有执行:请重新预览"})
|
|
754
|
+
"detail": _("预览之后情况有变化(写入范围不同),没有执行:请重新预览")})
|
|
399
755
|
continue
|
|
400
756
|
try:
|
|
401
757
|
results.append(_apply_step(provider, fresh, effective))
|
|
@@ -406,6 +762,31 @@ def apply(provider: Any, planned: dict, choices: dict[str, str]) -> dict:
|
|
|
406
762
|
return {"results": results, "simulated": False}
|
|
407
763
|
|
|
408
764
|
|
|
765
|
+
def _apply_scaffold(provider: Any, step: dict, choices: dict[str, str], base: dict) -> dict:
|
|
766
|
+
fix_id = step["id"]
|
|
767
|
+
module = scaffold_module(provider)
|
|
768
|
+
env = provider.host.current_env()
|
|
769
|
+
payload = step["payload"]
|
|
770
|
+
if fix_id == "repair-lock":
|
|
771
|
+
result = module.repair_lock(provider.root, apply=True)
|
|
772
|
+
return {**base, "verify": _("内容锁版本已修复") if result["applied"] else _("没有需要修复的内容")}
|
|
773
|
+
accepted = payload["accepted"]
|
|
774
|
+
written = []
|
|
775
|
+
if fix_id == "add-skeleton" and accepted:
|
|
776
|
+
result = module.add_skeleton(provider.root, accepted, apply=True, state_dir=env.state_dir)
|
|
777
|
+
written = [item["path"] for item in result["create"]]
|
|
778
|
+
if fix_id == "profile" and accepted:
|
|
779
|
+
result = module.fix_profile(provider.root, accepted, apply=True, recovery_dir=Path(payload["recovery"]),
|
|
780
|
+
state_dir=env.state_dir)
|
|
781
|
+
written = [".agents/moe.sakanano.agent-pack/project.json"]
|
|
782
|
+
prefix = "skel" if fix_id == "add-skeleton" else "profile"
|
|
783
|
+
recorded = record_decisions(provider, payload["decisions"], choices.get("_reason", ""), prefix=prefix, apply=True)
|
|
784
|
+
return {**base, "written": written + ([DECISIONS_FILE] if recorded else []),
|
|
785
|
+
"verify": _("重新检查:") + (_("、").join(accepted) or _("没有写入文件"))
|
|
786
|
+
+ (_(";已记录决定:{0}").format(_('、').join(recorded)) if recorded else ""),
|
|
787
|
+
"recovery": payload["recovery"] if fix_id == "profile" and accepted else _("只新建文件,无需备份")}
|
|
788
|
+
|
|
789
|
+
|
|
409
790
|
def _apply_step(provider: Any, step: dict, choices: dict[str, str]) -> dict:
|
|
410
791
|
fix_id = step["id"]
|
|
411
792
|
base = {"id": fix_id, "status": "applied", "written": [w["path"] for w in step["writes"]], "kept": [],
|
|
@@ -413,16 +794,20 @@ def _apply_step(provider: Any, step: dict, choices: dict[str, str]) -> dict:
|
|
|
413
794
|
if fix_id == "rebuild-index":
|
|
414
795
|
result = assets.rebuild(provider.assets_project(), force=True)
|
|
415
796
|
state = assets.status(provider.assets_project())
|
|
416
|
-
return {**base, "verify":
|
|
417
|
-
|
|
797
|
+
return {**base, "verify": _("索引{0},{1} 个文件").format(_('最新') if state['status'] == 'fresh' else _('状态:') + state['status'], result['file_count'])}
|
|
798
|
+
if fix_id in ("add-skeleton", "profile", "repair-lock"):
|
|
799
|
+
try:
|
|
800
|
+
return _apply_scaffold(provider, step, choices, base)
|
|
801
|
+
except Exception as exc: # noqa: BLE001
|
|
802
|
+
raise _entry_error(exc) from exc
|
|
418
803
|
if fix_id == "add-entry":
|
|
419
804
|
name = step["payload"]["name"]
|
|
420
805
|
result = _run_init(provider, name, choices, step["payload"]["recovery"], apply=True)
|
|
421
806
|
if not result.get("applied"):
|
|
422
|
-
raise EntryError("没有写入:" + str(result.get("hint") or result.get("error") or result.get("blocked")), "failed")
|
|
807
|
+
raise EntryError(_("没有写入:") + str(result.get("hint") or result.get("error") or result.get("blocked")), "failed")
|
|
423
808
|
launcher = provider.root / name
|
|
424
809
|
ready = launcher.is_file() and launcher.stat().st_mode & 0o111
|
|
425
|
-
return {**base, "verify":
|
|
810
|
+
return {**base, "verify": _("./{0} {1};Profile 已声明 entry").format(name, _('已就绪') if ready else _('未就绪'))}
|
|
426
811
|
host = provider.host
|
|
427
812
|
env = host.current_env()
|
|
428
813
|
if fix_id in ("install-runtime", "update-runtime"):
|
|
@@ -430,9 +815,9 @@ def _apply_step(provider: Any, step: dict, choices: dict[str, str]) -> dict:
|
|
|
430
815
|
if step["payload"].get("from_release"):
|
|
431
816
|
import tempfile
|
|
432
817
|
with tempfile.TemporaryDirectory(prefix="agenthelm-runtime-") as temporary:
|
|
433
|
-
latest,
|
|
818
|
+
latest, _origin = host.load_latest(env, None, max_age=0)
|
|
434
819
|
root = host.release_source(latest, Path(temporary))
|
|
435
|
-
planned = runtime.plan(env, root, "GitHub Release(已核对摘要)")
|
|
820
|
+
planned = runtime.plan(env, root, _("GitHub Release(已核对摘要)"))
|
|
436
821
|
result = runtime.apply(env, planned, root)
|
|
437
822
|
else:
|
|
438
823
|
planned = step["payload"]["plan"]
|
|
@@ -447,22 +832,27 @@ def _apply_step(provider: Any, step: dict, choices: dict[str, str]) -> dict:
|
|
|
447
832
|
behind = [item for item in after["plugins"] if item["status"] == "behind"]
|
|
448
833
|
return {**base, "status": "failed" if failed else "applied",
|
|
449
834
|
"detail": failed[0].get("output", "") if failed else "",
|
|
450
|
-
"verify": "重新检查:" + ("全部为最新" if not behind else
|
|
835
|
+
"verify": _("重新检查:") + (_("全部为最新") if not behind else _("仍有 {0} 个落后").format(len(behind)))}
|
|
451
836
|
# update-content
|
|
452
837
|
custom = {item["path"] for item in step["custom"]}
|
|
453
838
|
keep = [path for path in custom if choices.get(path, "keep") == "keep"]
|
|
454
839
|
accept = [path for path in custom if choices.get(path) == "accept"]
|
|
455
840
|
before = {path: _digest(provider.root / path) for path in keep}
|
|
456
841
|
recovery = Path(step["payload"]["recovery"])
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
842
|
+
if step["payload"].get("direct"):
|
|
843
|
+
result = _run_refresh(provider, accept, keep, recovery, apply=True)
|
|
844
|
+
if not result.get("applied"):
|
|
845
|
+
raise EntryError(_("模板更新没有执行:") + str(result.get("hint") or result), "failed")
|
|
846
|
+
else:
|
|
847
|
+
steps = host.update_steps(env, provider._report, provider.root, None, env.home, accept, keep, recovery, "content")
|
|
848
|
+
outcome = host.execute_steps(steps, apply=True, recovery_root=None, child_env=env.child_environment())[0]
|
|
849
|
+
result = outcome.get("result") or {}
|
|
850
|
+
if outcome["status"] == "failed" or not result.get("applied"):
|
|
851
|
+
raise EntryError(_("模板更新没有执行:") + str(outcome.get("error") or result.get("hint") or result), "failed")
|
|
462
852
|
untouched = all(_digest(provider.root / path) == digest for path, digest in before.items())
|
|
463
|
-
return {**base, "kept": keep, "accepted": accept, "recovery": result.get("recovery") or "没有替换文件,无需备份",
|
|
464
|
-
"verify": "重新读取:保留的文件" + ("内容未变" if untouched else "被改动了,请检查")
|
|
465
|
-
+ ";内容锁已更新到新版本"}
|
|
853
|
+
return {**base, "kept": keep, "accepted": accept, "recovery": result.get("recovery") or _("没有替换文件,无需备份"),
|
|
854
|
+
"verify": _("重新读取:保留的文件") + (_("内容未变") if untouched else _("被改动了,请检查"))
|
|
855
|
+
+ _(";内容锁已更新到新版本")}
|
|
466
856
|
|
|
467
857
|
|
|
468
858
|
def _digest(path: Path) -> str | None:
|