@dhfpub/clawpool 0.1.3 → 0.2.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 +70 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +9 -2
- package/skills/clawpool-auth-access/SKILL.md +224 -0
- package/skills/clawpool-auth-access/agents/openai.yaml +4 -0
- package/skills/clawpool-auth-access/references/api-contract.md +126 -0
- package/skills/clawpool-auth-access/references/clawpool-concepts.md +29 -0
- package/skills/clawpool-auth-access/references/openclaw-setup.md +96 -0
- package/skills/clawpool-auth-access/references/user-replies.md +29 -0
- package/skills/clawpool-auth-access/scripts/clawpool_auth.py +1538 -0
- package/skills/message-send/SKILL.md +147 -0
- package/skills/message-unsend/SKILL.md +221 -0
- package/skills/message-unsend/flowchart.mermaid +113 -0
|
@@ -0,0 +1,1538 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://clawpool.dhf.pub"
|
|
17
|
+
DEFAULT_TIMEOUT_SECONDS = 15
|
|
18
|
+
DEFAULT_OPENCLAW_CONFIG_PATH = "~/.openclaw/openclaw.json"
|
|
19
|
+
DEFAULT_PORTAL_URL = "https://clawpool.dhf.pub/"
|
|
20
|
+
DEFAULT_OPENCLAW_TOOLS_PROFILE = "coding"
|
|
21
|
+
DEFAULT_OPENCLAW_TOOLS_VISIBILITY = "agent"
|
|
22
|
+
REQUIRED_OPENCLAW_TOOLS = [
|
|
23
|
+
"message",
|
|
24
|
+
"clawpool_group",
|
|
25
|
+
"clawpool_agent_admin",
|
|
26
|
+
]
|
|
27
|
+
REQUIRED_ADMIN_PLUGIN_TOOLS = [
|
|
28
|
+
"clawpool_group",
|
|
29
|
+
"clawpool_agent_admin",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClawpoolAuthError(RuntimeError):
|
|
34
|
+
def __init__(self, message, status=0, code=-1, payload=None):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status = status
|
|
37
|
+
self.code = code
|
|
38
|
+
self.payload = payload
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_base_url(raw_base_url: str) -> str:
|
|
42
|
+
base = (raw_base_url or "").strip() or DEFAULT_BASE_URL
|
|
43
|
+
parsed = urllib.parse.urlparse(base)
|
|
44
|
+
if not parsed.scheme or not parsed.netloc:
|
|
45
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
46
|
+
|
|
47
|
+
path = parsed.path.rstrip("/")
|
|
48
|
+
if not path:
|
|
49
|
+
path = "/v1"
|
|
50
|
+
elif not path.endswith("/v1"):
|
|
51
|
+
path = f"{path}/v1"
|
|
52
|
+
|
|
53
|
+
normalized = parsed._replace(path=path, params="", query="", fragment="")
|
|
54
|
+
return urllib.parse.urlunparse(normalized).rstrip("/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def request_json(method: str, path: str, base_url: str, body=None, headers=None):
|
|
58
|
+
api_base_url = normalize_base_url(base_url)
|
|
59
|
+
url = f"{api_base_url}{path if path.startswith('/') else '/' + path}"
|
|
60
|
+
data = None
|
|
61
|
+
final_headers = dict(headers or {})
|
|
62
|
+
if body is not None:
|
|
63
|
+
data = json.dumps(body).encode("utf-8")
|
|
64
|
+
final_headers["Content-Type"] = "application/json"
|
|
65
|
+
|
|
66
|
+
req = urllib.request.Request(url=url, data=data, headers=final_headers, method=method)
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp:
|
|
69
|
+
status = getattr(resp, "status", 200)
|
|
70
|
+
raw = resp.read().decode("utf-8")
|
|
71
|
+
except urllib.error.HTTPError as exc:
|
|
72
|
+
status = exc.code
|
|
73
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
74
|
+
except urllib.error.URLError as exc:
|
|
75
|
+
raise ClawpoolAuthError(f"network error: {exc.reason}") from exc
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
payload = json.loads(raw)
|
|
79
|
+
except json.JSONDecodeError as exc:
|
|
80
|
+
raise ClawpoolAuthError(f"invalid json response: {raw[:256]}", status=status) from exc
|
|
81
|
+
|
|
82
|
+
code = int(payload.get("code", -1))
|
|
83
|
+
msg = str(payload.get("msg", "")).strip() or "unknown error"
|
|
84
|
+
if status >= 400 or code != 0:
|
|
85
|
+
raise ClawpoolAuthError(msg, status=status, code=code, payload=payload)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"api_base_url": api_base_url,
|
|
89
|
+
"status": status,
|
|
90
|
+
"data": payload.get("data"),
|
|
91
|
+
"payload": payload,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def maybe_write_captcha_image(b64s: str):
|
|
96
|
+
text = (b64s or "").strip()
|
|
97
|
+
if not text.startswith("data:image/"):
|
|
98
|
+
return ""
|
|
99
|
+
marker = ";base64,"
|
|
100
|
+
idx = text.find(marker)
|
|
101
|
+
if idx < 0:
|
|
102
|
+
return ""
|
|
103
|
+
encoded = text[idx + len(marker) :]
|
|
104
|
+
try:
|
|
105
|
+
content = base64.b64decode(encoded)
|
|
106
|
+
except Exception:
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
fd, path = tempfile.mkstemp(prefix="clawpool-captcha-", suffix=".png")
|
|
110
|
+
try:
|
|
111
|
+
with os.fdopen(fd, "wb") as handle:
|
|
112
|
+
handle.write(content)
|
|
113
|
+
except Exception:
|
|
114
|
+
try:
|
|
115
|
+
os.unlink(path)
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
return ""
|
|
119
|
+
return path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def print_json(payload):
|
|
123
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
124
|
+
sys.stdout.write("\n")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_auth_result(action: str, result: dict):
|
|
128
|
+
data = result.get("data") or {}
|
|
129
|
+
user = data.get("user") or {}
|
|
130
|
+
user_id = user.get("id", "")
|
|
131
|
+
payload = {
|
|
132
|
+
"ok": True,
|
|
133
|
+
"action": action,
|
|
134
|
+
"api_base_url": result["api_base_url"],
|
|
135
|
+
"access_token": data.get("access_token", ""),
|
|
136
|
+
"refresh_token": data.get("refresh_token", ""),
|
|
137
|
+
"expires_in": data.get("expires_in", 0),
|
|
138
|
+
"user_id": user_id,
|
|
139
|
+
"data": data,
|
|
140
|
+
}
|
|
141
|
+
payload.update(
|
|
142
|
+
build_portal_guidance(
|
|
143
|
+
True,
|
|
144
|
+
f"账号已可用,可直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
payload.update(build_user_reply_templates("login_ready"))
|
|
148
|
+
return payload
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_agent_result(result: dict):
|
|
152
|
+
data = result.get("data") or {}
|
|
153
|
+
return {
|
|
154
|
+
"ok": True,
|
|
155
|
+
"action": "create-api-agent",
|
|
156
|
+
"api_base_url": result["api_base_url"],
|
|
157
|
+
"agent_id": data.get("id", ""),
|
|
158
|
+
"agent_name": data.get("agent_name", ""),
|
|
159
|
+
"provider_type": data.get("provider_type", 0),
|
|
160
|
+
"api_endpoint": data.get("api_endpoint", ""),
|
|
161
|
+
"api_key": data.get("api_key", ""),
|
|
162
|
+
"api_key_hint": data.get("api_key_hint", ""),
|
|
163
|
+
"session_id": data.get("session_id", ""),
|
|
164
|
+
"data": data,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def login_with_credentials(base_url: str, account: str, password: str, device_id: str, platform: str):
|
|
169
|
+
result = request_json(
|
|
170
|
+
"POST",
|
|
171
|
+
"/auth/login",
|
|
172
|
+
base_url,
|
|
173
|
+
body={
|
|
174
|
+
"account": account,
|
|
175
|
+
"password": password,
|
|
176
|
+
"device_id": device_id,
|
|
177
|
+
"platform": platform,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
return build_auth_result("login", result)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def create_api_agent(base_url: str, access_token: str, agent_name: str, avatar_url: str):
|
|
184
|
+
request_body = {
|
|
185
|
+
"agent_name": agent_name.strip(),
|
|
186
|
+
"provider_type": 3,
|
|
187
|
+
}
|
|
188
|
+
normalized_avatar_url = (avatar_url or "").strip()
|
|
189
|
+
if normalized_avatar_url:
|
|
190
|
+
request_body["avatar_url"] = normalized_avatar_url
|
|
191
|
+
|
|
192
|
+
result = request_json(
|
|
193
|
+
"POST",
|
|
194
|
+
"/agents/create",
|
|
195
|
+
base_url,
|
|
196
|
+
body=request_body,
|
|
197
|
+
headers={
|
|
198
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
return build_agent_result(result)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def list_agents(base_url: str, access_token: str):
|
|
205
|
+
result = request_json(
|
|
206
|
+
"GET",
|
|
207
|
+
"/agents/list",
|
|
208
|
+
base_url,
|
|
209
|
+
headers={
|
|
210
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
data = result.get("data") or {}
|
|
214
|
+
items = data.get("list") or []
|
|
215
|
+
if not isinstance(items, list):
|
|
216
|
+
items = []
|
|
217
|
+
return items
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def rotate_api_agent_key(base_url: str, access_token: str, agent_id: str):
|
|
221
|
+
result = request_json(
|
|
222
|
+
"POST",
|
|
223
|
+
f"/agents/{str(agent_id).strip()}/api/key/rotate",
|
|
224
|
+
base_url,
|
|
225
|
+
body={},
|
|
226
|
+
headers={
|
|
227
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
payload = build_agent_result(result)
|
|
231
|
+
payload["action"] = "rotate-api-agent-key"
|
|
232
|
+
return payload
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def find_existing_api_agent(agents, agent_name: str):
|
|
236
|
+
normalized_name = (agent_name or "").strip()
|
|
237
|
+
if not normalized_name:
|
|
238
|
+
return None
|
|
239
|
+
for item in agents:
|
|
240
|
+
if not isinstance(item, dict):
|
|
241
|
+
continue
|
|
242
|
+
if str(item.get("agent_name", "")).strip() != normalized_name:
|
|
243
|
+
continue
|
|
244
|
+
if int(item.get("provider_type", 0) or 0) != 3:
|
|
245
|
+
continue
|
|
246
|
+
if int(item.get("status", 0) or 0) == 3:
|
|
247
|
+
continue
|
|
248
|
+
return item
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_or_reuse_api_agent(
|
|
253
|
+
base_url: str,
|
|
254
|
+
access_token: str,
|
|
255
|
+
agent_name: str,
|
|
256
|
+
avatar_url: str,
|
|
257
|
+
prefer_existing: bool,
|
|
258
|
+
rotate_on_reuse: bool,
|
|
259
|
+
):
|
|
260
|
+
if prefer_existing:
|
|
261
|
+
agents = list_agents(base_url, access_token)
|
|
262
|
+
existing = find_existing_api_agent(agents, agent_name)
|
|
263
|
+
if existing is not None:
|
|
264
|
+
if not rotate_on_reuse:
|
|
265
|
+
raise ClawpoolAuthError(
|
|
266
|
+
"existing provider_type=3 agent found but rotate-on-reuse is disabled; cannot obtain api_key safely",
|
|
267
|
+
payload={"existing_agent": existing},
|
|
268
|
+
)
|
|
269
|
+
rotated = rotate_api_agent_key(base_url, access_token, str(existing.get("id", "")).strip())
|
|
270
|
+
rotated["source"] = "reused_existing_agent_with_rotated_key"
|
|
271
|
+
rotated["existing_agent"] = existing
|
|
272
|
+
return rotated
|
|
273
|
+
|
|
274
|
+
created = create_api_agent(base_url, access_token, agent_name, avatar_url)
|
|
275
|
+
created["source"] = "created_new_agent"
|
|
276
|
+
return created
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def shell_command(cmd):
|
|
280
|
+
return " ".join(shlex.quote(part) for part in cmd)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def run_command_capture(cmd):
|
|
284
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
285
|
+
return {
|
|
286
|
+
"command": shell_command(cmd),
|
|
287
|
+
"returncode": proc.returncode,
|
|
288
|
+
"stdout": proc.stdout.strip(),
|
|
289
|
+
"stderr": proc.stderr.strip(),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def parse_json_fragment(raw: str):
|
|
294
|
+
text = (raw or "").strip()
|
|
295
|
+
if not text:
|
|
296
|
+
return None
|
|
297
|
+
for idx, char in enumerate(text):
|
|
298
|
+
if char not in "[{":
|
|
299
|
+
continue
|
|
300
|
+
fragment = text[idx:]
|
|
301
|
+
try:
|
|
302
|
+
return json.loads(fragment)
|
|
303
|
+
except json.JSONDecodeError:
|
|
304
|
+
continue
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def build_openclaw_base_cmd(args):
|
|
309
|
+
base_cmd = [(args.openclaw_bin or "").strip() or "openclaw"]
|
|
310
|
+
profile = str(getattr(args, "openclaw_profile", "") or "").strip()
|
|
311
|
+
if profile:
|
|
312
|
+
base_cmd.extend(["--profile", profile])
|
|
313
|
+
return base_cmd
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_gateway_restart_command(args):
|
|
317
|
+
return build_openclaw_base_cmd(args) + ["gateway", "restart"]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def normalize_string_list(values):
|
|
321
|
+
if not isinstance(values, list):
|
|
322
|
+
return []
|
|
323
|
+
normalized = []
|
|
324
|
+
seen = set()
|
|
325
|
+
for item in values:
|
|
326
|
+
text = str(item or "").strip()
|
|
327
|
+
if not text or text in seen:
|
|
328
|
+
continue
|
|
329
|
+
seen.add(text)
|
|
330
|
+
normalized.append(text)
|
|
331
|
+
return normalized
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def build_reference_commands(args, agent_id: str, api_endpoint: str, api_key: str):
|
|
335
|
+
commands = []
|
|
336
|
+
openclaw_cmd = build_openclaw_base_cmd(args)
|
|
337
|
+
if not args.skip_plugin_install:
|
|
338
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool"])
|
|
339
|
+
if not args.skip_plugin_enable:
|
|
340
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool"])
|
|
341
|
+
if not bool(getattr(args, "skip_admin_plugin_install", False)):
|
|
342
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool-admin"])
|
|
343
|
+
if not bool(getattr(args, "skip_admin_plugin_enable", False)):
|
|
344
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool-admin"])
|
|
345
|
+
commands.append(
|
|
346
|
+
openclaw_cmd
|
|
347
|
+
+ [
|
|
348
|
+
"channels",
|
|
349
|
+
"add",
|
|
350
|
+
"--channel",
|
|
351
|
+
"clawpool",
|
|
352
|
+
"--name",
|
|
353
|
+
args.channel_name.strip(),
|
|
354
|
+
"--http-url",
|
|
355
|
+
api_endpoint,
|
|
356
|
+
"--user-id",
|
|
357
|
+
agent_id,
|
|
358
|
+
"--token",
|
|
359
|
+
api_key,
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
if not args.skip_gateway_restart:
|
|
363
|
+
commands.append(build_gateway_restart_command(args))
|
|
364
|
+
return commands
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def inspect_plugin_state(args, plugin_id: str, required_channel_ids=None, required_tool_names=None, skip_install=False, skip_enable=False):
|
|
368
|
+
required_channels = list(required_channel_ids or [])
|
|
369
|
+
required_tools = list(required_tool_names or [])
|
|
370
|
+
entry = run_command_capture(build_openclaw_base_cmd(args) + ["plugins", "info", plugin_id, "--json"])
|
|
371
|
+
parsed = parse_json_fragment(entry["stdout"])
|
|
372
|
+
payload = {
|
|
373
|
+
"plugin_id": plugin_id,
|
|
374
|
+
"inspection_command": entry["command"],
|
|
375
|
+
"inspection_returncode": entry["returncode"],
|
|
376
|
+
"detected": False,
|
|
377
|
+
"enabled": False,
|
|
378
|
+
"status": "missing",
|
|
379
|
+
"source": "",
|
|
380
|
+
"origin": "",
|
|
381
|
+
"channel_ids": [],
|
|
382
|
+
"tool_names": [],
|
|
383
|
+
"needs_install": not skip_install,
|
|
384
|
+
"needs_enable": False,
|
|
385
|
+
"ready": False,
|
|
386
|
+
}
|
|
387
|
+
if entry["returncode"] != 0:
|
|
388
|
+
payload["inspection_error"] = entry["stderr"] or entry["stdout"] or "plugin inspection failed"
|
|
389
|
+
payload["inspection_stdout"] = entry["stdout"]
|
|
390
|
+
payload["inspection_stderr"] = entry["stderr"]
|
|
391
|
+
payload["needs_enable"] = not skip_enable
|
|
392
|
+
return payload
|
|
393
|
+
if not isinstance(parsed, dict):
|
|
394
|
+
payload["status"] = "unknown"
|
|
395
|
+
payload["needs_enable"] = not skip_enable
|
|
396
|
+
payload["inspection_error"] = "failed to parse openclaw plugin json output"
|
|
397
|
+
payload["inspection_stdout"] = entry["stdout"]
|
|
398
|
+
payload["inspection_stderr"] = entry["stderr"]
|
|
399
|
+
return payload
|
|
400
|
+
|
|
401
|
+
enabled = bool(parsed.get("enabled", False))
|
|
402
|
+
status = str(parsed.get("status", "")).strip() or "unknown"
|
|
403
|
+
channel_ids = parsed.get("channelIds")
|
|
404
|
+
tool_names = parsed.get("toolNames")
|
|
405
|
+
normalized_channel_ids = channel_ids if isinstance(channel_ids, list) else []
|
|
406
|
+
normalized_tool_names = tool_names if isinstance(tool_names, list) else []
|
|
407
|
+
ready = (
|
|
408
|
+
enabled
|
|
409
|
+
and status == "loaded"
|
|
410
|
+
and all(item in normalized_channel_ids for item in required_channels)
|
|
411
|
+
and all(item in normalized_tool_names for item in required_tools)
|
|
412
|
+
)
|
|
413
|
+
payload.update(
|
|
414
|
+
{
|
|
415
|
+
"detected": True,
|
|
416
|
+
"enabled": enabled,
|
|
417
|
+
"status": status,
|
|
418
|
+
"source": str(parsed.get("source", "")).strip(),
|
|
419
|
+
"origin": str(parsed.get("origin", "")).strip(),
|
|
420
|
+
"channel_ids": normalized_channel_ids,
|
|
421
|
+
"tool_names": normalized_tool_names,
|
|
422
|
+
"needs_install": False,
|
|
423
|
+
"needs_enable": (not skip_enable) and (not ready),
|
|
424
|
+
"ready": ready,
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
return payload
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def inspect_openclaw_plugin(args):
|
|
431
|
+
return inspect_plugin_state(
|
|
432
|
+
args,
|
|
433
|
+
"clawpool",
|
|
434
|
+
required_channel_ids=["clawpool"],
|
|
435
|
+
skip_install=bool(getattr(args, "skip_plugin_install", False)),
|
|
436
|
+
skip_enable=bool(getattr(args, "skip_plugin_enable", False)),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def inspect_openclaw_admin_plugin(args):
|
|
441
|
+
return inspect_plugin_state(
|
|
442
|
+
args,
|
|
443
|
+
"clawpool-admin",
|
|
444
|
+
required_tool_names=REQUIRED_ADMIN_PLUGIN_TOOLS,
|
|
445
|
+
skip_install=bool(getattr(args, "skip_admin_plugin_install", False)),
|
|
446
|
+
skip_enable=bool(getattr(args, "skip_admin_plugin_enable", False)),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def build_plugin_commands(args, plugin_status=None):
|
|
451
|
+
commands = []
|
|
452
|
+
openclaw_cmd = build_openclaw_base_cmd(args)
|
|
453
|
+
if isinstance(plugin_status, dict):
|
|
454
|
+
if bool(plugin_status.get("needs_install", False)):
|
|
455
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool"])
|
|
456
|
+
if bool(plugin_status.get("needs_enable", False)):
|
|
457
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool"])
|
|
458
|
+
return commands
|
|
459
|
+
|
|
460
|
+
if not args.skip_plugin_install:
|
|
461
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool"])
|
|
462
|
+
if not args.skip_plugin_enable:
|
|
463
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool"])
|
|
464
|
+
return commands
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def build_admin_plugin_commands(args, plugin_status=None):
|
|
468
|
+
commands = []
|
|
469
|
+
openclaw_cmd = build_openclaw_base_cmd(args)
|
|
470
|
+
if isinstance(plugin_status, dict):
|
|
471
|
+
if bool(plugin_status.get("needs_install", False)):
|
|
472
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool-admin"])
|
|
473
|
+
if bool(plugin_status.get("needs_enable", False)):
|
|
474
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool-admin"])
|
|
475
|
+
return commands
|
|
476
|
+
|
|
477
|
+
if not bool(getattr(args, "skip_admin_plugin_install", False)):
|
|
478
|
+
commands.append(openclaw_cmd + ["plugins", "install", "@dhfpub/clawpool-admin"])
|
|
479
|
+
if not bool(getattr(args, "skip_admin_plugin_enable", False)):
|
|
480
|
+
commands.append(openclaw_cmd + ["plugins", "enable", "clawpool-admin"])
|
|
481
|
+
return commands
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def build_direct_config(agent_id: str, api_endpoint: str, api_key: str):
|
|
485
|
+
return {
|
|
486
|
+
"channels": {
|
|
487
|
+
"clawpool": {
|
|
488
|
+
"enabled": True,
|
|
489
|
+
"wsUrl": api_endpoint,
|
|
490
|
+
"agentId": agent_id,
|
|
491
|
+
"apiKey": api_key,
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
"tools": {
|
|
495
|
+
"profile": DEFAULT_OPENCLAW_TOOLS_PROFILE,
|
|
496
|
+
"alsoAllow": list(REQUIRED_OPENCLAW_TOOLS),
|
|
497
|
+
"sessions": {
|
|
498
|
+
"visibility": DEFAULT_OPENCLAW_TOOLS_VISIBILITY,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def expand_path(path: str) -> str:
|
|
505
|
+
return os.path.abspath(os.path.expanduser((path or "").strip() or DEFAULT_OPENCLAW_CONFIG_PATH))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def resolve_config_path(args) -> str:
|
|
509
|
+
raw_path = str(getattr(args, "config_path", "") or "").strip()
|
|
510
|
+
if raw_path and raw_path != DEFAULT_OPENCLAW_CONFIG_PATH:
|
|
511
|
+
return expand_path(raw_path)
|
|
512
|
+
|
|
513
|
+
profile = str(getattr(args, "openclaw_profile", "") or "").strip()
|
|
514
|
+
if profile:
|
|
515
|
+
return expand_path(f"~/.openclaw-{profile}/openclaw.json")
|
|
516
|
+
|
|
517
|
+
return expand_path(DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def load_json_file(path: str):
|
|
521
|
+
if not os.path.exists(path):
|
|
522
|
+
return {}
|
|
523
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
524
|
+
raw = handle.read().strip()
|
|
525
|
+
if not raw:
|
|
526
|
+
return {}
|
|
527
|
+
return json.loads(raw)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def extract_main_clawpool_config(cfg):
|
|
531
|
+
channels = cfg.get("channels") if isinstance(cfg, dict) else None
|
|
532
|
+
clawpool = channels.get("clawpool") if isinstance(channels, dict) else None
|
|
533
|
+
if not isinstance(clawpool, dict):
|
|
534
|
+
return {}
|
|
535
|
+
return {
|
|
536
|
+
"enabled": bool(clawpool.get("enabled", False)),
|
|
537
|
+
"wsUrl": str(clawpool.get("wsUrl", "")).strip(),
|
|
538
|
+
"agentId": str(clawpool.get("agentId", "")).strip(),
|
|
539
|
+
"apiKey": str(clawpool.get("apiKey", "")).strip(),
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def extract_openclaw_tools_config(cfg):
|
|
544
|
+
tools = cfg.get("tools") if isinstance(cfg, dict) else None
|
|
545
|
+
if not isinstance(tools, dict):
|
|
546
|
+
return {}
|
|
547
|
+
|
|
548
|
+
sessions = dict(tools.get("sessions") or {})
|
|
549
|
+
if not isinstance(sessions, dict):
|
|
550
|
+
sessions = {}
|
|
551
|
+
|
|
552
|
+
payload = dict(tools)
|
|
553
|
+
payload["profile"] = str(payload.get("profile", "")).strip()
|
|
554
|
+
payload["alsoAllow"] = normalize_string_list(payload.get("alsoAllow"))
|
|
555
|
+
payload["sessions"] = sessions
|
|
556
|
+
payload["sessions"]["visibility"] = str(sessions.get("visibility", "")).strip()
|
|
557
|
+
return payload
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def build_required_tools_config():
|
|
561
|
+
return {
|
|
562
|
+
"profile": DEFAULT_OPENCLAW_TOOLS_PROFILE,
|
|
563
|
+
"alsoAllow": list(REQUIRED_OPENCLAW_TOOLS),
|
|
564
|
+
"sessions": {
|
|
565
|
+
"visibility": DEFAULT_OPENCLAW_TOOLS_VISIBILITY,
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def mask_secret(value: str):
|
|
571
|
+
text = str(value or "").strip()
|
|
572
|
+
if not text:
|
|
573
|
+
return ""
|
|
574
|
+
if len(text) <= 8:
|
|
575
|
+
return "*" * len(text)
|
|
576
|
+
return f"{text[:4]}...{text[-4:]}"
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def redact_current_main_channel(channel):
|
|
580
|
+
payload = dict(channel or {})
|
|
581
|
+
api_key = str(payload.get("apiKey", "")).strip()
|
|
582
|
+
if api_key:
|
|
583
|
+
payload["apiKey"] = "<redacted>"
|
|
584
|
+
payload["apiKeyMasked"] = mask_secret(api_key)
|
|
585
|
+
return payload
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def build_portal_guidance(portal_ready: bool, portal_hint: str):
|
|
589
|
+
payload = {
|
|
590
|
+
"portal_url": DEFAULT_PORTAL_URL,
|
|
591
|
+
"portal_ready": bool(portal_ready),
|
|
592
|
+
}
|
|
593
|
+
normalized_hint = str(portal_hint or "").strip()
|
|
594
|
+
if normalized_hint:
|
|
595
|
+
payload["portal_hint"] = normalized_hint
|
|
596
|
+
return payload
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def build_clawpool_explainer():
|
|
600
|
+
return {
|
|
601
|
+
"clawpool_intro": "ClawPool 完全适配 OpenClaw 的通讯协议,实现 OpenClaw 交互完美打通。",
|
|
602
|
+
"clawpool_highlights": [
|
|
603
|
+
"支持多 agent 建群互聊",
|
|
604
|
+
"支持 agent 私聊",
|
|
605
|
+
"agent 之间通信链路完美打通",
|
|
606
|
+
"配置简单易用,只需要将这个技能交给 OpenClaw、Claude、Codex,即可完成检查与配置",
|
|
607
|
+
"如需群管理能力,还需要安装启用 @dhfpub/clawpool-admin,并在 tools 中允许 message、clawpool_group、clawpool_agent_admin",
|
|
608
|
+
],
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def build_user_reply_templates(scenario: str):
|
|
613
|
+
one_liner = "ClawPool 完全适配 OpenClaw 通讯协议,支持多 agent 群聊、agent 私聊和 agent 间互通。"
|
|
614
|
+
short_intro = (
|
|
615
|
+
"ClawPool 把 OpenClaw 的渠道交互和 agent 通信直接打通,"
|
|
616
|
+
"配置好后,多 agent 建群互聊、agent 私聊都可以直接用起来。"
|
|
617
|
+
)
|
|
618
|
+
messages = {
|
|
619
|
+
"already_configured": f"{one_liner}当前主通道和群管理能力都已配置完成,你现在可以直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
620
|
+
"configured_now": f"{one_liner}现在已经配置完成,你可以直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
621
|
+
"main_ready_admin_pending": (
|
|
622
|
+
f"{one_liner}当前主通道已经可用,你可以先直接登录 {DEFAULT_PORTAL_URL} 体验;"
|
|
623
|
+
"如果还要在 OpenClaw 里使用群管理能力,我可以继续安装启用 @dhfpub/clawpool-admin,"
|
|
624
|
+
"并补齐 message、clawpool_group、clawpool_agent_admin 这三个工具权限。"
|
|
625
|
+
),
|
|
626
|
+
"needs_setup": f"{one_liner}当前还没有完全配置好,我可以继续帮你完成检查和配置。",
|
|
627
|
+
"login_ready": f"{one_liner}账号已经可用,你可以直接登录 {DEFAULT_PORTAL_URL} 体验;如果需要,我也可以继续帮你把 OpenClaw 主通道配好。",
|
|
628
|
+
}
|
|
629
|
+
normalized_scenario = str(scenario or "").strip() or "needs_setup"
|
|
630
|
+
return {
|
|
631
|
+
"user_reply_templates": {
|
|
632
|
+
"scenario": normalized_scenario,
|
|
633
|
+
"one_liner": one_liner,
|
|
634
|
+
"short_intro": short_intro,
|
|
635
|
+
"recommended_message": messages.get(normalized_scenario, messages["needs_setup"]),
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def extract_ws_agent_id(ws_url: str):
|
|
641
|
+
text = str(ws_url or "").strip()
|
|
642
|
+
if not text:
|
|
643
|
+
return ""
|
|
644
|
+
try:
|
|
645
|
+
parsed = urllib.parse.urlparse(text)
|
|
646
|
+
except ValueError:
|
|
647
|
+
return ""
|
|
648
|
+
if parsed.scheme not in ("ws", "wss"):
|
|
649
|
+
return ""
|
|
650
|
+
values = urllib.parse.parse_qs(parsed.query)
|
|
651
|
+
candidates = values.get("agent_id") or values.get("agentId") or []
|
|
652
|
+
if not candidates:
|
|
653
|
+
return ""
|
|
654
|
+
return str(candidates[0]).strip()
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def inspect_main_clawpool_channel(channel):
|
|
658
|
+
current = dict(channel or {})
|
|
659
|
+
issues = []
|
|
660
|
+
ws_url = str(current.get("wsUrl", "")).strip()
|
|
661
|
+
agent_id = str(current.get("agentId", "")).strip()
|
|
662
|
+
api_key = str(current.get("apiKey", "")).strip()
|
|
663
|
+
ws_agent_id = extract_ws_agent_id(ws_url)
|
|
664
|
+
|
|
665
|
+
if not current:
|
|
666
|
+
issues.append(
|
|
667
|
+
{
|
|
668
|
+
"code": "main_channel_missing",
|
|
669
|
+
"message": "channels.clawpool is not configured for the main OpenClaw agent",
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
return {
|
|
673
|
+
"configured": False,
|
|
674
|
+
"issues": issues,
|
|
675
|
+
"ws_agent_id": "",
|
|
676
|
+
"agent_id_matches_ws_url": False,
|
|
677
|
+
"ready": False,
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if not bool(current.get("enabled", False)):
|
|
681
|
+
issues.append(
|
|
682
|
+
{
|
|
683
|
+
"code": "main_channel_disabled",
|
|
684
|
+
"message": "channels.clawpool exists but is disabled",
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
if not ws_url:
|
|
689
|
+
issues.append(
|
|
690
|
+
{
|
|
691
|
+
"code": "main_channel_missing_ws_url",
|
|
692
|
+
"message": "channels.clawpool.wsUrl is empty",
|
|
693
|
+
}
|
|
694
|
+
)
|
|
695
|
+
else:
|
|
696
|
+
try:
|
|
697
|
+
parsed = urllib.parse.urlparse(ws_url)
|
|
698
|
+
except ValueError:
|
|
699
|
+
parsed = None
|
|
700
|
+
if parsed is None or parsed.scheme not in ("ws", "wss"):
|
|
701
|
+
issues.append(
|
|
702
|
+
{
|
|
703
|
+
"code": "main_channel_invalid_ws_url",
|
|
704
|
+
"message": "channels.clawpool.wsUrl must be a ws:// or wss:// URL",
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
if not ws_agent_id:
|
|
708
|
+
issues.append(
|
|
709
|
+
{
|
|
710
|
+
"code": "main_channel_missing_ws_agent_id",
|
|
711
|
+
"message": "channels.clawpool.wsUrl does not contain agent_id query parameter",
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
elif agent_id and ws_agent_id != agent_id:
|
|
715
|
+
issues.append(
|
|
716
|
+
{
|
|
717
|
+
"code": "main_channel_agent_id_mismatch",
|
|
718
|
+
"message": "channels.clawpool.agentId does not match the wsUrl agent_id",
|
|
719
|
+
"ws_agent_id": ws_agent_id,
|
|
720
|
+
"agent_id": agent_id,
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
if not agent_id:
|
|
725
|
+
issues.append(
|
|
726
|
+
{
|
|
727
|
+
"code": "main_channel_missing_agent_id",
|
|
728
|
+
"message": "channels.clawpool.agentId is empty",
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if not api_key:
|
|
733
|
+
issues.append(
|
|
734
|
+
{
|
|
735
|
+
"code": "main_channel_missing_api_key",
|
|
736
|
+
"message": "channels.clawpool.apiKey is empty",
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
"configured": True,
|
|
742
|
+
"issues": issues,
|
|
743
|
+
"ws_agent_id": ws_agent_id,
|
|
744
|
+
"agent_id_matches_ws_url": bool(agent_id) and bool(ws_agent_id) and ws_agent_id == agent_id,
|
|
745
|
+
"ready": len(issues) == 0,
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def apply_main_clawpool_config(cfg, agent_id: str, api_endpoint: str, api_key: str):
|
|
750
|
+
next_cfg = dict(cfg or {})
|
|
751
|
+
channels = dict(next_cfg.get("channels") or {})
|
|
752
|
+
clawpool = dict(channels.get("clawpool") or {})
|
|
753
|
+
clawpool["enabled"] = True
|
|
754
|
+
clawpool["wsUrl"] = api_endpoint
|
|
755
|
+
clawpool["agentId"] = agent_id
|
|
756
|
+
clawpool["apiKey"] = api_key
|
|
757
|
+
channels["clawpool"] = clawpool
|
|
758
|
+
next_cfg["channels"] = channels
|
|
759
|
+
return next_cfg
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def inspect_openclaw_tools_config(cfg):
|
|
763
|
+
current = extract_openclaw_tools_config(cfg)
|
|
764
|
+
issues = []
|
|
765
|
+
missing_tools = []
|
|
766
|
+
|
|
767
|
+
if not current:
|
|
768
|
+
issues.append(
|
|
769
|
+
{
|
|
770
|
+
"code": "tools_config_missing",
|
|
771
|
+
"message": "tools config is missing",
|
|
772
|
+
}
|
|
773
|
+
)
|
|
774
|
+
missing_tools = list(REQUIRED_OPENCLAW_TOOLS)
|
|
775
|
+
return {
|
|
776
|
+
"configured": False,
|
|
777
|
+
"issues": issues,
|
|
778
|
+
"missing_required_tools": missing_tools,
|
|
779
|
+
"required_tools": list(REQUIRED_OPENCLAW_TOOLS),
|
|
780
|
+
"ready": False,
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if current.get("profile", "") != DEFAULT_OPENCLAW_TOOLS_PROFILE:
|
|
784
|
+
issues.append(
|
|
785
|
+
{
|
|
786
|
+
"code": "tools_profile_invalid",
|
|
787
|
+
"message": f"tools.profile must be {DEFAULT_OPENCLAW_TOOLS_PROFILE}",
|
|
788
|
+
"current": current.get("profile", ""),
|
|
789
|
+
"expected": DEFAULT_OPENCLAW_TOOLS_PROFILE,
|
|
790
|
+
}
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
current_also_allow = normalize_string_list(current.get("alsoAllow"))
|
|
794
|
+
missing_tools = [tool for tool in REQUIRED_OPENCLAW_TOOLS if tool not in current_also_allow]
|
|
795
|
+
if missing_tools:
|
|
796
|
+
issues.append(
|
|
797
|
+
{
|
|
798
|
+
"code": "tools_required_tools_missing",
|
|
799
|
+
"message": "tools.alsoAllow is missing required ClawPool tool ids",
|
|
800
|
+
"missing_tools": missing_tools,
|
|
801
|
+
"expected_tools": list(REQUIRED_OPENCLAW_TOOLS),
|
|
802
|
+
}
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
sessions = current.get("sessions") if isinstance(current.get("sessions"), dict) else {}
|
|
806
|
+
visibility = str(sessions.get("visibility", "")).strip()
|
|
807
|
+
if visibility != DEFAULT_OPENCLAW_TOOLS_VISIBILITY:
|
|
808
|
+
issues.append(
|
|
809
|
+
{
|
|
810
|
+
"code": "tools_sessions_visibility_invalid",
|
|
811
|
+
"message": f"tools.sessions.visibility must be {DEFAULT_OPENCLAW_TOOLS_VISIBILITY}",
|
|
812
|
+
"current": visibility,
|
|
813
|
+
"expected": DEFAULT_OPENCLAW_TOOLS_VISIBILITY,
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
"configured": True,
|
|
819
|
+
"issues": issues,
|
|
820
|
+
"missing_required_tools": missing_tools,
|
|
821
|
+
"required_tools": list(REQUIRED_OPENCLAW_TOOLS),
|
|
822
|
+
"ready": len(issues) == 0,
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def apply_required_openclaw_tools_config(cfg):
|
|
827
|
+
next_cfg = dict(cfg or {})
|
|
828
|
+
tools = dict(next_cfg.get("tools") or {})
|
|
829
|
+
existing_also_allow = normalize_string_list(tools.get("alsoAllow"))
|
|
830
|
+
next_also_allow = list(REQUIRED_OPENCLAW_TOOLS)
|
|
831
|
+
for item in existing_also_allow:
|
|
832
|
+
if item not in next_also_allow:
|
|
833
|
+
next_also_allow.append(item)
|
|
834
|
+
|
|
835
|
+
sessions = dict(tools.get("sessions") or {})
|
|
836
|
+
if not isinstance(sessions, dict):
|
|
837
|
+
sessions = {}
|
|
838
|
+
|
|
839
|
+
tools["profile"] = DEFAULT_OPENCLAW_TOOLS_PROFILE
|
|
840
|
+
tools["alsoAllow"] = next_also_allow
|
|
841
|
+
sessions["visibility"] = DEFAULT_OPENCLAW_TOOLS_VISIBILITY
|
|
842
|
+
tools["sessions"] = sessions
|
|
843
|
+
next_cfg["tools"] = tools
|
|
844
|
+
return next_cfg
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def write_json_file_with_backup(path: str, payload):
|
|
848
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
849
|
+
backup_path = ""
|
|
850
|
+
if os.path.exists(path):
|
|
851
|
+
backup_path = f"{path}.bak.{uuid.uuid4().hex[:8]}"
|
|
852
|
+
with open(path, "rb") as src, open(backup_path, "wb") as dst:
|
|
853
|
+
dst.write(src.read())
|
|
854
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
855
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
856
|
+
handle.write("\n")
|
|
857
|
+
return backup_path
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def run_commands(commands):
|
|
861
|
+
results = []
|
|
862
|
+
for cmd in commands:
|
|
863
|
+
entry = run_command_capture(cmd)
|
|
864
|
+
results.append(entry)
|
|
865
|
+
if entry["returncode"] != 0:
|
|
866
|
+
raise ClawpoolAuthError(
|
|
867
|
+
f"openclaw command failed: {entry['command']}",
|
|
868
|
+
payload={"command_results": results},
|
|
869
|
+
)
|
|
870
|
+
return results
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def build_onboard_values(agent_id: str, api_endpoint: str, api_key: str):
|
|
874
|
+
return {
|
|
875
|
+
"channel": "Clawpool",
|
|
876
|
+
"wsUrl": api_endpoint,
|
|
877
|
+
"agentId": agent_id,
|
|
878
|
+
"apiKey": api_key,
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def build_channel_environment_variables(agent_id: str, api_endpoint: str, api_key: str):
|
|
883
|
+
return {
|
|
884
|
+
"CLAWPOOL_WS_URL": api_endpoint,
|
|
885
|
+
"CLAWPOOL_AGENT_ID": agent_id,
|
|
886
|
+
"CLAWPOOL_API_KEY": api_key,
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def is_ready(payload):
|
|
891
|
+
return isinstance(payload, dict) and bool(payload.get("ready", False))
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def is_configured(payload):
|
|
895
|
+
return isinstance(payload, dict) and bool(payload.get("configured", False))
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def ready_for_main_agent(plugin_status, channel_inspection):
|
|
899
|
+
return is_ready(plugin_status) and is_ready(channel_inspection)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def ready_for_group_governance(plugin_status, channel_inspection, admin_plugin_status, tools_inspection):
|
|
903
|
+
return (
|
|
904
|
+
ready_for_main_agent(plugin_status, channel_inspection)
|
|
905
|
+
and is_ready(admin_plugin_status)
|
|
906
|
+
and is_ready(tools_inspection)
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def collect_inspection_gaps(plugin_status, channel_inspection, admin_plugin_status, tools_inspection):
|
|
911
|
+
gaps = []
|
|
912
|
+
if not isinstance(plugin_status, dict):
|
|
913
|
+
gaps.append("plugin_verification_failed")
|
|
914
|
+
elif not bool(plugin_status.get("detected")):
|
|
915
|
+
gaps.append("plugin_missing")
|
|
916
|
+
elif not bool(plugin_status.get("ready")):
|
|
917
|
+
gaps.append("plugin_not_ready")
|
|
918
|
+
|
|
919
|
+
if not is_configured(channel_inspection):
|
|
920
|
+
gaps.append("main_channel_missing")
|
|
921
|
+
elif not is_ready(channel_inspection):
|
|
922
|
+
gaps.append("main_channel_invalid")
|
|
923
|
+
|
|
924
|
+
if not isinstance(admin_plugin_status, dict):
|
|
925
|
+
gaps.append("admin_plugin_verification_failed")
|
|
926
|
+
elif not bool(admin_plugin_status.get("detected")):
|
|
927
|
+
gaps.append("admin_plugin_missing")
|
|
928
|
+
elif not bool(admin_plugin_status.get("ready")):
|
|
929
|
+
gaps.append("admin_plugin_not_ready")
|
|
930
|
+
|
|
931
|
+
if not is_configured(tools_inspection):
|
|
932
|
+
gaps.append("tools_config_missing")
|
|
933
|
+
elif not is_ready(tools_inspection):
|
|
934
|
+
gaps.append("tools_not_ready")
|
|
935
|
+
|
|
936
|
+
return gaps
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def collect_setup_gaps(plugin_status, needs_main_update: bool, admin_plugin_status, needs_tools_update: bool):
|
|
940
|
+
gaps = []
|
|
941
|
+
if not isinstance(plugin_status, dict):
|
|
942
|
+
gaps.append("plugin_verification_failed")
|
|
943
|
+
elif not bool(plugin_status.get("detected")):
|
|
944
|
+
gaps.append("plugin_missing")
|
|
945
|
+
elif not bool(plugin_status.get("ready")):
|
|
946
|
+
gaps.append("plugin_not_ready")
|
|
947
|
+
|
|
948
|
+
if needs_main_update:
|
|
949
|
+
gaps.append("needs_main_config_update")
|
|
950
|
+
|
|
951
|
+
if not isinstance(admin_plugin_status, dict):
|
|
952
|
+
gaps.append("admin_plugin_verification_failed")
|
|
953
|
+
elif not bool(admin_plugin_status.get("detected")):
|
|
954
|
+
gaps.append("admin_plugin_missing")
|
|
955
|
+
elif not bool(admin_plugin_status.get("ready")):
|
|
956
|
+
gaps.append("admin_plugin_not_ready")
|
|
957
|
+
|
|
958
|
+
if needs_tools_update:
|
|
959
|
+
gaps.append("needs_tools_config_update")
|
|
960
|
+
|
|
961
|
+
return gaps
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def classify_gap_state(gaps):
|
|
965
|
+
if not gaps:
|
|
966
|
+
return "already_configured"
|
|
967
|
+
return str(gaps[0]).strip() or "needs_verification"
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def build_recommended_next_steps(gaps):
|
|
971
|
+
mapping = {
|
|
972
|
+
"plugin_verification_failed": "verify_clawpool_plugin_state",
|
|
973
|
+
"plugin_missing": "install_or_enable_clawpool_plugin",
|
|
974
|
+
"plugin_not_ready": "repair_or_enable_clawpool_plugin",
|
|
975
|
+
"main_channel_missing": "configure_main_clawpool_channel",
|
|
976
|
+
"main_channel_invalid": "repair_main_clawpool_channel",
|
|
977
|
+
"needs_main_config_update": "update_main_clawpool_channel",
|
|
978
|
+
"admin_plugin_verification_failed": "verify_clawpool_admin_plugin_state",
|
|
979
|
+
"admin_plugin_missing": "install_or_enable_clawpool_admin_plugin",
|
|
980
|
+
"admin_plugin_not_ready": "repair_or_enable_clawpool_admin_plugin",
|
|
981
|
+
"tools_config_missing": "configure_required_clawpool_tools",
|
|
982
|
+
"tools_not_ready": "repair_required_clawpool_tools",
|
|
983
|
+
"needs_tools_config_update": "update_required_clawpool_tools",
|
|
984
|
+
}
|
|
985
|
+
steps = []
|
|
986
|
+
for gap in gaps:
|
|
987
|
+
step = mapping.get(str(gap).strip())
|
|
988
|
+
if step and step not in steps:
|
|
989
|
+
steps.append(step)
|
|
990
|
+
return steps
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def build_openclaw_inspection_result(args):
|
|
994
|
+
config_path = resolve_config_path(args)
|
|
995
|
+
current_cfg = load_json_file(config_path)
|
|
996
|
+
current_main = extract_main_clawpool_config(current_cfg)
|
|
997
|
+
current_tools = extract_openclaw_tools_config(current_cfg)
|
|
998
|
+
plugin_status = inspect_openclaw_plugin(args)
|
|
999
|
+
admin_plugin_status = inspect_openclaw_admin_plugin(args)
|
|
1000
|
+
channel_inspection = inspect_main_clawpool_channel(current_main)
|
|
1001
|
+
tools_inspection = inspect_openclaw_tools_config(current_cfg)
|
|
1002
|
+
gaps = collect_inspection_gaps(plugin_status, channel_inspection, admin_plugin_status, tools_inspection)
|
|
1003
|
+
inspection_state = classify_gap_state(gaps)
|
|
1004
|
+
main_ready = ready_for_main_agent(plugin_status, channel_inspection)
|
|
1005
|
+
governance_ready = ready_for_group_governance(
|
|
1006
|
+
plugin_status,
|
|
1007
|
+
channel_inspection,
|
|
1008
|
+
admin_plugin_status,
|
|
1009
|
+
tools_inspection,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
payload = {
|
|
1013
|
+
"ok": True,
|
|
1014
|
+
"action": "inspect-openclaw",
|
|
1015
|
+
"inspection_state": inspection_state,
|
|
1016
|
+
"ready_for_main_agent": main_ready,
|
|
1017
|
+
"ready_for_group_governance": governance_ready,
|
|
1018
|
+
"config_path": config_path,
|
|
1019
|
+
"plugin_status": plugin_status,
|
|
1020
|
+
"admin_plugin_status": admin_plugin_status,
|
|
1021
|
+
"current_main_channel": redact_current_main_channel(current_main),
|
|
1022
|
+
"current_tools_config": current_tools,
|
|
1023
|
+
"main_channel_checks": channel_inspection,
|
|
1024
|
+
"tools_checks": tools_inspection,
|
|
1025
|
+
"required_tools_config": build_required_tools_config(),
|
|
1026
|
+
"recommended_next_steps": build_recommended_next_steps(gaps),
|
|
1027
|
+
}
|
|
1028
|
+
if governance_ready:
|
|
1029
|
+
payload.update(
|
|
1030
|
+
build_portal_guidance(
|
|
1031
|
+
True,
|
|
1032
|
+
f"主通道和群管理能力已配置完成,可直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
payload.update(build_user_reply_templates("already_configured"))
|
|
1036
|
+
elif main_ready:
|
|
1037
|
+
payload.update(
|
|
1038
|
+
build_portal_guidance(
|
|
1039
|
+
True,
|
|
1040
|
+
(
|
|
1041
|
+
f"主通道已配置完成,可直接登录 {DEFAULT_PORTAL_URL} 体验;"
|
|
1042
|
+
"如需群管理能力,还需安装启用 @dhfpub/clawpool-admin 并补齐 required tools 配置。"
|
|
1043
|
+
),
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
payload.update(build_user_reply_templates("main_ready_admin_pending"))
|
|
1047
|
+
else:
|
|
1048
|
+
payload.update(build_portal_guidance(False, ""))
|
|
1049
|
+
payload.update(build_user_reply_templates("needs_setup"))
|
|
1050
|
+
payload.update(build_clawpool_explainer())
|
|
1051
|
+
return payload
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def build_openclaw_setup_result(args, agent_id: str, api_endpoint: str, api_key: str):
|
|
1055
|
+
config_path = resolve_config_path(args)
|
|
1056
|
+
current_cfg = load_json_file(config_path)
|
|
1057
|
+
current_main = extract_main_clawpool_config(current_cfg)
|
|
1058
|
+
current_tools = extract_openclaw_tools_config(current_cfg)
|
|
1059
|
+
next_cfg = apply_required_openclaw_tools_config(
|
|
1060
|
+
apply_main_clawpool_config(current_cfg, agent_id, api_endpoint, api_key)
|
|
1061
|
+
)
|
|
1062
|
+
next_main = extract_main_clawpool_config(next_cfg)
|
|
1063
|
+
next_tools = extract_openclaw_tools_config(next_cfg)
|
|
1064
|
+
needs_main_update = current_main != next_main
|
|
1065
|
+
needs_tools_update = current_tools != next_tools
|
|
1066
|
+
needs_update = needs_main_update or needs_tools_update
|
|
1067
|
+
plugin_status = inspect_openclaw_plugin(args)
|
|
1068
|
+
admin_plugin_status = inspect_openclaw_admin_plugin(args)
|
|
1069
|
+
plugin_commands = build_plugin_commands(args, plugin_status)
|
|
1070
|
+
admin_plugin_commands = build_admin_plugin_commands(args, admin_plugin_status)
|
|
1071
|
+
reference_commands = build_reference_commands(args, agent_id, api_endpoint, api_key)
|
|
1072
|
+
channel_inspection = inspect_main_clawpool_channel(current_main)
|
|
1073
|
+
tools_inspection = inspect_openclaw_tools_config(current_cfg)
|
|
1074
|
+
planned_apply_commands = list(plugin_commands) + list(admin_plugin_commands)
|
|
1075
|
+
restart_needed = (not args.skip_gateway_restart) and (
|
|
1076
|
+
bool(plugin_commands) or bool(admin_plugin_commands) or needs_update
|
|
1077
|
+
)
|
|
1078
|
+
if restart_needed:
|
|
1079
|
+
planned_apply_commands.append(build_gateway_restart_command(args))
|
|
1080
|
+
setup_gaps = collect_setup_gaps(plugin_status, needs_main_update, admin_plugin_status, needs_tools_update)
|
|
1081
|
+
main_ready = ready_for_main_agent(plugin_status, channel_inspection) and not needs_main_update
|
|
1082
|
+
governance_ready = main_ready and is_ready(admin_plugin_status) and not needs_tools_update
|
|
1083
|
+
payload = {
|
|
1084
|
+
"ok": True,
|
|
1085
|
+
"action": "configure-openclaw",
|
|
1086
|
+
"apply": bool(args.apply),
|
|
1087
|
+
"apply_strategy": "direct_config_for_main_agent",
|
|
1088
|
+
"setup_state": classify_gap_state(setup_gaps),
|
|
1089
|
+
"ready_for_main_agent": main_ready,
|
|
1090
|
+
"ready_for_group_governance": governance_ready,
|
|
1091
|
+
"config_path": config_path,
|
|
1092
|
+
"channel_name": args.channel_name.strip(),
|
|
1093
|
+
"needs_update": needs_update,
|
|
1094
|
+
"needs_main_channel_update": needs_main_update,
|
|
1095
|
+
"needs_tools_update": needs_tools_update,
|
|
1096
|
+
"setup_gaps": setup_gaps,
|
|
1097
|
+
"current_main_channel": redact_current_main_channel(current_main),
|
|
1098
|
+
"next_main_channel": next_main,
|
|
1099
|
+
"current_tools_config": current_tools,
|
|
1100
|
+
"next_tools_config": next_tools,
|
|
1101
|
+
"main_channel_checks": channel_inspection,
|
|
1102
|
+
"tools_checks": tools_inspection,
|
|
1103
|
+
"required_tools_config": build_required_tools_config(),
|
|
1104
|
+
"recommended_next_steps": build_recommended_next_steps(setup_gaps),
|
|
1105
|
+
"plugin_status": plugin_status,
|
|
1106
|
+
"admin_plugin_status": admin_plugin_status,
|
|
1107
|
+
"plugin_commands": [shell_command(cmd) for cmd in plugin_commands],
|
|
1108
|
+
"admin_plugin_commands": [shell_command(cmd) for cmd in admin_plugin_commands],
|
|
1109
|
+
"planned_apply_commands": [shell_command(cmd) for cmd in planned_apply_commands],
|
|
1110
|
+
"reference_commands": [shell_command(cmd) for cmd in reference_commands],
|
|
1111
|
+
"direct_config": build_direct_config(agent_id, api_endpoint, api_key),
|
|
1112
|
+
"onboard_values": build_onboard_values(agent_id, api_endpoint, api_key),
|
|
1113
|
+
"environment_variables": build_channel_environment_variables(agent_id, api_endpoint, api_key),
|
|
1114
|
+
}
|
|
1115
|
+
if governance_ready:
|
|
1116
|
+
payload.update(
|
|
1117
|
+
build_portal_guidance(
|
|
1118
|
+
True,
|
|
1119
|
+
f"主通道和群管理能力已配置完成,可直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
1120
|
+
)
|
|
1121
|
+
)
|
|
1122
|
+
payload.update(build_user_reply_templates("already_configured"))
|
|
1123
|
+
elif main_ready:
|
|
1124
|
+
payload.update(
|
|
1125
|
+
build_portal_guidance(
|
|
1126
|
+
True,
|
|
1127
|
+
(
|
|
1128
|
+
f"主通道已配置完成,可直接登录 {DEFAULT_PORTAL_URL} 体验;"
|
|
1129
|
+
"如需群管理能力,还需安装启用 @dhfpub/clawpool-admin 并补齐 required tools 配置。"
|
|
1130
|
+
),
|
|
1131
|
+
)
|
|
1132
|
+
)
|
|
1133
|
+
payload.update(build_user_reply_templates("main_ready_admin_pending"))
|
|
1134
|
+
else:
|
|
1135
|
+
payload.update(build_portal_guidance(False, ""))
|
|
1136
|
+
payload.update(build_user_reply_templates("needs_setup"))
|
|
1137
|
+
payload.update(build_clawpool_explainer())
|
|
1138
|
+
if args.apply:
|
|
1139
|
+
command_results = []
|
|
1140
|
+
if plugin_commands:
|
|
1141
|
+
command_results.extend(run_commands(plugin_commands))
|
|
1142
|
+
if admin_plugin_commands:
|
|
1143
|
+
command_results.extend(run_commands(admin_plugin_commands))
|
|
1144
|
+
backup_path = ""
|
|
1145
|
+
if needs_update:
|
|
1146
|
+
backup_path = write_json_file_with_backup(config_path, next_cfg)
|
|
1147
|
+
payload["config_write"] = {
|
|
1148
|
+
"changed": needs_update,
|
|
1149
|
+
"backup_path": backup_path,
|
|
1150
|
+
}
|
|
1151
|
+
if restart_needed:
|
|
1152
|
+
command_results.extend(run_commands([build_gateway_restart_command(args)]))
|
|
1153
|
+
payload["command_results"] = command_results
|
|
1154
|
+
applied_cfg = load_json_file(config_path)
|
|
1155
|
+
applied_main = extract_main_clawpool_config(applied_cfg)
|
|
1156
|
+
applied_tools = extract_openclaw_tools_config(applied_cfg)
|
|
1157
|
+
applied_plugin_status = inspect_openclaw_plugin(args)
|
|
1158
|
+
applied_admin_plugin_status = inspect_openclaw_admin_plugin(args)
|
|
1159
|
+
applied_channel_checks = inspect_main_clawpool_channel(applied_main)
|
|
1160
|
+
applied_tools_checks = inspect_openclaw_tools_config(applied_cfg)
|
|
1161
|
+
payload["applied_state"] = {
|
|
1162
|
+
"plugin_status": applied_plugin_status,
|
|
1163
|
+
"admin_plugin_status": applied_admin_plugin_status,
|
|
1164
|
+
"main_channel_checks": applied_channel_checks,
|
|
1165
|
+
"tools_checks": applied_tools_checks,
|
|
1166
|
+
"current_main_channel": redact_current_main_channel(applied_main),
|
|
1167
|
+
"current_tools_config": applied_tools,
|
|
1168
|
+
}
|
|
1169
|
+
payload["ready_for_main_agent"] = ready_for_main_agent(applied_plugin_status, applied_channel_checks)
|
|
1170
|
+
payload["ready_for_group_governance"] = ready_for_group_governance(
|
|
1171
|
+
applied_plugin_status,
|
|
1172
|
+
applied_channel_checks,
|
|
1173
|
+
applied_admin_plugin_status,
|
|
1174
|
+
applied_tools_checks,
|
|
1175
|
+
)
|
|
1176
|
+
payload["setup_state"] = classify_gap_state(
|
|
1177
|
+
collect_inspection_gaps(
|
|
1178
|
+
applied_plugin_status,
|
|
1179
|
+
applied_channel_checks,
|
|
1180
|
+
applied_admin_plugin_status,
|
|
1181
|
+
applied_tools_checks,
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
1184
|
+
payload["recommended_next_steps"] = build_recommended_next_steps(
|
|
1185
|
+
collect_inspection_gaps(
|
|
1186
|
+
applied_plugin_status,
|
|
1187
|
+
applied_channel_checks,
|
|
1188
|
+
applied_admin_plugin_status,
|
|
1189
|
+
applied_tools_checks,
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
if payload["ready_for_group_governance"]:
|
|
1193
|
+
payload.update(
|
|
1194
|
+
build_portal_guidance(
|
|
1195
|
+
True,
|
|
1196
|
+
f"配置已完成,可直接登录 {DEFAULT_PORTAL_URL} 体验。",
|
|
1197
|
+
)
|
|
1198
|
+
)
|
|
1199
|
+
payload.update(build_user_reply_templates("configured_now"))
|
|
1200
|
+
elif payload["ready_for_main_agent"]:
|
|
1201
|
+
payload.update(
|
|
1202
|
+
build_portal_guidance(
|
|
1203
|
+
True,
|
|
1204
|
+
(
|
|
1205
|
+
f"主通道已完成配置,可直接登录 {DEFAULT_PORTAL_URL} 体验;"
|
|
1206
|
+
"如需群管理能力,还需继续补齐 @dhfpub/clawpool-admin 或 required tools 配置。"
|
|
1207
|
+
),
|
|
1208
|
+
)
|
|
1209
|
+
)
|
|
1210
|
+
payload.update(build_user_reply_templates("main_ready_admin_pending"))
|
|
1211
|
+
else:
|
|
1212
|
+
payload.update(build_portal_guidance(False, ""))
|
|
1213
|
+
payload.update(build_user_reply_templates("needs_setup"))
|
|
1214
|
+
return payload
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def handle_fetch_captcha(args):
|
|
1218
|
+
result = request_json("GET", "/auth/captcha", args.base_url)
|
|
1219
|
+
data = result.get("data") or {}
|
|
1220
|
+
image_path = maybe_write_captcha_image(str(data.get("b64s", "")))
|
|
1221
|
+
payload = {
|
|
1222
|
+
"ok": True,
|
|
1223
|
+
"action": "fetch-captcha",
|
|
1224
|
+
"api_base_url": result["api_base_url"],
|
|
1225
|
+
"captcha_id": data.get("captcha_id", ""),
|
|
1226
|
+
"b64s": data.get("b64s", ""),
|
|
1227
|
+
}
|
|
1228
|
+
if image_path:
|
|
1229
|
+
payload["captcha_image_path"] = image_path
|
|
1230
|
+
print_json(payload)
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def handle_send_email_code(args):
|
|
1234
|
+
result = request_json(
|
|
1235
|
+
"POST",
|
|
1236
|
+
"/auth/send-code",
|
|
1237
|
+
args.base_url,
|
|
1238
|
+
body={
|
|
1239
|
+
"email": args.email.strip(),
|
|
1240
|
+
"scene": args.scene.strip(),
|
|
1241
|
+
"captcha_id": args.captcha_id.strip(),
|
|
1242
|
+
"captcha_value": args.captcha_value.strip(),
|
|
1243
|
+
},
|
|
1244
|
+
)
|
|
1245
|
+
print_json(
|
|
1246
|
+
{
|
|
1247
|
+
"ok": True,
|
|
1248
|
+
"action": "send-email-code",
|
|
1249
|
+
"api_base_url": result["api_base_url"],
|
|
1250
|
+
"data": result.get("data"),
|
|
1251
|
+
}
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def default_device_id(platform: str) -> str:
|
|
1256
|
+
normalized_platform = (platform or "").strip() or "web"
|
|
1257
|
+
return f"{normalized_platform}_{uuid.uuid4()}"
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def handle_register(args):
|
|
1261
|
+
platform = (args.platform or "").strip() or "web"
|
|
1262
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
1263
|
+
result = request_json(
|
|
1264
|
+
"POST",
|
|
1265
|
+
"/auth/register",
|
|
1266
|
+
args.base_url,
|
|
1267
|
+
body={
|
|
1268
|
+
"email": args.email.strip(),
|
|
1269
|
+
"password": args.password.strip(),
|
|
1270
|
+
"email_code": args.email_code.strip(),
|
|
1271
|
+
"device_id": device_id,
|
|
1272
|
+
"platform": platform,
|
|
1273
|
+
},
|
|
1274
|
+
)
|
|
1275
|
+
print_json(build_auth_result("register", result))
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def handle_login(args):
|
|
1279
|
+
account = (args.email or args.account or "").strip()
|
|
1280
|
+
if not account:
|
|
1281
|
+
raise ClawpoolAuthError("either --email or --account is required")
|
|
1282
|
+
platform = (args.platform or "").strip() or "web"
|
|
1283
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
1284
|
+
print_json(
|
|
1285
|
+
login_with_credentials(
|
|
1286
|
+
args.base_url,
|
|
1287
|
+
account,
|
|
1288
|
+
args.password.strip(),
|
|
1289
|
+
device_id,
|
|
1290
|
+
platform,
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def handle_create_api_agent(args):
|
|
1296
|
+
print_json(
|
|
1297
|
+
create_or_reuse_api_agent(
|
|
1298
|
+
args.base_url,
|
|
1299
|
+
args.access_token.strip(),
|
|
1300
|
+
args.agent_name.strip(),
|
|
1301
|
+
args.avatar_url,
|
|
1302
|
+
not bool(args.no_reuse_existing_agent),
|
|
1303
|
+
not bool(args.no_rotate_key_on_reuse),
|
|
1304
|
+
)
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def handle_inspect_openclaw(args):
|
|
1309
|
+
print_json(build_openclaw_inspection_result(args))
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def handle_configure_openclaw(args):
|
|
1313
|
+
print_json(
|
|
1314
|
+
build_openclaw_setup_result(
|
|
1315
|
+
args,
|
|
1316
|
+
args.agent_id.strip(),
|
|
1317
|
+
args.api_endpoint.strip(),
|
|
1318
|
+
args.api_key.strip(),
|
|
1319
|
+
)
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def handle_bootstrap_openclaw(args):
|
|
1324
|
+
access_token = (args.access_token or "").strip()
|
|
1325
|
+
login_result = None
|
|
1326
|
+
if not access_token:
|
|
1327
|
+
account = (args.email or args.account or "").strip()
|
|
1328
|
+
if not account:
|
|
1329
|
+
raise ClawpoolAuthError("bootstrap-openclaw requires --access-token or login identity")
|
|
1330
|
+
if not (args.password or "").strip():
|
|
1331
|
+
raise ClawpoolAuthError("bootstrap-openclaw requires --password when access token is not provided")
|
|
1332
|
+
platform = (args.platform or "").strip() or "web"
|
|
1333
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
1334
|
+
login_result = login_with_credentials(
|
|
1335
|
+
args.base_url,
|
|
1336
|
+
account,
|
|
1337
|
+
args.password.strip(),
|
|
1338
|
+
device_id,
|
|
1339
|
+
platform,
|
|
1340
|
+
)
|
|
1341
|
+
access_token = str(login_result.get("access_token", "")).strip()
|
|
1342
|
+
if not access_token:
|
|
1343
|
+
raise ClawpoolAuthError("login did not return access_token")
|
|
1344
|
+
|
|
1345
|
+
create_result = create_or_reuse_api_agent(
|
|
1346
|
+
args.base_url,
|
|
1347
|
+
access_token,
|
|
1348
|
+
args.agent_name.strip(),
|
|
1349
|
+
args.avatar_url,
|
|
1350
|
+
not bool(args.no_reuse_existing_agent),
|
|
1351
|
+
not bool(args.no_rotate_key_on_reuse),
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
api_endpoint = str(create_result.get("api_endpoint", "")).strip()
|
|
1355
|
+
agent_id = str(create_result.get("agent_id", "")).strip()
|
|
1356
|
+
api_key = str(create_result.get("api_key", "")).strip()
|
|
1357
|
+
if not api_endpoint or not agent_id or not api_key:
|
|
1358
|
+
raise ClawpoolAuthError("create-api-agent did not return full Clawpool channel credentials")
|
|
1359
|
+
|
|
1360
|
+
payload = {
|
|
1361
|
+
"ok": True,
|
|
1362
|
+
"action": "bootstrap-openclaw",
|
|
1363
|
+
"used_access_token_source": "provided" if (args.access_token or "").strip() else "login",
|
|
1364
|
+
"login": login_result,
|
|
1365
|
+
"created_agent": create_result,
|
|
1366
|
+
"openclaw_setup": None,
|
|
1367
|
+
"channel_credentials": build_onboard_values(agent_id, api_endpoint, api_key),
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if not args.skip_openclaw_setup:
|
|
1371
|
+
payload["openclaw_setup"] = build_openclaw_setup_result(args, agent_id, api_endpoint, api_key)
|
|
1372
|
+
payload["bootstrap_state"] = payload["openclaw_setup"].get("setup_state", "")
|
|
1373
|
+
payload.update(
|
|
1374
|
+
build_portal_guidance(
|
|
1375
|
+
bool(payload["openclaw_setup"].get("portal_ready")),
|
|
1376
|
+
str(payload["openclaw_setup"].get("portal_hint", "")).strip(),
|
|
1377
|
+
)
|
|
1378
|
+
)
|
|
1379
|
+
payload.update(
|
|
1380
|
+
build_user_reply_templates(
|
|
1381
|
+
"already_configured"
|
|
1382
|
+
if bool(payload["openclaw_setup"].get("ready_for_group_governance"))
|
|
1383
|
+
and str(payload["bootstrap_state"]).strip() == "already_configured"
|
|
1384
|
+
else "configured_now"
|
|
1385
|
+
if bool(payload["openclaw_setup"].get("ready_for_group_governance"))
|
|
1386
|
+
else "main_ready_admin_pending"
|
|
1387
|
+
if bool(payload["openclaw_setup"].get("ready_for_main_agent"))
|
|
1388
|
+
else "needs_setup"
|
|
1389
|
+
)
|
|
1390
|
+
)
|
|
1391
|
+
else:
|
|
1392
|
+
payload["bootstrap_state"] = "agent_ready_openclaw_setup_skipped"
|
|
1393
|
+
payload.update(build_portal_guidance(False, ""))
|
|
1394
|
+
payload.update(build_user_reply_templates("login_ready"))
|
|
1395
|
+
payload.update(build_clawpool_explainer())
|
|
1396
|
+
|
|
1397
|
+
print_json(payload)
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
def build_parser():
|
|
1401
|
+
parser = argparse.ArgumentParser(description="ClawPool public auth API helper")
|
|
1402
|
+
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="ClawPool web base URL")
|
|
1403
|
+
|
|
1404
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
1405
|
+
|
|
1406
|
+
fetch_captcha = subparsers.add_parser("fetch-captcha", help="Fetch captcha image")
|
|
1407
|
+
fetch_captcha.set_defaults(handler=handle_fetch_captcha)
|
|
1408
|
+
|
|
1409
|
+
send_email_code = subparsers.add_parser("send-email-code", help="Send email verification code")
|
|
1410
|
+
send_email_code.add_argument("--email", required=True)
|
|
1411
|
+
send_email_code.add_argument("--scene", required=True, choices=["register", "reset", "change_password"])
|
|
1412
|
+
send_email_code.add_argument("--captcha-id", required=True)
|
|
1413
|
+
send_email_code.add_argument("--captcha-value", required=True)
|
|
1414
|
+
send_email_code.set_defaults(handler=handle_send_email_code)
|
|
1415
|
+
|
|
1416
|
+
register = subparsers.add_parser("register", help="Register by email verification code")
|
|
1417
|
+
register.add_argument("--email", required=True)
|
|
1418
|
+
register.add_argument("--password", required=True)
|
|
1419
|
+
register.add_argument("--email-code", required=True)
|
|
1420
|
+
register.add_argument("--device-id", default="")
|
|
1421
|
+
register.add_argument("--platform", default="web")
|
|
1422
|
+
register.set_defaults(handler=handle_register)
|
|
1423
|
+
|
|
1424
|
+
login = subparsers.add_parser("login", help="Login and obtain tokens")
|
|
1425
|
+
login_identity = login.add_mutually_exclusive_group(required=True)
|
|
1426
|
+
login_identity.add_argument("--account")
|
|
1427
|
+
login_identity.add_argument("--email")
|
|
1428
|
+
login.add_argument("--password", required=True)
|
|
1429
|
+
login.add_argument("--device-id", default="")
|
|
1430
|
+
login.add_argument("--platform", default="web")
|
|
1431
|
+
login.set_defaults(handler=handle_login)
|
|
1432
|
+
|
|
1433
|
+
create_api_agent_parser = subparsers.add_parser(
|
|
1434
|
+
"create-api-agent",
|
|
1435
|
+
help="Create a provider_type=3 API agent with a user access token",
|
|
1436
|
+
)
|
|
1437
|
+
create_api_agent_parser.add_argument("--access-token", required=True)
|
|
1438
|
+
create_api_agent_parser.add_argument("--agent-name", required=True)
|
|
1439
|
+
create_api_agent_parser.add_argument("--avatar-url", default="")
|
|
1440
|
+
create_api_agent_parser.add_argument("--no-reuse-existing-agent", action="store_true")
|
|
1441
|
+
create_api_agent_parser.add_argument("--no-rotate-key-on-reuse", action="store_true")
|
|
1442
|
+
create_api_agent_parser.set_defaults(handler=handle_create_api_agent)
|
|
1443
|
+
|
|
1444
|
+
inspect_openclaw = subparsers.add_parser(
|
|
1445
|
+
"inspect-openclaw",
|
|
1446
|
+
help="Inspect local OpenClaw clawpool readiness without mutating local state",
|
|
1447
|
+
)
|
|
1448
|
+
inspect_openclaw.add_argument("--openclaw-bin", default="openclaw")
|
|
1449
|
+
inspect_openclaw.add_argument("--openclaw-profile", default="")
|
|
1450
|
+
inspect_openclaw.add_argument("--config-path", default=DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
1451
|
+
inspect_openclaw.add_argument("--skip-plugin-install", action="store_true")
|
|
1452
|
+
inspect_openclaw.add_argument("--skip-plugin-enable", action="store_true")
|
|
1453
|
+
inspect_openclaw.add_argument("--skip-admin-plugin-install", action="store_true")
|
|
1454
|
+
inspect_openclaw.add_argument("--skip-admin-plugin-enable", action="store_true")
|
|
1455
|
+
inspect_openclaw.set_defaults(handler=handle_inspect_openclaw)
|
|
1456
|
+
|
|
1457
|
+
configure_openclaw = subparsers.add_parser(
|
|
1458
|
+
"configure-openclaw",
|
|
1459
|
+
help="Prepare or apply local OpenClaw clawpool channel setup",
|
|
1460
|
+
)
|
|
1461
|
+
configure_openclaw.add_argument("--agent-id", required=True)
|
|
1462
|
+
configure_openclaw.add_argument("--api-endpoint", required=True)
|
|
1463
|
+
configure_openclaw.add_argument("--api-key", required=True)
|
|
1464
|
+
configure_openclaw.add_argument("--channel-name", default="clawpool-main")
|
|
1465
|
+
configure_openclaw.add_argument("--openclaw-bin", default="openclaw")
|
|
1466
|
+
configure_openclaw.add_argument("--openclaw-profile", default="")
|
|
1467
|
+
configure_openclaw.add_argument("--config-path", default=DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
1468
|
+
configure_openclaw.add_argument("--skip-plugin-install", action="store_true")
|
|
1469
|
+
configure_openclaw.add_argument("--skip-plugin-enable", action="store_true")
|
|
1470
|
+
configure_openclaw.add_argument("--skip-admin-plugin-install", action="store_true")
|
|
1471
|
+
configure_openclaw.add_argument("--skip-admin-plugin-enable", action="store_true")
|
|
1472
|
+
configure_openclaw.add_argument("--skip-gateway-restart", action="store_true")
|
|
1473
|
+
configure_openclaw.add_argument("--apply", action="store_true")
|
|
1474
|
+
configure_openclaw.set_defaults(handler=handle_configure_openclaw)
|
|
1475
|
+
|
|
1476
|
+
bootstrap_openclaw = subparsers.add_parser(
|
|
1477
|
+
"bootstrap-openclaw",
|
|
1478
|
+
help="Login if needed, create provider_type=3 agent, then prepare or apply OpenClaw setup",
|
|
1479
|
+
)
|
|
1480
|
+
bootstrap_openclaw.add_argument("--access-token", default="")
|
|
1481
|
+
bootstrap_identity = bootstrap_openclaw.add_mutually_exclusive_group(required=False)
|
|
1482
|
+
bootstrap_identity.add_argument("--account")
|
|
1483
|
+
bootstrap_identity.add_argument("--email")
|
|
1484
|
+
bootstrap_openclaw.add_argument("--password", default="")
|
|
1485
|
+
bootstrap_openclaw.add_argument("--device-id", default="")
|
|
1486
|
+
bootstrap_openclaw.add_argument("--platform", default="web")
|
|
1487
|
+
bootstrap_openclaw.add_argument("--agent-name", required=True)
|
|
1488
|
+
bootstrap_openclaw.add_argument("--avatar-url", default="")
|
|
1489
|
+
bootstrap_openclaw.add_argument("--channel-name", default="clawpool-main")
|
|
1490
|
+
bootstrap_openclaw.add_argument("--openclaw-bin", default="openclaw")
|
|
1491
|
+
bootstrap_openclaw.add_argument("--openclaw-profile", default="")
|
|
1492
|
+
bootstrap_openclaw.add_argument("--config-path", default=DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
1493
|
+
bootstrap_openclaw.add_argument("--no-reuse-existing-agent", action="store_true")
|
|
1494
|
+
bootstrap_openclaw.add_argument("--no-rotate-key-on-reuse", action="store_true")
|
|
1495
|
+
bootstrap_openclaw.add_argument("--skip-plugin-install", action="store_true")
|
|
1496
|
+
bootstrap_openclaw.add_argument("--skip-plugin-enable", action="store_true")
|
|
1497
|
+
bootstrap_openclaw.add_argument("--skip-admin-plugin-install", action="store_true")
|
|
1498
|
+
bootstrap_openclaw.add_argument("--skip-admin-plugin-enable", action="store_true")
|
|
1499
|
+
bootstrap_openclaw.add_argument("--skip-gateway-restart", action="store_true")
|
|
1500
|
+
bootstrap_openclaw.add_argument("--skip-openclaw-setup", action="store_true")
|
|
1501
|
+
bootstrap_openclaw.add_argument("--apply", action="store_true")
|
|
1502
|
+
bootstrap_openclaw.set_defaults(handler=handle_bootstrap_openclaw)
|
|
1503
|
+
|
|
1504
|
+
return parser
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def main():
|
|
1508
|
+
parser = build_parser()
|
|
1509
|
+
args = parser.parse_args()
|
|
1510
|
+
try:
|
|
1511
|
+
args.handler(args)
|
|
1512
|
+
except ClawpoolAuthError as exc:
|
|
1513
|
+
print_json(
|
|
1514
|
+
{
|
|
1515
|
+
"ok": False,
|
|
1516
|
+
"action": args.action,
|
|
1517
|
+
"status": exc.status,
|
|
1518
|
+
"code": exc.code,
|
|
1519
|
+
"error": str(exc),
|
|
1520
|
+
"payload": exc.payload,
|
|
1521
|
+
}
|
|
1522
|
+
)
|
|
1523
|
+
raise SystemExit(1)
|
|
1524
|
+
except Exception as exc:
|
|
1525
|
+
print_json(
|
|
1526
|
+
{
|
|
1527
|
+
"ok": False,
|
|
1528
|
+
"action": args.action,
|
|
1529
|
+
"status": 0,
|
|
1530
|
+
"code": -1,
|
|
1531
|
+
"error": str(exc),
|
|
1532
|
+
}
|
|
1533
|
+
)
|
|
1534
|
+
raise SystemExit(1)
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
if __name__ == "__main__":
|
|
1538
|
+
main()
|