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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sophhub",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "SophHub CLI - Manage and download AI Agent skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "bot-api-status",
3
- "version": "1.0.2",
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-22"
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
- 🔑 新密钥:<new_secret>
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
+
@@ -1,5 +1,5 @@
1
1
  [project]
2
2
  name = "bot-api-status"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  description = "Manage bot-api lifecycle and secret for the current Agent"
5
5
  requires-python = ">=3.10"
@@ -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
- print(f"🔑 新密钥:{secret}")
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,5 @@
1
+ [project]
2
+ name = "claw-agent-get-send"
3
+ version = "1.0.0"
4
+ description = "Appia Claw groups.get / message.send helper"
5
+ requires-python = ">=3.10"
@@ -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.0",
3
+ "version": "2.0.1",
4
4
  "types": [
5
- "builtin"
5
+ "tmp"
6
6
  ],
7
7
  "displayName": "",
8
8
  "description": "",
9
9
  "changelog": [
10
10
  {
11
- "version": "2.0.0",
12
- "date": "2026-04-09",
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-09"
26
+ "updatedAt": "2026-04-28"
20
27
  }
@@ -1,20 +1,27 @@
1
1
  {
2
2
  "name": "sophnet-training-install",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "types": [
5
- "builtin"
5
+ "tmp"
6
6
  ],
7
7
  "displayName": "",
8
8
  "description": "",
9
9
  "changelog": [
10
10
  {
11
- "version": "2.0.0",
12
- "date": "2026-04-09",
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-09"
26
+ "updatedAt": "2026-04-28"
20
27
  }