@xmoxmo/bncr 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +37 -1
  2. package/index.ts +208 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,6 +34,14 @@ openclaw plugins update bncr
34
34
  openclaw gateway restart
35
35
  ```
36
36
 
37
+ > 兼容范围:`openclaw >= 2026.3.22`
38
+ >
39
+ > 如果你是从精确版本升级,或本地安装记录仍钉在旧版本,也可以显式执行:
40
+ >
41
+ > ```bash
42
+ > openclaw plugins install @xmoxmo/bncr@0.1.1
43
+ > openclaw gateway restart
44
+ > ```
37
45
 
38
46
  ### Bncr / 无界侧
39
47
 
@@ -137,7 +145,35 @@ openclaw health --json
137
145
 
138
146
  ---
139
147
 
140
- ## 8. 自检与测试
148
+ ## 8. 常见安装/加载问题
149
+
150
+ ### 报错:`Cannot find module 'openclaw/plugin-sdk/core'`
151
+
152
+ 这通常不是 bncr 没装上,而是:
153
+
154
+ - bncr 已经安装到 `~/.openclaw/extensions/bncr`
155
+ - 但插件目录当前解析不到宿主 `openclaw` 包
156
+ - 因而在加载 `openclaw/plugin-sdk/core` 时失败
157
+
158
+ bncr 0.1.1 会先尝试自动修复插件目录下的 `node_modules/openclaw` 解析链;如果仍失败,可手动执行:
159
+
160
+ ```bash
161
+ mkdir -p ~/.openclaw/extensions/bncr/node_modules
162
+ ln -s "$(npm root -g)/openclaw" ~/.openclaw/extensions/bncr/node_modules/openclaw
163
+ openclaw gateway restart
164
+ openclaw plugins inspect bncr
165
+ ```
166
+
167
+ 如果 `npm root -g` 指向的不是实际宿主位置,请先检查:
168
+
169
+ ```bash
170
+ which openclaw
171
+ npm root -g
172
+ ```
173
+
174
+ 然后把 `openclaw` 的真实安装目录软链接到 `~/.openclaw/extensions/bncr/node_modules/openclaw`。
175
+
176
+ ## 9. 自检与测试
141
177
 
142
178
  ```bash
143
179
  cd plugins/bncr
package/index.ts CHANGED
@@ -1,25 +1,207 @@
1
- import type {
2
- OpenClawPluginApi,
3
- GatewayRequestHandlerOptions,
4
- } from "openclaw/plugin-sdk/core";
5
- import { BncrConfigSchema } from "./src/core/config-schema.ts";
6
- import { createBncrBridge, createBncrChannelPlugin } from "./src/channel.ts";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { BncrConfigSchema } from './src/core/config-schema.ts';
7
7
 
