@zaofan/dsh-qqbot 1.1.7 → 1.3.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 +15 -5
- package/README_EN.md +3 -1
- package/client/qqbot-settings.js +132 -26
- package/dist/api/group-admin.d.ts +13 -3
- package/dist/api/group-admin.d.ts.map +1 -1
- package/dist/api/group-admin.js +49 -5
- package/dist/api/group-admin.js.map +1 -1
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +544 -29
- package/dist/channel-tools.js.map +1 -1
- package/dist/commands/answer.d.ts +29 -0
- package/dist/commands/answer.d.ts.map +1 -0
- package/dist/commands/answer.js +29 -0
- package/dist/commands/answer.js.map +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +5 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/misc.d.ts.map +1 -1
- package/dist/commands/misc.js +15 -1
- package/dist/commands/misc.js.map +1 -1
- package/dist/commands/outmode.d.ts.map +1 -1
- package/dist/commands/outmode.js +3 -2
- package/dist/commands/outmode.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/features/broadcast.d.ts.map +1 -1
- package/dist/features/broadcast.js +3 -1
- package/dist/features/broadcast.js.map +1 -1
- package/dist/features/group-hub.d.ts +3 -1
- package/dist/features/group-hub.d.ts.map +1 -1
- package/dist/features/group-hub.js +5 -3
- package/dist/features/group-hub.js.map +1 -1
- package/dist/features/group-join-request.d.ts +2 -2
- package/dist/features/group-join-request.d.ts.map +1 -1
- package/dist/features/group-join-request.js +25 -10
- package/dist/features/group-join-request.js.map +1 -1
- package/dist/features/outbound-mode-switch.d.ts +8 -2
- package/dist/features/outbound-mode-switch.d.ts.map +1 -1
- package/dist/features/outbound-mode-switch.js +2 -2
- package/dist/features/outbound-mode-switch.js.map +1 -1
- package/dist/features/poll-join-requests.js +2 -2
- package/dist/features/poll-join-requests.js.map +1 -1
- package/dist/features/qq-user-questions.d.ts +127 -4
- package/dist/features/qq-user-questions.d.ts.map +1 -1
- package/dist/features/qq-user-questions.js +314 -74
- package/dist/features/qq-user-questions.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +17 -0
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/gateway/data-root.d.ts +13 -4
- package/dist/gateway/data-root.d.ts.map +1 -1
- package/dist/gateway/data-root.js +22 -12
- package/dist/gateway/data-root.js.map +1 -1
- package/dist/gateway/debounce.d.ts.map +1 -1
- package/dist/gateway/debounce.js +5 -1
- package/dist/gateway/debounce.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/media-history.d.ts.map +1 -1
- package/dist/middleware/media-history.js +3 -0
- package/dist/middleware/media-history.js.map +1 -1
- package/dist/transport/inbound.js +4 -1
- package/dist/transport/inbound.js.map +1 -1
- package/dist/transport/outbound.d.ts.map +1 -1
- package/dist/transport/outbound.js +60 -5
- package/dist/transport/outbound.js.map +1 -1
- package/package.json +1 -1
- package/settings-host.js +80 -8
package/README.md
CHANGED
|
@@ -61,14 +61,16 @@
|
|
|
61
61
|
|
|
62
62
|
### 给AI的话
|
|
63
63
|
- QQ 会话内可直接调用的标准工具:发图/撤图/查库/打标/查未整理/定时(`send_media`/`recall_message`/`list_stickers`/`sticker_tag`/`sticker_untagged`/`schedule_timer`/`schedule_cancel`…),会话按账号精确路由
|
|
64
|
-
- **群管理工具**(`group_join_requests`/`group_approve_join`/`group_mute_state`/`group_mute_member`…):入群审批与禁言,需机器人为该群管理员;对话内管当前群,web/非群会话用配置的 `manageGroup
|
|
64
|
+
- **群管理工具**(`group_join_requests`/`group_approve_join`/`group_join_auto`/`group_mute_state`/`group_mute_member`…):入群审批与禁言,需机器人为该群管理员;对话内管当前群,web/非群会话用配置的 `manageGroup`。**入群审批默认听主人的,不自动批**——先 `group_join_requests` 查看,把申请人+验证信息汇报给主人,等主人明确说"通过/拒绝"再 `group_approve_join`;**主人明确要求**"按关键词自动批"时才用 `group_join_auto`(命中关键词放行 / 未命中拒绝,`dry_run` 可预览)
|
|
65
65
|
- **纯文本也能发图撤消息**:让 AI 在回复里写 `[MEDIA:image|图片路径或网址]` 就自动变成真图发出去(`voice`/`video`/`file` 同理);写 `[RECALL]` 撤回自己刚发的那条
|
|
66
66
|
- **跨会话通信**:`session_list` 列出全部会话(含潜在群);`session_wake(session_id 或 scope+peer_id, text, send_qq?, media?)` 向指定会话/群发消息并唤醒对方 LLM,可同时走 QQBot 通道发给人看,`media` 支持跨群发图
|
|
67
|
+
- **省 token 设计(v1.3.0)**:群历史行默认 `[昵称] 内容`(只有被 @ 的那条带 openid),`session_list` 默认"群名+尾号"(`full=true` 才给完整),`group_join_requests`/`list_stickers` 等列表默认 3~5 条并可翻页 —— 长聊天的上下文开销明显更小
|
|
67
68
|
- **入群申请的两种形态**:
|
|
68
69
|
- 会话流里出现【入群申请】= **消息注记**(静默记录、未唤醒你)——不用立刻行动,等主人下一条消息时自然接应
|
|
69
70
|
- 收到【审批轮询】= **系统唤醒**——主动 `group_join_requests` 查看新申请,把申请人+验证信息汇报给主人,等主人拍板再批/拒
|
|
70
71
|
- 消息里带 `@bot` = 有人 @ 本机器人(`@bot` 就是自己)
|
|
71
72
|
- 会话归属、工作区挂载等宿主问题已按官方机制修好(移植上游 PR #21,幂等、全 fail-soft)
|
|
73
|
+
- **官方接口文档**(写扩展、查事件字段/错误码、确认按钮与卡片字段时先看这里):QQ 机器人 api-v2 文档 <https://bot.q.qq.com/wiki/develop/api-v2/>
|
|
72
74
|
- **你可以自己写 markdown、自己写工具、自己写斜杠命令**(不用等人给你做功能):
|
|
73
75
|
- 卡片正文就是**你直接写的 markdown**(`#` 标题、加粗、``、代码块)——**本插件没有模板引擎,模板就是你**;要"带按钮的卡片"则走 botplay 事件或 dock 卡片编辑器(按钮回调须由 host 注册)。
|
|
74
76
|
- 工具/命令写在**账号数据目录**的 `.qqbot-extensions/{tools,commands}/`,**不在插件包内** → **升级/重装插件(换 node_modules)不会覆盖你的扩展**,扩展原样保留。
|
|
@@ -231,6 +233,8 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
231
233
|
|
|
232
234
|
**入群审批怎么用**: 事件到达 → bot 在群里发一条提醒(含申请人昵称/验证语)→ 你在对话里说"通过/拒绝"(AI 调 `group_approve_join`)→ 官方落库审批。也可以在设置面板「⑥ QQ 群管理 → 入群审批」页看待审批清单手动批。
|
|
233
235
|
|
|
236
|
+
**按关键词自动审批(v1.3.0+)**: 你说「自动审批, 关键词=早饭」这类指令 → AI 调 `group_join_auto` 执行: 官方待审列表里**验证消息命中任一关键词的放行、未命中的拒绝**(`rejectUnmatched:false` 只放行不清场, `dry_run:true` 只预览); 同一人重复申请**去重后按最新一条判**, 拒绝理由中性(默认"答非所问")。⚠️ 仅在主人明确要求时使用。
|
|
237
|
+
|
|
234
238
|
**入群申请双通道(v1.1.4+)**:
|
|
235
239
|
- **消息注记** = QQ 实时事件, 静默 append 到目标会话(web 可见、不唤醒 AI), 格式【入群申请】;
|
|
236
240
|
- **系统提醒** = 轮询兜底, 唤醒 AI 起来处理, 格式【审批轮询】;
|
|
@@ -238,10 +242,12 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
238
242
|
|
|
239
243
|
**群组管理四个独立开关(dock 设置, v1.1.4+)**: `唤醒AI`=唤醒群管会话 AI 处理; `注入群管会话`=hub web 注记(不唤醒); `通知普通群`=申请所在群 web 注记(不唤醒); `唤醒普通群AI`=唤醒申请所在群 AI——事件与轮询两条链路都生效, 互不干扰。
|
|
240
244
|
|
|
241
|
-
**跨群/跨会话工具**(v1.1.0+):
|
|
245
|
+
**跨群/跨会话工具**(v1.1.0+ / **v1.2.0 扩充**):
|
|
242
246
|
- `group_join_requests(gid=…)` / `group_approve_join(member_openids=…)`: 传 `gid` 可查询/审批**指定群**的入群申请(不限于当前会话群), 支持一次批量审批多人;
|
|
243
|
-
- `session_list`: 列出全部会话(含群注册表里的"潜在群", 重启后仍可靠)
|
|
244
|
-
- `
|
|
247
|
+
- `session_list` / `session_wake`: 列出全部会话(含群注册表里的"潜在群", 重启后仍可靠)供寻址 / 向指定会话发消息并唤醒对方 LLM(可带 `media` 跨群发图);
|
|
248
|
+
- **`broadcast_send`(v1.2.0)**: **一键群发** —— 同一段内容一次发到多个群/私聊, `targets` 直接写**分组名 / 群名 / 备注或 openid**; 走插件广播队列(串行+失败重试, 与 dock「📤 群发 · 广播」面板**同一份任务**), 默认直接发, 返回逐目标 `message_id`, 2 分钟内可 `action:"recall"` 撤回;
|
|
249
|
+
- **`target_group`(v1.2.0)**: **分组管理** —— list/create/rename/delete/add/remove, 读写的正是 dock「📇 群组管理 → 🗂 分组」那份数据 → **AI 与主人共用同一份分组**: 主人在面板分好组, AI 直接"发给群友"就能群发。
|
|
250
|
+
- **`group_join_auto`(v1.3.0)**: **按关键词自动审批入群申请** —— 命中任一关键词的**直接放行**、未命中的**直接拒绝**(不可逆, 可 `dry_run` 预览); 源数据是**官方待审列表**(非本地流水), 同一人重复申请**去重按最新一条判**; 仅在主人明确要求时使用。
|
|
245
251
|
|
|
246
252
|
**配置项**(Web 面板 ⑥ 可改, 见下表 `groupAdmin.*`)
|
|
247
253
|
|
|
@@ -254,7 +260,8 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
254
260
|
| `provider` | string | `deepseek-official` | LLM 提供商名称 |
|
|
255
261
|
| `model` | string | `deepseek-chat` | 模型名称 |
|
|
256
262
|
| `preset` | string | - | Agent preset id |
|
|
257
|
-
| `cwd` | string | `process.cwd()` | Agent 工作目录 |
|
|
263
|
+
| `cwd` | string | `process.cwd()` | Agent 工作目录(**不是**数据目录) |
|
|
264
|
+
| `dataRoot` | string | `{cwd}/dshqqbot` | 插件数据根(表情包 / `.qqbot` / `.qqbot-extensions`)。**默认就在 `{cwd}/dshqqbot`** —— 工作目录保持干净; 想放别处显式配置; 老数据首次启动自动迁入 |
|
|
258
265
|
| `requireMention` | boolean | `true` | 群聊是否需要 @bot 才触发 |
|
|
259
266
|
| `groupPrompt` | string | - | 群聊额外 system prompt |
|
|
260
267
|
| `directPrompt` | string | - | 私聊额外 system prompt |
|
|
@@ -277,6 +284,7 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
277
284
|
|------|------|
|
|
278
285
|
| `/outmode` | 查看当前出站模式与四档说明 |
|
|
279
286
|
| `/outmode adaptive` | 切到 **适配主动**(默认): 收到真人消息前5条带引用回你, 之后自动转独立消息, 连发不被吞 |
|
|
287
|
+
| `/outmode detail` | 切到 **详细主动**(v1.2.0): 聊天同"适配主动", 但**额外把 AI 的工具调用/结果也推到 QQ**(看进度用, 消息会变多) |
|
|
280
288
|
| `/outmode passive` | 切到 **被动**: 始终回复你那条(连发约4~5条后被QQ吞) |
|
|
281
289
|
| `/outmode silent` | 切到 **完全不出站**: 她照常思考但不向QQ发任何回复(web可对话) |
|
|
282
290
|
| `/outmode nothink` | 切到 **完全不思考**: QQ入站不唤醒AI, 消息只记录(逃生通道, 可随时切回) |
|
|
@@ -291,6 +299,7 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
291
299
|
| `/botplay` | 出互动事件目录卡(点事件直接触发, 自动翻页); `/botplay 事件名` 直接触发(如 `/botplay 签到`) |
|
|
292
300
|
| `/perm` | 切换权限档: `/perm` 查看; `/perm 只读\|工作区\|全权` 切换(即时生效) |
|
|
293
301
|
| `/new [preset]` | 以指定人格开新会话(旧会话存档可回看); `/presets` 看可用人格 |
|
|
302
|
+
| `/答 <内容>` / `/ans` | **回答提问卡片**(v1.2.0): 按钮点不动或想自己打字时用 —— `/答 A`(选第1个)、`/答 1 3`(多选)、`/答 #2 B`(多个提问时指定第2问)、`/答 你的话`(不是选项 → 当自由回答原样转给 AI) |
|
|
294
303
|
| `/bot-help` | 查看所有指令 |
|
|
295
304
|
| `/tools-reload` | 热刷新 QQ 通道工具(开发用, 新工具无需重启即可用) |
|
|
296
305
|
|
|
@@ -353,6 +362,7 @@ export default {
|
|
|
353
362
|
5. 生效方式: 工具发 `/tools-reload` 或调 `tools_reload` —— 新工具即时生效; **同名工具改内容会被工具注册表跳过(`already registered`) → 换名或重启宿主**; 命令一律需重启宿主(`/bot-restart`)。
|
|
354
363
|
6. 能力边界: **扩展工具无法注册"按钮点击回调"** —— 按钮回调只能由 host 侧的 botplay 事件 / dock 卡片编辑器注册。纯扩展方案的交互范式 = "卡片 + 用户回个编号", 由 AI 当状态机再调一次工具。
|
|
355
364
|
7. 示范(点歌): ①工具里 fetch 搜索接口 → ②拼一段 markdown(封面/歌名/歌手/歌词) → ③`sender.sendMarkdown(replyTarget, 卡片)` → ④要试听就 `sender.sendMedia(...)` → ⑤返回 `{ok:true,msg:'已发卡'}`。
|
|
365
|
+
8. **接口字段别猜**: 写扩展遇到不确定的官方字段/事件/错误码, 先查官方 api-v2 文档 <https://bot.q.qq.com/wiki/develop/api-v2/> (按钮 `action.type` 0=跳转/1=回调/2=指令、键盘 5 行上限、错误码等都在里面), 不要凭印象写。
|
|
356
366
|
|
|
357
367
|
## 富媒体指令(AI 回复里写标记,自动变成真消息)
|
|
358
368
|
|
package/README_EN.md
CHANGED
|
@@ -43,9 +43,10 @@ Right: sticker-battle in action — the bot answers with stickers from its own l
|
|
|
43
43
|
|
|
44
44
|
### For developers
|
|
45
45
|
- Standard tools available inside QQ sessions: `send_media` / `recall_message` / `list_stickers` / `sticker_tag` / `sticker_untagged` / `schedule_timer` / `schedule_cancel` …, routed per bot account.
|
|
46
|
-
- **Group admin tools** (`group_join_requests` / `group_approve_join` / `group_mute_state` / `group_mute_member` …): join-request approval and mute management — requires the bot to be a group admin; inside a group session the current group is used, elsewhere the configured `manageGroup` applies. **Never auto-approve: first run `group_join_requests`, report the applicant + verify text to the owner, then act on an explicit "approve/deny" instruction (`group_approve_join`).**
|
|
46
|
+
- **Group admin tools** (`group_join_requests` / `group_approve_join` / `group_join_auto` / `group_mute_state` / `group_mute_member` …): join-request approval and mute management — requires the bot to be a group admin; inside a group session the current group is used, elsewhere the configured `manageGroup` applies. **Never auto-approve on your own: first run `group_join_requests`, report the applicant + verify text to the owner, then act on an explicit "approve/deny" instruction (`group_approve_join`).** Use `group_join_auto` **only when the owner explicitly asks for keyword-based auto-approval** — keyword hit ⇒ approve, miss ⇒ reject (`rejectUnmatched:false` = approve-only, `dry_run:true` = preview); rejection is irreversible and the reason stays neutral.
|
|
47
47
|
- **Plain text can send media or recall messages**: writing `[MEDIA:image|path-or-url]` in a reply turns it into a real image message (`voice`/`video`/`file` work the same); a lone `[RECALL]` line recalls the bot's own last message.
|
|
48
48
|
- **Cross-session messaging**: `session_list` lists all sessions (incl. latent groups); `session_wake(session_id or scope+peer_id, text, send_qq?, media?)` sends a message to a target session/group and wakes its LLM, optionally also delivering to the bound QQ group/private chat (for humans); `media` supports cross-group images.
|
|
49
|
+
- **Token-lean by design (v1.3.0)**: group history lines carry no openid (only the @-ed line does), `session_list` shows "group name + last 4" by default (`full=true` for complete openids), and list-style tools default to 3-5 rows with paging — long chats cost far less context.
|
|
49
50
|
- **Two forms of a join request**:
|
|
50
51
|
- 【入群申请】appearing in the session stream = **message note** (silent record, no wake) — no immediate action; handle it naturally on the owner's next message.
|
|
51
52
|
- 【审批轮询】alert = **system wake** — actively run `group_join_requests`, report new applicants + verify text to the owner, and wait for the owner's explicit approve/deny.
|
|
@@ -268,6 +269,7 @@ Then run `/tools-reload` (or ask the AI to call the `tools_reload` tool). Comman
|
|
|
268
269
|
- Hot reload: `/tools-reload` (or call `tools_reload`) picks up **new** tools; **editing a tool under the same name is skipped by the registry → rename it or restart the host**; commands always need a restart.
|
|
269
270
|
- Limitation: extensions **cannot register button callbacks** — those require a botplay event or the dock card editor. Extension-only flows are "card + user replies a number", with the AI acting as the state machine.
|
|
270
271
|
- Example (song request): fetch a search API → build a markdown card (cover / title / artist / lyrics) → `sender.sendMarkdown(replyTarget, card)` → optionally `sender.sendMedia(...)` for a preview → return `{ ok: true, msg: 'card sent' }`.
|
|
272
|
+
- API reference: when a field/event/error code is unclear, read the official QQ bot api-v2 docs <https://bot.q.qq.com/wiki/develop/api-v2/> instead of guessing (button `action.type` 0=link / 1=callback / 2=command, keyboard 5-row limit, error codes…).
|
|
271
273
|
|
|
272
274
|
## Core Modules
|
|
273
275
|
|
package/client/qqbot-settings.js
CHANGED
|
@@ -220,7 +220,7 @@ window.__ModuleLoader__.load({
|
|
|
220
220
|
groupPrompt: typeof v.groupPrompt === 'string' ? v.groupPrompt : DEFAULT_GROUP_PROMPT,
|
|
221
221
|
enableApprovals: v.enableApprovals === true,
|
|
222
222
|
approvalTimeoutMs: typeof v.approvalTimeoutMs === 'number' ? v.approvalTimeoutMs : 120000,
|
|
223
|
-
outboundMode: (v.outboundMode === '
|
|
223
|
+
outboundMode: (v.outboundMode === 'detail' || v.outboundMode === 'passive' || v.outboundMode === 'silent' || v.outboundMode === 'nothink' ? v.outboundMode : 'adaptive'),
|
|
224
224
|
groupAdmin: { enabled: v.groupAdmin && v.groupAdmin.enabled === true, owners: Array.isArray(v.groupAdmin && v.groupAdmin.owners) ? v.groupAdmin.owners : [], manageGroup: v.groupAdmin && typeof v.groupAdmin.manageGroup === 'string' ? v.groupAdmin.manageGroup : '', watchJoinRequests: !!(v.groupAdmin && v.groupAdmin.watchJoinRequests), notifyInGroup: v.groupAdmin && v.groupAdmin.notifyInGroup !== false },
|
|
225
225
|
}
|
|
226
226
|
setCfg(base); setRev(d.revision); setMsg('')
|
|
@@ -269,7 +269,7 @@ window.__ModuleLoader__.load({
|
|
|
269
269
|
groupPrompt: typeof gpOverride === 'string' ? gpOverride : (typeof cfg.groupPrompt === 'string' ? cfg.groupPrompt : ''),
|
|
270
270
|
enableApprovals: cfg.enableApprovals === true,
|
|
271
271
|
approvalTimeoutMs: typeof cfg.approvalTimeoutMs === 'number' ? cfg.approvalTimeoutMs : 120000,
|
|
272
|
-
outboundMode: cfg.outboundMode === '
|
|
272
|
+
outboundMode: cfg.outboundMode === 'detail' || cfg.outboundMode === 'passive' || cfg.outboundMode === 'silent' || cfg.outboundMode === 'nothink' ? cfg.outboundMode : 'adaptive',
|
|
273
273
|
groupAdmin: { enabled: cfg.groupAdmin && cfg.groupAdmin.enabled === true, owners: Array.isArray(cfg.groupAdmin && cfg.groupAdmin.owners) ? cfg.groupAdmin.owners : [], manageGroup: cfg.groupAdmin && typeof cfg.groupAdmin.manageGroup === 'string' ? cfg.groupAdmin.manageGroup : '', watchJoinRequests: !!(cfg.groupAdmin && cfg.groupAdmin.watchJoinRequests), notifyInGroup: cfg.groupAdmin && cfg.groupAdmin.notifyInGroup !== false },
|
|
274
274
|
}
|
|
275
275
|
fetch(UPDATE, {
|
|
@@ -285,7 +285,7 @@ window.__ModuleLoader__.load({
|
|
|
285
285
|
groupPrompt: typeof v2.groupPrompt === 'string' ? v2.groupPrompt : (typeof cfg.groupPrompt === 'string' ? cfg.groupPrompt : DEFAULT_GROUP_PROMPT),
|
|
286
286
|
enableApprovals: v2.enableApprovals === true,
|
|
287
287
|
approvalTimeoutMs: typeof v2.approvalTimeoutMs === 'number' ? v2.approvalTimeoutMs : 120000,
|
|
288
|
-
outboundMode: (v2.outboundMode === '
|
|
288
|
+
outboundMode: (v2.outboundMode === 'detail' || v2.outboundMode === 'passive' || v2.outboundMode === 'silent' || v2.outboundMode === 'nothink' ? v2.outboundMode : 'adaptive'),
|
|
289
289
|
groupAdmin: { enabled: v2.groupAdmin && v2.groupAdmin.enabled === true, owners: Array.isArray(v2.groupAdmin && v2.groupAdmin.owners) ? v2.groupAdmin.owners : [], manageGroup: v2.groupAdmin && typeof v2.groupAdmin.manageGroup === 'string' ? v2.groupAdmin.manageGroup : '', watchJoinRequests: !!(v2.groupAdmin && v2.groupAdmin.watchJoinRequests), notifyInGroup: v2.groupAdmin && v2.groupAdmin.notifyInGroup !== false },
|
|
290
290
|
})
|
|
291
291
|
setRev(d.revision); setMsg('已保存 ✓(live 生效)')
|
|
@@ -319,7 +319,7 @@ window.__ModuleLoader__.load({
|
|
|
319
319
|
NumRow({ label: '群里没人 @ 她时,隔几秒才回一次(0=每条都回)', value: cfg.behavior.freeIntervalSec, onChange: function (v) { setBehavior({ freeIntervalSec: v }) } }),
|
|
320
320
|
NumRow({ label: '有人 @ 她时,两次回复至少隔几秒(0=随叫随到)', value: cfg.behavior.mentionIntervalSec, onChange: function (v) { setBehavior({ mentionIntervalSec: v }) } }),
|
|
321
321
|
NumRow({ label: '私聊里隔几秒回一次(0=不限制)', value: cfg.behavior.directIntervalSec, onChange: function (v) { setBehavior({ directIntervalSec: v }) } }),
|
|
322
|
-
h('div', { style: { fontSize: 12, color: '#666', margin: '10px 0 2px' } }, '出站方式(连发消息 QQ 端丢失时切主动):'), h('div', { style: { display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' } }, ['adaptive', 'passive', 'silent', 'nothink'].map(function (m) { var cur = (cfg.outboundMode || 'adaptive'); if (cur === 'active') cur = 'adaptive'; return h('label', { style: { display: 'inline-flex', gap: 5, alignItems: 'center', fontSize: 12, color: '#333', cursor: 'pointer' } }, h('input', { type: 'radio', name: 'qqs-outbound', checked: cur === m, onChange: function () { setCfg(function (c) { return Object.assign({}, c, { outboundMode: m }) }) } }), m === 'adaptive' ? '适配主动(推荐默认)' : (m === 'passive' ? '被动(只回最后一句)' : (m === 'silent' ? '完全不出站(静默)' : '完全不思考(QQ入站不唤醒,仅设置页)'))) })), h('div', { style: { fontSize: 12, color: '#888' } }, '适配主动=刚收到真人消息时前5条带引用回你, 第6条起自动转独立新消息(连发不被QQ吞); 一段时间没新消息的主动推送(定时等)也走独立消息。被动=始终回你那条(连发约4~5条后被QQ吞)。完全不出站=照常思考但不向QQ发任何回复(AI 可用工具切回)。完全不思考=QQ入站不唤醒AI, 消息只记录(仅本页可开; 唤醒请发 /outmode adaptive)。保存即热更新, 不用重启。'), h('div', { style: { fontSize: 12, color: '#666', margin: '10px 0 2px' } }, '延迟聚合(另一套机制,和上面冷却不冲突): 她收到消息先等一小会儿, 把连发的话攒一起综合回, 免得只回第一句。'),
|
|
322
|
+
h('div', { style: { fontSize: 12, color: '#666', margin: '10px 0 2px' } }, '出站方式(连发消息 QQ 端丢失时切主动):'), h('div', { style: { display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' } }, ['adaptive', 'detail', 'passive', 'silent', 'nothink'].map(function (m) { var cur = (cfg.outboundMode || 'adaptive'); if (cur === 'active') cur = 'adaptive'; return h('label', { style: { display: 'inline-flex', gap: 5, alignItems: 'center', fontSize: 12, color: '#333', cursor: 'pointer' } }, h('input', { type: 'radio', name: 'qqs-outbound', checked: cur === m, onChange: function () { setCfg(function (c) { return Object.assign({}, c, { outboundMode: m }) }) } }), m === 'adaptive' ? '适配主动(推荐默认)' : (m === 'detail' ? '详细主动(连工具调用一起推)' : (m === 'passive' ? '被动(只回最后一句)' : (m === 'silent' ? '完全不出站(静默)' : '完全不思考(QQ入站不唤醒,仅设置页)')))) })), h('div', { style: { fontSize: 12, color: '#888' } }, '适配主动=刚收到真人消息时前5条带引用回你, 第6条起自动转独立新消息(连发不被QQ吞); 一段时间没新消息的主动推送(定时等)也走独立消息。被动=始终回你那条(连发约4~5条后被QQ吞)。完全不出站=照常思考但不向QQ发任何回复(AI 可用工具切回)。完全不思考=QQ入站不唤醒AI, 消息只记录(仅本页可开; 唤醒请发 /outmode adaptive)。保存即热更新, 不用重启。'), h('div', { style: { fontSize: 12, color: '#666', margin: '10px 0 2px' } }, '延迟聚合(另一套机制,和上面冷却不冲突): 她收到消息先等一小会儿, 把连发的话攒一起综合回, 免得只回第一句。'),
|
|
323
323
|
BoolRow({ label: '开启延迟聚合(不勾=回到来一条回一条)', value: dbc.enabled !== false, onChange: function (v) { setBehavior({ debounce: { ...dbc, enabled: v } }) } }),
|
|
324
324
|
NumRow({ label: '对方停口几秒后她才开口(默认3;0=不停顿)', value: dbc.silenceSec != null ? dbc.silenceSec : 3, onChange: function (v) { setBehavior({ debounce: { ...dbc, silenceSec: v } }) } }),
|
|
325
325
|
NumRow({ label: '攒满几条立即开口,不等对方停(默认10)', value: dbc.maxMsgs != null ? dbc.maxMsgs : 10, onChange: function (v) { setBehavior({ debounce: { ...dbc, maxMsgs: v } }) } }),
|
|
@@ -1887,7 +1887,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
1887
1887
|
|
|
1888
1888
|
// ── 面板状态(每个实例独立保存, 切回不丢) ──
|
|
1889
1889
|
var state = { ns: '', accts: [], gid: '', groups: [], tab: 'chat', sendScope: 'group', sendTo: '', sendName: '', sendText: '', insertCtx: true, targetQ: '', c2cs: [], joins: null, mutes: null, members: null, muteSecs: '60', bindGid: '', bindName: '', msg: '', busy: '', wantPeer: null, lookedUp: false, detected: null, detectedHit: null, chatItems: [], chatMore: false, chatBusy: '', chatErr: '', chatOldest: 0, chatText: '', chatIns: true, outMode: '', outRev: undefined, bpEvents: [], bpSel: null, bpDraft: null, rosterSel: {}, rosterScope: 'all', rosterQ: '', hubSid: '', hubRev: undefined, hubBusy: '', hubMsg: '', gaEnabled: false, gaPoll: false, gaPollWake: true, gaHubNotify: true, gaNotifyGroup: true, gaInterval: 5, gaMinCount: 1, gaMsg: '', gaBusy: '', bcDraft: null, bcTasks: null, cardMd: '', cardBtns: '', cardGid: '', cardBusy: '', cardQ: '', tgGroups: [], tgCur: '' }
|
|
1890
|
-
// 🗂 自定义目标分组(仿 QQ
|
|
1890
|
+
// 🗂 自定义目标分组(仿 QQ 分组): 2026-09-12 起**host 持久化**({dataRoot}/.qqbot/target-groups.json),
|
|
1891
|
+
// localStorage 只当秒开缓存 —— 这样 **AI 与主人共用同一份分组**(agent 侧 broadcast_send 可直接写分组名群发),
|
|
1892
|
+
// 顺带修掉"换个浏览器分组就没了"的老毛病。
|
|
1891
1893
|
// 结构: [{ id, name, members: ['group:xxx' | 'c2c:yyy', ...] }]
|
|
1892
1894
|
try {
|
|
1893
1895
|
var _tg = localStorage.getItem('qqs-target-groups')
|
|
@@ -1895,6 +1897,45 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
1895
1897
|
} catch (e) {}
|
|
1896
1898
|
function saveTgGroups() {
|
|
1897
1899
|
try { localStorage.setItem('qqs-target-groups', JSON.stringify(state.tgGroups || [])) } catch (e) {}
|
|
1900
|
+
// 同步 host(与 AI 共用); 失败不影响本地缓存与当前操作
|
|
1901
|
+
try {
|
|
1902
|
+
fetch(api('group/target-groups'), {
|
|
1903
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1904
|
+
body: JSON.stringify({ ns: state.ns || undefined, groups: state.tgGroups || [] }),
|
|
1905
|
+
}).catch(function () {})
|
|
1906
|
+
} catch (e) {}
|
|
1907
|
+
}
|
|
1908
|
+
function loadTgGroupsFromHost() {
|
|
1909
|
+
fetch(api('group/target-groups' + (state.ns ? '?ns=' + encodeURIComponent(state.ns) : '')), { cache: 'no-store' })
|
|
1910
|
+
.then(function (r) { return r.json() }).then(function (d) {
|
|
1911
|
+
if (!d || !d.ok) return
|
|
1912
|
+
var host = Array.isArray(d.groups) ? d.groups : []
|
|
1913
|
+
var local = Array.isArray(state.tgGroups) ? state.tgGroups : []
|
|
1914
|
+
if (host.length === 0) {
|
|
1915
|
+
if (local.length > 0) saveTgGroups() // 迁移: 本地有、host 还空 → 推上去(第一次打开新版时发生一次)
|
|
1916
|
+
} else if (local.length === 0) {
|
|
1917
|
+
state.tgGroups = host
|
|
1918
|
+
try { localStorage.setItem('qqs-target-groups', JSON.stringify(host)) } catch (e) {}
|
|
1919
|
+
} else {
|
|
1920
|
+
// 两边都非空 → **按 id 合并并集**: host 打底(以它为准), 本地**独有的组保留**, 同 id 取成员多的一份。
|
|
1921
|
+
// 旧实现是"host 有数据就整体覆盖本地" → 会把本地刚建好、还没同步成功的分组凭空冲掉(2026-09-12 修)。
|
|
1922
|
+
var byId = {}, order = []
|
|
1923
|
+
var put = function (g, localWinsIfMore) {
|
|
1924
|
+
if (!g || !g.id) return
|
|
1925
|
+
var old = byId[g.id]
|
|
1926
|
+
if (!old) { byId[g.id] = g; order.push(g.id); return }
|
|
1927
|
+
var a = (old.members || []).length, b = (g.members || []).length
|
|
1928
|
+
if (localWinsIfMore ? b > a : b >= a) byId[g.id] = g
|
|
1929
|
+
}
|
|
1930
|
+
host.forEach(function (g) { put(g, false) })
|
|
1931
|
+
local.forEach(function (g) { put(g, true) })
|
|
1932
|
+
var merged = order.map(function (id) { return byId[id] })
|
|
1933
|
+
state.tgGroups = merged
|
|
1934
|
+
try { localStorage.setItem('qqs-target-groups', JSON.stringify(merged)) } catch (e) {}
|
|
1935
|
+
if (JSON.stringify(merged) !== JSON.stringify(host)) saveTgGroups() // 合并结果 ≠ host → 推回, 让 AI 与主人看到同一份
|
|
1936
|
+
}
|
|
1937
|
+
if (state.tab === 'roster' || state.tab === 'broadcast') paintBody()
|
|
1938
|
+
}).catch(function () {})
|
|
1898
1939
|
}
|
|
1899
1940
|
// 📇 群组管理 M1: 勾选集合本地持久化(刷新/重开不丢, 供后续群发/批量操作使用)
|
|
1900
1941
|
try { var _rs = localStorage.getItem('qqs-roster-sel'); if (_rs) { var _rso = JSON.parse(_rs); if (_rso && typeof _rso === 'object') state.rosterSel = _rso } } catch (e) {}
|
|
@@ -1947,7 +1988,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
1947
1988
|
function loadOutMode() {
|
|
1948
1989
|
if (state.tab !== 'out') return
|
|
1949
1990
|
fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
|
|
1950
|
-
if (d && d.value) { state.outMode = (d.value.outboundMode === '
|
|
1991
|
+
if (d && d.value) { state.outMode = (d.value.outboundMode === 'detail' || d.value.outboundMode === 'passive' || d.value.outboundMode === 'silent' || d.value.outboundMode === 'nothink' ? d.value.outboundMode : 'adaptive'); state.outRev = d.revision; paintBody() }
|
|
1951
1992
|
}).catch(function () {})
|
|
1952
1993
|
}
|
|
1953
1994
|
// ⚙️ 出站方式: 保存(全量 patch 只带 outboundMode; settings.update 部分合并)
|
|
@@ -1961,7 +2002,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
1961
2002
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1962
2003
|
body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: curRev }),
|
|
1963
2004
|
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
1964
|
-
if (d && d.value) { state.outMode = (d.value.outboundMode === '
|
|
2005
|
+
if (d && d.value) { state.outMode = (d.value.outboundMode === 'detail' || d.value.outboundMode === 'passive' || d.value.outboundMode === 'silent' || d.value.outboundMode === 'nothink' ? d.value.outboundMode : 'adaptive'); state.outRev = d.revision; var h2 = panel ? panel.querySelector('#dk-out-hint') : null; if (h2) h2.textContent = '已保存 ✓ live 热更新已生效(不用重启)' }
|
|
1965
2006
|
else {
|
|
1966
2007
|
var conflicted = !!(d && d.error && String(d.error).indexOf('changed since it was read') >= 0)
|
|
1967
2008
|
if (conflicted && !attempt) { refreshOutModeAndRetry(m) }
|
|
@@ -2371,6 +2412,30 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2371
2412
|
var ta = panel && panel.querySelector('#dk-bc-text')
|
|
2372
2413
|
if (ta) ta.focus()
|
|
2373
2414
|
}
|
|
2415
|
+
/**
|
|
2416
|
+
* 撤回"勾选目标"最近一条广播消息(2026-09-12 主人要求): **不必去任务列表翻 task_id** ——
|
|
2417
|
+
* 服务端自己找该目标最新的、已成功且有 message_id 的广播记录(2 分钟窗口内)。
|
|
2418
|
+
* 主人原话: "直接在勾选里面加撤回按钮, 无条件发送撤回"。
|
|
2419
|
+
*/
|
|
2420
|
+
function recallSelectedLast() {
|
|
2421
|
+
var tg = rosterSelectedTargets()
|
|
2422
|
+
if (!tg.length) { rosterHint('先勾选要撤回的目标'); return }
|
|
2423
|
+
if (!window.confirm('撤回这些目标**最近一条广播消息**?\n\n' + tg.slice(0, 8).map(function (t) { return '· ' + (t.name || t.peerId) }).join('\n') + (tg.length > 8 ? '\n…' : '') + '\n\n(发出超过 2 分钟的会失败)')) return
|
|
2424
|
+
rosterHint('撤回中…')
|
|
2425
|
+
apiPost('group/recall-last', { ns: state.ns || undefined, targets: tg.map(function (t) { return { scope: t.scope, peerId: t.peerId } }) }).then(function (d) {
|
|
2426
|
+
if (!d || !d.ok) { rosterHint('❌ ' + ((d && d.error) || '撤回失败')); return }
|
|
2427
|
+
var rs = d.results || []
|
|
2428
|
+
var okN = rs.filter(function (r) { return r.ok }).length
|
|
2429
|
+
var fails = rs.filter(function (r) { return !r.ok })
|
|
2430
|
+
rosterHint('✅ 已撤回 ' + okN + '/' + rs.length + ' 个' + (fails.length ? ';失败 ' + fails.length + ' 个: ' + fails.slice(0, 2).map(function (r) { return (r.err || '未知') }).join(' / ') : ''))
|
|
2431
|
+
}).catch(function () { rosterHint('❌ 撤回异常') })
|
|
2432
|
+
}
|
|
2433
|
+
/** 选目标页的提示行(与 bcHint 同款, 元素 id 不同) */
|
|
2434
|
+
function rosterHint(t) {
|
|
2435
|
+
state.msg = t || ''
|
|
2436
|
+
var el = panel && panel.querySelector('#dk-roster-hint')
|
|
2437
|
+
if (el) el.textContent = t || ''
|
|
2438
|
+
}
|
|
2374
2439
|
function bcHint(t) {
|
|
2375
2440
|
state.msg = t || ''
|
|
2376
2441
|
var el = panel && panel.querySelector('.dk-msg#dk-bc-hint')
|
|
@@ -2412,6 +2477,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2412
2477
|
if (!panel) return
|
|
2413
2478
|
var sendB = panel.querySelector('#dk-roster-send')
|
|
2414
2479
|
if (sendB) sendB.onclick = function () { openBroadcastDraft('') }
|
|
2480
|
+
// ↩ 撤回勾选项最近消息(2026-09-12): 选目标页与广播页共用同一个按钮 id(tab 互斥渲染)
|
|
2481
|
+
var recallB = panel.querySelector('#dk-roster-recall')
|
|
2482
|
+
if (recallB) recallB.onclick = function () { recallSelectedLast() }
|
|
2415
2483
|
// 广播子页: 跳去「📇 群组管理」勾选目标(2026-09-10)
|
|
2416
2484
|
var gotoB = panel.querySelector('#dk-bc-goto')
|
|
2417
2485
|
if (gotoB) gotoB.onclick = function () { state.tab = 'roster'; paintBody(); bindBodyEvents() }
|
|
@@ -2424,15 +2492,23 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2424
2492
|
var text = ta ? ta.value.trim() : state.bcDraft.text
|
|
2425
2493
|
if (!text) { bcHint('内容不能为空'); return }
|
|
2426
2494
|
var targets = state.bcDraft.targets
|
|
2495
|
+
bcHint('发送中…')
|
|
2427
2496
|
apiPost('group/broadcast/create', { ns: state.ns || undefined, type: 'text', text: text, targets: targets }).then(function (d) {
|
|
2428
|
-
if (!d || !d.ok || !d.task) { bcHint((d && d.error) || '创建失败'); return }
|
|
2497
|
+
if (!d || !d.ok || !d.task) { bcHint('❌ ' + ((d && d.error) || '创建失败')); return }
|
|
2429
2498
|
// 二次确认: 已弹「确认群发」按钮, 这里直接 confirm 入队
|
|
2430
2499
|
return apiPost('group/broadcast/confirm', { ns: state.ns || undefined, task_id: d.task.task_id }).then(function (d2) {
|
|
2431
2500
|
state.bcDraft = null
|
|
2432
|
-
if (d2 && d2.ok) {
|
|
2433
|
-
|
|
2501
|
+
if (d2 && d2.ok) {
|
|
2502
|
+
// 2026-09-12 修: 确认后**切到「📢 广播」子页** —— 任务列表(含逐目标撤回)只在该页渲染,
|
|
2503
|
+
// 以前留在「🎯 选目标」页: 界面毫无变化(主人以为没反应/是 bug), 也从来没见过撤回按钮。
|
|
2504
|
+
state.tab = 'broadcast'
|
|
2505
|
+
state.msg = '✅ 已确认群发 ' + targets.length + ' 个目标(逐目标发送中, 2 分钟内可逐目标撤回)'
|
|
2506
|
+
paintBody(); bindBodyEvents()
|
|
2507
|
+
loadBroadcastTasks(true)
|
|
2508
|
+
startBroadcastAdvance()
|
|
2509
|
+
} else { bcHint('❌ ' + ((d2 && (d2.msg || d2.error)) || '确认失败')) }
|
|
2434
2510
|
})
|
|
2435
|
-
}).catch(function () { bcHint('创建异常') })
|
|
2511
|
+
}).catch(function () { bcHint('❌ 创建异常') })
|
|
2436
2512
|
}
|
|
2437
2513
|
var refreshB = panel.querySelector('#dk-bc-refresh')
|
|
2438
2514
|
if (refreshB) refreshB.onclick = function () { loadBroadcastTasks(true) }
|
|
@@ -2441,15 +2517,19 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2441
2517
|
var b = e.target
|
|
2442
2518
|
var cn = b.getAttribute && b.getAttribute('data-bccancel')
|
|
2443
2519
|
if (cn) { apiPost('group/broadcast/cancel', { ns: state.ns || undefined, task_id: cn }).then(function () { loadBroadcastTasks(true) }); return }
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
var
|
|
2449
|
-
|
|
2450
|
-
var
|
|
2451
|
-
|
|
2452
|
-
apiPost('group/broadcast/recall', { ns: state.ns || undefined, task_id:
|
|
2520
|
+
// 逐目标撤回(2026-09-12 重做): 任务卡片里每个"已发出"的目标自带「↩ 撤回」按钮, 点一下直接撤 ——
|
|
2521
|
+
// 不再弹 window.prompt 让主人手填 peerId 后 6 位(旧交互隐蔽又别扭, 主人根本没找到过)。
|
|
2522
|
+
var r1 = b.getAttribute && b.getAttribute('data-bcrecall1')
|
|
2523
|
+
if (r1) {
|
|
2524
|
+
var parts = String(r1).split('|')
|
|
2525
|
+
var t1 = (state.bcTasks || []).find(function (x) { return x.task_id === parts[0] })
|
|
2526
|
+
var who = t1 && t1.targets.find(function (t2) { return t2.peerId === parts[1] })
|
|
2527
|
+
bcHint('撤回中…')
|
|
2528
|
+
apiPost('group/broadcast/recall', { ns: state.ns || undefined, task_id: parts[0], peerId: parts[1] }).then(function (d) {
|
|
2529
|
+
bcHint(d && d.ok ? ('✅ 已撤回 ' + ((who && who.name) || parts[1])) : ('❌ ' + ((d && d.err) || '撤回失败')))
|
|
2530
|
+
loadBroadcastTasks(false)
|
|
2531
|
+
})
|
|
2532
|
+
return
|
|
2453
2533
|
}
|
|
2454
2534
|
}
|
|
2455
2535
|
}
|
|
@@ -2753,19 +2833,38 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2753
2833
|
+ '<button class="dk-btn ok" id="dk-bc-confirm">✅ 确认群发(发 ' + bc.targets.length + ' 个目标)</button>'
|
|
2754
2834
|
+ '<span class="dk-msg" style="flex:1;text-align:right;font-size:11px;color:#c23131">⚠️ 高影响操作: 发送后 2 分钟内可逐目标撤回</span></div>'
|
|
2755
2835
|
}
|
|
2836
|
+
// 反馈行(2026-09-12 修): bcHint() 一直在找 `#dk-bc-hint`, 但本面板从未渲染过这个元素
|
|
2837
|
+
// → 点「确认群发」后界面**零反馈**(消息其实已经发出去了, 主人以为没反应/是 bug)。
|
|
2838
|
+
out += '<div class="dk-row"><span class="dk-msg" id="dk-bc-hint" style="color:#2f9e44;font-size:12px;margin:2px 0"></span></div>'
|
|
2756
2839
|
if (state.bcTasks && state.bcTasks.length) {
|
|
2757
2840
|
out += '<div class="dk-row"><b style="font-size:12px">📋 群发任务</b>'
|
|
2758
2841
|
+ '<button class="dk-btn" id="dk-bc-refresh" style="margin-left:auto">🔄 刷新进度</button></div>'
|
|
2759
2842
|
state.bcTasks.forEach(function (tk) {
|
|
2760
2843
|
var stc = { draft: '#888', queued: '#1971c2', sending: '#f08c00', partial_failed: '#e03131', done: '#2f9e44', cancelled: '#868e96' }[tk.state] || '#888'
|
|
2761
2844
|
var okN = tk.targets.filter(function (t2) { return tk.results[t2.peerId] && tk.results[t2.peerId].ok }).length
|
|
2762
|
-
|
|
2845
|
+
// 2 分钟撤回窗口(以服务端为准): 从任务完成时刻起算的剩余秒数
|
|
2846
|
+
var left = ''
|
|
2847
|
+
if ((tk.state === 'done' || tk.state === 'partial_failed') && tk.done_at) {
|
|
2848
|
+
var sec = Math.floor((tk.done_at + 120000 - Date.now()) / 1000)
|
|
2849
|
+
left = sec > 0 ? ('可撤 ' + sec + 's') : '已过撤回窗口'
|
|
2850
|
+
}
|
|
2851
|
+
out += '<div class="dk-item" style="border:1px solid #eee;border-radius:6px;margin:2px 0;padding:4px 6px;display:block">'
|
|
2852
|
+
+ '<div style="display:flex;align-items:center;gap:6px">'
|
|
2763
2853
|
+ '<span style="font-size:11px;font-weight:700;color:' + stc + '">' + ({ draft: '草稿', queued: '排队中', sending: '发送中', partial_failed: '部分失败', done: '已完成', cancelled: '已中止' }[tk.state] || tk.state) + '</span>'
|
|
2764
2854
|
+ '<span style="flex:1;font-size:11px;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> ' + esc(String(tk.payload.content || '').slice(0, 30)) + '</span>'
|
|
2765
|
-
+ '<span style="font-size:11px;color:#666">' + okN + '/' + tk.targets.length + '</span>'
|
|
2855
|
+
+ '<span style="font-size:11px;color:#666">' + okN + '/' + tk.targets.length + (left ? ' · ' + left : '') + '</span>'
|
|
2766
2856
|
+ (tk.state === 'queued' || tk.state === 'sending' ? '<button class="dk-btn no" data-bccancel="' + esc(tk.task_id) + '" style="margin-left:6px">⏹ 中止</button>' : '')
|
|
2767
|
-
+ (tk.state === 'done' || tk.state === 'partial_failed' ? '<button class="dk-btn" data-bcrecall="' + esc(tk.task_id) + '" style="margin-left:6px" title="撤回该任务 2 分钟内已发的消息(逐目标)">↩ 撤回</button>' : '')
|
|
2768
2857
|
+ '</div>'
|
|
2858
|
+
// 逐目标明细 + 该目标的「↩ 撤回」(2026-09-12): 谁发出去了、谁失败了、谁能撤, 一眼看清
|
|
2859
|
+
tk.targets.forEach(function (t2) {
|
|
2860
|
+
var r2 = tk.results && tk.results[t2.peerId]
|
|
2861
|
+
var mark = !r2 ? '⏳ 待发' : (r2.ok ? '✅ 已发' : ('❌ ' + esc(String(r2.err || '失败'))))
|
|
2862
|
+
out += '<div style="display:flex;align-items:center;gap:6px;font-size:11px;color:#555;padding-left:6px">'
|
|
2863
|
+
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + (t2.scope === 'group' ? '👥' : '👤') + ' ' + esc(t2.name || String(t2.peerId).slice(0, 12)) + ' <span style="color:#aaa">…' + esc(String(t2.peerId).slice(-4)) + '</span> ' + mark + '</span>'
|
|
2864
|
+
+ (r2 && r2.ok ? '<button class="dk-btn no" data-bcrecall1="' + esc(tk.task_id) + '|' + esc(t2.peerId) + '" style="font-size:11px">↩ 撤回</button>' : '')
|
|
2865
|
+
+ '</div>'
|
|
2866
|
+
})
|
|
2867
|
+
out += '</div>'
|
|
2769
2868
|
})
|
|
2770
2869
|
}
|
|
2771
2870
|
out += '</div>'
|
|
@@ -2820,7 +2919,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2820
2919
|
+ '<button class="dk-btn ok" id="dk-tg-add"' + (state.tgCur ? '' : ' disabled') + ' title="把上面勾选的目标加入当前分组">➕ 勾选的加入本组</button>'
|
|
2821
2920
|
+ '<button class="dk-btn" id="dk-tg-remove"' + (state.tgCur ? '' : ' disabled') + ' title="把勾选的从当前分组移出">➖ 从本组移出</button>'
|
|
2822
2921
|
+ '<button class="dk-btn" id="dk-tg-pickall"' + (state.tgCur ? '' : ' disabled') + ' title="勾选当前分组的全部成员">☑ 选中本组</button>'
|
|
2823
|
-
+ '<span class="dk-msg" style="font-size:11px;color:#888"
|
|
2922
|
+
+ '<span class="dk-msg" style="font-size:11px;color:#888">分组已存到 host(与 AI 共用): 直接说"发给【分组名】"即可群发</span>'
|
|
2824
2923
|
+ '</div>'
|
|
2825
2924
|
body += '<div class="dk-row">范围: '
|
|
2826
2925
|
+ '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="all"' + (state.rosterScope === 'all' ? ' checked' : '') + '> 全部</label>'
|
|
@@ -2923,7 +3022,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2923
3022
|
+ '<button class="dk-btn" id="dk-roster-all">全选(当前范围)</button>'
|
|
2924
3023
|
+ '<button class="dk-btn" id="dk-roster-clear">清空</button>'
|
|
2925
3024
|
+ '<span class="dk-msg" style="flex:1;text-align:right;color:#2f9e44">已选 ' + rosterSelN() + ' 个</span>'
|
|
2926
|
-
+ '<button class="dk-btn ok" id="dk-roster-send">🚀 群发勾选项</button
|
|
3025
|
+
+ '<button class="dk-btn ok" id="dk-roster-send">🚀 群发勾选项</button>'
|
|
3026
|
+
+ '<button class="dk-btn no" id="dk-roster-recall" title="撤回这些目标最近一条广播消息(2 分钟内有效); 不用去任务列表翻 task_id">↩ 撤回勾选项最近消息</button></div>'
|
|
3027
|
+
body += '<div class="dk-row"><span class="dk-msg" id="dk-roster-hint" style="color:#2f9e44;font-size:12px"></span></div>'
|
|
2927
3028
|
body += '<div class="dk-list" id="dk-roster-list"></div>'
|
|
2928
3029
|
// ── M3 群发面板(与「📤 群发 · 广播」子页共用, 2026-09-10 抽成 broadcastPanel) ──
|
|
2929
3030
|
body += broadcastPanel()
|
|
@@ -2936,7 +3037,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2936
3037
|
body += '<div class="dk-row" style="gap:6px;flex-wrap:wrap">'
|
|
2937
3038
|
+ '<button class="dk-btn" id="dk-bc-goto">📇 去勾选目标</button>'
|
|
2938
3039
|
+ '<button class="dk-btn ok" id="dk-roster-send"' + (btg.length ? '' : ' disabled') + ' title="把勾选的目标纳入群发">🚀 群发勾选项</button>'
|
|
3040
|
+
+ '<button class="dk-btn no" id="dk-roster-recall" title="撤回这些目标最近一条广播消息(2 分钟内有效)">↩ 撤回勾选项最近消息</button>'
|
|
2939
3041
|
+ '</div>'
|
|
3042
|
+
body += '<div class="dk-row"><span class="dk-msg" id="dk-roster-hint" style="color:#2f9e44;font-size:12px"></span></div>'
|
|
2940
3043
|
if (btg.length) {
|
|
2941
3044
|
body += '<div class="dk-item" style="border:1px solid #eef7ee;border-radius:6px;margin:4px 0;padding:3px 6px;max-height:16vh;overflow:auto">'
|
|
2942
3045
|
btg.forEach(function (t2) {
|
|
@@ -2950,9 +3053,10 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2950
3053
|
if (om === 'nothink') body += '<div class="dk-msg" style="color:#c23131;margin:2px 0">⚠️ 当前为「完全不思考」(设置页开启): QQ 入站不唤醒 AI。发 /outmode adaptive 可唤醒。</div>'
|
|
2951
3054
|
body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">⇄ 出站方式(保存即热更新,不用重启)</div>'
|
|
2952
3055
|
body += '<div class="dk-row">'
|
|
2953
|
-
+ ['adaptive','passive','silent'].map(function (m2) { return '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px;color:#333"><input type="radio" name="dk-outmode" value="' + m2 + '"' + (om === m2 ? ' checked' : '') + '> ' + (m2==='adaptive' ? '适配主动(推荐默认)' : (m2==='passive' ? '被动(只回最后一句)' : '完全不出站(静默)')) + '</label>' }).join('')
|
|
3056
|
+
+ ['adaptive','detail','passive','silent'].map(function (m2) { return '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px;color:#333"><input type="radio" name="dk-outmode" value="' + m2 + '"' + (om === m2 ? ' checked' : '') + '> ' + (m2==='adaptive' ? '适配主动(推荐默认)' : (m2==='detail' ? '详细主动(连工具调用一起推)' : (m2==='passive' ? '被动(只回最后一句)' : '完全不出站(静默)'))) + '</label>' }).join('')
|
|
2954
3057
|
+ '</div>'
|
|
2955
3058
|
body += '<div class="dk-msg" style="line-height:1.6">适配主动=和QQ有关的会话回复都发到QQ: 刚收到真人消息时前5条带引用回你(能看到回的是哪句), 第6条起自动转独立新消息, 连发不被QQ吞; 定时/后台等没有新真人消息的主动推送也走独立消息。'
|
|
3059
|
+
body += '详细主动=聊天行为与适配主动完全一样, 但额外把 AI 的工具调用(🔧 工具调用)与工具结果也推到 QQ —— 能在 QQ 上看她正在干什么; 消息会明显变多, 看完记得切回适配主动。'
|
|
2956
3060
|
body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(AI 可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
|
|
2957
3061
|
body += '<span class="dk-msg" id="dk-out-hint" style="color:#2f9e44;margin:4px 0"></span>'
|
|
2958
3062
|
} else if (state.tab === 'card') {
|
|
@@ -3081,6 +3185,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
3081
3185
|
if (state.tab === 'card') bindCardEvents()
|
|
3082
3186
|
// 📤 广播子页: 绑定群发按钮 + 刷新任务列表(任务列表静默拉取, 不重绘 → 防死循环)
|
|
3083
3187
|
if (state.tab === 'broadcast') { bindBroadcastEvents(); loadBroadcastTasks(false, true); startBroadcastAdvance() }
|
|
3188
|
+
// 分组(host 持久化, 与 AI 共用): 进选目标/广播页时拉一次, 保证多浏览器与 AI 侧看到的是同一份
|
|
3189
|
+
if (state.tab === 'roster' || state.tab === 'broadcast') loadTgGroupsFromHost()
|
|
3084
3190
|
// 注: 📤 群发子标签栏的点击绑定由上面 bindBodyEvents() 统一处理(它含 [data-subtab])
|
|
3085
3191
|
// 首次进入聊天 tab 自动拉最新一页
|
|
3086
3192
|
if (state.tab === 'chat' && !state.chatItems.length && !state.chatBusy) loadChat(true)
|
|
@@ -65,7 +65,12 @@ export type ApiResult<T> = {
|
|
|
65
65
|
human: string;
|
|
66
66
|
};
|
|
67
67
|
};
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 官方错误码 → 人话(可扩充; code 映射不到时回落通用文案)。
|
|
70
|
+
* 2026-09-12 增强(主人要求"错误人话化"): 不只说"哪里错了", 还要给**可执行的下一步** ——
|
|
71
|
+
* 之前只覆盖群管理类码, 发消息类的报错会把裸码抛给用户/AI(如"被动回复时间或次数超过限制"),
|
|
72
|
+
* 以致 AI 得自己猜怎么绕。
|
|
73
|
+
*/
|
|
69
74
|
export declare function mapErrHuman(code: string | number, gateHuman?: string): string;
|
|
70
75
|
export interface GroupAdminOptions {
|
|
71
76
|
appId: string;
|
|
@@ -151,9 +156,14 @@ export declare class GroupAdminClient {
|
|
|
151
156
|
}>>;
|
|
152
157
|
/**
|
|
153
158
|
* 撤回机器人自己发的消息(官方撤回窗口 2 分钟, 仅能撤自己发的)。
|
|
154
|
-
*
|
|
159
|
+
* ⚠️ 2026-09-12 修(主人实测报 **11001**): 原实现打的是 `/v2/messages/{id}` —— **官方没有这个路径**,
|
|
160
|
+
* 正确路径按场景分(官方文档已核对):
|
|
161
|
+
* 群聊 DELETE `/v2/groups/{group_openid}/messages/{message_id}`
|
|
162
|
+
* 单聊 DELETE `/v2/users/{user_openid}/messages/{message_id}`
|
|
163
|
+
* @param peerId 群 openid(scope=group) 或 用户 openid(scope=c2c)
|
|
164
|
+
* @param msgId 发送成功时返回的 message_id
|
|
155
165
|
*/
|
|
156
|
-
recallMessage(msgId: string): Promise<ApiResult<Record<string, unknown>>>;
|
|
166
|
+
recallMessage(peerId: string, msgId: string, scope?: 'group' | 'c2c'): Promise<ApiResult<Record<string, unknown>>>;
|
|
157
167
|
}
|
|
158
168
|
/** 工厂: 每实例一个 client(配置来自各 bot config.appId/appSecret) */
|
|
159
169
|
export declare function createGroupAdmin(opts: GroupAdminOptions): GroupAdminClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-admin.d.ts","sourceRoot":"","sources":["../../src/api/group-admin.ts"],"names":[],"mappings":"AAqBA,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAO1E,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAC1H;AAUD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,MAAM,CAQrJ;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IACjH,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/G;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEvG
|
|
1
|
+
{"version":3,"file":"group-admin.d.ts","sourceRoot":"","sources":["../../src/api/group-admin.ts"],"names":[],"mappings":"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"}
|
package/dist/api/group-admin.js
CHANGED
|
@@ -51,10 +51,16 @@ export function verifyHuman(vi) {
|
|
|
51
51
|
return VERIFY_METHOD_HUMAN[vi.method] ?? `验证方式:${vi.method}`;
|
|
52
52
|
return '';
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* 官方错误码 → 人话(可扩充; code 映射不到时回落通用文案)。
|
|
56
|
+
* 2026-09-12 增强(主人要求"错误人话化"): 不只说"哪里错了", 还要给**可执行的下一步** ——
|
|
57
|
+
* 之前只覆盖群管理类码, 发消息类的报错会把裸码抛给用户/AI(如"被动回复时间或次数超过限制"),
|
|
58
|
+
* 以致 AI 得自己猜怎么绕。
|
|
59
|
+
*/
|
|
55
60
|
export function mapErrHuman(code, gateHuman) {
|
|
56
61
|
const c = String(code);
|
|
57
62
|
const table = {
|
|
63
|
+
// ── 群管理/权限类 ──
|
|
58
64
|
'11253': gateHuman || '该能力官方尚未开放/未授权(内邀中或需白名单), 请联系平台运营',
|
|
59
65
|
'11703': gateHuman || '机器人无该接口权限: 需为该群管理员, 或该能力尚未对应用开放(探针实测 2026-09-05)',
|
|
60
66
|
'40103004': '不能操作该成员: 群主/管理员/机器人不可被禁言(只能禁普通成员)',
|
|
@@ -63,8 +69,40 @@ export function mapErrHuman(code, gateHuman) {
|
|
|
63
69
|
'40101': '频控: 请求太快, 稍等再试',
|
|
64
70
|
'403': '机器人缺少群管理员身份或未授权',
|
|
65
71
|
'401': '访问凭证无效, 请刷新后重试',
|
|
72
|
+
'11001': '接口或参数不正确(实测: 调用的 API 路径/字段写错时会返回它) —— 检查 URL 与参数, 不是网络问题',
|
|
73
|
+
// ── 消息发送类(2026-09-12 补: 每条都给"怎么办") ──
|
|
74
|
+
'304103': '被动回复的 msg_id 已过期(窗口 5 分钟): 去掉 msg_id 改**主动消息**, 或等群里有人先说一句',
|
|
75
|
+
'40034005': '回复的 msg_id 已过期(被动回复 5 分钟内有效): 改**主动消息**发送, 或让群里先有人说话',
|
|
76
|
+
'304036': '机器人没有 Markdown 模板权限: 改用 `type=text` 纯文本(或去 QQ 开放平台申请 Markdown 模板)',
|
|
77
|
+
'40034008': 'markdown 参数里有空值: 每个模板参数都要有值',
|
|
78
|
+
'40034009': 'markdown 参数里有换行符: 去掉参数中的换行',
|
|
79
|
+
'40034010': '模板参数里不能含 markdown 语法: 参数用纯文本',
|
|
80
|
+
'40034006': '内容违规: 改一下措辞(去掉敏感词/可疑链接)',
|
|
81
|
+
'40034029': '键盘按钮超限(最多 5 行 × 每行 5 个): 减少按钮数量',
|
|
82
|
+
'304004': '无权限使用该 ARK 模板',
|
|
83
|
+
'305007': 'keyboard 结构不对: 检查 rows/buttons 层级',
|
|
84
|
+
'340069': 'msg_type 无效: 只能 0=文本 / 2=markdown / 7=富媒体',
|
|
85
|
+
'22006': '消息类型与内容不匹配: msg_type 要和实际内容字段对应',
|
|
86
|
+
'40034004': '富媒体转存失败(平台没能下载你的 URL): 换可直连的直链, 或改用本地文件上传后重试',
|
|
87
|
+
'304080': 'file_info 无效/已过期: 重新上传文件再发(带 ttl)',
|
|
88
|
+
'304064': '订阅消息未授权: 需用户先授权订阅',
|
|
66
89
|
};
|
|
67
|
-
|
|
90
|
+
const hit = table[c] ?? table[String(Number(c))];
|
|
91
|
+
if (hit)
|
|
92
|
+
return hit;
|
|
93
|
+
// 有些错误不是数字码而是官方 message 文本(如"被动回复时间或次数超过限制") → 关键词兜底, 同样给下一步
|
|
94
|
+
if (!/^\d+$/u.test(c)) {
|
|
95
|
+
if (/被动|回复.*(超|限)/u.test(c)) {
|
|
96
|
+
return `被动回复超限(同一条 msg_id 最多回 ~5 条 / 5 分钟窗口): 去掉 msg_id 改主动消息, 或隔 5 秒重试 · 原文: ${c}`;
|
|
97
|
+
}
|
|
98
|
+
if (/频|限流|太快|too\s*many/iu.test(c))
|
|
99
|
+
return `触发频控, 等 5~10 秒再试 · 原文: ${c}`;
|
|
100
|
+
if (/过期|expire/iu.test(c))
|
|
101
|
+
return `引用的消息已过期(被动回复 5 分钟窗口): 改用主动消息 · 原文: ${c}`;
|
|
102
|
+
if (/markdown|模板/iu.test(c))
|
|
103
|
+
return `Markdown 模板问题: 改用 type=text 纯文本最稳 · 原文: ${c}`;
|
|
104
|
+
}
|
|
105
|
+
return `调用失败(错误码 ${c}), 请稍后重试`;
|
|
68
106
|
}
|
|
69
107
|
/** per-appId token 缓存(带过期提前刷新与并发单飞) */
|
|
70
108
|
class TokenCache {
|
|
@@ -242,10 +280,16 @@ export class GroupAdminClient {
|
|
|
242
280
|
}
|
|
243
281
|
/**
|
|
244
282
|
* 撤回机器人自己发的消息(官方撤回窗口 2 分钟, 仅能撤自己发的)。
|
|
245
|
-
*
|
|
283
|
+
* ⚠️ 2026-09-12 修(主人实测报 **11001**): 原实现打的是 `/v2/messages/{id}` —— **官方没有这个路径**,
|
|
284
|
+
* 正确路径按场景分(官方文档已核对):
|
|
285
|
+
* 群聊 DELETE `/v2/groups/{group_openid}/messages/{message_id}`
|
|
286
|
+
* 单聊 DELETE `/v2/users/{user_openid}/messages/{message_id}`
|
|
287
|
+
* @param peerId 群 openid(scope=group) 或 用户 openid(scope=c2c)
|
|
288
|
+
* @param msgId 发送成功时返回的 message_id
|
|
246
289
|
*/
|
|
247
|
-
async recallMessage(msgId) {
|
|
248
|
-
|
|
290
|
+
async recallMessage(peerId, msgId, scope = 'group') {
|
|
291
|
+
const base = scope === 'c2c' ? '/v2/users' : '/v2/groups';
|
|
292
|
+
return this.call('DELETE', `${base}/${encodeURIComponent(peerId)}/messages/${encodeURIComponent(msgId)}`);
|
|
249
293
|
}
|
|
250
294
|
}
|
|
251
295
|
/** 工厂: 每实例一个 client(配置来自各 bot config.appId/appSecret) */
|