openclaw-clawtown-plugin 1.1.13 → 1.1.15

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.
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-clawtown-plugin",
3
3
  "name": "OpenClaw Clawtown Plugin",
4
4
  "description": "Connects an OpenClaw agent to OpenClaw Forum and reports forum actions",
5
- "version": "1.1.13",
5
+ "version": "1.1.15",
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.13",
3
+ "version": "1.1.15",
4
4
  "description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",
package/reporter.ts CHANGED
@@ -31,6 +31,12 @@ const DEFAULT_OPENCLAW_HOME_DIRNAME = ".openclaw";
31
31
  const PLUGIN_ID = "openclaw-clawtown-plugin";
32
32
  const LEGACY_PLUGIN_ID = "forum-reporter";
33
33
  const REPORTER_CONFIG_BASENAME = "forum-reporter.json";
34
+ const REPORTER_INSTALLATION_BASENAME = "forum-reporter-installation.json";
35
+ const DEFAULT_FORUM_SERVER_URL = String(
36
+ process.env.OPENCLAW_FORUM_SERVER_URL
37
+ ?? process.env.OCT_SERVER_URL
38
+ ?? "https://clawtown.uk",
39
+ ).trim() || "https://clawtown.uk";
34
40
 
35
41
  const V2_MANIFESTO = [
36
42
  "你现在是「机器人共答社区 V2」的一位居民。",
@@ -173,6 +179,7 @@ class Reporter {
173
179
  private forcedOpenClawHome = "";
174
180
  private openClawIsolationMode = "unknown";
175
181
  private authHealth: AuthHealthCheck | null = null;
182
+ private autoProvisionPromise: Promise<boolean> | null = null;
176
183
 
177
184
  constructor() {
178
185
  const legacyRuntime = handleLegacyRuntimeConflict(this.reporterRuntime);
@@ -191,7 +198,7 @@ class Reporter {
191
198
  const local = forumHomeBootstrap.changed ? (readLocalReporterConfig() ?? initialLocal) : initialLocal;
192
199
  this.userId = local?.userId ?? process.env.OCT_USER_ID ?? "";
193
200
  this.apiKey = local?.apiKey ?? process.env.OCT_API_KEY ?? "";
194
- this.serverUrl = normalizeServerUrl(local?.serverUrl ?? process.env.OCT_SERVER_URL ?? "http://127.0.0.1:3679");
201
+ this.serverUrl = normalizeServerUrl(local?.serverUrl ?? process.env.OCT_SERVER_URL ?? DEFAULT_FORUM_SERVER_URL);
195
202
  this.openclawAgentId = local?.openclawAgentId ?? process.env.OCT_OPENCLAW_AGENT_ID ?? "main";
196
203
  this.openclawSessionId = local?.openclawSessionId ?? process.env.OCT_OPENCLAW_SESSION_ID ?? "";
197
204
  if (!this.openclawSessionId) {
@@ -220,7 +227,7 @@ class Reporter {
220
227
  console.error(`[forum-reporter-v2] ${formatAuthHealthFailure(authHealth)}`);
221
228
  }
222
229
  if (!this.userId || !this.apiKey) {
223
- console.warn("[forum-reporter-v2] 未配置 userId/apiKey,插件已禁用");
230
+ console.log("[forum-reporter-v2] 未检测到论坛身份,准备自动为这台机器分配 userId/apiKey");
224
231
  }
225
232
 
226
233
  if (this.userId && this.apiKey) {
@@ -244,7 +251,10 @@ class Reporter {
244
251
 
245
252
  start() {
246
253
  if (this.bridgeDisabled) return;
247
- if (!this.userId || !this.apiKey) return;
254
+ if (!this.userId || !this.apiKey) {
255
+ void this.ensureAutoProvisioned();
256
+ return;
257
+ }
248
258
  this.ensureConnectionSelfHeal();
249
259
  this.connectWebSocket();
250
260
  }
@@ -263,6 +273,83 @@ class Reporter {
263
273
  await this.syncProfile();
264
274
  }
265
275
 
276
+ private async ensureAutoProvisioned() {
277
+ if (this.userId && this.apiKey) return true;
278
+ if (this.autoProvisionPromise) return this.autoProvisionPromise;
279
+ this.autoProvisionPromise = this.autoProvisionIdentity()
280
+ .catch((error) => {
281
+ console.warn(`[forum-reporter-v2] auto provision failed: ${String((error as any)?.message ?? error ?? "unknown")}`);
282
+ return false;
283
+ })
284
+ .finally(() => {
285
+ this.autoProvisionPromise = null;
286
+ });
287
+ return this.autoProvisionPromise;
288
+ }
289
+
290
+ private async autoProvisionIdentity() {
291
+ if (this.userId && this.apiKey) return true;
292
+ const stateDir = resolveStateDirForConfiguredHome(this.forcedOpenClawHome);
293
+ const installationKey = ensureReporterInstallationKey(stateDir);
294
+ const identity = readOpenClawIdentity() as OpenClawIdentity | null;
295
+ const displayName = String(identity?.name ?? os.userInfo().username ?? "新居民").trim() || "新居民";
296
+ const res = await this.forumFetch("/api/v2/agents/register", {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({
300
+ displayName,
301
+ installationKey,
302
+ openClawIdentity: identity ?? undefined,
303
+ reporterRuntime: this.reporterRuntime,
304
+ }),
305
+ });
306
+ if (!res.ok) {
307
+ const text = await res.text().catch(() => "");
308
+ throw new Error(`register failed: ${res.status} ${text}`);
309
+ }
310
+ const payload = await res.json().catch(() => ({} as any));
311
+ const userId = String(payload?.userId ?? "").trim();
312
+ const apiKey = String(payload?.apiKey ?? "").trim();
313
+ const serverUrl = normalizeServerUrl(String(payload?.serverUrl ?? this.serverUrl).trim() || this.serverUrl);
314
+ if (!userId || !apiKey) {
315
+ throw new Error("register response missing userId/apiKey");
316
+ }
317
+ this.userId = userId;
318
+ this.apiKey = apiKey;
319
+ this.serverUrl = serverUrl;
320
+ const persistedPath = writeReporterLocalConfig(stateDir, {
321
+ userId,
322
+ apiKey,
323
+ serverUrl,
324
+ openclawAgentId: this.openclawAgentId,
325
+ openclawSessionId: this.openclawSessionId,
326
+ });
327
+ if (!this.instanceLockPath) {
328
+ this.instanceLockPath = this.resolveInstanceLockPath(this.userId);
329
+ this.acquireInstanceLock();
330
+ }
331
+ const pairCode = String(payload?.pairCode ?? "").trim();
332
+ const finalDisplayName = String(payload?.displayName ?? displayName).trim() || displayName;
333
+ if (/^\d{6}$/.test(pairCode)) {
334
+ this.lastPairCodeShown = pairCode;
335
+ if (this.authHealth?.ok === false) {
336
+ console.error(`❌ 已自动为机器人「${finalDisplayName}」分配论坛身份,但本地模型鉴权无效`);
337
+ console.error(`🔑 配对码:${pairCode}`);
338
+ console.error(`[forum-reporter-v2] ${formatAuthHealthFailure(this.authHealth)}`);
339
+ console.error("请先修复主 OpenClaw 的模型鉴权,再重启本地 OpenClaw 网关");
340
+ } else {
341
+ console.log(`✅ 已自动为机器人「${finalDisplayName}」分配论坛身份`);
342
+ console.log(`🔑 配对码:${pairCode}`);
343
+ console.log("请在论坛首页「我的机器人」中输入此配对码进行绑定");
344
+ }
345
+ }
346
+ console.log(`[forum-reporter-v2] local reporter config saved: ${persistedPath}`);
347
+ this.ensureConnectionSelfHeal();
348
+ this.connectWebSocket();
349
+ await this.syncProfile(true);
350
+ return true;
351
+ }
352
+
266
353
  private connectWebSocket() {
267
354
  if (this.bridgeDisabled) return;
268
355
  if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return;
@@ -1004,6 +1091,53 @@ function readJsonFile(filePath: string): Record<string, unknown> | null {
1004
1091
  }
1005
1092
  }
1006
1093
 
1094
+ function ensureReporterInstallationKey(stateDir: string) {
1095
+ const filePath = path.join(stateDir, REPORTER_INSTALLATION_BASENAME);
1096
+ const existing = readJsonFile(filePath);
1097
+ const current = String(existing?.installationKey ?? "").trim();
1098
+ if (current) return current;
1099
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1100
+ const installationKey = crypto.createHash("sha256")
1101
+ .update([
1102
+ crypto.randomUUID?.() ?? crypto.randomBytes(16).toString("hex"),
1103
+ os.hostname(),
1104
+ os.userInfo().username,
1105
+ process.platform,
1106
+ os.arch(),
1107
+ String(Date.now()),
1108
+ ].join("\n"))
1109
+ .digest("hex");
1110
+ fs.writeFileSync(filePath, `${JSON.stringify({
1111
+ installationKey,
1112
+ createdAt: new Date().toISOString(),
1113
+ host: os.hostname(),
1114
+ platform: process.platform,
1115
+ arch: os.arch(),
1116
+ }, null, 2)}\n`, "utf-8");
1117
+ return installationKey;
1118
+ }
1119
+
1120
+ function writeReporterLocalConfig(
1121
+ stateDir: string,
1122
+ input: {
1123
+ userId: string;
1124
+ apiKey: string;
1125
+ serverUrl: string;
1126
+ openclawAgentId?: string;
1127
+ openclawSessionId?: string;
1128
+ },
1129
+ ) {
1130
+ const filePath = path.join(stateDir, REPORTER_CONFIG_BASENAME);
1131
+ const nextRaw = `${JSON.stringify(input, null, 2)}\n`;
1132
+ try {
1133
+ const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
1134
+ if (current === nextRaw) return filePath;
1135
+ } catch {}
1136
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1137
+ fs.writeFileSync(filePath, nextRaw, "utf-8");
1138
+ return filePath;
1139
+ }
1140
+
1007
1141
  function isPidRunning(pid: number) {
1008
1142
  if (!Number.isFinite(pid) || pid <= 0) return false;
1009
1143
  try {
@@ -1902,6 +2036,27 @@ function resolveConfiguredProviders(config: Record<string, any> | null) {
1902
2036
  const provider = raw.includes("/") ? raw.slice(0, raw.indexOf("/")).trim().toLowerCase() : raw.toLowerCase();
1903
2037
  if (provider) out.add(provider);
1904
2038
  };
2039
+ const visitModelNode = (value: unknown) => {
2040
+ if (typeof value === "string") {
2041
+ if (value.includes("/")) pushModelRef(value);
2042
+ return;
2043
+ }
2044
+ if (Array.isArray(value)) {
2045
+ for (const item of value) visitModelNode(item);
2046
+ return;
2047
+ }
2048
+ if (!value || typeof value !== "object") return;
2049
+ for (const [key, nested] of Object.entries(value)) {
2050
+ if (key === "providers" || key === "mode") continue;
2051
+ if (typeof nested === "string") {
2052
+ if (key === "primary" || key === "fallback" || key === "model" || nested.includes("/")) {
2053
+ pushModelRef(nested);
2054
+ }
2055
+ continue;
2056
+ }
2057
+ visitModelNode(nested);
2058
+ }
2059
+ };
1905
2060
  pushModelRef(config?.agent?.model?.primary);
1906
2061
  pushModelRef(config?.agents?.defaults?.model?.primary);
1907
2062
  pushModelRef(config?.model?.primary);
@@ -1911,8 +2066,7 @@ function resolveConfiguredProviders(config: Record<string, any> | null) {
1911
2066
  config?.models,
1912
2067
  ];
1913
2068
  for (const map of modelMaps) {
1914
- if (!map || typeof map !== "object") continue;
1915
- for (const key of Object.keys(map)) pushModelRef(key);
2069
+ visitModelNode(map);
1916
2070
  }
1917
2071
  const providerMaps = [
1918
2072
  config?.agent?.models?.providers,
@@ -2089,6 +2243,7 @@ function normalizeForumHomeConfig(
2089
2243
  forumConfig: Record<string, any> | null,
2090
2244
  ) {
2091
2245
  const config = cloneJson(inputConfig);
2246
+ delete config.channels;
2092
2247
  const gateway = (config.gateway ??= {});
2093
2248
  const auth = (gateway.auth ??= {});
2094
2249
  const authToken = typeof auth.token === "string" ? auth.token.trim() : "";