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,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Appia Claw 共享 helper:凭证解析 + JWT + 通用 HTTP(GET/POST JSON/multipart/raw)。
|
|
3
|
+
|
|
4
|
+
供 appia_extensions.py 复用。appia_claw.py 保留自己的实现不动。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import mimetypes
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib import error, parse, request
|
|
15
|
+
|
|
16
|
+
DEFAULT_APPIA_BASE_URL = "https://sophgo.appia.cn"
|
|
17
|
+
|
|
18
|
+
# 凭证文件解析后的规范字段;未用凭证文件时为 None。
|
|
19
|
+
CRED: dict[str, str] | None = None
|
|
20
|
+
|
|
21
|
+
# (规范名, JSON/文件中允许的别名)。任一别名非空即写入规范键。
|
|
22
|
+
CRED_CANON_ALIASES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
23
|
+
("APPIA_BASE_URL", ("APPIA_BASE_URL", "appia_base_url", "base_url", "APPIA_HOST", "appia_host")),
|
|
24
|
+
("CLAW_JWT", ("CLAW_JWT", "claw_jwt", "jwt", "APPIA_CLAW_JWT", "appia_claw_jwt", "MCP_JWT", "mcp_jwt")),
|
|
25
|
+
("APP_AGENT_ID", ("APP_AGENT_ID", "app_agent_id", "agent_id", "AGENT_ID")),
|
|
26
|
+
(
|
|
27
|
+
"CLAW_USER_ID",
|
|
28
|
+
(
|
|
29
|
+
"CLAW_USER_ID",
|
|
30
|
+
"claw_user_id",
|
|
31
|
+
"user_id",
|
|
32
|
+
"USER_ID",
|
|
33
|
+
"creator_user_id",
|
|
34
|
+
"APPIA_USER_ID",
|
|
35
|
+
"appia_user_id",
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
("TARGET_RID", ("TARGET_RID", "target_rid")),
|
|
39
|
+
("TARGET_GROUP_NAME", ("TARGET_GROUP_NAME", "target_group_name")),
|
|
40
|
+
("TARGET_GROUP_MATCH", ("TARGET_GROUP_MATCH", "target_group_match")),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _scalar_from_raw(raw: dict[str, Any], aliases: tuple[str, ...]) -> str:
|
|
45
|
+
for key in aliases:
|
|
46
|
+
if key not in raw:
|
|
47
|
+
continue
|
|
48
|
+
val = raw[key]
|
|
49
|
+
if val is None:
|
|
50
|
+
continue
|
|
51
|
+
s = str(val).strip()
|
|
52
|
+
if s:
|
|
53
|
+
return s
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_cred_file(path: str) -> dict[str, str]:
|
|
58
|
+
exp = os.path.expanduser(path)
|
|
59
|
+
try:
|
|
60
|
+
with open(exp, encoding="utf-8") as f:
|
|
61
|
+
raw_any: Any = json.load(f)
|
|
62
|
+
except OSError as exc:
|
|
63
|
+
sys.exit(f"无法读取凭证文件 {exp}: {exc}")
|
|
64
|
+
except json.JSONDecodeError as exc:
|
|
65
|
+
sys.exit(f"凭证文件 JSON 无效 {exp}: {exc}")
|
|
66
|
+
if not isinstance(raw_any, dict):
|
|
67
|
+
sys.exit("凭证文件顶层必须是 JSON 对象")
|
|
68
|
+
out: dict[str, str] = {}
|
|
69
|
+
for canon, aliases in CRED_CANON_ALIASES:
|
|
70
|
+
v = _scalar_from_raw(raw_any, aliases)
|
|
71
|
+
if v:
|
|
72
|
+
out[canon] = v
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def init_cred(cred_file: str | None) -> None:
|
|
77
|
+
"""在 main() 解析完 --cred-file 后调用,设置模块级 CRED。"""
|
|
78
|
+
global CRED
|
|
79
|
+
if cred_file:
|
|
80
|
+
CRED = load_cred_file(cred_file)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _env(name: str, *fallbacks: str) -> str:
|
|
84
|
+
for key in (name,) + fallbacks:
|
|
85
|
+
v = os.environ.get(key, "").strip()
|
|
86
|
+
if v:
|
|
87
|
+
return v
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cfg(canon: str, *env_fallbacks: str) -> str:
|
|
92
|
+
"""凭证文件非空字段优先,否则回退环境变量。"""
|
|
93
|
+
if CRED:
|
|
94
|
+
v = CRED.get(canon, "").strip()
|
|
95
|
+
if v:
|
|
96
|
+
return v
|
|
97
|
+
return _env(canon, *env_fallbacks)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def base_url() -> str:
|
|
101
|
+
base = cfg("APPIA_BASE_URL", "APPIA_HOST").rstrip("/")
|
|
102
|
+
if not base:
|
|
103
|
+
base = DEFAULT_APPIA_BASE_URL.rstrip("/")
|
|
104
|
+
return base
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def jwt() -> str:
|
|
108
|
+
tok = cfg("CLAW_JWT", "APPIA_CLAW_JWT", "MCP_JWT")
|
|
109
|
+
if not tok:
|
|
110
|
+
sys.exit("缺少配置:凭证文件或环境变量 CLAW_JWT(或 MCP_JWT)")
|
|
111
|
+
return tok
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def agent_id() -> str:
|
|
115
|
+
aid = cfg("APP_AGENT_ID", "AGENT_ID")
|
|
116
|
+
if not aid:
|
|
117
|
+
sys.exit("缺少配置:凭证文件或环境变量 APP_AGENT_ID(或 AGENT_ID)")
|
|
118
|
+
return aid
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def user_id() -> str:
|
|
122
|
+
uid = cfg("CLAW_USER_ID", "USER_ID", "APPIA_USER_ID")
|
|
123
|
+
if not uid:
|
|
124
|
+
sys.exit(
|
|
125
|
+
"缺少配置:凭证文件或环境变量 CLAW_USER_ID(或 USER_ID、APPIA_USER_ID、creator_user_id)"
|
|
126
|
+
)
|
|
127
|
+
return uid
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _auth_headers() -> dict[str, str]:
|
|
131
|
+
return {"Authorization": f"Bearer {jwt()}", "Accept": "application/json"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def claw_url(path: str) -> str:
|
|
135
|
+
"""path 形如 'agent.users.search',返回 {base}/api/v1/claw/{path}。"""
|
|
136
|
+
return f"{base_url()}/api/v1/claw/{path}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def api_get(path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
140
|
+
"""GET {base}/api/v1/claw/{path}?params,返回 JSON dict。"""
|
|
141
|
+
qs = parse.urlencode({k: v for k, v in params.items() if v is not None and v != ""})
|
|
142
|
+
url = f"{claw_url(path)}?{qs}" if qs else claw_url(path)
|
|
143
|
+
req = request.Request(url, headers=_auth_headers(), method="GET")
|
|
144
|
+
try:
|
|
145
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
146
|
+
body = resp.read().decode("utf-8")
|
|
147
|
+
except error.HTTPError as exc:
|
|
148
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
149
|
+
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
|
150
|
+
data = json.loads(body)
|
|
151
|
+
if not data.get("success"):
|
|
152
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
153
|
+
return data
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def api_get_raw(path: str, params: dict[str, Any], timeout: float) -> bytes:
|
|
157
|
+
"""GET 二进制流(如 agent.file.fetch),返回原始 bytes。"""
|
|
158
|
+
qs = parse.urlencode({k: v for k, v in params.items() if v is not None and v != ""})
|
|
159
|
+
url = f"{claw_url(path)}?{qs}" if qs else claw_url(path)
|
|
160
|
+
req = request.Request(url, headers=_auth_headers(), method="GET")
|
|
161
|
+
try:
|
|
162
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
163
|
+
return resp.read()
|
|
164
|
+
except error.HTTPError as exc:
|
|
165
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
166
|
+
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def api_post_json(path: str, body: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
170
|
+
"""POST JSON 到 {base}/api/v1/claw/{path},返回 JSON dict。"""
|
|
171
|
+
raw = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
172
|
+
headers = _auth_headers()
|
|
173
|
+
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
174
|
+
req = request.Request(claw_url(path), data=raw, headers=headers, method="POST")
|
|
175
|
+
try:
|
|
176
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
177
|
+
out = resp.read().decode("utf-8")
|
|
178
|
+
except error.HTTPError as exc:
|
|
179
|
+
out = exc.read().decode("utf-8", errors="replace")
|
|
180
|
+
raise RuntimeError(f"HTTP {exc.code}: {out}") from exc
|
|
181
|
+
data = json.loads(out)
|
|
182
|
+
if not data.get("success"):
|
|
183
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
184
|
+
return data
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def api_post_multipart(
|
|
188
|
+
path: str,
|
|
189
|
+
fields: dict[str, str],
|
|
190
|
+
file_field: str,
|
|
191
|
+
file_path: str,
|
|
192
|
+
timeout: float,
|
|
193
|
+
file_field_name: str | None = None,
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
"""POST multipart/form-data 到 claw/{path},单文件。返回 JSON dict。
|
|
196
|
+
|
|
197
|
+
file_field_name 为上传表单里的 filename,默认取 file_path 的 basename。
|
|
198
|
+
"""
|
|
199
|
+
fpath = os.path.expanduser(file_path)
|
|
200
|
+
fname = file_field_name or os.path.basename(fpath)
|
|
201
|
+
mime_type, _ = mimetypes.guess_type(fpath)
|
|
202
|
+
if not mime_type:
|
|
203
|
+
mime_type = "application/octet-stream"
|
|
204
|
+
|
|
205
|
+
boundary = "----ClawUploadBoundary" + os.urandom(16).hex()
|
|
206
|
+
parts: list[bytes] = []
|
|
207
|
+
for name, value in fields.items():
|
|
208
|
+
parts.append(f"--{boundary}\r\n".encode())
|
|
209
|
+
parts.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
|
|
210
|
+
parts.append(f"{value}\r\n".encode())
|
|
211
|
+
parts.append(f"--{boundary}\r\n".encode())
|
|
212
|
+
parts.append(
|
|
213
|
+
f'Content-Disposition: form-data; name="{file_field}"; filename="{fname}"\r\n'.encode()
|
|
214
|
+
)
|
|
215
|
+
parts.append(f"Content-Type: {mime_type}\r\n\r\n".encode())
|
|
216
|
+
with open(fpath, "rb") as fh:
|
|
217
|
+
parts.append(fh.read())
|
|
218
|
+
parts.append(f"\r\n--{boundary}--\r\n".encode())
|
|
219
|
+
body = b"".join(parts)
|
|
220
|
+
|
|
221
|
+
headers = _auth_headers()
|
|
222
|
+
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
|
|
223
|
+
req = request.Request(claw_url(path), data=body, headers=headers, method="POST")
|
|
224
|
+
try:
|
|
225
|
+
with request.urlopen(req, timeout=timeout) as resp:
|
|
226
|
+
out = resp.read().decode("utf-8")
|
|
227
|
+
except error.HTTPError as exc:
|
|
228
|
+
out = exc.read().decode("utf-8", errors="replace")
|
|
229
|
+
raise RuntimeError(f"HTTP {exc.code}: {out}") from exc
|
|
230
|
+
data = json.loads(out)
|
|
231
|
+
if not data.get("success"):
|
|
232
|
+
raise RuntimeError(json.dumps(data, ensure_ascii=False))
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def add_cred_arg(parser: Any) -> None:
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"--cred-file",
|
|
239
|
+
"-c",
|
|
240
|
+
metavar="PATH",
|
|
241
|
+
help="JSON 凭证文件(含 Appia 地址、JWT、agentId 等);文件中非空字段优先于环境变量",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def add_timeout_arg(parser: Any, default: float = 60.0) -> None:
|
|
246
|
+
parser.add_argument("--timeout", type=float, default=default)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def add_json_arg(parser: Any) -> None:
|
|
250
|
+
parser.add_argument("--json", action="store_true", help="输出接口原始 JSON")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def emit(data: Any, as_json: bool, human: str) -> int:
|
|
254
|
+
if as_json:
|
|
255
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
256
|
+
else:
|
|
257
|
+
print(human)
|
|
258
|
+
return 0
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Appia Claw 扩展能力:scopes + 人员/通知/待办/未读 + 频道 + 会议纪要/总览 + 写操作 + 聚合调用。
|
|
3
|
+
|
|
4
|
+
鉴权与 appia_claw.py 一致:MCP/CLAW JWT + userId + agentId。共享 helper 见 appia_common。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import appia_common as ac
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _channel_params(args: argparse.Namespace) -> dict[str, Any]:
|
|
18
|
+
"""频道类接口:--rid 优先,否则用 --name / --channel-name。"""
|
|
19
|
+
rid = args.rid or ac.cfg("TARGET_RID")
|
|
20
|
+
if rid:
|
|
21
|
+
return {"rid": rid}
|
|
22
|
+
name = args.name or args.channel_name
|
|
23
|
+
if not name:
|
|
24
|
+
sys.exit("⚠️ 需要 --rid(或 TARGET_RID)或 --name")
|
|
25
|
+
return {"name": name}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_json_arg(json_str: str | None, json_file: str | None) -> Any:
|
|
29
|
+
if json_str is not None:
|
|
30
|
+
try:
|
|
31
|
+
return json.loads(json_str)
|
|
32
|
+
except json.JSONDecodeError as exc:
|
|
33
|
+
sys.exit(f"⚠️ --json 参数解析失败: {exc}")
|
|
34
|
+
if json_file:
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
with open(os.path.expanduser(json_file), encoding="utf-8") as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _common_params() -> dict[str, Any]:
|
|
43
|
+
return {"userId": ac.user_id(), "agentId": ac.agent_id()}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---- 读:scopes / 人员 / 通知 / 待办 / 未读 ----
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_scopes(args: argparse.Namespace) -> int:
|
|
50
|
+
data = ac.api_get("agent.scopes.get", _common_params(), args.timeout)
|
|
51
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_scopes_request(args: argparse.Namespace) -> int:
|
|
55
|
+
scopes = [s.strip() for s in args.scopes.split(",") if s.strip()]
|
|
56
|
+
body = {**_common_params(), "scopes": scopes, "reason": args.reason or ""}
|
|
57
|
+
data = ac.api_post_json("agent.scopes.request", body, args.timeout)
|
|
58
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_users_search(args: argparse.Namespace) -> int:
|
|
62
|
+
params = {**_common_params(), "q": args.q, "limit": args.limit}
|
|
63
|
+
data = ac.api_get("agent.users.search", params, args.timeout)
|
|
64
|
+
if args.json:
|
|
65
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
66
|
+
return 0
|
|
67
|
+
users = (data.get("data") or {}).get("users") or []
|
|
68
|
+
print(f"total={len(users)}")
|
|
69
|
+
for u in users:
|
|
70
|
+
print(f"- _id={u.get('_id')} username={u.get('username')}")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_notifications(args: argparse.Namespace) -> int:
|
|
75
|
+
params = {**_common_params(), "limit": args.limit, "since": args.since}
|
|
76
|
+
data = ac.api_get("agent.notifications.list", params, args.timeout)
|
|
77
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_todos(args: argparse.Namespace) -> int:
|
|
81
|
+
params = {**_common_params(), "limit": args.limit, "status": args.status}
|
|
82
|
+
data = ac.api_get("agent.todos.list", params, args.timeout)
|
|
83
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_unread(args: argparse.Namespace) -> int:
|
|
87
|
+
data = ac.api_get("agent.unread.summary", _common_params(), args.timeout)
|
|
88
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---- 读:频道 ----
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_channels_search(args: argparse.Namespace) -> int:
|
|
95
|
+
params = {**_common_params(), "name": args.name, "keyword": args.keyword, "limit": args.limit}
|
|
96
|
+
data = ac.api_get("agent.channels.search", params, args.timeout)
|
|
97
|
+
if args.json:
|
|
98
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
99
|
+
return 0
|
|
100
|
+
chans = (data.get("data") or {}).get("channels") or (data.get("data") or {}).get("items") or []
|
|
101
|
+
print(f"total={len(chans)}")
|
|
102
|
+
for c in chans:
|
|
103
|
+
print(f"- rid={c.get('rid', c.get('_id'))} name={c.get('name')}")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_channel_info(args: argparse.Namespace) -> int:
|
|
108
|
+
data = ac.api_get("agent.channel.info.get", {**_common_params(), **_channel_params(args)}, args.timeout)
|
|
109
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cmd_channel_messages(args: argparse.Namespace) -> int:
|
|
113
|
+
params = {**_common_params(), **_channel_params(args), "limit": args.limit}
|
|
114
|
+
data = ac.api_get("agent.channel.messages.list", params, args.timeout)
|
|
115
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_channel_messages_search(args: argparse.Namespace) -> int:
|
|
119
|
+
params = {**_common_params(), **_channel_params(args), "keyword": args.keyword, "limit": args.limit}
|
|
120
|
+
data = ac.api_get("agent.channel.messages.search", params, args.timeout)
|
|
121
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cmd_channel_members(args: argparse.Namespace) -> int:
|
|
125
|
+
data = ac.api_get("agent.channel.members.list", {**_common_params(), **_channel_params(args)}, args.timeout)
|
|
126
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmd_channel_attachments(args: argparse.Namespace) -> int:
|
|
130
|
+
data = ac.api_get("agent.channel.attachments.list", {**_common_params(), **_channel_params(args)}, args.timeout)
|
|
131
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_channel_announcements(args: argparse.Namespace) -> int:
|
|
135
|
+
data = ac.api_get("agent.channel.announcements.list", {**_common_params(), **_channel_params(args)}, args.timeout)
|
|
136
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_file_metadata(args: argparse.Namespace) -> int:
|
|
140
|
+
if not args.file_id:
|
|
141
|
+
sys.exit("⚠️ file-metadata 需要 --file-id")
|
|
142
|
+
params = {**_common_params(), "fileId": args.file_id}
|
|
143
|
+
data = ac.api_get("agent.file.metadata.get", params, args.timeout)
|
|
144
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---- 读:会议纪要 / 总览 ----
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_meeting_minutes(args: argparse.Namespace) -> int:
|
|
151
|
+
params = {**_common_params(), "limit": args.limit}
|
|
152
|
+
data = ac.api_get("agent.meeting.minutes.list", params, args.timeout)
|
|
153
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_meeting_minutes_ingest(args: argparse.Namespace) -> int:
|
|
157
|
+
params = {**_common_params(), "limit": args.limit}
|
|
158
|
+
data = ac.api_get("agent.meeting.minutes.ingest.list", params, args.timeout)
|
|
159
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_overview_resolve(args: argparse.Namespace) -> int:
|
|
163
|
+
if not args.url:
|
|
164
|
+
sys.exit("⚠️ overview-resolve 需要 --url")
|
|
165
|
+
params = {**_common_params(), "url": args.url}
|
|
166
|
+
data = ac.api_get("agent.overview.resolve", params, args.timeout)
|
|
167
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cmd_overview_links(args: argparse.Namespace) -> int:
|
|
171
|
+
data = ac.api_get("agent.channel.overview.links.list", {**_common_params(), **_channel_params(args)}, args.timeout)
|
|
172
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---- 写:待办 / 频道 / 会议 ----
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_todos_update(args: argparse.Namespace) -> int:
|
|
179
|
+
if not args.id:
|
|
180
|
+
sys.exit("⚠️ todos-update 需要 --id")
|
|
181
|
+
body: dict[str, Any] = {**_common_params(), "id": args.id, "status": args.status}
|
|
182
|
+
if args.reminder_time:
|
|
183
|
+
body["reminderTime"] = args.reminder_time
|
|
184
|
+
data = ac.api_post_json("agent.todos.update-status", body, args.timeout)
|
|
185
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cmd_channels_create(args: argparse.Namespace) -> int:
|
|
189
|
+
members = _load_json_arg(args.members_json, args.members_file)
|
|
190
|
+
if members is None:
|
|
191
|
+
members = []
|
|
192
|
+
body: dict[str, Any] = {**_common_params(), "name": args.name or "", "members": members}
|
|
193
|
+
if args.dep_ids:
|
|
194
|
+
body["depIds"] = [d.strip() for d in args.dep_ids.split(",") if d.strip()]
|
|
195
|
+
if args.read_only:
|
|
196
|
+
body["readOnly"] = True
|
|
197
|
+
extra = _load_json_arg(args.extra_json, args.extra_file)
|
|
198
|
+
if extra is not None:
|
|
199
|
+
body["extraData"] = extra
|
|
200
|
+
data = ac.api_post_json("agent.channels.create", body, args.timeout)
|
|
201
|
+
if args.json:
|
|
202
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
203
|
+
return 0
|
|
204
|
+
d = data.get("data") or {}
|
|
205
|
+
print(f"created: rid={d.get('rid')} name={(d.get('channel') or {}).get('name', args.name or '')}")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_tool_invoke(args: argparse.Namespace) -> int:
|
|
210
|
+
if not args.tool:
|
|
211
|
+
sys.exit("⚠️ tool-invoke 需要 --tool")
|
|
212
|
+
tool_args = _load_json_arg(args.args_json, args.args_file)
|
|
213
|
+
body = {**_common_params(), "tool": args.tool, "args": tool_args or {}}
|
|
214
|
+
data = ac.api_post_json("agent.tool.invoke", body, args.timeout)
|
|
215
|
+
return ac.emit(data, args.json, json.dumps(data.get("data", {}), ensure_ascii=False, indent=2))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---- CLI ----
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _channel_args(parser: argparse.ArgumentParser) -> None:
|
|
222
|
+
parser.add_argument("--rid", help="频道 rid(覆盖 TARGET_RID)")
|
|
223
|
+
parser.add_argument("--name", "--channel-name", dest="name", help="频道名(无 rid 时用)")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main() -> int:
|
|
227
|
+
p = argparse.ArgumentParser(description="Appia Claw 扩展能力(scopes / 人员 / 通知 / 待办 / 频道 / 会议 / 总览 / 写操作)")
|
|
228
|
+
ac.add_cred_arg(p)
|
|
229
|
+
ac.add_timeout_arg(p)
|
|
230
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
231
|
+
|
|
232
|
+
def _json(sp):
|
|
233
|
+
ac.add_json_arg(sp)
|
|
234
|
+
|
|
235
|
+
sp = sub.add_parser("scopes", help="GET agent.scopes.get")
|
|
236
|
+
_json(sp)
|
|
237
|
+
sp.set_defaults(_run=cmd_scopes)
|
|
238
|
+
|
|
239
|
+
sp = sub.add_parser("scopes-request", help="POST agent.scopes.request(发 myAgent 授权卡片)")
|
|
240
|
+
sp.add_argument("--scopes", required=True, help="逗号分隔,如 channels:write")
|
|
241
|
+
sp.add_argument("--reason", help="申请原因")
|
|
242
|
+
_json(sp)
|
|
243
|
+
sp.set_defaults(_run=cmd_scopes_request)
|
|
244
|
+
|
|
245
|
+
sp = sub.add_parser("users-search", help="GET agent.users.search")
|
|
246
|
+
sp.add_argument("--q", required=True, help="姓名/username 关键词")
|
|
247
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
248
|
+
_json(sp)
|
|
249
|
+
sp.set_defaults(_run=cmd_users_search)
|
|
250
|
+
|
|
251
|
+
sp = sub.add_parser("notifications", help="GET agent.notifications.list")
|
|
252
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
253
|
+
sp.add_argument("--since", help="起始时间")
|
|
254
|
+
_json(sp)
|
|
255
|
+
sp.set_defaults(_run=cmd_notifications)
|
|
256
|
+
|
|
257
|
+
sp = sub.add_parser("todos", help="GET agent.todos.list")
|
|
258
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
259
|
+
sp.add_argument("--status", help="状态过滤")
|
|
260
|
+
_json(sp)
|
|
261
|
+
sp.set_defaults(_run=cmd_todos)
|
|
262
|
+
|
|
263
|
+
sp = sub.add_parser("unread", help="GET agent.unread.summary")
|
|
264
|
+
_json(sp)
|
|
265
|
+
sp.set_defaults(_run=cmd_unread)
|
|
266
|
+
|
|
267
|
+
sp = sub.add_parser("channels-search", help="GET agent.channels.search")
|
|
268
|
+
sp.add_argument("--name", help="频道名")
|
|
269
|
+
sp.add_argument("--keyword", help="关键词")
|
|
270
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
271
|
+
_json(sp)
|
|
272
|
+
sp.set_defaults(_run=cmd_channels_search)
|
|
273
|
+
|
|
274
|
+
sp = sub.add_parser("channel-info", help="GET agent.channel.info.get")
|
|
275
|
+
_channel_args(sp)
|
|
276
|
+
_json(sp)
|
|
277
|
+
sp.set_defaults(_run=cmd_channel_info)
|
|
278
|
+
|
|
279
|
+
sp = sub.add_parser("channel-messages", help="GET agent.channel.messages.list")
|
|
280
|
+
_channel_args(sp)
|
|
281
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
282
|
+
_json(sp)
|
|
283
|
+
sp.set_defaults(_run=cmd_channel_messages)
|
|
284
|
+
|
|
285
|
+
sp = sub.add_parser("channel-messages-search", help="GET agent.channel.messages.search")
|
|
286
|
+
_channel_args(sp)
|
|
287
|
+
sp.add_argument("--keyword", required=True)
|
|
288
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
289
|
+
_json(sp)
|
|
290
|
+
sp.set_defaults(_run=cmd_channel_messages_search)
|
|
291
|
+
|
|
292
|
+
sp = sub.add_parser("channel-members", help="GET agent.channel.members.list")
|
|
293
|
+
_channel_args(sp)
|
|
294
|
+
_json(sp)
|
|
295
|
+
sp.set_defaults(_run=cmd_channel_members)
|
|
296
|
+
|
|
297
|
+
sp = sub.add_parser("channel-attachments", help="GET agent.channel.attachments.list")
|
|
298
|
+
_channel_args(sp)
|
|
299
|
+
_json(sp)
|
|
300
|
+
sp.set_defaults(_run=cmd_channel_attachments)
|
|
301
|
+
|
|
302
|
+
sp = sub.add_parser("channel-announcements", help="GET agent.channel.announcements.list")
|
|
303
|
+
_channel_args(sp)
|
|
304
|
+
_json(sp)
|
|
305
|
+
sp.set_defaults(_run=cmd_channel_announcements)
|
|
306
|
+
|
|
307
|
+
sp = sub.add_parser("file-metadata", help="GET agent.file.metadata.get")
|
|
308
|
+
sp.add_argument("--file-id", required=True)
|
|
309
|
+
_json(sp)
|
|
310
|
+
sp.set_defaults(_run=cmd_file_metadata)
|
|
311
|
+
|
|
312
|
+
sp = sub.add_parser("meeting-minutes", help="GET agent.meeting.minutes.list")
|
|
313
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
314
|
+
_json(sp)
|
|
315
|
+
sp.set_defaults(_run=cmd_meeting_minutes)
|
|
316
|
+
|
|
317
|
+
sp = sub.add_parser("meeting-minutes-ingest", help="GET agent.meeting.minutes.ingest.list")
|
|
318
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
319
|
+
_json(sp)
|
|
320
|
+
sp.set_defaults(_run=cmd_meeting_minutes_ingest)
|
|
321
|
+
|
|
322
|
+
sp = sub.add_parser("overview-resolve", help="GET agent.overview.resolve(总览链接→元数据)")
|
|
323
|
+
sp.add_argument("--url", required=True, help="总览链接")
|
|
324
|
+
_json(sp)
|
|
325
|
+
sp.set_defaults(_run=cmd_overview_resolve)
|
|
326
|
+
|
|
327
|
+
sp = sub.add_parser("overview-links", help="GET agent.channel.overview.links.list")
|
|
328
|
+
_channel_args(sp)
|
|
329
|
+
_json(sp)
|
|
330
|
+
sp.set_defaults(_run=cmd_overview_links)
|
|
331
|
+
|
|
332
|
+
sp = sub.add_parser("todos-update", help="POST agent.todos.update-status")
|
|
333
|
+
sp.add_argument("--id", required=True, help="待办 id")
|
|
334
|
+
sp.add_argument("--status", required=True)
|
|
335
|
+
sp.add_argument("--reminder-time", dest="reminder_time")
|
|
336
|
+
_json(sp)
|
|
337
|
+
sp.set_defaults(_run=cmd_todos_update)
|
|
338
|
+
|
|
339
|
+
sp = sub.add_parser("channels-create", help="POST agent.channels.create")
|
|
340
|
+
sp.add_argument("--name", help="频道名")
|
|
341
|
+
sp.add_argument("--members-json", dest="members_json", help='username 数组 JSON,如 ["zhangsan","lisi"]')
|
|
342
|
+
sp.add_argument("--members-file", dest="members_file", help="username 数组 JSON 文件")
|
|
343
|
+
sp.add_argument("--dep-ids", dest="dep_ids", help="部门 id 逗号分隔")
|
|
344
|
+
sp.add_argument("--read-only", dest="read_only", action="store_true")
|
|
345
|
+
sp.add_argument("--extra-json", dest="extra_json", help="extraData JSON")
|
|
346
|
+
sp.add_argument("--extra-file", dest="extra_file", help="extraData JSON 文件")
|
|
347
|
+
_json(sp)
|
|
348
|
+
sp.set_defaults(_run=cmd_channels_create)
|
|
349
|
+
|
|
350
|
+
sp = sub.add_parser("tool-invoke", help="POST agent.tool.invoke(聚合调用)")
|
|
351
|
+
sp.add_argument("--tool", required=True, help="如 appia.todos.list")
|
|
352
|
+
sp.add_argument("--args-json", dest="args_json", help="args JSON")
|
|
353
|
+
sp.add_argument("--args-file", dest="args_file", help="args JSON 文件")
|
|
354
|
+
_json(sp)
|
|
355
|
+
sp.set_defaults(_run=cmd_tool_invoke)
|
|
356
|
+
|
|
357
|
+
args = p.parse_args()
|
|
358
|
+
ac.init_cred(args.cred_file)
|
|
359
|
+
runner = getattr(args, "_run", None)
|
|
360
|
+
if runner is None:
|
|
361
|
+
return 1
|
|
362
|
+
return int(runner(args))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
try:
|
|
367
|
+
raise SystemExit(main())
|
|
368
|
+
except Exception as exc: # pragma: no cover - CLI
|
|
369
|
+
print(f"⚠️ {exc}", file=sys.stderr)
|
|
370
|
+
raise SystemExit(1)
|