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.
@@ -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)