8
- type BridgeSingleton = ReturnType<typeof createBncrBridge>;
8
+ const pluginFile = fileURLToPath(import.meta.url);
9
+ const pluginDir = path.dirname(pluginFile);
10
+ const pluginRequire = createRequire(import.meta.url);
11
+ const sdkCoreSpecifier = 'openclaw/plugin-sdk/core';
12
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
13
+
14
+ type ChannelModule = typeof import('./src/channel.ts');
15
+ type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
16
+ type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
17
+
18
+ type LoadedRuntime = {
19
+ createBncrBridge: ChannelModule['createBncrBridge'];
20
+ createBncrChannelPlugin: ChannelModule['createBncrChannelPlugin'];
21
+ };
22
+
23
+ let runtime: LoadedRuntime | null = null;
24
+
25
+ const tryExec = (command: string, args: string[]) => {
26
+ try {
27
+ return execFileSync(command, args, {
28
+ encoding: 'utf8',
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ }).trim();
31
+ } catch {
32
+ return '';
33
+ }
34
+ };
35
+
36
+ const readOpenClawPackageName = (pkgPath: string) => {
37
+ try {
38
+ const raw = fs.readFileSync(pkgPath, 'utf8');
39
+ const parsed = JSON.parse(raw);
40
+ return typeof parsed?.name === 'string' ? parsed.name : '';
41
+ } catch {
42
+ return '';
43
+ }
44
+ };
45
+
46
+ const findOpenClawPackageRoot = (startPath: string) => {
47
+ let current = startPath;
48
+ try {
49
+ current = fs.realpathSync(startPath);
50
+ } catch {
51
+ // keep original path when realpath fails
52
+ }
53
+
54
+ let cursor = current;
55
+ while (true) {
56
+ const statPath = fs.existsSync(cursor) ? cursor : path.dirname(cursor);
57
+ const pkgPath = path.join(statPath, 'package.json');
58
+ if (fs.existsSync(pkgPath) && readOpenClawPackageName(pkgPath) === 'openclaw') {
59
+ return statPath;
60
+ }
61
+ const parent = path.dirname(statPath);
62
+ if (parent === statPath) break;
63
+ cursor = parent;
64
+ }
65
+ return '';
66
+ };
67
+
68
+ const unique = (items: string[]) => {
69
+ const seen = new Set<string>();
70
+ const out: string[] = [];
71
+ for (const item of items) {
72
+ if (!item) continue;
73
+ const normalized = path.normalize(item);
74
+ if (seen.has(normalized)) continue;
75
+ seen.add(normalized);
76
+ out.push(normalized);
77
+ }
78
+ return out;
79
+ };
80
+
81
+ const collectOpenClawCandidates = () => {
82
+ const directCandidates = [
83
+ path.join(pluginDir, 'node_modules', 'openclaw'),
84
+ path.join('/usr/lib/node_modules', 'openclaw'),
85
+ path.join('/usr/local/lib/node_modules', 'openclaw'),
86
+ path.join('/opt/homebrew/lib/node_modules', 'openclaw'),
87
+ path.join(process.env.HOME || '', '.npm-global/lib/node_modules', 'openclaw'),
88
+ ];
89
+
90
+ const npmRoot = tryExec('npm', ['root', '-g']);
91
+ if (npmRoot) directCandidates.push(path.join(npmRoot, 'openclaw'));
92
+
93
+ const nodePathEntries = (process.env.NODE_PATH || '')
94
+ .split(path.delimiter)
95
+ .map((entry) => entry.trim())
96
+ .filter(Boolean);
97
+ for (const entry of nodePathEntries) {
98
+ directCandidates.push(path.join(entry, 'openclaw'));
99
+ }
100
+
101
+ const openclawBin = tryExec('which', ['openclaw']);
102
+ if (openclawBin) {
103
+ directCandidates.push(openclawBin);
104
+ directCandidates.push(path.dirname(openclawBin));
105
+ }
106
+
107
+ const packageRoots = unique(
108
+ directCandidates
109
+ .map((candidate) => findOpenClawPackageRoot(candidate))
110
+ .filter(Boolean),
111
+ );
112
+
113
+ return packageRoots.filter((candidate) => {
114
+ const pkgJson = path.join(candidate, 'package.json');
115
+ return fs.existsSync(pkgJson) && readOpenClawPackageName(pkgJson) === 'openclaw';
116
+ });
117
+ };
118
+
119
+ const canResolveSdkCore = () => {
120
+ try {
121
+ pluginRequire.resolve(sdkCoreSpecifier);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ };
127
+
128
+ const ensurePluginNodeModulesLink = (targetRoot: string) => {
129
+ const nodeModulesDir = path.join(pluginDir, 'node_modules');
130
+ const linkPath = path.join(nodeModulesDir, 'openclaw');
131
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
132
+
133
+ try {
134
+ const stat = fs.lstatSync(linkPath);
135
+ if (stat.isSymbolicLink()) {
136
+ const existingTarget = fs.realpathSync(linkPath);
137
+ const normalizedExisting = path.normalize(existingTarget);
138
+ const normalizedTarget = path.normalize(fs.realpathSync(targetRoot));
139
+ if (normalizedExisting === normalizedTarget) return;
140
+ fs.unlinkSync(linkPath);
141
+ } else {
142
+ return;
143
+ }
144
+ } catch {
145
+ // missing link is fine
146
+ }
147
+
148
+ fs.symlinkSync(targetRoot, linkPath, linkType as fs.symlink.Type);
149
+ };
150
+
151
+ const ensureOpenClawSdkResolution = () => {
152
+ if (canResolveSdkCore()) return;
153
+
154
+ let lastError = '';
155
+ const candidates = collectOpenClawCandidates();
156
+ for (const candidate of candidates) {
157
+ try {
158
+ ensurePluginNodeModulesLink(candidate);
159
+ if (canResolveSdkCore()) return;
160
+ } catch (error) {
161
+ lastError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
162
+ }
163
+ }
164
+
165
+ const suffix = candidates.length
166
+ ? ` Tried candidates: ${candidates.join(', ')}.`
167
+ : ' No openclaw package root candidates were found from npm root, NODE_PATH, common global paths, or the openclaw binary path.';
168
+ const extra = lastError ? ` Last repair error: ${lastError}.` : '';
169
+ throw new Error(
170
+ `bncr failed to resolve ${sdkCoreSpecifier} from ${pluginDir}.${suffix}${extra} ` +
171
+ `You can repair manually with: mkdir -p ${path.join(pluginDir, 'node_modules')} && ln -s "$(npm root -g)/openclaw" ${path.join(pluginDir, 'node_modules', 'openclaw')}`,
172
+ );
173
+ };
174
+
175
+ const loadRuntimeSync = (): LoadedRuntime => {
176
+ if (runtime) return runtime;
177
+ ensureOpenClawSdkResolution();
178
+ try {
179
+ const mod = pluginRequire('./src/channel.ts') as ChannelModule;
180
+ runtime = {
181
+ createBncrBridge: mod.createBncrBridge,
182
+ createBncrChannelPlugin: mod.createBncrChannelPlugin,
183
+ };
184
+ return runtime;
185
+ } catch (error) {
186
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
187
+ throw new Error(`bncr failed to load channel runtime after dependency bootstrap: ${detail}`);
188
+ }
189
+ };
9
190
 
10
191
  const getBridgeSingleton = (api: OpenClawPluginApi) => {
192
+ const loaded = loadRuntimeSync();
11
193
  const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingleton };
12
- if (!g.__bncrBridge) g.__bncrBridge = createBncrBridge(api);
13
- return g.__bncrBridge;
194
+ if (!g.__bncrBridge) g.__bncrBridge = loaded.createBncrBridge(api);
195
+ return { bridge: g.__bncrBridge, runtime: loaded };
14
196
  };
