sophhub 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/agents/ai-cs-admin/.config.json +6 -1
  2. package/agents/ai-cs-qa/.config.json +9 -1
  3. package/agents/ai-cs-qa/AGENTS.md +43 -15
  4. package/agents/ai-cs-qa/scripts/setup_links.sh +39 -0
  5. package/agents/vip-admin/.config.json +51 -0
  6. package/agents/vip-admin/AGENTS.md +331 -0
  7. package/agents/vip-admin/BOOTSTRAP.md +21 -0
  8. package/agents/vip-admin/HEARTBEAT.md +19 -0
  9. package/agents/vip-admin/IDENTITY.md +6 -0
  10. package/agents/vip-admin/MEMORY.md +29 -0
  11. package/agents/vip-admin/SOUL.md +25 -0
  12. package/agents/vip-admin/TOOLS.md +102 -0
  13. package/agents/vip-admin/USER.md +17 -0
  14. package/agents/vip-qa/.config.json +58 -0
  15. package/agents/vip-qa/AGENTS.md +312 -0
  16. package/agents/vip-qa/BOOTSTRAP.md +74 -0
  17. package/agents/vip-qa/HEARTBEAT.md +23 -0
  18. package/agents/vip-qa/IDENTITY.md +6 -0
  19. package/agents/vip-qa/MEMORY.md +23 -0
  20. package/agents/vip-qa/SOUL.md +34 -0
  21. package/agents/vip-qa/TOOLS.md +41 -0
  22. package/agents/vip-qa/USER.md +16 -0
  23. package/agents/vip-qa/scripts/setup_links.sh +39 -0
  24. package/package.json +1 -1
  25. package/skills/agent-install/skill.json +27 -0
  26. package/skills/agent-install/src/SKILL.md +238 -0
  27. package/skills/agent-install/src/pyproject.toml +6 -0
  28. package/skills/agent-install/src/scripts/backup_agent.py +120 -0
  29. package/skills/agent-install/src/scripts/check_installed.py +479 -0
  30. package/skills/agent-install/src/scripts/common.py +487 -0
  31. package/skills/agent-install/src/scripts/copy_agent_files.py +59 -0
  32. package/skills/agent-install/src/scripts/list_agents.py +285 -0
  33. package/skills/agent-install/src/scripts/resolve_install_params.py +90 -0
  34. package/skills/agent-install/src/scripts/update_agent_md.py +76 -0
  35. package/skills/agent-install/src/scripts/update_openclaw.py +183 -0
  36. package/skills/agent-install/src/scripts/verify_download.py +148 -0
  37. package/skills/bot-api-status/skill.json +36 -0
  38. package/skills/bot-api-status/src/SKILL.md +89 -0
  39. package/skills/bot-api-status/src/pyproject.toml +5 -0
  40. package/skills/bot-api-status/src/scripts/secret.py +481 -0
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import difflib
6
+ import json
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ from typing import Any
11
+
12
+
13
+ ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]*$", re.IGNORECASE)
14
+
15
+
16
+ def normalize_text(value: str) -> str:
17
+ return re.sub(r"[\s_\-]+", "", value.strip().lower())
18
+
19
+
20
+ def run_sophhub_agent_list() -> tuple[int, str, str]:
21
+ result = subprocess.run(
22
+ ["npx", "-y", "sophhub@latest", "agent", "list", "--json"],
23
+ capture_output=True,
24
+ text=True,
25
+ check=False,
26
+ )
27
+ return result.returncode, result.stdout, result.stderr
28
+
29
+
30
+ def extract_agents_from_json(data: Any) -> list[dict[str, str]]:
31
+ if isinstance(data, dict):
32
+ for key in ("agents", "items", "data", "list"):
33
+ value = data.get(key)
34
+ if isinstance(value, list):
35
+ return extract_agents_from_json(value)
36
+ if any(key in data for key in ("id", "agent_id", "name", "description", "desc")):
37
+ return [normalize_agent_record(data)]
38
+ return []
39
+
40
+ if isinstance(data, list):
41
+ result: list[dict[str, str]] = []
42
+ for item in data:
43
+ if isinstance(item, dict):
44
+ result.append(normalize_agent_record(item))
45
+ elif isinstance(item, str):
46
+ parsed = parse_text_line(item)
47
+ if parsed:
48
+ result.append(parsed)
49
+ return dedupe_agents(result)
50
+
51
+ return []
52
+
53
+
54
+ def normalize_agent_record(item: dict[str, Any]) -> dict[str, str]:
55
+ agent_id = first_non_empty_str(
56
+ item.get("id"),
57
+ item.get("agent_id"),
58
+ item.get("agentId"),
59
+ item.get("slug"),
60
+ item.get("key"),
61
+ item.get("code"),
62
+ )
63
+ name = first_non_empty_str(item.get("name"), item.get("title"), item.get("displayName"))
64
+ description = first_non_empty_str(
65
+ item.get("description"),
66
+ item.get("desc"),
67
+ item.get("summary"),
68
+ item.get("intro"),
69
+ )
70
+
71
+ if not agent_id and name and ID_PATTERN.match(name):
72
+ agent_id = name
73
+
74
+ return {
75
+ "agent_id": agent_id or "",
76
+ "name": name or "",
77
+ "description": description or "",
78
+ }
79
+
80
+
81
+ def first_non_empty_str(*values: Any) -> str | None:
82
+ for value in values:
83
+ if isinstance(value, str) and value.strip():
84
+ return value.strip()
85
+ return None
86
+
87
+
88
+ def parse_text_line(line: str) -> dict[str, str] | None:
89
+ raw = line.strip()
90
+ if not raw:
91
+ return None
92
+ if raw.startswith(("npm ", "Error:", "error:", "WARN", "warning:")):
93
+ return None
94
+ if set(raw) <= {"-", "=", "|", " "}:
95
+ return None
96
+
97
+ cleaned = re.sub(r"^\s*(?:[-*]|\d+\.)\s*", "", raw)
98
+ if "|" in cleaned:
99
+ parts = [part.strip() for part in cleaned.split("|") if part.strip()]
100
+ if len(parts) >= 2 and ID_PATTERN.match(parts[0]):
101
+ return {
102
+ "agent_id": parts[0],
103
+ "name": parts[1] if len(parts) >= 2 else "",
104
+ "description": " | ".join(parts[2:]) if len(parts) >= 3 else "",
105
+ }
106
+
107
+ for splitter in (r"\s+-\s+", r"\s{2,}", r"\s+:\s+"):
108
+ parts = re.split(splitter, cleaned, maxsplit=2)
109
+ if len(parts) >= 2 and ID_PATTERN.match(parts[0].strip()):
110
+ return {
111
+ "agent_id": parts[0].strip(),
112
+ "name": parts[1].strip() if len(parts) >= 2 else "",
113
+ "description": parts[2].strip() if len(parts) >= 3 else "",
114
+ }
115
+
116
+ first_word, _, rest = cleaned.partition(" ")
117
+ if ID_PATTERN.match(first_word):
118
+ return {
119
+ "agent_id": first_word.strip(),
120
+ "name": "",
121
+ "description": rest.strip(),
122
+ }
123
+
124
+ return None
125
+
126
+
127
+ def parse_agents_output(stdout: str) -> list[dict[str, str]]:
128
+ stripped = stdout.strip()
129
+ if not stripped:
130
+ return []
131
+
132
+ try:
133
+ data = json.loads(stripped)
134
+ except json.JSONDecodeError:
135
+ data = None
136
+
137
+ if data is not None:
138
+ return dedupe_agents(extract_agents_from_json(data))
139
+
140
+ result: list[dict[str, str]] = []
141
+ for line in stripped.splitlines():
142
+ parsed = parse_text_line(line)
143
+ if parsed:
144
+ result.append(parsed)
145
+ return dedupe_agents(result)
146
+
147
+
148
+ def dedupe_agents(agents: list[dict[str, str]]) -> list[dict[str, str]]:
149
+ deduped: list[dict[str, str]] = []
150
+ seen: set[tuple[str, str]] = set()
151
+ for agent in agents:
152
+ agent_id = agent.get("agent_id", "").strip()
153
+ name = agent.get("name", "").strip()
154
+ description = agent.get("description", "").strip()
155
+ key = (agent_id.lower(), name.lower())
156
+ if not agent_id or key in seen:
157
+ continue
158
+ deduped.append(
159
+ {
160
+ "agent_id": agent_id,
161
+ "name": name,
162
+ "description": description,
163
+ }
164
+ )
165
+ seen.add(key)
166
+ return deduped
167
+
168
+
169
+ def score_agent(agent: dict[str, str], query: str) -> tuple[int, str]:
170
+ normalized_query = normalize_text(query)
171
+ agent_id = agent.get("agent_id", "")
172
+ name = agent.get("name", "")
173
+ description = agent.get("description", "")
174
+
175
+ fields = [
176
+ ("agent_id", agent_id),
177
+ ("name", name),
178
+ ("description", description),
179
+ ]
180
+
181
+ for label, value in fields:
182
+ if normalize_text(value) == normalized_query and value:
183
+ return 100, f"exact_{label}"
184
+
185
+ if normalized_query in normalize_text(agent_id):
186
+ return 95, "contains_agent_id"
187
+ if normalized_query in normalize_text(name):
188
+ return 90, "contains_name"
189
+ if normalized_query in normalize_text(description):
190
+ return 80, "contains_description"
191
+
192
+ ratios = []
193
+ for label, value in fields[:2]:
194
+ if value:
195
+ ratio = difflib.SequenceMatcher(None, normalized_query, normalize_text(value)).ratio()
196
+ ratios.append((int(ratio * 100), f"similar_{label}"))
197
+
198
+ if ratios:
199
+ best_score, best_reason = max(ratios, key=lambda item: item[0])
200
+ if best_score >= 45:
201
+ return best_score, best_reason
202
+
203
+ return 0, "no_match"
204
+
205
+
206
+ def build_resolution(agents: list[dict[str, str]], query: str, limit: int) -> dict[str, Any]:
207
+ scored = []
208
+ for agent in agents:
209
+ score, reason = score_agent(agent, query)
210
+ if score > 0:
211
+ item = dict(agent)
212
+ item["score"] = score
213
+ item["match_reason"] = reason
214
+ scored.append(item)
215
+
216
+ scored.sort(key=lambda item: (-item["score"], item["agent_id"]))
217
+ candidates = scored[:limit]
218
+
219
+ exact_agent = next((item for item in candidates if item["score"] == 100 and item["match_reason"] == "exact_agent_id"), None)
220
+ resolved_agent_id = exact_agent["agent_id"] if exact_agent else None
221
+ if resolved_agent_id:
222
+ confirmation_message = f"已精确匹配到 Agent:{resolved_agent_id},可以继续安装。"
223
+ suggested_replies = [resolved_agent_id]
224
+ status = "EXACT_MATCH"
225
+ elif candidates:
226
+ lines = ["我找到了以下候选 Agent,请确认要安装哪一个:"]
227
+ for index, candidate in enumerate(candidates, start=1):
228
+ name = f"({candidate['name']})" if candidate.get("name") else ""
229
+ desc = f" - {candidate['description']}" if candidate.get("description") else ""
230
+ lines.append(f"{index}. {candidate['agent_id']}{name}{desc}")
231
+ confirmation_message = "\n".join(lines)
232
+ suggested_replies = [candidate["agent_id"] for candidate in candidates]
233
+ status = "NEEDS_CONFIRMATION"
234
+ else:
235
+ confirmation_message = "没有找到匹配的 Agent,请换一个更准确的名称、ID 或功能描述。"
236
+ suggested_replies = []
237
+ status = "NO_MATCH"
238
+
239
+ return {
240
+ "query": query,
241
+ "status": status,
242
+ "resolved_agent_id": resolved_agent_id,
243
+ "needs_confirmation": resolved_agent_id is None,
244
+ "candidates": candidates,
245
+ "confirmation_message": confirmation_message,
246
+ "suggested_replies": suggested_replies,
247
+ }
248
+
249
+
250
+ def main() -> int:
251
+ parser = argparse.ArgumentParser(description="列出可用 Agent,并支持模糊匹配")
252
+ parser.add_argument("--query", help="用户输入的 Agent 名称、别名或描述关键词")
253
+ parser.add_argument("--limit", type=int, default=5, help="返回候选数量")
254
+ parser.add_argument("--input-file", help="从文件读取 sophhub agent list --json 输出,便于调试")
255
+ args = parser.parse_args()
256
+
257
+ if args.input_file:
258
+ stdout = open(args.input_file, "r", encoding="utf-8").read()
259
+ stderr = ""
260
+ returncode = 0
261
+ else:
262
+ returncode, stdout, stderr = run_sophhub_agent_list()
263
+
264
+ agents = parse_agents_output(stdout)
265
+
266
+ payload: dict[str, Any] = {
267
+ "ok": returncode == 0,
268
+ "total_agents": len(agents),
269
+ "agents": agents,
270
+ "raw_stdout": stdout,
271
+ }
272
+
273
+ if stderr:
274
+ payload["stderr"] = stderr
275
+
276
+ if args.query:
277
+ payload["resolution"] = build_resolution(agents, args.query, max(1, args.limit))
278
+
279
+ json.dump(payload, sys.stdout, indent=2, ensure_ascii=False)
280
+ sys.stdout.write("\n")
281
+ return 0 if args.input_file or returncode == 0 else returncode
282
+
283
+
284
+ if __name__ == "__main__":
285
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from common import (
10
+ build_agent_entry,
11
+ default_openclaw_config_path,
12
+ load_agent_definition,
13
+ load_openclaw_config,
14
+ merge_placeholder_catalog_with_detected,
15
+ normalize_placeholder_catalog,
16
+ read_agent_placeholders,
17
+ resolve_agent_directory,
18
+ resolve_existing_agent_entry,
19
+ )
20
+
21
+
22
+ def resolve_install_params(agent_id: str, source_path: Path, config_path: Path) -> dict[str, object]:
23
+ agent_def = load_agent_definition(agent_id, source_path)
24
+ agent_dir = resolve_agent_directory(agent_id, source_path)
25
+ entry = build_agent_entry(agent_def)
26
+ identity = entry.get("identity", {})
27
+ placeholders = read_agent_placeholders(agent_id, source_path)
28
+ placeholder_catalog = normalize_placeholder_catalog(agent_def.get("placeholder_catalog"))
29
+ placeholders_detail = merge_placeholder_catalog_with_detected(placeholder_catalog, placeholders)
30
+ current_openclaw_id = None
31
+ installed = False
32
+
33
+ try:
34
+ openclaw = load_openclaw_config(config_path)
35
+ except FileNotFoundError:
36
+ openclaw = None
37
+
38
+ if openclaw is not None:
39
+ existing_entry = resolve_existing_agent_entry(config_path, openclaw, agent_id, entry["workspace"])
40
+ if existing_entry is not None:
41
+ current_openclaw_id = existing_entry.get("id")
42
+ installed = True
43
+
44
+ return {
45
+ "ok": True,
46
+ "agent_id": agent_id,
47
+ "source_path": str(source_path),
48
+ "config_path": str(config_path),
49
+ "agent_directory": str(agent_dir),
50
+ "openclaw_id": current_openclaw_id or agent_id,
51
+ "current_openclaw_id": current_openclaw_id,
52
+ "installed": installed,
53
+ "workspace": entry["workspace"],
54
+ "agent_name": entry.get("name") or (identity.get("name") if isinstance(identity, dict) else None),
55
+ "identity_name": identity.get("name") if isinstance(identity, dict) else None,
56
+ "placeholders": placeholders,
57
+ "placeholder_catalog": placeholder_catalog,
58
+ "placeholders_detail": placeholders_detail,
59
+ "needs_name_confirmation": bool(placeholders),
60
+ "message": "已解析 Agent 的默认安装参数。",
61
+ }
62
+
63
+
64
+ def main() -> int:
65
+ parser = argparse.ArgumentParser(description="解析 Agent 安装参数")
66
+ parser.add_argument("--agent-id", required=True, help="Agent ID")
67
+ parser.add_argument(
68
+ "--path",
69
+ required=True,
70
+ help="Agent 下载目录(sophhub agent download 的 --path)",
71
+ )
72
+ parser.add_argument(
73
+ "--config",
74
+ default=str(default_openclaw_config_path()),
75
+ help="openclaw.json 路径",
76
+ )
77
+ args = parser.parse_args()
78
+
79
+ result = resolve_install_params(
80
+ args.agent_id,
81
+ Path(args.path).expanduser().resolve(),
82
+ Path(args.config).expanduser().resolve(),
83
+ )
84
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
85
+ sys.stdout.write("\n")
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ raise SystemExit(main())
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from common import markdown_files_in_dir, read_agent_placeholders, resolve_agent_directory
10
+
11
+
12
+ def parse_replacements(values: list[str]) -> dict[str, str]:
13
+ replacements: dict[str, str] = {}
14
+ for value in values:
15
+ if "=" not in value:
16
+ raise ValueError(f"replace 参数格式错误: {value}")
17
+ key, raw_replacement = value.split("=", 1)
18
+ placeholder = key.strip()
19
+ replacement = raw_replacement.strip()
20
+ if not placeholder or not replacement:
21
+ raise ValueError(f"replace 参数格式错误: {value}")
22
+ replacements[placeholder] = replacement
23
+ return replacements
24
+
25
+
26
+ def update_agent_md(agent_id: str, source_path: Path, replacements: dict[str, str]) -> dict[str, object]:
27
+ agent_dir = resolve_agent_directory(agent_id, source_path)
28
+ md_files = markdown_files_in_dir(agent_dir)
29
+ placeholders = read_agent_placeholders(agent_id, source_path)
30
+ updated_files: list[str] = []
31
+
32
+ for md_path in md_files:
33
+ original = md_path.read_text(encoding="utf-8")
34
+ updated = original
35
+ for placeholder, replacement in replacements.items():
36
+ updated = updated.replace(placeholder, replacement)
37
+ if updated != original:
38
+ md_path.write_text(updated, encoding="utf-8")
39
+ updated_files.append(md_path.name)
40
+
41
+ return {
42
+ "ok": True,
43
+ "agent_id": agent_id,
44
+ "agent_directory": str(agent_dir),
45
+ "placeholders": placeholders,
46
+ "replacements": replacements,
47
+ "updated_files": updated_files,
48
+ "message": "Agent 模板中的占位(placeholder_catalog / token)替换完成。",
49
+ }
50
+
51
+
52
+ def main() -> int:
53
+ parser = argparse.ArgumentParser(description="按 token 替换 Agent 模板 .md(见 placeholder_catalog)")
54
+ parser.add_argument("--agent-id", required=True, help="Agent ID")
55
+ parser.add_argument("--path", required=True, help="Agent 下载目录(sophhub agent download 的 --path)")
56
+ parser.add_argument(
57
+ "--replace",
58
+ action="append",
59
+ default=[],
60
+ help='token 替换,格式如 --replace "{{客服助手}}=客服助手A"(token 与配置侧 label 见 placeholder_catalog)',
61
+ )
62
+ args = parser.parse_args()
63
+
64
+ replacements = parse_replacements(args.replace)
65
+ result = update_agent_md(
66
+ args.agent_id,
67
+ Path(args.path).expanduser().resolve(),
68
+ replacements,
69
+ )
70
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
71
+ sys.stdout.write("\n")
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from common import (
10
+ build_agent_entry,
11
+ build_bot_api_account,
12
+ deep_merge_dict,
13
+ default_openclaw_config_path,
14
+ dump_json,
15
+ find_agent_entry,
16
+ find_bot_api_accounts_by_agent_id,
17
+ find_agent_by_workspace,
18
+ load_agent_definition,
19
+ load_openclaw_config,
20
+ resolve_existing_agent_entry,
21
+ write_install_state,
22
+ )
23
+
24
+
25
+ def normalized_override(value: str | None) -> str | None:
26
+ if isinstance(value, str):
27
+ stripped = value.strip()
28
+ if stripped:
29
+ return stripped
30
+ return None
31
+
32
+
33
+ def upsert_agent(config: dict, entry: dict) -> str:
34
+ config.setdefault("agents", {})
35
+ agents = config["agents"].setdefault("list", [])
36
+
37
+ for index, agent in enumerate(agents):
38
+ if isinstance(agent, dict) and agent.get("id") == entry["id"]:
39
+ agents[index] = deep_merge_dict(agent, entry)
40
+ return "updated"
41
+
42
+ agents.append(entry)
43
+ return "created"
44
+
45
+
46
+ def ensure_bot_api(
47
+ config: dict,
48
+ agent_def: dict,
49
+ *,
50
+ target_agent_id: str,
51
+ name_override: str | None = None,
52
+ ) -> dict[str, object] | None:
53
+ existing_accounts = find_bot_api_accounts_by_agent_id(config, target_agent_id)
54
+ existing_secret = None
55
+ if existing_accounts:
56
+ first_account = next(iter(existing_accounts.values()))
57
+ existing_secret = first_account.get("apiSecret")
58
+
59
+ built = build_bot_api_account(
60
+ agent_def,
61
+ agent_id_override=target_agent_id,
62
+ existing_secret=existing_secret,
63
+ name_override=name_override,
64
+ )
65
+ if built is None:
66
+ return None
67
+
68
+ account_id, account = built
69
+ plugins = config.setdefault("plugins", {})
70
+ allow = plugins.setdefault("allow", [])
71
+ if "bot-api" not in allow:
72
+ allow.append("bot-api")
73
+ entries = plugins.setdefault("entries", {})
74
+ entries.setdefault("bot-api", {})["enabled"] = True
75
+
76
+ channels = config.setdefault("channels", {})
77
+ bot_api = channels.setdefault("bot-api", {})
78
+ accounts = bot_api.setdefault("accounts", {})
79
+ action = "updated" if account_id in accounts else "created"
80
+ accounts[account_id] = account
81
+
82
+ return {
83
+ "account_id": account_id,
84
+ "action": action,
85
+ "apiSecret": account["apiSecret"],
86
+ }
87
+
88
+
89
+ def update_openclaw(
90
+ agent_id: str,
91
+ config_path: Path,
92
+ *,
93
+ source_path: Path | None = None,
94
+ openclaw_id_override: str | None = None,
95
+ workspace_override: str | None = None,
96
+ name_override: str | None = None,
97
+ ) -> dict[str, object]:
98
+ resolved_openclaw_id = normalized_override(openclaw_id_override)
99
+ resolved_workspace = normalized_override(workspace_override)
100
+ resolved_name = normalized_override(name_override)
101
+ agent_def = load_agent_definition(agent_id, source_path)
102
+ config = load_openclaw_config(config_path)
103
+ desired_workspace = resolved_workspace or agent_def.get("install", {}).get("workspace") or agent_def.get("workspace")
104
+ previous = resolve_existing_agent_entry(config_path, config, agent_id, desired_workspace)
105
+ if previous is None and resolved_openclaw_id:
106
+ existing_by_target_id = find_agent_entry(config, resolved_openclaw_id)
107
+ if existing_by_target_id is not None:
108
+ raise ValueError(f"目标 openclaw id 已存在且不属于当前 Agent: {resolved_openclaw_id}")
109
+ if previous is not None:
110
+ target_agent_id = previous.get("id") if previous.get("id") else agent_id
111
+ else:
112
+ target_agent_id = resolved_openclaw_id or agent_id
113
+
114
+ entry = build_agent_entry(
115
+ agent_def,
116
+ target_id_override=target_agent_id,
117
+ workspace_override=resolved_workspace,
118
+ name_override=resolved_name,
119
+ )
120
+ agent_action = upsert_agent(config, entry)
121
+ bot_api_result = ensure_bot_api(
122
+ config,
123
+ agent_def,
124
+ target_agent_id=target_agent_id,
125
+ name_override=resolved_name,
126
+ )
127
+
128
+ dump_json(config_path, config)
129
+ state_path = write_install_state(
130
+ config_path,
131
+ target_agent_id,
132
+ {
133
+ "agent_id": agent_id,
134
+ "openclaw_id": target_agent_id,
135
+ "version": agent_def["version"],
136
+ "workspace": entry["workspace"],
137
+ "agentDir": entry.get("agentDir"),
138
+ },
139
+ )
140
+
141
+ return {
142
+ "agent_id": agent_id,
143
+ "openclaw_id": target_agent_id,
144
+ "agent_action": agent_action,
145
+ "bot_api": bot_api_result,
146
+ "workspace": entry["workspace"],
147
+ "name": entry.get("name") or entry.get("identity", {}).get("name"),
148
+ "agent_dir": entry.get("agentDir"),
149
+ "previous_workspace": previous.get("workspace") if previous else None,
150
+ "version": agent_def["version"],
151
+ "install_state": str(state_path),
152
+ }
153
+
154
+
155
+ def main() -> int:
156
+ parser = argparse.ArgumentParser(description="根据 .config.json 幂等更新 openclaw.json")
157
+ parser.add_argument("--agent-id", required=True, help="Agent ID")
158
+ parser.add_argument("--source-path", help="Agent 下载目录(sophhub agent download 的 --path)")
159
+ parser.add_argument("--openclaw-id", help="目标 openclaw id(resolve_install_params 与流程确认结果)")
160
+ parser.add_argument("--workspace", help="目标 workspace(流程确认结果)")
161
+ parser.add_argument("--name", help="显示名称 agent_name(流程确认结果)")
162
+ parser.add_argument(
163
+ "--config",
164
+ default=str(default_openclaw_config_path()),
165
+ help="openclaw.json 路径",
166
+ )
167
+ args = parser.parse_args()
168
+
169
+ result = update_openclaw(
170
+ args.agent_id,
171
+ Path(args.config).expanduser().resolve(),
172
+ source_path=Path(args.source_path).expanduser().resolve() if args.source_path else None,
173
+ openclaw_id_override=args.openclaw_id,
174
+ workspace_override=args.workspace,
175
+ name_override=args.name,
176
+ )
177
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
178
+ sys.stdout.write("\n")
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ raise SystemExit(main())