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,481 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import secrets
9
+ import sys
10
+ from typing import Any
11
+
12
+
13
+ DEFAULT_CONFIG_PATH = Path(os.path.expanduser(os.environ.get("OPENCLAW_CONFIG_PATH", "~/.openclaw/openclaw.json"))).resolve()
14
+ DEFAULT_BASE_URL_PATH = Path("/home/node/.openclaw/.base.json")
15
+ BASEURL_FETCH_FAILED_MESSAGE = "获取BASEURL失败,刷新页面或者重新登录后重试。"
16
+
17
+
18
+ def load_json(path: Path) -> dict[str, Any]:
19
+ if not path.exists():
20
+ raise FileNotFoundError(f"Config file not found: {path}")
21
+ try:
22
+ with path.open("r", encoding="utf-8") as f:
23
+ return json.load(f)
24
+ except json.JSONDecodeError as exc:
25
+ raise ValueError(f"Invalid JSON in config file: {path}") from exc
26
+
27
+
28
+ def dump_json(path: Path, data: dict[str, Any]) -> None:
29
+ with path.open("w", encoding="utf-8") as f:
30
+ json.dump(data, f, ensure_ascii=False, indent=2)
31
+ f.write("\n")
32
+
33
+
34
+ def normalize_base_url(base_url: str | None) -> str | None:
35
+ if not isinstance(base_url, str):
36
+ return None
37
+ normalized = base_url.strip().rstrip("/")
38
+ return normalized or None
39
+
40
+
41
+ def read_base_url(base_url_path: Path = DEFAULT_BASE_URL_PATH) -> str | None:
42
+ try:
43
+ data = load_json(base_url_path)
44
+ except (FileNotFoundError, OSError, ValueError):
45
+ return None
46
+ return normalize_base_url(data.get("base_url"))
47
+
48
+
49
+ def require_base_url(base_url_path: Path = DEFAULT_BASE_URL_PATH) -> str:
50
+ base_url = read_base_url(base_url_path)
51
+ if not base_url:
52
+ raise ValueError(BASEURL_FETCH_FAILED_MESSAGE)
53
+ return base_url
54
+
55
+
56
+ def build_bot_api_url(account_id: str | None, path_suffix: str, base_url: str | None = None) -> str | None:
57
+ if not isinstance(account_id, str) or not account_id.strip():
58
+ return None
59
+ segment = account_id.strip()
60
+ path = f"/bot-api/v2/{segment}/{path_suffix}"
61
+ normalized = normalize_base_url(base_url)
62
+ if normalized:
63
+ return f"{normalized}{path}"
64
+ return None
65
+
66
+
67
+ def openclaw_root(config_path: Path) -> Path:
68
+ return config_path.expanduser().resolve().parent
69
+
70
+
71
+ def find_nearest_agent_config(start_dir: Path | None = None) -> Path | None:
72
+ current = (start_dir or Path.cwd()).resolve()
73
+ for candidate in [current, *current.parents]:
74
+ config_path = candidate / ".config.json"
75
+ if config_path.is_file():
76
+ return config_path
77
+ return None
78
+
79
+
80
+ def read_agent_definition(config_path: Path | None) -> dict[str, Any] | None:
81
+ if config_path is None or not config_path.is_file():
82
+ return None
83
+ data = load_json(config_path)
84
+ agent_id = data.get("agent_id")
85
+ if not isinstance(agent_id, str) or not agent_id.strip():
86
+ return None
87
+ return data
88
+
89
+
90
+ def read_install_state_source_agent_id(config_path: Path, openclaw_id: str) -> str | None:
91
+ state_path = openclaw_root(config_path) / "agents" / openclaw_id / "install-state.json"
92
+ if not state_path.is_file():
93
+ return None
94
+ try:
95
+ data = load_json(state_path)
96
+ except (OSError, ValueError):
97
+ return None
98
+ source_agent_id = data.get("agent_id")
99
+ return source_agent_id if isinstance(source_agent_id, str) else None
100
+
101
+
102
+ def read_agent_dir_source_agent_id(agent_entry: dict[str, Any]) -> str | None:
103
+ agent_dir = agent_entry.get("agentDir")
104
+ if not isinstance(agent_dir, str) or not agent_dir.strip():
105
+ return None
106
+ config_path = Path(agent_dir).expanduser().resolve() / ".config.json"
107
+ if not config_path.is_file():
108
+ return None
109
+ try:
110
+ data = load_json(config_path)
111
+ except (OSError, ValueError):
112
+ return None
113
+ source_agent_id = data.get("agent_id")
114
+ return source_agent_id if isinstance(source_agent_id, str) else None
115
+
116
+
117
+ def entry_matches_source_agent(config_path: Path, agent_entry: dict[str, Any], source_agent_id: str) -> bool:
118
+ entry_id = agent_entry.get("id")
119
+ if entry_id == source_agent_id:
120
+ return True
121
+ if isinstance(entry_id, str) and read_install_state_source_agent_id(config_path, entry_id) == source_agent_id:
122
+ return True
123
+ return read_agent_dir_source_agent_id(agent_entry) == source_agent_id
124
+
125
+
126
+ def is_path_within(path_value: Path, parent_value: Path) -> bool:
127
+ try:
128
+ path_value.relative_to(parent_value)
129
+ return True
130
+ except ValueError:
131
+ return False
132
+
133
+
134
+ def find_agent_entry_by_workspace(config: dict[str, Any], cwd: Path) -> dict[str, Any] | None:
135
+ agents = config.get("agents", {}).get("list", [])
136
+ best_match: dict[str, Any] | None = None
137
+ best_len = -1
138
+ for agent_entry in agents:
139
+ if not isinstance(agent_entry, dict):
140
+ continue
141
+ workspace = agent_entry.get("workspace")
142
+ if not isinstance(workspace, str) or not workspace.strip():
143
+ continue
144
+ workspace_path = Path(workspace).expanduser().resolve()
145
+ if not is_path_within(cwd, workspace_path):
146
+ continue
147
+ workspace_len = len(str(workspace_path))
148
+ if workspace_len > best_len:
149
+ best_match = agent_entry
150
+ best_len = workspace_len
151
+ return best_match
152
+
153
+
154
+ def resolve_current_agent(config_path: Path, cwd: Path | None = None) -> dict[str, Any]:
155
+ current_dir = (cwd or Path.cwd()).resolve()
156
+ local_config_path = find_nearest_agent_config(current_dir)
157
+ agent_def = read_agent_definition(local_config_path)
158
+ source_agent_id = agent_def.get("agent_id") if agent_def else None
159
+ config = load_json(config_path)
160
+ agents = config.get("agents", {}).get("list", [])
161
+
162
+ entry: dict[str, Any] | None = None
163
+ if isinstance(source_agent_id, str) and source_agent_id:
164
+ for agent_entry in agents:
165
+ if isinstance(agent_entry, dict) and entry_matches_source_agent(config_path, agent_entry, source_agent_id):
166
+ entry = agent_entry
167
+ break
168
+ if entry is None:
169
+ entry = find_agent_entry_by_workspace(config, current_dir)
170
+
171
+ if entry is None:
172
+ raise ValueError("无法识别当前 Agent,请在已安装 Agent 的 workspace 中执行。")
173
+
174
+ openclaw_id = entry.get("id")
175
+ if not isinstance(openclaw_id, str) or not openclaw_id.strip():
176
+ raise ValueError("当前 Agent 在 openclaw.json 中缺少 id。")
177
+
178
+ workspace = entry.get("workspace")
179
+ if not isinstance(workspace, str) or not workspace.strip():
180
+ raise ValueError("当前 Agent 在 openclaw.json 中缺少 workspace。")
181
+
182
+ account_ids = find_account_ids_by_agent_id(config, openclaw_id)
183
+ display_name = (
184
+ entry.get("name")
185
+ or entry.get("identity", {}).get("name")
186
+ or (agent_def or {}).get("description")
187
+ or openclaw_id
188
+ )
189
+ if not isinstance(display_name, str) or not display_name.strip():
190
+ display_name = openclaw_id
191
+
192
+ return {
193
+ "config": config,
194
+ "config_path": config_path,
195
+ "local_config_path": local_config_path,
196
+ "agent_definition": agent_def,
197
+ "source_agent_id": source_agent_id or openclaw_id,
198
+ "openclaw_id": openclaw_id,
199
+ "workspace": workspace,
200
+ "display_name": display_name.strip(),
201
+ "account_ids": account_ids,
202
+ }
203
+
204
+
205
+ def find_account_ids_by_agent_id(config: dict[str, Any], agent_id: str) -> list[str]:
206
+ accounts = config.get("channels", {}).get("bot-api", {}).get("accounts", {})
207
+ result: list[str] = []
208
+ for account_id, account in accounts.items():
209
+ if isinstance(account, dict) and account.get("agentId") == agent_id:
210
+ result.append(account_id)
211
+ return sorted(result)
212
+
213
+
214
+ def get_primary_account(config: dict[str, Any], account_ids: list[str]) -> tuple[str | None, dict[str, Any] | None]:
215
+ accounts = config.get("channels", {}).get("bot-api", {}).get("accounts", {})
216
+ for account_id in account_ids:
217
+ account = accounts.get(account_id)
218
+ if isinstance(account, dict):
219
+ return account_id, account
220
+ return None, None
221
+
222
+
223
+ def get_plugin_enabled(config: dict[str, Any]) -> bool:
224
+ allow = config.get("plugins", {}).get("allow", [])
225
+ entries = config.get("plugins", {}).get("entries", {})
226
+ allow_enabled = isinstance(allow, list) and "bot-api" in allow
227
+ entry_enabled = isinstance(entries, dict) and isinstance(entries.get("bot-api"), dict) and entries["bot-api"].get("enabled") is True
228
+ return allow_enabled and entry_enabled
229
+
230
+
231
+ def set_local_bot_api_enabled(local_config_path: Path | None, enabled: bool) -> bool:
232
+ if local_config_path is None or not local_config_path.is_file():
233
+ return False
234
+ data = load_json(local_config_path)
235
+ data["bot_api_enabled"] = enabled
236
+ dump_json(local_config_path, data)
237
+ return True
238
+
239
+
240
+ def build_status(context: dict[str, Any]) -> dict[str, Any]:
241
+ config = context["config"]
242
+ account_id, account = get_primary_account(config, context["account_ids"])
243
+ local_agent_def = context.get("agent_definition") or {}
244
+ base_url = context.get("base_url")
245
+ account_enabled = bool(account.get("enabled")) if isinstance(account, dict) else False
246
+ plugin_enabled = get_plugin_enabled(config)
247
+ bot_api_url = build_bot_api_url(account_id, "chat", base_url)
248
+ bot_api_stream_url = build_bot_api_url(account_id, "chat-stream", base_url)
249
+ return {
250
+ "source_agent_id": context["source_agent_id"],
251
+ "openclaw_id": context["openclaw_id"],
252
+ "workspace": context["workspace"],
253
+ "display_name": context["display_name"],
254
+ "config_bot_api_enabled": bool(local_agent_def.get("bot_api_enabled")),
255
+ "plugin_enabled": plugin_enabled,
256
+ "account_exists": bool(account),
257
+ "account_id": account_id,
258
+ "bot_api_url": bot_api_url,
259
+ "bot_api_stream_url": bot_api_stream_url,
260
+ "enabled": bool(account) and account_enabled and plugin_enabled,
261
+ "account_enabled": account_enabled,
262
+ "api_secret": account.get("apiSecret") if isinstance(account, dict) else None,
263
+ "security_notice": "🔒 请勿在公开群聊、工单或截图中泄漏 api_secret。",
264
+ }
265
+
266
+
267
+ def ensure_bot_api(context: dict[str, Any]) -> dict[str, Any]:
268
+ config = context["config"]
269
+ config.setdefault("plugins", {})
270
+ allow = config["plugins"].setdefault("allow", [])
271
+ if "bot-api" not in allow:
272
+ allow.append("bot-api")
273
+ entries = config["plugins"].setdefault("entries", {})
274
+ entries.setdefault("bot-api", {})["enabled"] = True
275
+
276
+ channels = config.setdefault("channels", {})
277
+ bot_api = channels.setdefault("bot-api", {})
278
+ accounts = bot_api.setdefault("accounts", {})
279
+
280
+ account_id, existing_account = get_primary_account(config, context["account_ids"])
281
+ if account_id is None:
282
+ account_id = context["source_agent_id"]
283
+
284
+ api_secret = None
285
+ if isinstance(existing_account, dict):
286
+ api_secret = existing_account.get("apiSecret")
287
+ if not isinstance(api_secret, str) or not api_secret:
288
+ api_secret = secrets.token_hex(32)
289
+
290
+ accounts[account_id] = {
291
+ "agentId": context["openclaw_id"],
292
+ "name": context["display_name"],
293
+ "apiSecret": api_secret,
294
+ "enabled": True,
295
+ }
296
+
297
+ dump_json(context["config_path"], config)
298
+ set_local_bot_api_enabled(context["local_config_path"], True)
299
+ if isinstance(context.get("agent_definition"), dict):
300
+ context["agent_definition"]["bot_api_enabled"] = True
301
+ context["account_ids"] = find_account_ids_by_agent_id(config, context["openclaw_id"])
302
+ return {
303
+ "action": "created" if existing_account is None else "updated",
304
+ **build_status(context),
305
+ }
306
+
307
+
308
+ def delete_bot_api(context: dict[str, Any]) -> dict[str, Any]:
309
+ config = context["config"]
310
+ accounts = config.get("channels", {}).get("bot-api", {}).get("accounts", {})
311
+ removed_account_ids: list[str] = []
312
+ for account_id in list(context["account_ids"]):
313
+ if account_id in accounts:
314
+ removed_account_ids.append(account_id)
315
+ del accounts[account_id]
316
+
317
+ remaining_accounts = config.get("channels", {}).get("bot-api", {}).get("accounts", {})
318
+ if not remaining_accounts:
319
+ config.setdefault("plugins", {})
320
+ allow = config.get("plugins", {}).get("allow", [])
321
+ if isinstance(allow, list):
322
+ config["plugins"]["allow"] = [item for item in allow if item != "bot-api"]
323
+ bot_api_entry = config.get("plugins", {}).get("entries", {}).get("bot-api")
324
+ if isinstance(bot_api_entry, dict):
325
+ bot_api_entry["enabled"] = False
326
+
327
+ dump_json(context["config_path"], config)
328
+ set_local_bot_api_enabled(context["local_config_path"], False)
329
+ if isinstance(context.get("agent_definition"), dict):
330
+ context["agent_definition"]["bot_api_enabled"] = False
331
+ context["account_ids"] = []
332
+ return {
333
+ "action": "deleted",
334
+ "removed_account_ids": removed_account_ids,
335
+ **build_status(context),
336
+ }
337
+
338
+
339
+ def reset_bot_api_secret(context: dict[str, Any]) -> dict[str, Any]:
340
+ config = context["config"]
341
+ account_id, account = get_primary_account(config, context["account_ids"])
342
+ if account_id is None or not isinstance(account, dict):
343
+ raise ValueError("当前 Agent 尚未创建 bot-api,无法重置密钥。")
344
+
345
+ new_secret = secrets.token_hex(32)
346
+ account["apiSecret"] = new_secret
347
+ config["channels"]["bot-api"]["accounts"][account_id] = account
348
+ dump_json(context["config_path"], config)
349
+ return {
350
+ "action": "reset",
351
+ "new_secret": new_secret,
352
+ **build_status(context),
353
+ }
354
+
355
+
356
+ def _fmt_secret(value: object) -> str:
357
+ if isinstance(value, str) and value.strip():
358
+ return value.strip()
359
+ return "📭 暂无"
360
+
361
+
362
+ def _fmt_url(value: object) -> str:
363
+ if isinstance(value, str) and value.strip():
364
+ return value.strip()
365
+ return "📭 暂无"
366
+
367
+
368
+ def _print_link_block(url: str, stream_url: str, baseurl_note: str) -> None:
369
+ if url != "📭 暂无":
370
+ print(f"🔗 非流式链接:{url}")
371
+ if stream_url != "📭 暂无":
372
+ print(f"🔗 流式链接:{stream_url}")
373
+
374
+
375
+ def print_human_response(command: str, payload: dict[str, Any]) -> int:
376
+ """非 --json 时输出,风格与 SKILL 模板一致。"""
377
+ action = payload.get("action")
378
+ enabled = payload.get("enabled")
379
+ url = _fmt_url(payload.get("bot_api_url"))
380
+ stream_url = _fmt_url(payload.get("bot_api_stream_url"))
381
+ secret = _fmt_secret(payload.get("api_secret") or payload.get("new_secret"))
382
+
383
+ if command in {"get", "status"}:
384
+ state = "🟢 开启" if enabled else "⚪ 关闭"
385
+ print(f"📋 🤖 bot-api 状态:{state}")
386
+ print()
387
+ _print_link_block(url, stream_url, "")
388
+ print(f"🔑 密钥:{secret}")
389
+ return 0
390
+
391
+ if command == "create":
392
+ title = "已更新" if action == "updated" else "已创建"
393
+ prefix = "📝" if action == "updated" else "✨"
394
+ print(f"{prefix} 🤖 bot-api {title}")
395
+ print()
396
+ _print_link_block(url, stream_url, "")
397
+ print(f"🔑 密钥:{secret}")
398
+ print()
399
+ print("🔒 请妥善保管,勿对外转发或截图。")
400
+ return 0
401
+
402
+ if command == "delete":
403
+ print("🛑 🤖 bot-api 已关闭")
404
+ print()
405
+ print("📴 当前 Agent 不再通过 bot-api 对外提供服务。")
406
+ return 0
407
+
408
+ if command == "reset":
409
+ if not isinstance(payload.get("new_secret"), str) or not payload["new_secret"].strip():
410
+ print("⚠️ 当前 Agent 尚未创建 bot-api,无法重置密钥。", file=sys.stderr)
411
+ return 1
412
+ print("🔄 🤖 bot-api 密钥已更新")
413
+ print()
414
+ print(f"🔑 新密钥:{secret}")
415
+ print()
416
+ _print_link_block(url, stream_url, "")
417
+ print()
418
+ print("🔒 请妥善保管,勿对外转发或截图。")
419
+ return 0
420
+
421
+ print(f"❓ 未知命令:{command}", file=sys.stderr)
422
+ return 1
423
+
424
+
425
+ def create_parser() -> argparse.ArgumentParser:
426
+ parser = argparse.ArgumentParser(description="管理当前 Agent 的 bot-api 与 API Secret")
427
+ parser.add_argument(
428
+ "--config",
429
+ default=str(DEFAULT_CONFIG_PATH),
430
+ help="openclaw.json 路径",
431
+ )
432
+ parser.add_argument(
433
+ "--json",
434
+ action="store_true",
435
+ help="输出 JSON",
436
+ )
437
+ parser.add_argument(
438
+ "command",
439
+ choices=["status", "create", "delete", "reset", "get"],
440
+ help="执行的动作:status/create/delete/reset,get 为 status 的兼容别名",
441
+ )
442
+ return parser
443
+
444
+
445
+ def main(argv: list[str] | None = None) -> int:
446
+ parser = create_parser()
447
+ args = parser.parse_args(argv)
448
+ config_path = Path(args.config).expanduser().resolve()
449
+
450
+ try:
451
+ context = resolve_current_agent(config_path)
452
+ if args.command in {"status", "get", "create", "reset"}:
453
+ context["base_url"] = require_base_url()
454
+ if args.command in {"status", "get"}:
455
+ payload = {"action": "status", **build_status(context)}
456
+ elif args.command == "create":
457
+ payload = ensure_bot_api(context)
458
+ elif args.command == "delete":
459
+ payload = delete_bot_api(context)
460
+ elif args.command == "reset":
461
+ payload = reset_bot_api_secret(context)
462
+ else:
463
+ raise ValueError(f"Unsupported command: {args.command}")
464
+ except Exception as exc: # pragma: no cover - CLI fallback
465
+ if args.json:
466
+ json.dump({"ok": False, "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2)
467
+ sys.stdout.write("\n")
468
+ else:
469
+ print(f"⚠️ {exc}", file=sys.stderr)
470
+ return 1
471
+
472
+ if args.json:
473
+ json.dump({"ok": True, **payload}, sys.stdout, ensure_ascii=False, indent=2)
474
+ sys.stdout.write("\n")
475
+ return 0
476
+
477
+ return print_human_response(args.command, payload)
478
+
479
+
480
+ if __name__ == "__main__":
481
+ raise SystemExit(main())