@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-alpha.1
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 +2 -15
- package/README.zh.md +3 -16
- package/dist/src/admin-resolver.d.ts +12 -6
- package/dist/src/admin-resolver.js +69 -34
- package/dist/src/api.d.ts +105 -1
- package/dist/src/api.js +185 -15
- package/dist/src/channel.js +13 -0
- package/dist/src/config.js +3 -10
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.js +450 -248
- package/dist/src/image-server.d.ts +27 -8
- package/dist/src/image-server.js +179 -71
- package/dist/src/inbound-attachments.d.ts +3 -1
- package/dist/src/inbound-attachments.js +28 -14
- package/dist/src/outbound-deliver.js +77 -148
- package/dist/src/outbound.d.ts +6 -4
- package/dist/src/outbound.js +266 -442
- package/dist/src/reply-dispatcher.js +4 -4
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/slash-commands.js +277 -32
- package/dist/src/startup-greeting.d.ts +5 -5
- package/dist/src/startup-greeting.js +32 -13
- package/dist/src/streaming.d.ts +250 -0
- package/dist/src/streaming.js +914 -0
- package/dist/src/tools/remind.js +11 -10
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +17 -1
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/audio-convert.d.ts +9 -0
- package/dist/src/utils/audio-convert.js +51 -0
- package/dist/src/utils/chunked-upload.d.ts +59 -0
- package/dist/src/utils/chunked-upload.js +289 -0
- package/dist/src/utils/file-utils.d.ts +7 -1
- package/dist/src/utils/file-utils.js +24 -2
- package/dist/src/utils/media-send.d.ts +147 -0
- package/dist/src/utils/media-send.js +434 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +5 -5
- package/node_modules/ws/lib/websocket.js +6 -6
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +10 -21
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/upgrade-via-npm.ps1 +161 -6
- package/scripts/upgrade-via-npm.sh +311 -104
- package/scripts/upgrade-via-source.sh +117 -0
- package/skills/qqbot-media/SKILL.md +9 -5
- package/skills/qqbot-remind/SKILL.md +3 -3
- package/src/admin-resolver.ts +76 -35
- package/src/api.ts +315 -12
- package/src/channel.ts +12 -0
- package/src/config.ts +3 -10
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +277 -67
- package/src/image-server.ts +213 -77
- package/src/inbound-attachments.ts +32 -15
- package/src/outbound-deliver.ts +77 -157
- package/src/outbound.ts +304 -451
- package/src/reply-dispatcher.ts +4 -4
- package/src/request-context.ts +39 -0
- package/src/slash-commands.ts +303 -33
- package/src/startup-greeting.ts +35 -13
- package/src/streaming.ts +1102 -0
- package/src/tools/remind.ts +15 -11
- package/src/types.ts +111 -0
- package/src/update-checker.ts +2 -7
- package/src/utils/audio-convert.ts +56 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +28 -2
- package/src/utils/media-send.ts +563 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/clawdbot.plugin.json +0 -16
- package/dist/src/user-messages.d.ts +0 -8
- package/dist/src/user-messages.js +0 -8
- package/moltbot.plugin.json +0 -16
- package/scripts/upgrade-via-alt-pkg.sh +0 -307
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
- package/src/gateway.log +0 -43
- package/src/openclaw-2026-03-21.log +0 -3729
- package/src/user-messages.ts +0 -7
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
|
|
12
12
|
|
|
13
|
-
### 🚀 Current Version: `v1.6.
|
|
13
|
+
### 🚀 Current Version: `v1.6.5`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bot.q.qq.com/wiki/)
|
|
@@ -183,24 +183,11 @@ Shows framework version, plugin version, and a direct link to the official repos
|
|
|
183
183
|
>
|
|
184
184
|
> **QQBot**: 📌 Current: v1.6.3 / ✅ New version v1.6.4 available / Click button below to confirm
|
|
185
185
|
|
|
186
|
-
Send in private chat to upgrade the plugin without server login. Supported usage:
|
|
187
|
-
|
|
188
|
-
| Command | Description |
|
|
189
|
-
|---------|-------------|
|
|
190
|
-
| `/bot-upgrade` | Check for updates, show confirmation button |
|
|
191
|
-
| `/bot-upgrade --latest` | Confirm upgrade to the latest version |
|
|
192
|
-
| `/bot-upgrade --version 1.6.4` | Upgrade to a specific version |
|
|
193
|
-
| `/bot-upgrade --force` | Force reinstall current version |
|
|
194
|
-
|
|
195
186
|
Credentials are automatically backed up before upgrade. Version existence is verified against npm before proceeding. Auto-recovery on failure.
|
|
196
187
|
|
|
197
|
-
|
|
198
|
-
<!-- TODO: add /bot-upgrade screenshot -->
|
|
199
|
-
=======
|
|
200
|
-
> **Platform Support:** Hot upgrade is currently supported on **Linux** and **macOS**. On Windows, `/bot-upgrade` will return a manual upgrade guide instead.
|
|
188
|
+
> ⚠️ Hot upgrade is currently not supported on Windows. Sending `/bot-upgrade` on Windows will return a manual upgrade guide instead.
|
|
201
189
|
|
|
202
190
|
<img width="360" src="docs/images/hot-update.jpg" alt="Hot Upgrade Demo" />
|
|
203
|
-
>>>>>>> Stashed changes
|
|
204
191
|
|
|
205
192
|
#### `/bot-logs` — Log Export
|
|
206
193
|
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
**让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
|
|
11
11
|
|
|
12
|
-
### 🚀 当前版本: `v1.6.
|
|
12
|
+
### 🚀 当前版本: `v1.6.5`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](https://bot.q.qq.com/wiki/)
|
|
@@ -178,24 +178,11 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
178
178
|
>
|
|
179
179
|
> **QQBot**:📌当前版本 v1.6.3 / ✅发现新版本 v1.6.4 / 点击下方按钮确认升级
|
|
180
180
|
|
|
181
|
-
在私聊中发送即可完成版本升级,全程无需登录服务器。支持的用法:
|
|
182
|
-
|
|
183
|
-
| 命令 | 说明 |
|
|
184
|
-
|------|------|
|
|
185
|
-
| `/bot-upgrade` | 检查是否有新版本,展示确认按钮 |
|
|
186
|
-
| `/bot-upgrade --latest` | 确认升级到最新版本 |
|
|
187
|
-
| `/bot-upgrade --version 1.6.4` | 升级到指定版本 |
|
|
188
|
-
| `/bot-upgrade --force` | 强制重新安装当前版本 |
|
|
189
|
-
|
|
190
181
|
升级流程自动备份凭证,升级前校验版本是否存在于 npm,升级失败自动恢复。
|
|
191
182
|
|
|
192
|
-
|
|
193
|
-
<!-- TODO: 补充 /bot-upgrade 截图 -->
|
|
194
|
-
=======
|
|
195
|
-
> **平台支持:** 热更新目前支持 **Linux** 和 **macOS**。Windows 暂不支持热更,发送 `/bot-upgrade` 后会返回手动升级指引。
|
|
183
|
+
> ⚠️ 热更新指令暂不支持 Windows 系统,在 Windows 上发送 `/bot-upgrade` 会返回手动升级指引。
|
|
196
184
|
|
|
197
|
-
<img width="360" src="docs/images/hot-update.jpg" alt="
|
|
198
|
-
>>>>>>> Stashed changes
|
|
185
|
+
<img width="360" src="docs/images/hot-update.jpg" alt="一键热更新演示" />
|
|
199
186
|
|
|
200
187
|
#### `/bot-logs` — 日志导出
|
|
201
188
|
|
|
@@ -13,15 +13,21 @@ export interface AdminResolverContext {
|
|
|
13
13
|
error: (msg: string) => void;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 读取 admin openid(按 accountId + appId 区分)
|
|
18
|
+
* 兼容策略:新路径优先 → fallback 旧路径 → 自动迁移
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadAdminOpenId(accountId: string, appId: string): string | undefined;
|
|
21
|
+
export declare function saveAdminOpenId(accountId: string, appId: string, openid: string): void;
|
|
22
|
+
export declare function loadUpgradeGreetingTargetOpenId(accountId: string, appId: string, log?: {
|
|
23
|
+
info: (msg: string) => void;
|
|
24
|
+
}): string | undefined;
|
|
19
25
|
export declare function clearUpgradeGreetingTargetOpenId(accountId: string, appId: string): void;
|
|
20
26
|
/**
|
|
21
27
|
* 解析管理员 openid:
|
|
22
|
-
* 1.
|
|
28
|
+
* 1. 优先读持久化文件(按 accountId + appId 区分)
|
|
23
29
|
* 2. fallback 取第一个私聊用户,并写入文件锁定
|
|
24
30
|
*/
|
|
25
|
-
export declare function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "log">): string | undefined;
|
|
26
|
-
/**
|
|
31
|
+
export declare function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "appId" | "log">): string | undefined;
|
|
32
|
+
/** 异步发送启动问候语(优先发给升级触发者,fallback 发给管理员) */
|
|
27
33
|
export declare function sendStartupGreetings(ctx: AdminResolverContext, trigger: "READY" | "RESUMED"): void;
|
|
@@ -11,49 +11,86 @@ import { listKnownUsers } from "./known-users.js";
|
|
|
11
11
|
import { getAccessToken, sendProactiveC2CMessage } from "./api.js";
|
|
12
12
|
import { getStartupGreetingPlan, markStartupGreetingSent, markStartupGreetingFailed } from "./startup-greeting.js";
|
|
13
13
|
// ---- 文件路径 ----
|
|
14
|
-
function
|
|
14
|
+
function safeName(id) {
|
|
15
|
+
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
/** 新版 admin 文件路径(按 accountId + appId 区分) */
|
|
18
|
+
function getAdminMarkerFile(accountId, appId) {
|
|
19
|
+
return path.join(getQQBotDataDir("data"), `admin-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
20
|
+
}
|
|
21
|
+
/** 旧版 admin 文件路径(仅按 accountId 区分,用于迁移兼容) */
|
|
22
|
+
function getLegacyAdminMarkerFile(accountId) {
|
|
15
23
|
return path.join(getQQBotDataDir("data"), `admin-${accountId}.json`);
|
|
16
24
|
}
|
|
17
25
|
function getUpgradeGreetingTargetFile(accountId, appId) {
|
|
18
|
-
|
|
19
|
-
const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
20
|
-
return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
26
|
+
return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
21
27
|
}
|
|
22
28
|
// ---- 管理员 openid 持久化 ----
|
|
23
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 读取 admin openid(按 accountId + appId 区分)
|
|
31
|
+
* 兼容策略:新路径优先 → fallback 旧路径 → 自动迁移
|
|
32
|
+
*/
|
|
33
|
+
export function loadAdminOpenId(accountId, appId) {
|
|
24
34
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
// 1. 先尝试新版路径
|
|
36
|
+
const newFile = getAdminMarkerFile(accountId, appId);
|
|
37
|
+
if (fs.existsSync(newFile)) {
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(newFile, "utf8"));
|
|
28
39
|
if (data.openid)
|
|
29
40
|
return data.openid;
|
|
30
41
|
}
|
|
42
|
+
// 2. fallback 旧版路径(仅按 accountId)
|
|
43
|
+
const legacyFile = getLegacyAdminMarkerFile(accountId);
|
|
44
|
+
if (fs.existsSync(legacyFile)) {
|
|
45
|
+
const data = JSON.parse(fs.readFileSync(legacyFile, "utf8"));
|
|
46
|
+
if (data.openid) {
|
|
47
|
+
// 自动迁移:写到新路径,删除旧文件
|
|
48
|
+
saveAdminOpenId(accountId, appId, data.openid);
|
|
49
|
+
try {
|
|
50
|
+
fs.unlinkSync(legacyFile);
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ }
|
|
53
|
+
return data.openid;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
}
|
|
32
57
|
catch { /* 文件损坏视为无 */ }
|
|
33
58
|
return undefined;
|
|
34
59
|
}
|
|
35
|
-
export function saveAdminOpenId(accountId, openid) {
|
|
60
|
+
export function saveAdminOpenId(accountId, appId, openid) {
|
|
36
61
|
try {
|
|
37
|
-
fs.writeFileSync(getAdminMarkerFile(accountId), JSON.stringify({ openid, savedAt: new Date().toISOString() }));
|
|
62
|
+
fs.writeFileSync(getAdminMarkerFile(accountId, appId), JSON.stringify({ accountId, appId, openid, savedAt: new Date().toISOString() }));
|
|
38
63
|
}
|
|
39
64
|
catch { /* ignore */ }
|
|
40
65
|
}
|
|
41
66
|
// ---- 升级问候目标 ----
|
|
42
|
-
export function loadUpgradeGreetingTargetOpenId(accountId, appId) {
|
|
67
|
+
export function loadUpgradeGreetingTargetOpenId(accountId, appId, log) {
|
|
43
68
|
try {
|
|
44
69
|
const file = getUpgradeGreetingTargetFile(accountId, appId);
|
|
45
70
|
if (fs.existsSync(file)) {
|
|
46
71
|
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
-
if (!data.openid)
|
|
72
|
+
if (!data.openid) {
|
|
73
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file found but openid is empty`);
|
|
48
74
|
return undefined;
|
|
49
|
-
|
|
75
|
+
}
|
|
76
|
+
if (data.appId && data.appId !== appId) {
|
|
77
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target appId mismatch: file=${data.appId}, current=${appId}`);
|
|
50
78
|
return undefined;
|
|
51
|
-
|
|
79
|
+
}
|
|
80
|
+
if (data.accountId && data.accountId !== accountId) {
|
|
81
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target accountId mismatch: file=${data.accountId}, current=${accountId}`);
|
|
52
82
|
return undefined;
|
|
83
|
+
}
|
|
84
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target loaded: openid=${data.openid}`);
|
|
53
85
|
return data.openid;
|
|
54
86
|
}
|
|
87
|
+
else {
|
|
88
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file not found: ${file}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
log?.info(`[qqbot:${accountId}] upgrade-greeting-target file read error: ${err}`);
|
|
55
93
|
}
|
|
56
|
-
catch { /* 文件损坏视为无 */ }
|
|
57
94
|
return undefined;
|
|
58
95
|
}
|
|
59
96
|
export function clearUpgradeGreetingTargetOpenId(accountId, appId) {
|
|
@@ -68,54 +105,52 @@ export function clearUpgradeGreetingTargetOpenId(accountId, appId) {
|
|
|
68
105
|
// ---- 解析管理员 ----
|
|
69
106
|
/**
|
|
70
107
|
* 解析管理员 openid:
|
|
71
|
-
* 1.
|
|
108
|
+
* 1. 优先读持久化文件(按 accountId + appId 区分)
|
|
72
109
|
* 2. fallback 取第一个私聊用户,并写入文件锁定
|
|
73
110
|
*/
|
|
74
111
|
export function resolveAdminOpenId(ctx) {
|
|
75
|
-
const saved = loadAdminOpenId(ctx.accountId);
|
|
112
|
+
const saved = loadAdminOpenId(ctx.accountId, ctx.appId);
|
|
76
113
|
if (saved)
|
|
77
114
|
return saved;
|
|
78
115
|
const first = listKnownUsers({ accountId: ctx.accountId, type: "c2c", sortBy: "firstSeenAt", sortOrder: "asc", limit: 1 })[0]?.openid;
|
|
79
116
|
if (first) {
|
|
80
|
-
saveAdminOpenId(ctx.accountId, first);
|
|
117
|
+
saveAdminOpenId(ctx.accountId, ctx.appId, first);
|
|
81
118
|
ctx.log?.info(`[qqbot:${ctx.accountId}] Auto-detected admin openid: ${first} (persisted)`);
|
|
82
119
|
}
|
|
83
120
|
return first;
|
|
84
121
|
}
|
|
85
122
|
// ---- 启动问候语 ----
|
|
86
|
-
/**
|
|
123
|
+
/** 异步发送启动问候语(优先发给升级触发者,fallback 发给管理员) */
|
|
87
124
|
export function sendStartupGreetings(ctx, trigger) {
|
|
88
125
|
(async () => {
|
|
89
|
-
const plan = getStartupGreetingPlan();
|
|
126
|
+
const plan = getStartupGreetingPlan(ctx.accountId, ctx.appId);
|
|
90
127
|
if (!plan.shouldSend || !plan.greeting) {
|
|
91
128
|
ctx.log?.info(`[qqbot:${ctx.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
|
|
92
129
|
return;
|
|
93
130
|
}
|
|
94
|
-
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
131
|
+
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId, ctx.log);
|
|
132
|
+
// 没有 upgrade-greeting-target 文件 → 不是通过 /bot-upgrade 触发的升级
|
|
133
|
+
// (console 手动重启、脚本升级等场景),静默更新 marker 不发消息
|
|
134
|
+
if (!upgradeTargetOpenId) {
|
|
135
|
+
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
136
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Version changed but no upgrade-greeting-target, silently updating marker (trigger=${trigger})`);
|
|
99
137
|
return;
|
|
100
138
|
}
|
|
101
139
|
try {
|
|
102
|
-
|
|
103
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to ${receiverType} (trigger=${trigger}): "${plan.greeting}"`);
|
|
140
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to upgrade-requester (trigger=${trigger}): "${plan.greeting}"`);
|
|
104
141
|
const token = await getAccessToken(ctx.appId, ctx.clientSecret);
|
|
105
142
|
const GREETING_TIMEOUT_MS = 10_000;
|
|
106
143
|
await Promise.race([
|
|
107
|
-
sendProactiveC2CMessage(token,
|
|
144
|
+
sendProactiveC2CMessage(token, upgradeTargetOpenId, plan.greeting),
|
|
108
145
|
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
109
146
|
]);
|
|
110
|
-
markStartupGreetingSent(plan.version);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to ${receiverType}: ${targetOpenId}`);
|
|
147
|
+
markStartupGreetingSent(ctx.accountId, ctx.appId, plan.version);
|
|
148
|
+
clearUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
|
|
149
|
+
ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to upgrade-requester: ${upgradeTargetOpenId}`);
|
|
115
150
|
}
|
|
116
151
|
catch (err) {
|
|
117
152
|
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
-
markStartupGreetingFailed(plan.version, message);
|
|
153
|
+
markStartupGreetingFailed(ctx.accountId, ctx.appId, plan.version, message);
|
|
119
154
|
ctx.log?.error(`[qqbot:${ctx.accountId}] Failed to send startup greeting: ${message}`);
|
|
120
155
|
}
|
|
121
156
|
})();
|
package/dist/src/api.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* QQ Bot API 鉴权和请求封装
|
|
3
3
|
* [修复版] 已重构为支持多实例并发,消除全局变量冲突
|
|
4
4
|
*/
|
|
5
|
+
/** API 请求错误,携带 HTTP status code */
|
|
6
|
+
export declare class ApiError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
constructor(message: string, status: number, path: string);
|
|
10
|
+
}
|
|
5
11
|
export declare const PLUGIN_USER_AGENT: string;
|
|
6
12
|
/** 出站消息元信息(结构化存储,不做预格式化) */
|
|
7
13
|
export interface OutboundMeta {
|
|
@@ -25,7 +31,6 @@ type OnMessageSentCallback = (refIdx: string, meta: OutboundMeta) => void;
|
|
|
25
31
|
export declare function onMessageSent(callback: OnMessageSentCallback): void;
|
|
26
32
|
/**
|
|
27
33
|
* 初始化 API 配置
|
|
28
|
-
* @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
|
|
29
34
|
*/
|
|
30
35
|
export declare function initApiConfig(options: {
|
|
31
36
|
markdownSupport?: boolean;
|
|
@@ -108,6 +113,90 @@ export interface UploadMediaResponse {
|
|
|
108
113
|
ttl: number;
|
|
109
114
|
id?: string;
|
|
110
115
|
}
|
|
116
|
+
/** 分片信息 */
|
|
117
|
+
export interface UploadPart {
|
|
118
|
+
/** 分片索引(从 1 开始) */
|
|
119
|
+
index: number;
|
|
120
|
+
/** 预签名上传链接 */
|
|
121
|
+
presigned_url: string;
|
|
122
|
+
}
|
|
123
|
+
/** 申请上传响应 */
|
|
124
|
+
export interface UploadPrepareResponse {
|
|
125
|
+
/** 上传任务 ID */
|
|
126
|
+
upload_id: string;
|
|
127
|
+
/** 分块大小(字节) */
|
|
128
|
+
block_size: number;
|
|
129
|
+
/** 分片列表(含预签名链接) */
|
|
130
|
+
parts: UploadPart[];
|
|
131
|
+
}
|
|
132
|
+
/** 完成文件上传响应(与 UploadMediaResponse 一致) */
|
|
133
|
+
export interface MediaUploadResponse {
|
|
134
|
+
/** 文件 UUID */
|
|
135
|
+
file_uuid: string;
|
|
136
|
+
/** 文件信息(用于发送消息),是 InnerUploadRsp 的序列化 */
|
|
137
|
+
file_info: string;
|
|
138
|
+
/** 文件信息过期时长(秒) */
|
|
139
|
+
ttl: number;
|
|
140
|
+
}
|
|
141
|
+
/** 申请上传时的文件哈希信息 */
|
|
142
|
+
export interface UploadPrepareHashes {
|
|
143
|
+
/** 整个文件的 MD5(十六进制) */
|
|
144
|
+
md5: string;
|
|
145
|
+
/** 整个文件的 SHA1(十六进制) */
|
|
146
|
+
sha1: string;
|
|
147
|
+
/** 文件前 10002432 Bytes 的 MD5(十六进制);文件不足该大小时为整文件 MD5 */
|
|
148
|
+
md5_10m: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 申请上传(C2C)
|
|
152
|
+
* POST /v2/users/{user_id}/upload_prepare
|
|
153
|
+
*
|
|
154
|
+
* @param accessToken - 访问令牌
|
|
155
|
+
* @param userId - 用户 openid
|
|
156
|
+
* @param fileType - 业务类型(1=图片, 2=视频, 3=语音, 4=文件)
|
|
157
|
+
* @param fileName - 文件名
|
|
158
|
+
* @param fileSize - 文件大小(字节)
|
|
159
|
+
* @param hashes - 文件哈希信息(md5, sha1, md5_10m)
|
|
160
|
+
* @returns 上传任务 ID、分块大小、分片预签名链接列表
|
|
161
|
+
*/
|
|
162
|
+
export declare function c2cUploadPrepare(accessToken: string, userId: string, fileType: MediaFileType, fileName: string, fileSize: number, hashes: UploadPrepareHashes): Promise<UploadPrepareResponse>;
|
|
163
|
+
/**
|
|
164
|
+
* 完成分片上传(C2C)
|
|
165
|
+
* POST /v2/users/{user_id}/upload_part_finish
|
|
166
|
+
*
|
|
167
|
+
* @param accessToken - 访问令牌
|
|
168
|
+
* @param userId - 用户 openid
|
|
169
|
+
* @param uploadId - 上传任务 ID
|
|
170
|
+
* @param partIndex - 分片索引(从 1 开始)
|
|
171
|
+
* @param blockSize - 分块大小(字节)
|
|
172
|
+
* @param md5 - 分片数据的 MD5(十六进制)
|
|
173
|
+
*/
|
|
174
|
+
export declare function c2cUploadPartFinish(accessToken: string, userId: string, uploadId: string, partIndex: number, blockSize: number, md5: string): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* 完成文件上传(C2C)
|
|
177
|
+
* POST /v2/users/{user_id}/files
|
|
178
|
+
*
|
|
179
|
+
* @param accessToken - 访问令牌
|
|
180
|
+
* @param userId - 用户 openid
|
|
181
|
+
* @param uploadId - 上传任务 ID
|
|
182
|
+
* @returns 文件信息(file_uuid, file_info, ttl)
|
|
183
|
+
*/
|
|
184
|
+
export declare function c2cCompleteUpload(accessToken: string, userId: string, uploadId: string): Promise<MediaUploadResponse>;
|
|
185
|
+
/**
|
|
186
|
+
* 申请上传(Group)
|
|
187
|
+
* POST /v2/groups/{group_id}/upload_prepare
|
|
188
|
+
*/
|
|
189
|
+
export declare function groupUploadPrepare(accessToken: string, groupId: string, fileType: MediaFileType, fileName: string, fileSize: number, hashes: UploadPrepareHashes): Promise<UploadPrepareResponse>;
|
|
190
|
+
/**
|
|
191
|
+
* 完成分片上传(Group)
|
|
192
|
+
* POST /v2/groups/{group_id}/upload_part_finish
|
|
193
|
+
*/
|
|
194
|
+
export declare function groupUploadPartFinish(accessToken: string, groupId: string, uploadId: string, partIndex: number, blockSize: number, md5: string): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* 完成文件上传(Group)
|
|
197
|
+
* POST /v2/groups/{group_id}/files
|
|
198
|
+
*/
|
|
199
|
+
export declare function groupCompleteUpload(accessToken: string, groupId: string, uploadId: string): Promise<MediaUploadResponse>;
|
|
111
200
|
export declare function uploadC2CMedia(accessToken: string, openid: string, fileType: MediaFileType, url?: string, fileData?: string, srvSendMsg?: boolean, fileName?: string): Promise<UploadMediaResponse>;
|
|
112
201
|
export declare function uploadGroupMedia(accessToken: string, groupOpenid: string, fileType: MediaFileType, url?: string, fileData?: string, srvSendMsg?: boolean, fileName?: string): Promise<UploadMediaResponse>;
|
|
113
202
|
export declare function sendC2CMediaMessage(accessToken: string, openid: string, fileInfo: string, msgId?: string, content?: string, meta?: OutboundMeta): Promise<MessageResponse>;
|
|
@@ -153,4 +242,19 @@ export declare function startBackgroundTokenRefresh(appId: string, clientSecret:
|
|
|
153
242
|
*/
|
|
154
243
|
export declare function stopBackgroundTokenRefresh(appId?: string): void;
|
|
155
244
|
export declare function isBackgroundTokenRefreshRunning(appId?: string): boolean;
|
|
245
|
+
import type { StreamMessageRequest, StreamMessageResponse } from "./types.js";
|
|
246
|
+
/**
|
|
247
|
+
* 发送流式消息(C2C 私聊)
|
|
248
|
+
*
|
|
249
|
+
* 流式协议:
|
|
250
|
+
* - 首次调用时不传 stream_msg_id,由平台返回
|
|
251
|
+
* - 后续分片携带 stream_msg_id 和递增 msg_seq
|
|
252
|
+
* - input_state="1" 表示生成中,"10" 表示生成结束(终结状态)
|
|
253
|
+
*
|
|
254
|
+
* @param accessToken - access_token
|
|
255
|
+
* @param openid - 用户 openid
|
|
256
|
+
* @param req - 流式消息请求体
|
|
257
|
+
* @returns 流式消息响应
|
|
258
|
+
*/
|
|
259
|
+
export declare function sendC2CStreamMessage(accessToken: string, openid: string, req: StreamMessageRequest): Promise<StreamMessageResponse>;
|
|
156
260
|
export {};
|
package/dist/src/api.js
CHANGED
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
* QQ Bot API 鉴权和请求封装
|
|
3
3
|
* [修复版] 已重构为支持多实例并发,消除全局变量冲突
|
|
4
4
|
*/
|
|
5
|
-
import { createRequire } from "node:module";
|
|
6
5
|
import os from "node:os";
|
|
7
6
|
import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
|
|
8
7
|
import { sanitizeFileName } from "./utils/platform.js";
|
|
8
|
+
// ============ 自定义错误 ============
|
|
9
|
+
/** API 请求错误,携带 HTTP status code */
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
path;
|
|
13
|
+
constructor(message, status, path) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.path = path;
|
|
17
|
+
this.name = "ApiError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
9
20
|
const API_BASE = "https://api.sgroup.qq.com";
|
|
10
21
|
const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
|
|
11
22
|
// ============ Plugin User-Agent ============
|
|
12
23
|
// 格式: QQBotPlugin/{version} (Node/{nodeVersion}; {os})
|
|
13
24
|
// 示例: QQBotPlugin/1.6.0 (Node/22.14.0; darwin)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
_pluginVersion = _require("../package.json").version ?? "unknown";
|
|
18
|
-
}
|
|
19
|
-
catch { /* fallback */ }
|
|
25
|
+
import { getPackageVersion } from "./utils/pkg-version.js";
|
|
26
|
+
const _pluginVersion = getPackageVersion(import.meta.url);
|
|
20
27
|
export const PLUGIN_USER_AGENT = `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()})`;
|
|
21
28
|
// 运行时配置
|
|
22
29
|
let currentMarkdownSupport = false;
|
|
@@ -31,7 +38,6 @@ export function onMessageSent(callback) {
|
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* 初始化 API 配置
|
|
34
|
-
* @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
|
|
35
41
|
*/
|
|
36
42
|
export function initApiConfig(options) {
|
|
37
43
|
currentMarkdownSupport = options.markdownSupport === true;
|
|
@@ -234,21 +240,48 @@ export async function apiRequest(accessToken, method, path, body, timeoutMs) {
|
|
|
234
240
|
});
|
|
235
241
|
const traceId = res.headers.get("x-tps-trace-id") ?? "";
|
|
236
242
|
console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`);
|
|
237
|
-
let data;
|
|
238
243
|
let rawBody;
|
|
239
244
|
try {
|
|
240
245
|
rawBody = await res.text();
|
|
241
|
-
console.log(`[qqbot-api] <<< Body:`, rawBody);
|
|
242
|
-
data = JSON.parse(rawBody);
|
|
243
246
|
}
|
|
244
247
|
catch (err) {
|
|
245
|
-
throw new Error(
|
|
248
|
+
throw new Error(`读取响应失败[${path}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
249
|
}
|
|
250
|
+
console.log(`[qqbot-api] <<< Body:`, rawBody);
|
|
251
|
+
// 检测非 JSON 响应(HTML 网关错误页 / CDN 限流页等)
|
|
252
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
253
|
+
const isHtmlResponse = contentType.includes("text/html") || rawBody.trimStart().startsWith("<");
|
|
247
254
|
if (!res.ok) {
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
if (isHtmlResponse) {
|
|
256
|
+
// HTML 响应 = 网关/限流层返回的错误页,给出友好提示
|
|
257
|
+
const statusHint = res.status === 502 || res.status === 503 || res.status === 504
|
|
258
|
+
? "调用发生异常,请稍候重试"
|
|
259
|
+
: res.status === 429
|
|
260
|
+
? "请求过于频繁,已被限流"
|
|
261
|
+
: `开放平台返回 HTTP ${res.status}`;
|
|
262
|
+
throw new ApiError(`${statusHint}(${path}),请稍后重试`, res.status, path);
|
|
263
|
+
}
|
|
264
|
+
// JSON 错误响应
|
|
265
|
+
try {
|
|
266
|
+
const error = JSON.parse(rawBody);
|
|
267
|
+
throw new ApiError(`API Error [${path}]: ${error.message ?? rawBody}`, res.status, path);
|
|
268
|
+
}
|
|
269
|
+
catch (parseErr) {
|
|
270
|
+
if (parseErr instanceof ApiError)
|
|
271
|
+
throw parseErr;
|
|
272
|
+
throw new ApiError(`API Error [${path}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`, res.status, path);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 成功响应但不是 JSON(极端异常情况)
|
|
276
|
+
if (isHtmlResponse) {
|
|
277
|
+
throw new Error(`QQ 服务端返回了非 JSON 响应(${path}),可能是临时故障,请稍后重试`);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(rawBody);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
throw new Error(`开放平台响应格式异常(${path}),请稍后重试`);
|
|
250
284
|
}
|
|
251
|
-
return data;
|
|
252
285
|
}
|
|
253
286
|
// ============ 上传重试(指数退避) ============
|
|
254
287
|
const UPLOAD_MAX_RETRIES = 2;
|
|
@@ -275,6 +308,51 @@ async function apiRequestWithRetry(accessToken, method, path, body, maxRetries =
|
|
|
275
308
|
}
|
|
276
309
|
throw lastError;
|
|
277
310
|
}
|
|
311
|
+
// ============ 完成上传重试(无条件,任何错误都重试) ============
|
|
312
|
+
const COMPLETE_UPLOAD_MAX_RETRIES = 2;
|
|
313
|
+
const COMPLETE_UPLOAD_BASE_DELAY_MS = 2000;
|
|
314
|
+
/**
|
|
315
|
+
* 完成上传专用重试:无条件重试所有错误(包括 4xx、5xx、网络错误、超时等)
|
|
316
|
+
* 分片上传完成接口的失败往往是平台侧异步处理未就绪,重试通常能成功
|
|
317
|
+
*/
|
|
318
|
+
async function completeUploadWithRetry(accessToken, method, path, body) {
|
|
319
|
+
let lastError = null;
|
|
320
|
+
for (let attempt = 0; attempt <= COMPLETE_UPLOAD_MAX_RETRIES; attempt++) {
|
|
321
|
+
try {
|
|
322
|
+
return await apiRequest(accessToken, method, path, body);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
326
|
+
if (attempt < COMPLETE_UPLOAD_MAX_RETRIES) {
|
|
327
|
+
const delay = COMPLETE_UPLOAD_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
328
|
+
console.warn(`[qqbot-api] CompleteUpload attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 200)}`);
|
|
329
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw lastError;
|
|
334
|
+
}
|
|
335
|
+
// ============ 分片完成重试(无条件,与 completeUpload 策略一致) ============
|
|
336
|
+
const PART_FINISH_MAX_RETRIES = 2;
|
|
337
|
+
const PART_FINISH_BASE_DELAY_MS = 1000;
|
|
338
|
+
async function partFinishWithRetry(accessToken, method, path, body) {
|
|
339
|
+
let lastError = null;
|
|
340
|
+
for (let attempt = 0; attempt <= PART_FINISH_MAX_RETRIES; attempt++) {
|
|
341
|
+
try {
|
|
342
|
+
await apiRequest(accessToken, method, path, body);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
347
|
+
if (attempt < PART_FINISH_MAX_RETRIES) {
|
|
348
|
+
const delay = PART_FINISH_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
349
|
+
console.warn(`[qqbot-api] PartFinish attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 200)}`);
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw lastError;
|
|
355
|
+
}
|
|
278
356
|
export async function getGatewayUrl(accessToken) {
|
|
279
357
|
const data = await apiRequest(accessToken, "GET", "/gateway");
|
|
280
358
|
return data.url;
|
|
@@ -383,6 +461,68 @@ export var MediaFileType;
|
|
|
383
461
|
MediaFileType[MediaFileType["VOICE"] = 3] = "VOICE";
|
|
384
462
|
MediaFileType[MediaFileType["FILE"] = 4] = "FILE";
|
|
385
463
|
})(MediaFileType || (MediaFileType = {}));
|
|
464
|
+
/**
|
|
465
|
+
* 申请上传(C2C)
|
|
466
|
+
* POST /v2/users/{user_id}/upload_prepare
|
|
467
|
+
*
|
|
468
|
+
* @param accessToken - 访问令牌
|
|
469
|
+
* @param userId - 用户 openid
|
|
470
|
+
* @param fileType - 业务类型(1=图片, 2=视频, 3=语音, 4=文件)
|
|
471
|
+
* @param fileName - 文件名
|
|
472
|
+
* @param fileSize - 文件大小(字节)
|
|
473
|
+
* @param hashes - 文件哈希信息(md5, sha1, md5_10m)
|
|
474
|
+
* @returns 上传任务 ID、分块大小、分片预签名链接列表
|
|
475
|
+
*/
|
|
476
|
+
export async function c2cUploadPrepare(accessToken, userId, fileType, fileName, fileSize, hashes) {
|
|
477
|
+
return apiRequest(accessToken, "POST", `/v2/users/${userId}/upload_prepare`, { file_type: fileType, file_name: fileName, file_size: fileSize, md5: hashes.md5, sha1: hashes.sha1, md5_10m: hashes.md5_10m });
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 完成分片上传(C2C)
|
|
481
|
+
* POST /v2/users/{user_id}/upload_part_finish
|
|
482
|
+
*
|
|
483
|
+
* @param accessToken - 访问令牌
|
|
484
|
+
* @param userId - 用户 openid
|
|
485
|
+
* @param uploadId - 上传任务 ID
|
|
486
|
+
* @param partIndex - 分片索引(从 1 开始)
|
|
487
|
+
* @param blockSize - 分块大小(字节)
|
|
488
|
+
* @param md5 - 分片数据的 MD5(十六进制)
|
|
489
|
+
*/
|
|
490
|
+
export async function c2cUploadPartFinish(accessToken, userId, uploadId, partIndex, blockSize, md5) {
|
|
491
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/users/${userId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* 完成文件上传(C2C)
|
|
495
|
+
* POST /v2/users/{user_id}/files
|
|
496
|
+
*
|
|
497
|
+
* @param accessToken - 访问令牌
|
|
498
|
+
* @param userId - 用户 openid
|
|
499
|
+
* @param uploadId - 上传任务 ID
|
|
500
|
+
* @returns 文件信息(file_uuid, file_info, ttl)
|
|
501
|
+
*/
|
|
502
|
+
export async function c2cCompleteUpload(accessToken, userId, uploadId) {
|
|
503
|
+
return completeUploadWithRetry(accessToken, "POST", `/v2/users/${userId}/files`, { upload_id: uploadId });
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 申请上传(Group)
|
|
507
|
+
* POST /v2/groups/{group_id}/upload_prepare
|
|
508
|
+
*/
|
|
509
|
+
export async function groupUploadPrepare(accessToken, groupId, fileType, fileName, fileSize, hashes) {
|
|
510
|
+
return apiRequest(accessToken, "POST", `/v2/groups/${groupId}/upload_prepare`, { file_type: fileType, file_name: fileName, file_size: fileSize, md5: hashes.md5, sha1: hashes.sha1, md5_10m: hashes.md5_10m });
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* 完成分片上传(Group)
|
|
514
|
+
* POST /v2/groups/{group_id}/upload_part_finish
|
|
515
|
+
*/
|
|
516
|
+
export async function groupUploadPartFinish(accessToken, groupId, uploadId, partIndex, blockSize, md5) {
|
|
517
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/groups/${groupId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 完成文件上传(Group)
|
|
521
|
+
* POST /v2/groups/{group_id}/files
|
|
522
|
+
*/
|
|
523
|
+
export async function groupCompleteUpload(accessToken, groupId, uploadId) {
|
|
524
|
+
return completeUploadWithRetry(accessToken, "POST", `/v2/groups/${groupId}/files`, { upload_id: uploadId });
|
|
525
|
+
}
|
|
386
526
|
export async function uploadC2CMedia(accessToken, openid, fileType, url, fileData, srvSendMsg = false, fileName) {
|
|
387
527
|
if (!url && !fileData)
|
|
388
528
|
throw new Error("uploadC2CMedia: url or fileData is required");
|
|
@@ -597,3 +737,33 @@ async function sleep(ms, signal) {
|
|
|
597
737
|
}
|
|
598
738
|
});
|
|
599
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* 发送流式消息(C2C 私聊)
|
|
742
|
+
*
|
|
743
|
+
* 流式协议:
|
|
744
|
+
* - 首次调用时不传 stream_msg_id,由平台返回
|
|
745
|
+
* - 后续分片携带 stream_msg_id 和递增 msg_seq
|
|
746
|
+
* - input_state="1" 表示生成中,"10" 表示生成结束(终结状态)
|
|
747
|
+
*
|
|
748
|
+
* @param accessToken - access_token
|
|
749
|
+
* @param openid - 用户 openid
|
|
750
|
+
* @param req - 流式消息请求体
|
|
751
|
+
* @returns 流式消息响应
|
|
752
|
+
*/
|
|
753
|
+
export async function sendC2CStreamMessage(accessToken, openid, req) {
|
|
754
|
+
const path = `/v2/users/${openid}/stream_messages`;
|
|
755
|
+
const body = {
|
|
756
|
+
input_mode: req.input_mode,
|
|
757
|
+
input_state: req.input_state,
|
|
758
|
+
content_type: req.content_type,
|
|
759
|
+
content_raw: req.content_raw,
|
|
760
|
+
event_id: req.event_id,
|
|
761
|
+
msg_id: req.msg_id,
|
|
762
|
+
msg_seq: req.msg_seq,
|
|
763
|
+
index: req.index,
|
|
764
|
+
};
|
|
765
|
+
if (req.stream_msg_id) {
|
|
766
|
+
body.stream_msg_id = req.stream_msg_id;
|
|
767
|
+
}
|
|
768
|
+
return apiRequest(accessToken, "POST", path, body);
|
|
769
|
+
}
|