openclaw-remote 0.4.4 → 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 -414
- 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,347 +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
|
-
if (!agentName) {
|
|
2826
|
-
const hbRes = await fetch(`${account.baseUrl}/api/v1/heartbeat`, {
|
|
2827
|
-
headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
|
|
2828
|
-
signal: AbortSignal.timeout(1e4)
|
|
2829
|
-
});
|
|
2830
|
-
if (hbRes.ok) {
|
|
2831
|
-
const hb = await hbRes.json();
|
|
2832
|
-
agentName = hb.agent_name || "";
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
log?.info?.(`Agent identity resolved: "${agentName}", roles: [${myRoleIds.join(", ")}], leader: ${isLeader}`);
|
|
2836
|
-
return { agentName, myRoleIds, isLeader };
|
|
2837
|
-
} catch (err) {
|
|
2838
|
-
log?.warn?.(`Failed to resolve agent identity: ${err}`);
|
|
2839
|
-
return null;
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
async function fetchLeaderRoles(supabase, log) {
|
|
2843
|
-
const now = Date.now();
|
|
2844
|
-
if (now - leaderRolesCacheTime < LEADER_CACHE_TTL && leaderRolesCache.length > 0) {
|
|
2845
|
-
return leaderRolesCache;
|
|
2846
|
-
}
|
|
2847
|
-
const { data: roles } = await supabase.from("project_roles").select("id").eq("is_leader", true);
|
|
2848
|
-
if (!roles || roles.length === 0) {
|
|
2849
|
-
leaderRolesCache = [];
|
|
2850
|
-
leaderRolesCacheTime = now;
|
|
2851
|
-
return [];
|
|
2852
|
-
}
|
|
2853
|
-
const roleIds = roles.map((r) => r.id);
|
|
2854
|
-
const { data: assignments } = await supabase.from("agent_role_assignments").select("project_role_id, agent_id").in("project_role_id", roleIds);
|
|
2855
|
-
const map = /* @__PURE__ */ new Map();
|
|
2856
|
-
for (const a of assignments ?? []) {
|
|
2857
|
-
const list = map.get(a.project_role_id) || [];
|
|
2858
|
-
list.push(a.agent_id);
|
|
2859
|
-
map.set(a.project_role_id, list);
|
|
2860
|
-
}
|
|
2861
|
-
leaderRolesCache = roleIds.map((id) => ({ roleId: id, agentIds: map.get(id) || [] }));
|
|
2862
|
-
leaderRolesCacheTime = now;
|
|
2863
|
-
log?.info?.(`Leader roles cache refreshed: ${leaderRolesCache.length} leader role(s)`);
|
|
2864
|
-
return leaderRolesCache;
|
|
2865
|
-
}
|
|
2866
|
-
function connectRealtime(account, identity, onEvent, onError, log) {
|
|
2867
|
-
const { supabaseUrl, supabaseKey } = account;
|
|
2868
|
-
if (!supabaseUrl || !supabaseKey) {
|
|
2869
|
-
throw new Error("Supabase URL and key are required for Realtime listener. Set supabaseUrl and supabaseKey in channels.remote config.");
|
|
2870
|
-
}
|
|
2871
|
-
log?.info?.(`Connecting to Supabase Realtime: ${supabaseUrl}`);
|
|
2872
|
-
const supabase = createClient(supabaseUrl, supabaseKey, {
|
|
2873
|
-
realtime: {
|
|
2874
|
-
params: { eventsPerSecond: 10 }
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2877
|
-
const channelName = `remote-${account.accountId}-${Date.now()}`;
|
|
2878
|
-
const channel = supabase.channel(channelName).on(
|
|
2879
|
-
"postgres_changes",
|
|
2880
|
-
{
|
|
2881
|
-
event: "INSERT",
|
|
2882
|
-
schema: "public",
|
|
2883
|
-
table: "task_comments"
|
|
2884
|
-
},
|
|
2885
|
-
(payload) => {
|
|
2886
|
-
try {
|
|
2887
|
-
const row = payload.new;
|
|
2888
|
-
if (row.agent_author_id && !row.author_id) {
|
|
2889
|
-
log?.info?.(`Skipping agent-authored comment on task ${row.task_id} (echo suppression)`);
|
|
2890
|
-
scheduleLeaderNotification(row.task_id, row.agent_author_id, supabase, log);
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
|
-
const body = row.body || row.content || "";
|
|
2894
|
-
if (!row.author_id && !row.agent_author_id && identity) {
|
|
2895
|
-
if (body.startsWith("\u{1F4CC} Assigned to ")) {
|
|
2896
|
-
const targetName = body.replace("\u{1F4CC} Assigned to ", "").trim();
|
|
2897
|
-
if (targetName.toLowerCase() !== identity.agentName.toLowerCase()) {
|
|
2898
|
-
log?.info?.(`Skipping assignment notification for ${targetName} (I am ${identity.agentName})`);
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
if (body.startsWith("\u{1F440} ")) {
|
|
2903
|
-
if (!identity.isLeader) {
|
|
2904
|
-
log?.info?.(`Skipping leader notification (I am not a leader)`);
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
const event = {
|
|
2910
|
-
type: "task.commented",
|
|
2911
|
-
task_id: row.task_id,
|
|
2912
|
-
comment: {
|
|
2913
|
-
id: row.id,
|
|
2914
|
-
content: body,
|
|
2915
|
-
author_name: row.author_name || "System",
|
|
2916
|
-
author_id: row.author_id || "",
|
|
2917
|
-
created_at: row.created_at
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
log?.info?.(`Realtime: new comment on task ${row.task_id}`);
|
|
2921
|
-
onEvent(event);
|
|
2922
|
-
} catch (err) {
|
|
2923
|
-
log?.warn?.(`Failed to process comment event: ${err}`);
|
|
2924
|
-
}
|
|
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;
|
|
2925
2811
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
log?.info?.("Supabase Realtime connected and subscribed.");
|
|
2930
|
-
} else if (status === "CHANNEL_ERROR") {
|
|
2931
|
-
log?.error?.(`Realtime channel error: ${err?.message || "unknown"}`);
|
|
2932
|
-
onError(new Error(`Realtime channel error: ${err?.message || "unknown"}`));
|
|
2933
|
-
} else if (status === "TIMED_OUT") {
|
|
2934
|
-
log?.warn?.("Realtime subscription timed out, will auto-retry...");
|
|
2935
|
-
} else if (status === "CLOSED") {
|
|
2936
|
-
log?.info?.("Realtime channel closed.");
|
|
2812
|
+
if (signal.aborted) {
|
|
2813
|
+
complete();
|
|
2814
|
+
return;
|
|
2937
2815
|
}
|
|
2816
|
+
signal.addEventListener("abort", complete, { once: true });
|
|
2938
2817
|
});
|
|
2939
|
-
return {
|
|
2940
|
-
unsubscribe: async () => {
|
|
2941
|
-
log?.info?.("Unsubscribing from Supabase Realtime...");
|
|
2942
|
-
for (const timer of leaderTimers.values()) clearTimeout(timer);
|
|
2943
|
-
leaderTimers.clear();
|
|
2944
|
-
await supabase.removeChannel(channel);
|
|
2945
|
-
log?.info?.("Realtime listener shut down cleanly.");
|
|
2946
|
-
}
|
|
2947
|
-
};
|
|
2948
2818
|
}
|
|
2949
|
-
|
|
2950
|
-
const
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
leaderTimers.delete(taskId);
|
|
2954
|
-
try {
|
|
2955
|
-
const leaders = await fetchLeaderRoles(supabase, log);
|
|
2956
|
-
if (leaders.length === 0) return;
|
|
2957
|
-
const { data: task } = await supabase.from("tasks").select("id, title, assigned_role_id").eq("id", taskId).single();
|
|
2958
|
-
if (!task) return;
|
|
2959
|
-
for (const leader of leaders) {
|
|
2960
|
-
if (task.assigned_role_id === leader.roleId) {
|
|
2961
|
-
log?.info?.(`Skipping leader notification for task ${taskId} \u2014 assigned to leader role ${leader.roleId}`);
|
|
2962
|
-
continue;
|
|
2963
|
-
}
|
|
2964
|
-
if (leader.agentIds.includes(commentingAgentId)) continue;
|
|
2965
|
-
if (leader.agentIds.length === 0) continue;
|
|
2966
|
-
const { error } = await supabase.from("task_comments").insert({
|
|
2967
|
-
task_id: taskId,
|
|
2968
|
-
author_id: null,
|
|
2969
|
-
agent_author_id: null,
|
|
2970
|
-
body: `\u{1F440} Activity on this task \u2014 leader review needed`
|
|
2971
|
-
});
|
|
2972
|
-
if (error) {
|
|
2973
|
-
log?.warn?.(`Failed to insert leader notification for task ${taskId}: ${error.message}`);
|
|
2974
|
-
} else {
|
|
2975
|
-
log?.info?.(`Leader notification sent for task ${taskId}`);
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
} catch (err) {
|
|
2979
|
-
log?.warn?.(`Leader notification error for task ${taskId}: ${err}`);
|
|
2980
|
-
}
|
|
2981
|
-
}, LEADER_DEBOUNCE_MS);
|
|
2982
|
-
leaderTimers.set(taskId, timer);
|
|
2983
|
-
log?.info?.(`Leader debounce timer set for task ${taskId} (60s)`);
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
// src/event-formatter.ts
|
|
2987
|
-
function formatEvent(event) {
|
|
2988
|
-
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":
|
|
2989
2823
|
case "task.created":
|
|
2990
|
-
return
|
|
2991
|
-
|
|
2992
|
-
|
|
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");
|
|
2993
2834
|
case "task.commented":
|
|
2994
|
-
return
|
|
2995
|
-
|
|
2996
|
-
|
|
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");
|
|
2997
2857
|
default:
|
|
2998
|
-
return
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
function formatTaskCreated(event) {
|
|
3002
|
-
const task = event.task;
|
|
3003
|
-
if (!task) return null;
|
|
3004
|
-
const meta = [task.type, task.priority].filter(Boolean).join(", ");
|
|
3005
|
-
let body = `\u{1F4CB} New task created: **${task.title}**`;
|
|
3006
|
-
if (meta) body += ` [${meta}]`;
|
|
3007
|
-
if (task.description) body += `
|
|
3008
|
-
${task.description}`;
|
|
3009
|
-
body += `
|
|
3010
|
-
|
|
3011
|
-
[task:${task.id}]`;
|
|
3012
|
-
return {
|
|
3013
|
-
body,
|
|
3014
|
-
taskId: task.id,
|
|
3015
|
-
taskTitle: task.title,
|
|
3016
|
-
isDirect: false
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
function formatTaskUpdated(event) {
|
|
3020
|
-
const task = event.task;
|
|
3021
|
-
if (!task) return null;
|
|
3022
|
-
const changes = event.changes;
|
|
3023
|
-
if (changes?.field === "assigned_role_id" || changes?.field === "assigned_to_agent" || changes?.field === "assigned_profile_id") {
|
|
3024
|
-
if (changes.to && changes.to !== "null" && changes.to !== "") {
|
|
3025
|
-
let body2 = `\u{1F464} You've been assigned: **${task.title}**`;
|
|
3026
|
-
if (task.description) body2 += `
|
|
3027
|
-
|
|
3028
|
-
${task.description}`;
|
|
3029
|
-
body2 += `
|
|
3030
|
-
|
|
3031
|
-
Priority: ${task.priority || "medium"} | Type: ${task.type || "task"}`;
|
|
3032
|
-
body2 += `
|
|
3033
|
-
|
|
3034
|
-
Acknowledge this task, move it to in_progress using remote_update_task, and share your plan.`;
|
|
3035
|
-
body2 += `
|
|
3036
|
-
|
|
3037
|
-
[task:${task.id}]`;
|
|
3038
|
-
return {
|
|
3039
|
-
body: body2,
|
|
3040
|
-
taskId: task.id,
|
|
3041
|
-
taskTitle: task.title,
|
|
3042
|
-
isDirect: true
|
|
3043
|
-
};
|
|
3044
|
-
}
|
|
3045
|
-
return null;
|
|
3046
|
-
}
|
|
3047
|
-
if (changes?.field === "status") {
|
|
3048
|
-
let body2 = `\u{1F504} Task **${task.title}** moved to **${changes.to}**`;
|
|
3049
|
-
body2 += `
|
|
3050
|
-
|
|
3051
|
-
[task:${task.id}]`;
|
|
3052
|
-
return {
|
|
3053
|
-
body: body2,
|
|
3054
|
-
taskId: task.id,
|
|
3055
|
-
taskTitle: task.title,
|
|
3056
|
-
isDirect: false
|
|
3057
|
-
};
|
|
3058
|
-
}
|
|
3059
|
-
let body = `\u{1F504} Task updated: **${task.title}**`;
|
|
3060
|
-
if (changes) {
|
|
3061
|
-
body += ` \u2014 ${changes.field} changed from "${changes.from}" to "${changes.to}"`;
|
|
2858
|
+
return `Task update on "${task_title}" (ID: ${task_id}): ${event}${wake_reason ? ` \u2014 ${wake_reason}` : ""}`;
|
|
3062
2859
|
}
|
|
3063
|
-
body += `
|
|
3064
|
-
|
|
3065
|
-
[task:${task.id}]`;
|
|
3066
|
-
return {
|
|
3067
|
-
body,
|
|
3068
|
-
taskId: task.id,
|
|
3069
|
-
taskTitle: task.title,
|
|
3070
|
-
isDirect: false
|
|
3071
|
-
};
|
|
3072
|
-
}
|
|
3073
|
-
function formatTaskCommented(event) {
|
|
3074
|
-
const comment = event.comment;
|
|
3075
|
-
const taskId = event.task_id ?? event.task?.id;
|
|
3076
|
-
if (!comment || !taskId) return null;
|
|
3077
|
-
const taskTitle = event.task?.title ?? `Task ${taskId}`;
|
|
3078
|
-
let body = `\u{1F4AC} ${comment.author_name} commented on **${taskTitle}**:
|
|
3079
|
-
${comment.content}`;
|
|
3080
|
-
body += `
|
|
3081
|
-
|
|
3082
|
-
[task:${taskId}]`;
|
|
3083
|
-
return {
|
|
3084
|
-
body,
|
|
3085
|
-
taskId,
|
|
3086
|
-
senderName: comment.author_name,
|
|
3087
|
-
senderId: comment.author_id,
|
|
3088
|
-
taskTitle,
|
|
3089
|
-
isDirect: false
|
|
3090
|
-
};
|
|
3091
|
-
}
|
|
3092
|
-
function formatTaskAssigned(event) {
|
|
3093
|
-
const task = event.task;
|
|
3094
|
-
const assignee = event.assigned_to;
|
|
3095
|
-
if (!task) return null;
|
|
3096
|
-
let body = `\u{1F464} Task **${task.title}** assigned`;
|
|
3097
|
-
if (assignee) {
|
|
3098
|
-
body += ` to ${assignee.name} (${assignee.role})`;
|
|
3099
|
-
}
|
|
3100
|
-
body += `
|
|
3101
|
-
|
|
3102
|
-
[task:${task.id}]`;
|
|
3103
|
-
return {
|
|
3104
|
-
body,
|
|
3105
|
-
taskId: task.id,
|
|
3106
|
-
senderName: assignee?.name,
|
|
3107
|
-
taskTitle: task.title,
|
|
3108
|
-
// Assigned events are direct — the agent is likely the assignee
|
|
3109
|
-
isDirect: true
|
|
3110
|
-
};
|
|
3111
|
-
}
|
|
3112
|
-
function formatGenericEvent(event) {
|
|
3113
|
-
const task = event.task;
|
|
3114
|
-
const taskId = event.task_id ?? task?.id;
|
|
3115
|
-
if (!taskId) return null;
|
|
3116
|
-
const taskTitle = task?.title ?? `Task ${taskId}`;
|
|
3117
|
-
let body = `\u{1F514} Remote event (${event.type}): **${taskTitle}**`;
|
|
3118
|
-
body += `
|
|
3119
|
-
|
|
3120
|
-
[task:${taskId}]`;
|
|
3121
|
-
return {
|
|
3122
|
-
body,
|
|
3123
|
-
taskId,
|
|
3124
|
-
taskTitle,
|
|
3125
|
-
isDirect: false
|
|
3126
|
-
};
|
|
3127
2860
|
}
|
|
3128
|
-
|
|
3129
|
-
// src/runtime.ts
|
|
3130
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
3131
|
-
var { setRuntime: setRemoteRuntime, getRuntime: getRemoteRuntime } = createPluginRuntimeStore(
|
|
3132
|
-
"Remote runtime not initialized - plugin not registered"
|
|
3133
|
-
);
|
|
3134
|
-
|
|
3135
|
-
// src/channel.ts
|
|
3136
|
-
var CHANNEL_ID = "remote";
|
|
3137
2861
|
function createRemotePlugin() {
|
|
3138
2862
|
return {
|
|
3139
2863
|
id: CHANNEL_ID,
|
|
@@ -3212,11 +2936,6 @@ function createRemotePlugin() {
|
|
|
3212
2936
|
"- Remote: apiKey is not configured. The plugin cannot authenticate with the Remote API."
|
|
3213
2937
|
);
|
|
3214
2938
|
}
|
|
3215
|
-
if (account.dmPolicy === "open") {
|
|
3216
|
-
warnings.push(
|
|
3217
|
-
'- Remote: dmPolicy="open" allows any board event to trigger agent actions. Consider "allowlist" for production use.'
|
|
3218
|
-
);
|
|
3219
|
-
}
|
|
3220
2939
|
return warnings;
|
|
3221
2940
|
}
|
|
3222
2941
|
},
|
|
@@ -3262,96 +2981,100 @@ function createRemotePlugin() {
|
|
|
3262
2981
|
},
|
|
3263
2982
|
gateway: {
|
|
3264
2983
|
startAccount: async (ctx) => {
|
|
3265
|
-
const { cfg, accountId, log
|
|
2984
|
+
const { cfg, accountId, log } = ctx;
|
|
3266
2985
|
const account = resolveAccount(cfg, accountId);
|
|
3267
2986
|
if (!account.enabled) {
|
|
3268
2987
|
log?.info?.(`Remote account ${accountId} is disabled, skipping`);
|
|
3269
|
-
return waitUntilAbort(abortSignal);
|
|
2988
|
+
return waitUntilAbort(ctx.abortSignal);
|
|
3270
2989
|
}
|
|
3271
2990
|
if (!account.baseUrl || !account.apiKey) {
|
|
3272
2991
|
log?.warn?.(
|
|
3273
2992
|
`Remote account ${accountId} not fully configured (missing baseUrl or apiKey)`
|
|
3274
2993
|
);
|
|
3275
|
-
return waitUntilAbort(abortSignal);
|
|
3276
|
-
}
|
|
3277
|
-
if (!account.supabaseUrl || !account.supabaseKey) {
|
|
3278
|
-
log?.warn?.(
|
|
3279
|
-
`Remote account ${accountId} missing supabaseUrl or supabaseKey \u2014 Realtime events disabled. Agent tools still work.`
|
|
3280
|
-
);
|
|
2994
|
+
return waitUntilAbort(ctx.abortSignal);
|
|
3281
2995
|
}
|
|
3282
2996
|
log?.info?.(
|
|
3283
|
-
`Starting Remote channel (account: ${accountId},
|
|
2997
|
+
`Starting Remote channel (account: ${accountId}, webhookPath: ${account.webhookPath})`
|
|
3284
2998
|
);
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
|
|
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}`;
|
|
3288
3010
|
try {
|
|
3289
|
-
const
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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
|
-
deliver: async (payload, _info) => {
|
|
3323
|
-
if (payload.isReasoning) return;
|
|
3324
|
-
const text = payload?.text;
|
|
3325
|
-
if (text) {
|
|
3326
|
-
await postComment(account, formatted.taskId, text);
|
|
3327
|
-
}
|
|
3328
|
-
},
|
|
3329
|
-
onReplyStart: () => {
|
|
3330
|
-
log?.info?.(`Agent reply started for task ${formatted.taskId}`);
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
});
|
|
3334
|
-
} catch (err) {
|
|
3335
|
-
log?.error?.(
|
|
3336
|
-
`Error dispatching Remote event: ${err instanceof Error ? err.message : String(err)}`
|
|
3337
|
-
);
|
|
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}`);
|
|
3338
3044
|
}
|
|
3339
|
-
}
|
|
3340
|
-
|
|
3341
|
-
log?.warn?.(`Realtime error: ${err.message}`);
|
|
3342
|
-
},
|
|
3343
|
-
log
|
|
3344
|
-
);
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3345
3047
|
} catch (err) {
|
|
3346
|
-
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" } };
|
|
3347
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);
|
|
3348
3061
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
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);
|
|
3353
3077
|
});
|
|
3354
|
-
log?.info?.(`Stopped Remote channel (account: ${accountId})`);
|
|
3355
3078
|
},
|
|
3356
3079
|
stopAccount: async (ctx) => {
|
|
3357
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
|
}
|