@openclaw/qqbot 2026.6.11 → 2026.7.1-beta.2
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/dist/api.js +6 -6
- package/dist/{channel-DvWrqcZB.js → channel-BQM6pDlD.js} +71 -126
- package/dist/{channel-entry-BGXFRmbg.js → channel-entry-C18ypTaG.js} +3 -3
- package/dist/channel-entry-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-CldhUlTO.js → channel.setup-2ItDYKhz.js} +1 -1
- package/dist/{config-schema-gMW9zKvQ.js → config-schema-JZEf1dvB.js} +2 -5
- package/dist/doctor-contract-api.js +168 -2
- package/dist/{gateway-CCkrdgaK.js → gateway-CjdB3B6n.js} +284 -264
- package/dist/{group-CvVXke_k.js → group-BmS1Qwst.js} +2 -2
- package/dist/{handler-runtime--OTMkFWa.js → handler-runtime-rU03VA43.js} +3 -3
- package/dist/{outbound-BQcPCo-z.js → outbound-BZHwMFUX.js} +496 -44
- package/dist/{runtime-D1hn91-s.js → runtime-B4HWCOa-.js} +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/{sender-CRZ2af7P.js → sender-BfVLJJYp.js} +16 -14
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{doctor-contract-BYTS8-ia.js → state-keys-CQKlAFyo.js} +7 -1
- package/dist/target-parser-DagYRQkP.js +71 -0
- package/dist/{tools-CeUI9pG-.js → tools-C8kTMXKc.js} +58 -13
- package/dist/tools-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
- package/skills/qqbot-channel/SKILL.md +54 -41
- package/skills/qqbot-channel/references/api_references.md +16 -8
- package/skills/qqbot-media/SKILL.md +19 -13
- package/skills/qqbot-remind/SKILL.md +15 -14
- package/dist/target-parser-BdCUmxK7.js +0 -285
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
通过 `qqbot_channel_api` 工具代理请求,工具自动处理鉴权。
|
|
6
6
|
|
|
7
|
+
## 调用安全规则
|
|
8
|
+
|
|
9
|
+
- `POST`、`PUT`、`PATCH` 和 `DELETE` 会修改真实 QQ 资源。调用前确认用户明确授权了该操作。
|
|
10
|
+
- 删除接口不可逆。删除前先用读取接口确认目标 ID、名称和范围,并把将要删除的对象复述给用户;`qqbot_channel_api` 要求 `confirmed: true` 才会执行 `DELETE`。
|
|
11
|
+
- 批量删除 sentinel 必须二次确认并额外传 `bulkConfirmed: true`;不要把模糊表达自动扩展成“删除全部”。
|
|
12
|
+
- 删除端点不作为普通 agent 速查路径列出。需要删除时,先用读取接口确认对象,再通过受确认保护的删除流程执行。
|
|
13
|
+
- 成员资料和头像 URL 只用于当前请求;除非用户明确要求查看头像/图标,不要内联展示或转发这些图片 URL。
|
|
14
|
+
|
|
7
15
|
---
|
|
8
16
|
|
|
9
17
|
## 📌 通用说明
|
|
@@ -346,9 +354,9 @@ interface Schedule {
|
|
|
346
354
|
|
|
347
355
|
---
|
|
348
356
|
|
|
349
|
-
###
|
|
357
|
+
### 删除子频道(破坏性操作)
|
|
350
358
|
|
|
351
|
-
> ⚠️
|
|
359
|
+
> ⚠️ 不可逆!仅私域机器人可用。调用前必须确认具体子频道 ID、子频道名称和用户删除意图,并传 `confirmed: true`;不要按模糊名称猜测删除目标。确认后使用子频道资源路径 `/channels/{channel_id}`。
|
|
352
360
|
|
|
353
361
|
---
|
|
354
362
|
|
|
@@ -415,9 +423,9 @@ interface Schedule {
|
|
|
415
423
|
|
|
416
424
|
---
|
|
417
425
|
|
|
418
|
-
###
|
|
426
|
+
### 删除公告(破坏性操作)
|
|
419
427
|
|
|
420
|
-
> `message_id
|
|
428
|
+
> 调用前必须确认具体公告 ID 并传 `confirmed: true`,确认后使用公告资源路径 `/guilds/{guild_id}/announces/{message_id}`。批量删除全部公告只能在用户明确要求并再次确认后使用 `/guilds/{guild_id}/announces/all`,并且必须额外传 `bulkConfirmed: true`。
|
|
421
429
|
|
|
422
430
|
---
|
|
423
431
|
|
|
@@ -453,9 +461,9 @@ interface Schedule {
|
|
|
453
461
|
|
|
454
462
|
---
|
|
455
463
|
|
|
456
|
-
###
|
|
464
|
+
### 删除帖子(破坏性操作)
|
|
457
465
|
|
|
458
|
-
> ⚠️
|
|
466
|
+
> ⚠️ 不可逆!仅私域机器人可用。调用前必须确认具体帖子 ID、帖子标题/作者和用户删除意图,并传 `confirmed: true`。确认后使用帖子资源路径 `/channels/{channel_id}/threads/{thread_id}`。
|
|
459
467
|
|
|
460
468
|
---
|
|
461
469
|
|
|
@@ -516,6 +524,6 @@ interface Schedule {
|
|
|
516
524
|
|
|
517
525
|
---
|
|
518
526
|
|
|
519
|
-
###
|
|
527
|
+
### 删除日程(破坏性操作)
|
|
520
528
|
|
|
521
|
-
> ⚠️
|
|
529
|
+
> ⚠️ 不可逆!需要管理频道权限。调用前必须确认具体日程 ID、日程名称/时间和用户删除意图,并传 `confirmed: true`。确认后使用日程资源路径 `/channels/{channel_id}/schedules/{schedule_id}`。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qqbot-media
|
|
3
|
-
description: QQBot rich media send and receive support. Use <qqmedia> tags
|
|
3
|
+
description: QQBot rich media send and receive support. Use <qqmedia> tags only for explicit media send/view requests, treating inbound attachment paths as private current-conversation context.
|
|
4
4
|
metadata: { "openclaw": { "emoji": "📸", "requires": { "config": ["channels.qqbot"] } } }
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ metadata: { "openclaw": { "emoji": "📸", "requires": { "config": ["channels.qq
|
|
|
9
9
|
## 用法
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
<qqmedia
|
|
12
|
+
<qqmedia>{实际路径或URL}</qqmedia>
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
系统根据文件扩展名自动识别类型并路由:
|
|
@@ -18,20 +18,26 @@ metadata: { "openclaw": { "emoji": "📸", "requires": { "config": ["channels.qq
|
|
|
18
18
|
- `.silk/.wav/.mp3/.ogg/.aac/.flac` 等 → 语音
|
|
19
19
|
- `.mp4/.mov/.avi/.mkv/.webm` 等 → 视频
|
|
20
20
|
- 其他扩展名 → 文件
|
|
21
|
-
-
|
|
21
|
+
- 无扩展名的当前会话本地/host-read 媒体 → 按加载出的实际媒体类型路由
|
|
22
|
+
- 无扩展名的远程 URL → 可能按文件发送;如需图片/语音/视频,请提供能识别类型的 URL/路径或使用明确媒体标签
|
|
22
23
|
|
|
23
24
|
## 接收媒体
|
|
24
25
|
|
|
25
|
-
-
|
|
26
|
-
- 用户发来的**语音**路径在上下文中;若有 STT
|
|
26
|
+
- 用户发来的**图片**会由 QQBot 运行时下载到 OpenClaw 管理的 QQBot media 目录,路径只作为当前会话的附件上下文使用。
|
|
27
|
+
- 用户发来的**语音**路径在上下文中;若有 STT 能力则优先转写。
|
|
28
|
+
- 附件路径和远程 URL 可能包含用户私有内容。不要无关输出本地绝对路径,不要把附件转发到其他会话;只有用户明确要求回发、分析或转存该媒体时才使用。
|
|
29
|
+
- 不承诺长期保留附件。若用户需要长期保存,说明应由用户自行保存或重新发送。
|
|
27
30
|
|
|
28
31
|
## 规则
|
|
29
32
|
|
|
30
|
-
1.
|
|
31
|
-
2.
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
35
|
-
6.
|
|
36
|
-
7.
|
|
37
|
-
8.
|
|
33
|
+
1. **标签必须用开闭标签包裹实际路径或 URL**:`<qqmedia>{实际路径或URL}</qqmedia>`
|
|
34
|
+
2. **使用你实际看到的文件路径**:刚创建文件时,用创建结果显示的路径;只有当沙箱 workspace-write 创建结果实际显示 `/workspace/...` 时,才按原样使用该路径,例如 `<qqmedia>/workspace/report.pdf</qqmedia>`。
|
|
35
|
+
3. **附件路径直接使用上下文给出的路径**:如果路径来自会话【附件】上下文,不要改写成 `/workspace/...`。
|
|
36
|
+
4. **URL 可以直接发送**:例如 `<qqmedia>https://example.com/image.png</qqmedia>`。
|
|
37
|
+
5. **本地路径仍受安全根限制**:只能发送当前会话授权的 agent workspace、scoped media roots、OpenClaw 媒体目录或 QQBot 媒体目录内的文件;不要使用 `..` 逃出工作区。
|
|
38
|
+
6. **不要扫描或主动发送上下文之外的本地文件**:只使用用户提供、工具刚生成,或当前会话上下文明确给出的路径。
|
|
39
|
+
7. **文件大小上限**:图片 30MB / 视频 100MB / 文件 100MB / 语音 20MB
|
|
40
|
+
8. **你有能力发送本地图片/文件**,直接用标签包裹路径即可,**不要说"无法发送"**
|
|
41
|
+
9. 发送语音时不要重复语音中已朗读的文字
|
|
42
|
+
10. 多个媒体用多个标签
|
|
43
|
+
11. 以会话上下文中的能力说明为准(如未启用语音则不要发语音)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qqbot-remind
|
|
3
|
-
description: QQBot scheduled reminders.
|
|
3
|
+
description: QQBot scheduled reminders. Use only for explicit user requests to create, list, or cancel one-time or recurring QQ reminders; ask for missing time, content, or timezone before scheduling.
|
|
4
4
|
metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqbot"] } } }
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# QQ Bot 定时提醒
|
|
8
8
|
|
|
9
|
-
## ⚠️
|
|
9
|
+
## ⚠️ 意图规则
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
只有当用户明确要求创建、查询或取消提醒/闹钟/定时任务时,才调用工具。闲聊、假设、解释提醒功能、讨论将来计划但未要求创建提醒时,不要调用工具。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
如果用户确实要求提醒,你没有内存或后台线程,口头承诺"到时候提醒"是无效的——必须调用工具才能真正注册定时任务。时间、提醒内容、目标会话或时区不清楚时先追问;不要替用户猜测。
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -18,13 +18,14 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
|
|
|
18
18
|
|
|
19
19
|
**第一步**:调用 `qqbot_remind` 工具,传入简单参数:
|
|
20
20
|
|
|
21
|
-
| 参数
|
|
22
|
-
|
|
|
23
|
-
| `action`
|
|
24
|
-
| `content`
|
|
25
|
-
| `to`
|
|
26
|
-
| `time`
|
|
27
|
-
| `
|
|
21
|
+
| 参数 | 说明 | 示例 |
|
|
22
|
+
| ---------- | -------------------------------------------- | ------------------------------------------- |
|
|
23
|
+
| `action` | 操作类型 | `"add"` / `"list"` / `"remove"` |
|
|
24
|
+
| `content` | 提醒内容 | `"喝水"` |
|
|
25
|
+
| `to` | 目标地址(可选,系统自动获取,通常无需填写) | — |
|
|
26
|
+
| `time` | 时间(相对时间或 cron 表达式) | `"5m"` / `"1h30m"` / `"0 8 * * *"` |
|
|
27
|
+
| `timezone` | IANA 时区(周期提醒建议明确传入) | `"Asia/Shanghai"` / `"America/Los_Angeles"` |
|
|
28
|
+
| `jobId` | 任务 ID(仅 remove) | `"xxx"` |
|
|
28
29
|
|
|
29
30
|
**第二步**:根据 `qqbot_remind` 的返回结果,回复用户。`qqbot_remind` 会直接创建、查询或取消 Gateway cron 任务;成功后不要再调用 `cron` 工具。
|
|
30
31
|
|
|
@@ -92,7 +93,7 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
|
|
|
92
93
|
"action": "add",
|
|
93
94
|
"job": {
|
|
94
95
|
"name": "{任务名}",
|
|
95
|
-
"schedule": { "kind": "cron", "expr": "0 8 * * *", "tz": "
|
|
96
|
+
"schedule": { "kind": "cron", "expr": "0 8 * * *", "tz": "{用户确认的 IANA 时区}" },
|
|
96
97
|
"sessionTarget": "isolated",
|
|
97
98
|
"wakeMode": "now",
|
|
98
99
|
"payload": {
|
|
@@ -124,7 +125,7 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
|
|
|
124
125
|
| 每周末上午10点 | `"0 10 * * 0,6"` |
|
|
125
126
|
| 每小时整点 | `"0 * * * *"` |
|
|
126
127
|
|
|
127
|
-
>
|
|
128
|
+
> 周期提醒应使用用户明确提供、用户资料/会话中可信可得,或用户确认过的 IANA 时区。无法判断时先追问;不要把所有用户都假定在同一时区。
|
|
128
129
|
|
|
129
130
|
---
|
|
130
131
|
|
|
@@ -141,7 +142,7 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
|
|
|
141
142
|
| "修改提醒时间" | `remove` → `add` | — |
|
|
142
143
|
| "提醒我"(无时间) | **需追问** | — |
|
|
143
144
|
|
|
144
|
-
纯相对时间("5分钟后"、"1小时后"
|
|
145
|
+
纯相对时间("5分钟后"、"1小时后")可直接计算,无需确认。时间、日期、周期、内容或时区模糊/缺失时需追问。周期提醒在回复中说明解释后的本地时间和时区。
|
|
145
146
|
|
|
146
147
|
---
|
|
147
148
|
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { c as getPlatformAdapter } from "./string-normalize-R_0cKO7Q.js";
|
|
2
|
-
import { a as formatErrorMessage, n as debugLog, r as debugWarn } from "./log-SDfMMBWe.js";
|
|
3
|
-
import * as fs$1 from "node:fs";
|
|
4
|
-
import * as os$1 from "node:os";
|
|
5
|
-
import * as path$1 from "node:path";
|
|
6
|
-
//#region extensions/qqbot/src/engine/utils/platform.ts
|
|
7
|
-
/**
|
|
8
|
-
* Cross-platform path and detection helpers for core/ modules.
|
|
9
|
-
*
|
|
10
|
-
* Provides home/data/media directory helpers, platform detection,
|
|
11
|
-
* silk-wasm availability checks — all without importing `openclaw/plugin-sdk`.
|
|
12
|
-
* The temp-directory fallback is delegated to the PlatformAdapter.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Resolve the current user's OS home directory safely across platforms.
|
|
16
|
-
*
|
|
17
|
-
* Priority:
|
|
18
|
-
* 1. `os.homedir()`
|
|
19
|
-
* 2. `$HOME` or `%USERPROFILE%`
|
|
20
|
-
* 3. PlatformAdapter.getTempDir() as a last resort
|
|
21
|
-
*
|
|
22
|
-
* This is the *operating-system* home and intentionally ignores
|
|
23
|
-
* `OPENCLAW_HOME`. QQ Bot still checks this tree for legacy state imports and
|
|
24
|
-
* media-path remaps from older releases.
|
|
25
|
-
*/
|
|
26
|
-
function getHomeDir() {
|
|
27
|
-
try {
|
|
28
|
-
const home = os$1.homedir();
|
|
29
|
-
if (home && fs$1.existsSync(home)) return home;
|
|
30
|
-
} catch {}
|
|
31
|
-
const envHome = process.env.HOME || process.env.USERPROFILE;
|
|
32
|
-
if (envHome && fs$1.existsSync(envHome)) return envHome;
|
|
33
|
-
return getPlatformAdapter().getTempDir();
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve the effective OpenClaw home directory.
|
|
37
|
-
*
|
|
38
|
-
* Mirrors the contract from core (`src/infra/home-dir.ts::resolveEffectiveHomeDir`)
|
|
39
|
-
* so QQ Bot media roots live under the same tree the rest of OpenClaw treats as
|
|
40
|
-
* `~`. The extension cannot import the core helper directly (it is a separate
|
|
41
|
-
* package with `openclaw` as a peer dependency), so this re-implements the
|
|
42
|
-
* minimal contract:
|
|
43
|
-
*
|
|
44
|
-
* 1. `OPENCLAW_HOME` when set (with `~` / `~/...` expanded against the OS home).
|
|
45
|
-
* 2. Otherwise fall back to {@link getHomeDir} so existing single-home
|
|
46
|
-
* deployments are unaffected.
|
|
47
|
-
*
|
|
48
|
-
* Empty / `"undefined"` / `"null"` strings are treated as unset to match how
|
|
49
|
-
* core normalizes the variable.
|
|
50
|
-
*/
|
|
51
|
-
function resolveOpenClawHome() {
|
|
52
|
-
const raw = process.env.OPENCLAW_HOME?.trim();
|
|
53
|
-
if (!raw || raw === "undefined" || raw === "null") return getHomeDir();
|
|
54
|
-
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
55
|
-
const osHome = getHomeDir();
|
|
56
|
-
if (raw === "~") return osHome;
|
|
57
|
-
return path$1.join(osHome, raw.slice(2));
|
|
58
|
-
}
|
|
59
|
-
return raw;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Return a legacy path under `~/.openclaw/qqbot` without creating it.
|
|
63
|
-
*
|
|
64
|
-
* Current QQ Bot runtime state lives in plugin SQLite KV. This path remains for
|
|
65
|
-
* legacy imports and media-path remaps from older releases.
|
|
66
|
-
*/
|
|
67
|
-
function getQQBotDataPath(...subPaths) {
|
|
68
|
-
return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
|
|
69
|
-
}
|
|
70
|
-
/** Return a path under `~/.openclaw/qqbot`, creating it on demand. */
|
|
71
|
-
function getQQBotDataDir(...subPaths) {
|
|
72
|
-
const dir = getQQBotDataPath(...subPaths);
|
|
73
|
-
if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
|
|
74
|
-
return dir;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Return a path under `<openclaw-home>/.openclaw/media/qqbot` without creating it.
|
|
78
|
-
*
|
|
79
|
-
* Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist
|
|
80
|
-
* so downloaded images and audio can be accessed by framework media tooling.
|
|
81
|
-
* The base honors `OPENCLAW_HOME` (when set) so files written by agents into
|
|
82
|
-
* the OpenClaw-managed media tree are reachable by this plugin even when
|
|
83
|
-
* `HOME` and `OPENCLAW_HOME` differ (Docker, multi-user hosts). Fixes #83562.
|
|
84
|
-
*/
|
|
85
|
-
function getQQBotMediaPath(...subPaths) {
|
|
86
|
-
return path$1.join(resolveOpenClawHome(), ".openclaw", "media", "qqbot", ...subPaths);
|
|
87
|
-
}
|
|
88
|
-
/** Return a path under `<openclaw-home>/.openclaw/media/qqbot`, creating it on demand. */
|
|
89
|
-
function getQQBotMediaDir(...subPaths) {
|
|
90
|
-
const dir = getQQBotMediaPath(...subPaths);
|
|
91
|
-
if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
|
|
92
|
-
return dir;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Return `<openclaw-home>/.openclaw/media`, OpenClaw's shared media root.
|
|
96
|
-
*
|
|
97
|
-
* This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
|
|
98
|
-
* allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
|
|
99
|
-
* QQ Bot payload root lets the plugin trust framework-produced files that live
|
|
100
|
-
* in sibling subdirectories such as `outbound/` (written by
|
|
101
|
-
* `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
|
|
102
|
-
* the check anchored to a single, well-known directory. Like
|
|
103
|
-
* {@link getQQBotMediaPath}, the base honors `OPENCLAW_HOME`.
|
|
104
|
-
*/
|
|
105
|
-
function getOpenClawMediaDir() {
|
|
106
|
-
return path$1.join(resolveOpenClawHome(), ".openclaw", "media");
|
|
107
|
-
}
|
|
108
|
-
function isWindows() {
|
|
109
|
-
return process.platform === "win32";
|
|
110
|
-
}
|
|
111
|
-
/** Return the preferred temporary directory. */
|
|
112
|
-
function getTempDir() {
|
|
113
|
-
return getPlatformAdapter().getTempDir();
|
|
114
|
-
}
|
|
115
|
-
let silkWasmAvailable = null;
|
|
116
|
-
/** Check whether silk-wasm can run in the current environment. */
|
|
117
|
-
async function checkSilkWasmAvailable() {
|
|
118
|
-
if (silkWasmAvailable !== null) return silkWasmAvailable;
|
|
119
|
-
try {
|
|
120
|
-
const { isSilk } = await import("silk-wasm");
|
|
121
|
-
isSilk(new Uint8Array(0));
|
|
122
|
-
silkWasmAvailable = true;
|
|
123
|
-
debugLog("[platform] silk-wasm: available");
|
|
124
|
-
} catch (err) {
|
|
125
|
-
silkWasmAvailable = false;
|
|
126
|
-
debugWarn(`[platform] silk-wasm: NOT available (${formatErrorMessage(err)})`);
|
|
127
|
-
}
|
|
128
|
-
return silkWasmAvailable;
|
|
129
|
-
}
|
|
130
|
-
/** Expand `~` to the current user's home directory. */
|
|
131
|
-
function expandTilde(p) {
|
|
132
|
-
if (!p) return p;
|
|
133
|
-
if (p === "~") return getHomeDir();
|
|
134
|
-
if (p.startsWith("~/") || p.startsWith("~\\")) return path$1.join(getHomeDir(), p.slice(2));
|
|
135
|
-
return p;
|
|
136
|
-
}
|
|
137
|
-
/** Normalize a user-provided path by trimming, stripping `file://`, and expanding `~`. */
|
|
138
|
-
function normalizePath(p) {
|
|
139
|
-
let result = p.trim();
|
|
140
|
-
if (result.startsWith("file://")) {
|
|
141
|
-
result = result.slice(7);
|
|
142
|
-
try {
|
|
143
|
-
result = decodeURIComponent(result);
|
|
144
|
-
} catch {}
|
|
145
|
-
}
|
|
146
|
-
return expandTilde(result);
|
|
147
|
-
}
|
|
148
|
-
/** Return true when the string looks like a local filesystem path rather than a URL. */
|
|
149
|
-
function isLocalPath(p) {
|
|
150
|
-
if (!p) return false;
|
|
151
|
-
if (p.startsWith("file://")) return true;
|
|
152
|
-
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return true;
|
|
153
|
-
if (p.startsWith("/")) return true;
|
|
154
|
-
if (/^[a-zA-Z]:[\\/]/.test(p)) return true;
|
|
155
|
-
if (p.startsWith("\\\\")) return true;
|
|
156
|
-
if (p.startsWith("./") || p.startsWith("../")) return true;
|
|
157
|
-
if (p.startsWith(".\\") || p.startsWith("..\\")) return true;
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
function isPathWithinRoot(candidate, root) {
|
|
161
|
-
const relative = path$1.relative(root, candidate);
|
|
162
|
-
return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
|
|
163
|
-
}
|
|
164
|
-
/** Remap legacy or hallucinated QQ Bot local media paths to real files when possible. */
|
|
165
|
-
function resolveQQBotLocalMediaPath(p) {
|
|
166
|
-
const normalized = normalizePath(p);
|
|
167
|
-
if (!isLocalPath(normalized) || fs$1.existsSync(normalized)) return normalized;
|
|
168
|
-
const osHomeDir = getHomeDir();
|
|
169
|
-
const openclawHomeDir = resolveOpenClawHome();
|
|
170
|
-
const mediaRoot = getQQBotMediaPath();
|
|
171
|
-
const dataRoot = getQQBotDataPath();
|
|
172
|
-
const candidateRoots = [
|
|
173
|
-
...Array.from(new Set([path$1.join(osHomeDir, ".openclaw", "workspace", "qqbot"), path$1.join(openclawHomeDir, ".openclaw", "workspace", "qqbot")])).map((from) => ({
|
|
174
|
-
from,
|
|
175
|
-
to: mediaRoot
|
|
176
|
-
})),
|
|
177
|
-
{
|
|
178
|
-
from: dataRoot,
|
|
179
|
-
to: mediaRoot
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
from: mediaRoot,
|
|
183
|
-
to: dataRoot
|
|
184
|
-
}
|
|
185
|
-
];
|
|
186
|
-
for (const { from, to } of candidateRoots) {
|
|
187
|
-
if (!isPathWithinRoot(normalized, from)) continue;
|
|
188
|
-
const relative = path$1.relative(from, normalized);
|
|
189
|
-
const candidate = path$1.join(to, relative);
|
|
190
|
-
if (fs$1.existsSync(candidate)) {
|
|
191
|
-
debugWarn(`[platform] Remapped missing QQBot media path ${normalized} -> ${candidate}`);
|
|
192
|
-
return candidate;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return normalized;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Resolve a structured-payload local file path and enforce that it stays within
|
|
199
|
-
* QQ Bot-owned storage roots.
|
|
200
|
-
*/
|
|
201
|
-
function resolveQQBotPayloadLocalFilePath(p) {
|
|
202
|
-
const candidate = resolveQQBotLocalMediaPath(p);
|
|
203
|
-
if (!candidate.trim()) return null;
|
|
204
|
-
const resolvedCandidate = path$1.resolve(candidate);
|
|
205
|
-
if (!fs$1.existsSync(resolvedCandidate)) return null;
|
|
206
|
-
const canonicalCandidate = fs$1.realpathSync(resolvedCandidate);
|
|
207
|
-
const allowedRoots = [getOpenClawMediaDir(), getQQBotMediaPath()];
|
|
208
|
-
for (const root of allowedRoots) {
|
|
209
|
-
const resolvedRoot = path$1.resolve(root);
|
|
210
|
-
if (isPathWithinRoot(canonicalCandidate, fs$1.existsSync(resolvedRoot) ? fs$1.realpathSync(resolvedRoot) : resolvedRoot)) return canonicalCandidate;
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region extensions/qqbot/src/engine/messaging/target-parser.ts
|
|
216
|
-
/**
|
|
217
|
-
* Parse a qqbot target string into a structured delivery target.
|
|
218
|
-
*
|
|
219
|
-
* Supported formats:
|
|
220
|
-
* - `qqbot:c2c:openid` → C2C direct message
|
|
221
|
-
* - `qqbot:group:groupid` → Group message
|
|
222
|
-
* - `qqbot:channel:channelid` → Channel message
|
|
223
|
-
* - `c2c:openid` → C2C (without qqbot: prefix)
|
|
224
|
-
* - `group:groupid` → Group (without qqbot: prefix)
|
|
225
|
-
* - `channel:channelid` → Channel (without qqbot: prefix)
|
|
226
|
-
* - `openid` → C2C (bare openid, default)
|
|
227
|
-
*
|
|
228
|
-
* @param to - Raw target string.
|
|
229
|
-
* @returns Parsed target with type and id.
|
|
230
|
-
* @throws {Error} When the target format is invalid.
|
|
231
|
-
*/
|
|
232
|
-
function parseTarget(to) {
|
|
233
|
-
const id = to.replace(/^qqbot:/i, "");
|
|
234
|
-
if (id.startsWith("c2c:")) {
|
|
235
|
-
const userId = id.slice(4);
|
|
236
|
-
if (!userId) throw new Error(`Invalid c2c target format: ${to} - missing user ID`);
|
|
237
|
-
return {
|
|
238
|
-
type: "c2c",
|
|
239
|
-
id: userId
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (id.startsWith("group:")) {
|
|
243
|
-
const groupId = id.slice(6);
|
|
244
|
-
if (!groupId) throw new Error(`Invalid group target format: ${to} - missing group ID`);
|
|
245
|
-
return {
|
|
246
|
-
type: "group",
|
|
247
|
-
id: groupId
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
if (id.startsWith("channel:")) {
|
|
251
|
-
const channelId = id.slice(8);
|
|
252
|
-
if (!channelId) throw new Error(`Invalid channel target format: ${to} - missing channel ID`);
|
|
253
|
-
return {
|
|
254
|
-
type: "channel",
|
|
255
|
-
id: channelId
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (!id) throw new Error(`Invalid target format: ${to} - empty ID after removing qqbot: prefix`);
|
|
259
|
-
return {
|
|
260
|
-
type: "c2c",
|
|
261
|
-
id
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Normalize a QQ Bot target string into the canonical `qqbot:...` form.
|
|
266
|
-
*
|
|
267
|
-
* Returns `undefined` when the target does not look like a QQ Bot address.
|
|
268
|
-
*/
|
|
269
|
-
function normalizeTarget(target) {
|
|
270
|
-
const id = target.replace(/^qqbot:/i, "");
|
|
271
|
-
if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) return `qqbot:${id}`;
|
|
272
|
-
if (/^[0-9a-fA-F]{32}$/.test(id)) return `qqbot:c2c:${id}`;
|
|
273
|
-
if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) return `qqbot:c2c:${id}`;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Return true when the string looks like a QQ Bot target ID.
|
|
277
|
-
*/
|
|
278
|
-
function looksLikeQQBotTarget(id) {
|
|
279
|
-
if (/^qqbot:(c2c|group|channel):/i.test(id)) return true;
|
|
280
|
-
if (/^(c2c|group|channel):/i.test(id)) return true;
|
|
281
|
-
if (/^[0-9a-fA-F]{32}$/.test(id)) return true;
|
|
282
|
-
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id);
|
|
283
|
-
}
|
|
284
|
-
//#endregion
|
|
285
|
-
export { getHomeDir as a, getQQBotMediaDir as c, isLocalPath as d, isWindows as f, checkSilkWasmAvailable as i, getQQBotMediaPath as l, resolveQQBotPayloadLocalFilePath as m, normalizeTarget as n, getQQBotDataDir as o, normalizePath as p, parseTarget as r, getQQBotDataPath as s, looksLikeQQBotTarget as t, getTempDir as u };
|