@acedatacloud/skills 2026.627.1 → 2026.628.0

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/README.md CHANGED
@@ -69,6 +69,7 @@ These skills drive third-party connectors users wire up at [auth.acedata.cloud/u
69
69
  | [notion](skills/notion/) | Read and search Notion pages, databases, and blocks | `notion` |
70
70
  | [slack](skills/slack/) | Read Slack channels, messages, and user info via Web API | `slack` |
71
71
  | [wechat-official-account](skills/wechat-official-account/) | Manage WeChat MP — drafts, publishing, materials, user tags | `wechat` (BYOC) |
72
+ | [personal-wechat](skills/personal-wechat/) | Operate a personal WeChat account through a self-hosted Wisdom service | `personalwechat` (BYOC) |
72
73
 
73
74
  ## Prerequisites
74
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.627.1",
3
+ "version": "2026.628.0",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: personal-wechat
3
+ description: Operate the user's personal WeChat account through their self-hosted Wisdom service (BYOC) — check login status, list contacts/conversations, read and summarize history, search contacts, refresh the local history DB, and send messages only after explicit confirmation. Use when the user mentions 个人微信, 我的微信, WeChat personal chat, 微信聊天记录, 微信联系人, reading/summarizing WeChat messages, or sending a WeChat message.
4
+ when_to_use: |
5
+ Trigger for the user's personal WeChat account via their own Wisdom server:
6
+ check status/account, list contacts, list recent conversations, read or
7
+ summarize a chat, query local history, search contacts, or send a message.
8
+ This acts on the user's real desktop WeChat, so writes are gated behind
9
+ explicit confirmation.
10
+ connections: [personalwechat]
11
+ allowed_tools: [Bash]
12
+ license: Apache-2.0
13
+ metadata:
14
+ author: acedatacloud
15
+ version: "1.0"
16
+ ---
17
+
18
+ # Personal WeChat via Wisdom
19
+
20
+ Use the user's self-hosted **Wisdom** service to operate their personal WeChat
21
+ account. Wisdom runs on a Windows host with WeChat Desktop logged in and exposes
22
+ an HTTP API.
23
+
24
+ Credentials are injected by the `personalwechat` BYOC connector:
25
+
26
+ - `PERSONALWECHAT_BASE_URL` — Wisdom server base URL, e.g. `http://82.156.126.14:8000`.
27
+ - `PERSONALWECHAT_API_TOKEN` — Wisdom `API_TOKEN`. Secret — never echo, print, or log it.
28
+
29
+ The helper sends the token as `Authorization: Bearer ...`; it never puts the
30
+ token in the URL.
31
+
32
+ This is the user's **real personal WeChat account**. Read operations can run
33
+ directly. Sending messages or files must be dry-run first, then performed only
34
+ after the user explicitly approves the exact target and content.
35
+
36
+ ## CLI
37
+
38
+ The skill ships a stdlib-only helper:
39
+
40
+ ```bash
41
+ WX=$SKILL_DIR/scripts/personal_wechat.py
42
+ ```
43
+
44
+ ## Verify Connection First
45
+
46
+ Always start with status when the user asks to use WeChat:
47
+
48
+ ```bash
49
+ python3 $WX status
50
+ ```
51
+
52
+ Expected healthy shape:
53
+
54
+ ```json
55
+ {"auth":{"logged_in":true,"wechat_running":true,"page":"logged_in"}}
56
+ ```
57
+
58
+ If `logged_in=false`, tell the user to open the Wisdom web UI / RDP and scan the
59
+ WeChat QR code. If the API returns 401, ask the user to reconnect the Personal
60
+ WeChat connector with the current Wisdom API token.
61
+
62
+ ## Read Workflows
63
+
64
+ ### Current Account
65
+
66
+ ```bash
67
+ python3 $WX account
68
+ ```
69
+
70
+ ### Contacts
71
+
72
+ ```bash
73
+ python3 $WX contacts --limit 50
74
+ ```
75
+
76
+ ### Recent Conversations
77
+
78
+ Use the normal conversations endpoint first; it prefers WeChat DB when ready and
79
+ falls back to Wisdom's app DB.
80
+
81
+ ```bash
82
+ python3 $WX conversations --limit 20
83
+ ```
84
+
85
+ For decrypted local WeChat history sessions:
86
+
87
+ ```bash
88
+ python3 $WX conversations --history --limit 20
89
+ ```
90
+
91
+ ### Messages in a Conversation
92
+
93
+ First list conversations, then use the `id` as `conversation_id`:
94
+
95
+ ```bash
96
+ python3 $WX messages "34642176898@chatroom" --limit 50 --order asc
97
+ ```
98
+
99
+ ### Historical Messages
100
+
101
+ Read from Wisdom's decrypted WeChat local databases:
102
+
103
+ ```bash
104
+ python3 $WX history --limit 50
105
+ python3 $WX history --talker "34642176898@chatroom" --limit 50
106
+ python3 $WX history --limit 20 --offset 20
107
+ ```
108
+
109
+ ### Raw SQL, Read-Only Only
110
+
111
+ Wisdom permits only `SELECT` and `PRAGMA`:
112
+
113
+ ```bash
114
+ python3 $WX sql MicroMsg.db 'SELECT count(*) AS cnt FROM Session'
115
+ ```
116
+
117
+ Use raw SQL only for diagnostics or targeted metadata queries. Do not dump large
118
+ message tables unless the user explicitly asks.
119
+
120
+ ### Refresh History DB
121
+
122
+ If history looks stale, refresh the decrypted DB snapshot:
123
+
124
+ ```bash
125
+ python3 $WX refresh-history
126
+ ```
127
+
128
+ ## Search
129
+
130
+ ```bash
131
+ python3 $WX search "Alice"
132
+ ```
133
+
134
+ Search drives the WeChat UI, so it may be slower than local DB history reads.
135
+
136
+ ## Sending Messages — GATED
137
+
138
+ `send` dry-runs by default. It never sends unless `--confirm` is present, or
139
+ unless an AceDataCloud scheduled task pre-authorized this Skill and you use
140
+ `--unattended-confirm`.
141
+
142
+ ```bash
143
+ python3 $WX send "Alice" "今晚 8 点开会吗?"
144
+ # -> {"dry_run": true, ...}
145
+ ```
146
+
147
+ Show the dry-run output to the user and ask for explicit approval of the exact
148
+ recipient and text. Only then run:
149
+
150
+ ```bash
151
+ python3 $WX send "Alice" "今晚 8 点开会吗?" --confirm
152
+ ```
153
+
154
+ Never add `--confirm` in the first attempt. Never infer consent from vague text.
155
+ The user must clearly approve sending this exact message.
156
+
157
+ ### Scheduled-task unattended confirmation
158
+
159
+ When running inside an AceDataCloud scheduled task, the platform may pre-authorize
160
+ specific Skills for unattended execution. If all of these are true:
161
+
162
+ - `AICHAT_UNATTENDED_MODE=true`
163
+ - `AICHAT_ACTIVE_SKILL` is `personal-wechat` or `acedatacloud/personal-wechat`
164
+ - `AICHAT_ACTIVE_SKILL` appears in `AICHAT_UNATTENDED_ALLOWED_SKILLS`
165
+
166
+ then the user has pre-authorized this Skill for that scheduled task. In that
167
+ case, use:
168
+
169
+ ```bash
170
+ python3 $WX send "Alice" "今晚 8 点开会吗?" --unattended-confirm
171
+ ```
172
+
173
+ If the helper returns `unattended_confirmation_denied`, do not retry with
174
+ `--confirm`; report the dry-run and explain that the task needs this Skill to be
175
+ selected in its unattended authorization settings.
176
+
177
+ ## Safety Rules
178
+
179
+ - Never print `PERSONALWECHAT_API_TOKEN`.
180
+ - Treat `PERSONALWECHAT_BASE_URL + API_TOKEN` as full remote control of the user's WeChat.
181
+ - For normal chat write/send operations: dry-run first, ask for explicit approval, then re-run with `--confirm`.
182
+ - For scheduled-task unattended writes: use `--unattended-confirm` only when the platform env says this Skill is pre-authorized.
183
+ - Do not call logout/restart endpoints from the skill unless the user explicitly asks to repair the Wisdom service.
184
+ - If Wisdom returns 503 for history, run `python3 $WX refresh-history` once, then retry the read.
185
+ - If the server is unreachable, ask the user to check the Windows host / security group / port 8000.
186
+
187
+ ## Endpoint Mapping
188
+
189
+ The helper wraps these Wisdom endpoints:
190
+
191
+ - `GET /api/status`
192
+ - `GET /api/auth/status`
193
+ - `GET /api/account`
194
+ - `GET /api/contacts?version=2.0`
195
+ - `GET /api/conversations`
196
+ - `GET /api/conversations/history`
197
+ - `GET /api/messages`
198
+ - `GET /api/messages/history`
199
+ - `POST /api/messages/history/query`
200
+ - `POST /api/messages/history/refresh`
201
+ - `POST /api/search`
202
+ - `POST /api/messages/send` (only after `--confirm` or verified `--unattended-confirm`)
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """Small CLI for the Personal WeChat / Wisdom BYOC skill."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+
15
+
16
+ BASE_URL = os.environ.get("PERSONALWECHAT_BASE_URL", "").rstrip("/")
17
+ API_TOKEN = os.environ.get("PERSONALWECHAT_API_TOKEN", "")
18
+ MAX_TEXT_CHARS = 800
19
+ SKILL_SLUGS = {"personal-wechat", "acedatacloud/personal-wechat"}
20
+
21
+
22
+ def _die(message: str, code: int = 1) -> None:
23
+ print(json.dumps({"error": message}, ensure_ascii=False), file=sys.stderr)
24
+ raise SystemExit(code)
25
+
26
+
27
+ def _json(data) -> None:
28
+ print(json.dumps(data, ensure_ascii=False, default=str))
29
+
30
+
31
+ def unattended_confirm_allowed() -> tuple[bool, str]:
32
+ if os.environ.get("AICHAT_UNATTENDED_MODE") != "true":
33
+ return False, "not running in AceDataCloud unattended scheduled-task mode"
34
+
35
+ active_skill = os.environ.get("AICHAT_ACTIVE_SKILL", "")
36
+ if active_skill not in SKILL_SLUGS:
37
+ return False, f"active skill {active_skill or '<empty>'!r} is not personal-wechat"
38
+
39
+ raw_allowed = os.environ.get("AICHAT_UNATTENDED_ALLOWED_SKILLS", "[]")
40
+ try:
41
+ allowed = json.loads(raw_allowed)
42
+ except json.JSONDecodeError:
43
+ return False, "AICHAT_UNATTENDED_ALLOWED_SKILLS is not valid JSON"
44
+ if not isinstance(allowed, list) or active_skill not in allowed:
45
+ return False, f"skill {active_skill!r} is not pre-authorized for unattended confirmation"
46
+
47
+ expires_raw = os.environ.get("AICHAT_UNATTENDED_EXPIRES_AT", "")
48
+ if expires_raw:
49
+ try:
50
+ expires_at = int(expires_raw)
51
+ except ValueError:
52
+ return False, "AICHAT_UNATTENDED_EXPIRES_AT is invalid"
53
+ if expires_at < int(time.time()):
54
+ return False, "unattended authorization has expired"
55
+
56
+ return True, "ok"
57
+
58
+
59
+ def request(method: str, path: str, *, params: dict | None = None, body: dict | None = None):
60
+ if not BASE_URL:
61
+ _die("PERSONALWECHAT_BASE_URL is not set. Reconnect the Personal WeChat connector.")
62
+ if not API_TOKEN:
63
+ _die("PERSONALWECHAT_API_TOKEN is not set. Reconnect the Personal WeChat connector.")
64
+
65
+ query = dict(params or {})
66
+ suffix = f"?{urllib.parse.urlencode(query)}" if query else ""
67
+ url = f"{BASE_URL}{path}{suffix}"
68
+ payload = None
69
+ headers = {"Accept": "application/json", "Authorization": f"Bearer {API_TOKEN}"}
70
+ if body is not None:
71
+ payload = json.dumps(body).encode("utf-8")
72
+ headers["Content-Type"] = "application/json"
73
+
74
+ req = urllib.request.Request(url, data=payload, headers=headers, method=method)
75
+ try:
76
+ with urllib.request.urlopen(req, timeout=45) as response:
77
+ raw = response.read().decode("utf-8", "replace")
78
+ if not raw:
79
+ return {}
80
+ try:
81
+ return json.loads(raw)
82
+ except json.JSONDecodeError:
83
+ return {"raw": raw}
84
+ except urllib.error.HTTPError as exc:
85
+ raw = exc.read().decode("utf-8", "replace")
86
+ try:
87
+ body = json.loads(raw) if raw else {}
88
+ except json.JSONDecodeError:
89
+ body = {"raw": raw}
90
+ _die(f"HTTP {exc.code}: {body}", code=2)
91
+ except urllib.error.URLError as exc:
92
+ reason = getattr(exc, "reason", None) or "connection failed"
93
+ _die(f"Cannot reach Wisdom at {BASE_URL}: {reason}", code=3)
94
+
95
+
96
+ def compact_conversation(item: dict) -> dict:
97
+ return {
98
+ "id": item.get("id") or item.get("strUsrName"),
99
+ "name": item.get("name") or item.get("display_name") or item.get("strNickName"),
100
+ "type": item.get("type"),
101
+ "unread_count": item.get("unread_count") if "unread_count" in item else item.get("nUnReadCount"),
102
+ "last_active_at": item.get("last_active_at") or item.get("nTime"),
103
+ "last_message_count": len(item.get("messages") or []),
104
+ }
105
+
106
+
107
+ def compact_message(item: dict) -> dict:
108
+ text = item.get("text") or item.get("StrContent") or item.get("DisplayContent")
109
+ if isinstance(text, str) and len(text) > MAX_TEXT_CHARS:
110
+ text = text[:MAX_TEXT_CHARS] + f"... [truncated {len(text) - MAX_TEXT_CHARS} chars]"
111
+ return {
112
+ "id": item.get("id") or item.get("MsgSvrID") or item.get("localId"),
113
+ "conversation_id": item.get("conversation_id") or item.get("StrTalker"),
114
+ "conversation_name": item.get("conversation_name"),
115
+ "sender_id": item.get("sender_id"),
116
+ "sender_name": item.get("sender_name"),
117
+ "direction": item.get("direction"),
118
+ "type": item.get("type") or item.get("Type"),
119
+ "text": text,
120
+ "sent_at": item.get("sent_at") or item.get("CreateTime"),
121
+ }
122
+
123
+
124
+ def main() -> None:
125
+ parser = argparse.ArgumentParser(description="Personal WeChat (Wisdom) CLI")
126
+ sub = parser.add_subparsers(dest="cmd", required=True)
127
+
128
+ sub.add_parser("status")
129
+ sub.add_parser("account")
130
+
131
+ contacts = sub.add_parser("contacts")
132
+ contacts.add_argument("--limit", type=int, default=20)
133
+
134
+ convs = sub.add_parser("conversations")
135
+ convs.add_argument("--limit", type=int, default=20)
136
+ convs.add_argument("--history", action="store_true")
137
+
138
+ msgs = sub.add_parser("messages")
139
+ msgs.add_argument("conversation_id")
140
+ msgs.add_argument("--limit", type=int, default=50)
141
+ msgs.add_argument("--offset", type=int, default=0)
142
+ msgs.add_argument("--order", choices=["asc", "desc"], default="asc")
143
+
144
+ hist = sub.add_parser("history")
145
+ hist.add_argument("--talker", default="")
146
+ hist.add_argument("--limit", type=int, default=50)
147
+ hist.add_argument("--offset", type=int, default=0)
148
+
149
+ sql = sub.add_parser("sql")
150
+ sql.add_argument("db")
151
+ sql.add_argument("sql")
152
+
153
+ search = sub.add_parser("search")
154
+ search.add_argument("query")
155
+
156
+ send = sub.add_parser("send")
157
+ send.add_argument("target")
158
+ send.add_argument("text")
159
+ send.add_argument("--confirm", action="store_true")
160
+ send.add_argument("--unattended-confirm", action="store_true")
161
+
162
+ refresh = sub.add_parser("refresh-history")
163
+ refresh.set_defaults(cmd="refresh-history")
164
+
165
+ args = parser.parse_args()
166
+
167
+ if args.cmd == "status":
168
+ _json({"status": request("GET", "/api/status"), "auth": request("GET", "/api/auth/status")})
169
+ elif args.cmd == "account":
170
+ _json(request("GET", "/api/account"))
171
+ elif args.cmd == "contacts":
172
+ data = request("GET", "/api/contacts", params={"limit": args.limit, "version": "2.0"})
173
+ _json({"total": data.get("total"), "contacts": data.get("contacts", [])})
174
+ elif args.cmd == "conversations":
175
+ if args.history:
176
+ data = request("GET", "/api/conversations/history", params={"limit": args.limit})
177
+ _json({"count": data.get("count"), "conversations": [compact_conversation(i) for i in data.get("conversations", [])]})
178
+ else:
179
+ data = request("GET", "/api/conversations", params={"limit": args.limit})
180
+ _json({"total": data.get("total"), "conversations": [compact_conversation(i) for i in data.get("conversations", [])]})
181
+ elif args.cmd == "messages":
182
+ data = request(
183
+ "GET",
184
+ "/api/messages",
185
+ params={"conversation_id": args.conversation_id, "limit": args.limit, "offset": args.offset, "order": args.order},
186
+ )
187
+ _json([compact_message(i) for i in data])
188
+ elif args.cmd == "history":
189
+ params = {"limit": args.limit, "offset": args.offset}
190
+ if args.talker:
191
+ params["talker"] = args.talker
192
+ data = request("GET", "/api/messages/history", params=params)
193
+ _json({"count": data.get("count"), "db_ready": data.get("db_ready"), "messages": [compact_message(i) for i in data.get("messages", [])]})
194
+ elif args.cmd == "sql":
195
+ data = request("POST", "/api/messages/history/query", body={"db": args.db, "sql": args.sql})
196
+ _json(data)
197
+ elif args.cmd == "search":
198
+ _json(request("POST", "/api/search", body={"query": args.query}))
199
+ elif args.cmd == "send":
200
+ if args.unattended_confirm:
201
+ allowed, reason = unattended_confirm_allowed()
202
+ if not allowed:
203
+ _json({"dry_run": True, "target": args.target, "text": args.text, "error": "unattended_confirmation_denied", "reason": reason})
204
+ return
205
+ elif not args.confirm:
206
+ _json({"dry_run": True, "target": args.target, "text": args.text, "note": "Re-run with --confirm after explicit user approval, or --unattended-confirm when this Skill is pre-authorized for an AceDataCloud scheduled task."})
207
+ return
208
+ _json(request("POST", "/api/messages/send", body={"target": args.target, "type": "text", "text": args.text}))
209
+ elif args.cmd == "refresh-history":
210
+ _json(request("POST", "/api/messages/history/refresh"))
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()