@tencent-connect/openclaw-qqbot 1.6.0-alpha.1 → 1.6.0-alpha.qqchannel

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.
@@ -3,7 +3,7 @@
3
3
  "name": "OpenClaw QQ Bot",
4
4
  "description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
5
5
  "channels": ["qqbot"],
6
- "skills": ["skills/qqbot-cron", "skills/qqbot-media"],
6
+ "skills": ["skills/qqbot-channel", "skills/qqbot-cron", "skills/qqbot-media"],
7
7
  "capabilities": {
8
8
  "proactiveMessaging": true,
9
9
  "cronJobs": true
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
2
  import { qqbotPlugin } from "./src/channel.js";
3
3
  import { setQQBotRuntime } from "./src/runtime.js";
4
+ import { registerChannelTool } from "./src/tools/channel.js";
4
5
  const plugin = {
5
6
  id: "openclaw-qqbot",
6
7
  name: "QQ Bot",
@@ -9,6 +10,7 @@ const plugin = {
9
10
  register(api) {
10
11
  setQQBotRuntime(api.runtime);
11
12
  api.registerChannel({ plugin: qqbotPlugin });
13
+ registerChannelTool(api);
12
14
  },
13
15
  };
14
16
  export default plugin;
package/dist/src/api.d.ts CHANGED
@@ -81,6 +81,15 @@ export declare function sendChannelMessage(accessToken: string, channelId: strin
81
81
  id: string;
82
82
  timestamp: string;
83
83
  }>;
84
+ /**
85
+ * 发送频道私信消息
86
+ * @param guildId - 私信会话的 guild_id(由 DIRECT_MESSAGE_CREATE 事件提供)
87
+ * @param msgId - 被动回复时必填
88
+ */
89
+ export declare function sendDmMessage(accessToken: string, guildId: string, content: string, msgId?: string): Promise<{
90
+ id: string;
91
+ timestamp: string;
92
+ }>;
84
93
  export declare function sendGroupMessage(accessToken: string, groupOpenid: string, content: string, msgId?: string): Promise<MessageResponse>;
85
94
  export declare function sendProactiveC2CMessage(accessToken: string, openid: string, content: string): Promise<MessageResponse>;
86
95
  export declare function sendProactiveGroupMessage(accessToken: string, groupOpenid: string, content: string): Promise<{
package/dist/src/api.js CHANGED
@@ -335,6 +335,17 @@ export async function sendChannelMessage(accessToken, channelId, content, msgId)
335
335
  ...(msgId ? { msg_id: msgId } : {}),
336
336
  });
337
337
  }
338
+ /**
339
+ * 发送频道私信消息
340
+ * @param guildId - 私信会话的 guild_id(由 DIRECT_MESSAGE_CREATE 事件提供)
341
+ * @param msgId - 被动回复时必填
342
+ */
343
+ export async function sendDmMessage(accessToken, guildId, content, msgId) {
344
+ return apiRequest(accessToken, "POST", `/dms/${guildId}/messages`, {
345
+ content,
346
+ ...(msgId ? { msg_id: msgId } : {}),
347
+ });
348
+ }
338
349
  export async function sendGroupMessage(accessToken, groupOpenid, content, msgId) {
339
350
  const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
340
351
  const body = buildMessageBody(content, msgId, msgSeq);
@@ -7,6 +7,7 @@ import { recordKnownUser, flushKnownUsers, listKnownUsers } from "./known-users.
7
7
  import { getQQBotRuntime } from "./runtime.js";
8
8
  import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex } from "./ref-index-store.js";
9
9
  import { matchSlashCommand } from "./slash-commands.js";
10
+ import { triggerUpdateCheck } from "./update-checker.js";
10
11
  import { startImageServer, isImageServerRunning, downloadFile } from "./image-server.js";
11
12
  import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "./utils/image-size.js";
12
13
  import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload } from "./utils/payload.js";
