@dhf-openclaw/grix 0.4.10 → 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 +136 -175
- package/package.json +1 -1
- package/skills/grix-admin/SKILL.md +81 -0
- package/skills/{grix-agent-admin → grix-admin}/references/api-contract.md +22 -3
- package/skills/{egg-install → grix-egg}/SKILL.md +1 -1
- package/skills/{grix-group-governance → grix-group}/SKILL.md +1 -1
- 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
- package/skills/grix-agent-admin/SKILL.md +0 -85
- package/skills/grix-agent-admin/agents/openai.yaml +0 -4
- package/skills/grix-group-governance/agents/openai.yaml +0 -4
- package/skills/grix-query/agents/openai.yaml +0 -4
- /package/skills/{grix-agent-admin → grix-admin}/scripts/grix_agent_bind.py +0 -0
- /package/skills/{egg-install → grix-egg}/references/api-contract.md +0 -0
- /package/skills/{grix-group-governance → grix-group}/references/api-contract.md +0 -0
package/README.md
CHANGED
|
@@ -1,75 +1,46 @@
|
|
|
1
|
-
# OpenClaw Grix
|
|
1
|
+
# OpenClaw Grix 插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 OpenClaw 接到 Grix(`grix.dhf.pub`)的统一插件,包含:
|
|
4
4
|
|
|
5
|
-
- Grix
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- bundled Grix workflow skills
|
|
5
|
+
- Grix Channel(收发消息、流式回复、`unsend`、`delete`)
|
|
6
|
+
- 管理工具:`grix_query`、`grix_group`、`grix_agent_admin`
|
|
7
|
+
- 运维命令:`openclaw grix doctor`、`openclaw grix create-agent`
|
|
8
|
+
- 内置技能包(`skills/`)
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
## 兼容性
|
|
12
11
|
|
|
13
|
-
-
|
|
12
|
+
- `OpenClaw >= 2026.3.23-1`
|
|
14
13
|
|
|
15
|
-
##
|
|
14
|
+
## 5 分钟安装(推荐)
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- Channel runtime:
|
|
20
|
-
- inbound message receive
|
|
21
|
-
- outbound reply / media / streaming chunk send
|
|
22
|
-
- native channel actions (`unsend`, `delete`)
|
|
23
|
-
- Typed admin tools:
|
|
24
|
-
- `grix_query`
|
|
25
|
-
- `grix_group`
|
|
26
|
-
- `grix_agent_admin`
|
|
27
|
-
- Operator CLI:
|
|
28
|
-
- `openclaw grix doctor`
|
|
29
|
-
- `openclaw grix create-agent ...`
|
|
30
|
-
- Bundled skills:
|
|
31
|
-
- `message-send`
|
|
32
|
-
- `message-unsend`
|
|
33
|
-
- `egg-install`
|
|
34
|
-
- `grix-query`
|
|
35
|
-
- `grix-group-governance`
|
|
36
|
-
- `grix-agent-admin`
|
|
37
|
-
|
|
38
|
-
## Install
|
|
16
|
+
### 1) 安装并启用插件
|
|
39
17
|
|
|
40
18
|
```bash
|
|
41
19
|
openclaw plugins install @dhf-openclaw/grix
|
|
42
20
|
openclaw plugins enable grix
|
|
43
|
-
openclaw gateway restart
|
|
44
21
|
```
|
|
45
22
|
|
|
46
|
-
###
|
|
23
|
+
### 2) 绑定 Grix Channel
|
|
47
24
|
|
|
48
|
-
|
|
25
|
+
用你已有的 Grix API agent 信息执行:
|
|
49
26
|
|
|
50
27
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
openclaw channels add \
|
|
29
|
+
--channel grix \
|
|
30
|
+
--name grix-main \
|
|
31
|
+
--http-url "wss://grix.dhf.pub/v1/agent-api/ws?agent_id={agent_id}" \
|
|
32
|
+
--user-id "<YOUR_AGENT_ID>" \
|
|
33
|
+
--token "<YOUR_API_KEY>"
|
|
53
34
|
```
|
|
54
35
|
|
|
55
|
-
|
|
36
|
+
说明:
|
|
56
37
|
|
|
57
|
-
|
|
38
|
+
- `--http-url` 可以带 `agent_id`,也可以不带。不带时会自动按 `--user-id` 补上。
|
|
39
|
+
- `--name` 是本地账户名,可自定义(如 `ops`、`prod`)。
|
|
58
40
|
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
"channels": {
|
|
62
|
-
"grix": {
|
|
63
|
-
"enabled": true,
|
|
64
|
-
"wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<YOUR_AGENT_ID>",
|
|
65
|
-
"agentId": "<YOUR_AGENT_ID>",
|
|
66
|
-
"apiKey": "<YOUR_API_KEY>"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
41
|
+
### 3) 开放工具权限
|
|
71
42
|
|
|
72
|
-
|
|
43
|
+
在 OpenClaw 配置里确保有:
|
|
73
44
|
|
|
74
45
|
```json
|
|
75
46
|
{
|
|
@@ -88,7 +59,31 @@ openclaw plugins install ./grix.ts
|
|
|
88
59
|
}
|
|
89
60
|
```
|
|
90
61
|
|
|
91
|
-
|
|
62
|
+
### 4) 重启网关
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
openclaw gateway restart
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 5) 验证安装结果
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
openclaw plugins info grix --json
|
|
72
|
+
openclaw grix doctor
|
|
73
|
+
openclaw skills list
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
预期:
|
|
77
|
+
|
|
78
|
+
- `grix` 插件已启用
|
|
79
|
+
- `doctor` 能看到可用账户
|
|
80
|
+
- `skills list` 能看到本插件内置技能
|
|
81
|
+
|
|
82
|
+
## 配置参数说明(完整)
|
|
83
|
+
|
|
84
|
+
`channels.grix` 支持“单账户”或“多账户(accounts)”两种写法。
|
|
85
|
+
|
|
86
|
+
### 最小可用配置(单账户)
|
|
92
87
|
|
|
93
88
|
```json
|
|
94
89
|
{
|
|
@@ -99,66 +94,92 @@ Full example:
|
|
|
99
94
|
"agentId": "<YOUR_AGENT_ID>",
|
|
100
95
|
"apiKey": "<YOUR_API_KEY>"
|
|
101
96
|
}
|
|
102
|
-
},
|
|
103
|
-
"tools": {
|
|
104
|
-
"profile": "coding",
|
|
105
|
-
"alsoAllow": [
|
|
106
|
-
"message",
|
|
107
|
-
"grix_query",
|
|
108
|
-
"grix_group",
|
|
109
|
-
"grix_agent_admin"
|
|
110
|
-
],
|
|
111
|
-
"sessions": {
|
|
112
|
-
"visibility": "agent"
|
|
113
|
-
}
|
|
114
97
|
}
|
|
115
98
|
}
|
|
116
99
|
```
|
|
117
100
|
|
|
118
|
-
|
|
101
|
+
### 多账户配置示例
|
|
119
102
|
|
|
120
|
-
```
|
|
121
|
-
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"channels": {
|
|
106
|
+
"grix": {
|
|
107
|
+
"enabled": true,
|
|
108
|
+
"defaultAccount": "ops",
|
|
109
|
+
"accounts": {
|
|
110
|
+
"ops": {
|
|
111
|
+
"enabled": true,
|
|
112
|
+
"name": "Ops",
|
|
113
|
+
"wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<OPS_AGENT_ID>",
|
|
114
|
+
"agentId": "<OPS_AGENT_ID>",
|
|
115
|
+
"apiKey": "<OPS_API_KEY>"
|
|
116
|
+
},
|
|
117
|
+
"prod": {
|
|
118
|
+
"enabled": true,
|
|
119
|
+
"wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<PROD_AGENT_ID>",
|
|
120
|
+
"agentId": "<PROD_AGENT_ID>",
|
|
121
|
+
"apiKey": "<PROD_API_KEY>"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
122
127
|
```
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- `
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
129
|
+
### 字段说明
|
|
130
|
+
|
|
131
|
+
| 字段 | 必填 | 默认值 | 说明 |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `enabled` | 否 | `true` | 全局开关。设为 `false` 后该通道停用。 |
|
|
134
|
+
| `defaultAccount` | 否 | 自动选择 | 多账户时默认账户 ID。 |
|
|
135
|
+
| `accounts.<id>` | 否 | - | 多账户配置项,`<id>` 自定义(如 `ops`)。 |
|
|
136
|
+
| `name` | 否 | - | 账户显示名。 |
|
|
137
|
+
| `wsUrl` | 是(可用环境变量兜底) | `ws://127.0.0.1:27189/...`(当有 `agentId` 且未填时) | Grix WebSocket 地址。 |
|
|
138
|
+
| `agentId` | 是(可用环境变量兜底) | - | Grix agent ID。 |
|
|
139
|
+
| `apiKey` | 是(可用环境变量兜底) | - | Grix API Key。 |
|
|
140
|
+
| `reconnectMs` | 否 | `2000` | 重连基础延迟(毫秒)。 |
|
|
141
|
+
| `reconnectMaxMs` | 否 | `max(30000, reconnectMs*8)` | 重连最大延迟(毫秒)。 |
|
|
142
|
+
| `reconnectStableMs` | 否 | `30000` | 连接保持多久算“稳定”(毫秒)。 |
|
|
143
|
+
| `connectTimeoutMs` | 否 | `10000` | 建连超时(毫秒)。 |
|
|
144
|
+
| `keepalivePingMs` | 否 | 自动计算 | 心跳发送间隔(毫秒)。 |
|
|
145
|
+
| `keepaliveTimeoutMs` | 否 | 自动计算 | 心跳超时阈值(毫秒)。 |
|
|
146
|
+
| `upstreamRetryMaxAttempts` | 否 | `3` | 上游发送失败重试次数(1-5)。 |
|
|
147
|
+
| `upstreamRetryBaseDelayMs` | 否 | `300` | 上游重试基础延迟(毫秒)。 |
|
|
148
|
+
| `upstreamRetryMaxDelayMs` | 否 | `2000` | 上游重试最大延迟(毫秒)。 |
|
|
149
|
+
| `maxChunkChars` | 否 | `1200` | 普通回复分片长度上限(最大 2000)。 |
|
|
150
|
+
| `streamChunkChars` | 否 | `48` | 流式回复分片长度上限(最大 2000)。 |
|
|
151
|
+
| `streamChunkDelayMs` | 否 | `0` | 流式分片发送间隔(毫秒)。 |
|
|
152
|
+
| `dmPolicy` | 否 | `open` | 私聊策略:`open` / `pairing` / `allowlist` / `disabled`。 |
|
|
153
|
+
| `allowFrom` | 否 | `[]` | 白名单发送者列表(配合 `dmPolicy=allowlist`)。 |
|
|
154
|
+
| `defaultTo` | 否 | - | 默认发送目标会话。 |
|
|
155
|
+
| `execApprovals.enabled` | 否 | `false` | 是否启用聊天内执行审批。 |
|
|
156
|
+
| `execApprovals.approvers` | 条件必填 | `[]` | 审批人 sender ID 列表。启用审批时需填写。 |
|
|
157
|
+
|
|
158
|
+
### 环境变量兜底
|
|
159
|
+
|
|
160
|
+
如果配置文件没填,插件会按下列环境变量读取:
|
|
161
|
+
|
|
162
|
+
- `GRIX_WS_URL`
|
|
163
|
+
- `GRIX_AGENT_ID`
|
|
164
|
+
- `GRIX_API_KEY`
|
|
165
|
+
|
|
166
|
+
## 工具与命令
|
|
167
|
+
|
|
168
|
+
### Agent 可调用工具
|
|
169
|
+
|
|
170
|
+
- `grix_query`:`contact_search`、`session_search`、`message_history`
|
|
171
|
+
- `grix_group`:`create`、`detail`、`add_members`、`remove_members`、`update_member_role`、`update_all_members_muted`、`update_member_speaking`、`dissolve`
|
|
172
|
+
- `grix_agent_admin`:创建 `provider_type=3` 的 Grix API agent(只创建远端 agent,不会直接改本地 `channels.grix`)
|
|
173
|
+
|
|
174
|
+
### 运维命令
|
|
175
|
+
|
|
176
|
+
查看账户:
|
|
156
177
|
|
|
157
178
|
```bash
|
|
158
179
|
openclaw grix doctor
|
|
159
180
|
```
|
|
160
181
|
|
|
161
|
-
|
|
182
|
+
创建 API agent:
|
|
162
183
|
|
|
163
184
|
```bash
|
|
164
185
|
openclaw grix create-agent \
|
|
@@ -166,19 +187,18 @@ openclaw grix create-agent \
|
|
|
166
187
|
--describe-message-tool '{"actions":["unsend","delete"]}'
|
|
167
188
|
```
|
|
168
189
|
|
|
169
|
-
|
|
190
|
+
参数说明:
|
|
170
191
|
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
192
|
+
- `--agent-name`:必填,小写字母开头,只允许小写字母、数字、`-`,长度 3-32。
|
|
193
|
+
- `--describe-message-tool`:必填,JSON 对象,至少包含 `actions` 数组。
|
|
194
|
+
- `--account-id`:可选,指定用哪个本地 Grix 账户发起创建。
|
|
195
|
+
- `--avatar-url`:可选,给新 agent 设置头像地址。
|
|
174
196
|
|
|
175
|
-
|
|
197
|
+
命令输出里会给出一次性 `api_key` 和下一步绑定命令模板。
|
|
176
198
|
|
|
177
|
-
## Exec
|
|
199
|
+
## 聊天内 Exec 审批(可选)
|
|
178
200
|
|
|
179
|
-
Grix
|
|
180
|
-
|
|
181
|
-
### 1. Configure Grix approvers
|
|
201
|
+
先在 Grix 账户里配置审批人:
|
|
182
202
|
|
|
183
203
|
```json
|
|
184
204
|
{
|
|
@@ -193,26 +213,7 @@ Grix can approve OpenClaw host `exec` requests in chat.
|
|
|
193
213
|
}
|
|
194
214
|
```
|
|
195
215
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```json
|
|
199
|
-
{
|
|
200
|
-
"channels": {
|
|
201
|
-
"grix": {
|
|
202
|
-
"accounts": {
|
|
203
|
-
"ops": {
|
|
204
|
-
"execApprovals": {
|
|
205
|
-
"enabled": true,
|
|
206
|
-
"approvers": ["<GRIX_SENDER_ID>"]
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### 2. Enable OpenClaw exec approvals
|
|
216
|
+
再开启 OpenClaw 的 exec 审批:
|
|
216
217
|
|
|
217
218
|
```json
|
|
218
219
|
{
|
|
@@ -228,58 +229,18 @@ If using named accounts:
|
|
|
228
229
|
"enabled": true,
|
|
229
230
|
"mode": "session"
|
|
230
231
|
}
|
|
231
|
-
},
|
|
232
|
-
"channels": {
|
|
233
|
-
"grix": {
|
|
234
|
-
"execApprovals": {
|
|
235
|
-
"enabled": true,
|
|
236
|
-
"approvers": ["<GRIX_SENDER_ID>"]
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
232
|
}
|
|
240
233
|
}
|
|
241
234
|
```
|
|
242
235
|
|
|
243
|
-
|
|
236
|
+
`mode` 说明:
|
|
244
237
|
|
|
245
|
-
- `session
|
|
246
|
-
- `targets
|
|
247
|
-
- `both
|
|
238
|
+
- `session`:发到当前会话
|
|
239
|
+
- `targets`:发到 `approvals.exec.targets`
|
|
240
|
+
- `both`:两边都发
|
|
248
241
|
|
|
249
|
-
|
|
242
|
+
配置改完后重启:
|
|
250
243
|
|
|
251
244
|
```bash
|
|
252
245
|
openclaw gateway restart
|
|
253
246
|
```
|
|
254
|
-
|
|
255
|
-
### 4. Approve in chat
|
|
256
|
-
|
|
257
|
-
Flow:
|
|
258
|
-
|
|
259
|
-
1. Ask OpenClaw to run an `exec` command that needs approval.
|
|
260
|
-
2. OpenClaw sends approval prompt to Grix.
|
|
261
|
-
3. An allowed approver can:
|
|
262
|
-
- click `Allow Once`, `Allow Always`, or `Deny`
|
|
263
|
-
- or send `/approve <id> allow-once|allow-always|deny`
|
|
264
|
-
4. OpenClaw continues or denies execution.
|
|
265
|
-
|
|
266
|
-
Notes:
|
|
267
|
-
|
|
268
|
-
- approvers must be Grix sender IDs, not OpenClaw agent IDs
|
|
269
|
-
- configure approvers under the serving account
|
|
270
|
-
- approval requests and results are posted in chat
|
|
271
|
-
|
|
272
|
-
## Verification
|
|
273
|
-
|
|
274
|
-
```bash
|
|
275
|
-
openclaw plugins info grix --json
|
|
276
|
-
openclaw skills list
|
|
277
|
-
openclaw grix doctor
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Expected:
|
|
281
|
-
|
|
282
|
-
- plugin `grix` is enabled and loaded
|
|
283
|
-
- typed tools are callable when `tools.alsoAllow` is configured
|
|
284
|
-
- bundled skills are visible in skills list
|
|
285
|
-
- `openclaw grix doctor` can read configured `channels.grix` accounts
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grix-admin
|
|
3
|
+
description: 负责 OpenClaw 本地配置与后续 agent 管理;支持接收 grix-register 交接参数直接落地,也支持在已有主密钥下新建 agent 再落地。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Grix Agent Admin
|
|
7
|
+
|
|
8
|
+
`grix-admin` 只负责本地配置和管理动作。支持两个入口模式,二选一执行。
|
|
9
|
+
|
|
10
|
+
## Mode A: bind-local(来自 grix-register 的首次交接)
|
|
11
|
+
|
|
12
|
+
输入参数(全必填):
|
|
13
|
+
|
|
14
|
+
1. `mode=bind-local`
|
|
15
|
+
2. `agent_name`
|
|
16
|
+
3. `agent_id`
|
|
17
|
+
4. `api_endpoint`
|
|
18
|
+
5. `api_key`
|
|
19
|
+
|
|
20
|
+
执行规则:
|
|
21
|
+
|
|
22
|
+
1. 不做远端创建,直接执行本地绑定:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
scripts/grix_agent_bind.py configure-local-openclaw \
|
|
26
|
+
--agent-name <agent_name> \
|
|
27
|
+
--agent-id <agent_id> \
|
|
28
|
+
--api-endpoint '<api_endpoint>' \
|
|
29
|
+
--api-key '<api_key>' \
|
|
30
|
+
--apply
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. 可选执行检查:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Mode B: create-and-bind(已有主密钥时的后续管理)
|
|
40
|
+
|
|
41
|
+
输入参数:
|
|
42
|
+
|
|
43
|
+
1. `agentName`(必填):`^[a-z][a-z0-9-]{2,31}$`
|
|
44
|
+
2. `describeMessageTool`(必填):`actions` 非空
|
|
45
|
+
3. `accountId`(可选)
|
|
46
|
+
4. `avatarUrl`(可选)
|
|
47
|
+
|
|
48
|
+
执行规则:
|
|
49
|
+
|
|
50
|
+
1. 先检查 `~/.openclaw/openclaw.json` 的 `channels.grix.apiKey`。
|
|
51
|
+
2. 若缺失或为空,说明主通道还没完成,不做本模式,立刻切回 `grix-register`。
|
|
52
|
+
3. 若已存在,再调用 `grix_agent_admin` 创建远端 agent(仅一次,不自动重试)。
|
|
53
|
+
4. 创建成功后,执行本地绑定命令(同 Mode A)。
|
|
54
|
+
|
|
55
|
+
## Guardrails(两种模式都适用)
|
|
56
|
+
|
|
57
|
+
1. Never ask user for website account/password.
|
|
58
|
+
2. `bind-local` 模式禁止再次回调 `grix-register`,避免循环路由。
|
|
59
|
+
3. 远端创建(Mode B)视为非幂等,不确认不自动重试。
|
|
60
|
+
4. 完整 `api_key` 仅一次性回传,不要重复明文回显。
|
|
61
|
+
5. 本地 `--apply` 没成功前,不得宣称配置完成。
|
|
62
|
+
|
|
63
|
+
## Error Handling Rules
|
|
64
|
+
|
|
65
|
+
1. `bind-local` 缺少字段:明确指出缺哪个字段并停止。
|
|
66
|
+
2. invalid name(Mode B):要求用户提供合法小写英文名。
|
|
67
|
+
3. `403/20011`:提示 owner 授权 `agent.api.create`。
|
|
68
|
+
4. `401/10001`:检查本地 `agent_api_key` 或 grix 账号配置。
|
|
69
|
+
5. `409/20002`:要求更换 agent 名称。
|
|
70
|
+
6. 本地 apply 失败:返回失败命令与结果并停止。
|
|
71
|
+
|
|
72
|
+
## Response Style
|
|
73
|
+
|
|
74
|
+
1. 明确写出当前执行的是 `bind-local` 还是 `create-and-bind`。
|
|
75
|
+
2. 分阶段汇报:远端(如有)+ 本地绑定。
|
|
76
|
+
3. 明确说明本地是否已生效,失败则给具体原因。
|
|
77
|
+
|
|
78
|
+
## References
|
|
79
|
+
|
|
80
|
+
1. [references/api-contract.md](references/api-contract.md)
|
|
81
|
+
2. [scripts/grix_agent_bind.py](scripts/grix_agent_bind.py)
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`grix-admin` 负责本地绑定,支持两种入口:
|
|
6
|
+
|
|
7
|
+
1. `bind-local`:接收 `grix-register` 交接参数,直接本地绑定。
|
|
8
|
+
2. `create-and-bind`:在已有主密钥下先远端创建,再本地绑定。
|
|
6
9
|
|
|
7
10
|
## Base Rules
|
|
8
11
|
|
|
@@ -12,7 +15,7 @@ Map remote provisioning action to Aibot Agent API HTTP route, then hand over to
|
|
|
12
15
|
4. Route must pass scope middleware before service business checks.
|
|
13
16
|
5. Do not ask users to provide website account/password for this flow.
|
|
14
17
|
|
|
15
|
-
## Action Mapping (
|
|
18
|
+
## Action Mapping (create-and-bind only)
|
|
16
19
|
|
|
17
20
|
| Tool | Method | Route | Required Scope |
|
|
18
21
|
|---|---|---|---|
|
|
@@ -72,7 +75,7 @@ Map remote provisioning action to Aibot Agent API HTTP route, then hand over to
|
|
|
72
75
|
|
|
73
76
|
## Post-Create Handover
|
|
74
77
|
|
|
75
|
-
After `code=0
|
|
78
|
+
After `code=0` (or when using `bind-local` mode), continue with local OpenClaw binding via bundled script:
|
|
76
79
|
|
|
77
80
|
1. apply local changes directly:
|
|
78
81
|
- `scripts/grix_agent_bind.py configure-local-openclaw --agent-name <agent_name> --agent-id <agent_id> --api-endpoint '<api_endpoint>' --api-key '<api_key>' --apply`
|
|
@@ -85,3 +88,19 @@ Local apply writes:
|
|
|
85
88
|
2. `channels.grix.accounts.<agent_name>` entry
|
|
86
89
|
3. `bindings` route for `channel=grix`
|
|
87
90
|
4. required tools config and gateway restart
|
|
91
|
+
|
|
92
|
+
## bind-local Input Contract
|
|
93
|
+
|
|
94
|
+
When called from `grix-register`, `grix-admin` should accept:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mode": "bind-local",
|
|
99
|
+
"agent_name": "grix-main",
|
|
100
|
+
"agent_id": "2029786829095440384",
|
|
101
|
+
"api_endpoint": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=2029786829095440384",
|
|
102
|
+
"api_key": "ak_xxx"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In this mode, skip remote create and execute local bind directly.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: grix-group
|
|
2
|
+
name: grix-group
|
|
3
3
|
description: Use the typed `grix_group` tool for Grix group lifecycle and membership operations. Trigger when users ask to create, inspect, update, or dissolve groups, or when these operations fail with scope or permission errors.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grix-register
|
|
3
|
+
description: 仅用于初次安装阶段,完成 grix.dhf.pub 账号注册/登录并拿到第一个 provider_type=3 Agent 的参数;本技能不做任何本地 OpenClaw 配置。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Grix Register
|
|
7
|
+
|
|
8
|
+
这个技能只负责“初次安装”的云端准备:账号注册/登录 + 生成首个 `provider_type=3` Agent 参数。
|
|
9
|
+
你(AI)在终端里全自动操作,**不需要用户打开浏览器**。拿到参数后,必须移交给 `grix-admin` 做本地配置。
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
### 0. 角色边界(先声明再执行)
|
|
14
|
+
|
|
15
|
+
1. 本技能**只能**做账号与云端 Agent 参数准备。
|
|
16
|
+
2. 本技能**不能**执行 `openclaw` 命令,也不能修改本地 `openclaw.json`。
|
|
17
|
+
3. 涉及本地配置、插件安装、工具权限、重启网关,一律交给 `grix-admin`。
|
|
18
|
+
|
|
19
|
+
### 1. 询问邮箱并发送验证码
|
|
20
|
+
|
|
21
|
+
1. 向用户询问 Email 地址。**不要让用户去网页端注册**,明确表示你会在对话里完成。
|
|
22
|
+
2. 拿到邮箱后,在终端执行发送验证码的命令:
|
|
23
|
+
```bash
|
|
24
|
+
scripts/grix_auth.py send-email-code --email "<用户的email>" --scene "register"
|
|
25
|
+
```
|
|
26
|
+
3. 等待命令执行成功后,提示用户去邮箱查收验证码,并询问该验证码。
|
|
27
|
+
|
|
28
|
+
### 2. 执行自动注册(获取 Token)
|
|
29
|
+
|
|
30
|
+
1. 用户提供验证码后,你需要为用户生成一个复杂度高且随机安全的密码(建议生成一个12位的强密码,包含大小写、数字和特殊字符)。
|
|
31
|
+
2. 使用收集到的信息,执行注册命令:
|
|
32
|
+
```bash
|
|
33
|
+
scripts/grix_auth.py register --email "<邮箱>" --password "<生成的随机密码>" --email-code "<验证码>"
|
|
34
|
+
```
|
|
35
|
+
3. 这个命令成功后会返回用户的 `access_token`。请在回复中安全地**将生成的密码告知用户**,建议他们妥善保存。
|
|
36
|
+
|
|
37
|
+
注:如果注册提示邮箱已注册,可切换 `scripts/grix_auth.py login` 路径继续获取 `access_token`。
|
|
38
|
+
|
|
39
|
+
### 3. 创建首个云端 Agent 参数
|
|
40
|
+
|
|
41
|
+
拿到 `access_token` 后,询问 Agent 名称(如果上下文已有就直接用),执行:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
scripts/grix_auth.py create-api-agent --access-token "<token>" --agent-name "<agent名称>"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
若同名 `provider_type=3` Agent 已存在,脚本会自动轮换 API Key 后复用。
|
|
48
|
+
|
|
49
|
+
### 4. 强制移交给 grix-admin
|
|
50
|
+
|
|
51
|
+
第三步执行成功后,脚本会返回一些关键设定:
|
|
52
|
+
- `agent_id`
|
|
53
|
+
- `agent_name`
|
|
54
|
+
- `api_endpoint`
|
|
55
|
+
- `api_key`
|
|
56
|
+
|
|
57
|
+
然后立刻交给 `grix-admin`,并传递如下 payload:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mode": "bind-local",
|
|
62
|
+
"agent_name": "<agent_name>",
|
|
63
|
+
"agent_id": "<agent_id>",
|
|
64
|
+
"api_endpoint": "<api_endpoint>",
|
|
65
|
+
"api_key": "<api_key>"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Guardrails
|
|
70
|
+
|
|
71
|
+
1. 不要求用户去网页注册或手动点页面。
|
|
72
|
+
2. 不修改任何本地 OpenClaw 配置。
|
|
73
|
+
3. 不安装插件、不改工具权限、不重启 gateway。
|
|
74
|
+
4. 创建或复用出参数后,必须交接给 `grix-admin`。
|
|
75
|
+
|
|
76
|
+
## References
|
|
77
|
+
|
|
78
|
+
1. [references/api-contract.md](references/api-contract.md)
|
|
79
|
+
2. [references/handoff-contract.md](references/handoff-contract.md)
|
|
80
|
+
3. [scripts/grix_auth.py](scripts/grix_auth.py)
|
|
@@ -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()
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: grix-agent-admin
|
|
3
|
-
description: 通过 Grix Agent API 协议创建 `provider_type=3` 的 API agent,并直接完成本地 OpenClaw agent 与 Grix 渠道绑定配置(默认直接应用并返回结果)。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Grix Agent Admin
|
|
7
|
-
|
|
8
|
-
Create a remote `provider_type=3` API agent, then complete local OpenClaw agent + grix channel binding in one flow.
|
|
9
|
-
|
|
10
|
-
## Security + Auth Path
|
|
11
|
-
|
|
12
|
-
1. This skill does **not** ask the user for website account/password.
|
|
13
|
-
2. Remote create action uses local `channels.grix` credentials and `Authorization: Bearer <agent_api_key>`.
|
|
14
|
-
3. Local OpenClaw config is handled by `scripts/grix_agent_bind.py`.
|
|
15
|
-
|
|
16
|
-
## Required Input
|
|
17
|
-
|
|
18
|
-
1. `agentName` (required): regex `^[a-z][a-z0-9-]{2,31}$`
|
|
19
|
-
2. `describeMessageTool` (required): must contain non-empty `actions`
|
|
20
|
-
3. `accountId` (optional)
|
|
21
|
-
4. `avatarUrl` (optional)
|
|
22
|
-
|
|
23
|
-
## Full Workflow
|
|
24
|
-
|
|
25
|
-
### A. Create Remote API Agent
|
|
26
|
-
|
|
27
|
-
1. Validate `agentName` and `describeMessageTool`.
|
|
28
|
-
2. Call `grix_agent_admin` once.
|
|
29
|
-
3. Read result fields: `id`, `agent_name`, `api_endpoint`, `api_key`, `api_key_hint`.
|
|
30
|
-
|
|
31
|
-
### B. Apply Local OpenClaw Binding Directly
|
|
32
|
-
|
|
33
|
-
Run with `--apply` directly:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
scripts/grix_agent_bind.py configure-local-openclaw \
|
|
37
|
-
--agent-name <agent_name> \
|
|
38
|
-
--agent-id <agent_id> \
|
|
39
|
-
--api-endpoint '<api_endpoint>' \
|
|
40
|
-
--api-key '<api_key>' \
|
|
41
|
-
--apply
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
This applies:
|
|
45
|
-
|
|
46
|
-
1. upsert `agents.list` for `<agent_name>`
|
|
47
|
-
2. upsert `channels.grix.accounts.<agent_name>`
|
|
48
|
-
3. upsert `bindings` route to `channel=grix`, `accountId=<agent_name>`
|
|
49
|
-
4. ensure required tools (`message`, `grix_group`, `grix_agent_admin`)
|
|
50
|
-
5. create workspace defaults under `~/.openclaw/workspace-<agent_name>/`
|
|
51
|
-
6. run `openclaw gateway restart`
|
|
52
|
-
|
|
53
|
-
### C. Optional Verification
|
|
54
|
-
|
|
55
|
-
If you need explicit post-check state, run:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Guardrails
|
|
62
|
-
|
|
63
|
-
1. Never ask user for website account/password.
|
|
64
|
-
2. Treat remote create as non-idempotent; do not auto-retry without confirmation.
|
|
65
|
-
3. Keep full `api_key` one-time only; do not repeatedly echo it.
|
|
66
|
-
4. Do not claim success before apply command returns success.
|
|
67
|
-
|
|
68
|
-
## Error Handling Rules
|
|
69
|
-
|
|
70
|
-
1. invalid name: ask user for a valid English lowercase name.
|
|
71
|
-
2. `403/20011`: ask owner to grant `agent.api.create` scope.
|
|
72
|
-
3. `401/10001`: verify local `agent_api_key` / grix account config.
|
|
73
|
-
4. `409/20002`: ask for another agent name.
|
|
74
|
-
5. local apply failed: return concrete failed command/result and stop.
|
|
75
|
-
|
|
76
|
-
## Response Style
|
|
77
|
-
|
|
78
|
-
1. Report two stages separately: remote create status + local binding status.
|
|
79
|
-
2. Include created `agent_id`, `agent_name`, `api_endpoint`, `api_key_hint`.
|
|
80
|
-
3. Clearly state local config has been applied (or failed with concrete reason).
|
|
81
|
-
|
|
82
|
-
## References
|
|
83
|
-
|
|
84
|
-
1. Load [references/api-contract.md](references/api-contract.md).
|
|
85
|
-
2. Use [scripts/grix_agent_bind.py](scripts/grix_agent_bind.py) for local binding apply.
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Grix Agent Admin"
|
|
3
|
-
short_description: "Create Grix API agents and finish local OpenClaw binding."
|
|
4
|
-
default_prompt: "Use this skill when users ask to create a new Grix API agent and finish OpenClaw binding. Never ask for website account/password. Validate `agentName` and require `describeMessageTool` (`actions` must be non-empty), then call `grix_agent_admin` once. After successful create, directly run `scripts/grix_agent_bind.py configure-local-openclaw ... --apply` and return final local binding result (success or exact failure reason)."
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Grix Group Governance"
|
|
3
|
-
short_description: "Use the typed grix_group tool for Grix group operations."
|
|
4
|
-
default_prompt: "Use this skill when users ask to create, inspect, update, or dissolve Grix groups. Validate parameters, call grix_group exactly once per action, and return clear remediation for scope/auth/parameter failures."
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
version: 1
|
|
2
|
-
agent:
|
|
3
|
-
name: grix-query
|
|
4
|
-
default_prompt: "Use this skill when users ask to find Grix contacts, locate sessions, or inspect a known session's message history. Validate required parameters, call grix_query exactly once per action, and do not fabricate a sessionId."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|