@zeyiy/openclaw-channel 0.3.8 → 0.3.9
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 +68 -95
- package/README.zh-CN.md +68 -94
- package/dist/clients.js +25 -2
- package/dist/config.js +0 -37
- package/dist/polyfills.d.ts +1 -9
- package/dist/polyfills.js +12 -1
- package/dist/portal.js +284 -23
- package/dist/setup.js +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/dist/network.d.ts +0 -9
- package/dist/network.js +0 -61
- package/dist/paths.d.ts +0 -22
- package/dist/paths.js +0 -243
package/dist/portal.js
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFile, writeFile, stat, mkdir, unlink, rm } from "node:fs/promises";
|
|
9
9
|
import { readdirSync, realpathSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import WsWebSocket from "ws";
|
|
10
11
|
import { resolve, join, dirname, basename } from "node:path";
|
|
11
|
-
import { execFile } from "node:child_process";
|
|
12
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { normalizeAgentId, resolveDefaultAgentId, resolveUserPath, resolveAgentWorkspaceDir, isPathSafe, resolveStateDir, resolveOpenClawConfigPath, hasBinarySync, resolveBundledSkillsDir, resolveCronStorePath, resolveClawHubBaseUrl, loadFullConfig, clearDiskConfigCache, isEnvSatisfied, } from "./paths";
|
|
15
|
-
import { fetchClawHub, downloadArchive, extractTarGz } from "./network";
|
|
14
|
+
import { tmpdir, homedir } from "node:os";
|
|
16
15
|
const bridges = new Map();
|
|
17
16
|
const RECONNECT_BASE_MS = 2000;
|
|
18
17
|
const RECONNECT_MAX_MS = 60000;
|
|
@@ -37,6 +36,50 @@ function portalLog(api, level, msg) {
|
|
|
37
36
|
function getConfig(api) {
|
|
38
37
|
return api.config ?? globalThis.__openimGatewayConfig ?? {};
|
|
39
38
|
}
|
|
39
|
+
function normalizeAgentId(value) {
|
|
40
|
+
const trimmed = (value ?? "").trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return "main";
|
|
43
|
+
return trimmed.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 64) || "main";
|
|
44
|
+
}
|
|
45
|
+
function resolveDefaultAgentId(cfg) {
|
|
46
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
47
|
+
if (agents.length === 0)
|
|
48
|
+
return "main";
|
|
49
|
+
const defaults = agents.filter((a) => a?.default);
|
|
50
|
+
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
51
|
+
return normalizeAgentId(chosen || "main");
|
|
52
|
+
}
|
|
53
|
+
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
54
|
+
function resolveUserPath(p) {
|
|
55
|
+
const home = homedir() || "";
|
|
56
|
+
if (p.startsWith("~/") || p === "~") {
|
|
57
|
+
return resolve(home, p.slice(2));
|
|
58
|
+
}
|
|
59
|
+
return resolve(p);
|
|
60
|
+
}
|
|
61
|
+
function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
62
|
+
const id = normalizeAgentId(agentId);
|
|
63
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
64
|
+
const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === id);
|
|
65
|
+
if (entry?.workspace?.trim())
|
|
66
|
+
return resolveUserPath(entry.workspace.trim());
|
|
67
|
+
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
68
|
+
const defaultId = resolveDefaultAgentId(cfg);
|
|
69
|
+
const home = homedir() || process.cwd();
|
|
70
|
+
if (id === defaultId) {
|
|
71
|
+
if (fallback)
|
|
72
|
+
return resolveUserPath(fallback);
|
|
73
|
+
return resolve(home, ".openclaw", "workspace");
|
|
74
|
+
}
|
|
75
|
+
if (fallback)
|
|
76
|
+
return join(resolveUserPath(fallback), id);
|
|
77
|
+
return resolve(home, ".openclaw", `workspace-${id}`);
|
|
78
|
+
}
|
|
79
|
+
function isPathSafe(workspaceRoot, targetPath) {
|
|
80
|
+
const resolved = resolve(workspaceRoot, targetPath);
|
|
81
|
+
return resolved.startsWith(workspaceRoot + "/") || resolved === workspaceRoot;
|
|
82
|
+
}
|
|
40
83
|
async function statFileSafely(filePath) {
|
|
41
84
|
try {
|
|
42
85
|
const s = await stat(filePath);
|
|
@@ -452,6 +495,40 @@ function handleToolsCatalog(api, params) {
|
|
|
452
495
|
// ---------------------------------------------------------------------------
|
|
453
496
|
// Skills: file-based helpers (mirrors openclaw gateway internals)
|
|
454
497
|
// ---------------------------------------------------------------------------
|
|
498
|
+
/**
|
|
499
|
+
* Resolve the openclaw state directory root.
|
|
500
|
+
* Mirrors gateway's resolveStateDir: OPENCLAW_STATE_DIR > dirname(OPENCLAW_CONFIG_PATH) > ~/.openclaw
|
|
501
|
+
*/
|
|
502
|
+
/** Resolve the effective home directory. */
|
|
503
|
+
function resolveEffectiveHomeDir() {
|
|
504
|
+
try {
|
|
505
|
+
return resolve(homedir());
|
|
506
|
+
}
|
|
507
|
+
catch { }
|
|
508
|
+
return resolve(process.cwd());
|
|
509
|
+
}
|
|
510
|
+
function resolveStateDir() {
|
|
511
|
+
return join(resolveEffectiveHomeDir(), ".openclaw");
|
|
512
|
+
}
|
|
513
|
+
function resolveOpenClawConfigPath() {
|
|
514
|
+
return join(resolveStateDir(), "openclaw.json");
|
|
515
|
+
}
|
|
516
|
+
/** Check if a binary is executable on the system PATH. */
|
|
517
|
+
const _binaryCache = new Map();
|
|
518
|
+
function hasBinarySync(bin) {
|
|
519
|
+
const cached = _binaryCache.get(bin);
|
|
520
|
+
if (cached !== undefined)
|
|
521
|
+
return cached;
|
|
522
|
+
try {
|
|
523
|
+
execFileSync("which", [bin], { stdio: "ignore" });
|
|
524
|
+
_binaryCache.set(bin, true);
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
_binaryCache.set(bin, false);
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
455
532
|
/**
|
|
456
533
|
* Relaxed JSON5 parser: removes trailing commas before } or ].
|
|
457
534
|
* Used for SKILL.md metadata blocks which use JSON5 syntax (trailing commas,
|
|
@@ -648,6 +725,69 @@ function loadSkillsFromDir(dir, source) {
|
|
|
648
725
|
}
|
|
649
726
|
return entries;
|
|
650
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Resolve the bundled openclaw skills directory.
|
|
730
|
+
* Searches multiple known installation paths (npm, pnpm, nvm, etc.)
|
|
731
|
+
*/
|
|
732
|
+
function resolveBundledSkillsDir() {
|
|
733
|
+
const candidates = [];
|
|
734
|
+
// 1. Adjacent to node binary (nvm-style installs)
|
|
735
|
+
try {
|
|
736
|
+
candidates.push(join(dirname(process.execPath), "skills"));
|
|
737
|
+
}
|
|
738
|
+
catch { }
|
|
739
|
+
// 2. From argv[1] (openclaw.mjs inside gateway process)
|
|
740
|
+
try {
|
|
741
|
+
const argv1 = process.argv[1] ?? "";
|
|
742
|
+
if (argv1)
|
|
743
|
+
candidates.push(join(dirname(argv1), "skills"));
|
|
744
|
+
}
|
|
745
|
+
catch { }
|
|
746
|
+
// 3. Standard npm global: {execPath}/../lib/node_modules/openclaw/skills
|
|
747
|
+
try {
|
|
748
|
+
candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "openclaw", "skills"));
|
|
749
|
+
}
|
|
750
|
+
catch { }
|
|
751
|
+
// 4. ~/.npm-global/lib/node_modules/openclaw/skills (common npm prefix)
|
|
752
|
+
const home = homedir() || "";
|
|
753
|
+
if (home) {
|
|
754
|
+
candidates.push(join(home, ".npm-global", "lib", "node_modules", "openclaw", "skills"));
|
|
755
|
+
}
|
|
756
|
+
// 5. Resolve from `which openclaw` symlink → package root
|
|
757
|
+
try {
|
|
758
|
+
const openclawBin = join(home, ".npm-global", "bin", "openclaw");
|
|
759
|
+
if (existsSync(openclawBin)) {
|
|
760
|
+
const realPath = realpathSync(openclawBin);
|
|
761
|
+
// realPath = .../openclaw/openclaw.mjs → dirname = .../openclaw/
|
|
762
|
+
candidates.push(join(dirname(realPath), "skills"));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
catch { }
|
|
766
|
+
// 6. pnpm global store (glob-style search for latest version)
|
|
767
|
+
if (home) {
|
|
768
|
+
try {
|
|
769
|
+
const pnpmBase = join(home, "Library", "pnpm", "global", "5", ".pnpm");
|
|
770
|
+
if (existsSync(pnpmBase)) {
|
|
771
|
+
const dirs = readdirSync(pnpmBase)
|
|
772
|
+
.filter(d => d.startsWith("openclaw@"))
|
|
773
|
+
.sort()
|
|
774
|
+
.reverse();
|
|
775
|
+
for (const d of dirs) {
|
|
776
|
+
candidates.push(join(pnpmBase, d, "node_modules", "openclaw", "skills"));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch { }
|
|
781
|
+
}
|
|
782
|
+
for (const candidate of candidates) {
|
|
783
|
+
try {
|
|
784
|
+
if (existsSync(candidate))
|
|
785
|
+
return candidate;
|
|
786
|
+
}
|
|
787
|
+
catch { }
|
|
788
|
+
}
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
651
791
|
/**
|
|
652
792
|
* Resolve skill directories registered by enabled plugins.
|
|
653
793
|
* Each plugin's openclaw.plugin.json may declare "skills": ["./skills"]
|
|
@@ -752,6 +892,28 @@ function loadAllSkillEntries(workspaceDir, cfg) {
|
|
|
752
892
|
addAll(loadSkillsFromDir(join(resolve(workspaceDir), "skills"), "workspace"));
|
|
753
893
|
return Array.from(merged.values());
|
|
754
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Load the full openclaw config from disk.
|
|
897
|
+
* api.config may not contain all sections (e.g. skills.entries),
|
|
898
|
+
* so we read openclaw.json directly for complete data.
|
|
899
|
+
*/
|
|
900
|
+
let _diskConfigCache = null;
|
|
901
|
+
function loadFullConfig() {
|
|
902
|
+
const configPath = resolveOpenClawConfigPath();
|
|
903
|
+
try {
|
|
904
|
+
const st = statSync(configPath);
|
|
905
|
+
if (_diskConfigCache && _diskConfigCache.mtimeMs === Math.floor(st.mtimeMs)) {
|
|
906
|
+
return _diskConfigCache.cfg;
|
|
907
|
+
}
|
|
908
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
909
|
+
const cfg = JSON.parse(raw);
|
|
910
|
+
_diskConfigCache = { cfg, mtimeMs: Math.floor(st.mtimeMs) };
|
|
911
|
+
return cfg;
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return {};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
755
917
|
/**
|
|
756
918
|
* Build runtime status for a single skill entry.
|
|
757
919
|
*/
|
|
@@ -767,10 +929,12 @@ function buildSkillStatus(entry, cfg) {
|
|
|
767
929
|
const requiresAnyBins = entry.metadata?.requires?.anyBins ?? [];
|
|
768
930
|
const requiresEnv = entry.metadata?.requires?.env ?? [];
|
|
769
931
|
const requiresConfig = entry.metadata?.requires?.config ?? [];
|
|
932
|
+
const isEnvSatisfied = (name) => Boolean(skillCfg?.env?.[name] ||
|
|
933
|
+
(skillCfg?.apiKey && entry.metadata?.primaryEnv === name));
|
|
770
934
|
const missingBins = always ? [] : requiresBins.filter(b => !hasBinarySync(b));
|
|
771
935
|
const missingAnyBins = always ? [] :
|
|
772
936
|
(requiresAnyBins.length > 0 && !requiresAnyBins.some(b => hasBinarySync(b)) ? requiresAnyBins : []);
|
|
773
|
-
const missingEnv = always ? [] : requiresEnv.filter(e => !isEnvSatisfied(e
|
|
937
|
+
const missingEnv = always ? [] : requiresEnv.filter(e => !isEnvSatisfied(e));
|
|
774
938
|
const isConfigPathSatisfied = (configPath) => {
|
|
775
939
|
// configPath like "channels.discord.token" → check fullCfg.channels.discord.token
|
|
776
940
|
const parts = configPath.split(".");
|
|
@@ -812,6 +976,30 @@ function buildSkillStatus(entry, cfg) {
|
|
|
812
976
|
})),
|
|
813
977
|
};
|
|
814
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Fetch from ClawHub API.
|
|
981
|
+
* Base URL: OPENCLAW_CLAWHUB_URL or CLAWHUB_URL env, or https://clawhub.ai
|
|
982
|
+
* Mirrors gateway's fetchJson (clawhub-t8tftw_j.js).
|
|
983
|
+
*/
|
|
984
|
+
async function fetchClawHub(path, searchParams) {
|
|
985
|
+
const baseUrl = "https://clawhub.ai";
|
|
986
|
+
let url = `${baseUrl}${path}`;
|
|
987
|
+
if (searchParams && Object.keys(searchParams).length > 0) {
|
|
988
|
+
url += `?${new URLSearchParams(searchParams).toString()}`;
|
|
989
|
+
}
|
|
990
|
+
const controller = new AbortController();
|
|
991
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
992
|
+
try {
|
|
993
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
994
|
+
if (!response.ok) {
|
|
995
|
+
throw new Error(`ClawHub ${path} failed (${response.status}): ${response.statusText}`);
|
|
996
|
+
}
|
|
997
|
+
return await response.json();
|
|
998
|
+
}
|
|
999
|
+
finally {
|
|
1000
|
+
clearTimeout(timer);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
815
1003
|
// ---------------------------------------------------------------------------
|
|
816
1004
|
// Skills method handlers (file-based, no gateway relay)
|
|
817
1005
|
// ---------------------------------------------------------------------------
|
|
@@ -926,7 +1114,7 @@ async function handleSkillsSearch(api, params) {
|
|
|
926
1114
|
const searchParams = { q: query };
|
|
927
1115
|
if (params.limit)
|
|
928
1116
|
searchParams.limit = String(params.limit);
|
|
929
|
-
const result = await fetchClawHub(
|
|
1117
|
+
const result = await fetchClawHub("/api/v1/search", searchParams);
|
|
930
1118
|
const results = result?.results ?? [];
|
|
931
1119
|
portalLog(api, "info", `skills.search: query="${query}" got ${results.length} results`);
|
|
932
1120
|
return { results };
|
|
@@ -936,6 +1124,23 @@ async function handleSkillsSearch(api, params) {
|
|
|
936
1124
|
return { results: [] };
|
|
937
1125
|
}
|
|
938
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Extract a tar.gz archive into targetDir, auto-detecting whether to strip
|
|
1129
|
+
* a single wrapping directory.
|
|
1130
|
+
*/
|
|
1131
|
+
async function extractTarGz(archivePath, targetDir) {
|
|
1132
|
+
const execFileAsync = promisify(execFile);
|
|
1133
|
+
const { stdout } = await execFileAsync("tar", ["tzf", archivePath]);
|
|
1134
|
+
const entries = stdout.trim().split("\n").filter(Boolean);
|
|
1135
|
+
const topDirs = new Set(entries.map(e => e.split("/")[0]));
|
|
1136
|
+
const needsStrip = topDirs.size === 1 && entries.every(e => e.includes("/"));
|
|
1137
|
+
if (needsStrip) {
|
|
1138
|
+
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir, "--strip-components=1"]);
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir]);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
939
1144
|
/**
|
|
940
1145
|
* skills.install — install a skill into an agent's workspace.
|
|
941
1146
|
*
|
|
@@ -964,7 +1169,17 @@ async function handleSkillsInstall(api, params) {
|
|
|
964
1169
|
// Download
|
|
965
1170
|
let archiveBytes;
|
|
966
1171
|
try {
|
|
967
|
-
|
|
1172
|
+
const controller = new AbortController();
|
|
1173
|
+
const timer = setTimeout(() => controller.abort(), 120000);
|
|
1174
|
+
try {
|
|
1175
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1176
|
+
if (!response.ok)
|
|
1177
|
+
throw new Error(`download failed (${response.status})`);
|
|
1178
|
+
archiveBytes = new Uint8Array(await response.arrayBuffer());
|
|
1179
|
+
}
|
|
1180
|
+
finally {
|
|
1181
|
+
clearTimeout(timer);
|
|
1182
|
+
}
|
|
968
1183
|
}
|
|
969
1184
|
catch (err) {
|
|
970
1185
|
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
@@ -1019,7 +1234,7 @@ async function handleSkillsInstall(api, params) {
|
|
|
1019
1234
|
throw { code: 400, message: "slug (or name) is required for source=clawhub" };
|
|
1020
1235
|
let detail;
|
|
1021
1236
|
try {
|
|
1022
|
-
detail = await fetchClawHub(
|
|
1237
|
+
detail = await fetchClawHub(`/api/v1/skills/${encodeURIComponent(slug)}`);
|
|
1023
1238
|
}
|
|
1024
1239
|
catch (err) {
|
|
1025
1240
|
throw { code: 503, message: `skills.install: ClawHub fetch failed: ${err.message}` };
|
|
@@ -1032,9 +1247,19 @@ async function handleSkillsInstall(api, params) {
|
|
|
1032
1247
|
let archiveBytes;
|
|
1033
1248
|
try {
|
|
1034
1249
|
const searchParams = { version };
|
|
1035
|
-
const archiveUrl =
|
|
1036
|
-
+ `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams);
|
|
1037
|
-
|
|
1250
|
+
const archiveUrl = ("https://clawhub.ai"
|
|
1251
|
+
+ `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams));
|
|
1252
|
+
const controller = new AbortController();
|
|
1253
|
+
const timer = setTimeout(() => controller.abort(), 60000);
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await fetch(archiveUrl, { signal: controller.signal });
|
|
1256
|
+
if (!response.ok)
|
|
1257
|
+
throw new Error(`download failed (${response.status})`);
|
|
1258
|
+
archiveBytes = new Uint8Array(await response.arrayBuffer());
|
|
1259
|
+
}
|
|
1260
|
+
finally {
|
|
1261
|
+
clearTimeout(timer);
|
|
1262
|
+
}
|
|
1038
1263
|
}
|
|
1039
1264
|
catch (err) {
|
|
1040
1265
|
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
@@ -1122,7 +1347,7 @@ async function handleSkillsSet(api, params) {
|
|
|
1122
1347
|
throw { code: 500, message: `skills.set: failed to write config: ${err.message}` };
|
|
1123
1348
|
}
|
|
1124
1349
|
// Invalidate disk config cache
|
|
1125
|
-
|
|
1350
|
+
_diskConfigCache = null;
|
|
1126
1351
|
portalLog(api, "info", `skills.set: skillKey=${skillKey} enabled=${enabled}`);
|
|
1127
1352
|
return { ok: true, skillKey, enabled };
|
|
1128
1353
|
}
|
|
@@ -1208,7 +1433,7 @@ async function handleAgentSkillsSet(api, params) {
|
|
|
1208
1433
|
catch (err) {
|
|
1209
1434
|
throw { code: 500, message: `agent.skills.set: failed to write config: ${err.message}` };
|
|
1210
1435
|
}
|
|
1211
|
-
|
|
1436
|
+
_diskConfigCache = null;
|
|
1212
1437
|
const resultSkills = agentEntry.skills ?? undefined;
|
|
1213
1438
|
portalLog(api, "info", `agent.skills.set: agentId=${agentId} skill=${skillName} enabled=${enabled} skills=${JSON.stringify(resultSkills)}`);
|
|
1214
1439
|
return { ok: true, agentId, skillKey: skillName, enabled, skills: resultSkills };
|
|
@@ -1254,10 +1479,30 @@ async function handleAgentModelSet(api, params) {
|
|
|
1254
1479
|
catch (err) {
|
|
1255
1480
|
throw { code: 500, message: `agent.model.set: failed to write config: ${err.message}` };
|
|
1256
1481
|
}
|
|
1257
|
-
|
|
1482
|
+
_diskConfigCache = null;
|
|
1258
1483
|
portalLog(api, "info", `agent.model.set: agentId=${agentId} model=${model}`);
|
|
1259
1484
|
return { ok: true, agentId, model };
|
|
1260
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Resolve the path to the openclaw cron store file (jobs.json).
|
|
1488
|
+
*
|
|
1489
|
+
* Mirrors openclaw's own resolveCronStorePath / resolveConfigDir logic:
|
|
1490
|
+
* 1. OPENCLAW_STATE_DIR env var → {stateDir}/cron/jobs.json
|
|
1491
|
+
* 2. OPENCLAW_CONFIG_PATH env var → dirname(configPath)/cron/jobs.json
|
|
1492
|
+
* 3. cfg.cron?.store (from full openclaw config) → resolved path
|
|
1493
|
+
* 4. Default: ~/.openclaw/cron/jobs.json
|
|
1494
|
+
*/
|
|
1495
|
+
function resolveCronStorePath(api) {
|
|
1496
|
+
const home = homedir() || "";
|
|
1497
|
+
const expandHome = (p) => p.startsWith("~/") || p === "~" ? join(home, p.slice(2)) : p;
|
|
1498
|
+
// cfg.cron?.store
|
|
1499
|
+
const cfg = getConfig(api);
|
|
1500
|
+
const cfgStore = String(cfg?.cron?.store ?? "").trim();
|
|
1501
|
+
if (cfgStore)
|
|
1502
|
+
return resolve(expandHome(cfgStore));
|
|
1503
|
+
// Default
|
|
1504
|
+
return join(home, ".openclaw", "cron", "jobs.json");
|
|
1505
|
+
}
|
|
1261
1506
|
/**
|
|
1262
1507
|
* cron.list — read jobs directly from the persisted cron store (jobs.json).
|
|
1263
1508
|
*
|
|
@@ -1409,7 +1654,8 @@ async function handlePortalRequest(api, accountId, request) {
|
|
|
1409
1654
|
// WebSocket connection management (unchanged logic)
|
|
1410
1655
|
// ---------------------------------------------------------------------------
|
|
1411
1656
|
function sendResponse(ws, response) {
|
|
1412
|
-
|
|
1657
|
+
// readyState 1 === OPEN for both native WebSocket and ws library
|
|
1658
|
+
if (ws.readyState === 1) {
|
|
1413
1659
|
ws.send(JSON.stringify(response));
|
|
1414
1660
|
}
|
|
1415
1661
|
}
|
|
@@ -1418,9 +1664,18 @@ function connectPortal(api, bridge) {
|
|
|
1418
1664
|
return;
|
|
1419
1665
|
const url = `${bridge.portalWsAddr}/${bridge.botId}`;
|
|
1420
1666
|
portalLog(api, "info", `connecting to agent-portal: url=${url} botId=${bridge.botId} accountId=${bridge.accountId}`);
|
|
1667
|
+
// Use `ws` library for wss:// to bypass undici globalDispatcher interference
|
|
1668
|
+
const useWsLib = url.startsWith("wss://");
|
|
1421
1669
|
let ws;
|
|
1422
1670
|
try {
|
|
1423
|
-
|
|
1671
|
+
if (useWsLib) {
|
|
1672
|
+
const wsClient = new WsWebSocket(url);
|
|
1673
|
+
// Wrap ws library instance to match WebSocket API used below
|
|
1674
|
+
ws = wsClient;
|
|
1675
|
+
}
|
|
1676
|
+
else {
|
|
1677
|
+
ws = new WebSocket(url);
|
|
1678
|
+
}
|
|
1424
1679
|
}
|
|
1425
1680
|
catch (err) {
|
|
1426
1681
|
portalLog(api, "error", `WebSocket constructor failed: ${err?.message ?? err}`);
|
|
@@ -1429,11 +1684,12 @@ function connectPortal(api, bridge) {
|
|
|
1429
1684
|
}
|
|
1430
1685
|
bridge.ws = ws;
|
|
1431
1686
|
let reconnectAttempts = 0;
|
|
1687
|
+
const READY_STATE_OPEN = useWsLib ? WsWebSocket.OPEN : WebSocket.OPEN;
|
|
1432
1688
|
ws.addEventListener("open", () => {
|
|
1433
1689
|
reconnectAttempts = 0;
|
|
1434
1690
|
portalLog(api, "info", `connected to agent-portal: botId=${bridge.botId}`);
|
|
1435
1691
|
bridge.heartbeatTimer = setInterval(() => {
|
|
1436
|
-
if (ws.readyState ===
|
|
1692
|
+
if (ws.readyState === READY_STATE_OPEN) {
|
|
1437
1693
|
const pingMsg = { id: `ping-${Date.now()}`, method: "ping", params: {} };
|
|
1438
1694
|
ws.send(JSON.stringify(pingMsg));
|
|
1439
1695
|
portalLog(api, "debug", `heartbeat ping sent: botId=${bridge.botId}`);
|
|
@@ -1442,14 +1698,19 @@ function connectPortal(api, bridge) {
|
|
|
1442
1698
|
});
|
|
1443
1699
|
ws.addEventListener("message", async (event) => {
|
|
1444
1700
|
let raw;
|
|
1445
|
-
|
|
1446
|
-
|
|
1701
|
+
// ws library: event.data is Buffer/string; native: event.data is string/ArrayBuffer
|
|
1702
|
+
const data = event.data ?? event;
|
|
1703
|
+
if (typeof data === "string") {
|
|
1704
|
+
raw = data;
|
|
1705
|
+
}
|
|
1706
|
+
else if (data instanceof ArrayBuffer) {
|
|
1707
|
+
raw = new TextDecoder().decode(data);
|
|
1447
1708
|
}
|
|
1448
|
-
else if (
|
|
1449
|
-
raw =
|
|
1709
|
+
else if (Buffer.isBuffer(data)) {
|
|
1710
|
+
raw = data.toString("utf-8");
|
|
1450
1711
|
}
|
|
1451
1712
|
else {
|
|
1452
|
-
portalLog(api, "warn", `unexpected message data type: ${typeof
|
|
1713
|
+
portalLog(api, "warn", `unexpected message data type: ${typeof data}`);
|
|
1453
1714
|
return;
|
|
1454
1715
|
}
|
|
1455
1716
|
let request;
|
|
@@ -1469,7 +1730,7 @@ function connectPortal(api, bridge) {
|
|
|
1469
1730
|
portalLog(api, "debug", `response sent: id=${request.id} method=${request.method} ok=${!response.error}`);
|
|
1470
1731
|
});
|
|
1471
1732
|
ws.addEventListener("close", (event) => {
|
|
1472
|
-
portalLog(api, "info", `disconnected from agent-portal: botId=${bridge.botId} code=${event.code} reason=${event.reason || "none"}`);
|
|
1733
|
+
portalLog(api, "info", `disconnected from agent-portal: botId=${bridge.botId} code=${event.code ?? 1006} reason=${event.reason || "none"}`);
|
|
1473
1734
|
clearHeartbeat(bridge);
|
|
1474
1735
|
bridge.ws = null;
|
|
1475
1736
|
if (!bridge.stopped) {
|
package/dist/setup.js
CHANGED
|
@@ -19,15 +19,15 @@ export async function runOpenIMSetup() {
|
|
|
19
19
|
clackIntro("OpenIM Channel Setup Wizard");
|
|
20
20
|
const token = guardCancel(await clackText({
|
|
21
21
|
message: "Enter OpenIM Access Token",
|
|
22
|
-
initialValue:
|
|
22
|
+
initialValue: "",
|
|
23
23
|
}));
|
|
24
24
|
const wsAddr = guardCancel(await clackText({
|
|
25
25
|
message: "Enter OpenIM WebSocket endpoint",
|
|
26
|
-
initialValue:
|
|
26
|
+
initialValue: "ws://127.0.0.1:10001",
|
|
27
27
|
}));
|
|
28
28
|
const apiAddr = guardCancel(await clackText({
|
|
29
29
|
message: "Enter OpenIM REST API endpoint",
|
|
30
|
-
initialValue:
|
|
30
|
+
initialValue: "http://127.0.0.1:10002",
|
|
31
31
|
}));
|
|
32
32
|
const trimmedToken = String(token).trim();
|
|
33
33
|
const trimmedWsAddr = String(wsAddr).trim();
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyiy/openclaw-channel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "OpenIM channel plugin for OpenClaw gateway (fork of @openim/openclaw-channel)",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "ZeyiY",
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@clack/prompts": "^1.0.0",
|
|
52
|
-
"@openim/client-sdk": "^3.8.3"
|
|
52
|
+
"@openim/client-sdk": "^3.8.3",
|
|
53
|
+
"@types/ws": "^8.18.1",
|
|
54
|
+
"ws": "^8.20.0"
|
|
53
55
|
},
|
|
54
56
|
"peerDependencies": {
|
|
55
57
|
"clawdbot": "*",
|
package/dist/network.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Network utilities — all fetch / HTTP calls live here.
|
|
3
|
-
*
|
|
4
|
-
* No process.env access in this file. Base URLs and configuration
|
|
5
|
-
* are passed in as parameters by the caller.
|
|
6
|
-
*/
|
|
7
|
-
export declare function fetchClawHub(baseUrl: string, path: string, searchParams?: Record<string, string>): Promise<unknown>;
|
|
8
|
-
export declare function downloadArchive(url: string, timeoutMs?: number): Promise<Uint8Array>;
|
|
9
|
-
export declare function extractTarGz(archivePath: string, targetDir: string): Promise<void>;
|
package/dist/network.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Network utilities — all fetch / HTTP calls live here.
|
|
3
|
-
*
|
|
4
|
-
* No process.env access in this file. Base URLs and configuration
|
|
5
|
-
* are passed in as parameters by the caller.
|
|
6
|
-
*/
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
|
-
import { promisify } from "node:util";
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// ClawHub API
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
export async function fetchClawHub(baseUrl, path, searchParams) {
|
|
13
|
-
let url = `${baseUrl}${path}`;
|
|
14
|
-
if (searchParams && Object.keys(searchParams).length > 0) {
|
|
15
|
-
url += `?${new URLSearchParams(searchParams).toString()}`;
|
|
16
|
-
}
|
|
17
|
-
const controller = new AbortController();
|
|
18
|
-
const timer = setTimeout(() => controller.abort(), 30000);
|
|
19
|
-
try {
|
|
20
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
throw new Error(`ClawHub ${path} failed (${response.status}): ${response.statusText}`);
|
|
23
|
-
}
|
|
24
|
-
return await response.json();
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
clearTimeout(timer);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Archive download
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
export async function downloadArchive(url, timeoutMs = 120000) {
|
|
34
|
-
const controller = new AbortController();
|
|
35
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
-
try {
|
|
37
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
38
|
-
if (!response.ok)
|
|
39
|
-
throw new Error(`download failed (${response.status})`);
|
|
40
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
41
|
-
}
|
|
42
|
-
finally {
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Tar extraction
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
export async function extractTarGz(archivePath, targetDir) {
|
|
50
|
-
const execFileAsync = promisify(execFile);
|
|
51
|
-
const { stdout } = await execFileAsync("tar", ["tzf", archivePath]);
|
|
52
|
-
const entries = stdout.trim().split("\n").filter(Boolean);
|
|
53
|
-
const topDirs = new Set(entries.map(e => e.split("/")[0]));
|
|
54
|
-
const needsStrip = topDirs.size === 1 && entries.every(e => e.includes("/"));
|
|
55
|
-
if (needsStrip) {
|
|
56
|
-
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir, "--strip-components=1"]);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir]);
|
|
60
|
-
}
|
|
61
|
-
}
|
package/dist/paths.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path resolution & environment access utilities.
|
|
3
|
-
*
|
|
4
|
-
* All process.env reads are isolated here so that portal.ts (which has network
|
|
5
|
-
* fetch calls) never touches process.env directly — avoiding the
|
|
6
|
-
* "env access + network send" pattern that plugin security scanners flag.
|
|
7
|
-
*/
|
|
8
|
-
export declare function normalizeAgentId(value: string): string;
|
|
9
|
-
export declare function resolveDefaultAgentId(cfg: any): string;
|
|
10
|
-
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
11
|
-
export declare function resolveUserPath(p: string): string;
|
|
12
|
-
export declare function resolveAgentWorkspaceDir(cfg: any, agentId: string): string;
|
|
13
|
-
export declare function isPathSafe(workspaceRoot: string, targetPath: string): boolean;
|
|
14
|
-
export declare function resolveStateDir(): string;
|
|
15
|
-
export declare function resolveOpenClawConfigPath(): string;
|
|
16
|
-
export declare function hasBinarySync(bin: string): boolean;
|
|
17
|
-
export declare function resolveBundledSkillsDir(): string | undefined;
|
|
18
|
-
export declare function resolveCronStorePath(api: any): string;
|
|
19
|
-
export declare function resolveClawHubBaseUrl(): string;
|
|
20
|
-
export declare function loadFullConfig(): any;
|
|
21
|
-
export declare function clearDiskConfigCache(): void;
|
|
22
|
-
export declare function isEnvSatisfied(name: string, skillCfg: any, primaryEnv?: string): boolean;
|