@sakanano-moe/agenthelm 6.5.3 → 6.7.0
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/README.md +68 -0
- package/README.zh-CN.md +28 -0
- package/bin/agenthelm.js +1 -1
- package/package.json +2 -2
- package/runtime/index.json +80 -0
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +26 -0
- package/runtime/plugins/agent-pack/.mcp.json +12 -0
- package/runtime/plugins/agent-pack/hooks/hooks.json +16 -0
- package/runtime/plugins/agent-pack/mcp.json +14 -0
- package/runtime/plugins/agent-pack/plugin.json +20 -0
- package/runtime/plugins/agent-pack/references/capability-config.md +148 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_claude.py +448 -0
- package/runtime/{agent_pack_config.py → plugins/agent-pack/runtime/agent_pack_config.py} +178 -22
- package/runtime/{agenthelm.py → plugins/agent-pack/runtime/agenthelm.py} +42 -1
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +388 -0
- package/runtime/{mcp_server.py → plugins/agent-pack/runtime/mcp_server.py} +30 -8
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +46 -0
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +22 -0
- package/runtime/plugins/agents-init/content.json +31 -0
- package/runtime/plugins/agents-init/plugin.json +19 -0
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +376 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +119 -0
- package/runtime/plugins/agents-init/skills/agents-init/index.html +357 -0
- package/runtime/plugins/agents-init/skills/agents-init/launcher.sh +93 -0
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +171 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +1190 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +678 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +122 -0
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +26 -0
- package/runtime/plugins/opinion-manager/catalog/README.md +15 -0
- package/runtime/plugins/opinion-manager/catalog/rules/.gitkeep +0 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.align-requirements/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.assumptions-authority/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.batch-questions/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.bilingual-terms/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.calculations/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.chinese-response/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.clean-deliverable/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.complete-options/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.conclusion-first/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.concrete-titles/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.consistent-items/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.device-readable/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.editable-diagrams/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.evidence-inference/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-chinese/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-project-language/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.markdown-structure/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.portable-files/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-context/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-formats/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-review/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.response-brief/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.response-dense/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.rules-tools/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.semantic-wrap/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-proportional/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-three-level/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.tables-compact/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.tables-list-first/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.test-evidence/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.versioned-files/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/templates/.gitkeep +0 -0
- package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/compact-tables/versions/1.0.0.json +21 -0
- package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/list-first/versions/1.0.0.json +21 -0
- package/runtime/plugins/opinion-manager/catalog/templates/development-standard/chinese-maintenance/versions/1.0.0.json +15 -0
- package/runtime/plugins/opinion-manager/catalog/templates/development-standard/project-language/versions/1.0.0.json +15 -0
- package/runtime/plugins/opinion-manager/catalog/templates/response-style/brief-chinese/versions/1.0.0.json +19 -0
- package/runtime/plugins/opinion-manager/catalog/templates/response-style/dense-chinese/versions/1.0.0.json +19 -0
- package/runtime/plugins/opinion-manager/catalog/templates/scenario/concise-project/versions/1.0.0.json +39 -0
- package/runtime/plugins/opinion-manager/catalog/templates/scenario/reviewed-project/versions/1.0.0.json +39 -0
- package/runtime/plugins/opinion-manager/catalog/templates/work-method/proportional/versions/1.0.0.json +20 -0
- package/runtime/plugins/opinion-manager/catalog/templates/work-method/three-level/versions/1.0.0.json +20 -0
- package/runtime/plugins/opinion-manager/content.json +63 -0
- package/runtime/plugins/opinion-manager/plugin.json +18 -0
- package/runtime/plugins/opinion-manager/references/lock.schema.json +14 -0
- package/runtime/plugins/opinion-manager/references/onboarding.md +57 -0
- package/runtime/plugins/opinion-manager/references/profile.schema.json +56 -0
- package/runtime/plugins/opinion-manager/references/rule.schema.json +16 -0
- package/runtime/plugins/opinion-manager/references/template.schema.json +71 -0
- package/runtime/plugins/opinion-manager/references/versioned-template.schema.json +19 -0
- package/runtime/plugins/opinion-manager/references/versioning.md +263 -0
- package/runtime/plugins/opinion-manager/runtime/constants.py +7 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +180 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +395 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +419 -0
- package/runtime/plugins/opinion-manager/runtime/versioned.py +705 -0
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +62 -0
- package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +4 -0
- package/runtime/plugins/opinion-manager/templates/.gitkeep +1 -0
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +24 -0
- package/runtime/plugins/project-orchestrator/content.json +37 -0
- package/runtime/plugins/project-orchestrator/plugin.json +19 -0
- package/runtime/plugins/project-orchestrator/references/asset-synonyms.example.json +8 -0
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +159 -0
- package/runtime/plugins/project-orchestrator/references/case.schema.json +31 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +1288 -0
- package/runtime/plugins/project-orchestrator/runtime/case.py +557 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +350 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +363 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +107 -0
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +599 -0
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +504 -0
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +470 -0
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +394 -0
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +522 -0
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +329 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +439 -0
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +230 -0
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/agents/openai.yaml +4 -0
- package/runtime/plugins/project-orchestrator/templates/CASE.md +39 -0
- /package/runtime/{mcp_env_launcher.py → plugins/agent-pack/runtime/mcp_env_launcher.py} +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""项目入口的维护:版本检查,以及“检查 → 选择 → 看影响 → 确认 → 验证”的处理项。
|
|
2
|
+
|
|
3
|
+
处理项按影响对象分开,互不混合:
|
|
4
|
+
|
|
5
|
+
- ``rebuild-index``:重建资料索引,只写用户缓存;
|
|
6
|
+
- ``update-plugins``:各客户端里的插件代码(调用客户端自己的市场命令,需联网);
|
|
7
|
+
- ``update-content``:项目里由模板生成的文件,改过的文件逐个保留或采用,先备份;
|
|
8
|
+
- ``install-runtime`` / ``update-runtime``:用户级运行时,新版本装在旁边,旧版本保留;
|
|
9
|
+
- ``add-entry``:为已初始化的项目补上根目录启动器与 Profile 的 entry 段。
|
|
10
|
+
|
|
11
|
+
个人规则(Opinion)的新版本只报告,从不在这里采纳:那由 opinion-manager 预览后经用户确认。
|
|
12
|
+
插件、模板与运行时的检查经 agent-pack(``HOST``)完成;没有它时这些项标为不可用。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import assets
|
|
25
|
+
from project_base import DEFAULT_LAUNCHER, LAUNCHER_RE, EntryError
|
|
26
|
+
|
|
27
|
+
CLIENT_LABEL = {"claude": "Claude Code", "codex": "Codex", "cursor": "Cursor", "legacy": "旧方式安装"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _iso(seconds: float | None) -> str:
|
|
31
|
+
return datetime.fromtimestamp(seconds).astimezone().replace(microsecond=0).isoformat() if seconds else ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def runtime_module(host: Any) -> Any:
|
|
35
|
+
path = Path(host.__file__).resolve().parent / "agenthelm_runtime.py"
|
|
36
|
+
spec = importlib.util.spec_from_file_location("agenthelm_runtime", path)
|
|
37
|
+
module = importlib.util.module_from_spec(spec)
|
|
38
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
39
|
+
return module
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------- check
|
|
43
|
+
|
|
44
|
+
def check_updates(provider: Any, findings: list[dict], ok: list[str]) -> dict:
|
|
45
|
+
host = provider.host
|
|
46
|
+
if host is None:
|
|
47
|
+
return {"status": "unavailable", "cached_at": ""}
|
|
48
|
+
env = host.current_env()
|
|
49
|
+
try:
|
|
50
|
+
report = host.run_check(env, provider.root, offline=provider.offline)
|
|
51
|
+
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": ""}]})
|
|
55
|
+
provider._report = None
|
|
56
|
+
_check_runtime(provider, env, None, findings, ok)
|
|
57
|
+
return {"status": "offline", "cached_at": ""}
|
|
58
|
+
provider._report = report
|
|
59
|
+
origin = report["latest"]["source"]
|
|
60
|
+
if origin.get("stale") or (provider.offline and origin.get("cached")):
|
|
61
|
+
source = {"status": "offline", "cached_at": _iso(origin.get("fetched_at"))}
|
|
62
|
+
else:
|
|
63
|
+
source = {"status": "cache" if origin.get("cached") else "online", "cached_at": _iso(origin.get("fetched_at"))}
|
|
64
|
+
_plugin_findings(provider, env, report, findings, ok)
|
|
65
|
+
_content_findings(report, findings, ok)
|
|
66
|
+
_opinion_findings(report, findings, ok)
|
|
67
|
+
_check_runtime(provider, env, report["_latest"], findings, ok)
|
|
68
|
+
return source
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _plugin_findings(provider: Any, env: Any, report: dict, findings: list[dict], ok: list[str]) -> None:
|
|
72
|
+
host = provider.host
|
|
73
|
+
plugins = [item for item in report["plugins"] if item["client"] != "legacy"]
|
|
74
|
+
behind = [item for item in plugins if item["status"] == "behind"]
|
|
75
|
+
if behind:
|
|
76
|
+
breaking = [item for item in behind if item["breaking"]]
|
|
77
|
+
details = ";".join(f"{item['plugin']} {item['version']} → {item['latest']}({CLIENT_LABEL[item['client']]})"
|
|
78
|
+
for item in behind)
|
|
79
|
+
findings.append({"id": "update-plugins", "level": "update", "area": "插件",
|
|
80
|
+
"title": f"{len(behind)} 个插件有新版本" + ("(含不兼容变更)" if breaking else ""),
|
|
81
|
+
"reason": details, "impact": "只影响 AI 客户端里的插件;不更新也能继续使用本菜单",
|
|
82
|
+
"next": [{"label": "查看更新内容与迁移说明,再决定是否更新", "action": "fix:update-plugins"}]})
|
|
83
|
+
elif plugins:
|
|
84
|
+
ok.append("客户端插件(最新)")
|
|
85
|
+
if not plugins:
|
|
86
|
+
clients = host.detect_clients(env)
|
|
87
|
+
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": ""}]})
|
|
93
|
+
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": ""}]})
|
|
98
|
+
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": ""}]})
|
|
102
|
+
for issue in host.doctor(env, report)["issues"]:
|
|
103
|
+
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": ""}]})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _content_findings(report: dict, findings: list[dict], ok: list[str]) -> None:
|
|
112
|
+
updates = [item for item in report["content"] if item.get("status") in ("update-available", "conflict")]
|
|
113
|
+
for item in report["content"]:
|
|
114
|
+
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": ""}]})
|
|
118
|
+
if updates:
|
|
119
|
+
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
|
+
+ (f";你改过的:{'、'.join(changed)}" if changed else ""),
|
|
124
|
+
"impact": "不更新不影响使用;更新时你改过的文件逐个选择保留或采用",
|
|
125
|
+
"next": [{"label": "查看影响并选择", "action": "fix:update-content"}]})
|
|
126
|
+
elif report["content"]:
|
|
127
|
+
ok.append("项目模板(最新)")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _opinion_findings(report: dict, findings: list[dict], ok: list[str]) -> None:
|
|
131
|
+
for item in report["opinion"]:
|
|
132
|
+
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": ""}]})
|
|
138
|
+
elif item["status"] in ("error", "unknown"):
|
|
139
|
+
findings.append({"id": "opinion-check", "level": "unavailable", "area": "个人规则",
|
|
140
|
+
"title": "行为规则版本无法检查", "reason": item.get("error", ""),
|
|
141
|
+
"impact": "只影响规则更新提示", "next": []})
|
|
142
|
+
else:
|
|
143
|
+
ok.append("行为规则(已是锁定的最新版本)")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _check_runtime(provider: Any, env: Any, latest: dict | None, findings: list[dict], ok: list[str]) -> None:
|
|
147
|
+
runtime = runtime_module(provider.host)
|
|
148
|
+
data = runtime.report(env, latest)
|
|
149
|
+
active = data["current"]
|
|
150
|
+
if active is None:
|
|
151
|
+
findings.append({"id": "install-runtime", "level": "suggest", "area": "运行时", "title": "还没有安装用户级运行时",
|
|
152
|
+
"reason": f"本次由 {data['running_from']} 提供;装好运行时后,不依赖 AI 客户端与 npx 也能打开菜单",
|
|
153
|
+
"impact": "现在能用;换一台没有 AI 客户端的机器或客户端插件被移除时会打不开",
|
|
154
|
+
"next": [{"label": "安装运行时(只写用户数据目录)", "action": "fix:install-runtime"}]})
|
|
155
|
+
elif data["update_available"]:
|
|
156
|
+
findings.append({"id": "update-runtime", "level": "update", "area": "运行时", "title": "运行时有新版本",
|
|
157
|
+
"reason": f"当前 {active['version']},最新 {data['latest_release']}",
|
|
158
|
+
"impact": "不更新也能继续使用", "next": [{"label": "下载并切换(旧版本保留)", "action": "fix:update-runtime"}]})
|
|
159
|
+
else:
|
|
160
|
+
ok.append(f"运行时 {active['version']}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------- plan
|
|
164
|
+
|
|
165
|
+
FIXES = ("rebuild-index", "update-plugins", "update-content", "install-runtime", "update-runtime", "add-entry")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def available_ids(provider: Any) -> list[str]:
|
|
169
|
+
health = provider._health or provider.health()
|
|
170
|
+
found = []
|
|
171
|
+
for finding in health["findings"]:
|
|
172
|
+
for step in finding["next"]:
|
|
173
|
+
verb, _, target = step["action"].partition(":")
|
|
174
|
+
if verb == "fix" and target in FIXES and target not in found:
|
|
175
|
+
found.append(target)
|
|
176
|
+
return found
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def plan(provider: Any, ids: list[str], choices: dict[str, str]) -> dict:
|
|
180
|
+
offered = available_ids(provider)
|
|
181
|
+
chosen = ids or offered
|
|
182
|
+
unknown = [fix_id for fix_id in chosen if fix_id not in FIXES]
|
|
183
|
+
if unknown:
|
|
184
|
+
raise EntryError("没有这些处理项:" + "、".join(unknown), "not-found", ["可处理:" + ("、".join(offered) or "(无)")])
|
|
185
|
+
steps = [_plan_step(provider, fix_id, choices) for fix_id in chosen]
|
|
186
|
+
return {"steps": steps, "simulated": False, "update_source": (provider._health or {}).get("update_source")}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _step(fix_id: str, title: str, **values: Any) -> dict:
|
|
190
|
+
step = {"id": fix_id, "title": title, "kind": fix_id, "network": False, "changes": [], "writes": [],
|
|
191
|
+
"custom": [], "breaking": False, "recovery": "", "verify": "", "blocked": "", "payload": {}}
|
|
192
|
+
step.update(values)
|
|
193
|
+
return step
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _need_host(provider: Any, fix_id: str, title: str) -> dict | None:
|
|
197
|
+
if provider.host is None:
|
|
198
|
+
return _step(fix_id, title, blocked="当前运行方式没有携带 agent-pack:用 agenthelm 运行时打开后再处理")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _plan_step(provider: Any, fix_id: str, choices: dict[str, str]) -> dict:
|
|
203
|
+
if fix_id == "rebuild-index":
|
|
204
|
+
state_dir = provider.assets_project().state_dir()
|
|
205
|
+
return _step(fix_id, "重建资料索引", changes=["按当前文件重新登记资料、版本与引用"],
|
|
206
|
+
writes=[{"path": str(state_dir), "scope": "用户缓存(不改项目文件)"}],
|
|
207
|
+
recovery="不涉及项目文件;索引可随时再次重建", verify="重新读取索引状态")
|
|
208
|
+
if fix_id == "add-entry":
|
|
209
|
+
return _plan_entry(provider, choices)
|
|
210
|
+
blocked = _need_host(provider, fix_id, fix_id)
|
|
211
|
+
if blocked:
|
|
212
|
+
return blocked
|
|
213
|
+
host = provider.host
|
|
214
|
+
env = host.current_env()
|
|
215
|
+
if fix_id in ("install-runtime", "update-runtime"):
|
|
216
|
+
return _plan_runtime(provider, env, fix_id)
|
|
217
|
+
report = getattr(provider, "_report", None)
|
|
218
|
+
if report is None:
|
|
219
|
+
return _step(fix_id, fix_id, blocked="版本信息不可用(离线且没有缓存):联网后重新检查")
|
|
220
|
+
if fix_id == "update-plugins":
|
|
221
|
+
steps = host.update_steps(env, report, None, None, env.home, [], [], None, "plugins")
|
|
222
|
+
behind = [item for item in report["plugins"] if item["status"] == "behind"]
|
|
223
|
+
changes = [f"{item['plugin']} → {step['to']}:{step['summary']}" for item in behind for step in item["steps"]]
|
|
224
|
+
actions = [action for item in behind for step in item["steps"] for action in step["actions"]]
|
|
225
|
+
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 + [f"需要的动作:{a}" for a in actions],
|
|
228
|
+
writes=[{"path": step.description + (f"({' '.join(step.command)})" if step.command else ""),
|
|
229
|
+
"scope": "客户端插件目录"} for step in steps],
|
|
230
|
+
breaking=any(item["breaking"] for item in behind),
|
|
231
|
+
recovery="客户端保留旧版本缓存;需要时用客户端命令回到旧版本",
|
|
232
|
+
verify="重新检查各客户端已安装的版本", blocked=blocked)
|
|
233
|
+
return _plan_content(provider, env, report, choices)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _plan_content(provider: Any, env: Any, report: dict, choices: dict[str, str]) -> dict:
|
|
237
|
+
host = provider.host
|
|
238
|
+
accept = sorted(path for path, choice in choices.items() if choice == "accept")
|
|
239
|
+
keep = sorted(path for path, choice in choices.items() if choice == "keep")
|
|
240
|
+
recovery = Path(choices.get("_recovery") or host.recovery_dir_for(env, "content"))
|
|
241
|
+
steps = host.update_steps(env, report, provider.root, None, env.home, accept, keep, recovery, "content")
|
|
242
|
+
if not steps:
|
|
243
|
+
return _step("update-content", "项目模板/样例", blocked="没有需要更新的模板")
|
|
244
|
+
step = steps[0]
|
|
245
|
+
if step.blocked:
|
|
246
|
+
return _step("update-content", step.description, blocked=step.blocked)
|
|
247
|
+
preview = host.execute_steps([step], apply=False, recovery_root=None, child_env=env.child_environment())[0]
|
|
248
|
+
files = (preview.get("result") or {}).get("files") or []
|
|
249
|
+
if preview.get("status") == "failed":
|
|
250
|
+
return _step("update-content", step.description, blocked=preview.get("error", "预览失败"))
|
|
251
|
+
custom = [{"path": item["path"], "state": "你改过", "default": "keep"} for item in files
|
|
252
|
+
if item["state"] == "modified" or (item["path"] in choices and item["state"] in ("modified", "missing"))]
|
|
253
|
+
for item in files:
|
|
254
|
+
if item["state"] == "missing" and not any(entry["path"] == item["path"] for entry in custom):
|
|
255
|
+
custom.append({"path": item["path"], "state": "新文件(目前不存在)", "default": "keep"})
|
|
256
|
+
effective = {item["path"]: choices.get(item["path"], item["default"]) for item in custom}
|
|
257
|
+
writes = []
|
|
258
|
+
for item in files:
|
|
259
|
+
action = item["action"]
|
|
260
|
+
if action == "blocked":
|
|
261
|
+
action = "replace" if effective.get(item["path"]) == "accept" else "keep"
|
|
262
|
+
if action == "skip":
|
|
263
|
+
action = "create" if effective.get(item["path"]) == "accept" else "keep"
|
|
264
|
+
if action in ("replace", "create"):
|
|
265
|
+
writes.append({"path": item["path"], "scope": "项目(先备份)" if action == "replace" else "项目(新建)"})
|
|
266
|
+
# 刷新总会把内容锁更新到新版本(保留的文件沿用旧基线),所以它总在写入范围里。
|
|
267
|
+
writes.append({"path": ".agents/moe.sakanano.agent-pack/content.lock.json", "scope": "项目(内容锁)"})
|
|
268
|
+
return _step("update-content", step.description, changes=[_content_change(item, effective) for item in files],
|
|
269
|
+
writes=writes, custom=custom, recovery=str(recovery),
|
|
270
|
+
verify="重新检查模板版本;保留的文件内容不变", payload={"recovery": str(recovery), "files": files})
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
CONTENT_STATE = {"unchanged": "与新模板相同", "clean": "没改过", "modified": "你改过", "missing": "目前不存在",
|
|
274
|
+
"local-only": "只有你的改动,新模板没变"}
|
|
275
|
+
CONTENT_ACTION = {"keep": "保留", "replace": "换成新模板(先备份)", "create": "新建", "skip": "不新建",
|
|
276
|
+
"blocked": "需要你选择"}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _content_change(item: dict, effective: dict[str, str]) -> str:
|
|
280
|
+
action = item["action"]
|
|
281
|
+
if action in ("blocked", "skip"):
|
|
282
|
+
chosen = effective.get(item["path"], "keep")
|
|
283
|
+
action = ("replace" if action == "blocked" else "create") if chosen == "accept" else \
|
|
284
|
+
("keep" if action == "blocked" else "skip")
|
|
285
|
+
note = "(保留则不含新模板的改动)" if item["state"] == "modified" and action == "keep" else ""
|
|
286
|
+
return f"{item['path']}:{CONTENT_STATE.get(item['state'], item['state'])} → {CONTENT_ACTION.get(action, action)}{note}"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _plan_runtime(provider: Any, env: Any, fix_id: str) -> dict:
|
|
290
|
+
runtime = runtime_module(provider.host)
|
|
291
|
+
host = provider.host
|
|
292
|
+
title = "安装用户级运行时" if fix_id == "install-runtime" else "更新用户级运行时"
|
|
293
|
+
if fix_id == "install-runtime" and runtime.bundled_root() is not None:
|
|
294
|
+
root, origin = runtime.bundled_root(), "随附内容"
|
|
295
|
+
network = False
|
|
296
|
+
else:
|
|
297
|
+
if provider.offline:
|
|
298
|
+
return _step(fix_id, title, network=True, blocked="当前离线:需要下载发布包,只能预览;也可用 agenthelm runtime install --source <发布包>")
|
|
299
|
+
try:
|
|
300
|
+
latest, _ = host.load_latest(env, None)
|
|
301
|
+
except (host.CheckError, OSError, ValueError) as exc:
|
|
302
|
+
return _step(fix_id, title, network=True, blocked=f"拿不到版本信息:{exc}")
|
|
303
|
+
version = runtime.release_version(latest)
|
|
304
|
+
return _step(fix_id, title, network=True, changes=[f"下载发布包 {version or '(最新)'} 并核对摘要"],
|
|
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="重新读取当前运行时版本",
|
|
308
|
+
payload={"from_release": True})
|
|
309
|
+
planned = runtime.plan(env, root, origin)
|
|
310
|
+
return _step(fix_id, title, network=network, changes=[f"运行时 {planned['version']}({origin})"],
|
|
311
|
+
writes=planned["writes"], recovery="旧版本保留;agenthelm runtime use <旧版本> --apply 切回",
|
|
312
|
+
verify="重新读取当前运行时版本", payload={"from_release": False, "plan": planned})
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _init_script(provider: Any) -> Path | None:
|
|
316
|
+
candidates = [Path(__file__).resolve().parents[2] / "agents-init"]
|
|
317
|
+
if provider.host is not None:
|
|
318
|
+
env = provider.host.current_env()
|
|
319
|
+
candidates += [Path(item["path"]) for item in provider.host.installed_plugins(env, {"agents-init"})]
|
|
320
|
+
for root in candidates:
|
|
321
|
+
script = root / "skills" / "agents-init" / "scripts" / "init_project.py"
|
|
322
|
+
if script.is_file() and "--entry-name" in script.read_text(encoding="utf-8"):
|
|
323
|
+
return script
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _run_init(provider: Any, name: str, choices: dict[str, str], recovery: str, *, apply: bool) -> dict:
|
|
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", "entry",
|
|
332
|
+
"--entry-name", name, "--output", "json"]
|
|
333
|
+
if choices.get(name) in ("keep", "accept"):
|
|
334
|
+
command += [f"--{choices[name]}", name]
|
|
335
|
+
if choices.get(name) == "accept":
|
|
336
|
+
command += ["--recovery-dir", recovery]
|
|
337
|
+
if choices.get("README.md") == "accept":
|
|
338
|
+
command.append("--readme-entry")
|
|
339
|
+
if apply:
|
|
340
|
+
command.append("--apply")
|
|
341
|
+
completed = subprocess.run(command, capture_output=True, text=True, check=False, timeout=120)
|
|
342
|
+
try:
|
|
343
|
+
return json.loads(completed.stdout)
|
|
344
|
+
except ValueError as exc:
|
|
345
|
+
raise EntryError("agents-init 没有返回结果:" + (completed.stderr or "").strip()[:200], "failed") from exc
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _recovery_dir(provider: Any, label: str) -> str:
|
|
349
|
+
if provider.host is not None:
|
|
350
|
+
return str(provider.host.recovery_dir_for(provider.host.current_env(), label))
|
|
351
|
+
stamp = datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
352
|
+
return str(Path.home() / ".agent-pack" / "recovery" / f"{stamp}-agenthelm-{label}")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _plan_entry(provider: Any, choices: dict[str, str]) -> dict:
|
|
356
|
+
name = choices.get("entry-name") or provider.launcher() or DEFAULT_LAUNCHER
|
|
357
|
+
if not LAUNCHER_RE.fullmatch(name):
|
|
358
|
+
raise EntryError(f"启动器名称不合法:{name}", "usage", ["只能用字母、数字、. _ -,例如 menu、go"])
|
|
359
|
+
title = f"生成根目录启动器 ./{name}" if provider.launcher() != name else f"完善入口 ./{name}"
|
|
360
|
+
recovery = choices.get("_recovery") or _recovery_dir(provider, "entry")
|
|
361
|
+
try:
|
|
362
|
+
result = _run_init(provider, name, choices, recovery, apply=False)
|
|
363
|
+
if (result.get("entry") or {}).get("collisions") and name not in choices:
|
|
364
|
+
choices = {**choices, name: "keep"} # 同名文件默认保留:预览就按保留来算写入范围
|
|
365
|
+
result = _run_init(provider, name, choices, recovery, apply=False)
|
|
366
|
+
except EntryError as exc:
|
|
367
|
+
return _step("add-entry", title, blocked=str(exc))
|
|
368
|
+
if result.get("code"):
|
|
369
|
+
return _step("add-entry", title, blocked=result.get("error", "agents-init 拒绝"))
|
|
370
|
+
entry = result.get("entry") or {}
|
|
371
|
+
custom = [{"path": name, "state": "已有同名文件(你的)", "default": "keep"}] \
|
|
372
|
+
if entry.get("action") in ("keep", "replace", "blocked") else []
|
|
373
|
+
readme = provider.root / "README.md"
|
|
374
|
+
has_section = readme.is_file() and "<!-- agenthelm:entry -->" in readme.read_text(encoding="utf-8", errors="replace")
|
|
375
|
+
if not has_section:
|
|
376
|
+
custom.append({"path": "README.md", "state": "可追加「怎样使用」一节(带标记)", "default": "keep"})
|
|
377
|
+
writes = [{"path": path, "scope": "项目"} for path in entry.get("writes", [])]
|
|
378
|
+
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=f"重新读取 Profile 的 entry 段与 ./{name}", payload={"name": name, "recovery": recovery})
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------- apply
|
|
384
|
+
|
|
385
|
+
def apply(provider: Any, planned: dict, choices: dict[str, str]) -> dict:
|
|
386
|
+
results = []
|
|
387
|
+
for step in planned["steps"]:
|
|
388
|
+
if step["blocked"]:
|
|
389
|
+
results.append({"id": step["id"], "status": "skipped", "detail": step["blocked"]})
|
|
390
|
+
continue
|
|
391
|
+
effective = {**{item["path"]: item["default"] for item in step["custom"]}, **choices,
|
|
392
|
+
"_recovery": step["payload"].get("recovery", "")}
|
|
393
|
+
if step["id"] == "add-entry":
|
|
394
|
+
effective["entry-name"] = step["payload"]["name"]
|
|
395
|
+
fresh = _plan_step(provider, step["id"], effective)
|
|
396
|
+
if [w["path"] for w in fresh["writes"]] != [w["path"] for w in step["writes"]]:
|
|
397
|
+
results.append({"id": step["id"], "status": "skipped",
|
|
398
|
+
"detail": "预览之后情况有变化(写入范围不同),没有执行:请重新预览"})
|
|
399
|
+
continue
|
|
400
|
+
try:
|
|
401
|
+
results.append(_apply_step(provider, fresh, effective))
|
|
402
|
+
except EntryError as exc:
|
|
403
|
+
results.append({"id": step["id"], "status": "failed", "detail": str(exc)})
|
|
404
|
+
break
|
|
405
|
+
provider._health = None
|
|
406
|
+
return {"results": results, "simulated": False}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _apply_step(provider: Any, step: dict, choices: dict[str, str]) -> dict:
|
|
410
|
+
fix_id = step["id"]
|
|
411
|
+
base = {"id": fix_id, "status": "applied", "written": [w["path"] for w in step["writes"]], "kept": [],
|
|
412
|
+
"accepted": [], "recovery": step["recovery"]}
|
|
413
|
+
if fix_id == "rebuild-index":
|
|
414
|
+
result = assets.rebuild(provider.assets_project(), force=True)
|
|
415
|
+
state = assets.status(provider.assets_project())
|
|
416
|
+
return {**base, "verify": f"索引{'最新' if state['status'] == 'fresh' else '状态:' + state['status']},"
|
|
417
|
+
f"{result['file_count']} 个文件"}
|
|
418
|
+
if fix_id == "add-entry":
|
|
419
|
+
name = step["payload"]["name"]
|
|
420
|
+
result = _run_init(provider, name, choices, step["payload"]["recovery"], apply=True)
|
|
421
|
+
if not result.get("applied"):
|
|
422
|
+
raise EntryError("没有写入:" + str(result.get("hint") or result.get("error") or result.get("blocked")), "failed")
|
|
423
|
+
launcher = provider.root / name
|
|
424
|
+
ready = launcher.is_file() and launcher.stat().st_mode & 0o111
|
|
425
|
+
return {**base, "verify": f"./{name} {'已就绪' if ready else '未就绪'};Profile 已声明 entry"}
|
|
426
|
+
host = provider.host
|
|
427
|
+
env = host.current_env()
|
|
428
|
+
if fix_id in ("install-runtime", "update-runtime"):
|
|
429
|
+
runtime = runtime_module(host)
|
|
430
|
+
if step["payload"].get("from_release"):
|
|
431
|
+
import tempfile
|
|
432
|
+
with tempfile.TemporaryDirectory(prefix="agenthelm-runtime-") as temporary:
|
|
433
|
+
latest, _ = host.load_latest(env, None, max_age=0)
|
|
434
|
+
root = host.release_source(latest, Path(temporary))
|
|
435
|
+
planned = runtime.plan(env, root, "GitHub Release(已核对摘要)")
|
|
436
|
+
result = runtime.apply(env, planned, root)
|
|
437
|
+
else:
|
|
438
|
+
planned = step["payload"]["plan"]
|
|
439
|
+
result = runtime.apply(env, planned, runtime.bundled_root())
|
|
440
|
+
return {**base, "verify": result["verify"],
|
|
441
|
+
"recovery": result["rollback"] or step["recovery"], "status": "applied" if result["ok"] else "failed"}
|
|
442
|
+
if fix_id == "update-plugins":
|
|
443
|
+
steps = host.update_steps(env, provider._report, None, None, env.home, [], [], None, "plugins")
|
|
444
|
+
outcome = host.execute_steps(steps, apply=True, recovery_root=None, child_env=env.child_environment())
|
|
445
|
+
failed = [item for item in outcome if item["status"] == "failed"]
|
|
446
|
+
after = host.run_check(env, provider.root, max_age=0)
|
|
447
|
+
behind = [item for item in after["plugins"] if item["status"] == "behind"]
|
|
448
|
+
return {**base, "status": "failed" if failed else "applied",
|
|
449
|
+
"detail": failed[0].get("output", "") if failed else "",
|
|
450
|
+
"verify": "重新检查:" + ("全部为最新" if not behind else f"仍有 {len(behind)} 个落后")}
|
|
451
|
+
# update-content
|
|
452
|
+
custom = {item["path"] for item in step["custom"]}
|
|
453
|
+
keep = [path for path in custom if choices.get(path, "keep") == "keep"]
|
|
454
|
+
accept = [path for path in custom if choices.get(path) == "accept"]
|
|
455
|
+
before = {path: _digest(provider.root / path) for path in keep}
|
|
456
|
+
recovery = Path(step["payload"]["recovery"])
|
|
457
|
+
steps = host.update_steps(env, provider._report, provider.root, None, env.home, accept, keep, recovery, "content")
|
|
458
|
+
outcome = host.execute_steps(steps, apply=True, recovery_root=None, child_env=env.child_environment())[0]
|
|
459
|
+
result = outcome.get("result") or {}
|
|
460
|
+
if outcome["status"] == "failed" or not result.get("applied"):
|
|
461
|
+
raise EntryError("模板更新没有执行:" + str(outcome.get("error") or result.get("hint") or result), "failed")
|
|
462
|
+
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
|
+
+ ";内容锁已更新到新版本"}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _digest(path: Path) -> str | None:
|
|
469
|
+
import hashlib
|
|
470
|
+
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|