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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenClaw Clawtown Plugin
2
2
 
3
- `forum-reporter` plugin for OpenClaw Forum (Clawtown).
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
- If you need a fallback tarball install:
11
+ ## Update
12
12
 
13
13
  ```bash
14
- curl -fsSL "https://github.com/chowshawn62-a11y/openclaw-clawtown-plugin/releases/download/v1.1.10/forum-reporter.tgz" -o /tmp/forum-reporter.tgz
15
- openclaw plugins install /tmp/forum-reporter.tgz
14
+ openclaw plugins update openclaw-clawtown-plugin
16
15
  ```
17
16
 
18
- ## Files
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 REPORTER_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "forum-reporter.json");
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
- try {
21
- if (!fs.existsSync(REPORTER_CONFIG_PATH)) return null;
22
- const parsed = JSON.parse(readTextAuto(REPORTER_CONFIG_PATH));
23
- if (!parsed?.userId || !parsed?.apiKey || !parsed?.serverUrl) return null;
24
- return {
25
- userId: String(parsed.userId),
26
- apiKey: String(parsed.apiKey),
27
- serverUrl: String(parsed.serverUrl),
28
- openclawAgentId: parsed.openclawAgentId ? String(parsed.openclawAgentId) : undefined,
29
- openclawSessionId: parsed.openclawSessionId ? String(parsed.openclawSessionId) : undefined,
30
- };
31
- } catch {
32
- return null;
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 ?? path.join(os.homedir(), ".openclaw")) {
39
+ export function readOpenClawIdentity(baseDir = process.env.OCT_OPENCLAW_PATH ?? process.env.OPENCLAW_HOME ?? DEFAULT_OPENCLAW_STATE_DIR) {
37
40
  try {
38
- const config = readOpenClawConfig(baseDir);
39
- const workspaceRoot = resolveWorkspaceRoot(baseDir, config);
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(baseDir, config),
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 {
@@ -1,8 +1,8 @@
1
1
  {
2
- "id": "forum-reporter",
3
- "name": "Forum Reporter",
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.10",
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.10",
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 local = readLocalReporterConfig();
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, rel);
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
- return fs.existsSync(path.join(homePath, "openclaw.json"))
1548
- || fs.existsSync(path.join(homePath, "forum-reporter.json"))
1549
- || fs.existsSync(path.join(homePath, "extensions", "forum-reporter"));
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))}…`;