@dhf-openclaw/grix 0.4.9 → 0.4.11
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 +137 -191
- package/dist/index.js +1279 -1
- package/package.json +4 -6
- package/skills/grix-admin/SKILL.md +81 -0
- package/skills/grix-admin/references/api-contract.md +106 -0
- package/skills/grix-admin/scripts/grix_agent_bind.py +587 -0
- package/skills/grix-egg/SKILL.md +170 -0
- package/skills/grix-egg/references/api-contract.md +38 -0
- package/skills/grix-group/SKILL.md +144 -0
- package/skills/grix-group/references/api-contract.md +73 -0
- package/skills/grix-query/SKILL.md +151 -0
- package/skills/grix-register/SKILL.md +80 -0
- package/skills/grix-register/references/api-contract.md +71 -0
- package/skills/grix-register/references/grix-concepts.md +26 -0
- package/skills/grix-register/references/handoff-contract.md +24 -0
- package/skills/grix-register/references/openclaw-setup.md +6 -0
- package/skills/grix-register/references/user-replies.md +25 -0
- package/skills/grix-register/scripts/grix_auth.py +453 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# API Contract
|
|
2
|
+
|
|
3
|
+
## Responsibility Boundary
|
|
4
|
+
|
|
5
|
+
1. `grix-register` 仅负责账号鉴权与云端 `provider_type=3` Agent 参数产出。
|
|
6
|
+
2. 本技能不负责本地 OpenClaw 配置。
|
|
7
|
+
3. 本地配置由 `grix-admin` 接手。
|
|
8
|
+
|
|
9
|
+
## Base
|
|
10
|
+
|
|
11
|
+
1. Website: `https://grix.dhf.pub/`
|
|
12
|
+
2. Public Grix API base: `https://grix.dhf.pub/v1`
|
|
13
|
+
|
|
14
|
+
## Route Mapping
|
|
15
|
+
|
|
16
|
+
### Agent bootstrap action
|
|
17
|
+
|
|
18
|
+
| Action | Method | Route | Auth |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `create-api-agent` | `POST` | `/agents/create` | `Authorization: Bearer <access_token>` |
|
|
21
|
+
| `list-agents` (internal helper) | `GET` | `/agents/list` | `Authorization: Bearer <access_token>` |
|
|
22
|
+
| `rotate-api-agent-key` (internal helper) | `POST` | `/agents/:id/api/key/rotate` | `Authorization: Bearer <access_token>` |
|
|
23
|
+
|
|
24
|
+
## Payloads
|
|
25
|
+
|
|
26
|
+
### `create-api-agent`
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"agent_name": "grix-main",
|
|
31
|
+
"provider_type": 3
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`provider_type=3` means Agent API type.
|
|
36
|
+
|
|
37
|
+
## Reuse flow
|
|
38
|
+
|
|
39
|
+
When the same-name `provider_type=3` agent already exists, the skill should:
|
|
40
|
+
|
|
41
|
+
1. read `/agents/list`
|
|
42
|
+
2. find the exact-name API agent
|
|
43
|
+
3. rotate its key through `/agents/:id/api/key/rotate`
|
|
44
|
+
4. reuse the returned `api_endpoint` and fresh `api_key`
|
|
45
|
+
|
|
46
|
+
## Success Highlights
|
|
47
|
+
|
|
48
|
+
### `create-api-agent`
|
|
49
|
+
|
|
50
|
+
The bundled script lifts these fields to the top level:
|
|
51
|
+
|
|
52
|
+
1. `agent_id`
|
|
53
|
+
2. `agent_name`
|
|
54
|
+
3. `provider_type`
|
|
55
|
+
4. `api_endpoint`
|
|
56
|
+
5. `api_key`
|
|
57
|
+
6. `api_key_hint`
|
|
58
|
+
7. `session_id`
|
|
59
|
+
|
|
60
|
+
## Common Errors
|
|
61
|
+
|
|
62
|
+
1. create-agent or rotate-key returns missing `api_endpoint` or `api_key`
|
|
63
|
+
|
|
64
|
+
## Handoff
|
|
65
|
+
|
|
66
|
+
成功后输出这些字段,直接交给 `grix-admin`:
|
|
67
|
+
|
|
68
|
+
1. `agent_id`
|
|
69
|
+
2. `agent_name`
|
|
70
|
+
3. `api_endpoint`
|
|
71
|
+
4. `api_key`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Grix Concepts
|
|
2
|
+
|
|
3
|
+
## Canonical Explanation
|
|
4
|
+
|
|
5
|
+
这个插件接入是为了在 `https://grix.dhf.pub/` 网站管理 OpenClaw,并支持移动端 PWA 页面。
|
|
6
|
+
|
|
7
|
+
## Feature Highlights
|
|
8
|
+
|
|
9
|
+
1. `grix-register` 负责初次账号准备与首个 agent 参数生成
|
|
10
|
+
2. `grix-admin` 负责 OpenClaw 本地配置与后续管理
|
|
11
|
+
3. 两者串联后,用户可在 `https://grix.dhf.pub/` 使用和管理
|
|
12
|
+
|
|
13
|
+
## Default User-Facing Framing
|
|
14
|
+
|
|
15
|
+
### One sentence
|
|
16
|
+
|
|
17
|
+
`grix-register` 只做“注册账号并拿到第一个 agent 参数”,本地配置统一交给 `grix-admin`。
|
|
18
|
+
|
|
19
|
+
### Short paragraph
|
|
20
|
+
|
|
21
|
+
`grix-register` 只负责初次安装中的云端准备:注册/登录账号并生成第一个 `provider_type=3` agent 参数;随后必须把参数交给 `grix-admin`,由 `grix-admin` 负责本地 OpenClaw 配置。
|
|
22
|
+
|
|
23
|
+
## After Setup
|
|
24
|
+
|
|
25
|
+
1. `grix-register` 产出参数后,直接交接给 `grix-admin`。
|
|
26
|
+
2. `grix-register` 不执行任何本地配置动作。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Handoff Contract to grix-admin
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`grix-register` 完成账号与首个 Agent 参数准备后,统一把本地配置工作交给 `grix-admin`。
|
|
6
|
+
|
|
7
|
+
## Required Payload
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mode": "bind-local",
|
|
12
|
+
"agent_name": "grix-main",
|
|
13
|
+
"agent_id": "2029786829095440384",
|
|
14
|
+
"api_endpoint": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=2029786829095440384",
|
|
15
|
+
"api_key": "ak_xxx"
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
1. `mode` 固定为 `bind-local`。
|
|
22
|
+
2. `agent_name`、`agent_id`、`api_endpoint`、`api_key` 必填。
|
|
23
|
+
3. `grix-register` 只负责生成以上参数,不执行本地配置命令。
|
|
24
|
+
4. 本地写入、插件处理、工具权限、gateway 重启都由 `grix-admin` 负责。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# User Replies
|
|
2
|
+
|
|
3
|
+
## One-liner Pitch
|
|
4
|
+
|
|
5
|
+
我会先帮你完成注册并拿到第一个 agent 参数,然后交给 `grix-admin` 完成本地配置。
|
|
6
|
+
|
|
7
|
+
## Short Intro
|
|
8
|
+
|
|
9
|
+
`grix-register` 只负责账号和云端参数准备,不改本地配置;本地配置由 `grix-admin` 接手。
|
|
10
|
+
|
|
11
|
+
## Ready Reply
|
|
12
|
+
|
|
13
|
+
账号和首个 agent 参数已经准备好,接下来我会把参数交给 `grix-admin` 做本地配置。
|
|
14
|
+
|
|
15
|
+
## Main Ready, Admin Pending Reply
|
|
16
|
+
|
|
17
|
+
`grix-register` 阶段已完成,我现在只做参数移交,后续本地配置请由 `grix-admin` 继续。
|
|
18
|
+
|
|
19
|
+
## Configured Now Reply
|
|
20
|
+
|
|
21
|
+
参数已交接给 `grix-admin`,接下来由它完成本地配置。
|
|
22
|
+
|
|
23
|
+
## Needs Setup Reply
|
|
24
|
+
|
|
25
|
+
当前仍在 `grix-register` 阶段,我会继续完成注册并拿到第一个 agent 参数。
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://grix.dhf.pub"
|
|
15
|
+
DEFAULT_TIMEOUT_SECONDS = 15
|
|
16
|
+
DEFAULT_PORTAL_URL = "https://grix.dhf.pub/"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GrixAuthError(RuntimeError):
|
|
20
|
+
def __init__(self, message, status=0, code=-1, payload=None):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.status = status
|
|
23
|
+
self.code = code
|
|
24
|
+
self.payload = payload
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_base_url(raw_base_url: str) -> str:
|
|
28
|
+
base = (raw_base_url or "").strip() or DEFAULT_BASE_URL
|
|
29
|
+
parsed = urllib.parse.urlparse(base)
|
|
30
|
+
if not parsed.scheme or not parsed.netloc:
|
|
31
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
32
|
+
|
|
33
|
+
path = parsed.path.rstrip("/")
|
|
34
|
+
if not path:
|
|
35
|
+
path = "/v1"
|
|
36
|
+
elif not path.endswith("/v1"):
|
|
37
|
+
path = f"{path}/v1"
|
|
38
|
+
|
|
39
|
+
normalized = parsed._replace(path=path, params="", query="", fragment="")
|
|
40
|
+
return urllib.parse.urlunparse(normalized).rstrip("/")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def request_json(method: str, path: str, base_url: str, body=None, headers=None):
|
|
44
|
+
api_base_url = normalize_base_url(base_url)
|
|
45
|
+
url = f"{api_base_url}{path if path.startswith('/') else '/' + path}"
|
|
46
|
+
data = None
|
|
47
|
+
final_headers = dict(headers or {})
|
|
48
|
+
if body is not None:
|
|
49
|
+
data = json.dumps(body).encode("utf-8")
|
|
50
|
+
final_headers["Content-Type"] = "application/json"
|
|
51
|
+
|
|
52
|
+
req = urllib.request.Request(url=url, data=data, headers=final_headers, method=method)
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp:
|
|
55
|
+
status = getattr(resp, "status", 200)
|
|
56
|
+
raw = resp.read().decode("utf-8")
|
|
57
|
+
except urllib.error.HTTPError as exc:
|
|
58
|
+
status = exc.code
|
|
59
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
60
|
+
except urllib.error.URLError as exc:
|
|
61
|
+
raise GrixAuthError(f"network error: {exc.reason}") from exc
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
payload = json.loads(raw)
|
|
65
|
+
except json.JSONDecodeError as exc:
|
|
66
|
+
raise GrixAuthError(f"invalid json response: {raw[:256]}", status=status) from exc
|
|
67
|
+
|
|
68
|
+
code = int(payload.get("code", -1))
|
|
69
|
+
msg = str(payload.get("msg", "")).strip() or "unknown error"
|
|
70
|
+
if status >= 400 or code != 0:
|
|
71
|
+
raise GrixAuthError(msg, status=status, code=code, payload=payload)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"api_base_url": api_base_url,
|
|
75
|
+
"status": status,
|
|
76
|
+
"data": payload.get("data"),
|
|
77
|
+
"payload": payload,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def maybe_write_captcha_image(b64s: str):
|
|
82
|
+
text = (b64s or "").strip()
|
|
83
|
+
if not text.startswith("data:image/"):
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
marker = ";base64,"
|
|
87
|
+
idx = text.find(marker)
|
|
88
|
+
if idx < 0:
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
encoded = text[idx + len(marker) :]
|
|
92
|
+
try:
|
|
93
|
+
content = base64.b64decode(encoded)
|
|
94
|
+
except Exception:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
fd, path = tempfile.mkstemp(prefix="grix-captcha-", suffix=".png")
|
|
98
|
+
try:
|
|
99
|
+
with os.fdopen(fd, "wb") as handle:
|
|
100
|
+
handle.write(content)
|
|
101
|
+
except Exception:
|
|
102
|
+
try:
|
|
103
|
+
os.unlink(path)
|
|
104
|
+
except OSError:
|
|
105
|
+
pass
|
|
106
|
+
return ""
|
|
107
|
+
return path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def print_json(payload):
|
|
111
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
112
|
+
sys.stdout.write("\n")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_auth_result(action: str, result: dict):
|
|
116
|
+
data = result.get("data") or {}
|
|
117
|
+
user = data.get("user") or {}
|
|
118
|
+
return {
|
|
119
|
+
"ok": True,
|
|
120
|
+
"action": action,
|
|
121
|
+
"api_base_url": result["api_base_url"],
|
|
122
|
+
"access_token": data.get("access_token", ""),
|
|
123
|
+
"refresh_token": data.get("refresh_token", ""),
|
|
124
|
+
"expires_in": data.get("expires_in", 0),
|
|
125
|
+
"user_id": user.get("id", ""),
|
|
126
|
+
"portal_url": DEFAULT_PORTAL_URL,
|
|
127
|
+
"data": data,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_agent_result(action: str, result: dict):
|
|
132
|
+
data = result.get("data") or {}
|
|
133
|
+
agent_id = str(data.get("id", "")).strip()
|
|
134
|
+
api_endpoint = str(data.get("api_endpoint", "")).strip()
|
|
135
|
+
api_key = str(data.get("api_key", "")).strip()
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"ok": True,
|
|
139
|
+
"action": action,
|
|
140
|
+
"api_base_url": result["api_base_url"],
|
|
141
|
+
"agent_id": agent_id,
|
|
142
|
+
"agent_name": data.get("agent_name", ""),
|
|
143
|
+
"provider_type": data.get("provider_type", 0),
|
|
144
|
+
"api_endpoint": api_endpoint,
|
|
145
|
+
"api_key": api_key,
|
|
146
|
+
"api_key_hint": data.get("api_key_hint", ""),
|
|
147
|
+
"session_id": data.get("session_id", ""),
|
|
148
|
+
"handoff": {
|
|
149
|
+
"target_skill": "grix-admin",
|
|
150
|
+
"payload": {
|
|
151
|
+
"agent_id": agent_id,
|
|
152
|
+
"agent_name": data.get("agent_name", ""),
|
|
153
|
+
"api_endpoint": api_endpoint,
|
|
154
|
+
"api_key": api_key,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
"data": data,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def login_with_credentials(base_url: str, account: str, password: str, device_id: str, platform: str):
|
|
162
|
+
result = request_json(
|
|
163
|
+
"POST",
|
|
164
|
+
"/auth/login",
|
|
165
|
+
base_url,
|
|
166
|
+
body={
|
|
167
|
+
"account": account,
|
|
168
|
+
"password": password,
|
|
169
|
+
"device_id": device_id,
|
|
170
|
+
"platform": platform,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
return build_auth_result("login", result)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def create_api_agent(base_url: str, access_token: str, agent_name: str, avatar_url: str):
|
|
177
|
+
request_body = {
|
|
178
|
+
"agent_name": agent_name.strip(),
|
|
179
|
+
"provider_type": 3,
|
|
180
|
+
"is_main": True,
|
|
181
|
+
}
|
|
182
|
+
normalized_avatar_url = (avatar_url or "").strip()
|
|
183
|
+
if normalized_avatar_url:
|
|
184
|
+
request_body["avatar_url"] = normalized_avatar_url
|
|
185
|
+
|
|
186
|
+
result = request_json(
|
|
187
|
+
"POST",
|
|
188
|
+
"/agents/create",
|
|
189
|
+
base_url,
|
|
190
|
+
body=request_body,
|
|
191
|
+
headers={
|
|
192
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
return build_agent_result("create-api-agent", result)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def list_agents(base_url: str, access_token: str):
|
|
199
|
+
result = request_json(
|
|
200
|
+
"GET",
|
|
201
|
+
"/agents/list",
|
|
202
|
+
base_url,
|
|
203
|
+
headers={
|
|
204
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
data = result.get("data") or {}
|
|
208
|
+
items = data.get("list") or []
|
|
209
|
+
if not isinstance(items, list):
|
|
210
|
+
items = []
|
|
211
|
+
return items
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def rotate_api_agent_key(base_url: str, access_token: str, agent_id: str):
|
|
215
|
+
result = request_json(
|
|
216
|
+
"POST",
|
|
217
|
+
f"/agents/{str(agent_id).strip()}/api/key/rotate",
|
|
218
|
+
base_url,
|
|
219
|
+
body={},
|
|
220
|
+
headers={
|
|
221
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
return build_agent_result("rotate-api-agent-key", result)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def find_existing_api_agent(agents, agent_name: str):
|
|
228
|
+
normalized_name = (agent_name or "").strip()
|
|
229
|
+
if not normalized_name:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
for item in agents:
|
|
233
|
+
if not isinstance(item, dict):
|
|
234
|
+
continue
|
|
235
|
+
if str(item.get("agent_name", "")).strip() != normalized_name:
|
|
236
|
+
continue
|
|
237
|
+
if int(item.get("provider_type", 0) or 0) != 3:
|
|
238
|
+
continue
|
|
239
|
+
if int(item.get("status", 0) or 0) == 3:
|
|
240
|
+
continue
|
|
241
|
+
return item
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def create_or_reuse_api_agent(
|
|
246
|
+
base_url: str,
|
|
247
|
+
access_token: str,
|
|
248
|
+
agent_name: str,
|
|
249
|
+
avatar_url: str,
|
|
250
|
+
prefer_existing: bool,
|
|
251
|
+
rotate_on_reuse: bool,
|
|
252
|
+
):
|
|
253
|
+
if prefer_existing:
|
|
254
|
+
agents = list_agents(base_url, access_token)
|
|
255
|
+
existing = find_existing_api_agent(agents, agent_name)
|
|
256
|
+
if existing is not None:
|
|
257
|
+
if not rotate_on_reuse:
|
|
258
|
+
raise GrixAuthError(
|
|
259
|
+
"existing provider_type=3 agent found but rotate-on-reuse is disabled; cannot obtain api_key safely",
|
|
260
|
+
payload={"existing_agent": existing},
|
|
261
|
+
)
|
|
262
|
+
rotated = rotate_api_agent_key(base_url, access_token, str(existing.get("id", "")).strip())
|
|
263
|
+
rotated["source"] = "reused_existing_agent_with_rotated_key"
|
|
264
|
+
rotated["existing_agent"] = existing
|
|
265
|
+
return rotated
|
|
266
|
+
|
|
267
|
+
created = create_api_agent(base_url, access_token, agent_name, avatar_url)
|
|
268
|
+
created["source"] = "created_new_agent"
|
|
269
|
+
return created
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def default_device_id(platform: str) -> str:
|
|
273
|
+
normalized_platform = (platform or "").strip() or "web"
|
|
274
|
+
return f"{normalized_platform}_{uuid.uuid4()}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def handle_fetch_captcha(args):
|
|
278
|
+
result = request_json("GET", "/auth/captcha", args.base_url)
|
|
279
|
+
data = result.get("data") or {}
|
|
280
|
+
image_path = maybe_write_captcha_image(str(data.get("b64s", "")))
|
|
281
|
+
payload = {
|
|
282
|
+
"ok": True,
|
|
283
|
+
"action": "fetch-captcha",
|
|
284
|
+
"api_base_url": result["api_base_url"],
|
|
285
|
+
"captcha_id": data.get("captcha_id", ""),
|
|
286
|
+
"b64s": data.get("b64s", ""),
|
|
287
|
+
}
|
|
288
|
+
if image_path:
|
|
289
|
+
payload["captcha_image_path"] = image_path
|
|
290
|
+
print_json(payload)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def handle_send_email_code(args):
|
|
294
|
+
scene = args.scene.strip()
|
|
295
|
+
payload = {
|
|
296
|
+
"email": args.email.strip(),
|
|
297
|
+
"scene": scene,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
captcha_id = (args.captcha_id or "").strip()
|
|
301
|
+
captcha_value = (args.captcha_value or "").strip()
|
|
302
|
+
if scene in {"reset", "change_password"}:
|
|
303
|
+
if not captcha_id or not captcha_value:
|
|
304
|
+
raise GrixAuthError("captcha_id and captcha_value are required for reset/change_password")
|
|
305
|
+
if captcha_id:
|
|
306
|
+
payload["captcha_id"] = captcha_id
|
|
307
|
+
if captcha_value:
|
|
308
|
+
payload["captcha_value"] = captcha_value
|
|
309
|
+
|
|
310
|
+
result = request_json(
|
|
311
|
+
"POST",
|
|
312
|
+
"/auth/send-code",
|
|
313
|
+
args.base_url,
|
|
314
|
+
body=payload,
|
|
315
|
+
)
|
|
316
|
+
print_json(
|
|
317
|
+
{
|
|
318
|
+
"ok": True,
|
|
319
|
+
"action": "send-email-code",
|
|
320
|
+
"api_base_url": result["api_base_url"],
|
|
321
|
+
"data": result.get("data"),
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def handle_register(args):
|
|
327
|
+
platform = (args.platform or "").strip() or "web"
|
|
328
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
329
|
+
result = request_json(
|
|
330
|
+
"POST",
|
|
331
|
+
"/auth/register",
|
|
332
|
+
args.base_url,
|
|
333
|
+
body={
|
|
334
|
+
"email": args.email.strip(),
|
|
335
|
+
"password": args.password.strip(),
|
|
336
|
+
"email_code": args.email_code.strip(),
|
|
337
|
+
"device_id": device_id,
|
|
338
|
+
"platform": platform,
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
print_json(build_auth_result("register", result))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def handle_login(args):
|
|
345
|
+
account = (args.email or args.account or "").strip()
|
|
346
|
+
if not account:
|
|
347
|
+
raise GrixAuthError("either --email or --account is required")
|
|
348
|
+
|
|
349
|
+
platform = (args.platform or "").strip() or "web"
|
|
350
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
351
|
+
print_json(
|
|
352
|
+
login_with_credentials(
|
|
353
|
+
args.base_url,
|
|
354
|
+
account,
|
|
355
|
+
args.password.strip(),
|
|
356
|
+
device_id,
|
|
357
|
+
platform,
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def handle_create_api_agent(args):
|
|
363
|
+
print_json(
|
|
364
|
+
create_or_reuse_api_agent(
|
|
365
|
+
args.base_url,
|
|
366
|
+
args.access_token.strip(),
|
|
367
|
+
args.agent_name.strip(),
|
|
368
|
+
args.avatar_url,
|
|
369
|
+
not bool(args.no_reuse_existing_agent),
|
|
370
|
+
not bool(args.no_rotate_key_on_reuse),
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def build_parser():
|
|
376
|
+
parser = argparse.ArgumentParser(description="Grix public auth API helper")
|
|
377
|
+
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Grix web base URL")
|
|
378
|
+
|
|
379
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
380
|
+
|
|
381
|
+
fetch_captcha = subparsers.add_parser("fetch-captcha", help="Fetch captcha image")
|
|
382
|
+
fetch_captcha.set_defaults(handler=handle_fetch_captcha)
|
|
383
|
+
|
|
384
|
+
send_email_code = subparsers.add_parser("send-email-code", help="Send email verification code")
|
|
385
|
+
send_email_code.add_argument("--email", required=True)
|
|
386
|
+
send_email_code.add_argument("--scene", required=True, choices=["register", "reset", "change_password"])
|
|
387
|
+
send_email_code.add_argument("--captcha-id", default="")
|
|
388
|
+
send_email_code.add_argument("--captcha-value", default="")
|
|
389
|
+
send_email_code.set_defaults(handler=handle_send_email_code)
|
|
390
|
+
|
|
391
|
+
register = subparsers.add_parser("register", help="Register by email verification code")
|
|
392
|
+
register.add_argument("--email", required=True)
|
|
393
|
+
register.add_argument("--password", required=True)
|
|
394
|
+
register.add_argument("--email-code", required=True)
|
|
395
|
+
register.add_argument("--device-id", default="")
|
|
396
|
+
register.add_argument("--platform", default="web")
|
|
397
|
+
register.set_defaults(handler=handle_register)
|
|
398
|
+
|
|
399
|
+
login = subparsers.add_parser("login", help="Login and obtain tokens")
|
|
400
|
+
login_identity = login.add_mutually_exclusive_group(required=True)
|
|
401
|
+
login_identity.add_argument("--account")
|
|
402
|
+
login_identity.add_argument("--email")
|
|
403
|
+
login.add_argument("--password", required=True)
|
|
404
|
+
login.add_argument("--device-id", default="")
|
|
405
|
+
login.add_argument("--platform", default="web")
|
|
406
|
+
login.set_defaults(handler=handle_login)
|
|
407
|
+
|
|
408
|
+
create_api_agent_parser = subparsers.add_parser(
|
|
409
|
+
"create-api-agent",
|
|
410
|
+
help="Create a provider_type=3 API agent with a user access token",
|
|
411
|
+
)
|
|
412
|
+
create_api_agent_parser.add_argument("--access-token", required=True)
|
|
413
|
+
create_api_agent_parser.add_argument("--agent-name", required=True)
|
|
414
|
+
create_api_agent_parser.add_argument("--avatar-url", default="")
|
|
415
|
+
create_api_agent_parser.add_argument("--no-reuse-existing-agent", action="store_true")
|
|
416
|
+
create_api_agent_parser.add_argument("--no-rotate-key-on-reuse", action="store_true")
|
|
417
|
+
create_api_agent_parser.set_defaults(handler=handle_create_api_agent)
|
|
418
|
+
|
|
419
|
+
return parser
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def main():
|
|
423
|
+
parser = build_parser()
|
|
424
|
+
args = parser.parse_args()
|
|
425
|
+
try:
|
|
426
|
+
args.handler(args)
|
|
427
|
+
except GrixAuthError as exc:
|
|
428
|
+
print_json(
|
|
429
|
+
{
|
|
430
|
+
"ok": False,
|
|
431
|
+
"action": args.action,
|
|
432
|
+
"status": exc.status,
|
|
433
|
+
"code": exc.code,
|
|
434
|
+
"error": str(exc),
|
|
435
|
+
"payload": exc.payload,
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
raise SystemExit(1)
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
print_json(
|
|
441
|
+
{
|
|
442
|
+
"ok": False,
|
|
443
|
+
"action": args.action,
|
|
444
|
+
"status": 0,
|
|
445
|
+
"code": -1,
|
|
446
|
+
"error": str(exc),
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
raise SystemExit(1)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
main()
|