@ryantest/openclaw-qqbot 1.6.6-alpha.1 → 1.6.6-alpha.3

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.
@@ -1,4 +1,4 @@
1
- import { type ChannelPlugin } from "openclaw/plugin-sdk";
1
+ import { type ChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import type { ResolvedQQBotAccount } from "./types.js";
3
3
  /** QQ Bot 单条消息文本长度上限 */
4
4
  export declare const TEXT_CHUNK_LIMIT = 5000;
@@ -1,4 +1,4 @@
1
- import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk";
1
+ import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/core";
2
2
  import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId } from "./config.js";
3
3
  import { sendText, sendMedia } from "./outbound.js";
4
4
  import { startGateway } from "./gateway.js";
@@ -40,6 +40,7 @@ export const qqbotPlugin = {
40
40
  },
41
41
  reload: { configPrefixes: ["channels.qqbot"] },
42
42
  // CLI onboarding wizard
43
+ // @ts-expect-error onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
43
44
  onboarding: qqbotOnboardingAdapter,
44
45
  config: {
45
46
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
@@ -76,7 +77,7 @@ export const qqbotPlugin = {
76
77
  }),
77
78
  // 关键:解析 allowFrom 配置,用于命令授权
78
79
  resolveAllowFrom: ({ cfg, accountId }) => {
79
- const account = resolveQQBotAccount(cfg, accountId);
80
+ const account = resolveQQBotAccount(cfg, accountId ?? undefined);
80
81
  const allowFrom = account.config?.allowFrom ?? [];
81
82
  console.log(`[qqbot] resolveAllowFrom: accountId=${accountId}, allowFrom=${JSON.stringify(allowFrom)}`);
82
83
  return allowFrom.map((entry) => String(entry));
@@ -201,20 +202,21 @@ export const qqbotPlugin = {
201
202
  sendText: async ({ to, text, accountId, replyToId, cfg }) => {
202
203
  console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
203
204
  console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
204
- const account = resolveQQBotAccount(cfg, accountId);
205
+ const account = resolveQQBotAccount(cfg, accountId ?? undefined);
205
206
  initApiConfig({ markdownSupport: account.markdownSupport });
206
207
  console.log(`[qqbot:channel] sendText resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
207
208
  const result = await sendText({ to, text, accountId, replyToId, account });
208
209
  console.log(`[qqbot:channel] sendText result: messageId=${result.messageId}, error=${result.error ?? "none"}`);
210
+ if (result.error)
211
+ throw new Error(result.error);
209
212
  return {
210
213
  channel: "qqbot",
211
- messageId: result.messageId,
212
- error: result.error ? new Error(result.error) : undefined,
214
+ messageId: result.messageId ?? "",
213
215
  };
214
216
  },
215
217
  sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
216
218
  console.log(`[qqbot:channel] sendMedia called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, mediaUrl=${mediaUrl?.slice(0, 80)}, text.length=${text?.length ?? 0}`);
217
- const account = resolveQQBotAccount(cfg, accountId);
219
+ const account = resolveQQBotAccount(cfg, accountId ?? undefined);
218
220
  initApiConfig({ markdownSupport: account.markdownSupport });
219
221
  console.log(`[qqbot:channel] sendMedia resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
220
222
  const result = await sendMedia({ to, text: text ?? "", mediaUrl: mediaUrl ?? "", accountId, replyToId, account });
@@ -231,11 +233,11 @@ export const qqbotPlugin = {
231
233
  catch (fallbackErr) {
232
234
  console.error(`[qqbot:channel] sendMedia fallback text failed: ${fallbackErr}`);
233
235
  }
236
+ throw new Error(result.error);
234
237
  }
235
238
  return {
236
239
  channel: "qqbot",
237
- messageId: result.messageId,
238
- error: result.error ? new Error(result.error) : undefined,
240
+ messageId: result.messageId ?? "",
239
241
  };
240
242
  },
241
243
  },
@@ -356,8 +358,8 @@ export const qqbotPlugin = {
356
358
  enabled: account?.enabled ?? false,
357
359
  configured: Boolean(account?.appId && account?.clientSecret),
358
360
  tokenSource: account?.secretSource,
359
- running: runtime?.running ?? false,
360
- connected: runtime?.connected ?? false,
361
+ running: Boolean(runtime?.running ?? false),
362
+ connected: Boolean(runtime?.connected ?? false),
361
363
  lastConnectedAt: runtime?.lastConnectedAt ?? null,
362
364
  lastError: runtime?.lastError ?? null,
363
365
  lastInboundAt: runtime?.lastInboundAt ?? null,
@@ -586,6 +586,38 @@ function copyScriptToTemp(scriptPath) {
586
586
  return null;
587
587
  }
588
588
  }
589
+ const REMOTE_UPGRADE_SCRIPT_URL = "https://raw.githubusercontent.com/tencent-connect/openclaw-qqbot/main/scripts/upgrade-via-npm.sh";
590
+ const REMOTE_UPGRADE_SCRIPT_URL_WIN = "https://raw.githubusercontent.com/tencent-connect/openclaw-qqbot/main/scripts/upgrade-via-npm.ps1";
591
+ /**
592
+ * 从远端下载升级脚本到临时目录,返回临时脚本路径,失败返回 null。
593
+ */
594
+ function downloadRemoteUpgradeScript() {
595
+ try {
596
+ const url = isWindows() ? REMOTE_UPGRADE_SCRIPT_URL_WIN : REMOTE_UPGRADE_SCRIPT_URL;
597
+ const ext = isWindows() ? ".ps1" : ".sh";
598
+ const tmpDir = path.join(getHomeDir(), ".openclaw", ".qqbot-upgrade-tmp");
599
+ fs.mkdirSync(tmpDir, { recursive: true });
600
+ const tmpScript = path.join(tmpDir, `upgrade-via-npm${ext}`);
601
+ // 使用 curl 同步下载(macOS/Linux/Windows 均内置 curl)
602
+ execFileSync("curl", ["-fsSL", "--max-time", "15", "-o", tmpScript, url], {
603
+ timeout: 20_000,
604
+ stdio: "pipe",
605
+ });
606
+ if (!fs.existsSync(tmpScript) || fs.statSync(tmpScript).size < 100) {
607
+ console.error(`[qqbot] downloadRemoteUpgradeScript: downloaded file too small or missing`);
608
+ return null;
609
+ }
610
+ if (!isWindows()) {
611
+ fs.chmodSync(tmpScript, 0o755);
612
+ }
613
+ console.log(`[qqbot] downloadRemoteUpgradeScript: fetched from ${url} → ${tmpScript}`);
614
+ return tmpScript;
615
+ }
616
+ catch (e) {
617
+ console.error(`[qqbot] downloadRemoteUpgradeScript: failed: ${e.message}`);
618
+ return null;
619
+ }
620
+ }
589
621
  /**
590
622
  * 清理临时升级脚本目录
591
623
  */
@@ -611,11 +643,16 @@ function cleanupTempScript() {
611
643
  * 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
612
644
  */
613
645
  function fireHotUpgrade(targetVersion) {
614
- const originalScriptPath = getUpgradeScriptPath();
615
- if (!originalScriptPath)
646
+ // 优先从远端下载升级脚本,避免使用本地可能过时的版本
647
+ const scriptPath = downloadRemoteUpgradeScript() || (() => {
648
+ const local = getUpgradeScriptPath();
649
+ if (!local)
650
+ return null;
651
+ console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
652
+ return copyScriptToTemp(local) || local;
653
+ })();
654
+ if (!scriptPath)
616
655
  return { ok: false, reason: "no-script" };
617
- // 将脚本复制到临时位置,避免升级过程中脚本被删除
618
- const scriptPath = copyScriptToTemp(originalScriptPath) || originalScriptPath;
619
656
  const cli = findCli();
620
657
  if (!cli)
621
658
  return { ok: false, reason: "no-cli" };
@@ -642,7 +679,7 @@ function fireHotUpgrade(targetVersion) {
642
679
  shell = bash;
643
680
  shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
644
681
  }
645
- console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath} (original: ${originalScriptPath}), cli=${cli}, target=${targetVersion || "latest"}`);
682
+ console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
646
683
  // 异步执行升级脚本
647
684
  execFile(shell, shellArgs, {
648
685
  timeout: 120_000,
@@ -28,7 +28,7 @@ export interface ChunkedUploadProgress {
28
28
  export interface ChunkedUploadOptions {
29
29
  /** 进度回调 */
30
30
  onProgress?: (progress: ChunkedUploadProgress) => void;
31
- /** 最大并发数(默认 5) */
31
+ /** 最大并发数(默认 2) */
32
32
  maxConcurrent?: number;
33
33
  /** 日志前缀 */
34
34
  logPrefix?: string;
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
17
17
  import { c2cUploadPrepare, c2cUploadPartFinish, c2cCompleteUpload, groupUploadPrepare, groupUploadPartFinish, groupCompleteUpload, getAccessToken, } from "../api.js";
18
18
  import { formatFileSize } from "./file-utils.js";
19
19
  /** 分片上传并发控制:最多同时上传 N 个分片 */
20
- const MAX_CONCURRENT_PARTS = 5;
20
+ const MAX_CONCURRENT_PARTS = 1;
21
21
  /** 单个分片上传超时(毫秒)— 5 分钟,兼容低带宽场景 */
22
22
  const PART_UPLOAD_TIMEOUT = 300_000;
23
23
  /** 单个分片上传最大重试次数 */
@@ -5,7 +5,7 @@
5
5
  export declare const UPLOAD_SIZE_LIMITS: Record<number, number>;
6
6
  /** 获取文件类型的中文名称;未知类型返回 "文件" */
7
7
  export declare function getFileTypeName(fileType: number): string;
8
- /** 获取指定文件类型的上传大小限制;未知类型默认 200MB */
8
+ /** 获取指定文件类型的上传大小限制;未知类型默认 100MB */
9
9
  export declare function getMaxUploadSize(fileType: number): number;
10
10
  /** @deprecated 使用 getMaxUploadSize(fileType) 代替 */
11
11
  export declare const MAX_UPLOAD_SIZE: number;
@@ -7,9 +7,9 @@ import crypto from "node:crypto";
7
7
  /** QQ Bot API 各类型文件上传大小限制(QQ 机器人上行) */
8
8
  export const UPLOAD_SIZE_LIMITS = {
9
9
  1: 30 * 1024 * 1024, // IMAGE: 30MB
10
- 2: 200 * 1024 * 1024, // VIDEO: 200MB
10
+ 2: 100 * 1024 * 1024, // VIDEO: 100MB
11
11
  3: 20 * 1024 * 1024, // VOICE: 20MB
12
- 4: 200 * 1024 * 1024, // FILE: 200MB
12
+ 4: 100 * 1024 * 1024, // FILE: 100MB
13
13
  };
14
14
  /** 文件类型中文名映射 */
15
15
  const FILE_TYPE_NAMES = {
@@ -22,12 +22,12 @@ const FILE_TYPE_NAMES = {
22
22
  export function getFileTypeName(fileType) {
23
23
  return FILE_TYPE_NAMES[fileType] ?? "文件";
24
24
  }
25
- /** 获取指定文件类型的上传大小限制;未知类型默认 200MB */
25
+ /** 获取指定文件类型的上传大小限制;未知类型默认 100MB */
26
26
  export function getMaxUploadSize(fileType) {
27
- return UPLOAD_SIZE_LIMITS[fileType] ?? 200 * 1024 * 1024;
27
+ return UPLOAD_SIZE_LIMITS[fileType] ?? 100 * 1024 * 1024;
28
28
  }
29
29
  /** @deprecated 使用 getMaxUploadSize(fileType) 代替 */
30
- export const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
30
+ export const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
31
31
  /** 大文件阈值(超过此值发送进度提示):5MB */
32
32
  export const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024;
33
33
  /**
package/index.ts CHANGED
@@ -13,7 +13,7 @@ const plugin = {
13
13
  configSchema: emptyPluginConfigSchema(),
14
14
  register(api: OpenClawPluginApi) {
15
15
  setQQBotRuntime(api.runtime);
16
- api.registerChannel({ plugin: qqbotPlugin });
16
+ api.registerChannel({ plugin: qqbotPlugin as any });
17
17
  registerChannelTool(api);
18
18
  registerRemindTool(api);
19
19
  },
@@ -1,22 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const createWebSocketStream = require('./lib/stream');
4
- const extension = require('./lib/extension');
5
- const PerMessageDeflate = require('./lib/permessage-deflate');
6
- const Receiver = require('./lib/receiver');
7
- const Sender = require('./lib/sender');
8
- const subprotocol = require('./lib/subprotocol');
9
3
  const WebSocket = require('./lib/websocket');
10
- const WebSocketServer = require('./lib/websocket-server');
11
4
 
12
- WebSocket.createWebSocketStream = createWebSocketStream;
13
- WebSocket.extension = extension;
14
- WebSocket.PerMessageDeflate = PerMessageDeflate;
15
- WebSocket.Receiver = Receiver;
16
- WebSocket.Sender = Sender;
17
- WebSocket.Server = WebSocketServer;
18
- WebSocket.subprotocol = subprotocol;
5
+ WebSocket.createWebSocketStream = require('./lib/stream');
6
+ WebSocket.Server = require('./lib/websocket-server');
7
+ WebSocket.Receiver = require('./lib/receiver');
8
+ WebSocket.Sender = require('./lib/sender');
9
+
19
10
  WebSocket.WebSocket = WebSocket;
20
- WebSocket.WebSocketServer = WebSocketServer;
11
+ WebSocket.WebSocketServer = WebSocket.Server;
21
12
 
22
13
  module.exports = WebSocket;
@@ -37,9 +37,6 @@ class PerMessageDeflate {
37
37
  * acknowledge disabling of client context takeover
38
38
  * @param {Number} [options.concurrencyLimit=10] The number of concurrent
39
39
  * calls to zlib
40
- * @param {Boolean} [options.isServer=false] Create the instance in either
41
- * server or client mode
42
- * @param {Number} [options.maxPayload=0] The maximum allowed message length
43
40
  * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
44
41
  * use of a custom server window size
45
42
  * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
@@ -50,13 +47,16 @@ class PerMessageDeflate {
50
47
  * deflate
51
48
  * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
52
49
  * inflate
50
+ * @param {Boolean} [isServer=false] Create the instance in either server or
51
+ * client mode
52
+ * @param {Number} [maxPayload=0] The maximum allowed message length
53
53
  */
54
- constructor(options) {
54
+ constructor(options, isServer, maxPayload) {
55
+ this._maxPayload = maxPayload | 0;
55
56
  this._options = options || {};
56
57
  this._threshold =
57
58
  this._options.threshold !== undefined ? this._options.threshold : 1024;
58
- this._maxPayload = this._options.maxPayload | 0;
59
- this._isServer = !!this._options.isServer;
59
+ this._isServer = !!isServer;
60
60
  this._deflate = null;
61
61
  this._inflate = null;
62
62
 
@@ -293,11 +293,11 @@ class WebSocketServer extends EventEmitter {
293
293
  this.options.perMessageDeflate &&
294
294
  secWebSocketExtensions !== undefined
295
295
  ) {
296
- const perMessageDeflate = new PerMessageDeflate({
297
- ...this.options.perMessageDeflate,
298
- isServer: true,
299
- maxPayload: this.options.maxPayload
300
- });
296
+ const perMessageDeflate = new PerMessageDeflate(
297
+ this.options.perMessageDeflate,
298
+ true,
299
+ this.options.maxPayload
300
+ );
301
301
 
302
302
  try {
303
303
  const offers = extension.parse(secWebSocketExtensions);
@@ -693,7 +693,7 @@ function initAsClient(websocket, address, protocols, options) {
693
693
  } else {
694
694
  try {
695
695
  parsedUrl = new URL(address);
696
- } catch {
696
+ } catch (e) {
697
697
  throw new SyntaxError(`Invalid URL: ${address}`);
698
698
  }
699
699
  }
@@ -755,11 +755,11 @@ function initAsClient(websocket, address, protocols, options) {
755
755
  opts.timeout = opts.handshakeTimeout;
756
756
 
757
757
  if (opts.perMessageDeflate) {
758
- perMessageDeflate = new PerMessageDeflate({
759
- ...opts.perMessageDeflate,
760
- isServer: false,
761
- maxPayload: opts.maxPayload
762
- });
758
+ perMessageDeflate = new PerMessageDeflate(
759
+ opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
760
+ false,
761
+ opts.maxPayload
762
+ );
763
763
  opts.headers['Sec-WebSocket-Extensions'] = format({
764
764
  [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
765
765
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ws",
3
- "version": "8.20.0",
3
+ "version": "8.19.0",
4
4
  "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
5
  "keywords": [
6
6
  "HyBi",
@@ -55,13 +55,12 @@
55
55
  }
56
56
  },
57
57
  "devDependencies": {
58
- "@eslint/js": "^10.0.1",
59
58
  "benchmark": "^2.1.4",
60
59
  "bufferutil": "^4.0.1",
61
- "eslint": "^10.0.1",
60
+ "eslint": "^9.0.0",
62
61
  "eslint-config-prettier": "^10.0.1",
63
62
  "eslint-plugin-prettier": "^5.0.0",
64
- "globals": "^17.0.0",
63
+ "globals": "^16.0.0",
65
64
  "mocha": "^8.4.0",
66
65
  "nyc": "^15.0.0",
67
66
  "prettier": "^3.0.0",
@@ -1,21 +1,8 @@
1
1
  import createWebSocketStream from './lib/stream.js';
2
- import extension from './lib/extension.js';
3
- import PerMessageDeflate from './lib/permessage-deflate.js';
4
2
  import Receiver from './lib/receiver.js';
5
3
  import Sender from './lib/sender.js';
6
- import subprotocol from './lib/subprotocol.js';
7
4
  import WebSocket from './lib/websocket.js';
8
5
  import WebSocketServer from './lib/websocket-server.js';
9
6
 
10
- export {
11
- createWebSocketStream,
12
- extension,
13
- PerMessageDeflate,
14
- Receiver,
15
- Sender,
16
- subprotocol,
17
- WebSocket,
18
- WebSocketServer
19
- };
20
-
7
+ export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
21
8
  export default WebSocket;
@@ -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
- "extensions": ["./dist/index.js"],
6
+ "extensions": ["./preload.cjs"],
7
7
  "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
8
8
  "capabilities": {
9
9
  "proactiveMessaging": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryantest/openclaw-qqbot",
3
- "version": "1.6.6-alpha.1",
3
+ "version": "1.6.6-alpha.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,13 +15,14 @@
15
15
  "skills",
16
16
  "scripts",
17
17
  "index.ts",
18
+ "preload.cjs",
18
19
  "tsconfig.json",
19
20
  "openclaw.plugin.json"
20
21
  ],
21
22
  "openclaw": {
22
23
  "id": "openclaw-qqbot",
23
24
  "extensions": [
24
- "./dist/index.js"
25
+ "./preload.cjs"
25
26
  ],
26
27
  "channel": {
27
28
  "id": "qqbot",
@@ -30,6 +31,7 @@
30
31
  },
31
32
  "scripts": {
32
33
  "build": "tsc || true",
34
+ "postbuild": "node -e \"const fs=require('fs'),p=require('path'),ext=p.join(require('os').homedir(),'.openclaw/extensions/openclaw-qqbot');if(fs.existsSync(ext)&&!fs.lstatSync(ext).isSymbolicLink()){const d=p.join(ext,'dist'),pr=p.join(ext,'preload.cjs');fs.cpSync('dist',d,{recursive:true});fs.copyFileSync('preload.cjs',pr);console.log('[postbuild] synced to',ext)}\"",
33
35
  "dev": "tsc --watch",
34
36
  "prepack": "npm install --omit=dev",
35
37
  "postinstall": "node scripts/postinstall-link-sdk.js 2>/dev/null || true"
package/preload.cjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 插件预加载入口(CJS 格式)。
3
+ *
4
+ * openclaw 框架通过 require() 加载插件,因此需要 .cjs 后缀
5
+ * 确保在 "type": "module" 的 package 中也能被正确 require()。
6
+ *
7
+ * 在 require 真正的插件代码(依赖 openclaw/plugin-sdk)之前,
8
+ * 先同步确保 node_modules/openclaw symlink 存在。
9
+ */
10
+ "use strict";
11
+
12
+ const { ensurePluginSdkSymlink } = require("./scripts/link-sdk-core.cjs");
13
+
14
+ // 1) 同步创建 symlink
15
+ ensurePluginSdkSymlink(__dirname, "[preload]");
16
+
17
+ // 2) Node 22 原生支持 CJS require() 加载 ESM 模块
18
+ // 同步加载插件入口,确保框架同步检查 register/activate 时能找到
19
+ const _pluginModule = require("./dist/index.js");
20
+
21
+ // 3) 展平 default export:框架检查 register/activate 在顶级属性
22
+ // ESM 的 export default 在 require() 后变成 { default: plugin, ... }
23
+ const _default = _pluginModule.default;
24
+ const merged = Object.assign({}, _pluginModule);
25
+ if (_default && typeof _default === "object") {
26
+ for (const key of Object.keys(_default)) {
27
+ if (!(key in merged)) {
28
+ merged[key] = _default[key];
29
+ }
30
+ }
31
+ }
32
+
33
+ module.exports = merged;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * 公共模块:openclaw plugin-sdk symlink 创建逻辑。
3
+ *
4
+ * 被 preload.cjs 和 postinstall-link-sdk.js 共同使用,避免代码重复。
5
+ * 必须是 CJS 格式,因为 preload.cjs 需要同步 require()。
6
+ */
7
+ "use strict";
8
+
9
+ const path = require("node:path");
10
+ const fs = require("node:fs");
11
+ const { execSync } = require("node:child_process");
12
+
13
+ const CLI_NAMES = ["openclaw", "clawdbot", "moltbot"];
14
+
15
+ /**
16
+ * 比较版本号是否 >= target
17
+ * Strip pre-release suffix (e.g. "2026.3.23-2" → "2026.3.23")
18
+ */
19
+ function compareVersionGte(version, target) {
20
+ const parts = version.replace(/-.*$/, "").split(".").map(Number);
21
+ for (let i = 0; i < target.length; i++) {
22
+ const v = parts[i] || 0;
23
+ const t = target[i];
24
+ if (v > t) return true;
25
+ if (v < t) return false;
26
+ }
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * 检查 openclaw 版本是否 >= 2026.3.22(需要 symlink 的最低版本)。
32
+ * 如果无法检测版本,返回 true(保守策略:宁可多创建也不遗漏)。
33
+ */
34
+ function isOpenclawVersionRequiresSymlink() {
35
+ const REQUIRED = [2026, 3, 22];
36
+
37
+ // Strategy 1: 从全局 openclaw 的 package.json 读取版本
38
+ try {
39
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
40
+ for (const name of CLI_NAMES) {
41
+ const pkgPath = path.join(globalRoot, name, "package.json");
42
+ if (fs.existsSync(pkgPath)) {
43
+ const v = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
44
+ if (v) return compareVersionGte(v, REQUIRED);
45
+ }
46
+ }
47
+ } catch {}
48
+
49
+ // Strategy 2: 从 CLI 命令获取版本
50
+ for (const name of CLI_NAMES) {
51
+ try {
52
+ const out = execSync(`${name} --version`, {
53
+ encoding: "utf-8",
54
+ timeout: 5000,
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ }).trim();
57
+ const m = out.match(/(\d+\.\d+\.\d+)/);
58
+ if (m) return compareVersionGte(m[1], REQUIRED);
59
+ } catch {}
60
+ }
61
+
62
+ return true;
63
+ }
64
+
65
+ /**
66
+ * 查找全局 openclaw 安装路径。
67
+ * 三种策略依次尝试:npm root -g、which <cli>、从 extensions 目录推断。
68
+ */
69
+ function findOpenclawRoot(pluginRoot) {
70
+ // Strategy 1: npm root -g
71
+ try {
72
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
73
+ for (const name of CLI_NAMES) {
74
+ const candidate = path.join(globalRoot, name);
75
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
76
+ }
77
+ } catch {}
78
+
79
+ // Strategy 2: which <cli>
80
+ const whichCmd = process.platform === "win32" ? "where" : "which";
81
+ for (const name of CLI_NAMES) {
82
+ try {
83
+ const bin = execSync(`${whichCmd} ${name}`, {
84
+ encoding: "utf-8",
85
+ timeout: 5000,
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ }).trim().split("\n")[0];
88
+ if (!bin) continue;
89
+ const realBin = fs.realpathSync(bin);
90
+ const c1 = path.resolve(path.dirname(realBin), "..", "lib", "node_modules", name);
91
+ if (fs.existsSync(path.join(c1, "package.json"))) return c1;
92
+ const c2 = path.resolve(path.dirname(realBin), "..");
93
+ if (fs.existsSync(path.join(c2, "package.json")) && fs.existsSync(path.join(c2, "plugin-sdk"))) return c2;
94
+ } catch {}
95
+ }
96
+
97
+ // Strategy 3: 从 extensions 目录推断
98
+ const extensionsDir = path.dirname(pluginRoot);
99
+ const dataDir = path.dirname(extensionsDir);
100
+ const dataDirName = path.basename(dataDir);
101
+ const cliName = dataDirName.replace(/^\./, "");
102
+ if (cliName) {
103
+ try {
104
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
105
+ const candidate = path.join(globalRoot, cliName);
106
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
107
+ } catch {}
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * 验证现有 node_modules/openclaw 是否完整可用。
115
+ *
116
+ * openclaw plugins install 可能安装了不完整的 peerDep 副本
117
+ * (只有 dist/plugin-sdk/index.js,缺少 core.js 等子模块),覆盖了之前的 symlink。
118
+ *
119
+ * 判断标准:
120
+ * - symlink → 只需确认 dist/plugin-sdk 目录存在(target 有完整文件树)
121
+ * - 真实目录 → 必须检查 dist/plugin-sdk/core.js 是否存在
122
+ */
123
+ function isLinkValid(linkTarget) {
124
+ try {
125
+ const stat = fs.lstatSync(linkTarget);
126
+ if (stat.isSymbolicLink()) {
127
+ return fs.existsSync(path.join(linkTarget, "dist", "plugin-sdk"))
128
+ || fs.existsSync(path.join(linkTarget, "plugin-sdk"));
129
+ }
130
+ // 真实目录
131
+ return fs.existsSync(path.join(linkTarget, "dist", "plugin-sdk", "core.js"));
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * 确保 plugin-sdk symlink 存在。
139
+ *
140
+ * @param {string} pluginRoot - 插件根目录路径
141
+ * @param {string} [tag="[link-sdk]"] - 日志前缀
142
+ * @returns {boolean} true 如果 symlink 已存在或成功创建
143
+ */
144
+ function ensurePluginSdkSymlink(pluginRoot, tag) {
145
+ tag = tag || "[link-sdk]";
146
+ try {
147
+ if (!pluginRoot.includes("extensions")) return true;
148
+
149
+ const linkTarget = path.join(pluginRoot, "node_modules", "openclaw");
150
+
151
+ if (fs.existsSync(linkTarget)) {
152
+ if (isLinkValid(linkTarget)) return true;
153
+ // 无效/不完整 → 删除后重建
154
+ try {
155
+ fs.rmSync(linkTarget, { recursive: true, force: true });
156
+ console.log(`${tag} removed incomplete node_modules/openclaw`);
157
+ } catch {}
158
+ }
159
+
160
+ if (!isOpenclawVersionRequiresSymlink()) return true;
161
+
162
+ const openclawRoot = findOpenclawRoot(pluginRoot);
163
+ if (!openclawRoot) {
164
+ console.error(`${tag} WARNING: could not find openclaw global installation, symlink not created`);
165
+ return false;
166
+ }
167
+
168
+ fs.mkdirSync(path.join(pluginRoot, "node_modules"), { recursive: true });
169
+ fs.symlinkSync(openclawRoot, linkTarget, "junction");
170
+ console.log(`${tag} symlink created: node_modules/openclaw -> ${openclawRoot}`);
171
+ return true;
172
+ } catch (e) {
173
+ console.error(`${tag} WARNING: symlink check failed: ${e.message || e}`);
174
+ return false;
175
+ }
176
+ }
177
+
178
+ module.exports = {
179
+ CLI_NAMES,
180
+ compareVersionGte,
181
+ isOpenclawVersionRequiresSymlink,
182
+ findOpenclawRoot,
183
+ isLinkValid,
184
+ ensurePluginSdkSymlink,
185
+ };