@clawhive/openclaw-plugin 0.2.0
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 +217 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +324 -0
- package/dist/runtime-api.d.ts +1 -0
- package/dist/runtime-api.js +1 -0
- package/dist/setup-entry.d.ts +4 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/agency-install/claimLoop.d.ts +41 -0
- package/dist/src/agency-install/claimLoop.js +188 -0
- package/dist/src/agent-panel/claimLoop.d.ts +32 -0
- package/dist/src/agent-panel/claimLoop.js +118 -0
- package/dist/src/agent-panel/executor.d.ts +8 -0
- package/dist/src/agent-panel/executor.js +368 -0
- package/dist/src/agent-panel/plan.d.ts +20 -0
- package/dist/src/agent-panel/plan.js +111 -0
- package/dist/src/agent-panel/prompts.d.ts +38 -0
- package/dist/src/agent-panel/prompts.js +87 -0
- package/dist/src/agent-panel/types.d.ts +45 -0
- package/dist/src/agent-panel/types.js +1 -0
- package/dist/src/agents.d.ts +44 -0
- package/dist/src/agents.js +195 -0
- package/dist/src/authorization.d.ts +34 -0
- package/dist/src/authorization.js +183 -0
- package/dist/src/channel.d.ts +5 -0
- package/dist/src/channel.js +93 -0
- package/dist/src/claimPolling.d.ts +9 -0
- package/dist/src/claimPolling.js +13 -0
- package/dist/src/client.d.ts +386 -0
- package/dist/src/client.js +595 -0
- package/dist/src/config.d.ts +28 -0
- package/dist/src/config.js +94 -0
- package/dist/src/defaults.d.ts +40 -0
- package/dist/src/defaults.js +52 -0
- package/dist/src/deviceCode.d.ts +16 -0
- package/dist/src/deviceCode.js +65 -0
- package/dist/src/heartbeat.d.ts +25 -0
- package/dist/src/heartbeat.js +65 -0
- package/dist/src/imageAttachments.d.ts +14 -0
- package/dist/src/imageAttachments.js +81 -0
- package/dist/src/inbound.d.ts +10 -0
- package/dist/src/inbound.js +140 -0
- package/dist/src/installMutex.d.ts +4 -0
- package/dist/src/installMutex.js +18 -0
- package/dist/src/market-install/claimLoop.d.ts +41 -0
- package/dist/src/market-install/claimLoop.js +183 -0
- package/dist/src/market-install/downloader.d.ts +36 -0
- package/dist/src/market-install/downloader.js +133 -0
- package/dist/src/market-install/executor.d.ts +28 -0
- package/dist/src/market-install/executor.js +352 -0
- package/dist/src/market-install/types.d.ts +62 -0
- package/dist/src/market-install/types.js +1 -0
- package/dist/src/market-install/verifier.d.ts +32 -0
- package/dist/src/market-install/verifier.js +60 -0
- package/dist/src/market-publish/packager.d.ts +34 -0
- package/dist/src/market-publish/packager.js +168 -0
- package/dist/src/market-publish/publishFlow.d.ts +70 -0
- package/dist/src/market-publish/publishFlow.js +107 -0
- package/dist/src/market-publish/uploader.d.ts +73 -0
- package/dist/src/market-publish/uploader.js +132 -0
- package/dist/src/openclawVersion.d.ts +4 -0
- package/dist/src/openclawVersion.js +13 -0
- package/dist/src/outbound.d.ts +13 -0
- package/dist/src/outbound.js +41 -0
- package/dist/src/pairing.d.ts +32 -0
- package/dist/src/pairing.js +64 -0
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +26 -0
- package/dist/src/telemetry.d.ts +34 -0
- package/dist/src/telemetry.js +89 -0
- package/dist/src/transport/realtime.d.ts +49 -0
- package/dist/src/transport/realtime.js +273 -0
- package/dist/src/transport.d.ts +38 -0
- package/dist/src/transport.js +15 -0
- package/dist/src/wake.d.ts +31 -0
- package/dist/src/wake.js +101 -0
- package/openclaw.config.example.yaml +10 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +84 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { resolveSupabaseDefaults } from "./defaults.js";
|
|
3
|
+
export const CLAWHIVE_CHANNEL_ID = "clawhive";
|
|
4
|
+
export function readRawChannelSection(cfg) {
|
|
5
|
+
const channels = cfg.channels ?? {};
|
|
6
|
+
return channels[CLAWHIVE_CHANNEL_ID] ?? {};
|
|
7
|
+
}
|
|
8
|
+
function readAllowFrom(raw) {
|
|
9
|
+
if (!Array.isArray(raw))
|
|
10
|
+
return [];
|
|
11
|
+
return raw.filter((entry) => typeof entry === "string" || typeof entry === "number");
|
|
12
|
+
}
|
|
13
|
+
export function resolveResolvedAccount(cfg, accountId) {
|
|
14
|
+
const section = readRawChannelSection(cfg);
|
|
15
|
+
const rawUserId = section.userId ?? process.env.CLAWHIVE_USER_ID;
|
|
16
|
+
const resolvedSupabase = resolveSupabaseDefaults({
|
|
17
|
+
mode: process.env.CLAWHIVE_SUPABASE_MODE,
|
|
18
|
+
sectionProjectUrl: typeof section.projectUrl === "string" ? section.projectUrl : null,
|
|
19
|
+
sectionAnonKey: typeof section.anonKey === "string" ? section.anonKey : null,
|
|
20
|
+
envProjectUrl: process.env.CLAWHIVE_PROJECT_URL,
|
|
21
|
+
envAnonKey: process.env.CLAWHIVE_SUPABASE_ANON_KEY,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
|
25
|
+
projectUrl: resolvedSupabase.projectUrl,
|
|
26
|
+
anonKey: resolvedSupabase.anonKey,
|
|
27
|
+
pluginToken: String(section.pluginToken ?? process.env.CLAWHIVE_PLUGIN_TOKEN ?? ""),
|
|
28
|
+
userId: rawUserId ? String(rawUserId) : null,
|
|
29
|
+
pluginNodeId: section.pluginNodeId != null ? String(section.pluginNodeId) : null,
|
|
30
|
+
nodeName: String(section.nodeName ?? "OpenClaw Desktop"),
|
|
31
|
+
pluginVersion: String(section.pluginVersion ?? "0.2.0"),
|
|
32
|
+
publishSourceRef: typeof section.publishSourceRef === "string" ? section.publishSourceRef : null,
|
|
33
|
+
publishFingerprint: typeof section.publishFingerprint === "string" ? section.publishFingerprint : null,
|
|
34
|
+
minPluginVersion: typeof section.minPluginVersion === "string" ? section.minPluginVersion : null,
|
|
35
|
+
minOpenClawVersion: typeof section.minOpenClawVersion === "string" ? section.minOpenClawVersion : null,
|
|
36
|
+
dmPolicy: typeof section.dmPolicy === "string" ? section.dmPolicy : null,
|
|
37
|
+
allowFrom: readAllowFrom(section.allowFrom),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function writeChannelSection(cfg, input) {
|
|
41
|
+
const existing = readRawChannelSection(cfg);
|
|
42
|
+
const nextSection = {
|
|
43
|
+
...existing,
|
|
44
|
+
projectUrl: input.url ?? process.env.CLAWHIVE_PROJECT_URL ?? existing.projectUrl ?? "",
|
|
45
|
+
anonKey: process.env.CLAWHIVE_SUPABASE_ANON_KEY ?? existing.anonKey ?? "",
|
|
46
|
+
pluginToken: input.token ?? process.env.CLAWHIVE_PLUGIN_TOKEN ?? existing.pluginToken ?? "",
|
|
47
|
+
nodeName: input.name ?? existing.nodeName ?? "OpenClaw Desktop",
|
|
48
|
+
pluginVersion: existing.pluginVersion ?? "0.2.0",
|
|
49
|
+
publishSourceRef: existing.publishSourceRef ?? null,
|
|
50
|
+
publishFingerprint: existing.publishFingerprint ?? null,
|
|
51
|
+
minPluginVersion: existing.minPluginVersion ?? null,
|
|
52
|
+
minOpenClawVersion: existing.minOpenClawVersion ?? null,
|
|
53
|
+
};
|
|
54
|
+
const explicitUserId = input.userId ?? process.env.CLAWHIVE_USER_ID ?? existing.userId;
|
|
55
|
+
if (explicitUserId) {
|
|
56
|
+
nextSection.userId = explicitUserId;
|
|
57
|
+
}
|
|
58
|
+
const nextChannels = {
|
|
59
|
+
...(cfg.channels ?? {}),
|
|
60
|
+
[CLAWHIVE_CHANNEL_ID]: nextSection,
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
...cfg,
|
|
64
|
+
channels: nextChannels,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function writePluginNodeId(cfg, pluginNodeId) {
|
|
68
|
+
return patchChannelSection(cfg, { pluginNodeId });
|
|
69
|
+
}
|
|
70
|
+
export function writeRegistrationIdentity(cfg, patch) {
|
|
71
|
+
const next = {};
|
|
72
|
+
if (patch.userId)
|
|
73
|
+
next.userId = patch.userId;
|
|
74
|
+
if (patch.pluginNodeId)
|
|
75
|
+
next.pluginNodeId = patch.pluginNodeId;
|
|
76
|
+
if (patch.pluginToken)
|
|
77
|
+
next.pluginToken = patch.pluginToken;
|
|
78
|
+
if (patch.anonKey)
|
|
79
|
+
next.anonKey = patch.anonKey;
|
|
80
|
+
return patchChannelSection(cfg, next);
|
|
81
|
+
}
|
|
82
|
+
function patchChannelSection(cfg, patch) {
|
|
83
|
+
const nextChannels = {
|
|
84
|
+
...(cfg.channels ?? {}),
|
|
85
|
+
[CLAWHIVE_CHANNEL_ID]: {
|
|
86
|
+
...readRawChannelSection(cfg),
|
|
87
|
+
...patch,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
...cfg,
|
|
92
|
+
channels: nextChannels,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ClawHive cloud project URL baked into the plugin.
|
|
3
|
+
*
|
|
4
|
+
* Override at build time by setting `CLAWHIVE_DEFAULT_PROJECT_URL` before `tsc`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_PROJECT_URL: string;
|
|
7
|
+
/**
|
|
8
|
+
* Default ClawHive cloud anon key baked into the plugin.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_SUPABASE_ANON_KEY: string;
|
|
11
|
+
export type ClawHiveSupabaseMode = "cloud" | "local";
|
|
12
|
+
/**
|
|
13
|
+
* Normalize the optional runtime mode switch for choosing the Supabase target.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeSupabaseMode(value: string | undefined): ClawHiveSupabaseMode | null;
|
|
16
|
+
type ResolveSupabaseDefaultsInput = {
|
|
17
|
+
mode?: string;
|
|
18
|
+
sectionProjectUrl?: string | null;
|
|
19
|
+
sectionAnonKey?: string | null;
|
|
20
|
+
envProjectUrl?: string | null;
|
|
21
|
+
envAnonKey?: string | null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the plugin Supabase URL and anon key from legacy config sources or the
|
|
25
|
+
* explicit mode switch when present.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSupabaseDefaults({ mode, sectionProjectUrl, sectionAnonKey, envProjectUrl, envAnonKey, }: ResolveSupabaseDefaultsInput): {
|
|
28
|
+
supabaseMode: "cloud";
|
|
29
|
+
projectUrl: string;
|
|
30
|
+
anonKey: string;
|
|
31
|
+
} | {
|
|
32
|
+
supabaseMode: "local";
|
|
33
|
+
projectUrl: string;
|
|
34
|
+
anonKey: string;
|
|
35
|
+
} | {
|
|
36
|
+
supabaseMode: "legacy";
|
|
37
|
+
projectUrl: string;
|
|
38
|
+
anonKey: string;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ClawHive cloud project URL baked into the plugin.
|
|
3
|
+
*
|
|
4
|
+
* Override at build time by setting `CLAWHIVE_DEFAULT_PROJECT_URL` before `tsc`.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_PROJECT_URL = process.env.CLAWHIVE_DEFAULT_PROJECT_URL ?? "https://idqnatwcrdxdzgcxtwdr.supabase.co";
|
|
7
|
+
/**
|
|
8
|
+
* Default ClawHive cloud anon key baked into the plugin.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlkcW5hdHdjcmR4ZHpnY3h0d2RyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY2NTMyOTIsImV4cCI6MjA5MjIyOTI5Mn0.nfU8hxlOaHlC2lRBwaO13FQ1mcIueqsWoyYbkLRo4-8";
|
|
11
|
+
/**
|
|
12
|
+
* Normalize the optional runtime mode switch for choosing the Supabase target.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeSupabaseMode(value) {
|
|
15
|
+
const normalized = value?.trim().toLowerCase();
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (normalized === "cloud" || normalized === "managed") {
|
|
20
|
+
return "cloud";
|
|
21
|
+
}
|
|
22
|
+
if (normalized === "local") {
|
|
23
|
+
return "local";
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Unsupported CLAWHIVE_SUPABASE_MODE: ${value}. Use "cloud" or "local".`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the plugin Supabase URL and anon key from legacy config sources or the
|
|
29
|
+
* explicit mode switch when present.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveSupabaseDefaults({ mode, sectionProjectUrl, sectionAnonKey, envProjectUrl, envAnonKey, }) {
|
|
32
|
+
const resolvedMode = normalizeSupabaseMode(mode);
|
|
33
|
+
if (resolvedMode === "cloud") {
|
|
34
|
+
return {
|
|
35
|
+
supabaseMode: resolvedMode,
|
|
36
|
+
projectUrl: DEFAULT_PROJECT_URL,
|
|
37
|
+
anonKey: DEFAULT_SUPABASE_ANON_KEY,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (resolvedMode === "local") {
|
|
41
|
+
return {
|
|
42
|
+
supabaseMode: resolvedMode,
|
|
43
|
+
projectUrl: envProjectUrl ?? sectionProjectUrl ?? "",
|
|
44
|
+
anonKey: envAnonKey ?? sectionAnonKey ?? "",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
supabaseMode: "legacy",
|
|
49
|
+
projectUrl: sectionProjectUrl ?? envProjectUrl ?? DEFAULT_PROJECT_URL,
|
|
50
|
+
anonKey: sectionAnonKey ?? envAnonKey ?? DEFAULT_SUPABASE_ANON_KEY,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ClawHiveCloudApi, type DeviceCodeStartResult } from "./client.js";
|
|
2
|
+
export type DeviceCodeLogger = {
|
|
3
|
+
info: (msg: string) => void;
|
|
4
|
+
warn: (msg: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export type DeviceCodeAwaitResult = {
|
|
7
|
+
user_id: string;
|
|
8
|
+
plugin_token: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function printDeviceCodePrompt(start: DeviceCodeStartResult, logger: DeviceCodeLogger): Promise<void>;
|
|
11
|
+
export declare function awaitDeviceCodeApproval(params: {
|
|
12
|
+
api: ClawHiveCloudApi;
|
|
13
|
+
start: DeviceCodeStartResult;
|
|
14
|
+
logger: DeviceCodeLogger;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}): Promise<DeviceCodeAwaitResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
2
|
+
import { DeviceCodeDeniedError, DeviceCodeExpiredError, } from "./client.js";
|
|
3
|
+
async function renderQr(text) {
|
|
4
|
+
return await new Promise((resolve) => {
|
|
5
|
+
qrcodeTerminal.generate(text, { small: true }, (qr) => resolve(qr));
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export async function printDeviceCodePrompt(start, logger) {
|
|
9
|
+
const qr = await renderQr(start.verification_uri_complete);
|
|
10
|
+
const expiresInMin = Math.floor(start.expires_in / 60);
|
|
11
|
+
const lines = [
|
|
12
|
+
"",
|
|
13
|
+
"════════════════════════════════════════════════════════════",
|
|
14
|
+
" ClawHive: authorize this OpenClaw install.",
|
|
15
|
+
"",
|
|
16
|
+
` Visit: ${start.verification_uri}`,
|
|
17
|
+
` Enter the code: ${start.user_code}`,
|
|
18
|
+
"",
|
|
19
|
+
" Or scan this QR to jump straight to the approval page:",
|
|
20
|
+
qr,
|
|
21
|
+
` (Expires in ${expiresInMin}m; polling for approval…)`,
|
|
22
|
+
"════════════════════════════════════════════════════════════",
|
|
23
|
+
"",
|
|
24
|
+
];
|
|
25
|
+
for (const line of lines)
|
|
26
|
+
logger.info(line);
|
|
27
|
+
}
|
|
28
|
+
export async function awaitDeviceCodeApproval(params) {
|
|
29
|
+
const { api, start, logger, signal } = params;
|
|
30
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
31
|
+
let interval = Math.max(start.interval, 1) * 1000;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
if (signal?.aborted) {
|
|
34
|
+
throw new Error("Device authorization aborted");
|
|
35
|
+
}
|
|
36
|
+
await sleep(interval);
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = await api.exchangeDeviceCode(start.device_code);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof DeviceCodeDeniedError) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
if (error instanceof DeviceCodeExpiredError) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
49
|
+
logger.warn(`ClawHive: device-code poll failed, will retry: ${reason}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (result.status === "approved") {
|
|
53
|
+
return {
|
|
54
|
+
user_id: result.user_id,
|
|
55
|
+
plugin_token: result.plugin_token,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// result.status === "pending"
|
|
59
|
+
interval = Math.max(result.interval, 1) * 1000;
|
|
60
|
+
}
|
|
61
|
+
throw new DeviceCodeExpiredError();
|
|
62
|
+
}
|
|
63
|
+
function sleep(ms) {
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ClawHiveCloudApi } from "./client.js";
|
|
2
|
+
export type HeartbeatStatus = "online" | "offline" | "degraded";
|
|
3
|
+
export type HeartbeatLoopLogger = {
|
|
4
|
+
info: (msg: string) => void;
|
|
5
|
+
warn: (msg: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export type HeartbeatLoopOptions = {
|
|
8
|
+
api: ClawHiveCloudApi;
|
|
9
|
+
pluginNodeId: string;
|
|
10
|
+
intervalSeconds: number;
|
|
11
|
+
logger?: HeartbeatLoopLogger;
|
|
12
|
+
};
|
|
13
|
+
export declare class HeartbeatLoop {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private timer;
|
|
16
|
+
private running;
|
|
17
|
+
private lastError;
|
|
18
|
+
constructor(options: HeartbeatLoopOptions);
|
|
19
|
+
start(): void;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
getLastError(): unknown;
|
|
22
|
+
private schedule;
|
|
23
|
+
private tick;
|
|
24
|
+
private jittered;
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class HeartbeatLoop {
|
|
2
|
+
options;
|
|
3
|
+
timer = null;
|
|
4
|
+
running = false;
|
|
5
|
+
lastError = null;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
start() {
|
|
10
|
+
if (this.running)
|
|
11
|
+
return;
|
|
12
|
+
this.running = true;
|
|
13
|
+
void this.tick(true);
|
|
14
|
+
}
|
|
15
|
+
async stop() {
|
|
16
|
+
this.running = false;
|
|
17
|
+
if (this.timer) {
|
|
18
|
+
clearTimeout(this.timer);
|
|
19
|
+
this.timer = null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await this.options.api.heartbeat({
|
|
23
|
+
pluginNodeId: this.options.pluginNodeId,
|
|
24
|
+
status: "offline",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
29
|
+
this.options.logger?.warn(`ClawHive heartbeat: failed to mark plugin offline on shutdown: ${reason}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
getLastError() {
|
|
33
|
+
return this.lastError;
|
|
34
|
+
}
|
|
35
|
+
schedule(delayMs) {
|
|
36
|
+
if (!this.running)
|
|
37
|
+
return;
|
|
38
|
+
this.timer = setTimeout(() => {
|
|
39
|
+
this.timer = null;
|
|
40
|
+
void this.tick(false);
|
|
41
|
+
}, delayMs);
|
|
42
|
+
}
|
|
43
|
+
async tick(initial) {
|
|
44
|
+
if (!this.running)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
await this.options.api.heartbeat({
|
|
48
|
+
pluginNodeId: this.options.pluginNodeId,
|
|
49
|
+
status: "online",
|
|
50
|
+
});
|
|
51
|
+
this.lastError = null;
|
|
52
|
+
this.schedule(this.jittered(this.options.intervalSeconds * 1000));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.lastError = error;
|
|
56
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
57
|
+
this.options.logger?.warn(`ClawHive heartbeat failed initial=${initial}: ${reason}`);
|
|
58
|
+
this.schedule(this.jittered(Math.min(this.options.intervalSeconds, 15) * 1000));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
jittered(baseMs) {
|
|
62
|
+
const spread = Math.floor(baseMs * 0.1);
|
|
63
|
+
return baseMs + Math.floor(Math.random() * spread);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RuntimeImageContent = {
|
|
2
|
+
type: "image";
|
|
3
|
+
data: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
};
|
|
6
|
+
export type ImageMessageAttachment = {
|
|
7
|
+
storagePath: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseImageMessageAttachment(metadata: unknown): ImageMessageAttachment | null;
|
|
11
|
+
export declare function downloadImageAsContent(input: {
|
|
12
|
+
signedUrl: string;
|
|
13
|
+
mimeType: string;
|
|
14
|
+
}): Promise<RuntimeImageContent>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: null;
|
|
5
|
+
}
|
|
6
|
+
export function parseImageMessageAttachment(metadata) {
|
|
7
|
+
const record = asRecord(metadata);
|
|
8
|
+
const attachments = record?.attachments;
|
|
9
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const attachment = asRecord(attachments[0]);
|
|
13
|
+
if (!attachment || attachment.kind !== "image") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (typeof attachment.storagePath !== "string" ||
|
|
17
|
+
typeof attachment.mimeType !== "string") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
storagePath: attachment.storagePath,
|
|
22
|
+
mimeType: attachment.mimeType,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function dataUrlToImageContent(dataUrl, fallbackMimeType) {
|
|
26
|
+
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error("Invalid image data URL returned for attachment");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
type: "image",
|
|
32
|
+
mimeType: match[1] || fallbackMimeType,
|
|
33
|
+
data: match[2],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function redactImageUrl(rawUrl) {
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(rawUrl);
|
|
39
|
+
if (url.searchParams.has("token")) {
|
|
40
|
+
url.searchParams.set("token", "[redacted]");
|
|
41
|
+
}
|
|
42
|
+
return url.toString();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "<invalid-url>";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function formatFetchFailure(error, signedUrl) {
|
|
49
|
+
const details = error instanceof Error ? [error.message] : [String(error)];
|
|
50
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
51
|
+
if (cause instanceof Error) {
|
|
52
|
+
details.push(`cause=${cause.name}: ${cause.message}`);
|
|
53
|
+
}
|
|
54
|
+
else if (cause !== undefined) {
|
|
55
|
+
details.push(`cause=${String(cause)}`);
|
|
56
|
+
}
|
|
57
|
+
details.push(`url=${redactImageUrl(signedUrl)}`);
|
|
58
|
+
return details.join("; ");
|
|
59
|
+
}
|
|
60
|
+
export async function downloadImageAsContent(input) {
|
|
61
|
+
if (input.signedUrl.startsWith("data:")) {
|
|
62
|
+
return dataUrlToImageContent(input.signedUrl, input.mimeType);
|
|
63
|
+
}
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(input.signedUrl);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new Error(`Failed to download image attachment: ${formatFetchFailure(error, input.signedUrl)}`);
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Failed to download image attachment: status=${response.status}; url=${redactImageUrl(input.signedUrl)}`);
|
|
73
|
+
}
|
|
74
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
75
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
76
|
+
return {
|
|
77
|
+
type: "image",
|
|
78
|
+
data: buffer.toString("base64"),
|
|
79
|
+
mimeType: response.headers.get("content-type") ?? input.mimeType,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InboundMessage } from "./transport.js";
|
|
2
|
+
import { type ClawHiveRuntime } from "./runtime.js";
|
|
3
|
+
type DispatchOptions = {
|
|
4
|
+
runtime?: ClawHiveRuntime;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches a ClawHive inbound user message into the mapped local OpenClaw agent.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleInboundMessage(message: InboundMessage, options?: DispatchOptions): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
|
2
|
+
import { CLAWHIVE_CHANNEL_ID } from "./config.js";
|
|
3
|
+
import { downloadImageAsContent, parseImageMessageAttachment, } from "./imageAttachments.js";
|
|
4
|
+
import { sendAgentText } from "./outbound.js";
|
|
5
|
+
import { buildClawHiveSessionKey, clawhiveRuntimeStore, indexSession, resolveLocalAgentId, } from "./runtime.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the effective provider/model pair for a local OpenClaw agent.
|
|
8
|
+
*/
|
|
9
|
+
function resolveAgentModelSelection(runtime, localAgentId) {
|
|
10
|
+
const cfg = runtime.pluginApi.runtime.config.loadConfig();
|
|
11
|
+
return resolveDefaultModelForAgent({
|
|
12
|
+
cfg,
|
|
13
|
+
agentId: localAgentId,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extracts the final user-visible reply text when OpenClaw did not send it itself.
|
|
18
|
+
*/
|
|
19
|
+
function extractVisibleReplyText(result) {
|
|
20
|
+
const finalText = result.meta?.finalAssistantVisibleText?.trim();
|
|
21
|
+
if (finalText)
|
|
22
|
+
return finalText;
|
|
23
|
+
const payloadText = (result.payloads ?? [])
|
|
24
|
+
.filter((payload) => !payload?.isReasoning && !payload?.isError)
|
|
25
|
+
.map((payload) => payload?.text?.trim() ?? "")
|
|
26
|
+
.filter((text) => text.length > 0)
|
|
27
|
+
.join("\n\n")
|
|
28
|
+
.trim();
|
|
29
|
+
return payloadText || null;
|
|
30
|
+
}
|
|
31
|
+
function resolveInboundPrompt(message) {
|
|
32
|
+
if (message.type !== "image") {
|
|
33
|
+
return message.content;
|
|
34
|
+
}
|
|
35
|
+
const content = message.content.trim();
|
|
36
|
+
if (!content || content === "Image") {
|
|
37
|
+
return "Please analyze this image.";
|
|
38
|
+
}
|
|
39
|
+
return message.content;
|
|
40
|
+
}
|
|
41
|
+
async function sendImageReadFailure(input) {
|
|
42
|
+
await input.runtime.api.writeAgentMessage({
|
|
43
|
+
pluginNodeId: input.runtime.pluginNodeId,
|
|
44
|
+
sessionId: input.message.sessionId,
|
|
45
|
+
agentId: input.message.agentId,
|
|
46
|
+
content: `I couldn't read the image attachment: ${input.reason}`,
|
|
47
|
+
}).catch(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Dispatches a ClawHive inbound user message into the mapped local OpenClaw agent.
|
|
51
|
+
*/
|
|
52
|
+
export async function handleInboundMessage(message, options = {}) {
|
|
53
|
+
const runtime = options.runtime ?? clawhiveRuntimeStore.tryGetRuntime();
|
|
54
|
+
if (!runtime)
|
|
55
|
+
return;
|
|
56
|
+
if (message.userId !== runtime.userId)
|
|
57
|
+
return;
|
|
58
|
+
if (message.role !== "user")
|
|
59
|
+
return;
|
|
60
|
+
const localAgentId = resolveLocalAgentId(runtime, message.agentId);
|
|
61
|
+
if (!localAgentId) {
|
|
62
|
+
runtime.pluginApi.logger.error(`ClawHive inbound dispatch failed session=${message.sessionId} agent=${message.agentId} msg=${message.id}: no local OpenClaw agent mapping for cloud agent id`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const sessionKey = buildClawHiveSessionKey(message.sessionId);
|
|
66
|
+
const binding = {
|
|
67
|
+
sessionId: message.sessionId,
|
|
68
|
+
cloudAgentId: message.agentId,
|
|
69
|
+
localAgentId,
|
|
70
|
+
userId: message.userId,
|
|
71
|
+
sessionKey,
|
|
72
|
+
lastSeenSequence: message.sequence,
|
|
73
|
+
};
|
|
74
|
+
indexSession(runtime, binding);
|
|
75
|
+
const logger = runtime.pluginApi.logger;
|
|
76
|
+
try {
|
|
77
|
+
const cfg = runtime.pluginApi.runtime.config.loadConfig();
|
|
78
|
+
const modelSelection = resolveAgentModelSelection(runtime, localAgentId);
|
|
79
|
+
const workspaceDir = runtime.pluginApi.runtime.agent.resolveAgentWorkspaceDir(cfg, localAgentId);
|
|
80
|
+
const sessionFile = runtime.pluginApi.runtime.agent.session.resolveSessionFilePath(message.sessionId, undefined, { agentId: localAgentId });
|
|
81
|
+
const timeoutMs = runtime.pluginApi.runtime.agent.resolveAgentTimeoutMs({
|
|
82
|
+
cfg,
|
|
83
|
+
});
|
|
84
|
+
const imageAttachment = message.type === "image"
|
|
85
|
+
? parseImageMessageAttachment(message.metadata)
|
|
86
|
+
: null;
|
|
87
|
+
const images = imageAttachment
|
|
88
|
+
? [
|
|
89
|
+
await downloadImageAsContent(await runtime.api.getMessageAttachmentDownloadUrl({
|
|
90
|
+
messageId: message.id,
|
|
91
|
+
storagePath: imageAttachment.storagePath,
|
|
92
|
+
})),
|
|
93
|
+
]
|
|
94
|
+
: undefined;
|
|
95
|
+
const prompt = resolveInboundPrompt(message);
|
|
96
|
+
const result = await runtime.pluginApi.runtime.agent.runEmbeddedPiAgent({
|
|
97
|
+
runId: `clawhive-inbound-${message.id}`,
|
|
98
|
+
trigger: "user",
|
|
99
|
+
sessionId: message.sessionId,
|
|
100
|
+
sessionKey,
|
|
101
|
+
agentId: localAgentId,
|
|
102
|
+
messageChannel: CLAWHIVE_CHANNEL_ID,
|
|
103
|
+
messageTo: sessionKey,
|
|
104
|
+
senderId: message.userId,
|
|
105
|
+
currentMessageId: message.id,
|
|
106
|
+
sessionFile,
|
|
107
|
+
workspaceDir,
|
|
108
|
+
config: cfg,
|
|
109
|
+
provider: modelSelection.provider,
|
|
110
|
+
model: modelSelection.model,
|
|
111
|
+
prompt,
|
|
112
|
+
images,
|
|
113
|
+
timeoutMs,
|
|
114
|
+
});
|
|
115
|
+
if (!result.didSendViaMessagingTool) {
|
|
116
|
+
const replyText = extractVisibleReplyText(result);
|
|
117
|
+
if (replyText) {
|
|
118
|
+
await sendAgentText({
|
|
119
|
+
cfg,
|
|
120
|
+
to: sessionKey,
|
|
121
|
+
text: replyText,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
logger.warn(`ClawHive inbound dispatch completed without a visible reply session=${message.sessionId} agent=${localAgentId} cloudAgent=${message.agentId} msg=${message.id}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
131
|
+
logger.error(`ClawHive inbound dispatch failed session=${message.sessionId} agent=${localAgentId} cloudAgent=${message.agentId} msg=${message.id}: ${reason}`);
|
|
132
|
+
if (message.type === "image") {
|
|
133
|
+
await sendImageReadFailure({
|
|
134
|
+
runtime,
|
|
135
|
+
message,
|
|
136
|
+
reason,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let tail = Promise.resolve();
|
|
2
|
+
/**
|
|
3
|
+
* Serialize local install application because each install rewrites the shared OpenClaw config.
|
|
4
|
+
*/
|
|
5
|
+
export async function runExclusiveInstall(operation) {
|
|
6
|
+
const previous = tail;
|
|
7
|
+
let release = () => { };
|
|
8
|
+
tail = new Promise((resolve) => {
|
|
9
|
+
release = resolve;
|
|
10
|
+
});
|
|
11
|
+
await previous.catch(() => { });
|
|
12
|
+
try {
|
|
13
|
+
return await operation();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
release();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { ClawHiveCloudApi, SyncedAgent } from "../client.js";
|
|
3
|
+
import type { ActiveInstallJobState } from "./types.js";
|
|
4
|
+
export type InstallClaimLoopLogger = {
|
|
5
|
+
info: (message: string) => void;
|
|
6
|
+
warn: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export type InstallClaimLoopOptions = {
|
|
9
|
+
api: ClawHiveCloudApi;
|
|
10
|
+
pluginApi: OpenClawPluginApi;
|
|
11
|
+
pluginNodeId: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
pluginVersion: string;
|
|
14
|
+
openClawVersion: string | null;
|
|
15
|
+
logger?: InstallClaimLoopLogger;
|
|
16
|
+
onActiveInstallChange?: (state: ActiveInstallJobState | null) => void;
|
|
17
|
+
onAgentsSynced?: (items: SyncedAgent[]) => void;
|
|
18
|
+
idlePollMs?: number;
|
|
19
|
+
retryPollMs?: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Poll the durable install queue without blocking the existing heartbeat or realtime loops.
|
|
23
|
+
*/
|
|
24
|
+
export declare class InstallClaimLoop {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private timer;
|
|
27
|
+
private wakeTimer;
|
|
28
|
+
private wakePending;
|
|
29
|
+
private claiming;
|
|
30
|
+
private running;
|
|
31
|
+
private activeRun;
|
|
32
|
+
private lastError;
|
|
33
|
+
constructor(options: InstallClaimLoopOptions);
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
getLastError(): unknown;
|
|
37
|
+
wake(): void;
|
|
38
|
+
private schedule;
|
|
39
|
+
private tick;
|
|
40
|
+
private processJob;
|
|
41
|
+
}
|