15
197
 
16
198
  const plugin = {
17
- id: "bncr",
18
- name: "Bncr",
19
- description: "Bncr channel plugin",
199
+ id: 'bncr',
200
+ name: 'Bncr',
201
+ description: 'Bncr channel plugin',
20
202
  configSchema: BncrConfigSchema,
21
203
  register(api: OpenClawPluginApi) {
22
- const bridge = getBridgeSingleton(api);
204
+ const { bridge, runtime } = getBridgeSingleton(api);
23
205
  const debugLog = (...args: any[]) => {
24
206
  if (!bridge.isDebugEnabled?.()) return;
25
207
  api.logger.info?.(...args);
@@ -37,7 +219,7 @@ const plugin = {
37
219
  };
38
220
 
39
221
  api.registerService({
40
- id: "bncr-bridge-service",
222
+ id: 'bncr-bridge-service',
41
223
  start: async (ctx) => {
42
224
  const debug = await resolveDebug();
43
225
  await bridge.startService(ctx, debug);
@@ -45,48 +227,18 @@ const plugin = {
45
227
  stop: bridge.stopService,
46
228
  });
47
229
 
48
- api.registerChannel({ plugin: createBncrChannelPlugin(bridge) });
49
-
50
- api.registerGatewayMethod(
51
- "bncr.connect",
52
- (opts: GatewayRequestHandlerOptions) => bridge.handleConnect(opts),
53
- );
54
- api.registerGatewayMethod(
55
- "bncr.inbound",
56
- (opts: GatewayRequestHandlerOptions) => bridge.handleInbound(opts),
57
- );
58
- api.registerGatewayMethod(
59
- "bncr.activity",
60
- (opts: GatewayRequestHandlerOptions) => bridge.handleActivity(opts),
61
- );
62
- api.registerGatewayMethod(
63
- "bncr.ack",
64
- (opts: GatewayRequestHandlerOptions) => bridge.handleAck(opts),
65
- );
66
- api.registerGatewayMethod(
67
- "bncr.diagnostics",
68
- (opts: GatewayRequestHandlerOptions) => bridge.handleDiagnostics(opts),
69
- );
70
- api.registerGatewayMethod(
71
- "bncr.file.init",
72
- (opts: GatewayRequestHandlerOptions) => bridge.handleFileInit(opts),
73
- );
74
- api.registerGatewayMethod(
75
- "bncr.file.chunk",
76
- (opts: GatewayRequestHandlerOptions) => bridge.handleFileChunk(opts),
77
- );
78
- api.registerGatewayMethod(
79
- "bncr.file.complete",
80
- (opts: GatewayRequestHandlerOptions) => bridge.handleFileComplete(opts),
81
- );
82
- api.registerGatewayMethod(
83
- "bncr.file.abort",
84
- (opts: GatewayRequestHandlerOptions) => bridge.handleFileAbort(opts),
85
- );
86
- api.registerGatewayMethod(
87
- "bncr.file.ack",
88
- (opts: GatewayRequestHandlerOptions) => bridge.handleFileAck(opts),
89
- );
230
+ api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
231
+
232
+ api.registerGatewayMethod('bncr.connect', (opts) => bridge.handleConnect(opts));
233
+ api.registerGatewayMethod('bncr.inbound', (opts) => bridge.handleInbound(opts));
234
+ api.registerGatewayMethod('bncr.activity', (opts) => bridge.handleActivity(opts));
235
+ api.registerGatewayMethod('bncr.ack', (opts) => bridge.handleAck(opts));
236
+ api.registerGatewayMethod('bncr.diagnostics', (opts) => bridge.handleDiagnostics(opts));
237
+ api.registerGatewayMethod('bncr.file.init', (opts) => bridge.handleFileInit(opts));
238
+ api.registerGatewayMethod('bncr.file.chunk', (opts) => bridge.handleFileChunk(opts));
239
+ api.registerGatewayMethod('bncr.file.complete', (opts) => bridge.handleFileComplete(opts));
240
+ api.registerGatewayMethod('bncr.file.abort', (opts) => bridge.handleFileAbort(opts));
241
+ api.registerGatewayMethod('bncr.file.ack', (opts) => bridge.handleFileAck(opts));
90
242
  },
91
243
  };
92
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",