@acedatacloud/skills 2026.703.1 → 2026.703.3

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
@@ -70,7 +70,9 @@ These skills drive third-party connectors users wire up at [auth.acedata.cloud/u
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
72
  | [personal-wechat](skills/personal-wechat/) | Operate a personal WeChat account through a self-hosted Wisdom service | `personalwechat` (BYOC) |
73
+ | [didi-ride](skills/didi-ride/) | Book DiDi rides, estimate fares, query/cancel orders, plan routes via the DiDi MCP | `didi` (BYOC) |
73
74
  | [wecom](skills/wecom/) | WeCom (企业微信) self-built app — contacts, app messages, WeDoc, schedules, meetings | `wecom` (BYOC) |
75
+ | [tencent-docs](skills/tencent-docs/) | Create / read / list / search / manage Tencent Docs — docs, sheets, slides, mind maps, flowcharts | `tencentdocs` (BYOC) |
74
76
 
75
77
  ## Prerequisites
76
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.703.1",
3
+ "version": "2026.703.3",
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,139 @@
1
+ ---
2
+ name: didi-ride
3
+ description: 通过滴滴出行 MCP 打车、查价、查询订单、取消订单、规划路线(驾车/公交/步行/骑行)和搜索地点。Use when the user mentions 滴滴, 打车, 叫车, 回家/上班要打车, 查一下从 A 到 B 多少钱/怎么走, 查询订单, 司机在哪/多久到, 预约叫车, 路线规划, DiDi, ride-hailing, or booking a taxi.
4
+ when_to_use: |
5
+ Trigger for anything involving the user's DiDi (滴滴出行) account:
6
+ book a ride ("打车去…", "回家", "上班"), get a fare/route estimate,
7
+ query an existing order (driver location, ETA, trip progress), cancel
8
+ an order, plan a driving/transit/walking/cycling route, or search
9
+ places / reverse-geocode. Creating and cancelling orders act on real
10
+ money and a real driver, so those writes are gated behind explicit
11
+ confirmation.
12
+ connections: [didi]
13
+ allowed_tools: [Bash]
14
+ license: Apache-2.0
15
+ metadata:
16
+ author: acedatacloud
17
+ version: "1.0"
18
+ ---
19
+
20
+ # DiDi Ride (滴滴出行)
21
+
22
+ Drive the user's **DiDi (滴滴出行)** account through the DiDi MCP server:
23
+ book rides, estimate fares, track orders, cancel, and plan routes.
24
+
25
+ The `didi` BYOC connector injects one env var into the sandbox:
26
+
27
+ - `DIDI_MCP_KEY` — the user's DiDi MCP key. **Secret — never echo, print, or log it.**
28
+
29
+ If `DIDI_MCP_KEY` is missing, tell the user to connect the DiDi connector at
30
+ [auth.acedata.cloud/user/connections](https://auth.acedata.cloud/user/connections)
31
+ (they get the key by scanning the QR in the 滴滴出行 App or via
32
+ <https://mcp.didichuxing.com/claw>).
33
+
34
+ ## CLI
35
+
36
+ The skill ships a stdlib-only helper that speaks the MCP Streamable-HTTP
37
+ protocol to DiDi. Point at it once:
38
+
39
+ ```bash
40
+ DIDI=$SKILL_DIR/scripts/didi.py
41
+ ```
42
+
43
+ Two commands:
44
+
45
+ ```bash
46
+ python3 $DIDI list # list tools + their JSON input schemas
47
+ python3 $DIDI call <tool> '<json-args>' # call any tool
48
+ ```
49
+
50
+ **Always check `references/api_references.md` for the exact tool + parameter
51
+ names before calling.** When unsure, run `python3 $DIDI list` — it returns the
52
+ authoritative input schema for every tool straight from DiDi. Do **not** guess
53
+ parameter names (common mistakes: `keyword` → `keywords`, `region` → `city`,
54
+ four coord fields → six `from_name/from_lat/from_lng/to_name/to_lat/to_lng`).
55
+
56
+ All argument **values must be strings** (including coordinates and
57
+ `product_category`), e.g. `{"product_category":"1"}` not `{"product_category":1}`.
58
+
59
+ ## Write gating (real money / real driver)
60
+
61
+ `taxi_create_order` and `taxi_cancel_order` are **gated**: without a trailing
62
+ `--confirm` the helper only DRY-RUNS and changes nothing. Run once without
63
+ `--confirm` to preview, then re-run with `--confirm` as the **last** argument
64
+ after the user approves:
65
+
66
+ ```bash
67
+ python3 $DIDI call taxi_create_order '{"estimate_trace_id":"...","product_category":"1"}' --confirm
68
+ ```
69
+
70
+ - Even when the user says "打车" / "取消订单", confirm the concrete details
71
+ (起终点、车型 for booking; the order for cancelling) before adding `--confirm`.
72
+ - Cancel intent ≠ cancel confirmation — always ask "确认取消吗?" first.
73
+
74
+ ## Booking flow (最小可执行)
75
+
76
+ 1. **Resolve addresses** — `maps_textsearch` (never invent coordinates; don't
77
+ reuse coordinates from earlier turns — the user may have moved).
78
+ - If the user references an address alias (家 / 公司 / etc.) that you don't
79
+ have, ask them; this skill has no stored preferences.
80
+ 2. **Confirm start/end** — if `maps_textsearch` returns ≥2 candidates, list at
81
+ least the top 3 and let the user pick; a single exact match needs no
82
+ confirmation.
83
+ 3. **Estimate** — `taxi_estimate`; record the returned `traceId` /
84
+ `estimate_trace_id`. It expires (`-32021`) — re-estimate if stale.
85
+ 4. **Pick car type** — user's current message wins ("叫快车"→`product_category`
86
+ `1`, "专车"→`8`); otherwise ask. Only use categories present in the
87
+ `taxi_estimate` response; never silently substitute a different service level
88
+ (快车 `1` ≠ 特惠快车 `201`).
89
+ 5. **Create order** — `taxi_create_order` with the latest `estimate_trace_id`
90
+ (dry-run → confirm → `--confirm`).
91
+ 6. **Report** — order id, start/end, car type, estimated fare. Tell the user
92
+ they can send 「查询订单」 to check status.
93
+
94
+ ## Query an order
95
+
96
+ Order id comes from (in priority): the user's message → the most recent order
97
+ created this conversation → else ask.
98
+
99
+ ```bash
100
+ python3 $DIDI call taxi_query_order '{"order_id":"ORDER_ID"}'
101
+ ```
102
+
103
+ Status codes (`code`):
104
+
105
+ | code | 含义 | 输出 |
106
+ |------|------|------|
107
+ | 0 | 匹配中 | ⏳ 正在为您匹配司机 |
108
+ | 1 | 司机已接单 | 展示司机姓名、车型、车牌、电话、距离与预计到达时间 |
109
+ | 2 | 司机已到达 | 🔔 司机已到达上车点 |
110
+ | 4 | 行程进行中 | 🚗 行程已开始 |
111
+ | 5 | 订单完成 | ✅ 行程结束(展示费用,如有) |
112
+ | 6 | 系统取消 | ❌ 订单已被系统取消 |
113
+ | 7 | 已取消 | ❌ 订单已取消 |
114
+ | 3 / 8-12 | 其他终态 | 显示对应状态描述 |
115
+
116
+ ## Routes & places (no order needed)
117
+
118
+ ```bash
119
+ python3 $DIDI call maps_direction_driving '{"from_name":"...","from_lat":"...","from_lng":"...","to_name":"...","to_lat":"...","to_lng":"..."}'
120
+ python3 $DIDI call maps_direction_transit '{...}' # 公交
121
+ python3 $DIDI call maps_direction_walking '{...}' # 步行
122
+ python3 $DIDI call maps_direction_bicycling '{...}' # 骑行
123
+ python3 $DIDI call maps_place_around '{"keywords":"咖啡","lat":"...","lng":"..."}'
124
+ python3 $DIDI call maps_regeocode '{"lat":"...","lng":"..."}'
125
+ ```
126
+
127
+ ## Gotchas
128
+
129
+ - **Never print the key or the raw endpoint URL** — the helper handles the
130
+ transport and keeps the key internal.
131
+ - `taxi_create_order` takes only `estimate_trace_id`, `product_category`, and
132
+ optional `caller_car_phone`. Don't pass the estimate's coordinate/name fields.
133
+ - No stored preferences: this skill doesn't remember home/work/car type — ask
134
+ the user, or wire richer memory in a higher layer.
135
+ - Auth failure surfaces as `-32002` (or HTTP 401/403): the key is missing/expired
136
+ → have the user reconnect the connector.
137
+
138
+ > **Setup:** See [authentication](../_shared/authentication.md). This skill uses
139
+ > the `didi` connector's injected `DIDI_MCP_KEY`, not `ACEDATACLOUD_API_TOKEN`.
@@ -0,0 +1,97 @@
1
+ # DiDi MCP — Tool Reference
2
+
3
+ Authoritative schemas come from `python3 $DIDI list` (the DiDi server returns
4
+ each tool's `inputSchema`). This file summarizes the tools and the parameter
5
+ names that are easy to get wrong. **All values are strings.**
6
+
7
+ ## Places & geocoding
8
+
9
+ ### `maps_textsearch` — search a place by text
10
+ | param | required | notes |
11
+ |-------|----------|-------|
12
+ | `keywords` | yes | search text, e.g. `北京西站` (NOT `keyword`) |
13
+ | `city` | no | city name to scope the search, e.g. `北京` (NOT `region`) |
14
+
15
+ Returns candidate places with names + coordinates. Use these coordinates for
16
+ `taxi_estimate` and the `maps_direction_*` tools.
17
+
18
+ ### `maps_regeocode` — coordinates → address
19
+ | param | required |
20
+ |-------|----------|
21
+ | `lat` | yes |
22
+ | `lng` | yes |
23
+
24
+ ## Routes
25
+
26
+ `maps_direction_driving` / `maps_direction_transit` / `maps_direction_walking`
27
+ / `maps_direction_bicycling` — all take the same six fields:
28
+
29
+ | param | required |
30
+ |-------|----------|
31
+ | `from_name` | yes |
32
+ | `from_lat` | yes |
33
+ | `from_lng` | yes |
34
+ | `to_name` | yes |
35
+ | `to_lat` | yes |
36
+ | `to_lng` | yes |
37
+
38
+ ### `maps_place_around` — nearby POI search
39
+ | param | required | notes |
40
+ |-------|----------|-------|
41
+ | `keywords` | yes | POI keyword, e.g. `咖啡` |
42
+ | `lat` | yes | center latitude |
43
+ | `lng` | yes | center longitude |
44
+
45
+ ## Ride hailing
46
+
47
+ ### `taxi_estimate` — price/ETA estimate (do this before ordering)
48
+ Six coordinate/name fields (same as routes):
49
+ `from_name`, `from_lat`, `from_lng`, `to_name`, `to_lat`, `to_lng`.
50
+
51
+ Returns a list of available car types, each with a `productCategory` and price,
52
+ plus a `traceId` / `estimate_trace_id` required by `taxi_create_order`. The
53
+ trace id expires — a stale one returns `-32021`; re-estimate to refresh.
54
+
55
+ Common `product_category` values:
56
+
57
+ | code | 车型 |
58
+ |------|------|
59
+ | `1` | 快车 |
60
+ | `8` | 专车 |
61
+ | `201` | 特惠快车 |
62
+
63
+ > Treat these as hints — always match against the `productCategory` values the
64
+ > live `taxi_estimate` response actually returns. `1` (快车) and `201`
65
+ > (特惠快车) are different service levels; never swap one for the other.
66
+
67
+ ### `taxi_create_order` — book the ride ⚠️ WRITE (needs `--confirm`)
68
+ | param | required | notes |
69
+ |-------|----------|-------|
70
+ | `estimate_trace_id` | yes | from the latest `taxi_estimate` |
71
+ | `product_category` | yes | chosen car type code (string) |
72
+ | `caller_car_phone` | no | omit unless the user gives a number |
73
+
74
+ Only these three fields. Do **not** pass coordinate/name fields.
75
+
76
+ ### `taxi_query_order` — status + driver location
77
+ | param | required |
78
+ |-------|----------|
79
+ | `order_id` | yes |
80
+
81
+ Status `code`: `0` 匹配中 · `1` 司机已接单 · `2` 司机已到达 · `4` 行程中 ·
82
+ `5` 完成 · `6` 系统取消 · `7` 已取消 · `3`/`8`-`12` 其他终态.
83
+
84
+ ### `taxi_cancel_order` — cancel ⚠️ WRITE (needs `--confirm`)
85
+ | param | required |
86
+ |-------|----------|
87
+ | `order_id` | yes |
88
+
89
+ Always ask "确认取消吗?" before adding `--confirm`.
90
+
91
+ ## Errors
92
+
93
+ | code | meaning | action |
94
+ |------|---------|--------|
95
+ | `-32002` / HTTP 401 / 403 | auth failed | key missing/expired → reconnect connector |
96
+ | `-32021` | estimate trace expired | re-run `taxi_estimate` |
97
+ | HTTP 400 | bad params | re-check parameter names against `didi.py list` |
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env python3
2
+ """DiDi ride helper — talks to the DiDi MCP server over the MCP
3
+ Streamable-HTTP transport using only the Python standard library.
4
+
5
+ The `didi` BYOC connector injects one env var:
6
+
7
+ DIDI_MCP_KEY the user's DiDi MCP key (secret — never printed)
8
+
9
+ Usage:
10
+ python3 didi.py list # list available tools + schemas
11
+ python3 didi.py call <tool> '<json-args>' # call any tool
12
+
13
+ State-changing tools (`taxi_create_order`, `taxi_cancel_order`) are
14
+ gated: without a trailing `--confirm` they only DRY-RUN and change
15
+ nothing. `--confirm` is honored only as the LAST argument.
16
+
17
+ Examples:
18
+ python3 didi.py call maps_textsearch '{"keywords":"北京西站","city":"北京"}'
19
+ python3 didi.py call taxi_estimate '{"from_name":"...","from_lat":"39.9","from_lng":"116.3","to_name":"...","to_lat":"39.9","to_lng":"116.4"}'
20
+ python3 didi.py call taxi_create_order '{"estimate_trace_id":"...","product_category":"1"}' --confirm
21
+ python3 didi.py call taxi_query_order '{"order_id":"..."}'
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+ import urllib.error
30
+ import urllib.parse
31
+ import urllib.request
32
+
33
+ MCP_ENDPOINT = os.environ.get("DIDI_MCP_URL", "https://mcp.didichuxing.com/mcp-servers")
34
+ MCP_KEY = os.environ.get("DIDI_MCP_KEY", "").strip()
35
+ PROTOCOL_VERSION = "2025-06-18"
36
+
37
+ # Tools that create or cancel a real ride — must be confirmed explicitly.
38
+ WRITE_TOOLS = {"taxi_create_order", "taxi_cancel_order"}
39
+
40
+
41
+ def die(payload: dict, code: int = 1) -> None:
42
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
43
+ sys.exit(code)
44
+
45
+
46
+ class MCPClient:
47
+ """Minimal MCP Streamable-HTTP client (stdlib only).
48
+
49
+ Never logs or echoes the key; the endpoint URL (which carries the
50
+ key as a query param) is kept internal and never printed.
51
+ """
52
+
53
+ def __init__(self, endpoint: str, key: str) -> None:
54
+ sep = "&" if "?" in endpoint else "?"
55
+ self._url = f"{endpoint}{sep}key={urllib.parse.quote(key, safe='')}"
56
+ self._session_id: str | None = None
57
+ self._rid = 0
58
+
59
+ def _post(self, payload: dict, expect_id):
60
+ body = json.dumps(payload).encode("utf-8")
61
+ req = urllib.request.Request(self._url, data=body, method="POST")
62
+ req.add_header("Content-Type", "application/json")
63
+ req.add_header("Accept", "application/json, text/event-stream")
64
+ req.add_header("MCP-Protocol-Version", PROTOCOL_VERSION)
65
+ if self._session_id:
66
+ req.add_header("Mcp-Session-Id", self._session_id)
67
+ try:
68
+ resp = urllib.request.urlopen(req, timeout=90)
69
+ except urllib.error.HTTPError as exc:
70
+ detail = exc.read().decode("utf-8", "replace")[:600]
71
+ die(
72
+ {
73
+ "error": f"HTTP {exc.code} from DiDi MCP",
74
+ "detail": detail,
75
+ "hint": "If 401/403, the DiDi connector key is missing or expired — "
76
+ "reconnect at https://auth.acedata.cloud/user/connections",
77
+ }
78
+ )
79
+ except urllib.error.URLError as exc:
80
+ die({"error": "network error reaching DiDi MCP", "detail": str(exc.reason)})
81
+ except Exception as exc: # noqa: BLE001
82
+ # Never let an unexpected traceback surface — self._url carries the
83
+ # key as a query param (DiDi mandates ?key=), so emit only the
84
+ # exception type, never its message or the URL.
85
+ die({"error": "unexpected error calling DiDi MCP", "detail": type(exc).__name__})
86
+ sid = resp.headers.get("Mcp-Session-Id")
87
+ if sid:
88
+ self._session_id = sid
89
+ ctype = (resp.headers.get("Content-Type") or "").lower()
90
+ raw = resp.read().decode("utf-8", "replace")
91
+ return _parse_response(raw, ctype, expect_id)
92
+
93
+ def initialize(self) -> None:
94
+ self._rid += 1
95
+ result = self._post(
96
+ {
97
+ "jsonrpc": "2.0",
98
+ "id": self._rid,
99
+ "method": "initialize",
100
+ "params": {
101
+ "protocolVersion": PROTOCOL_VERSION,
102
+ "capabilities": {},
103
+ "clientInfo": {"name": "acedata-didi-skill", "version": "1.0"},
104
+ },
105
+ },
106
+ expect_id=self._rid,
107
+ )
108
+ if isinstance(result, dict) and result.get("error"):
109
+ die({"error": "DiDi MCP initialize failed", "detail": result["error"]})
110
+ # Fire-and-forget the initialized notification (no response expected).
111
+ self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, expect_id=None)
112
+
113
+ def list_tools(self):
114
+ self._rid += 1
115
+ return self._post(
116
+ {"jsonrpc": "2.0", "id": self._rid, "method": "tools/list"},
117
+ expect_id=self._rid,
118
+ )
119
+
120
+ def call_tool(self, name: str, arguments: dict):
121
+ self._rid += 1
122
+ return self._post(
123
+ {
124
+ "jsonrpc": "2.0",
125
+ "id": self._rid,
126
+ "method": "tools/call",
127
+ "params": {"name": name, "arguments": arguments},
128
+ },
129
+ expect_id=self._rid,
130
+ )
131
+
132
+
133
+ def _parse_response(raw: str, ctype: str, expect_id):
134
+ """Return the JSON-RPC envelope for our request id.
135
+
136
+ Handles both plain application/json and text/event-stream (SSE),
137
+ where the response arrives as one or more `data: {...}` lines.
138
+ """
139
+ messages: list[dict] = []
140
+ if "text/event-stream" in ctype:
141
+ for line in raw.splitlines():
142
+ line = line.strip()
143
+ if not line.startswith("data:"):
144
+ continue
145
+ chunk = line[5:].strip()
146
+ if not chunk or chunk == "[DONE]":
147
+ continue
148
+ try:
149
+ messages.append(json.loads(chunk))
150
+ except json.JSONDecodeError:
151
+ continue
152
+ else:
153
+ raw = raw.strip()
154
+ if not raw:
155
+ return {}
156
+ try:
157
+ messages.append(json.loads(raw))
158
+ except json.JSONDecodeError:
159
+ die({"error": "unparseable response from DiDi MCP", "detail": raw[:600]})
160
+ if expect_id is not None:
161
+ for msg in messages:
162
+ if isinstance(msg, dict) and msg.get("id") == expect_id:
163
+ return msg
164
+ return messages[-1] if messages else {}
165
+
166
+
167
+ def emit(envelope: dict) -> None:
168
+ if not isinstance(envelope, dict):
169
+ print(json.dumps(envelope, ensure_ascii=False, indent=2))
170
+ return
171
+ if envelope.get("error"):
172
+ die({"error": envelope["error"]})
173
+ result = envelope.get("result", envelope)
174
+ # tools/call results wrap text in a content[] array.
175
+ if isinstance(result, dict) and isinstance(result.get("content"), list):
176
+ texts = [
177
+ c.get("text", "")
178
+ for c in result["content"]
179
+ if isinstance(c, dict) and c.get("type") == "text"
180
+ ]
181
+ if texts:
182
+ print("\n".join(texts))
183
+ if result.get("isError"):
184
+ sys.exit(1)
185
+ return
186
+ print(json.dumps(result, ensure_ascii=False, indent=2))
187
+
188
+
189
+ def _format_tools(envelope: dict) -> None:
190
+ tools = (envelope.get("result") or {}).get("tools") if isinstance(envelope, dict) else None
191
+ if not tools:
192
+ emit(envelope)
193
+ return
194
+ slim = [
195
+ {
196
+ "name": t.get("name"),
197
+ "description": t.get("description"),
198
+ "inputSchema": t.get("inputSchema"),
199
+ }
200
+ for t in tools
201
+ ]
202
+ print(json.dumps(slim, ensure_ascii=False, indent=2))
203
+
204
+
205
+ def main() -> None:
206
+ if not MCP_KEY:
207
+ die(
208
+ {
209
+ "error": "DIDI_MCP_KEY not set",
210
+ "hint": "Connect the DiDi connector at "
211
+ "https://auth.acedata.cloud/user/connections to inject the key.",
212
+ }
213
+ )
214
+
215
+ argv = sys.argv[1:]
216
+ confirm = bool(argv) and argv[-1] == "--confirm"
217
+ if confirm:
218
+ argv = argv[:-1]
219
+ if not argv:
220
+ die({"error": "usage: didi.py <list|call> ..."})
221
+
222
+ cmd, rest = argv[0], argv[1:]
223
+ client = MCPClient(MCP_ENDPOINT, MCP_KEY)
224
+
225
+ if cmd == "list":
226
+ client.initialize()
227
+ _format_tools(client.list_tools())
228
+ return
229
+
230
+ if cmd == "call":
231
+ if not rest:
232
+ die({"error": "call needs a tool name", "usage": "didi.py call <tool> '<json-args>'"})
233
+ tool = rest[0]
234
+ args_raw = rest[1] if len(rest) > 1 else "{}"
235
+ try:
236
+ arguments = json.loads(args_raw)
237
+ except json.JSONDecodeError as exc:
238
+ die({"error": "arguments must be valid JSON", "detail": str(exc), "got": args_raw})
239
+ if not isinstance(arguments, dict):
240
+ die({"error": "arguments JSON must be an object", "got": args_raw})
241
+
242
+ if tool in WRITE_TOOLS and not confirm:
243
+ print(
244
+ json.dumps(
245
+ {
246
+ "dry_run": True,
247
+ "tool": tool,
248
+ "arguments": arguments,
249
+ "note": "state-changing call — re-run with --confirm as the LAST "
250
+ "argument to actually perform it",
251
+ },
252
+ ensure_ascii=False,
253
+ indent=2,
254
+ )
255
+ )
256
+ return
257
+
258
+ client.initialize()
259
+ emit(client.call_tool(tool, arguments))
260
+ return
261
+
262
+ die({"error": f"unknown command: {cmd}", "usage": "didi.py <list|call> ..."})
263
+
264
+
265
+ if __name__ == "__main__":
266
+ main()
@@ -0,0 +1,224 @@
1
+ ---
2
+ name: tencent-docs
3
+ description: Create, read, list, search, and manage Tencent Docs (腾讯文档) — online documents, sheets, slides, mind maps, flowcharts, smart tables, and forms — via the Tencent Docs Open API. Use when the user mentions 腾讯文档 / Tencent Docs / docs.qq.com, a docs.qq.com link, or wants to create / read / organize a doc, sheet, slide, mind map, or flowchart in their Tencent Docs space.
4
+ when_to_use: |
5
+ Trigger when the user wants to work with their Tencent Docs (腾讯文档) space —
6
+ create a new document / sheet / slide / mind map / flowchart / smart table /
7
+ form, read a doc or sheet's content, list a folder, search files by keyword,
8
+ or rename / move / copy / delete a file. Acts in the user's own Tencent Docs
9
+ account, so destructive or outward-facing writes are gated behind confirmation.
10
+ connections: [tencentdocs]
11
+ allowed_tools: [Bash]
12
+ license: Apache-2.0
13
+ metadata:
14
+ author: acedatacloud
15
+ version: "1.0"
16
+ ---
17
+
18
+ # Tencent Docs (腾讯文档)
19
+
20
+ Drive the [Tencent Docs Open API](https://docs.qq.com/open/document/app/) with
21
+ `curl + jq`. Everything runs **in the user's own Tencent Docs account** — only
22
+ files they can access, only the scopes they granted at connect time.
23
+
24
+ ## Auth — three headers (NOT a single Bearer)
25
+
26
+ The `tencentdocs` BYOC connector injects three env vars; every call sends **all
27
+ three** headers:
28
+
29
+ - `TENCENTDOCS_ACCESS_TOKEN` → `Access-Token` header (OAuth2 access token). **Secret.**
30
+ - `TENCENTDOCS_CLIENT_ID` → `Client-Id` header (the developer app's Client ID).
31
+ - `TENCENTDOCS_OPEN_ID` → `Open-Id` header (the user id returned with the token).
32
+
33
+ All endpoints live under `https://docs.qq.com/openapi`. Define a reusable header
34
+ set once per session:
35
+
36
+ ```sh
37
+ AUTH=(-H "Access-Token: $TENCENTDOCS_ACCESS_TOKEN" \
38
+ -H "Client-Id: $TENCENTDOCS_CLIENT_ID" \
39
+ -H "Open-Id: $TENCENTDOCS_OPEN_ID")
40
+ ```
41
+
42
+ Never echo, print, or log `TENCENTDOCS_ACCESS_TOKEN` — it is full account access.
43
+
44
+ ## Response shape & error handling
45
+
46
+ Every response is `{"ret": 0, "msg": "Succeed", "data": {…}}`. **`ret == 0`
47
+ means success**; any non-zero `ret` is an error — surface `msg` to the user.
48
+ Always check it:
49
+
50
+ ```sh
51
+ resp=$(curl -sS "${AUTH[@]}" … )
52
+ echo "$resp" | jq 'if .ret == 0 then .data else error("Tencent Docs \(.ret): \(.msg)") end'
53
+ ```
54
+
55
+ Common error codes:
56
+
57
+ | code | meaning | what to do |
58
+ |---|---|---|
59
+ | `400006` | access token invalid / expired | tell the user to reconnect 腾讯文档 at <https://auth.acedata.cloud/user/connections> — do **not** loop-retry |
60
+ | `400007` | VIP (超级会员) privilege required | the requested capability needs a Tencent Docs 超级会员; tell the user, link <https://docs.qq.com/vip> |
61
+ | `400008` | 积分 (credits) insufficient | AI-generation quota is exhausted; tell the user to top up |
62
+ | `11607` / `-32603` | bad request params | recheck `fileID` / `type` / body fields against the recipe below |
63
+
64
+ ## Doc types (`type` when creating)
65
+
66
+ | type | Product | Notes |
67
+ |---|---|---|
68
+ | `doc` | 在线文档 (Word-style) | classic rich-text document |
69
+ | `sheet` | 在线表格 (Excel) | data tables |
70
+ | `slide` | 幻灯片 (PPT) | presentations |
71
+ | `mind` | 思维导图 | mind map |
72
+ | `flowchart` | 流程图 | flowchart |
73
+ | `smartsheet` | 智能表格 | structured multi-view table |
74
+ | `form` | 收集表 | form / survey |
75
+
76
+ ## Recipes
77
+
78
+ ### Verify auth (always run first)
79
+
80
+ Cheap sanity check — read the app's OpenAPI usage counter:
81
+
82
+ ```sh
83
+ curl -sS "${AUTH[@]}" "https://docs.qq.com/openapi/drive/v2/util/resource-use" \
84
+ | jq 'if .ret == 0 then .data else "ERR \(.ret): \(.msg)" end'
85
+ ```
86
+
87
+ If this returns `400006`, the connection is expired — have the user reconnect.
88
+
89
+ ### List a folder (root = `/`)
90
+
91
+ ```sh
92
+ curl -sS "${AUTH[@]}" \
93
+ "https://docs.qq.com/openapi/drive/v2/folders/%2F?listType=folder&sortType=browse&asc=0" \
94
+ | jq 'if .ret == 0 then [.data.list[]? | {ID, title, type, url}] else . end'
95
+ ```
96
+
97
+ `%2F` is the URL-encoded root folder id. For a sub-folder use its folder id in
98
+ the path.
99
+
100
+ ### Search files by keyword
101
+
102
+ ```sh
103
+ curl -sS -G "${AUTH[@]}" \
104
+ "https://docs.qq.com/openapi/drive/v2/search" \
105
+ --data-urlencode "searchName=Q1 预算" \
106
+ | jq 'if .ret == 0 then [.data.list[]? | {ID, title, type, url}] else . end'
107
+ ```
108
+
109
+ ### Read a file's metadata
110
+
111
+ The `fileID` is the last path segment of a `https://docs.qq.com/doc/<id>` (or
112
+ `/sheet/`, `/slide/`, …) URL.
113
+
114
+ ```sh
115
+ curl -sS "${AUTH[@]}" \
116
+ "https://docs.qq.com/openapi/drive/v2/files/FILE_ID/metadata" \
117
+ | jq 'if .ret == 0 then .data else . end'
118
+ ```
119
+
120
+ ### Read an online document's content
121
+
122
+ ```sh
123
+ curl -sS "${AUTH[@]}" \
124
+ "https://docs.qq.com/openapi/document/v3/files/FILE_ID/export?exportType=text" \
125
+ | jq 'if .ret == 0 then .data else . end'
126
+ ```
127
+
128
+ ### Read a sheet's cell range
129
+
130
+ Get the sheet's `sheetId` list from the metadata first, then read a range
131
+ (`A1:D10`):
132
+
133
+ ```sh
134
+ curl -sS "${AUTH[@]}" \
135
+ "https://docs.qq.com/openapi/spreadsheet/v2/files/FILE_ID/sheets/SHEET_ID/values?range=A1:D10" \
136
+ | jq 'if .ret == 0 then .data.values else . end'
137
+ ```
138
+
139
+ ### Create a file
140
+
141
+ `POST /openapi/drive/v2/files` with form-urlencoded `title` + `type`. Returns the
142
+ new file's `ID` and `url`.
143
+
144
+ ```sh
145
+ curl -sS -X POST "${AUTH[@]}" \
146
+ "https://docs.qq.com/openapi/drive/v2/files" \
147
+ --data-urlencode "title=会议纪要 2026-07-03" \
148
+ --data-urlencode "type=doc" \
149
+ | jq 'if .ret == 0 then {ID: .data.ID, url: .data.url} else . end'
150
+ ```
151
+
152
+ Swap `type=sheet` / `slide` / `mind` / `flowchart` / `smartsheet` / `form` for
153
+ other doc types. Pass `--data-urlencode "folderID=<id>"` to create inside a
154
+ folder instead of the root.
155
+
156
+ ### Upload an image (for embedding in docs)
157
+
158
+ ```sh
159
+ curl -sS -X POST "${AUTH[@]}" \
160
+ "https://docs.qq.com/openapi/resources/v2/images" \
161
+ -F "file=@./cover.png" \
162
+ | jq 'if .ret == 0 then .data else . end'
163
+ ```
164
+
165
+ ### Rename a file — GATED, confirm first
166
+
167
+ Show the user the current `title` + `url` and the new title, get an explicit
168
+ "yes", then run:
169
+
170
+ ```sh
171
+ curl -sS -X PATCH "${AUTH[@]}" \
172
+ "https://docs.qq.com/openapi/drive/v2/files/FILE_ID" \
173
+ --data-urlencode "title=新的标题" \
174
+ | jq 'if .ret == 0 then "renamed" else . end'
175
+ ```
176
+
177
+ ### Move a file — GATED, confirm first
178
+
179
+ Show the user the file's current `title` + location and the destination folder,
180
+ get an explicit "yes", then run. `folderID=/` moves it back to the root.
181
+
182
+ ```sh
183
+ curl -sS -X PATCH "${AUTH[@]}" \
184
+ "https://docs.qq.com/openapi/drive/v2/files/FILE_ID/move" \
185
+ --data-urlencode "folderID=DEST_FOLDER_ID" \
186
+ | jq 'if .ret == 0 then "moved" else . end'
187
+ ```
188
+
189
+ ### Copy a file
190
+
191
+ ```sh
192
+ curl -sS -X POST "${AUTH[@]}" \
193
+ "https://docs.qq.com/openapi/drive/v2/files/FILE_ID/copy" \
194
+ --data-urlencode "title=副本 - 项目计划" \
195
+ | jq 'if .ret == 0 then {ID: .data.ID, url: .data.url} else . end'
196
+ ```
197
+
198
+ ### Delete a file — GATED, confirm first
199
+
200
+ Deletion moves the file to the recycle bin but is still destructive. **Show the
201
+ user the exact `title` + `url`, get an explicit "yes", then run:**
202
+
203
+ ```sh
204
+ curl -sS -X DELETE "${AUTH[@]}" \
205
+ "https://docs.qq.com/openapi/drive/v2/files/FILE_ID" \
206
+ | jq 'if .ret == 0 then "deleted" else . end'
207
+ ```
208
+
209
+ ## Notes
210
+
211
+ - **Gate writes.** Rename / move / delete / copy and any share-permission change
212
+ act on the user's real files — confirm the exact target before running.
213
+ - **Extract ids from links.** When the user pastes a `docs.qq.com/doc/<id>` (or
214
+ `/sheet/`, `/slide/`, `/mind/`, `/flowchart/`, `/form/`, `/smartsheet/`) URL,
215
+ take the id from the path rather than asking for it.
216
+ - **Pagination.** List / search endpoints return a cursor / `next` marker in
217
+ `data`; pass it back to fetch the next page. Stop when the marker is empty.
218
+ - **Rate / quota.** Free apps get 20,000 API calls/month (超级会员 20,000/day,
219
+ Plus 40,000/day). AI-generation features additionally consume 积分 and may
220
+ require 超级会员 — a `400007` / `400008` means the account tier / credits, not a
221
+ bug in the request.
222
+ - **Full API index.** Beyond the recipes above, the Open API also covers
223
+ smart-table records, form collection, folder CRUD, permissions, and
224
+ import/export — see the [接口索引](https://docs.qq.com/open/document/app/openapi/v2/file/).