sophhub 0.4.7 → 0.4.8
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/package.json +1 -1
- package/skills/bot-api-status/skill.json +10 -2
- package/skills/bot-api-status/src/SKILL.md +11 -1
- package/skills/bot-api-status/src/pyproject.toml +1 -1
- package/skills/bot-api-status/src/scripts/secret.py +17 -1
- package/skills/claw-agent-get-send/skill.json +21 -0
- package/skills/claw-agent-get-send/src/SKILL.md +72 -0
- package/skills/claw-agent-get-send/src/pyproject.toml +5 -0
- package/skills/claw-agent-get-send/src/reference-http.md +149 -0
- package/skills/claw-agent-get-send/src/scripts/appia_claw.py +287 -0
- package/skills/sophnet-qa-install/skill.json +13 -6
- package/skills/sophnet-training-install/skill.json +13 -6
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bot-api-status",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"types": [
|
|
5
5
|
"store"
|
|
6
6
|
],
|
|
7
7
|
"displayName": "BOT-API",
|
|
8
8
|
"description": "查看当前 Agent 的 bot api 状态,同时也支持开启、关闭、重置密钥",
|
|
9
9
|
"changelog": [
|
|
10
|
+
{
|
|
11
|
+
"changes": [
|
|
12
|
+
"非 --json 输出补充 Agent 名字与 source_agent_id,与 SKILL 输出模板一致",
|
|
13
|
+
"修正 SKILL 页眉为合法 YAML frontmatter"
|
|
14
|
+
],
|
|
15
|
+
"date": "2026-04-28",
|
|
16
|
+
"version": "1.0.3"
|
|
17
|
+
},
|
|
10
18
|
{
|
|
11
19
|
"changes": [
|
|
12
20
|
"修正 SKILL 中 bot-api 链接路径说明(account_id 占位,与脚本一致)",
|
|
@@ -32,5 +40,5 @@
|
|
|
32
40
|
}
|
|
33
41
|
],
|
|
34
42
|
"createdAt": "2026-04-22",
|
|
35
|
-
"updatedAt": "2026-04-
|
|
43
|
+
"updatedAt": "2026-04-28"
|
|
36
44
|
}
|
|
@@ -45,6 +45,9 @@ uv run {baseDir}/scripts/secret.py reset --json
|
|
|
45
45
|
```text
|
|
46
46
|
📋 🤖 bot-api 状态:<🟢 开启 / ⚪ 关闭>
|
|
47
47
|
|
|
48
|
+
📛 Agent 名字:<display_name>
|
|
49
|
+
🆔 Agent ID:<source_agent_id>
|
|
50
|
+
|
|
48
51
|
🔗 非流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat
|
|
49
52
|
🔗 流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat-stream
|
|
50
53
|
🔑 密钥:<api_secret 或 📭 暂无>
|
|
@@ -55,6 +58,9 @@ uv run {baseDir}/scripts/secret.py reset --json
|
|
|
55
58
|
```text
|
|
56
59
|
✨ 🤖 bot-api 已创建
|
|
57
60
|
|
|
61
|
+
📛 Agent 名字:<display_name>
|
|
62
|
+
🆔 Agent ID:<source_agent_id>
|
|
63
|
+
|
|
58
64
|
🔗 非流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat
|
|
59
65
|
🔗 流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat-stream
|
|
60
66
|
🔑 密钥:<api_secret>
|
|
@@ -75,9 +81,12 @@ uv run {baseDir}/scripts/secret.py reset --json
|
|
|
75
81
|
```text
|
|
76
82
|
🔄 🤖 bot-api 密钥已更新
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
📛 Agent 名字:<display_name>
|
|
85
|
+
🆔 Agent ID:<source_agent_id>
|
|
86
|
+
|
|
79
87
|
🔗 非流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat
|
|
80
88
|
🔗 流式链接:{{BASEURL}}/bot-api/v2/<account_id>/chat-stream
|
|
89
|
+
🔑 新密钥:<new_secret>
|
|
81
90
|
|
|
82
91
|
🔒 请妥善保管,勿对外转发或截图。
|
|
83
92
|
```
|
|
@@ -87,3 +96,4 @@ uv run {baseDir}/scripts/secret.py reset --json
|
|
|
87
96
|
```text
|
|
88
97
|
⚠️ 获取BASEURL失败,刷新页面或者重新登录后重试。
|
|
89
98
|
```
|
|
99
|
+
|
|
@@ -365,6 +365,17 @@ def _fmt_url(value: object) -> str:
|
|
|
365
365
|
return "📭 暂无"
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
def _fmt_identity(value: object) -> str:
|
|
369
|
+
if isinstance(value, str) and value.strip():
|
|
370
|
+
return value.strip()
|
|
371
|
+
return "📭 暂无"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _print_agent_identity(display_name: object, source_agent_id: object) -> None:
|
|
375
|
+
print(f"📛 名字:{_fmt_identity(display_name)}")
|
|
376
|
+
print(f"🆔 source_agent_id:{_fmt_identity(source_agent_id)}")
|
|
377
|
+
|
|
378
|
+
|
|
368
379
|
def _print_link_block(url: str, stream_url: str, baseurl_note: str) -> None:
|
|
369
380
|
if url != "📭 暂无":
|
|
370
381
|
print(f"🔗 非流式链接:{url}")
|
|
@@ -384,6 +395,8 @@ def print_human_response(command: str, payload: dict[str, Any]) -> int:
|
|
|
384
395
|
state = "🟢 开启" if enabled else "⚪ 关闭"
|
|
385
396
|
print(f"📋 🤖 bot-api 状态:{state}")
|
|
386
397
|
print()
|
|
398
|
+
_print_agent_identity(payload.get("display_name"), payload.get("source_agent_id"))
|
|
399
|
+
print()
|
|
387
400
|
_print_link_block(url, stream_url, "")
|
|
388
401
|
print(f"🔑 密钥:{secret}")
|
|
389
402
|
return 0
|
|
@@ -393,6 +406,8 @@ def print_human_response(command: str, payload: dict[str, Any]) -> int:
|
|
|
393
406
|
prefix = "📝" if action == "updated" else "✨"
|
|
394
407
|
print(f"{prefix} 🤖 bot-api {title}")
|
|
395
408
|
print()
|
|
409
|
+
_print_agent_identity(payload.get("display_name"), payload.get("source_agent_id"))
|
|
410
|
+
print()
|
|
396
411
|
_print_link_block(url, stream_url, "")
|
|
397
412
|
print(f"🔑 密钥:{secret}")
|
|
398
413
|
print()
|
|
@@ -411,9 +426,10 @@ def print_human_response(command: str, payload: dict[str, Any]) -> int:
|
|
|
411
426
|
return 1
|
|
412
427
|
print("🔄 🤖 bot-api 密钥已更新")
|
|
413
428
|
print()
|
|
414
|
-
|
|
429
|
+
_print_agent_identity(payload.get("display_name"), payload.get("source_agent_id"))
|
|
415
430
|
print()
|
|
416
431
|
_print_link_block(url, stream_url, "")
|
|
432
|
+
print(f"🔑 新密钥:{secret}")
|
|
417
433
|
print()
|
|
418
434
|
print("🔒 请妥善保管,勿对外转发或截图。")
|
|
419
435
|
return 0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claw-agent-get-send",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"types": ["store"],
|
|
5
|
+
"displayName": "Claw Agent Get/Send",
|
|
6
|
+
"description": "Appia claw.agent.groups.get / claw.agent.message.send:脚本封装 + HTTP/curl 参考(JWT、msg/md 负载、错误码)",
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-04-28",
|
|
11
|
+
"changes": [
|
|
12
|
+
"groups / verify-target / send 分片子命令",
|
|
13
|
+
"reference-http.md:合并 claw.agent.groups.get / message.send curl 文档",
|
|
14
|
+
"skill 标识 claw-agent-get-send(原 appia-claw)",
|
|
15
|
+
"send-md 命令支持 Markdown AST 发送"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"createdAt": "2026-04-28",
|
|
20
|
+
"updatedAt": "2026-04-28"
|
|
21
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claw-agent-get-send
|
|
3
|
+
description: >-
|
|
4
|
+
Call Appia Claw HTTP APIs (agent.groups.get, agent.message.send) using env vars or curl.
|
|
5
|
+
Use when querying bot group memberships, verifying rid+group name, sending plaintext or Markdown AST to Appia rooms,
|
|
6
|
+
or when the user needs JWT auth rules, curl examples, or md payload shape for agent.message.send.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Claw Agent(groups.get / message.send)
|
|
10
|
+
|
|
11
|
+
使用 **环境变量** 鉴权,**不向仓库提交** JWT、`APP_AGENT_ID` 等明文。
|
|
12
|
+
|
|
13
|
+
## 环境变量
|
|
14
|
+
|
|
15
|
+
| 变量 | 必填 | 说明 |
|
|
16
|
+
|------|------|------|
|
|
17
|
+
| `APPIA_BASE_URL` | 是 | 根地址,例如 `https://sophgo.appia.cn` |
|
|
18
|
+
| `CLAW_JWT` | 是 | `Authorization: Bearer` 的值 |
|
|
19
|
+
| `APP_AGENT_ID` | 是 | SophClaw 机器人 Agent ID(与 `AGENT_ID` 二选一) |
|
|
20
|
+
| `TARGET_RID` | send / verify | 目标群 `rid` |
|
|
21
|
+
| `TARGET_GROUP_NAME` | verify | 群展示名,`verify-target` 双因子校验用 |
|
|
22
|
+
| `TARGET_GROUP_MATCH` | 否 | `exact`(默认)或 `contains` |
|
|
23
|
+
|
|
24
|
+
| 可选 | 说明 |
|
|
25
|
+
|------|------|
|
|
26
|
+
| `APPIA_MSG_CHUNK_SIZE` | 单条 `msg` 最大字符数,超出则拆分多次发送(默认 3500) |
|
|
27
|
+
|
|
28
|
+
## 用法
|
|
29
|
+
|
|
30
|
+
`{baseDir}` 为与本 `SKILL.md` 同级的安装根(通常含 `scripts/`)。
|
|
31
|
+
|
|
32
|
+
### 查询群列表
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv run {baseDir}/scripts/appia_claw.py groups --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 校验目标群
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv run {baseDir}/scripts/appia_claw.py verify-target
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 发送纯文本(支持分段)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv run {baseDir}/scripts/appia_claw.py send --file /tmp/brief.txt
|
|
48
|
+
uv run {baseDir}/scripts/appia_claw.py send --text "hello" --rid <rid>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 发送 Markdown AST
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 从 JSON 字符串
|
|
55
|
+
uv run {baseDir}/scripts/appia_claw.py send-md --md-json '[{"type":"PARAGRAPH","value":[{"type":"PLAIN_TEXT","value":"**粗体** 文本"}]}]'
|
|
56
|
+
|
|
57
|
+
# 从 JSON 文件
|
|
58
|
+
uv run {baseDir}/scripts/appia_claw.py send-md --md-file /tmp/md.json
|
|
59
|
+
|
|
60
|
+
# 纯文本自动转 md 结构
|
|
61
|
+
uv run {baseDir}/scripts/appia_claw.py send-md --text "简单文本"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 说明
|
|
65
|
+
|
|
66
|
+
- `send` 命令使用 **`msg` 纯文本**;极长正文按 `APPIA_MSG_CHUNK_SIZE` **分多条**发送。
|
|
67
|
+
- `send-md` 命令使用 **`md` Markdown AST** 结构,支持 Rocket.Chat 富文本格式。
|
|
68
|
+
- 请避免与业务侧「同 Sender 并发冲突」同时使用(串行调用本脚本即可)。
|
|
69
|
+
|
|
70
|
+
## HTTP / curl 参考
|
|
71
|
+
|
|
72
|
+
仓库根目录的 `claw.agent.groups.get.curl.md` 与 `claw.agent.message.send.curl.md` 已并入本 skill 的 [reference-http.md](reference-http.md)(鉴权说明、完整 curl、`md` 示例、常见错误码)。需要手写请求或排查 401 / 参数错误时优先读该文件。
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Claw HTTP API(curl 参考)
|
|
2
|
+
|
|
3
|
+
与仓库根目录 `claw.agent.groups.get.curl.md`、`claw.agent.message.send.curl.md` 同源;集成在 skill 内便于离线查阅。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `GET /api/v1/claw/agent.groups.get`
|
|
8
|
+
|
|
9
|
+
用于按 `agentId` 查询该机器人所在的所有群聊,返回 `rid` 和群名称。
|
|
10
|
+
|
|
11
|
+
### 鉴权方式
|
|
12
|
+
|
|
13
|
+
与 `mcpToDos` 一致:
|
|
14
|
+
|
|
15
|
+
- `authRequired: false`
|
|
16
|
+
- 通过请求头 `Authorization: Bearer <JWT>` 校验
|
|
17
|
+
- 校验规则受以下设置控制:
|
|
18
|
+
- `Appia_Antagent_JWT_Enable`
|
|
19
|
+
- `APPIA_JWT_SECRET`
|
|
20
|
+
|
|
21
|
+
### 请求示例
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
curl -sS -G 'https://YOUR_ROCKETCHAT_HOST/api/v1/claw/agent.groups.get' \
|
|
25
|
+
-H 'Authorization: Bearer YOUR_MCP_JWT_TOKEN' \
|
|
26
|
+
--data-urlencode 'agentId=AGENT_BOT_USER_ID'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 参数
|
|
30
|
+
|
|
31
|
+
- `agentId`(query,必填):OpenClaw 侧 Agent ID(会映射到机器人用户)
|
|
32
|
+
|
|
33
|
+
### 成功返回示例
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"success": true,
|
|
38
|
+
"data": {
|
|
39
|
+
"agentId": "AGENT_BOT_USER_ID",
|
|
40
|
+
"groups": [
|
|
41
|
+
{
|
|
42
|
+
"rid": "ROOM_ID_1",
|
|
43
|
+
"name": "群聊A"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"rid": "ROOM_ID_2",
|
|
47
|
+
"name": "群聊B"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"total": 2
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 常见失败
|
|
56
|
+
|
|
57
|
+
- `{"success":false,"message":"401"}`:JWT 无效或过期
|
|
58
|
+
- `{"success":false,"message":"agentId is required"}`:缺少 `agentId`
|
|
59
|
+
- `{"success":false,"message":"agent not found by agentId"}`:找不到对应 agentId 的机器人
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## `POST /api/v1/claw/agent.message.send`
|
|
64
|
+
|
|
65
|
+
用于根据 `rid + agentId` 向指定群聊发送消息。
|
|
66
|
+
|
|
67
|
+
接口会校验:
|
|
68
|
+
|
|
69
|
+
1. `rid` 对应房间存在
|
|
70
|
+
2. `agentId` 对应用户存在、为 bot、且 active
|
|
71
|
+
3. 该 `agentId` 确实在该 `rid` 的订阅列表里
|
|
72
|
+
|
|
73
|
+
满足后,走后端正常发消息逻辑(`executeSendMessage`)。
|
|
74
|
+
|
|
75
|
+
### 鉴权方式
|
|
76
|
+
|
|
77
|
+
与 `mcpToDos` 一致:
|
|
78
|
+
|
|
79
|
+
- `authRequired: false`
|
|
80
|
+
- 通过请求头 `Authorization: Bearer <JWT>` 校验
|
|
81
|
+
- 校验规则受以下设置控制:
|
|
82
|
+
- `Appia_Antagent_JWT_Enable`
|
|
83
|
+
- `APPIA_JWT_SECRET`
|
|
84
|
+
|
|
85
|
+
### 请求示例
|
|
86
|
+
|
|
87
|
+
#### 示例一:发送纯文本
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
curl -sS -X POST 'https://YOUR_ROCKETCHAT_HOST/api/v1/claw/agent.message.send' \
|
|
91
|
+
-H 'Content-Type: application/json' \
|
|
92
|
+
-H 'Authorization: Bearer YOUR_MCP_JWT_TOKEN' \
|
|
93
|
+
-d '{
|
|
94
|
+
"rid": "TARGET_ROOM_RID",
|
|
95
|
+
"agentId": "AGENT_BOT_USER_ID",
|
|
96
|
+
"msg": "这是一条由 Claw 机器人发送的消息"
|
|
97
|
+
}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### 示例二:发送 Markdown(`md`)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -sS -X POST 'https://YOUR_ROCKETCHAT_HOST/api/v1/claw/agent.message.send' \
|
|
104
|
+
-H 'Content-Type: application/json' \
|
|
105
|
+
-H 'Authorization: Bearer YOUR_MCP_JWT_TOKEN' \
|
|
106
|
+
-d '{
|
|
107
|
+
"rid": "TARGET_ROOM_RID",
|
|
108
|
+
"agentId": "OPENCLAW_AGENT_ID",
|
|
109
|
+
"md": [
|
|
110
|
+
{
|
|
111
|
+
"type": "PARAGRAPH",
|
|
112
|
+
"value": [
|
|
113
|
+
{ "type": "PLAIN_TEXT", "value": "这是一条 *Markdown* 消息" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 参数
|
|
121
|
+
|
|
122
|
+
- `rid`(body,必填):目标群聊房间 ID
|
|
123
|
+
- `agentId`(body,必填):OpenClaw 侧 Agent ID(会映射到机器人用户)
|
|
124
|
+
- `msg`(body,可选):消息文本
|
|
125
|
+
- `md`(body,可选):Markdown AST(Rocket.Chat `md` 结构)
|
|
126
|
+
- 约束:`msg` 和 `md` 至少提供一个
|
|
127
|
+
|
|
128
|
+
### 成功返回示例
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"success": true,
|
|
133
|
+
"data": {
|
|
134
|
+
"rid": "TARGET_ROOM_RID",
|
|
135
|
+
"agentId": "AGENT_BOT_USER_ID",
|
|
136
|
+
"status": "sent"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 常见失败
|
|
142
|
+
|
|
143
|
+
- `{"success":false,"message":"401"}`:JWT 无效或过期
|
|
144
|
+
- `{"success":false,"message":"rid and agentId are required, and either msg or md must be provided"}`:参数缺失
|
|
145
|
+
- `{"success":false,"message":"room not found"}`:房间不存在
|
|
146
|
+
- `{"success":false,"message":"agent not found by agentId"}`:找不到对应 agentId 的机器人
|
|
147
|
+
- `{"success":false,"message":"agent must be a bot user"}`:agent 不是 bot
|
|
148
|
+
- `{"success":false,"message":"agent is inactive"}`:机器人未激活
|
|
149
|
+
- `{"success":false,"message":"agent is not in this room"}`:机器人不在该群里
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Appia Claw HTTP API: agent.groups.get, agent.message.send (plaintext + md + chunking)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib import error, parse, request
|
|
12
|
+
|
|
13
|
+
DEFAULT_CHUNK_SIZE = 3500
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _env(name: str, *fallbacks: str) -> str:
|
|
17
|
+
for key in (name,) + fallbacks:
|
|
18
|
+
v = os.environ.get(key, "").strip()
|
|
19
|
+
if v:
|
|
20
|
+
return v
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _base_url() -> str:
|
|
25
|
+
base = _env("APPIA_BASE_URL", "APPIA_HOST").rstrip("/")
|
|
26
|
+
if not base:
|
|
27
|
+
sys.exit("缺少环境变量 APPIA_BASE_URL")
|
|
28
|
+
return base
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _jwt() -> str:
|
|
32
|
+
tok = _env("CLAW_JWT", "APPIA_CLAW_JWT")
|
|
33
|
+
if not tok:
|
|
34
|
+
sys.exit("缺少环境变量 CLAW_JWT")
|
|
35
|
+
return tok
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _agent_id() -> str:
|
|
39
|
+
aid = _env("APP_AGENT_ID", "AGENT_ID")
|
|
40
|
+
if not aid:
|
|
41
|
+
sys.exit("缺少环境变量 APP_AGENT_ID(或 AGENT_ID)")
|
|
42
|
+
return aid
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _groups_get(timeout: float) -> dict[str, Any]:
|
|
46
|
+
base = _base_url()
|
|
47
|
+
uid = _agent_id()
|
|
48
|
+
q = parse.urlencode({"agentId": uid})
|
|
49
|
+
url = f"{base}/api/v1/claw/agent.groups.get?{q}"
|
|
50
|
+
req = request.Request(
|
|
51
|
+
url,
|
|
52
|
+
headers={
|
|
53
|
+
"Authorization": f"Bearer {_jwt()}",
|
|
54
|
+
"Accept": "application/json",
|
|
55
|
+
},
|
|
56
|
+
method="GET",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
60
|
+
body = resp.read().decode("utf-8")
|
|
61
|
+
except error.HTTPError as exc:
|
|
62
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
63
|
+
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
|
64
|
+
data = json.loads(body)
|
|
65
|
+
if not data.get("success"):
|
|
66
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_groups(args: argparse.Namespace) -> int:
|
|
71
|
+
data = _groups_get(args.timeout)
|
|
72
|
+
if args.json:
|
|
73
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
74
|
+
return 0
|
|
75
|
+
payload = data.get("data") or {}
|
|
76
|
+
groups = payload.get("groups") or []
|
|
77
|
+
print(f"agentId={payload.get('agentId')} total={payload.get('total', len(groups))}")
|
|
78
|
+
for g in groups:
|
|
79
|
+
if isinstance(g, dict):
|
|
80
|
+
print(f"- rid={g.get('rid')} name={g.get('name')}")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_verify_target(args: argparse.Namespace) -> int:
|
|
85
|
+
rid = _env("TARGET_RID")
|
|
86
|
+
name = _env("TARGET_GROUP_NAME")
|
|
87
|
+
if not rid or not name:
|
|
88
|
+
print("⚠️ verify-target 需要环境变量 TARGET_RID 与 TARGET_GROUP_NAME", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
mode = (_env("TARGET_GROUP_MATCH") or "exact").lower()
|
|
91
|
+
if mode not in ("exact", "contains"):
|
|
92
|
+
mode = "exact"
|
|
93
|
+
data = _groups_get(args.timeout)
|
|
94
|
+
payload = data.get("data") or {}
|
|
95
|
+
groups = payload.get("groups") or []
|
|
96
|
+
hit: dict[str, Any] | None = None
|
|
97
|
+
for g in groups:
|
|
98
|
+
if isinstance(g, dict) and g.get("rid") == rid:
|
|
99
|
+
hit = g
|
|
100
|
+
break
|
|
101
|
+
if not hit:
|
|
102
|
+
print("⚠️ 校验失败:TARGET_RID 不在当前机器人已加入的群列表中。", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
gname = (hit.get("name") or "").strip()
|
|
105
|
+
name = name.strip()
|
|
106
|
+
if mode == "contains":
|
|
107
|
+
ok = name.lower() in gname.lower()
|
|
108
|
+
else:
|
|
109
|
+
ok = gname == name
|
|
110
|
+
if not ok:
|
|
111
|
+
print(
|
|
112
|
+
f"⚠️ 校验失败:群名与 TARGET_GROUP_NAME 不一致。期望匹配模式={mode} 配置名=[{name}] 实际=[{gname}]",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
return 1
|
|
116
|
+
print("✅ TARGET_RID 与 TARGET_GROUP_NAME 双因子校验通过。")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _send_request(body_obj: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
121
|
+
"""通用发送请求,支持 msg 或 md。"""
|
|
122
|
+
base = _base_url()
|
|
123
|
+
url = f"{base}/api/v1/claw/agent.message.send"
|
|
124
|
+
raw = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
|
|
125
|
+
req = request.Request(
|
|
126
|
+
url,
|
|
127
|
+
data=raw,
|
|
128
|
+
headers={
|
|
129
|
+
"Authorization": f"Bearer {_jwt()}",
|
|
130
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
131
|
+
"Accept": "application/json",
|
|
132
|
+
},
|
|
133
|
+
method="POST",
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
137
|
+
out = resp.read().decode("utf-8")
|
|
138
|
+
except error.HTTPError as exc:
|
|
139
|
+
out = exc.read().decode("utf-8", errors="replace")
|
|
140
|
+
raise RuntimeError(f"HTTP {exc.code}: {out}") from exc
|
|
141
|
+
data = json.loads(out)
|
|
142
|
+
if not data.get("success"):
|
|
143
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
144
|
+
return data
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _send_plain(rid: str, msg: str, timeout: float) -> dict[str, Any]:
|
|
148
|
+
"""发送纯文本消息。"""
|
|
149
|
+
uid = _agent_id()
|
|
150
|
+
body_obj: dict[str, Any] = {"rid": rid, "agentId": uid, "msg": msg}
|
|
151
|
+
return _send_request(body_obj, timeout)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _send_md(rid: str, md: list[dict[str, Any]], timeout: float) -> dict[str, Any]:
|
|
155
|
+
"""发送 Markdown AST 消息。"""
|
|
156
|
+
uid = _agent_id()
|
|
157
|
+
body_obj: dict[str, Any] = {"rid": rid, "agentId": uid, "md": md}
|
|
158
|
+
return _send_request(body_obj, timeout)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _chunk_text(text: str, size: int) -> list[str]:
|
|
162
|
+
if size <= 0:
|
|
163
|
+
return [text]
|
|
164
|
+
if len(text) <= size:
|
|
165
|
+
return [text]
|
|
166
|
+
return [text[i : i + size] for i in range(0, len(text), size)]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_send(args: argparse.Namespace) -> int:
|
|
170
|
+
"""发送纯文本消息(支持分段)。"""
|
|
171
|
+
rid = args.rid or _env("TARGET_RID")
|
|
172
|
+
if not rid:
|
|
173
|
+
print("⚠️ send 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
174
|
+
return 1
|
|
175
|
+
text = ""
|
|
176
|
+
if args.file:
|
|
177
|
+
path = os.path.expanduser(args.file)
|
|
178
|
+
with open(path, encoding="utf-8") as f:
|
|
179
|
+
text = f.read()
|
|
180
|
+
elif args.text is not None:
|
|
181
|
+
text = args.text
|
|
182
|
+
else:
|
|
183
|
+
print("⚠️ 需要 --file 或 --text", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
chunk_raw = _env("APPIA_MSG_CHUNK_SIZE")
|
|
186
|
+
chunk_size = int(chunk_raw) if chunk_raw else DEFAULT_CHUNK_SIZE
|
|
187
|
+
parts = _chunk_text(text, chunk_size)
|
|
188
|
+
for idx, part in enumerate(parts):
|
|
189
|
+
data = _send_plain(rid, part, args.timeout)
|
|
190
|
+
if args.json:
|
|
191
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
192
|
+
elif len(parts) > 1:
|
|
193
|
+
print(f"[{idx + 1}/{len(parts)}] ok: {data.get('data', data)!r}")
|
|
194
|
+
else:
|
|
195
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_send_md(args: argparse.Namespace) -> int:
|
|
200
|
+
"""发送 Markdown AST 消息。"""
|
|
201
|
+
rid = args.rid or _env("TARGET_RID")
|
|
202
|
+
if not rid:
|
|
203
|
+
print("⚠️ send-md 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
md: list[dict[str, Any]] = []
|
|
207
|
+
|
|
208
|
+
if args.md_json:
|
|
209
|
+
# 从 JSON 字符串解析
|
|
210
|
+
try:
|
|
211
|
+
md = json.loads(args.md_json)
|
|
212
|
+
except json.JSONDecodeError as exc:
|
|
213
|
+
print(f"⚠️ --md-json 解析失败: {exc}", file=sys.stderr)
|
|
214
|
+
return 1
|
|
215
|
+
elif args.md_file:
|
|
216
|
+
# 从 JSON 文件读取
|
|
217
|
+
path = os.path.expanduser(args.md_file)
|
|
218
|
+
try:
|
|
219
|
+
with open(path, encoding="utf-8") as f:
|
|
220
|
+
md = json.load(f)
|
|
221
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
222
|
+
print(f"⚠️ 读取 --md-file 失败: {exc}", file=sys.stderr)
|
|
223
|
+
return 1
|
|
224
|
+
elif args.text:
|
|
225
|
+
# 从纯文本自动构造简单 md 结构
|
|
226
|
+
md = [
|
|
227
|
+
{
|
|
228
|
+
"type": "PARAGRAPH",
|
|
229
|
+
"value": [{"type": "PLAIN_TEXT", "value": args.text}]
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
else:
|
|
233
|
+
print("⚠️ 需要 --md-json, --md-file 或 --text", file=sys.stderr)
|
|
234
|
+
return 1
|
|
235
|
+
|
|
236
|
+
if not isinstance(md, list):
|
|
237
|
+
print("⚠️ md 必须是数组格式", file=sys.stderr)
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
data = _send_md(rid, md, args.timeout)
|
|
241
|
+
if args.json:
|
|
242
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
243
|
+
else:
|
|
244
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main() -> int:
|
|
249
|
+
p = argparse.ArgumentParser(description="Appia Claw (groups.get / message.send)")
|
|
250
|
+
p.add_argument("--timeout", type=float, default=60.0)
|
|
251
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
252
|
+
|
|
253
|
+
gp = sub.add_parser("groups", help="GET agent.groups.get")
|
|
254
|
+
gp.add_argument("--json", action="store_true")
|
|
255
|
+
gp.set_defaults(_run=cmd_groups)
|
|
256
|
+
|
|
257
|
+
vp = sub.add_parser("verify-target", help="校验 TARGET_RID + TARGET_GROUP_NAME")
|
|
258
|
+
vp.set_defaults(_run=cmd_verify_target)
|
|
259
|
+
|
|
260
|
+
sp = sub.add_parser("send", help="POST agent.message.send(纯文本 msg;超出 APPIA_MSG_CHUNK_SIZE 则分多条)")
|
|
261
|
+
sp.add_argument("--file", "-f", help="长文本文件")
|
|
262
|
+
sp.add_argument("--text", "-t", help="短文本")
|
|
263
|
+
sp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
264
|
+
sp.add_argument("--json", action="store_true")
|
|
265
|
+
sp.set_defaults(_run=cmd_send)
|
|
266
|
+
|
|
267
|
+
smp = sub.add_parser("send-md", help="POST agent.message.send(Markdown AST md)")
|
|
268
|
+
smp.add_argument("--md-json", "-j", help="md JSON 字符串")
|
|
269
|
+
smp.add_argument("--md-file", "-f", help="md JSON 文件路径")
|
|
270
|
+
smp.add_argument("--text", "-t", help="纯文本(自动转为简单 md 结构)")
|
|
271
|
+
smp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
272
|
+
smp.add_argument("--json", action="store_true")
|
|
273
|
+
smp.set_defaults(_run=cmd_send_md)
|
|
274
|
+
|
|
275
|
+
args = p.parse_args()
|
|
276
|
+
runner = getattr(args, "_run", None)
|
|
277
|
+
if runner is None:
|
|
278
|
+
return 1
|
|
279
|
+
return int(runner(args))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
try:
|
|
284
|
+
raise SystemExit(main())
|
|
285
|
+
except Exception as exc: # pragma: no cover - CLI
|
|
286
|
+
print(f"⚠️ {exc}", file=sys.stderr)
|
|
287
|
+
raise SystemExit(1)
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sophnet-qa-install",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"types": [
|
|
5
|
-
"
|
|
5
|
+
"tmp"
|
|
6
6
|
],
|
|
7
7
|
"displayName": "",
|
|
8
8
|
"description": "",
|
|
9
9
|
"changelog": [
|
|
10
10
|
{
|
|
11
|
-
"version": "2.0.
|
|
12
|
-
"date": "2026-04-
|
|
11
|
+
"version": "2.0.1",
|
|
12
|
+
"date": "2026-04-28",
|
|
13
13
|
"changes": [
|
|
14
|
-
"
|
|
14
|
+
"目前有更有的方案,所有从内置skill中下架删除"
|
|
15
15
|
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"changes": [
|
|
19
|
+
"初次提交"
|
|
20
|
+
],
|
|
21
|
+
"date": "2026-04-09",
|
|
22
|
+
"version": "2.0.0"
|
|
16
23
|
}
|
|
17
24
|
],
|
|
18
25
|
"createdAt": "2026-04-09",
|
|
19
|
-
"updatedAt": "2026-04-
|
|
26
|
+
"updatedAt": "2026-04-28"
|
|
20
27
|
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sophnet-training-install",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"types": [
|
|
5
|
-
"
|
|
5
|
+
"tmp"
|
|
6
6
|
],
|
|
7
7
|
"displayName": "",
|
|
8
8
|
"description": "",
|
|
9
9
|
"changelog": [
|
|
10
10
|
{
|
|
11
|
-
"version": "2.0.
|
|
12
|
-
"date": "2026-04-
|
|
11
|
+
"version": "2.0.1",
|
|
12
|
+
"date": "2026-04-28",
|
|
13
13
|
"changes": [
|
|
14
|
-
"
|
|
14
|
+
"有更好的方案,所有取消内置,并下架删除"
|
|
15
15
|
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"changes": [
|
|
19
|
+
"初次提交"
|
|
20
|
+
],
|
|
21
|
+
"date": "2026-04-09",
|
|
22
|
+
"version": "2.0.0"
|
|
16
23
|
}
|
|
17
24
|
],
|
|
18
25
|
"createdAt": "2026-04-09",
|
|
19
|
-
"updatedAt": "2026-04-
|
|
26
|
+
"updatedAt": "2026-04-28"
|
|
20
27
|
}
|