@tencent-weixin/openclaw-weixin-cli 1.0.3 → 2.0.1

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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # openclaw-weixin-installer
2
+
3
+ OpenClaw 微信消息通道插件的统一安装器。自动检测宿主 OpenClaw 版本,安装兼容的插件版本。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ npx -y @tencent-weixin/openclaw-weixin-cli install
9
+ ```
10
+
11
+ 安装器会自动完成以下步骤:
12
+ 1. 检测本地 `openclaw --version`
13
+ 2. 根据兼容矩阵选择合适的插件版本(dist-tag)
14
+ 3. 调用 `openclaw plugins install` 安装对应版本
15
+ 4. 引导扫码连接微信
16
+ 5. 重启 OpenClaw Gateway
17
+
18
+ **无需手动指定版本号。**
19
+
20
+ ## 兼容矩阵
21
+
22
+ | openclaw-weixin | 支持的 OpenClaw | dist-tag | 说明 |
23
+ |-----------------|---------------------|----------|------------|
24
+ | 1.0.x | >=2026.3.0 <2026.3.22 | `legacy` | 旧宿主维护线 |
25
+ | 2.0.x | >=2026.3.22 | `latest` | 新宿主线 |
26
+
27
+ > 从 2.0.0 开始,插件采用独立 semver 版本号,不再对齐宿主 OpenClaw 版本号。
28
+
29
+ ## 手动安装
30
+
31
+ 如果需要手动指定版本,可以直接使用 openclaw 命令:
32
+
33
+ ```bash
34
+ # 查看当前 OpenClaw 版本
35
+ openclaw --version
36
+
37
+ # 新宿主 (>=2026.3.22):安装 latest 线
38
+ openclaw plugins install @tencent/openclaw-weixin@latest
39
+
40
+ # 旧宿主 (<2026.3.22):安装 legacy 线
41
+ openclaw plugins install @tencent/openclaw-weixin@legacy
42
+ ```
43
+
44
+ ## 运行时版本校验
45
+
46
+ 插件在启动时会自动检查宿主版本兼容性。如果版本不匹配,将立即抛出错误:
47
+
48
+ ```
49
+ [openclaw-weixin] 宿主版本不兼容!
50
+ 当前 OpenClaw 版本: 2026.3.10
51
+ 当前插件支持范围: >=2026.3.22
52
+ 请安装 openclaw-weixin@legacy (1.0.x (旧宿主线))
53
+ 或运行: npx @tencent-weixin/openclaw-weixin-cli install (自动选择兼容版本)
54
+ ```
55
+
56
+ ## 故障排查
57
+
58
+ ### 宿主版本不兼容
59
+
60
+ **症状**:插件启动时报 `宿主版本不兼容` 错误。
61
+
62
+ **解决**:
63
+ ```bash
64
+ # 1. 确认 OpenClaw 版本
65
+ openclaw --version
66
+
67
+ # 2. 用统一安装器重新安装(自动匹配版本)
68
+ npx -y @tencent-weixin/openclaw-weixin-cli install
69
+ ```
70
+
71
+ ### 如何安装 legacy 线
72
+
73
+ ```bash
74
+ # 方式一:统一安装器(自动判断)
75
+ npx -y @tencent-weixin/openclaw-weixin-cli install
76
+
77
+ # 方式二:手动指定 dist-tag
78
+ openclaw plugins install @tencent/openclaw-weixin@legacy
79
+ ```
80
+
81
+ ### 如何查看当前 openclaw 版本
82
+
83
+ ```bash
84
+ openclaw --version
85
+ ```
86
+
87
+ ## 发布策略
88
+
89
+ ### dist-tag 说明
90
+
91
+ | dist-tag | 对应版本线 | 说明 |
92
+ |----------|-----------|------|
93
+ | `latest` | 2.x | 新宿主线,`npm install` / `openclaw plugins install` 默认安装 |
94
+ | `legacy` | 1.x | 旧宿主维护线,需显式指定 `@legacy` |
95
+
96
+ ### 发布命令
97
+
98
+ ```bash
99
+ # 发布新宿主线 (2.x)
100
+ npm run publish:external:latest -- patch
101
+ npm run publish:internal:latest -- patch
102
+
103
+ # 发布旧宿主维护线 (1.x)
104
+ npm run publish:external:legacy -- patch
105
+ npm run publish:internal:legacy -- patch
106
+
107
+ # dry run 预检
108
+ npm run publish:external:latest -- patch --dry-run
109
+ ```
110
+
111
+ ## 开发
112
+
113
+ ```bash
114
+ # 安装开发依赖
115
+ npm install
116
+
117
+ # 本地测试 install 命令
118
+ node cli.mjs install
119
+ ```
package/cli.mjs CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync, spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import {
8
+ COMPAT_MATRIX,
9
+ findCompatEntry,
10
+ formatRange,
11
+ } from "./lib/compat.mjs";
4
12
 
5
13
  const PLUGIN_SPEC = "@tencent-weixin/openclaw-weixin";
6
14
  const CHANNEL_ID = "openclaw-weixin";
@@ -35,6 +43,107 @@ function which(bin) {
35
43
  }
36
44
  }
37
45
 
46
+ // ── version detection ───────────────────────────────────────────────────────
47
+
48
+ function getOpenclawVersion() {
49
+ try {
50
+ const raw = run("openclaw --version");
51
+ const match = raw.match(/(\d+\.\d+\.\d+)/);
52
+ return match ? match[1] : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function selectPluginTag(openclawVersion) {
59
+ const entry = findCompatEntry(openclawVersion);
60
+ if (entry) return entry;
61
+
62
+ error(`当前 OpenClaw 版本 ${openclawVersion} 不在任何已知兼容范围内`);
63
+ console.log("\n 已知兼容矩阵:");
64
+ for (const e of COMPAT_MATRIX) {
65
+ console.log(` ${e.label} → OpenClaw ${formatRange(e.openclawRange)}`);
66
+ }
67
+ console.log();
68
+ return null;
69
+ }
70
+
71
+ // ── symlink ──────────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Resolve the host openclaw package root from the `openclaw` binary.
75
+ * e.g. /Users/x/.nvm/versions/node/v22/lib/node_modules/openclaw
76
+ */
77
+ function resolveHostOpenclawRoot() {
78
+ const bin = which("openclaw");
79
+ if (!bin) return null;
80
+ try {
81
+ // Follow symlinks to the real binary, then go up to the package root.
82
+ const real = fs.realpathSync(bin);
83
+ // binary is at <root>/openclaw.mjs or <root>/dist/index.js etc.
84
+ // Walk up until we find a package.json with name "openclaw".
85
+ let dir = path.dirname(real);
86
+ for (let i = 0; i < 6; i++) {
87
+ try {
88
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
89
+ if (pkg.name === "openclaw") return dir;
90
+ } catch {}
91
+ const parent = path.dirname(dir);
92
+ if (parent === dir) break;
93
+ dir = parent;
94
+ }
95
+ } catch {}
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Resolve the plugin extensions directory.
101
+ * Default: ~/.openclaw/extensions/openclaw-weixin
102
+ */
103
+ function resolvePluginExtDir() {
104
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), ".openclaw");
105
+ return path.join(stateDir, "extensions", "openclaw-weixin");
106
+ }
107
+
108
+ /**
109
+ * Create a symlink from the plugin's node_modules/openclaw to the host
110
+ * openclaw package root. This lets jiti resolve openclaw/plugin-sdk/*
111
+ * without openclaw being a runtime dependency.
112
+ */
113
+ function ensureOpenclawSymlink() {
114
+ const hostRoot = resolveHostOpenclawRoot();
115
+ if (!hostRoot) {
116
+ error("无法定位宿主 openclaw 包根目录,跳过 symlink 创建");
117
+ return;
118
+ }
119
+
120
+ const pluginDir = resolvePluginExtDir();
121
+ if (!fs.existsSync(pluginDir)) {
122
+ // Plugin not in extensions dir (might be a -l link install); skip.
123
+ return;
124
+ }
125
+
126
+ const nmDir = path.join(pluginDir, "node_modules");
127
+ const linkPath = path.join(nmDir, "openclaw");
128
+
129
+ // Check if already correct
130
+ try {
131
+ const existing = fs.readlinkSync(linkPath);
132
+ if (fs.realpathSync(existing) === fs.realpathSync(hostRoot)) {
133
+ log("openclaw symlink 已存在且正确");
134
+ return;
135
+ }
136
+ // Wrong target — remove and recreate
137
+ fs.unlinkSync(linkPath);
138
+ } catch {
139
+ // Not a symlink or doesn't exist — fine
140
+ }
141
+
142
+ fs.mkdirSync(nmDir, { recursive: true });
143
+ fs.symlinkSync(hostRoot, linkPath);
144
+ log(`已创建 symlink: node_modules/openclaw → ${hostRoot}`);
145
+ }
146
+
38
147
  // ── commands ─────────────────────────────────────────────────────────────────
39
148
 
40
149
  function install() {
@@ -47,10 +156,26 @@ function install() {
47
156
  }
48
157
  log("已找到本地安装的 openclaw");
49
158
 
50
- // 2. Install plugin via openclaw
51
- log("正在安装插件...");
159
+ // 2. Detect host version and select compatible plugin
160
+ const hostVersion = getOpenclawVersion();
161
+ if (!hostVersion) {
162
+ error("无法获取 openclaw 版本号,请确认 `openclaw --version` 正常工作");
163
+ process.exit(1);
164
+ }
165
+ log(`检测到 OpenClaw 版本: ${hostVersion}`);
166
+
167
+ const compat = selectPluginTag(hostVersion);
168
+ if (!compat) {
169
+ process.exit(1);
170
+ }
171
+
172
+ const pluginInstallSpec = `${PLUGIN_SPEC}@${compat.distTag}`;
173
+ log(`匹配兼容版本: ${compat.label} (dist-tag: ${compat.distTag})`);
174
+
175
+ // 3. Install plugin via openclaw
176
+ log(`正在安装插件 ${pluginInstallSpec}...`);
52
177
  try {
53
- const installOut = run(`openclaw plugins install "${PLUGIN_SPEC}"`);
178
+ const installOut = run(`openclaw plugins install "${pluginInstallSpec}"`);
54
179
  if (installOut) log(installOut);
55
180
  } catch (installErr) {
56
181
  if (installErr.stderr && installErr.stderr.includes("already exists")) {
@@ -67,12 +192,16 @@ function install() {
67
192
  } else {
68
193
  error("插件安装失败,请手动执行:");
69
194
  if (installErr.stderr) console.error(installErr.stderr);
70
- console.log(` openclaw plugins install "${PLUGIN_SPEC}"`);
195
+ console.log(` openclaw plugins install "${pluginInstallSpec}"`);
71
196
  process.exit(1);
72
197
  }
73
198
  }
74
199
 
75
- // 3. Login (interactive QR scan)
200
+ // 4. Symlink host openclaw into plugin's node_modules
201
+ // so jiti can resolve openclaw/plugin-sdk/* without a runtime dependency.
202
+ ensureOpenclawSymlink();
203
+
204
+ // 5. Login (interactive QR scan)
76
205
  log("插件就绪,开始首次连接...");
77
206
  try {
78
207
  run(`openclaw channels login --channel ${CHANNEL_ID}`, { silent: false });
@@ -82,7 +211,7 @@ function install() {
82
211
  console.log(` openclaw channels login --channel ${CHANNEL_ID}`);
83
212
  }
84
213
 
85
- // 4. Restart gateway so it picks up the new account
214
+ // 6. Restart gateway so it picks up the new account
86
215
  log("正在重启 OpenClaw Gateway...");
87
216
  try {
88
217
  run(`openclaw gateway restart`, { silent: false });
@@ -90,7 +219,6 @@ function install() {
90
219
  error("重启失败,可手动执行:");
91
220
  console.log(` openclaw gateway restart`);
92
221
  }
93
-
94
222
  }
95
223
 
96
224
  function help() {
@@ -98,9 +226,14 @@ function help() {
98
226
  用法: npx -y @tencent-weixin/openclaw-weixin-cli <命令>
99
227
 
100
228
  命令:
101
- install 安装微信插件并扫码连接
229
+ install 自动检测 OpenClaw 版本,安装兼容的微信插件并扫码连接
102
230
  help 显示帮助信息
103
- `);
231
+
232
+ 兼容矩阵:`);
233
+ for (const e of COMPAT_MATRIX) {
234
+ console.log(` ${e.label.padEnd(28)} OpenClaw ${formatRange(e.openclawRange)}`);
235
+ }
236
+ console.log();
104
237
  }
105
238
 
106
239
  // ── main ─────────────────────────────────────────────────────────────────────
package/lib/compat.mjs ADDED
@@ -0,0 +1,61 @@
1
+ // lib/compat.mjs
2
+ // Centralized compatibility matrix for openclaw-weixin plugin versions.
3
+
4
+ /**
5
+ * Each entry maps a plugin major version to the supported OpenClaw host range.
6
+ * Used by both the installer CLI (auto-select) and the plugin runtime (fail-fast).
7
+ */
8
+ export const COMPAT_MATRIX = [
9
+ {
10
+ pluginMajor: 1,
11
+ distTag: 'legacy',
12
+ openclawRange: { gte: '2026.3.0', lt: '2026.3.22' },
13
+ label: '1.0.x (旧宿主线)',
14
+ },
15
+ {
16
+ pluginMajor: 2,
17
+ distTag: 'latest',
18
+ openclawRange: { gte: '2026.3.22' },
19
+ label: '2.0.x (新宿主线)',
20
+ },
21
+ ];
22
+
23
+ /** Parse a version string like "2026.3.22" into [major, minor, patch]. */
24
+ export function parseVersion(v) {
25
+ const m = String(v).match(/(\d+)\.(\d+)\.(\d+)/);
26
+ if (!m) return null;
27
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
28
+ }
29
+
30
+ /** Compare two version strings. Returns <0, 0, or >0. */
31
+ export function compareVersions(a, b) {
32
+ const va = parseVersion(a);
33
+ const vb = parseVersion(b);
34
+ if (!va || !vb) return NaN;
35
+ for (let i = 0; i < 3; i++) {
36
+ if (va[i] !== vb[i]) return va[i] - vb[i];
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ /** Check if `version` satisfies { gte, lt } range. */
42
+ export function satisfiesRange(version, range) {
43
+ if (range.gte && compareVersions(version, range.gte) < 0) return false;
44
+ if (range.lt && compareVersions(version, range.lt) >= 0) return false;
45
+ return true;
46
+ }
47
+
48
+ /** Find the matching matrix entry for a given OpenClaw host version. */
49
+ export function findCompatEntry(openclawVersion) {
50
+ return COMPAT_MATRIX.find(entry =>
51
+ satisfiesRange(openclawVersion, entry.openclawRange)
52
+ ) || null;
53
+ }
54
+
55
+ /** Format a range for display, e.g. ">=2026.3.22 <2026.4.0". */
56
+ export function formatRange(range) {
57
+ const parts = [];
58
+ if (range.gte) parts.push(`>=${range.gte}`);
59
+ if (range.lt) parts.push(`<${range.lt}`);
60
+ return parts.join(' ');
61
+ }
@@ -0,0 +1,86 @@
1
+ // lib/version-check.mjs
2
+ // Runtime fail-fast version check — call at plugin initialization.
3
+ //
4
+ // Usage (in plugin entry point):
5
+ // import { assertHostCompat } from '@tencent/openclaw-weixin/lib/version-check.mjs';
6
+ // assertHostCompat();
7
+
8
+ import { readFileSync } from 'node:fs';
9
+ import { resolve, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import {
12
+ COMPAT_MATRIX,
13
+ findCompatEntry,
14
+ parseVersion,
15
+ formatRange,
16
+ } from './compat.mjs';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+
20
+ /**
21
+ * Resolve the current OpenClaw host version.
22
+ * Checks (in order):
23
+ * 1. OPENCLAW_VERSION env var (set by host since 2026.3.x)
24
+ * 2. globalThis.__openclaw_version__ (injected by host runtime)
25
+ */
26
+ export function getHostVersion() {
27
+ if (process.env.OPENCLAW_VERSION) return process.env.OPENCLAW_VERSION;
28
+ if (globalThis.__openclaw_version__) return String(globalThis.__openclaw_version__);
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Read the plugin's own major version from its package.json.
34
+ */
35
+ function getPluginMajor() {
36
+ try {
37
+ const pkg = JSON.parse(
38
+ readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'),
39
+ );
40
+ return parseVersion(pkg.version)?.[0] ?? null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Assert that the current host is compatible with this plugin version.
48
+ * Throws a clear error if incompatible; silently returns on success.
49
+ */
50
+ export function assertHostCompat() {
51
+ const hostVersion = getHostVersion();
52
+ if (!hostVersion) {
53
+ console.warn(
54
+ '[openclaw-weixin] 无法检测 OpenClaw 宿主版本,跳过兼容性检查',
55
+ );
56
+ return;
57
+ }
58
+
59
+ const pluginMajor = getPluginMajor();
60
+ const entry = COMPAT_MATRIX.find(e => e.pluginMajor === pluginMajor);
61
+ if (!entry) {
62
+ console.warn(
63
+ `[openclaw-weixin] 未知插件主版本 ${pluginMajor},跳过兼容性检查`,
64
+ );
65
+ return;
66
+ }
67
+
68
+ const compat = findCompatEntry(hostVersion);
69
+ if (compat && compat.pluginMajor === pluginMajor) {
70
+ // Compatible — nothing to do.
71
+ return;
72
+ }
73
+
74
+ const supported = formatRange(entry.openclawRange);
75
+ const suggestion = compat
76
+ ? `请安装 openclaw-weixin@${compat.distTag} (${compat.label})`
77
+ : '请升级或降级 OpenClaw 宿主到受支持的版本';
78
+
79
+ throw new Error(
80
+ `[openclaw-weixin] 宿主版本不兼容!\n` +
81
+ ` 当前 OpenClaw 版本: ${hostVersion}\n` +
82
+ ` 当前插件支持范围: ${supported}\n` +
83
+ ` ${suggestion}\n` +
84
+ ` 或运行: npx @tencent-weixin/openclaw-weixin-cli install (自动选择兼容版本)`,
85
+ );
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-weixin/openclaw-weixin-cli",
3
- "version": "1.0.3",
3
+ "version": "2.0.1",
4
4
  "description": "Lightweight installer for the OpenClaw Weixin channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "Tencent",
@@ -9,9 +9,21 @@
9
9
  "weixin-installer": "./cli.mjs"
10
10
  },
11
11
  "files": [
12
- "cli.mjs"
12
+ "cli.mjs",
13
+ "lib/"
13
14
  ],
14
15
  "scripts": {},
16
+ "peerDependencies": {
17
+ "openclaw": ">=2026.3.0"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "openclaw": {
21
+ "optional": true
22
+ }
23
+ },
24
+ "devDependencies": {
25
+ "openclaw": "2026.3.22"
26
+ },
15
27
  "engines": {
16
28
  "node": ">=22"
17
29
  }