openclaw-remote 0.4.5 → 0.5.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/dist/index.js +137 -420
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var __export = (target, all) => {
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_ACCOUNT_ID,
|
|
10
10
|
setAccountEnabledInConfigSection,
|
|
11
|
-
|
|
11
|
+
registerPluginHttpRoute,
|
|
12
12
|
optionalStringEnum
|
|
13
13
|
} from "openclaw/plugin-sdk/compat";
|
|
14
14
|
|
|
@@ -2655,13 +2655,11 @@ function resolveAccount(cfg, accountId) {
|
|
|
2655
2655
|
baseUrl: (accountOverride.baseUrl ?? channelCfg.baseUrl ?? envBaseUrl).replace(/\/+$/, ""),
|
|
2656
2656
|
apiKey: accountOverride.apiKey ?? channelCfg.apiKey ?? envApiKey,
|
|
2657
2657
|
projectId: accountOverride.projectId ?? channelCfg.projectId,
|
|
2658
|
-
supabaseUrl: accountOverride.supabaseUrl ?? channelCfg.supabaseUrl ?? process.env.REMOTE_SUPABASE_URL,
|
|
2659
|
-
supabaseKey: accountOverride.supabaseKey ?? channelCfg.supabaseKey ?? process.env.REMOTE_SUPABASE_KEY,
|
|
2660
2658
|
dmPolicy: accountOverride.dmPolicy ?? channelCfg.dmPolicy ?? "open",
|
|
2661
2659
|
allowedUserIds: parseAllowedUserIds(
|
|
2662
2660
|
accountOverride.allowedUserIds ?? channelCfg.allowedUserIds ?? envAllowedUserIds
|
|
2663
2661
|
),
|
|
2664
|
-
webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath
|
|
2662
|
+
webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath ?? `/webhooks/remote/${id}`
|
|
2665
2663
|
};
|
|
2666
2664
|
}
|
|
2667
2665
|
|
|
@@ -2793,353 +2791,73 @@ async function getTeam(account) {
|
|
|
2793
2791
|
);
|
|
2794
2792
|
}
|
|
2795
2793
|
|
|
2796
|
-
// src/
|
|
2797
|
-
import {
|
|
2798
|
-
var
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
const data = await res.json();
|
|
2814
|
-
const roles = data.roles || [];
|
|
2815
|
-
const myRoles = roles.filter((r) => r.is_mine);
|
|
2816
|
-
const myRoleIds = myRoles.map((r) => r.id);
|
|
2817
|
-
const isLeader = myRoles.some((r) => r.is_leader);
|
|
2818
|
-
let agentName = "";
|
|
2819
|
-
for (const r of myRoles) {
|
|
2820
|
-
if (r.assigned_to?.type === "agent" && r.assigned_to?.name) {
|
|
2821
|
-
agentName = r.assigned_to.name;
|
|
2822
|
-
break;
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
let agentId = "";
|
|
2826
|
-
try {
|
|
2827
|
-
const hbRes = await fetch(`${account.baseUrl}/api/v1/heartbeat`, {
|
|
2828
|
-
headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
|
|
2829
|
-
signal: AbortSignal.timeout(1e4)
|
|
2830
|
-
});
|
|
2831
|
-
if (hbRes.ok) {
|
|
2832
|
-
const hb = await hbRes.json();
|
|
2833
|
-
agentId = hb.agent_id || "";
|
|
2834
|
-
if (!agentName) agentName = hb.agent_name || "";
|
|
2835
|
-
}
|
|
2836
|
-
} catch {
|
|
2837
|
-
}
|
|
2838
|
-
log?.info?.(`Agent identity resolved: "${agentName}" (${agentId}), roles: [${myRoleIds.join(", ")}], leader: ${isLeader}`);
|
|
2839
|
-
return { agentId, agentName, myRoleIds, isLeader };
|
|
2840
|
-
} catch (err) {
|
|
2841
|
-
log?.warn?.(`Failed to resolve agent identity: ${err}`);
|
|
2842
|
-
return null;
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
async function fetchLeaderRoles(supabase, log) {
|
|
2846
|
-
const now = Date.now();
|
|
2847
|
-
if (now - leaderRolesCacheTime < LEADER_CACHE_TTL && leaderRolesCache.length > 0) {
|
|
2848
|
-
return leaderRolesCache;
|
|
2849
|
-
}
|
|
2850
|
-
const { data: roles } = await supabase.from("project_roles").select("id").eq("is_leader", true);
|
|
2851
|
-
if (!roles || roles.length === 0) {
|
|
2852
|
-
leaderRolesCache = [];
|
|
2853
|
-
leaderRolesCacheTime = now;
|
|
2854
|
-
return [];
|
|
2855
|
-
}
|
|
2856
|
-
const roleIds = roles.map((r) => r.id);
|
|
2857
|
-
const { data: assignments } = await supabase.from("agent_role_assignments").select("project_role_id, agent_id").in("project_role_id", roleIds);
|
|
2858
|
-
const map = /* @__PURE__ */ new Map();
|
|
2859
|
-
for (const a of assignments ?? []) {
|
|
2860
|
-
const list = map.get(a.project_role_id) || [];
|
|
2861
|
-
list.push(a.agent_id);
|
|
2862
|
-
map.set(a.project_role_id, list);
|
|
2863
|
-
}
|
|
2864
|
-
leaderRolesCache = roleIds.map((id) => ({ roleId: id, agentIds: map.get(id) || [] }));
|
|
2865
|
-
leaderRolesCacheTime = now;
|
|
2866
|
-
log?.info?.(`Leader roles cache refreshed: ${leaderRolesCache.length} leader role(s)`);
|
|
2867
|
-
return leaderRolesCache;
|
|
2868
|
-
}
|
|
2869
|
-
function connectRealtime(account, identity, onEvent, onError, log) {
|
|
2870
|
-
const { supabaseUrl, supabaseKey } = account;
|
|
2871
|
-
if (!supabaseUrl || !supabaseKey) {
|
|
2872
|
-
throw new Error("Supabase URL and key are required for Realtime listener. Set supabaseUrl and supabaseKey in channels.remote config.");
|
|
2873
|
-
}
|
|
2874
|
-
log?.info?.(`Connecting to Supabase Realtime: ${supabaseUrl}`);
|
|
2875
|
-
const supabase = createClient(supabaseUrl, supabaseKey, {
|
|
2876
|
-
realtime: {
|
|
2877
|
-
params: { eventsPerSecond: 10 }
|
|
2878
|
-
}
|
|
2879
|
-
});
|
|
2880
|
-
const channelName = `remote-${account.accountId}-${Date.now()}`;
|
|
2881
|
-
const channel = supabase.channel(channelName).on(
|
|
2882
|
-
"postgres_changes",
|
|
2883
|
-
{
|
|
2884
|
-
event: "INSERT",
|
|
2885
|
-
schema: "public",
|
|
2886
|
-
table: "task_comments"
|
|
2887
|
-
},
|
|
2888
|
-
(payload) => {
|
|
2889
|
-
try {
|
|
2890
|
-
const row = payload.new;
|
|
2891
|
-
if (row.agent_author_id && !row.author_id) {
|
|
2892
|
-
scheduleLeaderNotification(row.task_id, row.agent_author_id, supabase, log);
|
|
2893
|
-
if (identity && row.agent_author_id === identity.agentId) {
|
|
2894
|
-
log?.info?.(`Skipping own comment on task ${row.task_id} (echo suppression)`);
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
log?.info?.(`Delivering agent comment from ${row.agent_author_id} on task ${row.task_id}`);
|
|
2898
|
-
}
|
|
2899
|
-
const body = row.body || row.content || "";
|
|
2900
|
-
if (!row.author_id && !row.agent_author_id && identity) {
|
|
2901
|
-
if (body.startsWith("\u{1F4CC} Assigned to ")) {
|
|
2902
|
-
const targetName = body.replace("\u{1F4CC} Assigned to ", "").trim();
|
|
2903
|
-
if (targetName.toLowerCase() !== identity.agentName.toLowerCase()) {
|
|
2904
|
-
log?.info?.(`Skipping assignment notification for ${targetName} (I am ${identity.agentName})`);
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
if (body.startsWith("\u{1F440} ")) {
|
|
2909
|
-
if (!identity.isLeader) {
|
|
2910
|
-
log?.info?.(`Skipping leader notification (I am not a leader)`);
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
const event = {
|
|
2916
|
-
type: "task.commented",
|
|
2917
|
-
task_id: row.task_id,
|
|
2918
|
-
comment: {
|
|
2919
|
-
id: row.id,
|
|
2920
|
-
content: body,
|
|
2921
|
-
author_name: row.author_name || "System",
|
|
2922
|
-
author_id: row.author_id || "",
|
|
2923
|
-
created_at: row.created_at
|
|
2924
|
-
}
|
|
2925
|
-
};
|
|
2926
|
-
log?.info?.(`Realtime: new comment on task ${row.task_id}`);
|
|
2927
|
-
onEvent(event);
|
|
2928
|
-
} catch (err) {
|
|
2929
|
-
log?.warn?.(`Failed to process comment event: ${err}`);
|
|
2930
|
-
}
|
|
2794
|
+
// src/runtime.ts
|
|
2795
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
2796
|
+
var { setRuntime: setRemoteRuntime, getRuntime: getRemoteRuntime } = createPluginRuntimeStore(
|
|
2797
|
+
"Remote runtime not initialized - plugin not registered"
|
|
2798
|
+
);
|
|
2799
|
+
|
|
2800
|
+
// src/channel.ts
|
|
2801
|
+
var CHANNEL_ID = "remote";
|
|
2802
|
+
var activeRouteUnregisters = /* @__PURE__ */ new Map();
|
|
2803
|
+
function waitUntilAbort(signal, onAbort) {
|
|
2804
|
+
return new Promise((resolve) => {
|
|
2805
|
+
const complete = () => {
|
|
2806
|
+
onAbort?.();
|
|
2807
|
+
resolve();
|
|
2808
|
+
};
|
|
2809
|
+
if (!signal) {
|
|
2810
|
+
return;
|
|
2931
2811
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
log?.info?.("Supabase Realtime connected and subscribed.");
|
|
2936
|
-
} else if (status === "CHANNEL_ERROR") {
|
|
2937
|
-
log?.error?.(`Realtime channel error: ${err?.message || "unknown"}`);
|
|
2938
|
-
onError(new Error(`Realtime channel error: ${err?.message || "unknown"}`));
|
|
2939
|
-
} else if (status === "TIMED_OUT") {
|
|
2940
|
-
log?.warn?.("Realtime subscription timed out, will auto-retry...");
|
|
2941
|
-
} else if (status === "CLOSED") {
|
|
2942
|
-
log?.info?.("Realtime channel closed.");
|
|
2812
|
+
if (signal.aborted) {
|
|
2813
|
+
complete();
|
|
2814
|
+
return;
|
|
2943
2815
|
}
|
|
2816
|
+
signal.addEventListener("abort", complete, { once: true });
|
|
2944
2817
|
});
|
|
2945
|
-
return {
|
|
2946
|
-
unsubscribe: async () => {
|
|
2947
|
-
log?.info?.("Unsubscribing from Supabase Realtime...");
|
|
2948
|
-
for (const timer of leaderTimers.values()) clearTimeout(timer);
|
|
2949
|
-
leaderTimers.clear();
|
|
2950
|
-
await supabase.removeChannel(channel);
|
|
2951
|
-
log?.info?.("Realtime listener shut down cleanly.");
|
|
2952
|
-
}
|
|
2953
|
-
};
|
|
2954
2818
|
}
|
|
2955
|
-
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
leaderTimers.delete(taskId);
|
|
2960
|
-
try {
|
|
2961
|
-
const leaders = await fetchLeaderRoles(supabase, log);
|
|
2962
|
-
if (leaders.length === 0) return;
|
|
2963
|
-
const { data: task } = await supabase.from("tasks").select("id, title, assigned_role_id").eq("id", taskId).single();
|
|
2964
|
-
if (!task) return;
|
|
2965
|
-
for (const leader of leaders) {
|
|
2966
|
-
if (task.assigned_role_id === leader.roleId) {
|
|
2967
|
-
log?.info?.(`Skipping leader notification for task ${taskId} \u2014 assigned to leader role ${leader.roleId}`);
|
|
2968
|
-
continue;
|
|
2969
|
-
}
|
|
2970
|
-
if (leader.agentIds.includes(commentingAgentId)) continue;
|
|
2971
|
-
if (leader.agentIds.length === 0) continue;
|
|
2972
|
-
const { error } = await supabase.from("task_comments").insert({
|
|
2973
|
-
task_id: taskId,
|
|
2974
|
-
author_id: null,
|
|
2975
|
-
agent_author_id: null,
|
|
2976
|
-
body: `\u{1F440} Activity on this task \u2014 leader review needed`
|
|
2977
|
-
});
|
|
2978
|
-
if (error) {
|
|
2979
|
-
log?.warn?.(`Failed to insert leader notification for task ${taskId}: ${error.message}`);
|
|
2980
|
-
} else {
|
|
2981
|
-
log?.info?.(`Leader notification sent for task ${taskId}`);
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
} catch (err) {
|
|
2985
|
-
log?.warn?.(`Leader notification error for task ${taskId}: ${err}`);
|
|
2986
|
-
}
|
|
2987
|
-
}, LEADER_DEBOUNCE_MS);
|
|
2988
|
-
leaderTimers.set(taskId, timer);
|
|
2989
|
-
log?.info?.(`Leader debounce timer set for task ${taskId} (60s)`);
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
|
-
// src/event-formatter.ts
|
|
2993
|
-
function formatEvent(event) {
|
|
2994
|
-
switch (event.type) {
|
|
2819
|
+
function formatWebhookEvent(payload) {
|
|
2820
|
+
const { event, task_id, task_title, comment_body, author_name, wake_reason } = payload;
|
|
2821
|
+
switch (event) {
|
|
2822
|
+
case "task.assigned":
|
|
2995
2823
|
case "task.created":
|
|
2996
|
-
return
|
|
2997
|
-
|
|
2998
|
-
|
|
2824
|
+
return [
|
|
2825
|
+
`You've been assigned a new task:`,
|
|
2826
|
+
``,
|
|
2827
|
+
`**${task_title}** (ID: ${task_id})`,
|
|
2828
|
+
``,
|
|
2829
|
+
wake_reason ? `Reason: ${wake_reason}` : "",
|
|
2830
|
+
``,
|
|
2831
|
+
`Use this task ID for API calls (e.g. to move it to in_progress).`,
|
|
2832
|
+
`Review the task and share your initial thoughts or plan.`
|
|
2833
|
+
].filter(Boolean).join("\n");
|
|
2999
2834
|
case "task.commented":
|
|
3000
|
-
return
|
|
3001
|
-
|
|
3002
|
-
|
|
2835
|
+
return [
|
|
2836
|
+
`${author_name || "Someone"} commented on "${task_title}" (ID: ${task_id}):`,
|
|
2837
|
+
``,
|
|
2838
|
+
`> ${comment_body || ""}`,
|
|
2839
|
+
``,
|
|
2840
|
+
`Respond to their comment.`
|
|
2841
|
+
].join("\n");
|
|
2842
|
+
case "task.mentioned":
|
|
2843
|
+
return [
|
|
2844
|
+
`You were mentioned in a comment on "${task_title}" (ID: ${task_id}):`,
|
|
2845
|
+
``,
|
|
2846
|
+
`> ${comment_body || ""}`,
|
|
2847
|
+
``,
|
|
2848
|
+
`Respond to the mention.`
|
|
2849
|
+
].join("\n");
|
|
2850
|
+
case "task.status_changed":
|
|
2851
|
+
return [
|
|
2852
|
+
`Task "${task_title}" (ID: ${task_id}) status changed.`,
|
|
2853
|
+
wake_reason ? `Details: ${wake_reason}` : "",
|
|
2854
|
+
``,
|
|
2855
|
+
`Acknowledge the status change if relevant to your work.`
|
|
2856
|
+
].filter(Boolean).join("\n");
|
|
3003
2857
|
default:
|
|
3004
|
-
return
|
|
2858
|
+
return `Task update on "${task_title}" (ID: ${task_id}): ${event}${wake_reason ? ` \u2014 ${wake_reason}` : ""}`;
|
|
3005
2859
|
}
|
|
3006
2860
|
}
|
|
3007
|
-
function formatTaskCreated(event) {
|
|
3008
|
-
const task = event.task;
|
|
3009
|
-
if (!task) return null;
|
|
3010
|
-
const meta = [task.type, task.priority].filter(Boolean).join(", ");
|
|
3011
|
-
let body = `\u{1F4CB} New task created: **${task.title}**`;
|
|
3012
|
-
if (meta) body += ` [${meta}]`;
|
|
3013
|
-
if (task.description) body += `
|
|
3014
|
-
${task.description}`;
|
|
3015
|
-
body += `
|
|
3016
|
-
|
|
3017
|
-
[task:${task.id}]`;
|
|
3018
|
-
return {
|
|
3019
|
-
body,
|
|
3020
|
-
taskId: task.id,
|
|
3021
|
-
taskTitle: task.title,
|
|
3022
|
-
isDirect: false
|
|
3023
|
-
};
|
|
3024
|
-
}
|
|
3025
|
-
function formatTaskUpdated(event) {
|
|
3026
|
-
const task = event.task;
|
|
3027
|
-
if (!task) return null;
|
|
3028
|
-
const changes = event.changes;
|
|
3029
|
-
if (changes?.field === "assigned_role_id" || changes?.field === "assigned_to_agent" || changes?.field === "assigned_profile_id") {
|
|
3030
|
-
if (changes.to && changes.to !== "null" && changes.to !== "") {
|
|
3031
|
-
let body2 = `\u{1F464} You've been assigned: **${task.title}**`;
|
|
3032
|
-
if (task.description) body2 += `
|
|
3033
|
-
|
|
3034
|
-
${task.description}`;
|
|
3035
|
-
body2 += `
|
|
3036
|
-
|
|
3037
|
-
Priority: ${task.priority || "medium"} | Type: ${task.type || "task"}`;
|
|
3038
|
-
body2 += `
|
|
3039
|
-
|
|
3040
|
-
Acknowledge this task, move it to in_progress using remote_update_task, and share your plan.`;
|
|
3041
|
-
body2 += `
|
|
3042
|
-
|
|
3043
|
-
[task:${task.id}]`;
|
|
3044
|
-
return {
|
|
3045
|
-
body: body2,
|
|
3046
|
-
taskId: task.id,
|
|
3047
|
-
taskTitle: task.title,
|
|
3048
|
-
isDirect: true
|
|
3049
|
-
};
|
|
3050
|
-
}
|
|
3051
|
-
return null;
|
|
3052
|
-
}
|
|
3053
|
-
if (changes?.field === "status") {
|
|
3054
|
-
let body2 = `\u{1F504} Task **${task.title}** moved to **${changes.to}**`;
|
|
3055
|
-
body2 += `
|
|
3056
|
-
|
|
3057
|
-
[task:${task.id}]`;
|
|
3058
|
-
return {
|
|
3059
|
-
body: body2,
|
|
3060
|
-
taskId: task.id,
|
|
3061
|
-
taskTitle: task.title,
|
|
3062
|
-
isDirect: false
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3065
|
-
let body = `\u{1F504} Task updated: **${task.title}**`;
|
|
3066
|
-
if (changes) {
|
|
3067
|
-
body += ` \u2014 ${changes.field} changed from "${changes.from}" to "${changes.to}"`;
|
|
3068
|
-
}
|
|
3069
|
-
body += `
|
|
3070
|
-
|
|
3071
|
-
[task:${task.id}]`;
|
|
3072
|
-
return {
|
|
3073
|
-
body,
|
|
3074
|
-
taskId: task.id,
|
|
3075
|
-
taskTitle: task.title,
|
|
3076
|
-
isDirect: false
|
|
3077
|
-
};
|
|
3078
|
-
}
|
|
3079
|
-
function formatTaskCommented(event) {
|
|
3080
|
-
const comment = event.comment;
|
|
3081
|
-
const taskId = event.task_id ?? event.task?.id;
|
|
3082
|
-
if (!comment || !taskId) return null;
|
|
3083
|
-
const taskTitle = event.task?.title ?? `Task ${taskId}`;
|
|
3084
|
-
let body = `\u{1F4AC} ${comment.author_name} commented on **${taskTitle}**:
|
|
3085
|
-
${comment.content}`;
|
|
3086
|
-
body += `
|
|
3087
|
-
|
|
3088
|
-
[task:${taskId}]`;
|
|
3089
|
-
return {
|
|
3090
|
-
body,
|
|
3091
|
-
taskId,
|
|
3092
|
-
senderName: comment.author_name,
|
|
3093
|
-
senderId: comment.author_id,
|
|
3094
|
-
taskTitle,
|
|
3095
|
-
isDirect: false
|
|
3096
|
-
};
|
|
3097
|
-
}
|
|
3098
|
-
function formatTaskAssigned(event) {
|
|
3099
|
-
const task = event.task;
|
|
3100
|
-
const assignee = event.assigned_to;
|
|
3101
|
-
if (!task) return null;
|
|
3102
|
-
let body = `\u{1F464} Task **${task.title}** assigned`;
|
|
3103
|
-
if (assignee) {
|
|
3104
|
-
body += ` to ${assignee.name} (${assignee.role})`;
|
|
3105
|
-
}
|
|
3106
|
-
body += `
|
|
3107
|
-
|
|
3108
|
-
[task:${task.id}]`;
|
|
3109
|
-
return {
|
|
3110
|
-
body,
|
|
3111
|
-
taskId: task.id,
|
|
3112
|
-
senderName: assignee?.name,
|
|
3113
|
-
taskTitle: task.title,
|
|
3114
|
-
// Assigned events are direct — the agent is likely the assignee
|
|
3115
|
-
isDirect: true
|
|
3116
|
-
};
|
|
3117
|
-
}
|
|
3118
|
-
function formatGenericEvent(event) {
|
|
3119
|
-
const task = event.task;
|
|
3120
|
-
const taskId = event.task_id ?? task?.id;
|
|
3121
|
-
if (!taskId) return null;
|
|
3122
|
-
const taskTitle = task?.title ?? `Task ${taskId}`;
|
|
3123
|
-
let body = `\u{1F514} Remote event (${event.type}): **${taskTitle}**`;
|
|
3124
|
-
body += `
|
|
3125
|
-
|
|
3126
|
-
[task:${taskId}]`;
|
|
3127
|
-
return {
|
|
3128
|
-
body,
|
|
3129
|
-
taskId,
|
|
3130
|
-
taskTitle,
|
|
3131
|
-
isDirect: false
|
|
3132
|
-
};
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
// src/runtime.ts
|
|
3136
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
3137
|
-
var { setRuntime: setRemoteRuntime, getRuntime: getRemoteRuntime } = createPluginRuntimeStore(
|
|
3138
|
-
"Remote runtime not initialized - plugin not registered"
|
|
3139
|
-
);
|
|
3140
|
-
|
|
3141
|
-
// src/channel.ts
|
|
3142
|
-
var CHANNEL_ID = "remote";
|
|
3143
2861
|
function createRemotePlugin() {
|
|
3144
2862
|
return {
|
|
3145
2863
|
id: CHANNEL_ID,
|
|
@@ -3218,11 +2936,6 @@ function createRemotePlugin() {
|
|
|
3218
2936
|
"- Remote: apiKey is not configured. The plugin cannot authenticate with the Remote API."
|
|
3219
2937
|
);
|
|
3220
2938
|
}
|
|
3221
|
-
if (account.dmPolicy === "open") {
|
|
3222
|
-
warnings.push(
|
|
3223
|
-
'- Remote: dmPolicy="open" allows any board event to trigger agent actions. Consider "allowlist" for production use.'
|
|
3224
|
-
);
|
|
3225
|
-
}
|
|
3226
2939
|
return warnings;
|
|
3227
2940
|
}
|
|
3228
2941
|
},
|
|
@@ -3268,96 +2981,100 @@ function createRemotePlugin() {
|
|
|
3268
2981
|
},
|
|
3269
2982
|
gateway: {
|
|
3270
2983
|
startAccount: async (ctx) => {
|
|
3271
|
-
const { cfg, accountId, log
|
|
2984
|
+
const { cfg, accountId, log } = ctx;
|
|
3272
2985
|
const account = resolveAccount(cfg, accountId);
|
|
3273
2986
|
if (!account.enabled) {
|
|
3274
2987
|
log?.info?.(`Remote account ${accountId} is disabled, skipping`);
|
|
3275
|
-
return waitUntilAbort(abortSignal);
|
|
2988
|
+
return waitUntilAbort(ctx.abortSignal);
|
|
3276
2989
|
}
|
|
3277
2990
|
if (!account.baseUrl || !account.apiKey) {
|
|
3278
2991
|
log?.warn?.(
|
|
3279
2992
|
`Remote account ${accountId} not fully configured (missing baseUrl or apiKey)`
|
|
3280
2993
|
);
|
|
3281
|
-
return waitUntilAbort(abortSignal);
|
|
3282
|
-
}
|
|
3283
|
-
if (!account.supabaseUrl || !account.supabaseKey) {
|
|
3284
|
-
log?.warn?.(
|
|
3285
|
-
`Remote account ${accountId} missing supabaseUrl or supabaseKey \u2014 Realtime events disabled. Agent tools still work.`
|
|
3286
|
-
);
|
|
2994
|
+
return waitUntilAbort(ctx.abortSignal);
|
|
3287
2995
|
}
|
|
3288
2996
|
log?.info?.(
|
|
3289
|
-
`Starting Remote channel (account: ${accountId},
|
|
2997
|
+
`Starting Remote channel (account: ${accountId}, webhookPath: ${account.webhookPath})`
|
|
3290
2998
|
);
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
|
|
2999
|
+
const handler = async (req) => {
|
|
3000
|
+
if (req.method !== "POST") {
|
|
3001
|
+
return { status: 405, body: { error: "Method not allowed" } };
|
|
3002
|
+
}
|
|
3003
|
+
const payload = req.body;
|
|
3004
|
+
if (!payload || !payload.event || !payload.task_id) {
|
|
3005
|
+
return { status: 400, body: { error: "Invalid payload: missing event or task_id" } };
|
|
3006
|
+
}
|
|
3007
|
+
log?.info?.(`Webhook received: ${payload.event} for task ${payload.task_id} (agent: ${payload.agent_name})`);
|
|
3008
|
+
const body = formatWebhookEvent(payload);
|
|
3009
|
+
const sessionKey = `remote:${account.accountId}:${payload.task_id}`;
|
|
3294
3010
|
try {
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
deliver: async (payload, _info) => {
|
|
3329
|
-
if (payload.isReasoning) return;
|
|
3330
|
-
const text = payload?.text;
|
|
3331
|
-
if (text) {
|
|
3332
|
-
await postComment(account, formatted.taskId, text);
|
|
3333
|
-
}
|
|
3334
|
-
},
|
|
3335
|
-
onReplyStart: () => {
|
|
3336
|
-
log?.info?.(`Agent reply started for task ${formatted.taskId}`);
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
});
|
|
3340
|
-
} catch (err) {
|
|
3341
|
-
log?.error?.(
|
|
3342
|
-
`Error dispatching Remote event: ${err instanceof Error ? err.message : String(err)}`
|
|
3343
|
-
);
|
|
3011
|
+
const rt = getRemoteRuntime();
|
|
3012
|
+
const currentCfg = await rt.config.loadConfig();
|
|
3013
|
+
const msgCtx = rt.channel.reply.finalizeInboundContext({
|
|
3014
|
+
Body: body,
|
|
3015
|
+
RawBody: body,
|
|
3016
|
+
CommandBody: body,
|
|
3017
|
+
From: `remote:${payload.task_id}`,
|
|
3018
|
+
To: `remote:${account.accountId}`,
|
|
3019
|
+
SessionKey: sessionKey,
|
|
3020
|
+
AccountId: account.accountId,
|
|
3021
|
+
OriginatingChannel: CHANNEL_ID,
|
|
3022
|
+
OriginatingTo: `remote:${payload.task_id}`,
|
|
3023
|
+
ChatType: "direct",
|
|
3024
|
+
SenderName: payload.author_name || "Remote Board",
|
|
3025
|
+
SenderId: payload.task_id,
|
|
3026
|
+
Provider: CHANNEL_ID,
|
|
3027
|
+
Surface: CHANNEL_ID,
|
|
3028
|
+
ConversationLabel: payload.task_title || `Task ${payload.task_id}`,
|
|
3029
|
+
Timestamp: Date.now()
|
|
3030
|
+
});
|
|
3031
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
3032
|
+
ctx: msgCtx,
|
|
3033
|
+
cfg: currentCfg,
|
|
3034
|
+
dispatcherOptions: {
|
|
3035
|
+
deliver: async (deliverPayload) => {
|
|
3036
|
+
if (deliverPayload.isReasoning) return;
|
|
3037
|
+
const text = deliverPayload?.text ?? deliverPayload?.body;
|
|
3038
|
+
if (text) {
|
|
3039
|
+
await postComment(account, payload.task_id, text);
|
|
3040
|
+
}
|
|
3041
|
+
},
|
|
3042
|
+
onReplyStart: () => {
|
|
3043
|
+
log?.info?.(`Agent reply started for task ${payload.task_id}`);
|
|
3344
3044
|
}
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
log?.warn?.(`Realtime error: ${err.message}`);
|
|
3348
|
-
},
|
|
3349
|
-
log
|
|
3350
|
-
);
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3351
3047
|
} catch (err) {
|
|
3352
|
-
log?.error?.(
|
|
3048
|
+
log?.error?.(
|
|
3049
|
+
`Error dispatching webhook event: ${err instanceof Error ? err.message : String(err)}`
|
|
3050
|
+
);
|
|
3051
|
+
return { status: 500, body: { error: "Internal error" } };
|
|
3353
3052
|
}
|
|
3053
|
+
return { status: 200, body: { ok: true } };
|
|
3054
|
+
};
|
|
3055
|
+
const routeKey = `${accountId}:${account.webhookPath}`;
|
|
3056
|
+
const prevUnregister = activeRouteUnregisters.get(routeKey);
|
|
3057
|
+
if (prevUnregister) {
|
|
3058
|
+
log?.info?.(`Deregistering stale route before re-registering: ${account.webhookPath}`);
|
|
3059
|
+
prevUnregister();
|
|
3060
|
+
activeRouteUnregisters.delete(routeKey);
|
|
3354
3061
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3062
|
+
const unregister = registerPluginHttpRoute({
|
|
3063
|
+
path: account.webhookPath,
|
|
3064
|
+
auth: "plugin",
|
|
3065
|
+
replaceExisting: true,
|
|
3066
|
+
pluginId: CHANNEL_ID,
|
|
3067
|
+
accountId: account.accountId,
|
|
3068
|
+
log: (msg) => log?.info?.(msg),
|
|
3069
|
+
handler
|
|
3070
|
+
});
|
|
3071
|
+
activeRouteUnregisters.set(routeKey, unregister);
|
|
3072
|
+
log?.info?.(`Registered HTTP route: ${account.webhookPath} for Remote`);
|
|
3073
|
+
return waitUntilAbort(ctx.abortSignal, () => {
|
|
3074
|
+
log?.info?.(`Stopping Remote channel (account: ${accountId})`);
|
|
3075
|
+
if (typeof unregister === "function") unregister();
|
|
3076
|
+
activeRouteUnregisters.delete(routeKey);
|
|
3359
3077
|
});
|
|
3360
|
-
log?.info?.(`Stopped Remote channel (account: ${accountId})`);
|
|
3361
3078
|
},
|
|
3362
3079
|
stopAccount: async (ctx) => {
|
|
3363
3080
|
ctx.log?.info?.(`Remote account ${ctx.accountId} stopped`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-remote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Remote project board channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"openclaw": "*"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@sinclair/typebox": "^0.34.48"
|
|
20
|
-
"@supabase/supabase-js": "^2.99.1"
|
|
19
|
+
"@sinclair/typebox": "^0.34.48"
|
|
21
20
|
}
|
|
22
21
|
}
|