@jerryliang122/openclaw-qqbot 1.0.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.
Files changed (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +967 -0
  3. package/README.zh.md +790 -0
  4. package/dist/index.cjs +18072 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +1222 -0
  7. package/index.ts +92 -0
  8. package/openclaw.plugin.json +38 -0
  9. package/package.json +69 -0
  10. package/preload.cjs +19 -0
  11. package/scripts/link-sdk-core.cjs +268 -0
  12. package/scripts/proactive-api-server.ts +369 -0
  13. package/scripts/send-proactive.ts +293 -0
  14. package/scripts/test-sendmedia.ts +116 -0
  15. package/skills/qqbot-channel/SKILL.md +285 -0
  16. package/skills/qqbot-channel/references/api_references.md +521 -0
  17. package/skills/qqbot-remind/SKILL.md +159 -0
  18. package/skills/qqbot-upgrade/SKILL.md +56 -0
  19. package/src/adapter/contract.ts +63 -0
  20. package/src/adapter/lint.ts +144 -0
  21. package/src/adapter/media.ts +40 -0
  22. package/src/adapter/pairing.ts +95 -0
  23. package/src/adapter/resolve.ts +255 -0
  24. package/src/adapter/setup.ts +13 -0
  25. package/src/adapter/webhook.ts +248 -0
  26. package/src/adapter/workspace.ts +21 -0
  27. package/src/agent-prompt-adapter.ts +26 -0
  28. package/src/bot-instance.ts +60 -0
  29. package/src/channel.ts +230 -0
  30. package/src/commands/bot-approve.ts +143 -0
  31. package/src/commands/bot-clear-storage.ts +114 -0
  32. package/src/commands/bot-group-always.ts +62 -0
  33. package/src/commands/bot-group-info.ts +48 -0
  34. package/src/commands/bot-help.ts +40 -0
  35. package/src/commands/bot-logs.ts +248 -0
  36. package/src/commands/bot-me.ts +18 -0
  37. package/src/commands/bot-pairing.ts +50 -0
  38. package/src/commands/bot-ping.ts +33 -0
  39. package/src/commands/bot-streaming.ts +55 -0
  40. package/src/commands/bot-upgrade.ts +56 -0
  41. package/src/commands/bot-version.ts +41 -0
  42. package/src/commands/config-util.ts +96 -0
  43. package/src/commands/index.ts +51 -0
  44. package/src/config.ts +403 -0
  45. package/src/constants.ts +6 -0
  46. package/src/dispatch/body-assembler.ts +308 -0
  47. package/src/dispatch/ctx-builder.ts +127 -0
  48. package/src/dispatch/dispatch.ts +667 -0
  49. package/src/dispatch/envelope-builder.ts +112 -0
  50. package/src/dispatch/index.ts +2 -0
  51. package/src/features/approval-capability.ts +302 -0
  52. package/src/features/approval-helpers.ts +271 -0
  53. package/src/features/approval-utils.ts +21 -0
  54. package/src/features/command-panel.ts +301 -0
  55. package/src/features/credential-backup.ts +74 -0
  56. package/src/features/group-mode-store.ts +79 -0
  57. package/src/features/history-store.ts +75 -0
  58. package/src/features/msgid-cache.ts +55 -0
  59. package/src/features/outbound-echo-store.ts +46 -0
  60. package/src/features/proactive-budget.ts +57 -0
  61. package/src/features/proactive.ts +549 -0
  62. package/src/features/question-helpers.ts +771 -0
  63. package/src/features/quota-manager.ts +173 -0
  64. package/src/features/ref-index-store.ts +289 -0
  65. package/src/features/secret-input-store.ts +118 -0
  66. package/src/features/secret-store-cli.ts +324 -0
  67. package/src/features/typing-refresh.ts +51 -0
  68. package/src/features/update-checker.ts +166 -0
  69. package/src/gateway/event-handlers.ts +456 -0
  70. package/src/gateway/index.ts +3 -0
  71. package/src/gateway/lifecycle.ts +236 -0
  72. package/src/gateway/middleware-setup.ts +173 -0
  73. package/src/gateway/qqbot-gateway.ts +458 -0
  74. package/src/gateway-adapter.ts +44 -0
  75. package/src/heartbeat-adapter.ts +57 -0
  76. package/src/message-adapter.ts +40 -0
  77. package/src/messaging-adapter.ts +78 -0
  78. package/src/middleware/access-control.ts +125 -0
  79. package/src/middleware/attachment.ts +373 -0
  80. package/src/middleware/inbound-guard.ts +102 -0
  81. package/src/middleware/policy-injector.ts +71 -0
  82. package/src/middleware/secret-capture.ts +161 -0
  83. package/src/middleware/typing.ts +110 -0
  84. package/src/openclaw-plugin-sdk.d.ts +543 -0
  85. package/src/outbound/chunker.ts +80 -0
  86. package/src/outbound/debounce.ts +102 -0
  87. package/src/outbound/deliver-pipeline.ts +235 -0
  88. package/src/outbound/index.ts +3 -0
  89. package/src/outbound/local-file-router.ts +145 -0
  90. package/src/outbound/media-send.ts +408 -0
  91. package/src/outbound/outbound-service.ts +298 -0
  92. package/src/outbound/reply-limiter.ts +139 -0
  93. package/src/outbound/sanitize.ts +32 -0
  94. package/src/outbound/streaming-controller.ts +332 -0
  95. package/src/outbound/target.ts +109 -0
  96. package/src/outbound-adapter.ts +323 -0
  97. package/src/plugin-base.ts +42 -0
  98. package/src/request-context.ts +50 -0
  99. package/src/runtime.ts +42 -0
  100. package/src/setup/account-key.ts +41 -0
  101. package/src/setup/finalize.ts +110 -0
  102. package/src/setup/login.ts +197 -0
  103. package/src/setup/surface.ts +40 -0
  104. package/src/status-adapter.ts +56 -0
  105. package/src/tools/platform.ts +149 -0
  106. package/src/tools/remind.ts +308 -0
  107. package/src/tools/secret-input.ts +185 -0
  108. package/src/types-augment.d.ts +54 -0
  109. package/src/types-plugin.ts +82 -0
  110. package/src/types.ts +620 -0
  111. package/src/typing-lifecycle.ts +182 -0
  112. package/src/utils/mention.ts +52 -0
  113. package/src/utils/pkg-version.ts +23 -0
  114. package/src/utils/platform.ts +459 -0
  115. package/src/utils/plugin-logger.ts +104 -0
  116. package/src/utils/ssrf-guard.ts +132 -0
  117. package/src/utils/stt.ts +150 -0
  118. package/src/utils/voice-text.ts +61 -0
  119. package/tsconfig.json +17 -0
  120. package/tsup.config.ts +64 -0
package/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @tencent-connect/openclaw-qqbot
3
+ *
4
+ * 独立版 QQ Bot 通道插件 — 基于 @tencent-connect/qqbot-nodejs SDK 重构。
5
+ *
6
+ * 直接依赖:
7
+ * - openclaw/plugin-sdk:OpenClaw 插件框架
8
+ * - @tencent-connect/qqbot-nodejs:QQ 开放平台 Node.js SDK
9
+ */
10
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
11
+ import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk/core';
12
+
13
+ import { qqbotPlugin } from './src/channel.js';
14
+ import { setQQBotRuntime } from './src/runtime.js';
15
+ import { registerPlatformTool } from './src/tools/platform.js';
16
+ import { registerRemindTool } from './src/tools/remind.js';
17
+ import { registerSecretInputTool } from './src/tools/secret-input.js';
18
+ import { verifyRuntimeContract } from './src/adapter/contract.js';
19
+
20
+ let registered = false;
21
+
22
+ const plugin = {
23
+ id: 'openclaw-qqbot',
24
+ name: 'QQ Bot',
25
+ description: 'QQ Bot channel plugin',
26
+ configSchema: emptyPluginConfigSchema(),
27
+ register(api: OpenClawPluginApi) {
28
+ setQQBotRuntime(api.runtime);
29
+
30
+ if (!registered) {
31
+ registered = true;
32
+ const contract = verifyRuntimeContract(api.runtime);
33
+ if (!contract.ok) {
34
+ throw new Error(
35
+ `openclaw-qqbot incompatible with openclaw ${contract.version}: ` +
36
+ `missing required APIs: ${contract.missing.join(', ')}. ` +
37
+ `Please upgrade openclaw or downgrade the plugin.`,
38
+ );
39
+ }
40
+ }
41
+
42
+ api.registerChannel({ plugin: qqbotPlugin as any });
43
+ registerPlatformTool(api);
44
+ registerRemindTool(api);
45
+ registerSecretInputTool(api);
46
+ },
47
+ };
48
+
49
+ export default plugin;
50
+
51
+ // ── Public API exports ──
52
+
53
+ export { qqbotPlugin } from './src/channel.js';
54
+ export { setQQBotRuntime, getQQBotRuntime } from './src/runtime.js';
55
+ export { getBotForAccount, tryGetBotForAccount, buildUserAgent } from './src/bot-instance.js';
56
+ export { QQBotGateway } from './src/gateway/index.js';
57
+ export { sendText, sendMedia } from './src/outbound/outbound-service.js';
58
+ export { parseTarget } from './src/outbound/target.js';
59
+ export { dispatchToOpenClaw } from './src/dispatch/index.js';
60
+ export {
61
+ PersistedRefIndexStore,
62
+ getPersistedRefIndexStore,
63
+ flushAllRefIndexStores,
64
+ } from './src/features/ref-index-store.js';
65
+ export {
66
+ StreamingController,
67
+ shouldUseStreaming,
68
+ } from './src/outbound/streaming-controller.js';
69
+ export * from './src/types.js';
70
+ export * from './src/config.js';
71
+ export { qqbotChannelOutbound } from './src/outbound-adapter.js';
72
+
73
+ // Quota management
74
+ export { ReplyLimiter } from './src/outbound/reply-limiter.js';
75
+ export {
76
+ checkPassiveReplyQuota,
77
+ inferQQBotScope,
78
+ clearQuotaCache,
79
+ getQuotaStats,
80
+ } from './src/features/quota-manager.js';
81
+
82
+ // Typing lifecycle
83
+ export {
84
+ c2cTypingIndicator,
85
+ resolveTypingIntervalMs,
86
+ MIN_TYPING_INTERVAL_MS,
87
+ POST_MESSAGE_REFRESH_DELAY_MS,
88
+ } from './src/middleware/typing.js';
89
+ export {
90
+ subscribeOutboundMessage,
91
+ notifyOutboundMessageSent,
92
+ } from './src/features/typing-refresh.js';
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "openclaw-qqbot",
3
+ "name": "OpenClaw QQ Bot",
4
+ "description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
5
+ "channels": ["qqbot"],
6
+ "extensions": ["./preload.cjs"],
7
+ "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-upgrade"],
8
+ "contracts": {
9
+ "tools": ["qqbot_platform_api", "qqbot_remind", "qqbot_secret_input"]
10
+ },
11
+ "capabilities": {
12
+ "proactiveMessaging": true,
13
+ "cronJobs": true
14
+ },
15
+ "configSchema": {
16
+ "type": "object",
17
+ "additionalProperties": true,
18
+ "properties": {
19
+ "appId": { "type": "string", "description": "QQ Bot AppID" },
20
+ "clientSecret": { "type": "string", "description": "QQ Bot AppSecret" }
21
+ }
22
+ },
23
+ "channelConfigs": {
24
+ "qqbot": {
25
+ "schema": {
26
+ "type": "object",
27
+ "additionalProperties": true,
28
+ "properties": {
29
+ "appId": { "type": "string", "description": "QQ Bot AppID" },
30
+ "clientSecret": { "type": "string", "description": "QQ Bot AppSecret" }
31
+ }
32
+ },
33
+ "label": "QQ Bot",
34
+ "description": "QQ Bot channel configuration with message support, cron jobs, and proactive messaging.",
35
+ "preferOver": ["qqbot"]
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@jerryliang122/openclaw-qqbot",
3
+ "version": "1.0.0",
4
+ "description": "QQ Bot channel plugin for OpenClaw (independently maintained fork)",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.cjs",
11
+ "types": "dist/index.d.cts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.cts",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "skills",
22
+ "scripts",
23
+ "index.ts",
24
+ "preload.cjs",
25
+ "tsconfig.json",
26
+ "tsup.config.ts",
27
+ "openclaw.plugin.json"
28
+ ],
29
+ "openclaw": {
30
+ "id": "openclaw-qqbot",
31
+ "extensions": [
32
+ "./preload.cjs"
33
+ ],
34
+ "channel": {
35
+ "id": "qqbot",
36
+ "label": "QQ Bot"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint:runtime": "npx tsx src/adapter/lint.ts"
44
+ },
45
+ "dependencies": {
46
+ "@tencent-connect/qqbot-connector": "1.2.0",
47
+ "@tencent-connect/qqbot-nodejs": "^1.0.4"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.19.43",
51
+ "openclaw": "2026.9.2",
52
+ "tsup": "^8.5.1",
53
+ "typescript": "^5.9.3",
54
+ "tsx": "^4.23.13"
55
+ },
56
+ "peerDependencies": {
57
+ "@tencent-connect/qqbot-connector": "1.2.0",
58
+ "openclaw": ">=2026.9.2"
59
+ },
60
+ "homepage": "https://github.com/jerryliang122/qqbot-openclaw",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/jerryliang122/qqbot-openclaw.git"
64
+ },
65
+ "allowScripts": {
66
+ "esbuild@0.27.7": true,
67
+ "esbuild@0.28.2": true
68
+ }
69
+ }
package/preload.cjs ADDED
@@ -0,0 +1,19 @@
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 path = require("node:path");
13
+ const { ensurePluginSdkSymlink } = require("./scripts/link-sdk-core.cjs");
14
+
15
+ // 1) 同步创建 symlink(确保 openclaw/plugin-sdk 可解析)
16
+ ensurePluginSdkSymlink(__dirname, "[preload]");
17
+
18
+ // 2) 加载编译产物(tsup CJS 输出)
19
+ module.exports = require(path.join(__dirname, "dist", "index.cjs"));
@@ -0,0 +1,268 @@
1
+ /**
2
+ * 公共模块:openclaw plugin-sdk symlink 创建逻辑。
3
+ *
4
+ * 被 preload.cjs 和 postinstall-link-sdk.js 共同使用,避免代码重复。
5
+ * 必须是 CJS 格式,因为 preload.cjs 需要同步 require()。
6
+ *
7
+ * 注意:本模块不使用 child_process,避免被 openclaw 安全检查拦截。
8
+ */
9
+ "use strict";
10
+
11
+ const path = require("node:path");
12
+ const fs = require("node:fs");
13
+ const os = require("node:os");
14
+
15
+ const CLI_NAMES = ["openclaw"];
16
+
17
+ /**
18
+ * 比较版本号是否 >= target
19
+ * Strip pre-release suffix (e.g. "2026.3.23-2" → "2026.3.23")
20
+ */
21
+ function compareVersionGte(version, target) {
22
+ const parts = version.replace(/-.*$/, "").split(".").map(Number);
23
+ for (let i = 0; i < target.length; i++) {
24
+ const v = parts[i] || 0;
25
+ const t = target[i];
26
+ if (v > t) return true;
27
+ if (v < t) return false;
28
+ }
29
+ return true;
30
+ }
31
+
32
+ /**
33
+ * 获取全局 node_modules 候选目录列表(按优先级排序)。
34
+ * 不依赖 npm/child_process,通过常用路径推导。
35
+ */
36
+ function getGlobalNodeModulesDirs() {
37
+ const dirs = [];
38
+ const npmPrefix = process.env.npm_config_prefix || process.env.PREFIX;
39
+ if (npmPrefix) {
40
+ // macOS: prefix/lib/node_modules; Windows: prefix/node_modules
41
+ dirs.push(path.join(npmPrefix, "lib", "node_modules"));
42
+ dirs.push(path.join(npmPrefix, "node_modules"));
43
+ }
44
+
45
+ // nvm / fnm / volta 等版本管理器
46
+ // process.execPath = .../versions/node/vX.Y.Z/bin/node
47
+ const execBase = path.dirname(path.dirname(process.execPath));
48
+ dirs.push(path.join(execBase, "lib", "node_modules"));
49
+ dirs.push(path.join(execBase, "node_modules"));
50
+
51
+ // Homebrew (macOS Intel)
52
+ dirs.push("/usr/local/lib/node_modules");
53
+ // Homebrew (macOS Apple Silicon)
54
+ dirs.push("/opt/homebrew/lib/node_modules");
55
+ // Linux / macOS 标准路径
56
+ dirs.push("/usr/lib/node_modules");
57
+ // pnpm global (XDG)
58
+ const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
59
+ dirs.push(path.join(xdgData, "pnpm", "global", "5", "node_modules"));
60
+ dirs.push(path.join(xdgData, "npm", "lib", "node_modules"));
61
+
62
+ return dirs;
63
+ }
64
+
65
+ /**
66
+ * 在全局 node_modules 目录中查找指定包的 package.json。
67
+ */
68
+ function findPackageInGlobalDirs(packageName) {
69
+ for (const dir of getGlobalNodeModulesDirs()) {
70
+ const pkgPath = path.join(dir, packageName, "package.json");
71
+ try {
72
+ if (fs.existsSync(pkgPath)) return pkgPath;
73
+ } catch {}
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * 在 $PATH 中查找 CLI 可执行文件。
80
+ */
81
+ function findCliInPath(cliName) {
82
+ const pathEnv = process.env.PATH || "";
83
+ const ext = process.platform === "win32" ? ".cmd" : "";
84
+ const dirs = pathEnv.split(path.delimiter);
85
+
86
+ for (const dir of dirs) {
87
+ const binPath = path.join(dir, cliName + ext);
88
+ try {
89
+ if (fs.existsSync(binPath)) return fs.realpathSync(binPath);
90
+ } catch {}
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * 从 CLI 二进制路径反查全局 node_modules 中的 package.json。
97
+ */
98
+ function resolveGlobalRootFromBin(binPath, cliName) {
99
+ // 方式 A: bin -> ../lib/node_modules/<cli>/package.json
100
+ const binDir = path.dirname(binPath);
101
+ const c1 = path.resolve(binDir, "..", "lib", "node_modules", cliName, "package.json");
102
+ try { if (fs.existsSync(c1)) return c1; } catch {}
103
+
104
+ // 方式 B: bin -> ../package.json (如果在 node_modules/.bin 下)
105
+ const c2 = path.resolve(binDir, "..", "package.json");
106
+ try {
107
+ if (fs.existsSync(c2)) {
108
+ const parentDir = path.dirname(c2);
109
+ if (fs.existsSync(path.join(parentDir, "plugin-sdk")) || fs.existsSync(path.join(parentDir, "dist", "plugin-sdk"))) {
110
+ return c2;
111
+ }
112
+ }
113
+ } catch {}
114
+
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * 读取 package.json 中的版本号。
120
+ */
121
+ function readPkgVersion(pkgPath) {
122
+ try {
123
+ const v = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
124
+ return v || null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 检查 openclaw 版本是否 >= 2026.3.22(需要 symlink 的最低版本)。
132
+ * 如果无法检测版本,返回 true(保守策略:宁可多创建也不遗漏)。
133
+ */
134
+ function isOpenclawVersionRequiresSymlink() {
135
+ const REQUIRED = [2026, 3, 22];
136
+
137
+ // Strategy 1: 从全局 node_modules 读取 package.json 版本
138
+ for (const name of CLI_NAMES) {
139
+ const pkgPath = findPackageInGlobalDirs(name);
140
+ if (pkgPath) {
141
+ const v = readPkgVersion(pkgPath);
142
+ if (v) return compareVersionGte(v, REQUIRED);
143
+ }
144
+ }
145
+
146
+ // Strategy 2: 从 CLI 二进制反查到 package.json
147
+ for (const name of CLI_NAMES) {
148
+ const binPath = findCliInPath(name);
149
+ if (binPath) {
150
+ const pkgPath = resolveGlobalRootFromBin(binPath, name);
151
+ if (pkgPath) {
152
+ const v = readPkgVersion(pkgPath);
153
+ if (v) return compareVersionGte(v, REQUIRED);
154
+ }
155
+ }
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * 查找全局 openclaw 安装路径。
163
+ * 三种策略依次尝试:全局 node_modules、$PATH 反查、extensions 目录推断。
164
+ */
165
+ function findOpenclawRoot(pluginRoot) {
166
+ // Strategy 1: 从全局 node_modules 查找
167
+ for (const name of CLI_NAMES) {
168
+ const pkgPath = findPackageInGlobalDirs(name);
169
+ if (pkgPath) return path.dirname(pkgPath);
170
+ }
171
+
172
+ // Strategy 2: 从 $PATH 反查
173
+ for (const name of CLI_NAMES) {
174
+ const binPath = findCliInPath(name);
175
+ if (binPath) {
176
+ const pkgPath = resolveGlobalRootFromBin(binPath, name);
177
+ if (pkgPath) return path.dirname(pkgPath);
178
+ }
179
+ }
180
+
181
+ // Strategy 3: 从 extensions 目录推断
182
+ const extensionsDir = path.dirname(pluginRoot);
183
+ const dataDir = path.dirname(extensionsDir);
184
+ const dataDirName = path.basename(dataDir);
185
+ const cliName = dataDirName.replace(/^\./, "");
186
+ if (cliName) {
187
+ for (const dir of getGlobalNodeModulesDirs()) {
188
+ const pkgPath = path.join(dir, cliName, "package.json");
189
+ try { if (fs.existsSync(pkgPath)) return path.dirname(pkgPath); } catch {}
190
+ }
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * 验证现有 node_modules/openclaw 是否完整可用。
198
+ *
199
+ * openclaw plugins install 可能安装了不完整的 peerDep 副本
200
+ * (只有 dist/plugin-sdk/index.js,缺少 core.js 等子模块),覆盖了之前的 symlink。
201
+ *
202
+ * 判断标准:
203
+ * - symlink → 只需确认 dist/plugin-sdk 目录存在(target 有完整文件树)
204
+ * - 真实目录 → 必须检查 dist/plugin-sdk/core.js 是否存在
205
+ */
206
+ function isLinkValid(linkTarget) {
207
+ try {
208
+ const stat = fs.lstatSync(linkTarget);
209
+ if (stat.isSymbolicLink()) {
210
+ return fs.existsSync(path.join(linkTarget, "dist", "plugin-sdk"))
211
+ || fs.existsSync(path.join(linkTarget, "plugin-sdk"));
212
+ }
213
+ // 真实目录
214
+ return fs.existsSync(path.join(linkTarget, "dist", "plugin-sdk", "core.js"));
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 确保 plugin-sdk symlink 存在。
222
+ *
223
+ * @param {string} pluginRoot - 插件根目录路径
224
+ * @param {string} [tag="[link-sdk]"] - 日志前缀
225
+ * @returns {boolean} true 如果 symlink 已存在或成功创建
226
+ */
227
+ function ensurePluginSdkSymlink(pluginRoot, tag) {
228
+ tag = tag || "[link-sdk]";
229
+ try {
230
+ if (!pluginRoot.includes("extensions")) return true;
231
+
232
+ const linkTarget = path.join(pluginRoot, "node_modules", "openclaw");
233
+
234
+ if (fs.existsSync(linkTarget)) {
235
+ if (isLinkValid(linkTarget)) return true;
236
+ // 无效/不完整 → 删除后重建
237
+ try {
238
+ fs.rmSync(linkTarget, { recursive: true, force: true });
239
+ console.log(`${tag} removed incomplete node_modules/openclaw`);
240
+ } catch {}
241
+ }
242
+
243
+ if (!isOpenclawVersionRequiresSymlink()) return true;
244
+
245
+ const openclawRoot = findOpenclawRoot(pluginRoot);
246
+ if (!openclawRoot) {
247
+ console.error(`${tag} WARNING: could not find openclaw global installation, symlink not created`);
248
+ return false;
249
+ }
250
+
251
+ fs.mkdirSync(path.join(pluginRoot, "node_modules"), { recursive: true });
252
+ fs.symlinkSync(openclawRoot, linkTarget, "junction");
253
+ console.log(`${tag} symlink created: node_modules/openclaw -> ${openclawRoot}`);
254
+ return true;
255
+ } catch (e) {
256
+ console.error(`${tag} WARNING: symlink check failed: ${e.message || e}`);
257
+ return false;
258
+ }
259
+ }
260
+
261
+ module.exports = {
262
+ CLI_NAMES,
263
+ compareVersionGte,
264
+ isOpenclawVersionRequiresSymlink,
265
+ findOpenclawRoot,
266
+ isLinkValid,
267
+ ensurePluginSdkSymlink,
268
+ };