@core-workspace/infoflow-openclaw-plugin 2026.3.9 → 2026.3.27-beta.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 (55) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CLAUDE.md +135 -0
  3. package/COLLABORATION_REPORT.md +209 -0
  4. package/PROJECT_GUIDE.md +355 -0
  5. package/README.md +158 -66
  6. package/docs/dev-guide.md +63 -50
  7. package/docs/qa-feature-list.md +452 -0
  8. package/docs/webhook-guide.md +178 -0
  9. package/index.ts +28 -2
  10. package/openclaw.plugin.json +131 -21
  11. package/package.json +16 -3
  12. package/scripts/deploy.sh +66 -7
  13. package/scripts/postinstall.cjs +80 -0
  14. package/skills/infoflow-dev/SKILL.md +2 -2
  15. package/skills/infoflow-dev/references/api.md +1 -1
  16. package/src/adapter/inbound/webhook-parser.ts +27 -5
  17. package/src/adapter/inbound/ws-receiver.ts +304 -43
  18. package/src/adapter/outbound/markdown-local-images.ts +80 -0
  19. package/src/adapter/outbound/reply-dispatcher.ts +146 -65
  20. package/src/adapter/outbound/target-resolver.ts +4 -3
  21. package/src/channel/accounts.ts +97 -22
  22. package/src/channel/channel.ts +456 -12
  23. package/src/channel/media.ts +20 -6
  24. package/src/channel/monitor.ts +8 -3
  25. package/src/channel/outbound.ts +358 -21
  26. package/src/channel/streaming.ts +740 -0
  27. package/src/commands/changelog.ts +80 -0
  28. package/src/commands/doctor.ts +545 -0
  29. package/src/commands/logs.ts +449 -0
  30. package/src/commands/version.ts +20 -0
  31. package/src/compat/openclaw-sdk.ts +218 -0
  32. package/src/handler/message-handler.ts +673 -166
  33. package/src/logging.ts +1 -1
  34. package/src/runtime.ts +1 -1
  35. package/src/security/dm-policy.ts +1 -4
  36. package/src/security/group-policy.ts +174 -51
  37. package/src/tools/actions/index.ts +15 -13
  38. package/src/tools/cron/relay.ts +1154 -0
  39. package/src/tools/hooks/index.ts +13 -1
  40. package/src/tools/index.ts +714 -32
  41. package/src/types.ts +144 -25
  42. package/src/utils/audio/g722/dct_tables.ts +381 -0
  43. package/src/utils/audio/g722/decoder.ts +919 -0
  44. package/src/utils/audio/g722/defs.ts +105 -0
  45. package/src/utils/audio/g722/hd-parser.ts +247 -0
  46. package/src/utils/audio/g722/huff_tables.ts +240 -0
  47. package/src/utils/audio/g722/index.ts +78 -0
  48. package/src/utils/audio/g722/output_decoded.pcm +0 -0
  49. package/src/utils/audio/g722/output_decoded.wav +0 -0
  50. package/src/utils/audio/g722/tables.ts +173 -0
  51. package/src/utils/audio/g722/test_api.ts +31 -0
  52. package/src/utils/audio/g722/test_voice.hd +0 -0
  53. package/src/utils/bos/im-bos-client.ts +219 -0
  54. package/src/utils/group-agent-cache.ts +142 -0
  55. package/src/utils/token-adapter.ts +120 -51
