@jerryliang122/openclaw-qqbot 1.0.7 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jerryliang122/openclaw-qqbot",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "QQ Bot channel plugin for OpenClaw (independently maintained fork)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -48,14 +48,14 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^20.19.43",
51
- "openclaw": "2026.9.2",
51
+ "openclaw": "2026.9.1",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^5.9.3",
54
54
  "tsx": "^4.23.13"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@tencent-connect/qqbot-connector": "1.2.0",
58
- "openclaw": ">=2026.9.2"
58
+ "openclaw": ">=2026.9.1"
59
59
  },
60
60
  "homepage": "https://github.com/jerryliang122/qqbot-openclaw",
61
61
  "repository": {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * readAllowFromStore / issueChallenge / buildReply 直接来自稳定 subpath。
5
5
  *
6
- * approveCode 例外:`approveChannelPairingCode` 在 2026.9.2 未从任何
6
+ * approveCode 例外:`approveChannelPairingCode` 在 2026.9.1/2026.9.2 均未从任何
7
7
  * plugin-sdk subpath 导出(只存在于内部 pairing-store chunk 与
8
8
  * `openclaw pairing approve` CLI),走与网关同源的 CLI 执行
9
9
  * (见 secret-store-cli 的 resolveOpenClawCli 同源约束)。
@@ -2,7 +2,7 @@
2
2
  * Runtime Adapters — 一次 resolve,全程复用。
3
3
  *
4
4
  * Capability Probe 模式:按候选 API 路径探测,选出第一个可用函数。
5
- * 构建基线为 openclaw 2026.9.2(peer >=2026.9.2),不再保留对更旧版本的回退。
5
+ * 构建基线为 openclaw 2026.9.1(peer >=2026.9.1),不再保留对更旧版本的回退。
6
6
  *
7
7
  * 调用方通过 `resolveRuntimeAdapters(runtime)` 获取适配层对象,
8
8
  * 后续所有 dispatch / channel 代码只使用 adapters 上的方法。
@@ -237,7 +237,7 @@ export async function persistAuthConfig(
237
237
  const config: Record<string, unknown> | undefined = runtime.config as any;
238
238
 
239
239
  if (typeof config?.mutateConfigFile !== 'function') {
240
- throw new Error('persistAuthConfig: runtime.config.mutateConfigFile is unavailable (requires openclaw >= 2026.9.2)');
240
+ throw new Error('persistAuthConfig: runtime.config.mutateConfigFile is unavailable (requires openclaw >= 2026.9.1)');
241
241
  }
242
242
  await (config.mutateConfigFile as Function)({
243
243
  mutate: () => cfg,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Setup 工具 re-export(plugin-sdk/setup + plugin-sdk/setup-tools)
3
3
  *
4
- * 构建基线 openclaw 2026.9.2,两个 subpath 均为正式导出,直接转发。
4
+ * 构建基线 openclaw 2026.9.1,两个 subpath 均为正式导出,直接转发。
5
5
  */
6
6
  export type { ChannelSetupWizard } from 'openclaw/plugin-sdk/setup';
7
7
 
@@ -97,7 +97,7 @@ export async function dispatchToOpenClaw(
97
97
  dlog?.debug(`received sender=${envelope.senderId} scope=${envelope.chatScope} msgId=${envelope.messageId}`);
98
98
 
99
99
  if (!adapters.inboundRun || !adapters.dispatchReply) {
100
- dlog?.error(`runtime adapter inboundRun/dispatchReply not available (openclaw=${adapters.version}, requires >=2026.9.2)`);
100
+ dlog?.error(`runtime adapter inboundRun/dispatchReply not available (openclaw=${adapters.version}, requires >=2026.9.1)`);
101
101
  return;
102
102
  }
103
103
 
@@ -74,7 +74,7 @@ let questionRuntimePromise: Promise<QuestionGatewayRuntime> | undefined;
74
74
 
75
75
  /**
76
76
  * question gateway runtime(openclaw/plugin-sdk/question-gateway-runtime,
77
- * 2026.8.1+ 稳定导出,peer 已要求 >=2026.9.2)。
77
+ * 2026.8.1+ 稳定导出,peer 已要求 >=2026.9.1)。
78
78
  */
79
79
  export function getQuestionGatewayRuntime(): Promise<QuestionGatewayRuntime> {
80
80
  questionRuntimePromise ??= import('openclaw/plugin-sdk/question-gateway-runtime')
@@ -77,6 +77,32 @@ declare module "openclaw/plugin-sdk" {
77
77
  execute: (toolCallId: string, params: unknown) => Promise<AgentToolResult> | AgentToolResult;
78
78
  }
79
79
 
80
+ /**
81
+ * 工具工厂上下文(registerTool 工厂形式)
82
+ *
83
+ * 框架在装配当前 run 的工具面(或缓存描述符路径下的每次执行)时调用
84
+ * 工厂,ctx 携带 run 级会话信息——其中 deliveryContext 是框架按当前
85
+ * run 的 sessionCtx 派生的可信投递路由(2026-09-23 修复:延迟/排队
86
+ * turn 中 ALS 请求上下文失效时,工具由此回退解析会话目标)。
87
+ * 仅声明本项目实际使用的字段,其余字段经索引签名透传。
88
+ */
89
+ export interface PluginToolFactoryContext {
90
+ config?: OpenClawConfig;
91
+ runtimeConfig?: OpenClawConfig;
92
+ agentId?: string;
93
+ sessionKey?: string;
94
+ sessionId?: string;
95
+ messageChannel?: string;
96
+ agentAccountId?: string;
97
+ /** 可信投递路由:{ channel, to, accountId, threadId? } */
98
+ deliveryContext?: { channel?: string; to?: string; accountId?: string; threadId?: string };
99
+ /** 当前入站 turn 的平台原生会话 ID */
100
+ nativeChannelId?: string;
101
+ /** 当前入站 turn 的发送者 ID(运行时提供,非工具参数) */
102
+ requesterSenderId?: string;
103
+ [key: string]: unknown;
104
+ }
105
+
80
106
  /**
81
107
  * Tool 注册选项
82
108
  */
@@ -103,8 +129,18 @@ declare module "openclaw/plugin-sdk" {
103
129
  };
104
130
  /** 注册频道 */
105
131
  registerChannel<TAccount = unknown>(options: { plugin: ChannelPlugin<TAccount> }): void;
106
- /** 注册工具 */
107
- registerTool(tool: AnyAgentTool, opts?: ToolRegistrationOptions): void;
132
+ /**
133
+ * 注册工具。支持两种形式:
134
+ * - 静态定义(AnyAgentTool)
135
+ * - 工厂函数:每次工具装配/执行时由框架调用,ctx 携带 run 级
136
+ * deliveryContext 等会话信息(推荐需要感知当前会话的工具使用)
137
+ */
138
+ registerTool(
139
+ tool:
140
+ | AnyAgentTool
141
+ | ((ctx: PluginToolFactoryContext) => AnyAgentTool | AnyAgentTool[] | null | undefined),
142
+ opts?: ToolRegistrationOptions,
143
+ ): void;
108
144
  /** 其他 API 方法 */
109
145
  [key: string]: unknown;
110
146
  }
@@ -536,7 +572,7 @@ declare module "openclaw/plugin-sdk" {
536
572
  }
537
573
 
538
574
  // 2026.8.1-beta 起 openclaw 发布包明确排除该子路径的 .d.ts(files 列表
539
- // "!dist/plugin-sdk/text-utility-runtime.d.ts"),2026.9.2 仍未恢复,运行时导出仍在。
575
+ // "!dist/plugin-sdk/text-utility-runtime.d.ts"),2026.9.1/2026.9.2 均未恢复,运行时导出仍在。
540
576
  // 仅声明本项目实际使用的函数(签名对齐 dist/utf16-slice-*.js)。
541
577
  declare module "openclaw/plugin-sdk/text-utility-runtime" {
542
578
  export function truncateUtf16Safe(input: string, maxLen: number): string;
@@ -1,8 +1,8 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
2
  import { listQQBotAccountIds } from '../config.js';
3
- import { getBotForAccount } from '../bot-instance.js';
4
- import { getRequestAccountId } from '../request-context.js';
3
+ import { resolveGatewayForSend } from '../outbound/outbound-service.js';
5
4
  import { createPluginLogger } from '../utils/plugin-logger.js';
5
+ import { resolveToolSessionRoute, type ToolDeliveryContext } from './tool-session.js';
6
6
 
7
7
  // 工具 execute 无 logger 参数;request-context 活跃期间 enrichMeta
8
8
  // 自动带上 accountId/openId/messageId
@@ -71,6 +71,8 @@ function validatePath(path: string): string | null {
71
71
  * - 频道(Guild/Channel/Member/Announce/Forum/Schedule)→ 见 qqbot-channel skill
72
72
  * - 群(Group/Member)→ 见 qqbot-group skill
73
73
  */
74
+ // 工厂注册形式(2026-09-23 修复):turn 被框架延迟/排队执行(ALS 失效)时,
75
+ // 从 deliveryContext 回退解析账户(见 tool-session.ts)。
74
76
  export function registerPlatformTool(api: OpenClawPluginApi): void {
75
77
  const cfg = api.config;
76
78
  if (!cfg) return;
@@ -79,78 +81,90 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
79
81
  if (accountIds.length === 0) return;
80
82
 
81
83
  api.registerTool(
82
- {
83
- name: 'qqbot_platform_api',
84
- label: 'QQBot Platform API Gateway',
85
- description:
86
- 'QQ 开放平台统一 HTTP API 网关,自动填充鉴权 Token。' +
87
- '常用接口速查:' +
88
- '【频道】GET /users/@me/guilds | /guilds/{guild_id}/channels | /channels/{channel_id} | ' +
89
- '【群】GET /v2/groups/{group_id}/bot_state | /v2/groups/{group_id}/members/{member_id} | /v2/groups/{group_id}/info。' +
90
- '更多接口和参数详情请阅读 qqbot-channel 和 qqbot-group skill。',
91
- parameters: PlatformApiSchema,
92
- async execute(_toolCallId, params) {
93
- const p = params as {
94
- method: string;
95
- path: string;
96
- body?: Record<string, unknown>;
97
- query?: Record<string, string>;
98
- };
99
-
100
- if (!p.method) return json({ error: 'method 为必填参数' });
101
- if (!p.path) return json({ error: 'path 为必填参数' });
102
-
103
- const method = p.method.toUpperCase();
104
- if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
105
- return json({ error: `不支持的 HTTP 方法: ${method}` });
106
- }
107
-
108
- const pathError = validatePath(p.path);
109
- if (pathError) return json({ error: pathError });
110
-
111
- const accountId = getRequestAccountId();
112
- if (!accountId) {
113
- return json({ error: '无法获取当前请求的账号信息,此工具仅支持在消息会话中使用' });
114
- }
115
-
116
- try {
117
- const bot = getBotForAccount(accountId);
118
- const apiGateway = bot.api;
119
- const startedAt = Date.now();
120
-
121
- let data: unknown;
122
- switch (method) {
123
- case 'GET':
124
- data = await apiGateway.get(p.path, p.query);
125
- break;
126
- case 'POST':
127
- data = await apiGateway.post(p.path, p.body);
128
- break;
129
- case 'PUT':
130
- data = await apiGateway.put(p.path, p.body);
131
- break;
132
- case 'PATCH':
133
- data = await apiGateway.patch(p.path, p.body);
134
- break;
135
- case 'DELETE':
136
- data = await apiGateway.delete(p.path);
137
- break;
84
+ (factoryCtx) => {
85
+ const delivery = (factoryCtx as { deliveryContext?: ToolDeliveryContext }).deliveryContext;
86
+ return {
87
+ name: 'qqbot_platform_api',
88
+ label: 'QQBot Platform API Gateway',
89
+ description:
90
+ 'QQ 开放平台统一 HTTP API 网关,自动填充鉴权 Token。' +
91
+ '常用接口速查:' +
92
+ '【频道】GET /users/@me/guilds | /guilds/{guild_id}/channels | /channels/{channel_id} | ' +
93
+ '【群】GET /v2/groups/{group_id}/bot_state | /v2/groups/{group_id}/members/{member_id} | /v2/groups/{group_id}/info。' +
94
+ '更多接口和参数详情请阅读 qqbot-channel 和 qqbot-group skill。',
95
+ parameters: PlatformApiSchema,
96
+ async execute(_toolCallId: string, params: unknown) {
97
+ const p = params as {
98
+ method: string;
99
+ path: string;
100
+ body?: Record<string, unknown>;
101
+ query?: Record<string, string>;
102
+ };
103
+
104
+ if (!p.method) return json({ error: 'method 为必填参数' });
105
+ if (!p.path) return json({ error: 'path 为必填参数' });
106
+
107
+ const method = p.method.toUpperCase();
108
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
109
+ return json({ error: `不支持的 HTTP 方法: ${method}` });
110
+ }
111
+
112
+ const pathError = validatePath(p.path);
113
+ if (pathError) return json({ error: pathError });
114
+
115
+ // 会话路由:ALS 优先,deliveryContext 回退(延迟/排队 turn)
116
+ const route = resolveToolSessionRoute(delivery);
117
+ if (!route) {
118
+ return json({
119
+ error:
120
+ '当前运行没有 QQ 会话来源(可能由定时任务或框架内部触发的 run),' +
121
+ '无法确定调用哪个 QQ Bot 账户。请在 QQ 消息会话中使用本工具。',
122
+ });
138
123
  }
139
124
 
140
- toolLog.info(`${method} ${p.path} ok (${Date.now() - startedAt}ms)`, { accountId });
141
- return json(data);
142
- } catch (err: unknown) {
143
- const errMsg = err instanceof Error ? err.message : String(err);
144
- const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
145
- toolLog.warn(`${method} ${p.path} failed status=${apiErr.httpStatus ?? '?'} code=${apiErr.bizCode ?? '?'}: ${errMsg}`, { accountId });
146
- return json({
147
- error: errMsg,
148
- status: apiErr.httpStatus,
149
- code: apiErr.bizCode,
150
- path: p.path,
151
- });
152
- }
153
- },
125
+ try {
126
+ const resolvedGateway = resolveGatewayForSend(route.accountId ?? 'default');
127
+ if (!resolvedGateway) {
128
+ return json({ error: `账户 ${route.accountId ?? 'default'} 的 gateway 尚未启动` });
129
+ }
130
+ const bot = resolvedGateway.gw.bot;
131
+ const apiGateway = bot.api;
132
+ const startedAt = Date.now();
133
+
134
+ let data: unknown;
135
+ switch (method) {
136
+ case 'GET':
137
+ data = await apiGateway.get(p.path, p.query);
138
+ break;
139
+ case 'POST':
140
+ data = await apiGateway.post(p.path, p.body);
141
+ break;
142
+ case 'PUT':
143
+ data = await apiGateway.put(p.path, p.body);
144
+ break;
145
+ case 'PATCH':
146
+ data = await apiGateway.patch(p.path, p.body);
147
+ break;
148
+ case 'DELETE':
149
+ data = await apiGateway.delete(p.path);
150
+ break;
151
+ }
152
+
153
+ toolLog.info(`${method} ${p.path} ok (${Date.now() - startedAt}ms)`, { accountId: resolvedGateway.accountId });
154
+ return json(data);
155
+ } catch (err: unknown) {
156
+ const errMsg = err instanceof Error ? err.message : String(err);
157
+ const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
158
+ toolLog.warn(`${method} ${p.path} failed status=${apiErr.httpStatus ?? '?'} code=${apiErr.bizCode ?? '?'}: ${errMsg}`, { accountId: route.accountId ?? 'default' });
159
+ return json({
160
+ error: errMsg,
161
+ status: apiErr.httpStatus,
162
+ code: apiErr.bizCode,
163
+ path: p.path,
164
+ });
165
+ }
166
+ },
167
+ };
154
168
  },
155
169
  { name: 'qqbot_platform_api' },
156
170
  );
@@ -1,5 +1,5 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { getRequestTarget, getRequestAccountId } from "../request-context.js";
2
+ import { resolveToolSessionRoute, type ToolDeliveryContext } from "./tool-session.js";
3
3
 
4
4
  // ========== 类型定义 ==========
5
5
 
@@ -218,52 +218,62 @@ function formatDelay(ms: number): string {
218
218
 
219
219
  // ========== 注册入口 ==========
220
220
 
221
+ // 工厂注册形式(2026-09-23 修复):turn 被框架延迟/排队执行(ALS 失效)时,
222
+ // 从 deliveryContext 回退解析投递目标(见 tool-session.ts)。
221
223
  export function registerRemindTool(api: OpenClawPluginApi): void {
222
224
  api.registerTool(
223
- {
224
- name: "qqbot_remind",
225
- label: "QQBot 定时提醒",
226
- description:
227
- "创建、查询、删除 QQ 定时提醒。" +
228
- "使用简单参数即可,无需手动构造 cron JSON。\n" +
229
- "创建提醒:action=add, content=提醒内容, to=目标地址, time=时间\n" +
230
- "查看提醒:action=list\n" +
231
- "删除提醒:action=remove, jobId=任务ID(先 list 获取)\n" +
232
- "时间格式示例:\"5m\"(5分钟后) \"1h\"(1小时后) \"0 8 * * *\"(每天8点)",
233
- parameters: RemindSchema,
234
- execute(_toolCallId, params) {
235
- const p = params as RemindParams;
225
+ (factoryCtx) => {
226
+ const delivery = (factoryCtx as { deliveryContext?: ToolDeliveryContext }).deliveryContext;
227
+ return {
228
+ name: "qqbot_remind",
229
+ label: "QQBot 定时提醒",
230
+ description:
231
+ "创建、查询、删除 QQ 定时提醒。" +
232
+ "使用简单参数即可,无需手动构造 cron JSON。\n" +
233
+ "创建提醒:action=add, content=提醒内容, to=目标地址, time=时间\n" +
234
+ "查看提醒:action=list\n" +
235
+ "删除提醒:action=remove, jobId=任务ID(先 list 获取)\n" +
236
+ "时间格式示例:\"5m\"(5分钟后) \"1h\"(1小时后) \"0 8 * * *\"(每天8点)",
237
+ parameters: RemindSchema,
238
+ async execute(_toolCallId: string, params: unknown) {
239
+ const p = params as RemindParams;
236
240
 
237
- // ===== list =====
238
- if (p.action === "list") {
239
- return json({
240
- _instruction: "请立即使用 cron 工具执行以下参数",
241
- cronParams: { action: "list" },
242
- });
243
- }
241
+ // ===== list =====
242
+ if (p.action === "list") {
243
+ return json({
244
+ _instruction: "请立即使用 cron 工具执行以下参数",
245
+ cronParams: { action: "list" },
246
+ });
247
+ }
244
248
 
245
- // ===== remove =====
246
- if (p.action === "remove") {
247
- if (!p.jobId) {
248
- return json({ error: "action=remove 时 jobId 为必填参数。请先用 action=list 获取 jobId。" });
249
+ // ===== remove =====
250
+ if (p.action === "remove") {
251
+ if (!p.jobId) {
252
+ return json({ error: "action=remove 时 jobId 为必填参数。请先用 action=list 获取 jobId。" });
253
+ }
254
+ return json({
255
+ _instruction: "请立即使用 cron 工具执行以下参数",
256
+ cronParams: { action: "remove", jobId: p.jobId },
257
+ });
249
258
  }
250
- return json({
251
- _instruction: "请立即使用 cron 工具执行以下参数",
252
- cronParams: { action: "remove", jobId: p.jobId },
253
- });
254
- }
255
259
 
256
- // ===== add =====
257
- if (!p.content) {
258
- return json({ error: "action=add 时 content(提醒内容)为必填参数" });
259
- }
260
- // 优先使用 AI 传入的 to,否则自动从请求级上下文获取(AsyncLocalStorage)
261
- const resolvedTo = p.to || getRequestTarget();
262
- if (!resolvedTo) {
263
- return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
264
- }
265
- // 从请求上下文获取当前账户 ID(多账户场景),fallback 到 "default"
266
- const resolvedAccountId = getRequestAccountId() || "default";
260
+ // ===== add =====
261
+ if (!p.content) {
262
+ return json({ error: "action=add 时 content(提醒内容)为必填参数" });
263
+ }
264
+ // 优先使用 AI 传入的 to;否则双层解析当前会话路由:
265
+ // ALS 请求级上下文优先,框架 run 级 deliveryContext 回退
266
+ const route = resolveToolSessionRoute(delivery);
267
+ const resolvedTo = p.to || route?.target;
268
+ if (!resolvedTo) {
269
+ return json({
270
+ error:
271
+ "action=add 时无法确定投递目标地址(当前 run 无 QQ 会话来源)。" +
272
+ '请显式传入 to(格式:qqbot:c2c:user_openid 或 qqbot:group:group_openid)后重试。',
273
+ });
274
+ }
275
+ // 账户 ID:会话路由优先,fallback 到 "default"
276
+ const resolvedAccountId = route?.accountId || "default";
267
277
  if (!p.time) {
268
278
  return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
269
279
  }
@@ -299,7 +309,8 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
299
309
  cronParams: onceJob,
300
310
  summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
301
311
  });
302
- },
312
+ },
313
+ };
303
314
  },
304
315
  { name: "qqbot_remind" },
305
316
  );