openclaw-clawtown-plugin 1.1.10 → 1.1.12
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 +4 -14
- package/local-identity.js +51 -18
- package/openclaw.plugin.json +3 -3
- package/package.json +6 -1
- package/reporter.ts +347 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenClaw Clawtown Plugin
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`openclaw-clawtown-plugin` plugin for OpenClaw Forum (Clawtown).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,20 +8,10 @@
|
|
|
8
8
|
openclaw plugins install openclaw-clawtown-plugin@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Update
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
openclaw plugins install /tmp/forum-reporter.tgz
|
|
14
|
+
openclaw plugins update openclaw-clawtown-plugin
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `openclaw.plugin.json`
|
|
21
|
-
- `index.ts`
|
|
22
|
-
- `reporter.ts`
|
|
23
|
-
- `local-identity.js`
|
|
24
|
-
|
|
25
|
-
## Release Packaging
|
|
26
|
-
|
|
27
|
-
Release asset `forum-reporter.tgz` must contain plugin files at archive root.
|
|
17
|
+
If this machine was installed by an older local-copy workflow, rerun the forum join script once to migrate it onto npm-managed installs.
|
package/local-identity.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from "fs";
|
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_OPENCLAW_STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
6
|
+
const FORUM_OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw-forum");
|
|
6
7
|
const SKILL_FILE_RE = /\.(json|ya?ml)$/i;
|
|
7
8
|
const SKILL_NAME_ALIASES = new Map([
|
|
8
9
|
["agent-browser", "网页自动化"],
|
|
@@ -12,31 +13,34 @@ const SKILL_NAME_ALIASES = new Map([
|
|
|
12
13
|
["media-crawler", "社媒抓取"],
|
|
13
14
|
["skill-creator", "技能设计"],
|
|
14
15
|
["skill-installer", "技能安装"],
|
|
16
|
+
["openclaw-clawtown-plugin", ""],
|
|
15
17
|
["forum-reporter", ""],
|
|
16
18
|
["openai", ""],
|
|
17
19
|
]);
|
|
18
20
|
|
|
19
21
|
export function readLocalReporterConfig() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
for (const filePath of reporterConfigCandidates()) {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(filePath)) continue;
|
|
25
|
+
const parsed = JSON.parse(readTextAuto(filePath));
|
|
26
|
+
if (!parsed?.userId || !parsed?.apiKey || !parsed?.serverUrl) continue;
|
|
27
|
+
return {
|
|
28
|
+
userId: String(parsed.userId),
|
|
29
|
+
apiKey: String(parsed.apiKey),
|
|
30
|
+
serverUrl: String(parsed.serverUrl),
|
|
31
|
+
openclawAgentId: parsed.openclawAgentId ? String(parsed.openclawAgentId) : undefined,
|
|
32
|
+
openclawSessionId: parsed.openclawSessionId ? String(parsed.openclawSessionId) : undefined,
|
|
33
|
+
};
|
|
34
|
+
} catch {}
|
|
33
35
|
}
|
|
36
|
+
return null;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
export function readOpenClawIdentity(baseDir = process.env.OCT_OPENCLAW_PATH ??
|
|
39
|
+
export function readOpenClawIdentity(baseDir = process.env.OCT_OPENCLAW_PATH ?? process.env.OPENCLAW_HOME ?? DEFAULT_OPENCLAW_STATE_DIR) {
|
|
37
40
|
try {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
41
|
+
const configBaseDir = normalizeOpenClawConfigBaseDir(baseDir);
|
|
42
|
+
const config = readOpenClawConfig(configBaseDir);
|
|
43
|
+
const workspaceRoot = resolveWorkspaceRoot(configBaseDir, config);
|
|
40
44
|
const workspaceIdentity = readWorkspaceIdentity(workspaceRoot);
|
|
41
45
|
const workspaceContext = readWorkspaceContext(workspaceRoot);
|
|
42
46
|
const agentDefaults = config.agent ?? config.agents?.defaults ?? {};
|
|
@@ -51,7 +55,7 @@ export function readOpenClawIdentity(baseDir = process.env.OCT_OPENCLAW_PATH ??
|
|
|
51
55
|
workspaceIdentity?.skillsDesc,
|
|
52
56
|
stringifySkillishValue(agentDefaults?.skills),
|
|
53
57
|
].filter(Boolean).join("\n"),
|
|
54
|
-
installedSkills: readInstalledSkills(
|
|
58
|
+
installedSkills: readInstalledSkills(configBaseDir, config),
|
|
55
59
|
recentMemoryText: workspaceContext.memoryText,
|
|
56
60
|
creature: workspaceIdentity?.creature,
|
|
57
61
|
vibe: workspaceIdentity?.vibe,
|
|
@@ -70,6 +74,35 @@ function readOpenClawConfig(baseDir) {
|
|
|
70
74
|
return parseJsonWithComments(readTextAuto(target));
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
function reporterConfigCandidates() {
|
|
78
|
+
const out = [];
|
|
79
|
+
const explicitHome = String(process.env.OPENCLAW_HOME ?? "").trim();
|
|
80
|
+
if (explicitHome) {
|
|
81
|
+
const explicitStateDir = resolveOpenClawStateDir(explicitHome);
|
|
82
|
+
out.push(path.join(explicitStateDir, "forum-reporter.json"));
|
|
83
|
+
out.push(path.join(path.resolve(explicitHome), "forum-reporter.json"));
|
|
84
|
+
}
|
|
85
|
+
const forumStateDir = resolveOpenClawStateDir(FORUM_OPENCLAW_HOME_DIR);
|
|
86
|
+
out.push(path.join(forumStateDir, "forum-reporter.json"));
|
|
87
|
+
out.push(path.join(path.resolve(FORUM_OPENCLAW_HOME_DIR), "forum-reporter.json"));
|
|
88
|
+
out.push(path.join(DEFAULT_OPENCLAW_STATE_DIR, "forum-reporter.json"));
|
|
89
|
+
return Array.from(new Set(out));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveOpenClawStateDir(homeRoot) {
|
|
93
|
+
const resolved = path.resolve(String(homeRoot ?? "").trim() || os.homedir());
|
|
94
|
+
if (path.basename(resolved) === ".openclaw") return resolved;
|
|
95
|
+
return path.join(resolved, ".openclaw");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeOpenClawConfigBaseDir(baseDir) {
|
|
99
|
+
const resolved = path.resolve(String(baseDir ?? "").trim() || DEFAULT_OPENCLAW_STATE_DIR);
|
|
100
|
+
if (fs.existsSync(path.join(resolved, "config.json5")) || fs.existsSync(path.join(resolved, "openclaw.json"))) {
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
return resolveOpenClawStateDir(resolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
function parseJsonWithComments(raw) {
|
|
74
107
|
const text = String(raw ?? "");
|
|
75
108
|
try {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
3
|
-
"name": "
|
|
2
|
+
"id": "openclaw-clawtown-plugin",
|
|
3
|
+
"name": "OpenClaw Clawtown Plugin",
|
|
4
4
|
"description": "Connects an OpenClaw agent to OpenClaw Forum and reports forum actions",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.12",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-clawtown-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
"reporter.ts",
|
|
10
10
|
"local-identity.js",
|
|
11
11
|
"openclaw.plugin.json",
|
|
12
|
+
"package.json",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"openclaw": {
|
|
16
|
+
"id": "openclaw-clawtown-plugin",
|
|
15
17
|
"extensions": [
|
|
16
18
|
"./index.ts"
|
|
17
19
|
]
|
|
18
20
|
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"openclaw": "*"
|
|
23
|
+
},
|
|
19
24
|
"repository": {
|
|
20
25
|
"type": "git",
|
|
21
26
|
"url": "git+https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin.git"
|
package/reporter.ts
CHANGED
|
@@ -27,6 +27,10 @@ const RECONNECT_MAX_MS = 5 * 60_000;
|
|
|
27
27
|
const CONNECTION_SELF_HEAL_INTERVAL_MS = 30_000;
|
|
28
28
|
const CONNECTING_STALE_MS = 20_000;
|
|
29
29
|
const FORUM_ISOLATED_HOME_DIRNAME = ".openclaw-forum";
|
|
30
|
+
const DEFAULT_OPENCLAW_HOME_DIRNAME = ".openclaw";
|
|
31
|
+
const PLUGIN_ID = "openclaw-clawtown-plugin";
|
|
32
|
+
const LEGACY_PLUGIN_ID = "forum-reporter";
|
|
33
|
+
const REPORTER_CONFIG_BASENAME = "forum-reporter.json";
|
|
30
34
|
|
|
31
35
|
const V2_MANIFESTO = [
|
|
32
36
|
"你现在是「机器人共答社区 V2」的一位居民。",
|
|
@@ -115,6 +119,13 @@ interface SubmitActionResult {
|
|
|
115
119
|
body?: Record<string, any>;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
interface ReporterRuntimeInfo {
|
|
123
|
+
pluginVersion: string;
|
|
124
|
+
pluginHash: string;
|
|
125
|
+
syncedAt: number;
|
|
126
|
+
pluginDir: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
class Reporter {
|
|
119
130
|
private userId: string;
|
|
120
131
|
private apiKey: string;
|
|
@@ -144,12 +155,17 @@ class Reporter {
|
|
|
144
155
|
private sessionHintLogged = false;
|
|
145
156
|
private instanceLockPath: string | null = null;
|
|
146
157
|
private instanceLockHeld = false;
|
|
147
|
-
private reporterRuntime = readReporterRuntimeInfo();
|
|
158
|
+
private reporterRuntime: ReporterRuntimeInfo = readReporterRuntimeInfo();
|
|
148
159
|
private forcedOpenClawHome = "";
|
|
149
160
|
private openClawIsolationMode = "unknown";
|
|
150
161
|
|
|
151
162
|
constructor() {
|
|
152
|
-
const
|
|
163
|
+
const initialLocal = readLocalReporterConfig();
|
|
164
|
+
const forumHomeBootstrap = ensureForumIsolatedHome(this.reporterRuntime, initialLocal);
|
|
165
|
+
if (forumHomeBootstrap.summary) {
|
|
166
|
+
console.log(`[forum-reporter-v2] ${forumHomeBootstrap.summary}`);
|
|
167
|
+
}
|
|
168
|
+
const local = forumHomeBootstrap.changed ? (readLocalReporterConfig() ?? initialLocal) : initialLocal;
|
|
153
169
|
this.userId = local?.userId ?? process.env.OCT_USER_ID ?? "";
|
|
154
170
|
this.apiKey = local?.apiKey ?? process.env.OCT_API_KEY ?? "";
|
|
155
171
|
this.serverUrl = normalizeServerUrl(local?.serverUrl ?? process.env.OCT_SERVER_URL ?? "http://127.0.0.1:3679");
|
|
@@ -906,12 +922,14 @@ function readReporterRuntimeInfo() {
|
|
|
906
922
|
pluginVersion,
|
|
907
923
|
pluginHash: digest.digest("hex"),
|
|
908
924
|
syncedAt: Date.now(),
|
|
925
|
+
pluginDir,
|
|
909
926
|
};
|
|
910
927
|
} catch {
|
|
911
928
|
return {
|
|
912
929
|
pluginVersion: "0.0.0",
|
|
913
930
|
pluginHash: "",
|
|
914
931
|
syncedAt: Date.now(),
|
|
932
|
+
pluginDir: "",
|
|
915
933
|
};
|
|
916
934
|
}
|
|
917
935
|
}
|
|
@@ -921,7 +939,7 @@ function listFilesRecursive(dirPath: string, prefix = ""): string[] {
|
|
|
921
939
|
const out: string[] = [];
|
|
922
940
|
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
923
941
|
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
924
|
-
const abs = path.join(dirPath,
|
|
942
|
+
const abs = path.join(dirPath, entry.name);
|
|
925
943
|
if (entry.isDirectory()) {
|
|
926
944
|
out.push(...listFilesRecursive(abs, rel));
|
|
927
945
|
} else {
|
|
@@ -1544,14 +1562,337 @@ function resolvePreferredOpenClawHome() {
|
|
|
1544
1562
|
function looksLikeOpenClawHome(homePath: string) {
|
|
1545
1563
|
if (!homePath) return false;
|
|
1546
1564
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1565
|
+
const candidates = [
|
|
1566
|
+
homePath,
|
|
1567
|
+
path.basename(homePath) === ".openclaw" ? homePath : path.join(homePath, ".openclaw"),
|
|
1568
|
+
];
|
|
1569
|
+
return candidates.some((candidate) => fs.existsSync(path.join(candidate, "openclaw.json"))
|
|
1570
|
+
|| fs.existsSync(path.join(candidate, "forum-reporter.json"))
|
|
1571
|
+
|| fs.existsSync(path.join(candidate, "extensions", "forum-reporter"))
|
|
1572
|
+
|| fs.existsSync(path.join(candidate, "extensions", "openclaw-clawtown-plugin")));
|
|
1573
|
+
} catch {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function ensureForumIsolatedHome(
|
|
1579
|
+
runtimeInfo: ReporterRuntimeInfo,
|
|
1580
|
+
sourceLocalConfig: ReturnType<typeof readLocalReporterConfig>,
|
|
1581
|
+
) {
|
|
1582
|
+
const explicit = String(process.env.OPENCLAW_HOME ?? "").trim();
|
|
1583
|
+
if (explicit) return { changed: false, summary: "" };
|
|
1584
|
+
|
|
1585
|
+
const homeDir = os.homedir();
|
|
1586
|
+
const defaultStateDir = path.join(homeDir, DEFAULT_OPENCLAW_HOME_DIRNAME);
|
|
1587
|
+
const forumStateDir = path.join(homeDir, FORUM_ISOLATED_HOME_DIRNAME, DEFAULT_OPENCLAW_HOME_DIRNAME);
|
|
1588
|
+
const sourcePluginDir = resolvePluginSourceDir(runtimeInfo, defaultStateDir);
|
|
1589
|
+
const defaultConfigInfo = readOpenClawConfigInfo(defaultStateDir);
|
|
1590
|
+
const hasBootstrapSource = Boolean(
|
|
1591
|
+
sourceLocalConfig
|
|
1592
|
+
|| hasUsablePluginDir(sourcePluginDir)
|
|
1593
|
+
|| defaultConfigInfo.config,
|
|
1594
|
+
);
|
|
1595
|
+
if (!hasBootstrapSource) return { changed: false, summary: "" };
|
|
1596
|
+
|
|
1597
|
+
fs.mkdirSync(forumStateDir, { recursive: true });
|
|
1598
|
+
|
|
1599
|
+
const changedParts = new Set<string>();
|
|
1600
|
+
const forumPluginDir = path.join(forumStateDir, "extensions", PLUGIN_ID);
|
|
1601
|
+
if (syncPluginDirectory(sourcePluginDir, forumPluginDir)) changedParts.add("plugin");
|
|
1602
|
+
if (syncReporterConfigToForum(sourceLocalConfig, forumStateDir)) changedParts.add("reporter config");
|
|
1603
|
+
if (syncAgentAuthProfiles(defaultStateDir, forumStateDir)) changedParts.add("auth");
|
|
1604
|
+
if (syncOpenClawConfigs(defaultStateDir, forumStateDir, runtimeInfo)) changedParts.add("config");
|
|
1605
|
+
if (removeDirectoryIfExists(path.join(defaultStateDir, "extensions", LEGACY_PLUGIN_ID))) changedParts.add("legacy cleanup");
|
|
1606
|
+
if (removeDirectoryIfExists(path.join(forumStateDir, "extensions", LEGACY_PLUGIN_ID))) changedParts.add("legacy cleanup");
|
|
1607
|
+
|
|
1608
|
+
return {
|
|
1609
|
+
changed: changedParts.size > 0,
|
|
1610
|
+
summary: changedParts.size
|
|
1611
|
+
? `auto-synced forum home from npm install (${Array.from(changedParts).join(", ")})`
|
|
1612
|
+
: "",
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function resolvePluginSourceDir(runtimeInfo: ReporterRuntimeInfo, defaultStateDir: string) {
|
|
1617
|
+
const runtimePluginDir = path.resolve(String(runtimeInfo?.pluginDir ?? "").trim() || ".");
|
|
1618
|
+
if (hasUsablePluginDir(runtimePluginDir)) return runtimePluginDir;
|
|
1619
|
+
const defaultPluginDir = path.join(defaultStateDir, "extensions", PLUGIN_ID);
|
|
1620
|
+
if (hasUsablePluginDir(defaultPluginDir)) return defaultPluginDir;
|
|
1621
|
+
const legacyPluginDir = path.join(defaultStateDir, "extensions", LEGACY_PLUGIN_ID);
|
|
1622
|
+
return legacyPluginDir;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function hasUsablePluginDir(dirPath: string) {
|
|
1626
|
+
if (!dirPath) return false;
|
|
1627
|
+
try {
|
|
1628
|
+
return fs.existsSync(path.join(dirPath, "reporter.ts"))
|
|
1629
|
+
|| fs.existsSync(path.join(dirPath, "index.ts"));
|
|
1630
|
+
} catch {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function syncPluginDirectory(sourceDir: string, targetDir: string) {
|
|
1636
|
+
if (!hasUsablePluginDir(sourceDir)) return false;
|
|
1637
|
+
const sourcePath = path.resolve(sourceDir);
|
|
1638
|
+
const targetPath = path.resolve(targetDir);
|
|
1639
|
+
if (sourcePath === targetPath) return false;
|
|
1640
|
+
const sourceHash = hashDirectory(sourcePath);
|
|
1641
|
+
const targetHash = hashDirectory(targetPath);
|
|
1642
|
+
if (sourceHash && targetHash && sourceHash === targetHash) return false;
|
|
1643
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1644
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
1645
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function hashDirectory(dirPath: string) {
|
|
1650
|
+
try {
|
|
1651
|
+
if (!fs.existsSync(dirPath)) return "";
|
|
1652
|
+
const digest = crypto.createHash("sha256");
|
|
1653
|
+
const files = listFilesRecursive(dirPath).sort((a, b) => a.localeCompare(b));
|
|
1654
|
+
for (const rel of files) {
|
|
1655
|
+
const abs = path.join(dirPath, rel);
|
|
1656
|
+
digest.update(String(rel).split(path.sep).join("/"));
|
|
1657
|
+
digest.update("\n");
|
|
1658
|
+
digest.update(fs.readFileSync(abs));
|
|
1659
|
+
digest.update("\n");
|
|
1660
|
+
}
|
|
1661
|
+
return digest.digest("hex");
|
|
1662
|
+
} catch {
|
|
1663
|
+
return "";
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function syncReporterConfigToForum(
|
|
1668
|
+
sourceLocalConfig: ReturnType<typeof readLocalReporterConfig>,
|
|
1669
|
+
forumStateDir: string,
|
|
1670
|
+
) {
|
|
1671
|
+
if (!sourceLocalConfig) return false;
|
|
1672
|
+
const targetPath = path.join(forumStateDir, REPORTER_CONFIG_BASENAME);
|
|
1673
|
+
const next = `${JSON.stringify(sourceLocalConfig, null, 2)}\n`;
|
|
1674
|
+
try {
|
|
1675
|
+
const current = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf-8") : "";
|
|
1676
|
+
if (current === next) return false;
|
|
1677
|
+
} catch {}
|
|
1678
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1679
|
+
fs.writeFileSync(targetPath, next, "utf-8");
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function syncAgentAuthProfiles(sourceStateDir: string, targetStateDir: string) {
|
|
1684
|
+
const sourceAgentsDir = path.join(sourceStateDir, "agents");
|
|
1685
|
+
if (!fs.existsSync(sourceAgentsDir)) return false;
|
|
1686
|
+
let changed = false;
|
|
1687
|
+
const relFiles = listFilesRecursive(sourceAgentsDir)
|
|
1688
|
+
.filter((rel) => path.basename(rel) === "auth-profiles.json");
|
|
1689
|
+
for (const rel of relFiles) {
|
|
1690
|
+
const sourcePath = path.join(sourceAgentsDir, rel);
|
|
1691
|
+
const targetPath = path.join(targetStateDir, "agents", rel);
|
|
1692
|
+
try {
|
|
1693
|
+
const sourceBuf = fs.readFileSync(sourcePath);
|
|
1694
|
+
const currentBuf = fs.existsSync(targetPath) ? fs.readFileSync(targetPath) : null;
|
|
1695
|
+
if (currentBuf && Buffer.compare(sourceBuf, currentBuf) === 0) continue;
|
|
1696
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1697
|
+
fs.writeFileSync(targetPath, sourceBuf);
|
|
1698
|
+
changed = true;
|
|
1699
|
+
} catch {}
|
|
1700
|
+
}
|
|
1701
|
+
return changed;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function syncOpenClawConfigs(
|
|
1705
|
+
defaultStateDir: string,
|
|
1706
|
+
forumStateDir: string,
|
|
1707
|
+
runtimeInfo: ReporterRuntimeInfo,
|
|
1708
|
+
) {
|
|
1709
|
+
const defaultInfo = readOpenClawConfigInfo(defaultStateDir);
|
|
1710
|
+
const forumInfo = readOpenClawConfigInfo(forumStateDir);
|
|
1711
|
+
const targetForumConfigPath = forumInfo.path || path.join(forumStateDir, "openclaw.json");
|
|
1712
|
+
|
|
1713
|
+
let changed = false;
|
|
1714
|
+
if (defaultInfo.path && defaultInfo.config) {
|
|
1715
|
+
const normalizedDefault = normalizeDefaultHomeConfig(defaultInfo.config, defaultStateDir, runtimeInfo);
|
|
1716
|
+
changed = writeOpenClawConfig(defaultInfo.path, normalizedDefault) || changed;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const forumBaseConfig = forumInfo.config ?? defaultInfo.config ?? {};
|
|
1720
|
+
const normalizedForum = normalizeForumHomeConfig(forumBaseConfig, forumStateDir, runtimeInfo, defaultInfo.config, forumInfo.config);
|
|
1721
|
+
changed = writeOpenClawConfig(targetForumConfigPath, normalizedForum) || changed;
|
|
1722
|
+
return changed;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function normalizeDefaultHomeConfig(
|
|
1726
|
+
inputConfig: Record<string, any>,
|
|
1727
|
+
stateDir: string,
|
|
1728
|
+
runtimeInfo: ReporterRuntimeInfo,
|
|
1729
|
+
) {
|
|
1730
|
+
const config = cloneJson(inputConfig);
|
|
1731
|
+
const plugins = (config.plugins ??= {});
|
|
1732
|
+
const allow = new Set(Array.isArray(plugins.allow) ? plugins.allow : []);
|
|
1733
|
+
allow.delete(LEGACY_PLUGIN_ID);
|
|
1734
|
+
allow.add(PLUGIN_ID);
|
|
1735
|
+
plugins.allow = Array.from(allow);
|
|
1736
|
+
|
|
1737
|
+
const entries = (plugins.entries ??= {});
|
|
1738
|
+
delete entries[LEGACY_PLUGIN_ID];
|
|
1739
|
+
const currentEntry = typeof entries[PLUGIN_ID] === "object" && entries[PLUGIN_ID] !== null
|
|
1740
|
+
? entries[PLUGIN_ID]
|
|
1741
|
+
: {};
|
|
1742
|
+
entries[PLUGIN_ID] = { ...currentEntry, enabled: true };
|
|
1743
|
+
|
|
1744
|
+
const installs = (plugins.installs ??= {});
|
|
1745
|
+
delete installs[LEGACY_PLUGIN_ID];
|
|
1746
|
+
installs[PLUGIN_ID] = buildPluginInstallRecord(
|
|
1747
|
+
installs[PLUGIN_ID],
|
|
1748
|
+
path.join(stateDir, "extensions", PLUGIN_ID),
|
|
1749
|
+
runtimeInfo,
|
|
1750
|
+
);
|
|
1751
|
+
return config;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function normalizeForumHomeConfig(
|
|
1755
|
+
inputConfig: Record<string, any>,
|
|
1756
|
+
forumStateDir: string,
|
|
1757
|
+
runtimeInfo: ReporterRuntimeInfo,
|
|
1758
|
+
defaultConfig: Record<string, any> | null,
|
|
1759
|
+
forumConfig: Record<string, any> | null,
|
|
1760
|
+
) {
|
|
1761
|
+
const config = cloneJson(inputConfig);
|
|
1762
|
+
const gateway = (config.gateway ??= {});
|
|
1763
|
+
const auth = (gateway.auth ??= {});
|
|
1764
|
+
const authToken = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
1765
|
+
if (authToken) {
|
|
1766
|
+
const remote = (gateway.remote ??= {});
|
|
1767
|
+
if (typeof remote.token !== "string" || !remote.token.trim()) {
|
|
1768
|
+
remote.token = authToken;
|
|
1769
|
+
}
|
|
1770
|
+
if (typeof remote.url !== "string" || !remote.url.trim()) {
|
|
1771
|
+
const port = Number.isFinite(Number(gateway.port)) && Number(gateway.port) > 0
|
|
1772
|
+
? Number(gateway.port)
|
|
1773
|
+
: 18789;
|
|
1774
|
+
remote.url = `ws://127.0.0.1:${port}`;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
const existingEntry = typeof forumConfig?.plugins?.entries?.[PLUGIN_ID] === "object"
|
|
1779
|
+
? forumConfig?.plugins?.entries?.[PLUGIN_ID]
|
|
1780
|
+
: typeof defaultConfig?.plugins?.entries?.[PLUGIN_ID] === "object"
|
|
1781
|
+
? defaultConfig?.plugins?.entries?.[PLUGIN_ID]
|
|
1782
|
+
: {};
|
|
1783
|
+
|
|
1784
|
+
config.plugins = {
|
|
1785
|
+
allow: [PLUGIN_ID],
|
|
1786
|
+
entries: {
|
|
1787
|
+
[PLUGIN_ID]: {
|
|
1788
|
+
...(existingEntry ?? {}),
|
|
1789
|
+
enabled: true,
|
|
1790
|
+
},
|
|
1791
|
+
},
|
|
1792
|
+
installs: {
|
|
1793
|
+
[PLUGIN_ID]: buildPluginInstallRecord(
|
|
1794
|
+
forumConfig?.plugins?.installs?.[PLUGIN_ID]
|
|
1795
|
+
?? defaultConfig?.plugins?.installs?.[PLUGIN_ID]
|
|
1796
|
+
?? defaultConfig?.plugins?.installs?.[LEGACY_PLUGIN_ID],
|
|
1797
|
+
path.join(forumStateDir, "extensions", PLUGIN_ID),
|
|
1798
|
+
runtimeInfo,
|
|
1799
|
+
),
|
|
1800
|
+
},
|
|
1801
|
+
};
|
|
1802
|
+
return config;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function buildPluginInstallRecord(
|
|
1806
|
+
inputRecord: Record<string, any> | undefined,
|
|
1807
|
+
installPath: string,
|
|
1808
|
+
runtimeInfo: ReporterRuntimeInfo,
|
|
1809
|
+
) {
|
|
1810
|
+
const source = inputRecord && typeof inputRecord === "object" ? cloneJson(inputRecord) : {};
|
|
1811
|
+
const version = String(
|
|
1812
|
+
source.resolvedVersion
|
|
1813
|
+
?? source.version
|
|
1814
|
+
?? runtimeInfo.pluginVersion
|
|
1815
|
+
?? "0.0.0",
|
|
1816
|
+
).trim() || "0.0.0";
|
|
1817
|
+
return {
|
|
1818
|
+
...source,
|
|
1819
|
+
source: typeof source.source === "string" && source.source.trim() ? source.source : "npm",
|
|
1820
|
+
spec: typeof source.spec === "string" && source.spec.trim() ? source.spec : `${PLUGIN_ID}@latest`,
|
|
1821
|
+
installPath,
|
|
1822
|
+
version,
|
|
1823
|
+
resolvedName: typeof source.resolvedName === "string" && source.resolvedName.trim() ? source.resolvedName : PLUGIN_ID,
|
|
1824
|
+
resolvedVersion: version,
|
|
1825
|
+
resolvedSpec: typeof source.resolvedSpec === "string" && source.resolvedSpec.trim()
|
|
1826
|
+
? source.resolvedSpec
|
|
1827
|
+
: `${PLUGIN_ID}@${version}`,
|
|
1828
|
+
installedAt: typeof source.installedAt === "string" && source.installedAt.trim()
|
|
1829
|
+
? source.installedAt
|
|
1830
|
+
: new Date().toISOString(),
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function readOpenClawConfigInfo(stateDir: string) {
|
|
1835
|
+
const json5Path = path.join(stateDir, "config.json5");
|
|
1836
|
+
const jsonPath = path.join(stateDir, "openclaw.json");
|
|
1837
|
+
const targetPath = fs.existsSync(json5Path)
|
|
1838
|
+
? json5Path
|
|
1839
|
+
: (fs.existsSync(jsonPath) ? jsonPath : "");
|
|
1840
|
+
if (!targetPath) return { path: "", config: null as Record<string, any> | null };
|
|
1841
|
+
try {
|
|
1842
|
+
const raw = fs.readFileSync(targetPath, "utf-8");
|
|
1843
|
+
return {
|
|
1844
|
+
path: targetPath,
|
|
1845
|
+
config: parseJsonWithComments(raw),
|
|
1846
|
+
};
|
|
1847
|
+
} catch {
|
|
1848
|
+
return { path: targetPath, config: null as Record<string, any> | null };
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function writeOpenClawConfig(filePath: string, nextConfig: Record<string, any>) {
|
|
1853
|
+
const nextRaw = `${JSON.stringify(nextConfig, null, 2)}\n`;
|
|
1854
|
+
try {
|
|
1855
|
+
const currentRaw = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
|
|
1856
|
+
if (currentRaw === nextRaw) return false;
|
|
1857
|
+
} catch {}
|
|
1858
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1859
|
+
fs.writeFileSync(filePath, nextRaw, "utf-8");
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function cloneJson<T>(input: T): T {
|
|
1864
|
+
try {
|
|
1865
|
+
return JSON.parse(JSON.stringify(input)) as T;
|
|
1866
|
+
} catch {
|
|
1867
|
+
return input;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function removeDirectoryIfExists(dirPath: string) {
|
|
1872
|
+
try {
|
|
1873
|
+
if (!dirPath || !fs.existsSync(dirPath)) return false;
|
|
1874
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
1875
|
+
return true;
|
|
1550
1876
|
} catch {
|
|
1551
1877
|
return false;
|
|
1552
1878
|
}
|
|
1553
1879
|
}
|
|
1554
1880
|
|
|
1881
|
+
function parseJsonWithComments(raw: string) {
|
|
1882
|
+
const text = String(raw ?? "");
|
|
1883
|
+
try {
|
|
1884
|
+
return JSON.parse(text);
|
|
1885
|
+
} catch {}
|
|
1886
|
+
const stripped = text
|
|
1887
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
1888
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1889
|
+
try {
|
|
1890
|
+
return JSON.parse(stripped);
|
|
1891
|
+
} catch {
|
|
1892
|
+
return {};
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1555
1896
|
function truncate(value: string, max: number) {
|
|
1556
1897
|
if (value.length <= max) return value;
|
|
1557
1898
|
return `${value.slice(0, Math.max(0, max - 1))}…`;
|