@@ -0,0 +1,449 @@
1
+ /**
2
+ * /infoflow-logs command
3
+ *
4
+ * 读取 OpenClaw 网关的结构化日志文件,筛选 Infoflow 相关条目并格式化输出。
5
+ * 帮助用户快速了解"收到了哪些消息、回复了什么、有没有报错"。
6
+ *
7
+ * 日志文件位置:/tmp/openclaw/openclaw-YYYY-MM-DD.log(jsonl 格式)
8
+ *
9
+ * 用法:
10
+ * /infoflow-logs — 展示最近 20 条会话记录
11
+ * /infoflow-logs 50 — 展示最近 50 条
12
+ * /infoflow-logs error — 只看 ERROR 级别
13
+ * /infoflow-logs warn — 只看 WARN 及以上(含 ERROR)
14
+ * /infoflow-logs error 50 — 组合:最近 50 条 ERROR
15
+ *
16
+ * 长输出由框架的 textChunkLimit(默认 1800 字符/条)自动分片发送。
17
+ * 总输出上限由 LOGS_TOTAL_CHAR_LIMIT 控制(默认 20000 字符)。
18
+ */
19
+
20
+ import { readFileSync, existsSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Constants
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const LOG_DIR = "/tmp/openclaw";
28
+ const DEFAULT_EVENTS = 20;
29
+ const MAX_EVENTS = 200;
30
+ /** 总输出字符上限 */
31
+ const LOGS_TOTAL_CHAR_LIMIT = 20_000;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Raw log entry parsing
35
+ // ---------------------------------------------------------------------------
36
+
37
+ type RawEntry = {
38
+ msg: string;
39
+ level: number; // 30=info, 40=warn, 50=error
40
+ timeStr: string; // HH:MM:SS
41
+ tsMs: number; // unix ms for grouping
42
+ };
43
+
44
+ function parseRawEntry(line: string): RawEntry | null {
45
+ const trimmed = line.trim();
46
+ if (!trimmed) return null;
47
+ try {
48
+ const obj = JSON.parse(trimmed) as Record<string, unknown>;
49
+ const field0 = String(obj["0"] ?? "");
50
+ const field1 = String(obj["1"] ?? "");
51
+ let msg = field1 || (!field0.startsWith("{") ? field0 : "");
52
+ if (!msg) return null;
53
+
54
+ // Strip embedded SDK timestamp+level prefix
55
+ msg = msg.replace(/^\d{4}-\d{2}-\d{2}T[\d:.Z]+\s*\[InfoFlow\]\s*\[\w+\]\s*/, "").trim();
56
+
57
+ const meta = obj["_meta"] as Record<string, unknown> | undefined;
58
+ const levelName = String(meta?.["logLevelName"] ?? "");
59
+ const level = Number(
60
+ obj["level"] ?? (levelName === "ERROR" ? 50 : levelName === "WARN" ? 40 : 30),
61
+ );
62
+
63
+ const time = obj["time"] as string | undefined;
64
+ let timeStr = "";
65
+ let tsMs = 0;
66
+ if (time) {
67
+ try {
68
+ const d = new Date(time);
69
+ timeStr = d.toLocaleTimeString("en-US", { hour12: false });
70
+ tsMs = d.getTime();
71
+ } catch {
72
+ /* ignore */
73
+ }
74
+ }
75
+
76
+ return { msg, level, timeStr, tsMs };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Event model — one "event" = one logical thing that happened
84
+ // ---------------------------------------------------------------------------
85
+
86
+ type LogEvent =
87
+ | {
88
+ kind: "exchange";
89
+ time: string;
90
+ from: string;
91
+ text: string;
92
+ elapsed?: number;
93
+ replyLen: number;
94
+ replyCount: number;
95
+ replyTypes: string[];
96
+ }
97
+ | { kind: "ws"; time: string; detail: string; level: number }
98
+ | { kind: "error"; time: string; detail: string; level: number }
99
+ | { kind: "system"; time: string; detail: string };
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Aggregation: raw entries → events
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function aggregateEvents(entries: RawEntry[]): LogEvent[] {
106
+ const events: LogEvent[] = [];
107
+
108
+ // State for building an exchange
109
+ let inExchange = false;
110
+ let fromUser = "";
111
+ let inText = "";
112
+ let dispatchTime = "";
113
+ let dispatchMs = 0;
114
+ let replyLen = 0;
115
+ let replyCount = 0;
116
+ let replyTypes: string[] = [];
117
+
118
+ function flushExchange() {
119
+ if (!inExchange) return;
120
+ events.push({
121
+ kind: "exchange",
122
+ time: dispatchTime,
123
+ from: fromUser,
124
+ text: inText,
125
+ replyLen,
126
+ replyCount,
127
+ replyTypes,
128
+ });
129
+ inExchange = false;
130
+ fromUser = "";
131
+ inText = "";
132
+ replyLen = 0;
133
+ replyCount = 0;
134
+ replyTypes = [];
135
+ }
136
+
137
+ for (const e of entries) {
138
+ const { msg, level, timeStr, tsMs } = e;
139
+
140
+ // ---- 收到消息(inbound)→ 开始新会话轮次 ----
141
+ // 格式: [inbound:dm] from=xxx, name=yyy, msgType=text, msgId=zzz, text=内容, images=0
142
+ const inboundDm = msg.match(
143
+ /^\[inbound:dm\] from=([^,]+),.*msgType=(\w+),.*\btext=(.*),\s*images=\d+/,
144
+ );
145
+ const inboundGroup = msg.match(
146
+ /^\[inbound:group\] from=([^,]+),.*\bgroup=(\d+),.*\btext=(.*),\s*images=\d+/,
147
+ );
148
+ if (inboundDm || inboundGroup) {
149
+ // 跳过 event 类型(连接/断开事件,不是真正的消息)
150
+ const msgType = inboundDm ? inboundDm[2] : "";
151
+ if (msgType === "event") continue;
152
+ flushExchange();
153
+ inExchange = true;
154
+ dispatchTime = timeStr;
155
+ dispatchMs = tsMs;
156
+ if (inboundDm) {
157
+ fromUser = inboundDm[1].trim();
158
+ inText = inboundDm[3]?.trim() ?? "";
159
+ } else if (inboundGroup) {
160
+ fromUser = `${inboundGroup[1].trim()}@群${inboundGroup[2]}`;
161
+ inText = inboundGroup[3]?.trim() ?? "";
162
+ }
163
+ continue;
164
+ }
165
+
166
+ // ---- 回复完成(dispatch:done)→ 记录耗时 ----
167
+ if (inExchange && msg.includes("[dispatch:done]")) {
168
+ const m = msg.match(/elapsed=(\d+)ms/);
169
+ if (m) {
170
+ const elapsed = parseInt(m[1], 10);
171
+ events.push({
172
+ kind: "exchange",
173
+ time: dispatchTime,
174
+ from: fromUser,
175
+ text: inText,
176
+ elapsed,
177
+ replyLen,
178
+ replyCount,
179
+ replyTypes: [...new Set(replyTypes)],
180
+ });
181
+ inExchange = false;
182
+ fromUser = "";
183
+ inText = "";
184
+ replyLen = 0;
185
+ replyCount = 0;
186
+ replyTypes = [];
187
+ }
188
+ continue;
189
+ }
190
+
191
+ // ---- 发出消息 ----
192
+ // 入口行: "[outbound] sendMessage: to=..., items=1, types=[markdown]" → 取类型
193
+ if (inExchange && msg.startsWith("[outbound] sendMessage:")) {
194
+ const typesM = msg.match(/types=\[([^\]]+)\]/);
195
+ if (typesM) replyTypes.push(...typesM[1].split(",").map((t) => t.trim()));
196
+ continue;
197
+ }
198
+ // 跳过 "[outbound] to=" 确认行(字数改用 outbound:dm/group 的 bodyLen 统计,更准确)
199
+ if (msg.startsWith("[outbound] to=")) continue;
200
+ // outbound:dm/group 发送行: "[outbound:dm] to=..., bodyLen=1733" → 取字数、计条数
201
+ if (inExchange && (msg.startsWith("[outbound:dm]") || msg.startsWith("[outbound:group]"))) {
202
+ const bodyLenM = msg.match(/bodyLen=(\d+)/);
203
+ if (bodyLenM) {
204
+ replyLen += parseInt(bodyLenM[1], 10);
205
+ replyCount++;
206
+ }
207
+ continue;
208
+ }
209
+ // 过滤 outbound:dm/group 的其他细节行(response 等)
210
+ if (msg.startsWith("[outbound:")) continue;
211
+
212
+ // ---- WS 连接事件 ----
213
+ if (
214
+ msg.startsWith("[ws:connect]") ||
215
+ msg.startsWith("[ws:disconnect]") ||
216
+ msg.startsWith("[ws:error]") ||
217
+ msg.includes("[Phase 1]") ||
218
+ msg.includes("[Phase 2]") ||
219
+ msg.includes("========== 开始两阶段")
220
+ ) {
221
+ flushExchange();
222
+ const wsLevel = msg.startsWith("[ws:error]")
223
+ ? 50
224
+ : msg.startsWith("[ws:disconnect]")
225
+ ? 40
226
+ : 30;
227
+ events.push({ kind: "ws", time: timeStr, detail: msg, level: wsLevel });
228
+ continue;
229
+ }
230
+
231
+ // ---- 心跳(合并掉,不展示) ----
232
+ if (
233
+ msg.includes("Sending heartbeat ping") ||
234
+ msg.includes("分发事件: heartbeat") ||
235
+ msg.includes("[ws:frame]")
236
+ ) {
237
+ continue;
238
+ }
239
+
240
+ // ---- 策略拒绝 / warn / error ----
241
+ // 注意:不调用 flushExchange(),允许 warn/error 与正在进行的 exchange 共存
242
+ // 否则 "dropped" 类 warn 日志会提前重置 inExchange 状态,导致后续 outbound 统计丢失
243
+ if (level >= 40 || msg.includes("rejected") || msg.includes("dropped")) {
244
+ events.push({ kind: "error", time: timeStr, detail: msg, level });
245
+ continue;
246
+ }
247
+
248
+ // ---- 系统/连接事件(default、dispatch 内部流转、infoflow:bot 等)→ 过滤 ----
249
+ if (
250
+ msg.startsWith("[dispatch]") ||
251
+ msg.startsWith("[dispatch:") ||
252
+ msg.startsWith("[ws:inbound]") ||
253
+ msg.startsWith("[ws:frame]") ||
254
+ msg.startsWith("[infoflow:bot]") ||
255
+ msg.startsWith("[dispatchPrivateMessage]") ||
256
+ msg.startsWith("[default]") ||
257
+ msg.includes("client ready")
258
+ ) {
259
+ // 静默过滤内部流转细节
260
+ continue;
261
+ }
262
+
263
+ // ---- 其余有价值的行作为系统事件 ----
264
+ if (level >= 40) {
265
+ events.push({ kind: "error", time: timeStr, detail: msg, level });
266
+ }
267
+ }
268
+
269
+ flushExchange();
270
+ return events;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Render events → Markdown
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function renderEvents(events: LogEvent[]): string[] {
278
+ const lines: string[] = [];
279
+ for (const ev of events) {
280
+ if (ev.kind === "exchange") {
281
+ const textPreview = ev.text.length > 60 ? ev.text.slice(0, 60) + "…" : ev.text || "(空)";
282
+ const elapsedStr = ev.elapsed != null ? ` ${ev.elapsed}ms` : "";
283
+ const replyStr =
284
+ ev.replyCount > 0
285
+ ? `${ev.replyLen}字${ev.replyCount > 1 ? ` ×${ev.replyCount}条` : ""}`
286
+ : ev.text.startsWith("/")
287
+ ? "指令"
288
+ : "无回复";
289
+ lines.push(`**${ev.time}** 💬 \`${ev.from}\` → ${textPreview} | 🤖${elapsedStr} ${replyStr}`);
290
+ lines.push("");
291
+ } else if (ev.kind === "ws") {
292
+ const icon = ev.level >= 50 ? "❌" : ev.level >= 40 ? "⚠️" : "🔗";
293
+ lines.push(`**${ev.time}** ${icon} ${ev.detail}`);
294
+ lines.push("");
295
+ } else if (ev.kind === "error") {
296
+ const icon = ev.level >= 50 ? "❌" : "⚠️";
297
+ lines.push(`**${ev.time}** ${icon} ${ev.detail}`);
298
+ lines.push("");
299
+ } else if (ev.kind === "system") {
300
+ lines.push(`**${ev.time}** ℹ️ ${ev.detail}`);
301
+ lines.push("");
302
+ }
303
+ }
304
+ return lines;
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // Log file discovery
309
+ // ---------------------------------------------------------------------------
310
+
311
+ function getTodayLogPath(): string {
312
+ const now = new Date();
313
+ const yyyy = now.getFullYear();
314
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
315
+ const dd = String(now.getDate()).padStart(2, "0");
316
+ return join(LOG_DIR, `openclaw-${yyyy}-${mm}-${dd}.log`);
317
+ }
318
+
319
+ function getYesterdayLogPath(): string {
320
+ const yesterday = new Date(Date.now() - 86_400_000);
321
+ const yyyy = yesterday.getFullYear();
322
+ const mm = String(yesterday.getMonth() + 1).padStart(2, "0");
323
+ const dd = String(yesterday.getDate()).padStart(2, "0");
324
+ return join(LOG_DIR, `openclaw-${yyyy}-${mm}-${dd}.log`);
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Command handler
329
+ // ---------------------------------------------------------------------------
330
+
331
+ export async function runLogsCommand(ctx: {
332
+ config?: unknown;
333
+ senderId?: string;
334
+ args?: string;
335
+ }): Promise<{ text: string }> {
336
+ const tokens = (ctx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
337
+
338
+ // 提取 level 过滤关键字
339
+ let minLevel = 0;
340
+ let levelLabel = "";
341
+ const remaining = tokens.filter((t) => {
342
+ if (t.toLowerCase() === "error") {
343
+ minLevel = 50;
344
+ levelLabel = "error";
345
+ return false;
346
+ }
347
+ if (t.toLowerCase() === "warn") {
348
+ minLevel = 40;
349
+ levelLabel = "warn";
350
+ return false;
351
+ }
352
+ return true;
353
+ });
354
+
355
+ const arg = remaining.join(" ");
356
+ let limit = DEFAULT_EVENTS;
357
+ if (arg) {
358
+ const n = parseInt(arg, 10);
359
+ limit = Number.isFinite(n) && n > 0 ? Math.min(n, MAX_EVENTS) : DEFAULT_EVENTS;
360
+ }
361
+
362
+ const todayPath = getTodayLogPath();
363
+ const yesterdayPath = getYesterdayLogPath();
364
+ const logPath = existsSync(todayPath)
365
+ ? todayPath
366
+ : existsSync(yesterdayPath)
367
+ ? yesterdayPath
368
+ : null;
369
+
370
+ if (!logPath) {
371
+ return {
372
+ text: [
373
+ "**Infoflow 日志**",
374
+ "",
375
+ "未找到日志文件。",
376
+ `期望位置:${todayPath}`,
377
+ "",
378
+ "可能的原因:",
379
+ "- Gateway 尚未启动过",
380
+ "- 日志路径与预期不符",
381
+ ].join("\n"),
382
+ };
383
+ }
384
+
385
+ let rawLines: string[];
386
+ try {
387
+ const content = readFileSync(logPath, "utf-8");
388
+ rawLines = content.split("\n");
389
+ } catch (err: unknown) {
390
+ return {
391
+ text: `**Infoflow 日志**\n\n读取日志文件失败:${err instanceof Error ? err.message : String(err)}`,
392
+ };
393
+ }
394
+
395
+ // Parse all raw entries
396
+ const allEntries: RawEntry[] = [];
397
+ for (const line of rawLines) {
398
+ const e = parseRawEntry(line);
399
+ if (e) allEntries.push(e);
400
+ }
401
+
402
+ // Aggregate into events
403
+ let events = aggregateEvents(allEntries);
404
+
405
+ // Level filter
406
+ if (minLevel > 0) {
407
+ events = events.filter((ev) => {
408
+ if (ev.kind === "error" || ev.kind === "ws") return ev.level >= minLevel;
409
+ return false; // exchange / system 不含 level,error/warn 模式下不显示
410
+ });
411
+ }
412
+
413
+ // Take last N events
414
+ if (events.length > limit) {
415
+ events = events.slice(events.length - limit);
416
+ }
417
+
418
+ const levelDesc = levelLabel ? ` [${levelLabel}]` : "";
419
+ const countDesc = `最近 ${events.length} 条`;
420
+
421
+ if (events.length === 0) {
422
+ return {
423
+ text: `**Infoflow 日志**${levelDesc} (${countDesc},来自 ${logPath.split("/").pop()})\n\n暂无相关日志。`,
424
+ };
425
+ }
426
+
427
+ // Render
428
+ const rendered = renderEvents(events);
429
+
430
+ // Apply total char limit
431
+ let totalChars = 0;
432
+ let dropped = 0;
433
+ const kept: string[] = [];
434
+ for (const line of rendered) {
435
+ if (totalChars + line.length + 1 > LOGS_TOTAL_CHAR_LIMIT) {
436
+ dropped++;
437
+ continue;
438
+ }
439
+ kept.push(line);
440
+ totalChars += line.length + 1;
441
+ }
442
+
443
+ const title = `**Infoflow 日志**${levelDesc} (${countDesc},来自 ${logPath.split("/").pop()})`;
444
+ const body = kept.join("\n");
445
+ const limitNote =
446
+ dropped > 0 ? `\n⚠️ 超出 ${LOGS_TOTAL_CHAR_LIMIT} 字符上限,已省略部分条目。` : "";
447
+
448
+ return { text: `${title}\n\n${body}${limitNote}` };
449
+ }
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_JSON_PATH = resolve(__dirname, "../../package.json");
7
+
8
+ let _version: string | undefined;
9
+
10
+ /** 读取当前安装的插件版本号(来自 package.json)。 */
11
+ export function getPluginVersion(): string {
12
+ if (_version !== undefined) return _version;
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf-8")) as Record<string, unknown>;
15
+ _version = typeof pkg.version === "string" ? pkg.version : "unknown";
16
+ } catch {
17
+ _version = "unknown";
18
+ }
19
+ return _version;
20
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Compatibility shim for openclaw/plugin-sdk.
3
+ *
4
+ * Supports both:
5
+ * - openclaw >= 2026.3.22: specific subpaths (infra-runtime, reply-history, …)
6
+ * - openclaw < 2026.3.22: monolithic root export (openclaw/plugin-sdk)
7
+ *
8
+ * Strategy: try the new subpath first; if that module doesn't exist, fall back
9
+ * to the root export which contained all symbols in the older versions.
10
+ *
11
+ * IMPORTANT: Only runtime values are handled here.
12
+ * Pure `import type` declarations are erased by tsx at runtime and need no shim.
13
+ */
14
+
15
+ import { createRequire } from "module";
16
+ import { readdirSync, realpathSync } from "fs";
17
+
18
+ const _require = createRequire(import.meta.url);
19
+
20
+ /**
21
+ * Build additional require functions that can resolve `openclaw/plugin-sdk/*`
22
+ * even when the plugin's own node_modules doesn't contain openclaw.
23
+ *
24
+ * When openclaw gateway loads this plugin, the gateway process itself has
25
+ * openclaw in its module resolution scope. We exploit this by creating a
26
+ * require function rooted at `process.argv[1]` (the openclaw main script),
27
+ * which walks up to the global node_modules and finds openclaw there.
28
+ *
29
+ * Additional fallback: try to detect common openclaw installation paths.
30
+ */
31
+ function buildRequirers(): NodeRequire[] {
32
+ const requirers: NodeRequire[] = [_require];
33
+
34
+ // Try the gateway main entry point first - most reliable when symlink is missing
35
+ if (process.argv[1]) {
36
+ try {
37
+ // Resolve symlink to real path so createRequire walks up from the actual
38
+ // node_modules directory, not the symlink's parent (e.g. bin/).
39
+ const realPath = realpathSync(process.argv[1]);
40
+ requirers.push(createRequire(realPath));
41
+ } catch {
42
+ // ignore
43
+ }
44
+ }
45
+
46
+ // Enumerate candidate openclaw main script paths (no glob - use fs to expand)
47
+ const candidateEntryFiles: string[] = [
48
+ '/opt/homebrew/lib/node_modules/openclaw/openclaw.mjs',
49
+ '/usr/local/lib/node_modules/openclaw/openclaw.mjs',
50
+ '/usr/lib/node_modules/openclaw/openclaw.mjs',
51
+ ];
52
+
53
+ // Honor custom npm prefix (e.g. npm_config_prefix=/home/work/opt/npm-global)
54
+ const npmPrefix = process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX;
55
+ if (npmPrefix) {
56
+ candidateEntryFiles.push(`${npmPrefix}/lib/node_modules/openclaw/openclaw.mjs`);
57
+ }
58
+
59
+ // Expand nvm node versions without glob
60
+ const home = process.env.HOME;
61
+ if (home) {
62
+ const nvmNodeBase = `${home}/.nvm/versions/node`;
63
+ try {
64
+ const nodeVersions = readdirSync(nvmNodeBase);
65
+ for (const version of nodeVersions) {
66
+ candidateEntryFiles.push(
67
+ `${nvmNodeBase}/${version}/lib/node_modules/openclaw/openclaw.mjs`,
68
+ );
69
+ }
70
+ } catch {
71
+ // nvm not installed or directory unreadable
72
+ }
73
+ }
74
+
75
+ for (const entryFile of candidateEntryFiles) {
76
+ try {
77
+ const testReq = createRequire(entryFile);
78
+ testReq.resolve("openclaw/plugin-sdk");
79
+ requirers.push(testReq);
80
+ break;
81
+ } catch {
82
+ // ignore, try next path
83
+ }
84
+ }
85
+
86
+ return requirers;
87
+ }
88
+
89
+ const _requirers = buildRequirers();
90
+
91
+ function tryLoad(...paths: string[]): Record<string, unknown> {
92
+ for (const path of paths) {
93
+ for (const req of _requirers) {
94
+ try {
95
+ return req(path) as Record<string, unknown>;
96
+ } catch {
97
+ // try next requirer / path
98
+ }
99
+ }
100
+ }
101
+ throw new Error(
102
+ `[infoflow-plugin] Failed to load openclaw SDK from any of: ${paths.join(", ")}`,
103
+ );
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // plugin-entry: emptyPluginConfigSchema, OpenClawPluginApi (type only)
108
+ // ---------------------------------------------------------------------------
109
+ const _pluginEntry = tryLoad("openclaw/plugin-sdk/plugin-entry", "openclaw/plugin-sdk");
110
+
111
+ export const emptyPluginConfigSchema =
112
+ _pluginEntry.emptyPluginConfigSchema as typeof import("openclaw/plugin-sdk/plugin-entry").emptyPluginConfigSchema;
113
+
114
+ // OpenClawPluginApi is a runtime type from newer openclaw versions (>=2026.3.23).
115
+ // Older versions don't export it at runtime, only in .d.ts.
116
+ // Define a compatible type here to avoid module resolution issues.
117
+ // The actual type is validated by the gateway's type system, so this just needs
118
+ // to be structurally compatible for the plugin's type checking.
119
+ export type OpenClawPluginApi = {
120
+ runtime: unknown;
121
+ registerChannel(config: unknown): void;
122
+ registerHttpRoute(config: unknown): void;
123
+ registerCommand(config: unknown): void;
124
+ registerService(service: unknown): void;
125
+ };
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // core: channel lifecycle helpers
129
+ // ---------------------------------------------------------------------------
130
+ const _core = tryLoad("openclaw/plugin-sdk/core", "openclaw/plugin-sdk");
131
+
132
+ export const applyAccountNameToChannelSection =
133
+ _core.applyAccountNameToChannelSection as typeof import("openclaw/plugin-sdk/core").applyAccountNameToChannelSection;
134
+
135
+ export const deleteAccountFromConfigSection =
136
+ _core.deleteAccountFromConfigSection as typeof import("openclaw/plugin-sdk/core").deleteAccountFromConfigSection;
137
+
138
+ export const formatPairingApproveHint =
139
+ _core.formatPairingApproveHint as typeof import("openclaw/plugin-sdk/core").formatPairingApproveHint;
140
+
141
+ export const migrateBaseNameToDefaultAccount =
142
+ _core.migrateBaseNameToDefaultAccount as typeof import("openclaw/plugin-sdk/core").migrateBaseNameToDefaultAccount;
143
+
144
+ export const setAccountEnabledInConfigSection =
145
+ _core.setAccountEnabledInConfigSection as typeof import("openclaw/plugin-sdk/core").setAccountEnabledInConfigSection;
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // account-id: DEFAULT_ACCOUNT_ID, normalizeAccountId
149
+ // In old openclaw these live in root; new openclaw has "account-id" subpath.
150
+ // "core" also re-exports them in new openclaw, so we try "account-id" first.
151
+ // ---------------------------------------------------------------------------
152
+ const _accountId = tryLoad("openclaw/plugin-sdk/account-id", "openclaw/plugin-sdk");
153
+
154
+ export const DEFAULT_ACCOUNT_ID =
155
+ _accountId.DEFAULT_ACCOUNT_ID as typeof import("openclaw/plugin-sdk/account-id").DEFAULT_ACCOUNT_ID;
156
+
157
+ export const normalizeAccountId =
158
+ _accountId.normalizeAccountId as typeof import("openclaw/plugin-sdk/account-id").normalizeAccountId;
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // infra-runtime: createDedupeCache
162
+ // ---------------------------------------------------------------------------
163
+ const _infraRuntime = tryLoad("openclaw/plugin-sdk/infra-runtime", "openclaw/plugin-sdk");
164
+
165
+ export const createDedupeCache =
166
+ _infraRuntime.createDedupeCache as typeof import("openclaw/plugin-sdk/infra-runtime").createDedupeCache;
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // reply-history
170
+ // ---------------------------------------------------------------------------
171
+ const _replyHistory = tryLoad("openclaw/plugin-sdk/reply-history", "openclaw/plugin-sdk");
172
+
173
+ export const buildPendingHistoryContextFromMap =
174
+ _replyHistory.buildPendingHistoryContextFromMap as typeof import("openclaw/plugin-sdk/reply-history").buildPendingHistoryContextFromMap;
175
+
176
+ export const clearHistoryEntriesIfEnabled =
177
+ _replyHistory.clearHistoryEntriesIfEnabled as typeof import("openclaw/plugin-sdk/reply-history").clearHistoryEntriesIfEnabled;
178
+
179
+ export const DEFAULT_GROUP_HISTORY_LIMIT =
180
+ _replyHistory.DEFAULT_GROUP_HISTORY_LIMIT as typeof import("openclaw/plugin-sdk/reply-history").DEFAULT_GROUP_HISTORY_LIMIT;
181
+
182
+ export const recordPendingHistoryEntryIfEnabled =
183
+ _replyHistory.recordPendingHistoryEntryIfEnabled as typeof import("openclaw/plugin-sdk/reply-history").recordPendingHistoryEntryIfEnabled;
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // media-runtime: buildAgentMediaPayload
187
+ // ---------------------------------------------------------------------------
188
+ const _mediaRuntime = tryLoad("openclaw/plugin-sdk/media-runtime", "openclaw/plugin-sdk");
189
+
190
+ export const buildAgentMediaPayload =
191
+ _mediaRuntime.buildAgentMediaPayload as typeof import("openclaw/plugin-sdk/media-runtime").buildAgentMediaPayload;
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // tool-send: extractToolSend
195
+ // ---------------------------------------------------------------------------
196
+ const _toolSend = tryLoad("openclaw/plugin-sdk/tool-send", "openclaw/plugin-sdk");
197
+
198
+ export const extractToolSend =
199
+ _toolSend.extractToolSend as typeof import("openclaw/plugin-sdk/tool-send").extractToolSend;
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // agent-runtime: jsonResult, readStringParam
203
+ // ---------------------------------------------------------------------------
204
+ const _agentRuntime = tryLoad("openclaw/plugin-sdk/agent-runtime", "openclaw/plugin-sdk");
205
+
206
+ export const jsonResult =
207
+ _agentRuntime.jsonResult as typeof import("openclaw/plugin-sdk/agent-runtime").jsonResult;
208
+
209
+ export const readStringParam =
210
+ _agentRuntime.readStringParam as typeof import("openclaw/plugin-sdk/agent-runtime").readStringParam;
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // channel-runtime: createReplyPrefixOptions
214
+ // ---------------------------------------------------------------------------
215
+ const _channelRuntime = tryLoad("openclaw/plugin-sdk/channel-runtime", "openclaw/plugin-sdk");
216
+
217
+ export const createReplyPrefixOptions =
218
+ _channelRuntime.createReplyPrefixOptions as typeof import("openclaw/plugin-sdk/channel-runtime").createReplyPrefixOptions;