@jerryliang122/openclaw-qqbot 1.0.5 → 1.0.7
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/index.cjs +608 -500
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/package.json +1 -1
- package/src/config.ts +64 -4
- package/src/dispatch/ctx-builder.ts +30 -2
- package/src/dispatch/envelope-builder.ts +7 -2
- package/src/gateway/lifecycle.ts +3 -1
- package/src/outbound/media-send.ts +9 -6
- package/src/outbound/outbound-service.ts +116 -21
package/dist/index.d.cts
CHANGED
|
@@ -960,6 +960,11 @@ declare function resolveQQBotEnvClientSecret(): string;
|
|
|
960
960
|
declare function listQQBotAccountIds(cfg: OpenClawConfig): string[];
|
|
961
961
|
/**
|
|
962
962
|
* 获取默认账户 ID
|
|
963
|
+
*
|
|
964
|
+
* 优先返回「可运行」的账号:
|
|
965
|
+
* 1. 顶层 default 账号可运行(appId + 凭证来源 + enabled)→ default
|
|
966
|
+
* 2. 否则第一个可运行的命名账号(accounts.<id>)
|
|
967
|
+
* 3. 都不可运行 → 保持旧行为返回 default(错误信息指向配置根因)
|
|
963
968
|
*/
|
|
964
969
|
declare function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string;
|
|
965
970
|
/**
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedQQBotAccount, QQBotAccountConfig, ToolPolicy, GroupConfig, GroupCoalesceConfig } from "./types.js";
|
|
2
2
|
import type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk";
|
|
3
|
+
import { loadCredentialBackup } from "./features/credential-backup.js";
|
|
3
4
|
|
|
4
5
|
// ============ Agent-aware mentionPatterns 解析 ============
|
|
5
6
|
|
|
@@ -248,22 +249,81 @@ export function listQQBotAccountIds(cfg: OpenClawConfig): string[] {
|
|
|
248
249
|
return Array.from(ids);
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/**
|
|
253
|
+
* 判断一个账号形状是否「可运行」:配置内声明了 appId、未被禁用,且任一
|
|
254
|
+
* 凭证来源可用(配置内 secret / secretFile / 环境变量 / 凭证暂存备份)。
|
|
255
|
+
*
|
|
256
|
+
* appId 必须来自配置——listQQBotAccountIds 只枚举配置里有 appId 的账号,
|
|
257
|
+
* 框架据此决定启动哪些账号;备份只能补 secret(appId 在、secret 丢的
|
|
258
|
+
* 恢复场景,lifecycle 启动时会用备份恢复),不能让一个未被枚举、根本
|
|
259
|
+
* 不会被启动的账号被选为默认(Sourcery 复审:选中≠可运行)。
|
|
260
|
+
*
|
|
261
|
+
* 用于默认账号解析——只看 appId 是否存在会把已登出(secret 被删、appId 残留)
|
|
262
|
+
* 或停用的顶层账号当成默认账号,导致 message 工具等框架侧主动发送解析到
|
|
263
|
+
* 一个永远不会启动的账号(issue #15:Bot "default" not running)。
|
|
264
|
+
*/
|
|
265
|
+
function accountShapeRunnable(params: {
|
|
266
|
+
appId: string;
|
|
267
|
+
enabled: boolean;
|
|
268
|
+
hasSecretSource: boolean;
|
|
269
|
+
accountId: string;
|
|
270
|
+
}): boolean {
|
|
271
|
+
if (!params.appId || !params.enabled) return false;
|
|
272
|
+
if (params.hasSecretSource) return true;
|
|
273
|
+
// 配置缺 secret 但备份可恢复(appId 已在配置内,账号会被枚举启动)
|
|
274
|
+
try {
|
|
275
|
+
return loadCredentialBackup(params.accountId) !== null;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
251
281
|
/**
|
|
252
282
|
* 获取默认账户 ID
|
|
283
|
+
*
|
|
284
|
+
* 优先返回「可运行」的账号:
|
|
285
|
+
* 1. 顶层 default 账号可运行(appId + 凭证来源 + enabled)→ default
|
|
286
|
+
* 2. 否则第一个可运行的命名账号(accounts.<id>)
|
|
287
|
+
* 3. 都不可运行 → 保持旧行为返回 default(错误信息指向配置根因)
|
|
253
288
|
*/
|
|
254
289
|
export function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
|
|
255
290
|
const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
|
|
256
|
-
|
|
291
|
+
|
|
292
|
+
// 1. 如果默认账户可运行,返回 default
|
|
257
293
|
if (qqbot?.appId || resolveQQBotEnvAppId()) {
|
|
258
|
-
|
|
294
|
+
const runnable = accountShapeRunnable({
|
|
295
|
+
appId: normalizeAppId(qqbot?.appId) || normalizeAppId(resolveQQBotEnvAppId()),
|
|
296
|
+
enabled: qqbot?.enabled !== false,
|
|
297
|
+
hasSecretSource: Boolean(
|
|
298
|
+
qqbot?.clientSecret
|
|
299
|
+
|| qqbot?.clientSecretFile
|
|
300
|
+
|| resolveQQBotEnvClientSecret(),
|
|
301
|
+
),
|
|
302
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
303
|
+
});
|
|
304
|
+
if (runnable) return DEFAULT_ACCOUNT_ID;
|
|
259
305
|
}
|
|
260
|
-
|
|
306
|
+
|
|
307
|
+
// 2. 否则返回第一个可运行的命名账户
|
|
261
308
|
if (qqbot?.accounts) {
|
|
309
|
+
for (const [accountId, account] of Object.entries(qqbot.accounts)) {
|
|
310
|
+
if (!account) continue;
|
|
311
|
+
const runnable = accountShapeRunnable({
|
|
312
|
+
appId: normalizeAppId(account.appId),
|
|
313
|
+
enabled: account.enabled !== false,
|
|
314
|
+
hasSecretSource: Boolean(account.clientSecret || account.clientSecretFile),
|
|
315
|
+
accountId,
|
|
316
|
+
});
|
|
317
|
+
if (runnable) return accountId;
|
|
318
|
+
}
|
|
319
|
+
// 兼容旧形态:命名账号均无 appId(凭 backup 恢复)时保持原顺序返回第一个
|
|
262
320
|
const ids = Object.keys(qqbot.accounts);
|
|
263
|
-
if (ids.length > 0) {
|
|
321
|
+
if (ids.length > 0 && ids.every((id) => !qqbot.accounts?.[id]?.appId)) {
|
|
264
322
|
return ids[0];
|
|
265
323
|
}
|
|
266
324
|
}
|
|
325
|
+
|
|
326
|
+
// 3. 旧回退
|
|
267
327
|
return DEFAULT_ACCOUNT_ID;
|
|
268
328
|
}
|
|
269
329
|
|
|
@@ -89,7 +89,26 @@ export function buildCtxPayload(params: CtxPayloadParams): any {
|
|
|
89
89
|
},
|
|
90
90
|
reply: {
|
|
91
91
|
to: envelope.targetId,
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* ReplyToId 契约语义 = 当前消息回复(引用)的那条消息的 ID —— 对齐
|
|
94
|
+
* telegram 原生通道(replyHead?.messageId ?? visibleReplyTarget?.id),
|
|
95
|
+
* 无引用时必须为 undefined。框架将其渲染为模型可见 Conversation info
|
|
96
|
+
* 的 reply_to_id 字段。
|
|
97
|
+
*
|
|
98
|
+
* 历史版本误填 envelope.messageId(消息自身 ID),导致每条消息都呈现
|
|
99
|
+
* message_id == reply_to_id —— 被上层 agent 的 AGENTS 规则判为「内部
|
|
100
|
+
* 回灌、非新指令」而拒绝响应(2026-09-22 事故)。
|
|
101
|
+
*
|
|
102
|
+
* QQ 被动回复锚点不依赖此字段:deliverCtx.replyToId / msgid-cache 兜底
|
|
103
|
+
* (resolvePassiveFirstReplyToId)/ attachMsgIdWithQuota 均独立取值,
|
|
104
|
+
* threading resolveReplyToMode='off' 阻断框架隐式回复线程。
|
|
105
|
+
*
|
|
106
|
+
* 防御守卫:即使上游异常传入当前消息 ID(自引用),也强制置空——
|
|
107
|
+
* 「reply_to_id == message_id」这一假回灌签名在任何路径都不允许再现。
|
|
108
|
+
*/
|
|
109
|
+
replyToId: envelope.quote?.messageId && envelope.quote.messageId !== envelope.messageId
|
|
110
|
+
? envelope.quote.messageId
|
|
111
|
+
: undefined,
|
|
93
112
|
originatingTo: envelope.targetId,
|
|
94
113
|
},
|
|
95
114
|
access: {
|
|
@@ -100,8 +119,17 @@ export function buildCtxPayload(params: CtxPayloadParams): any {
|
|
|
100
119
|
: undefined,
|
|
101
120
|
media: mediaFacts.length > 0 ? mediaFacts : undefined,
|
|
102
121
|
supplemental: {
|
|
122
|
+
// quote.id 同样必须是「被引用消息」的真实 ID(ref-index 命中时),
|
|
123
|
+
// 而非当前消息 ID——框架经 applySupplementalContext 映射为
|
|
124
|
+
// ctx.ReplyToId / "Reply target of current user message"。
|
|
103
125
|
quote: envelope.quote
|
|
104
|
-
? {
|
|
126
|
+
? {
|
|
127
|
+
...(envelope.quote.messageId && envelope.quote.messageId !== envelope.messageId
|
|
128
|
+
? { id: envelope.quote.messageId }
|
|
129
|
+
: {}),
|
|
130
|
+
body: envelope.quote.content,
|
|
131
|
+
sender: envelope.quote.senderId,
|
|
132
|
+
}
|
|
105
133
|
: undefined,
|
|
106
134
|
groupSystemPrompt: envelope.systemPrompt,
|
|
107
135
|
},
|
|
@@ -20,7 +20,7 @@ export interface OpenClawInboundMessage {
|
|
|
20
20
|
messageId: string;
|
|
21
21
|
content: string;
|
|
22
22
|
history?: Array<{ role: string; content: string; senderId?: string; senderName?: string }>;
|
|
23
|
-
quote?: { content: string; senderId: string; attachments?: unknown[] };
|
|
23
|
+
quote?: { content: string; senderId: string; messageId?: string; attachments?: unknown[] };
|
|
24
24
|
attachments?: unknown[];
|
|
25
25
|
/** 入站图片 URL 列表(从附件中提取) */
|
|
26
26
|
imageUrls?: string[];
|
|
@@ -44,7 +44,11 @@ function mapHistory(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* 将 ResolvedQuote 转换为 OpenClaw quote
|
|
47
|
+
* 将 ResolvedQuote 转换为 OpenClaw quote 格式。
|
|
48
|
+
*
|
|
49
|
+
* messageId 为被引用消息的真实平台消息 ID:仅 ref-index store 命中
|
|
50
|
+
* (quote.source === 'store',entry 存在)时可取;msg_elements 兜底
|
|
51
|
+
* 解析拿不到 id,保持 undefined —— 此时框架不渲染 reply_to_id。
|
|
48
52
|
*/
|
|
49
53
|
function mapQuote(
|
|
50
54
|
quote: ResolvedQuote | undefined,
|
|
@@ -53,6 +57,7 @@ function mapQuote(
|
|
|
53
57
|
return {
|
|
54
58
|
content: quote.text,
|
|
55
59
|
senderId: quote.entry?.senderId ?? '',
|
|
60
|
+
messageId: quote.entry?.messageId,
|
|
56
61
|
attachments: quote.attachments as unknown[],
|
|
57
62
|
};
|
|
58
63
|
}
|
package/src/gateway/lifecycle.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { tryGetQQBotRuntime } from '../runtime.js';
|
|
|
20
20
|
import { getAdapters } from '../adapter/resolve.js';
|
|
21
21
|
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
22
22
|
import { validateRemoteUrl } from '../utils/ssrf-guard.js';
|
|
23
|
-
import {
|
|
23
|
+
import { resolveGatewayForSend, notRunningError } from './outbound-service.js';
|
|
24
24
|
import { parseTarget } from './target.js';
|
|
25
25
|
import { checkAndConsumePassiveReplyQuota } from '../features/quota-manager.js';
|
|
26
26
|
import {
|
|
@@ -158,18 +158,21 @@ export async function sendMedia(params: SendMediaParams): Promise<SendMediaResul
|
|
|
158
158
|
?? (params.mimeType ? inferMediaKindFromMime(params.mimeType) : undefined)
|
|
159
159
|
?? inferMediaKind(resolved.path);
|
|
160
160
|
|
|
161
|
-
// 3. 获取 gateway
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
// 3. 获取 gateway(带单账号回退:issue #15——message 工具主动发媒体时
|
|
162
|
+
// 请求键可能解析到未运行的 "default",单账号部署回退到唯一运行中的网关)。
|
|
163
|
+
// 配额记账用 resolved.accountId(实际发送账号),见 resolveGatewayForSend 注释。
|
|
164
|
+
const sendGateway = resolveGatewayForSend(accountId);
|
|
165
|
+
if (!sendGateway) {
|
|
166
|
+
return { error: notRunningError(accountId) };
|
|
165
167
|
}
|
|
168
|
+
const gw = sendGateway.gw;
|
|
166
169
|
|
|
167
170
|
const target = parseTarget(params.to);
|
|
168
171
|
|
|
169
172
|
const reservation = params.quotaReserved || !params.replyToId
|
|
170
173
|
? { canReply: Boolean(params.replyToId), rollback: () => {} }
|
|
171
174
|
: checkAndConsumePassiveReplyQuota({
|
|
172
|
-
accountId,
|
|
175
|
+
accountId: sendGateway.accountId,
|
|
173
176
|
msgId: params.replyToId,
|
|
174
177
|
scope: target.scope,
|
|
175
178
|
log,
|
|
@@ -8,6 +8,7 @@ import * as path from 'node:path';
|
|
|
8
8
|
import { MediaFileType } from '@tencent-connect/qqbot-nodejs';
|
|
9
9
|
import type { QQBotGateway } from '../gateway/index.js';
|
|
10
10
|
import type { ResolvedQQBotAccount } from '../types.js';
|
|
11
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
11
12
|
import { parseTarget } from './target.js';
|
|
12
13
|
import {
|
|
13
14
|
checkAndConsumePassiveReplyQuota,
|
|
@@ -17,7 +18,41 @@ import {
|
|
|
17
18
|
|
|
18
19
|
// ── Gateway 注册表(生命周期由 channel.ts 管理)──
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* 进程级注册表桥(issue #15):
|
|
23
|
+
*
|
|
24
|
+
* openclaw 的 message 工具在部分执行上下文(后台任务 / bootstrap 兜底注册表)
|
|
25
|
+
* 会经 jiti/二次加载产生插件的**另一个模块实例**——模块级 Map 在两个实例里
|
|
26
|
+
* 各持一份,第二实例的注册表为空,getGateway 恒 miss,主动发送全部报
|
|
27
|
+
* `Bot "<accountId>" not running`(账号 id 本身解析是对的)。gateway 是
|
|
28
|
+
* 进程级资源(WebSocket 连接),注册表挂到 globalThis(Symbol.for 跨副本
|
|
29
|
+
* 共享)让所有模块实例看到同一份。
|
|
30
|
+
*/
|
|
31
|
+
const GATEWAYS_REGISTRY_KEY = Symbol.for('openclaw-qqbot.gateways');
|
|
32
|
+
|
|
33
|
+
const globalRegistry = globalThis as unknown as Record<symbol, unknown>;
|
|
34
|
+
const gateways: Map<string, QQBotGateway> =
|
|
35
|
+
(globalRegistry[GATEWAYS_REGISTRY_KEY] as Map<string, QQBotGateway> | undefined) ?? new Map();
|
|
36
|
+
globalRegistry[GATEWAYS_REGISTRY_KEY] = gateways;
|
|
37
|
+
|
|
38
|
+
// 契约入口(框架直发 / message 工具 / cron)无调用方 logger 可传,
|
|
39
|
+
// 回退日志用模块级 logger 兜底(与 outbound-adapter.ts 的 alog 同模式)。
|
|
40
|
+
// 注意:本模块处于 bot-instance → outbound-service → plugin-logger → runtime
|
|
41
|
+
// → bot-instance 的循环依赖环上,logger 必须惰性创建(顶层求值会在模块
|
|
42
|
+
// 环半初始化状态下崩溃),且日志失败绝不影响发送。
|
|
43
|
+
let svcLog: ReturnType<typeof createPluginLogger> | undefined;
|
|
44
|
+
|
|
45
|
+
function logFallbackInfo(msg: string): void {
|
|
46
|
+
try {
|
|
47
|
+
svcLog ??= createPluginLogger({ prefix: '[outbound]' });
|
|
48
|
+
svcLog.info(msg);
|
|
49
|
+
} catch {
|
|
50
|
+
/* logging must never break sends */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 单账号回退的 INFO 日志去重:每个失效账号键只打一次,避免刷屏 */
|
|
55
|
+
const gatewayFallbackLogged = new Set<string>();
|
|
21
56
|
|
|
22
57
|
/**
|
|
23
58
|
* 为一次实际 API 调用预留被动回复配额。
|
|
@@ -66,7 +101,16 @@ export function registerGateway(accountId: string, gw: QQBotGateway): void {
|
|
|
66
101
|
gateways.set(accountId, gw);
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
export function unregisterGateway(accountId: string): void {
|
|
104
|
+
export function unregisterGateway(accountId: string, expectGw?: QQBotGateway): void {
|
|
105
|
+
if (expectGw) {
|
|
106
|
+
// 所有权守卫(Sourcery 复审):仅当注册表里持有的是**本实例注册的同一个
|
|
107
|
+
// gateway 对象**才注销——旧模块实例延迟执行的 stop 不得删掉新实例已经
|
|
108
|
+
// 注册的替代网关(共享注册表后,两个实例看到同一份 Map)。
|
|
109
|
+
if (gateways.get(accountId) !== expectGw) return;
|
|
110
|
+
gateways.delete(accountId);
|
|
111
|
+
clearQuotaCacheForAccount(accountId);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
70
114
|
gateways.delete(accountId);
|
|
71
115
|
clearQuotaCacheForAccount(accountId);
|
|
72
116
|
}
|
|
@@ -75,6 +119,57 @@ export function getGateway(accountId: string): QQBotGateway | undefined {
|
|
|
75
119
|
return gateways.get(accountId);
|
|
76
120
|
}
|
|
77
121
|
|
|
122
|
+
/** 注册表中所有运行中的账号 ID(诊断与单账号回退用) */
|
|
123
|
+
export function getRegisteredAccountIds(): string[] {
|
|
124
|
+
return [...gateways.keys()];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 发送路径的 gateway 解析(带单账号回退,issue #15):
|
|
129
|
+
* - 精确命中 → 直接返回
|
|
130
|
+
* - 未命中且注册表里恰好只有一个运行中的账号 → 回退到它。单账号部署下
|
|
131
|
+
* 请求键与注册键不一致只可能来自配置残影(如顶层 appId 残留的 "default"),
|
|
132
|
+
* 不存在路由歧义;回退仅打一次 INFO 便于事后取证。
|
|
133
|
+
* - 未命中且运行中账号为 0 或多个 → 返回 undefined 由调用方报错——
|
|
134
|
+
* OpenID 跨账号不通用,多账号下盲目回退会把消息发给错误的 bot。
|
|
135
|
+
*
|
|
136
|
+
* 返回 `accountId` 为**实际发送账号**(回退时 ≠ 请求键):被动回复配额是
|
|
137
|
+
* 平台按「真实发送账号 × msg_id」计的,配额预留/回滚必须用 resolved.accountId
|
|
138
|
+
* 记账,否则会记到从未发送的账号下(绕过或误伤限额)。
|
|
139
|
+
*/
|
|
140
|
+
export interface ResolvedSendGateway {
|
|
141
|
+
gw: QQBotGateway;
|
|
142
|
+
/** 实际发送账号 ID(配额记账键) */
|
|
143
|
+
accountId: string;
|
|
144
|
+
/** 回退来源账号键(仅诊断;精确命中时无此字段) */
|
|
145
|
+
fallbackFrom?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveGatewayForSend(accountId: string): ResolvedSendGateway | undefined {
|
|
149
|
+
const gw = gateways.get(accountId);
|
|
150
|
+
if (gw) return { gw, accountId };
|
|
151
|
+
const running = getRegisteredAccountIds();
|
|
152
|
+
if (running.length === 1) {
|
|
153
|
+
const soleAccountId = running[0];
|
|
154
|
+
const soleGw = gateways.get(soleAccountId);
|
|
155
|
+
if (!soleGw) return undefined;
|
|
156
|
+
if (!gatewayFallbackLogged.has(accountId)) {
|
|
157
|
+
gatewayFallbackLogged.add(accountId);
|
|
158
|
+
logFallbackInfo(
|
|
159
|
+
`account "${accountId}" not running; falling back to sole running account "${soleAccountId}"`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { gw: soleGw, accountId: soleAccountId, fallbackFrom: accountId };
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** "not running" 错误(多账号时附运行中账号列表,便于排障) */
|
|
168
|
+
export function notRunningError(accountId: string): string {
|
|
169
|
+
const running = getRegisteredAccountIds();
|
|
170
|
+
return `Bot "${accountId}" not running${running.length ? ` (running accounts: ${running.join(', ')})` : ''}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
78
173
|
// ── 媒体类型映射 ──
|
|
79
174
|
|
|
80
175
|
export type MediaKind = 'image' | 'voice' | 'video' | 'file';
|
|
@@ -104,17 +199,17 @@ export async function sendText(params: {
|
|
|
104
199
|
quotaReserved?: boolean;
|
|
105
200
|
}): Promise<SendResult> {
|
|
106
201
|
const accountId = params.account.accountId;
|
|
107
|
-
const
|
|
108
|
-
if (!
|
|
202
|
+
const resolved = resolveGatewayForSend(accountId);
|
|
203
|
+
if (!resolved) return { error: notRunningError(accountId) };
|
|
109
204
|
const target = parseTarget(params.to);
|
|
110
205
|
const reservation = reservePassiveReply({
|
|
111
206
|
replyToId: params.replyToId,
|
|
112
|
-
accountId,
|
|
207
|
+
accountId: resolved.accountId,
|
|
113
208
|
scope: target.scope,
|
|
114
209
|
quotaReserved: params.quotaReserved,
|
|
115
210
|
});
|
|
116
211
|
try {
|
|
117
|
-
const result = await gw.sendText(target, params.text, { msgId: reservation.msgId });
|
|
212
|
+
const result = await resolved.gw.sendText(target, params.text, { msgId: reservation.msgId });
|
|
118
213
|
return { messageId: result.id };
|
|
119
214
|
} catch (err: unknown) {
|
|
120
215
|
reservation.rollback();
|
|
@@ -133,12 +228,12 @@ export async function sendMedia(params: {
|
|
|
133
228
|
quotaReserved?: boolean;
|
|
134
229
|
}): Promise<SendResult> {
|
|
135
230
|
const accountId = params.account.accountId;
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
231
|
+
const resolved = resolveGatewayForSend(accountId);
|
|
232
|
+
if (!resolved) return { error: notRunningError(accountId) };
|
|
138
233
|
const target = parseTarget(params.to);
|
|
139
234
|
const reservation = reservePassiveReply({
|
|
140
235
|
replyToId: params.replyToId,
|
|
141
|
-
accountId,
|
|
236
|
+
accountId: resolved.accountId,
|
|
142
237
|
scope: target.scope,
|
|
143
238
|
quotaReserved: params.quotaReserved,
|
|
144
239
|
});
|
|
@@ -147,19 +242,19 @@ export async function sendMedia(params: {
|
|
|
147
242
|
const msgId = reservation.msgId;
|
|
148
243
|
if (kind === 'voice') {
|
|
149
244
|
const source = resolveVoiceSource(params.mediaUrl);
|
|
150
|
-
const result = await gw.sendVoice(target, source, { text: params.text, msgId });
|
|
245
|
+
const result = await resolved.gw.sendVoice(target, source, { text: params.text, msgId });
|
|
151
246
|
return { messageId: result.id };
|
|
152
247
|
}
|
|
153
248
|
if (kind === 'video') {
|
|
154
|
-
const result = await gw.sendVideo(target, params.mediaUrl, { text: params.text, msgId });
|
|
249
|
+
const result = await resolved.gw.sendVideo(target, params.mediaUrl, { text: params.text, msgId });
|
|
155
250
|
return { messageId: result.id };
|
|
156
251
|
}
|
|
157
252
|
if (kind === 'file') {
|
|
158
|
-
const result = await gw.sendFile(target, params.mediaUrl, { text: params.text, msgId });
|
|
253
|
+
const result = await resolved.gw.sendFile(target, params.mediaUrl, { text: params.text, msgId });
|
|
159
254
|
return { messageId: result.id };
|
|
160
255
|
}
|
|
161
256
|
const fileType = MEDIA_KIND_TO_FILE_TYPE[kind];
|
|
162
|
-
const result = await gw.sendMedia(target, params.mediaUrl, { text: params.text, msgId, fileType });
|
|
257
|
+
const result = await resolved.gw.sendMedia(target, params.mediaUrl, { text: params.text, msgId, fileType });
|
|
163
258
|
return { messageId: result.id };
|
|
164
259
|
} catch (err: unknown) {
|
|
165
260
|
reservation.rollback();
|
|
@@ -176,17 +271,17 @@ export async function sendVoice(params: {
|
|
|
176
271
|
quotaReserved?: boolean;
|
|
177
272
|
}): Promise<SendResult> {
|
|
178
273
|
const accountId = params.account.accountId;
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
274
|
+
const resolved = resolveGatewayForSend(accountId);
|
|
275
|
+
if (!resolved) return { error: notRunningError(accountId) };
|
|
181
276
|
const target = parseTarget(params.to);
|
|
182
277
|
const reservation = reservePassiveReply({
|
|
183
278
|
replyToId: params.replyToId,
|
|
184
|
-
accountId,
|
|
279
|
+
accountId: resolved.accountId,
|
|
185
280
|
scope: target.scope,
|
|
186
281
|
quotaReserved: params.quotaReserved,
|
|
187
282
|
});
|
|
188
283
|
try {
|
|
189
|
-
const result = await gw.sendVoice(target, params.source, { msgId: reservation.msgId });
|
|
284
|
+
const result = await resolved.gw.sendVoice(target, params.source, { msgId: reservation.msgId });
|
|
190
285
|
return { messageId: result.id };
|
|
191
286
|
} catch (err: unknown) {
|
|
192
287
|
reservation.rollback();
|
|
@@ -203,17 +298,17 @@ export async function sendVideo(params: {
|
|
|
203
298
|
quotaReserved?: boolean;
|
|
204
299
|
}): Promise<SendResult> {
|
|
205
300
|
const accountId = params.account.accountId;
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
301
|
+
const resolved = resolveGatewayForSend(accountId);
|
|
302
|
+
if (!resolved) return { error: notRunningError(accountId) };
|
|
208
303
|
const target = parseTarget(params.to);
|
|
209
304
|
const reservation = reservePassiveReply({
|
|
210
305
|
replyToId: params.replyToId,
|
|
211
|
-
accountId,
|
|
306
|
+
accountId: resolved.accountId,
|
|
212
307
|
scope: target.scope,
|
|
213
308
|
quotaReserved: params.quotaReserved,
|
|
214
309
|
});
|
|
215
310
|
try {
|
|
216
|
-
const result = await gw.sendVideo(target, params.videoUrl, { msgId: reservation.msgId });
|
|
311
|
+
const result = await resolved.gw.sendVideo(target, params.videoUrl, { msgId: reservation.msgId });
|
|
217
312
|
return { messageId: result.id };
|
|
218
313
|
} catch (err: unknown) {
|
|
219
314
|
reservation.rollback();
|