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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/reporter.ts +160 -5
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.1.15",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
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 ??
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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() : "";
|