@@ -273,6 +274,8 @@ export async function startGateway(ctx) {
273
274
  log?.info(`[qqbot:${account.accountId}] ${w}`);
274
275
  }
275
276
  }
277
+ // 后台版本检查(detached 子进程,零阻塞)
278
+ triggerUpdateCheck(log);
276
279
  // 初始化 API 配置(markdown 支持)
277
280
  initApiConfig({
278
281
  markdownSupport: account.markdownSupport,
@@ -345,6 +348,7 @@ export async function startGateway(ctx) {
345
348
  let isConnecting = false; // 防止并发连接
346
349
  let reconnectTimer = null; // 重连定时器
347
350
  let shouldRefreshToken = false; // 下次连接是否需要刷新 token
351
+ let isFirstReady = true; // 仅 startGateway 后首次 READY 才发送上线通知
348
352
  // ============ P1-2: 尝试从持久化存储恢复 Session ============
349
353
  // 传入当前 appId,如果 appId 已变更(换了机器人),旧 session 自动失效
350
354
  const savedSession = loadSession(account.accountId, account.appId);
@@ -923,7 +927,7 @@ export async function startGateway(ctx) {
923
927
  // TTS 能力声明:仅在启用时告知 AI 可以发语音(与 qqbot-media SKILL.md 互补)
924
928
  // STT 无需声明:转写结果已在动态上下文的 ASR 行中,AI 自然可见
925
929
  if (hasTTS)
926
- staticParts.push("语音合成已启用,可用<qqmedia>发送语音");
930
+ staticParts.push("语音合成已启用,发送媒体格式:<qqmedia>路径</qqmedia>");
927
931
  const staticInstruction = staticParts.join(" | ");
928
932
  // 静态指引作为 systemPrompts 的首项注入
929
933
  systemPrompts.unshift(staticInstruction);
@@ -2190,46 +2194,53 @@ export async function startGateway(ctx) {
2190
2194
  appId: account.appId,
2191
2195
  });
2192
2196
  onReady?.(d);
2193
- // 启动成功后向已知用户发送上线通知(异步,不阻塞主流程)
2194
- (async () => {
2195
- try {
2196
- const greeting = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
2197
- const token = await getAccessToken(account.appId, account.clientSecret);
2198
- const users = listKnownUsers({ accountId: account.accountId, type: "c2c" });
2199
- for (const user of users) {
2200
- try {
2201
- await sendProactiveC2CMessage(token, user.openid, greeting);
2202
- log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
2197
+ // 仅 startGateway 后的首次 READY 才发送上线通知
2198
+ // ws 断线重连(resume 失败后重新 Identify)产生的 READY 不发送
2199
+ if (!isFirstReady) {
2200
+ log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (reconnect READY, not first startup)`);
2201
+ }
2202
+ else {
2203
+ isFirstReady = false;
2204
+ (async () => {
2205
+ try {
2206
+ const greeting = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
2207
+ const token = await getAccessToken(account.appId, account.clientSecret);
2208
+ const users = listKnownUsers({ accountId: account.accountId, type: "c2c" });
2209
+ for (const user of users) {
2210
+ try {
2211
+ await sendProactiveC2CMessage(token, user.openid, greeting);
2212
+ log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
2213
+ }
2214
+ catch (err) {
2215
+ log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
2216
+ }
2217
+ // 避免频率限制
2218
+ await new Promise(r => setTimeout(r, 500));
2203
2219
  }
2204
- catch (err) {
2205
- log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
2220
+ const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
2221
+ // 群组去重(同一群只发一次)
2222
+ const sentGroups = new Set();
2223
+ for (const user of groups) {
2224
+ const gid = user.groupOpenid;
2225
+ if (!gid || sentGroups.has(gid))
2226
+ continue;
2227
+ sentGroups.add(gid);
2228
+ try {
2229
+ await sendProactiveGroupMessage(token, gid, greeting);
2230
+ log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
2231
+ }
2232
+ catch (err) {
2233
+ log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
2234
+ }
2235
+ await new Promise(r => setTimeout(r, 500));
2206
2236
  }
2207
- // 避免频率限制
2208
- await new Promise(r => setTimeout(r, 500));
2237
+ log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
2209
2238
  }
2210
- const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
2211
- // 群组去重(同一群只发一次)
2212
- const sentGroups = new Set();
2213
- for (const user of groups) {
2214
- const gid = user.groupOpenid;
2215
- if (!gid || sentGroups.has(gid))
2216
- continue;
2217
- sentGroups.add(gid);
2218
- try {
2219
- await sendProactiveGroupMessage(token, gid, greeting);
2220
- log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
2221
- }
2222
- catch (err) {
2223
- log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
2224
- }
2225
- await new Promise(r => setTimeout(r, 500));
2239
+ catch (err) {
2240
+ log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
2226
2241
  }
2227
- log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
2228
- }
2229
- catch (err) {
2230
- log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
2231
- }
2232
- })();
2242
+ })();
2243
+ } // end isFirstReady
2233
2244
  }
2234
2245
  else if (t === "RESUMED") {
2235
2246
  log?.info(`[qqbot:${account.accountId}] Session resumed`);
@@ -16,6 +16,7 @@ import { promisify } from "node:util";
16
16
  import path from "node:path";
17
17
  import fs from "node:fs";
18
18
  import { fileURLToPath } from "node:url";
19
+ import { getUpdateInfo, formatUpdateNotice } from "./update-checker.js";
19
20
  const require = createRequire(import.meta.url);
20
21
  const execFileAsync = promisify(execFile);
21
22
  // 读取 package.json 中的版本号
@@ -53,7 +54,13 @@ registerCommand({
53
54
  name: "version",
54
55
  description: "插件版本号",
55
56
  handler: () => {
56
- return `QQBot Plugin v${PLUGIN_VERSION}`;
57
+ const lines = [`QQBot Plugin v${PLUGIN_VERSION}`];
58
+ const info = getUpdateInfo();
59
+ const notice = formatUpdateNotice(info);
60
+ if (notice) {
61
+ lines.push("", notice);
62
+ }
63
+ return lines.join("\n");
57
64
  },
58
65
  });
59
66
  /**
@@ -69,6 +76,12 @@ registerCommand({
69
76
  lines.push(`- \`/${name}\` — ${cmd.description}`);
70
77
  }
71
78
  lines.push(``, `**升级指引**: ${url}`);
79
+ // 如有更新可用,追加提示
80
+ const info = getUpdateInfo();
81
+ const notice = formatUpdateNotice(info);
82
+ if (notice) {
83
+ lines.push("", notice);
84
+ }
72
85
  return lines.join("\n");
73
86
  },
74
87
  });
@@ -0,0 +1,16 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ /**
3
+ * 注册 QQ 频道 API 代理工具。
4
+ *
5
+ * 该工具作为 QQ 开放平台频道 API 的 HTTP 代理,自动处理 Token 鉴权。
6
+ * AI 通过 skill 文档了解各接口的路径、方法和参数,构造请求后由此工具代理发送。
7
+ *
8
+ * 支持的能力:
9
+ * - 频道管理(Guild)
10
+ * - 子频道管理(Channel)
11
+ * - 成员管理(Member)
12
+ * - 公告管理(Announces)
13
+ * - 论坛管理(Forum Thread)
14
+ * - 日程管理(Schedule)
15
+ */
16
+ export declare function registerChannelTool(api: OpenClawPluginApi): void;
@@ -0,0 +1,234 @@
1
+ import { resolveQQBotAccount } from "../config.js";
2
+ import { listQQBotAccountIds } from "../config.js";
3
+ import { getAccessToken } from "../api.js";
4
+ // ========== 常量 ==========
5
+ const API_BASE = "https://api.sgroup.qq.com";
6
+ const DEFAULT_TIMEOUT_MS = 30000;
7
+ // ========== JSON Schema ==========
8
+ const ChannelApiSchema = {
9
+ type: "object",
10
+ properties: {
11
+ method: {
12
+ type: "string",
13
+ description: "HTTP 请求方法。可选值:GET, POST, PUT, PATCH, DELETE",
14
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
15
+ },
16
+ path: {
17
+ type: "string",
18
+ description: "API 路径(不含域名),占位符需替换为实际值。" +
19
+ "示例:/users/@me/guilds, /guilds/{guild_id}/channels, /channels/{channel_id}",
20
+ },
21
+ body: {
22
+ type: "object",
23
+ description: "请求体(JSON),用于 POST/PUT/PATCH 请求。" +
24
+ "GET/DELETE 请求不需要此参数。",
25
+ },
26
+ query: {
27
+ type: "object",
28
+ description: "URL 查询参数(键值对),会拼接到路径后面。" +
29
+ "如 { \"limit\": \"100\", \"after\": \"0\" } 会拼接为 ?limit=100&after=0",
30
+ additionalProperties: { type: "string" },
31
+ },
32
+ },
33
+ required: ["method", "path"],
34
+ };
35
+ // ========== 工具函数 ==========
36
+ function json(data) {
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
39
+ details: data,
40
+ };
41
+ }
42
+ /**
43
+ * 构建完整的请求 URL
44
+ */
45
+ function buildUrl(path, query) {
46
+ let url = `${API_BASE}${path}`;
47
+ if (query && Object.keys(query).length > 0) {
48
+ const params = new URLSearchParams();
49
+ for (const [key, value] of Object.entries(query)) {
50
+ if (value !== undefined && value !== null && value !== "") {
51
+ params.set(key, value);
52
+ }
53
+ }
54
+ const qs = params.toString();
55
+ if (qs) {
56
+ url += `?${qs}`;
57
+ }
58
+ }
59
+ return url;
60
+ }
61
+ /**
62
+ * 校验 path 防止 SSRF
63
+ */
64
+ function validatePath(path) {
65
+ if (!path.startsWith("/")) {
66
+ return "path 必须以 / 开头";
67
+ }
68
+ if (path.includes("..") || path.includes("//")) {
69
+ return "path 不允许包含 .. 或 //";
70
+ }
71
+ // 只允许合法的 URL path 字符
72
+ if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== "/") {
73
+ return "path 包含非法字符";
74
+ }
75
+ return null;
76
+ }
77
+ // ========== 注册入口 ==========
78
+ /**
79
+ * 注册 QQ 频道 API 代理工具。
80
+ *
81
+ * 该工具作为 QQ 开放平台频道 API 的 HTTP 代理,自动处理 Token 鉴权。
82
+ * AI 通过 skill 文档了解各接口的路径、方法和参数,构造请求后由此工具代理发送。
83
+ *
84
+ * 支持的能力:
85
+ * - 频道管理(Guild)
86
+ * - 子频道管理(Channel)
87
+ * - 成员管理(Member)
88
+ * - 公告管理(Announces)
89
+ * - 论坛管理(Forum Thread)
90
+ * - 日程管理(Schedule)
91
+ */
92
+ export function registerChannelTool(api) {
93
+ const cfg = api.config;
94
+ if (!cfg) {
95
+ console.log("[qqbot-channel-api] No config available, skipping");
96
+ return;
97
+ }
98
+ const accountIds = listQQBotAccountIds(cfg);
99
+ if (accountIds.length === 0) {
100
+ console.log("[qqbot-channel-api] No QQBot accounts configured, skipping");
101
+ return;
102
+ }
103
+ const firstAccountId = accountIds[0];
104
+ const account = resolveQQBotAccount(cfg, firstAccountId);
105
+ if (!account.appId || !account.clientSecret) {
106
+ console.log("[qqbot-channel-api] Account not fully configured, skipping");
107
+ return;
108
+ }
109
+ api.registerTool({
110
+ name: "qqbot_channel_api",
111
+ label: "QQBot Channel API",
112
+ description: "QQ 开放平台频道 API HTTP 代理,自动填充鉴权 Token。" +
113
+ "常用接口速查:" +
114
+ "频道列表 GET /users/@me/guilds | " +
115
+ "子频道列表 GET /guilds/{guild_id}/channels | " +
116
+ "子频道详情 GET /channels/{channel_id} | " +
117
+ "创建子频道 POST /guilds/{guild_id}/channels | " +
118
+ "成员列表 GET /guilds/{guild_id}/members?after=0&limit=100 | " +
119
+ "成员详情 GET /guilds/{guild_id}/members/{user_id} | " +
120
+ "帖子列表 GET /channels/{channel_id}/threads | " +
121
+ "发帖 PUT /channels/{channel_id}/threads | " +
122
+ "创建公告 POST /guilds/{guild_id}/announces | " +
123
+ "创建日程 POST /channels/{channel_id}/schedules。" +
124
+ "更多接口和参数详情请阅读 qqbot-channel skill。",
125
+ parameters: ChannelApiSchema,
126
+ async execute(_toolCallId, params) {
127
+ const p = params;
128
+ // 参数校验
129
+ if (!p.method) {
130
+ return json({ error: "method 为必填参数" });
131
+ }
132
+ if (!p.path) {
133
+ return json({ error: "path 为必填参数" });
134
+ }
135
+ const method = p.method.toUpperCase();
136
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
137
+ return json({ error: `不支持的 HTTP 方法: ${method},可选值:GET, POST, PUT, PATCH, DELETE` });
138
+ }
139
+ // 路径安全校验
140
+ const pathError = validatePath(p.path);
141
+ if (pathError) {
142
+ return json({ error: pathError });
143
+ }
144
+ // GET/DELETE 不应有 body
145
+ if ((method === "GET" || method === "DELETE") && p.body && Object.keys(p.body).length > 0) {
146
+ console.log(`[qqbot-channel-api] ${method} request with body, body will be ignored`);
147
+ }
148
+ try {
149
+ // 获取鉴权 Token
150
+ const accessToken = await getAccessToken(account.appId, account.clientSecret);
151
+ // 构建请求
152
+ const url = buildUrl(p.path, p.query);
153
+ const headers = {
154
+ Authorization: `QQBot ${accessToken}`,
155
+ "Content-Type": "application/json",
156
+ };
157
+ const controller = new AbortController();
158
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
159
+ const fetchOptions = {
160
+ method,
161
+ headers,
162
+ signal: controller.signal,
163
+ };
164
+ // POST/PUT/PATCH 才发送 body
165
+ if (p.body && ["POST", "PUT", "PATCH"].includes(method)) {
166
+ fetchOptions.body = JSON.stringify(p.body);
167
+ }
168
+ console.log(`[qqbot-channel-api] >>> ${method} ${url} (timeout: ${DEFAULT_TIMEOUT_MS}ms)`);
169
+ let res;
170
+ try {
171
+ res = await fetch(url, fetchOptions);
172
+ }
173
+ catch (err) {
174
+ clearTimeout(timeoutId);
175
+ if (err instanceof Error && err.name === "AbortError") {
176
+ console.error(`[qqbot-channel-api] <<< Request timeout after ${DEFAULT_TIMEOUT_MS}ms`);
177
+ return json({ error: `请求超时(${DEFAULT_TIMEOUT_MS}ms)`, path: p.path });
178
+ }
179
+ console.error(`[qqbot-channel-api] <<< Network error:`, err);
180
+ return json({
181
+ error: `网络错误: ${err instanceof Error ? err.message : String(err)}`,
182
+ path: p.path,
183
+ });
184
+ }
185
+ finally {
186
+ clearTimeout(timeoutId);
187
+ }
188
+ console.log(`[qqbot-channel-api] <<< Status: ${res.status} ${res.statusText}`);
189
+ // 解析响应
190
+ const rawBody = await res.text();
191
+ // 空响应(如 DELETE 204)
192
+ if (!rawBody || rawBody.trim() === "") {
193
+ if (res.ok) {
194
+ return json({ success: true, status: res.status, path: p.path });
195
+ }
196
+ return json({
197
+ error: `API 返回 ${res.status} ${res.statusText}`,
198
+ status: res.status,
199
+ path: p.path,
200
+ });
201
+ }
202
+ let data;
203
+ try {
204
+ data = JSON.parse(rawBody);
205
+ }
206
+ catch {
207
+ return json({
208
+ error: "响应解析失败",
209
+ status: res.status,
210
+ raw: rawBody.slice(0, 500),
211
+ path: p.path,
212
+ });
213
+ }
214
+ if (!res.ok) {
215
+ const errData = data;
216
+ return json({
217
+ error: errData.message ?? `API 错误 (HTTP ${res.status})`,
218
+ code: errData.code,
219
+ status: res.status,
220
+ path: p.path,
221
+ detail: data,
222
+ });
223
+ }
224
+ return json(data);
225
+ }
226
+ catch (err) {
227
+ const errMsg = err instanceof Error ? err.message : String(err);
228
+ console.error(`[qqbot-channel-api] Error [${method} ${p.path}]: ${errMsg}`);
229
+ return json({ error: errMsg, path: p.path });
230
+ }
231
+ },
232
+ }, { name: "qqbot_channel_api" });
233
+ console.log("[qqbot-channel-api] Registered QQ channel API proxy tool");
234
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 自动更新检查模块(方案 A + B)
3
+ *
4
+ * 灵感来源: update-notifier(npm 生态标准方案)
5
+ *
6
+ * 工作原理:
7
+ * 1. gateway 启动账户时调用 triggerUpdateCheck()
8
+ * 2. 通过 detached 子进程后台查询 npm registry,零阻塞主进程
9
+ * 3. 查询结果写入本地 JSON 缓存文件(~/.openclaw/qqbot/update-check.json)
10
+ * 4. 下次调用 getUpdateInfo() 时读取缓存,对比版本号
11
+ * 5. /version 指令展示更新提示,gateway 启动日志也输出提醒
12
+ *
13
+ * 检查间隔:默认 24 小时(可调),避免频繁网络请求
14
+ */
15
+ /** 对外暴露的更新信息 */
16
+ export interface UpdateInfo {
17
+ /** 当前版本 */
18
+ currentVersion: string;
19
+ /** 最新 stable 版本 */
20
+ latestVersion: string | null;
21
+ /** 最新 alpha 版本(如有) */
22
+ latestAlphaVersion: string | null;
23
+ /** 是否有 stable 更新可用 */
24
+ hasUpdate: boolean;
25
+ /** 是否有 alpha 更新可用(仅当前是 alpha 时才比较) */
26
+ hasAlphaUpdate: boolean;
27
+ /** 最后检查时间 */
28
+ lastCheck: number;
29
+ }
30
+ /**
31
+ * 获取更新信息(从缓存读取,不触发网络请求)
32
+ * 适合在 /version 指令、日志打印等场景调用
33
+ */
34
+ export declare function getUpdateInfo(): UpdateInfo;
35
+ /**
36
+ * 格式化更新提示文本(用于 /version 指令和日志输出)
37
+ */
38
+ export declare function formatUpdateNotice(info: UpdateInfo): string | null;
39
+ /**
40
+ * 触发后台版本检查(非阻塞)
41
+ *
42
+ * - 如果距上次检查不到 CHECK_INTERVAL_MS,跳过
43
+ * - 通过 spawn detached 子进程执行,主进程不等待
44
+ * - 子进程查询 npm registry 后将结果写入缓存文件
45
+ */
46
+ export declare function triggerUpdateCheck(log?: {
47
+ info: (msg: string) => void;
48
+ }): void;
49
+ /**
50
+ * 获取当前插件版本
51
+ */
52
+ export declare function getCurrentVersion(): string;