sophhub 0.4.63 → 0.4.65
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/agents/ai-cs-qa/.config.json +59 -1
- package/package.json +1 -1
- package/skills/agent-install/skill.json +9 -2
- package/skills/agent-install/src/scripts/common.py +8 -1
- package/skills/agent-install/src/scripts/update_openclaw.py +5 -1
- package/skills/agent-install/src/tests/test_bot_api_exec.py +116 -0
- package/skills/claw-agent-get-send/skill.json +18 -4
- package/skills/claw-agent-get-send/src/SKILL.md +103 -27
- package/skills/claw-agent-get-send/src/pyproject.toml +2 -2
- package/skills/claw-agent-get-send/src/reference-http.md +296 -0
- package/skills/claw-agent-get-send/src/references/auth.md +115 -0
- package/skills/claw-agent-get-send/src/references/creator-delegate-api.md +90 -0
- package/skills/claw-agent-get-send/src/references/overview-api.md +47 -0
- package/skills/claw-agent-get-send/src/references/query-api.md +109 -0
- package/skills/claw-agent-get-send/src/references/rooms-api.md +79 -0
- package/skills/claw-agent-get-send/src/references/session-flow.md +61 -0
- package/skills/claw-agent-get-send/src/references/write-api.md +38 -0
- package/skills/claw-agent-get-send/src/scripts/appia_claw.py +290 -2
- package/skills/claw-agent-get-send/src/scripts/appia_common.py +258 -0
- package/skills/claw-agent-get-send/src/scripts/appia_extensions.py +370 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# 创建频道(Agent 扩展能力)
|
|
2
|
+
|
|
3
|
+
**调用方是机器人:** `POST /api/v1/claw/agent.channels.create`
|
|
4
|
+
鉴权:MCP JWT。Scope:`channels:write`(`scopes.request` → myAgent 卡片)。
|
|
5
|
+
服务端按创建者权限落库,但对 Agent 而言仍是机器人调接口,无用户 authToken。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 主入口
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
POST /api/v1/claw/agent.channels.create
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Body | 必填 | 说明 |
|
|
16
|
+
|------|------|------|
|
|
17
|
+
| `userId` / `agentId` | 是 | MCP 上下文 |
|
|
18
|
+
| `members` | 建议 | username 字符串数组;按需加人,**不必**含 Claw Agent 创建者 |
|
|
19
|
+
| `name` | 否 | 可空;空时服务端可按成员生成 |
|
|
20
|
+
| `depIds` | 否 | 部门 id,整部门加入 |
|
|
21
|
+
| `extraData` | 否 | 见下 |
|
|
22
|
+
| `readOnly` | 否 | 默认 false |
|
|
23
|
+
|
|
24
|
+
**`extraData` 常见字段:**
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"all": false,
|
|
29
|
+
"employeeType": [],
|
|
30
|
+
"topic": "",
|
|
31
|
+
"description": "",
|
|
32
|
+
"rt": "p",
|
|
33
|
+
"broadcast": false,
|
|
34
|
+
"appiaUsage": []
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
会议专用频道可用 `"appiaUsage": ["Room_Sort_Meeting"]`。
|
|
39
|
+
|
|
40
|
+
### 示例
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -X POST "$SITE_URL/api/v1/claw/agent.channels.create" \
|
|
44
|
+
-H "Authorization: Bearer $MCP_JWT" -H "Content-Type: application/json" \
|
|
45
|
+
-d '{
|
|
46
|
+
"userId": "'"$USER_ID"'",
|
|
47
|
+
"agentId": "'"$AGENT_ID"'",
|
|
48
|
+
"name": "产品周会群",
|
|
49
|
+
"members": ["zhangsan", "lisi"],
|
|
50
|
+
"extraData": { "topic": "产品" }
|
|
51
|
+
}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
成功返回 `data.rid` / `data.channel`。可用作后续 Bot 入群发消息的 rid。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 推荐链路
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
用户:建一个有张三李四的群
|
|
62
|
+
→ users.search 解析 username
|
|
63
|
+
→ members = [zhangsan, lisi](按需;不要默认加 Claw 创建者)
|
|
64
|
+
→ 若提问者本人也要进群 → 再 search 其 username 加入
|
|
65
|
+
→ 确认 name
|
|
66
|
+
→ 确认 Agent 已有 channels:write(否则 scopes.request)
|
|
67
|
+
→ POST claw/agent.channels.create
|
|
68
|
+
→ 返回 rid
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> 预定会议(`agent.meeting.book`)暂未纳入本 skill;需要时引导用户在 Appia 网页操作。
|
|
72
|
+
|
|
73
|
+
**注意:**
|
|
74
|
+
|
|
75
|
+
- **不要默认把 Claw Agent 创建者加进 `members`**;只加用户明确要的人。
|
|
76
|
+
- 提问者要进群时单独加入其 username。
|
|
77
|
+
- 成员用 **username**,不是显示名;先用 `agent.users.search`。
|
|
78
|
+
- 缺 `channels:write` → `agent.scopes.request`,请创建者在 myAgent 点授权。
|
|
79
|
+
- 私有群 `groups.create` / 讨论组等尚未挂接;当前 Agent 只支持上述 `channels.create`。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Session 启动与轮询(MCP)
|
|
2
|
+
|
|
3
|
+
业务见 [../SKILL.md](../SKILL.md)。鉴权见 [auth.md](auth.md)。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 启动顺序
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. Read USER.md → SITE_URL、userId、agentId、MCP_JWT
|
|
11
|
+
2. 缺配置 → 请用户在 Appia 创建 Claw Agent(不要向用户要登录态 / 短信)
|
|
12
|
+
3. GET agent.groups.get → 已入群频道
|
|
13
|
+
4. 可选:GET agent.todos.list / agent.scopes.get 探测扩展能力
|
|
14
|
+
5. 写接口若 SCOPE_DENIED → scopes.request,请用户去 myAgent 点「授权」给本机器人,再重试
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## USER.md
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
## Appia Claw
|
|
23
|
+
|
|
24
|
+
| 项目 | 值 |
|
|
25
|
+
|------|-----|
|
|
26
|
+
| SITE_URL | `https://ssc.appia.cn` |
|
|
27
|
+
| userId | `<创建者 RC ID>` |
|
|
28
|
+
| agentId | `<OpenClaw agentId>` |
|
|
29
|
+
| MCP JWT | `<Bearer token>` |
|
|
30
|
+
|
|
31
|
+
### 企业识别码 → 地址(选型时用,大小写不敏感)
|
|
32
|
+
|
|
33
|
+
| 识别码 | SITE_URL |
|
|
34
|
+
|--------|----------|
|
|
35
|
+
| SSC | https://ssc.appia.cn |
|
|
36
|
+
| BITMAIN | https://appia.cn |
|
|
37
|
+
| SOPHGO | https://sophgo.appia.cn |
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## sender_id → rid
|
|
43
|
+
|
|
44
|
+
| 格式 | rid |
|
|
45
|
+
|------|-----|
|
|
46
|
+
| `SSC-{rid}-{YYYYMMDD}` | 中间段 |
|
|
47
|
+
| `openclaw-control-ui` | 无 rid,问用户 |
|
|
48
|
+
|
|
49
|
+
同条 stream 可能带 **`requestUserId`**(提问者)。总览行接口未落地;有该字段时也勿冒充去调不存在的 `overview.read`。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 轮询(cron)
|
|
54
|
+
|
|
55
|
+
`groups.get` → `messages.get` → 识别 @我 → 入群回复 / 扩展能力 API → `message.send`。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 相关
|
|
60
|
+
|
|
61
|
+
- [auth.md](auth.md) · [creator-delegate-api.md](creator-delegate-api.md) · [claw-api.md](claw-api.md)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 写操作
|
|
2
|
+
|
|
3
|
+
调用方是机器人,MCP JWT。
|
|
4
|
+
**缺 write scope:** `scopes.request` → 用户在 myAgent 点「授权」→ **同一机器人**重试。不要要 authToken。
|
|
5
|
+
鉴权见 [auth.md](auth.md)。
|
|
6
|
+
|
|
7
|
+
写前向用户确认。只读见 [query-api.md](query-api.md)。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. 更新待办状态
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
POST /api/v1/claw/agent.todos.update-status
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Body | 说明 |
|
|
18
|
+
|------|------|
|
|
19
|
+
| `userId` / `agentId` | 必填 |
|
|
20
|
+
| `id` | 待办 id |
|
|
21
|
+
| `status` | 状态 |
|
|
22
|
+
| `reminderTime` | 可选 |
|
|
23
|
+
|
|
24
|
+
Scope:`todos:write`。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. 创建频道
|
|
29
|
+
|
|
30
|
+
见 [rooms-api.md](rooms-api.md)。需要 `channels:write`。
|
|
31
|
+
|
|
32
|
+
> 预定会议(`agent.meeting.book`)暂未纳入本 skill;需要时引导用户在 Appia 网页操作或用独立 skill。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. 尚未包装
|
|
37
|
+
|
|
38
|
+
清通知、从消息创建待办、取消会议等**没有** claw 封装。缺能力时告知用户用网页操作,不要引导 RC 登录。
|
|
@@ -309,7 +309,7 @@ def _chunk_text(text: str, size: int) -> list[str]:
|
|
|
309
309
|
|
|
310
310
|
|
|
311
311
|
def cmd_send(args: argparse.Namespace) -> int:
|
|
312
|
-
"""
|
|
312
|
+
"""发送纯文本消息(支持分段与 @提及)。"""
|
|
313
313
|
rid = args.rid or _cfg("TARGET_RID")
|
|
314
314
|
if not rid:
|
|
315
315
|
print("⚠️ send 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
@@ -324,6 +324,10 @@ def cmd_send(args: argparse.Namespace) -> int:
|
|
|
324
324
|
else:
|
|
325
325
|
print("⚠️ 需要 --file 或 --text", file=sys.stderr)
|
|
326
326
|
return 1
|
|
327
|
+
# 解析 @提及(传入 rid 以校验群内成员)
|
|
328
|
+
mentions = _resolve_mentions(getattr(args, "mention", "") or "", args.timeout, rid=rid)
|
|
329
|
+
if mentions:
|
|
330
|
+
text = " ".join(mentions) + " " + text
|
|
327
331
|
chunk_raw = _cfg("APPIA_MSG_CHUNK_SIZE")
|
|
328
332
|
chunk_size = int(chunk_raw) if chunk_raw else DEFAULT_CHUNK_SIZE
|
|
329
333
|
parts = _chunk_text(text, chunk_size)
|
|
@@ -364,11 +368,16 @@ def cmd_send_md(args: argparse.Namespace) -> int:
|
|
|
364
368
|
print(f"⚠️ 读取 --md-file 失败: {exc}", file=sys.stderr)
|
|
365
369
|
return 1
|
|
366
370
|
elif args.text:
|
|
371
|
+
# 解析 @提及(传入 rid 以校验群内成员)
|
|
372
|
+
text = args.text
|
|
373
|
+
mentions = _resolve_mentions(getattr(args, "mention", "") or "", args.timeout, rid=rid)
|
|
374
|
+
if mentions:
|
|
375
|
+
text = " ".join(mentions) + " " + text
|
|
367
376
|
# 从纯文本自动构造简单 md 结构
|
|
368
377
|
md = [
|
|
369
378
|
{
|
|
370
379
|
"type": "PARAGRAPH",
|
|
371
|
-
"value": [{"type": "PLAIN_TEXT", "value":
|
|
380
|
+
"value": [{"type": "PLAIN_TEXT", "value": text}]
|
|
372
381
|
}
|
|
373
382
|
]
|
|
374
383
|
else:
|
|
@@ -415,6 +424,253 @@ def cmd_send_file(args: argparse.Namespace) -> int:
|
|
|
415
424
|
return 0
|
|
416
425
|
|
|
417
426
|
|
|
427
|
+
# ------------------------------------------------------------------
|
|
428
|
+
# 入群扩展子命令:messages.get / announcements.get / file.fetch / recall / leave
|
|
429
|
+
# 复用上方 _cfg / _jwt / _agent_id / _user_id / _base_url,不改动既有子命令。
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _claw_get(path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
434
|
+
base = _base_url()
|
|
435
|
+
q = parse.urlencode({k: v for k, v in params.items() if v is not None and v != ""})
|
|
436
|
+
url = f"{base}/api/v1/claw/{path}?{q}" if q else f"{base}/api/v1/claw/{path}"
|
|
437
|
+
req = request.Request(
|
|
438
|
+
url,
|
|
439
|
+
headers={"Authorization": f"Bearer {_jwt()}", "Accept": "application/json"},
|
|
440
|
+
method="GET",
|
|
441
|
+
)
|
|
442
|
+
try:
|
|
443
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
444
|
+
body = resp.read().decode("utf-8")
|
|
445
|
+
except error.HTTPError as exc:
|
|
446
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
447
|
+
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
|
448
|
+
data = json.loads(body)
|
|
449
|
+
if not data.get("success"):
|
|
450
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
451
|
+
return data
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _claw_post(path: str, body_obj: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
455
|
+
base = _base_url()
|
|
456
|
+
url = f"{base}/api/v1/claw/{path}"
|
|
457
|
+
raw = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
|
|
458
|
+
req = request.Request(
|
|
459
|
+
url,
|
|
460
|
+
data=raw,
|
|
461
|
+
headers={
|
|
462
|
+
"Authorization": f"Bearer {_jwt()}",
|
|
463
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
464
|
+
"Accept": "application/json",
|
|
465
|
+
},
|
|
466
|
+
method="POST",
|
|
467
|
+
)
|
|
468
|
+
try:
|
|
469
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
470
|
+
out = resp.read().decode("utf-8")
|
|
471
|
+
except error.HTTPError as exc:
|
|
472
|
+
out = exc.read().decode("utf-8", errors="replace")
|
|
473
|
+
raise RuntimeError(f"HTTP {exc.code}: {out}") from exc
|
|
474
|
+
data = json.loads(out)
|
|
475
|
+
if not data.get("success"):
|
|
476
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
477
|
+
return data
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _resolve_mentions(names: str, timeout: float, rid: str | None = None) -> list[str]:
|
|
481
|
+
"""将逗号分隔的显示名称(或 username)通过 agent.users.search 解析为 @mention 列表。
|
|
482
|
+
|
|
483
|
+
规则:
|
|
484
|
+
1. 先精确匹配 name 或 username,否则取搜索结果第一条。找不到则 stderr 警告并跳过。
|
|
485
|
+
2. 若指定 rid,则拉取频道成员,检查用户是否在群内:在群内用 @username,不在群内用 @显示名称。
|
|
486
|
+
"""
|
|
487
|
+
if not names or not names.strip():
|
|
488
|
+
return []
|
|
489
|
+
|
|
490
|
+
# 有 rid 时预取频道成员 username 集合(一次拉取,全员复用)
|
|
491
|
+
member_usernames: set[str] = set()
|
|
492
|
+
if rid:
|
|
493
|
+
try:
|
|
494
|
+
mparams: dict[str, Any] = {
|
|
495
|
+
"userId": _user_id(),
|
|
496
|
+
"agentId": _agent_id(),
|
|
497
|
+
"rid": rid,
|
|
498
|
+
}
|
|
499
|
+
mdata = _claw_get("agent.channel.members.list", mparams, timeout)
|
|
500
|
+
members = (mdata.get("data") or {}).get("members") or []
|
|
501
|
+
for m in members:
|
|
502
|
+
if isinstance(m, dict) and m.get("username"):
|
|
503
|
+
member_usernames.add(m["username"])
|
|
504
|
+
except Exception as exc:
|
|
505
|
+
print(f"⚠️ 获取频道成员失败(将跳过群内校验): {exc}", file=sys.stderr)
|
|
506
|
+
|
|
507
|
+
result: list[str] = []
|
|
508
|
+
for raw_name in names.split(","):
|
|
509
|
+
name = raw_name.strip()
|
|
510
|
+
if not name:
|
|
511
|
+
continue
|
|
512
|
+
params: dict[str, Any] = {
|
|
513
|
+
"userId": _user_id(),
|
|
514
|
+
"agentId": _agent_id(),
|
|
515
|
+
"q": name,
|
|
516
|
+
"limit": 5,
|
|
517
|
+
}
|
|
518
|
+
data = _claw_get("agent.users.search", params, timeout)
|
|
519
|
+
users = (data.get("data") or {}).get("users") or []
|
|
520
|
+
if not users:
|
|
521
|
+
# 查不到用户,直接用显示名称
|
|
522
|
+
print(f"⚠️ 未找到用户: {name},将直接 @显示名称", file=sys.stderr)
|
|
523
|
+
result.append(f"@{name}")
|
|
524
|
+
continue
|
|
525
|
+
# 精确匹配优先
|
|
526
|
+
matched: dict[str, Any] | None = None
|
|
527
|
+
for u in users:
|
|
528
|
+
if u.get("name") == name or u.get("username") == name:
|
|
529
|
+
matched = u
|
|
530
|
+
break
|
|
531
|
+
if matched is None:
|
|
532
|
+
matched = users[0]
|
|
533
|
+
matched_name = (matched.get("name") or "").strip()
|
|
534
|
+
username = (matched.get("username") or "").strip()
|
|
535
|
+
if not matched_name:
|
|
536
|
+
matched_name = name
|
|
537
|
+
if not username:
|
|
538
|
+
print(f"⚠️ 用户 {name} 缺少 username 字段,将直接 @显示名称", file=sys.stderr)
|
|
539
|
+
result.append(f"@{matched_name}")
|
|
540
|
+
continue
|
|
541
|
+
# 有 rid 时校验是否在群内
|
|
542
|
+
if rid and member_usernames and username not in member_usernames:
|
|
543
|
+
result.append(f"@{matched_name}")
|
|
544
|
+
else:
|
|
545
|
+
result.append(f"@{username}")
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def cmd_get_messages(args: argparse.Namespace) -> int:
|
|
550
|
+
"""读取频道历史消息(倒序,nextLatest 翻页)。"""
|
|
551
|
+
rid = args.rid or _cfg("TARGET_RID")
|
|
552
|
+
if not rid:
|
|
553
|
+
print("⚠️ get-messages 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
554
|
+
return 1
|
|
555
|
+
params: dict[str, Any] = {
|
|
556
|
+
"userId": _user_id(),
|
|
557
|
+
"agentId": _agent_id(),
|
|
558
|
+
"rid": rid,
|
|
559
|
+
"count": args.count,
|
|
560
|
+
"offset": args.offset,
|
|
561
|
+
"latest": args.latest,
|
|
562
|
+
"showThreadMessages": "true" if args.show_thread else "false",
|
|
563
|
+
}
|
|
564
|
+
data = _claw_get("agent.messages.get", params, args.timeout)
|
|
565
|
+
if args.json:
|
|
566
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
567
|
+
return 0
|
|
568
|
+
payload = data.get("data") or {}
|
|
569
|
+
msgs = payload.get("messages") or []
|
|
570
|
+
print(f"rid={rid} count={len(msgs)} nextLatest={payload.get('nextLatest', '')}")
|
|
571
|
+
for m in msgs:
|
|
572
|
+
if not isinstance(m, dict):
|
|
573
|
+
continue
|
|
574
|
+
print(f"- ts={m.get('ts')} from={m.get('u', {}).get('username', '')} msg={m.get('msg', '')}")
|
|
575
|
+
return 0
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
_ANN_TYPE_MAP = {"0": "普通", "1": "会议", "2": "纪要", "3": "HORS"}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def cmd_get_announcements(args: argparse.Namespace) -> int:
|
|
582
|
+
"""读取频道公告,支持类型过滤。"""
|
|
583
|
+
rid = args.rid or _cfg("TARGET_RID")
|
|
584
|
+
if not rid:
|
|
585
|
+
print("⚠️ get-announcements 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
586
|
+
return 1
|
|
587
|
+
params: dict[str, Any] = {
|
|
588
|
+
"userId": _user_id(),
|
|
589
|
+
"agentId": _agent_id(),
|
|
590
|
+
"rid": rid,
|
|
591
|
+
"type": args.type,
|
|
592
|
+
"announcementType": args.announcement_type,
|
|
593
|
+
}
|
|
594
|
+
data = _claw_get("agent.announcements.get", params, args.timeout)
|
|
595
|
+
if args.json:
|
|
596
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
597
|
+
return 0
|
|
598
|
+
payload = data.get("data") or {}
|
|
599
|
+
anns = payload.get("announcements") or payload.get("items") or []
|
|
600
|
+
print(f"rid={rid} total={len(anns)}")
|
|
601
|
+
for a in anns:
|
|
602
|
+
if not isinstance(a, dict):
|
|
603
|
+
continue
|
|
604
|
+
t = str(a.get("type", a.get("announcementType", "")))
|
|
605
|
+
tname = _ANN_TYPE_MAP.get(t, t)
|
|
606
|
+
print(f"- type={tname} ts={a.get('ts', a.get('createdAt', ''))} msg={a.get('message', a.get('msg', ''))}")
|
|
607
|
+
return 0
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def cmd_fetch_file(args: argparse.Namespace) -> int:
|
|
611
|
+
"""下载频道附件(agent.file.fetch,二进制流)。"""
|
|
612
|
+
if not args.file_id:
|
|
613
|
+
print("⚠️ fetch-file 需要 --file-id", file=sys.stderr)
|
|
614
|
+
return 1
|
|
615
|
+
params: dict[str, Any] = {
|
|
616
|
+
"userId": _user_id(),
|
|
617
|
+
"agentId": _agent_id(),
|
|
618
|
+
"fileId": args.file_id,
|
|
619
|
+
"rid": args.rid or _cfg("TARGET_RID"),
|
|
620
|
+
}
|
|
621
|
+
base = _base_url()
|
|
622
|
+
q = parse.urlencode({k: v for k, v in params.items() if v is not None and v != ""})
|
|
623
|
+
url = f"{base}/api/v1/claw/agent.file.fetch?{q}"
|
|
624
|
+
req = request.Request(
|
|
625
|
+
url,
|
|
626
|
+
headers={"Authorization": f"Bearer {_jwt()}"},
|
|
627
|
+
method="GET",
|
|
628
|
+
)
|
|
629
|
+
try:
|
|
630
|
+
with request.urlopen(req, timeout=args.timeout) as resp:
|
|
631
|
+
content = resp.read()
|
|
632
|
+
except error.HTTPError as exc:
|
|
633
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
634
|
+
print(f"⚠️ HTTP {exc.code}: {body}", file=sys.stderr)
|
|
635
|
+
return 1
|
|
636
|
+
out_path = os.path.expanduser(args.output)
|
|
637
|
+
with open(out_path, "wb") as fh:
|
|
638
|
+
fh.write(content)
|
|
639
|
+
print(f"saved: {out_path} bytes={len(content)}")
|
|
640
|
+
return 0
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def cmd_recall(args: argparse.Namespace) -> int:
|
|
644
|
+
"""撤回 bot 自己发送的消息。"""
|
|
645
|
+
if not args.message_id:
|
|
646
|
+
print("⚠️ recall 需要 --message-id", file=sys.stderr)
|
|
647
|
+
return 1
|
|
648
|
+
body_obj: dict[str, Any] = {
|
|
649
|
+
"userId": _user_id(),
|
|
650
|
+
"agentId": _agent_id(),
|
|
651
|
+
"messageId": args.message_id,
|
|
652
|
+
}
|
|
653
|
+
data = _claw_post("agent.message.recall", body_obj, args.timeout)
|
|
654
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
655
|
+
return 0
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def cmd_leave(args: argparse.Namespace) -> int:
|
|
659
|
+
"""bot 退出指定群聊。"""
|
|
660
|
+
rid = args.rid or _cfg("TARGET_RID")
|
|
661
|
+
if not rid:
|
|
662
|
+
print("⚠️ leave 需要 TARGET_RID 或 --rid", file=sys.stderr)
|
|
663
|
+
return 1
|
|
664
|
+
body_obj: dict[str, Any] = {
|
|
665
|
+
"userId": _user_id(),
|
|
666
|
+
"agentId": _agent_id(),
|
|
667
|
+
"rid": rid,
|
|
668
|
+
}
|
|
669
|
+
data = _claw_post("agent.group.leave", body_obj, args.timeout)
|
|
670
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
671
|
+
return 0
|
|
672
|
+
|
|
673
|
+
|
|
418
674
|
def main() -> int:
|
|
419
675
|
global _CRED
|
|
420
676
|
p = argparse.ArgumentParser(description="Appia Claw (groups.get / message.send)")
|
|
@@ -438,6 +694,7 @@ def main() -> int:
|
|
|
438
694
|
sp.add_argument("--file", "-f", help="长文本文件")
|
|
439
695
|
sp.add_argument("--text", "-t", help="短文本")
|
|
440
696
|
sp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
697
|
+
sp.add_argument("--mention", "-m", help="要 @ 的用户显示名称或 username,逗号分隔(如:张三,李四)。先通过 API 查用户名再 @")
|
|
441
698
|
sp.add_argument("--json", action="store_true")
|
|
442
699
|
sp.set_defaults(_run=cmd_send)
|
|
443
700
|
|
|
@@ -446,6 +703,7 @@ def main() -> int:
|
|
|
446
703
|
smp.add_argument("--md-file", "-f", help="md JSON 文件路径")
|
|
447
704
|
smp.add_argument("--text", "-t", help="纯文本(自动转为简单 md 结构)")
|
|
448
705
|
smp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
706
|
+
smp.add_argument("--mention", "-m", help="要 @ 的用户显示名称或 username,逗号分隔(如:张三,李四)。先通过 API 查用户名再 @")
|
|
449
707
|
smp.add_argument("--json", action="store_true")
|
|
450
708
|
smp.set_defaults(_run=cmd_send_md)
|
|
451
709
|
|
|
@@ -456,6 +714,36 @@ def main() -> int:
|
|
|
456
714
|
sfp.add_argument("--json", action="store_true")
|
|
457
715
|
sfp.set_defaults(_run=cmd_send_file)
|
|
458
716
|
|
|
717
|
+
gmp = sub.add_parser("get-messages", help="GET agent.messages.get(频道历史,倒序,nextLatest 翻页)")
|
|
718
|
+
gmp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
719
|
+
gmp.add_argument("--count", type=int, default=20)
|
|
720
|
+
gmp.add_argument("--offset", type=int, default=0)
|
|
721
|
+
gmp.add_argument("--latest", help="ISO 时间,返回此时间之前的消息(翻页用 nextLatest)")
|
|
722
|
+
gmp.add_argument("--show-thread", dest="show_thread", action="store_true", default=True)
|
|
723
|
+
gmp.add_argument("--json", action="store_true")
|
|
724
|
+
gmp.set_defaults(_run=cmd_get_messages)
|
|
725
|
+
|
|
726
|
+
gap = sub.add_parser("get-announcements", help="GET agent.announcements.get(频道公告,可按类型过滤)")
|
|
727
|
+
gap.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
728
|
+
gap.add_argument("--type", help="normal/meeting/summary/hors 或 0-3")
|
|
729
|
+
gap.add_argument("--announcement-type", dest="announcement_type", help="同 --type 的另一字段名")
|
|
730
|
+
gap.add_argument("--json", action="store_true")
|
|
731
|
+
gap.set_defaults(_run=cmd_get_announcements)
|
|
732
|
+
|
|
733
|
+
ffp = sub.add_parser("fetch-file", help="GET agent.file.fetch(下载频道附件,二进制流)")
|
|
734
|
+
ffp.add_argument("--file-id", required=True, help="附件 fileId")
|
|
735
|
+
ffp.add_argument("--rid", help="覆盖 TARGET_RID(建议传,校验文件属于该频道)")
|
|
736
|
+
ffp.add_argument("-o", "--output", required=True, help="保存到本地路径")
|
|
737
|
+
ffp.set_defaults(_run=cmd_fetch_file)
|
|
738
|
+
|
|
739
|
+
rp = sub.add_parser("recall", help="POST agent.message.recall(撤回 bot 自己发的消息)")
|
|
740
|
+
rp.add_argument("--message-id", required=True, help="消息 id")
|
|
741
|
+
rp.set_defaults(_run=cmd_recall)
|
|
742
|
+
|
|
743
|
+
lp = sub.add_parser("leave", help="POST agent.group.leave(bot 退出群聊)")
|
|
744
|
+
lp.add_argument("--rid", help="覆盖 TARGET_RID")
|
|
745
|
+
lp.set_defaults(_run=cmd_leave)
|
|
746
|
+
|
|
459
747
|
args = p.parse_args()
|
|
460
748
|
if args.cred_file:
|
|
461
749
|
_CRED = load_cred_file(args.cred_file)
|