@clawhive/openclaw-plugin 0.2.2 → 0.2.4
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/src/authorization.js +10 -3
- package/dist/src/client.d.ts +8 -1
- package/dist/src/client.js +11 -0
- package/dist/src/deviceCode.d.ts +1 -0
- package/dist/src/deviceCode.js +11 -2
- package/dist/src/transport/realtime.d.ts +5 -1
- package/dist/src/transport/realtime.js +130 -20
- package/dist/src/wake.js +12 -12
- package/package.json +1 -1
|
@@ -94,7 +94,8 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
|
|
|
94
94
|
const { account, cloudApi } = (deps.createCloudApi ?? createClawHiveCloudApi)(api);
|
|
95
95
|
if (cloudApi.pluginToken) {
|
|
96
96
|
try {
|
|
97
|
-
await cloudApi.registerPluginNode();
|
|
97
|
+
const registration = await cloudApi.registerPluginNode();
|
|
98
|
+
await syncRegistrationIdentity(api, { account, registration });
|
|
98
99
|
api.logger.info("ClawHive is already authorized. Restart the gateway if you are waiting for it to connect.");
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
@@ -118,13 +119,19 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
|
|
|
118
119
|
start,
|
|
119
120
|
logger: api.logger,
|
|
120
121
|
});
|
|
122
|
+
cloudApi.setAuthorizationIdentity({
|
|
123
|
+
pluginToken: approval.plugin_token,
|
|
124
|
+
userId: approval.user_id,
|
|
125
|
+
});
|
|
126
|
+
const registration = await cloudApi.registerPluginNode();
|
|
121
127
|
const latestCfg = api.runtime.config.loadConfig();
|
|
122
128
|
await api.runtime.config.writeConfigFile(writeRegistrationIdentity(latestCfg, {
|
|
123
129
|
anonKey: account.anonKey,
|
|
124
130
|
pluginToken: approval.plugin_token,
|
|
125
|
-
userId:
|
|
131
|
+
userId: registration.user_id,
|
|
132
|
+
pluginNodeId: registration.plugin_node.id,
|
|
126
133
|
}));
|
|
127
|
-
api.logger.info("ClawHive authorization completed. Restart the gateway
|
|
134
|
+
api.logger.info("ClawHive authorization completed and plugin node registered. Restart the gateway if you are waiting for it to connect.");
|
|
128
135
|
}
|
|
129
136
|
catch (error) {
|
|
130
137
|
if (error instanceof DeviceCodeDeniedError) {
|
package/dist/src/client.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type PluginNode = {
|
|
|
47
47
|
node_name?: string | null;
|
|
48
48
|
plugin_version?: string | null;
|
|
49
49
|
plugin_status: "online" | "offline" | "degraded";
|
|
50
|
+
runtime_type?: "openclaw" | "hermes";
|
|
50
51
|
last_heartbeat_at?: string | null;
|
|
51
52
|
};
|
|
52
53
|
export type PluginRegisterResult = {
|
|
@@ -77,11 +78,13 @@ export type SyncAgentInput = {
|
|
|
77
78
|
export type SyncedAgent = {
|
|
78
79
|
id: string;
|
|
79
80
|
user_id: string;
|
|
81
|
+
plugin_node_id?: string | null;
|
|
82
|
+
runtime_type: "openclaw" | "hermes";
|
|
83
|
+
runtime_agent_id: string;
|
|
80
84
|
openclaw_agent_id: string;
|
|
81
85
|
name: string;
|
|
82
86
|
description: string | null;
|
|
83
87
|
avatar_url: string | null;
|
|
84
|
-
source_type: string;
|
|
85
88
|
config: Record<string, unknown>;
|
|
86
89
|
created_at: string;
|
|
87
90
|
};
|
|
@@ -179,6 +182,10 @@ export declare class ClawHiveCloudApi {
|
|
|
179
182
|
get baseUrl(): string;
|
|
180
183
|
get userId(): string | null;
|
|
181
184
|
get pluginToken(): string | null;
|
|
185
|
+
setAuthorizationIdentity(identity: {
|
|
186
|
+
pluginToken: string;
|
|
187
|
+
userId: string;
|
|
188
|
+
}): void;
|
|
182
189
|
private requireUserId;
|
|
183
190
|
private requirePluginToken;
|
|
184
191
|
startDeviceCode(opts?: {
|
package/dist/src/client.js
CHANGED
|
@@ -68,6 +68,10 @@ export class ClawHiveCloudApi {
|
|
|
68
68
|
get pluginToken() {
|
|
69
69
|
return this.config.pluginToken ?? null;
|
|
70
70
|
}
|
|
71
|
+
setAuthorizationIdentity(identity) {
|
|
72
|
+
this.config.pluginToken = identity.pluginToken;
|
|
73
|
+
this.config.userId = identity.userId;
|
|
74
|
+
}
|
|
71
75
|
requireUserId() {
|
|
72
76
|
if (!this.config.userId) {
|
|
73
77
|
throw new Error("ClawHiveCloudApi: userId is not set. Call registerPluginNode() before making user-scoped calls.");
|
|
@@ -86,6 +90,7 @@ export class ClawHiveCloudApi {
|
|
|
86
90
|
method: "POST",
|
|
87
91
|
auth: "none",
|
|
88
92
|
body: {
|
|
93
|
+
runtime_type: "openclaw",
|
|
89
94
|
node_name: opts?.nodeName ?? this.config.nodeName ?? "OpenClaw Desktop",
|
|
90
95
|
plugin_version: opts?.pluginVersion ?? this.config.pluginVersion ?? "0.2.0",
|
|
91
96
|
description: opts?.description,
|
|
@@ -128,6 +133,7 @@ export class ClawHiveCloudApi {
|
|
|
128
133
|
plugin_node_id: this.config.pluginNodeId ?? null,
|
|
129
134
|
node_name: this.config.nodeName ?? "OpenClaw Desktop",
|
|
130
135
|
plugin_version: this.config.pluginVersion ?? "0.2.0",
|
|
136
|
+
runtime_type: "openclaw",
|
|
131
137
|
};
|
|
132
138
|
if (this.config.userId) {
|
|
133
139
|
body.user_id = this.config.userId;
|
|
@@ -139,6 +145,7 @@ export class ClawHiveCloudApi {
|
|
|
139
145
|
body,
|
|
140
146
|
});
|
|
141
147
|
this.config.userId = result.user_id;
|
|
148
|
+
this.config.pluginNodeId = result.plugin_node.id;
|
|
142
149
|
return result;
|
|
143
150
|
}
|
|
144
151
|
async createPairingCode(pluginNodeId, ttlSeconds) {
|
|
@@ -172,6 +179,7 @@ export class ClawHiveCloudApi {
|
|
|
172
179
|
auth: "plugin",
|
|
173
180
|
body: {
|
|
174
181
|
plugin_node_id: params.pluginNodeId,
|
|
182
|
+
runtime_type: "openclaw",
|
|
175
183
|
session_id: params.sessionId,
|
|
176
184
|
agent_id: params.agentId,
|
|
177
185
|
user_id: this.requireUserId(),
|
|
@@ -190,7 +198,10 @@ export class ClawHiveCloudApi {
|
|
|
190
198
|
auth: "plugin",
|
|
191
199
|
body: {
|
|
192
200
|
user_id: this.requireUserId(),
|
|
201
|
+
plugin_node_id: this.config.pluginNodeId,
|
|
202
|
+
runtime_type: "openclaw",
|
|
193
203
|
agents: agents.map((agent) => ({
|
|
204
|
+
runtime_agent_id: agent.openclawAgentId,
|
|
194
205
|
openclaw_agent_id: agent.openclawAgentId,
|
|
195
206
|
name: agent.name,
|
|
196
207
|
description: agent.description ?? null,
|
package/dist/src/deviceCode.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type DeviceCodeAwaitResult = {
|
|
|
7
7
|
user_id: string;
|
|
8
8
|
plugin_token: string;
|
|
9
9
|
};
|
|
10
|
+
export declare function normalizeDeviceCodeUrl(url: string): string;
|
|
10
11
|
export declare function printDeviceCodePrompt(start: DeviceCodeStartResult, logger: DeviceCodeLogger): Promise<void>;
|
|
11
12
|
export declare function awaitDeviceCodeApproval(params: {
|
|
12
13
|
api: ClawHiveCloudApi;
|
package/dist/src/deviceCode.js
CHANGED
|
@@ -5,15 +5,24 @@ async function renderQr(text) {
|
|
|
5
5
|
qrcodeTerminal.generate(text, { small: true }, (qr) => resolve(qr));
|
|
6
6
|
});
|
|
7
7
|
}
|
|
8
|
+
export function normalizeDeviceCodeUrl(url) {
|
|
9
|
+
const parsed = new URL(url);
|
|
10
|
+
if (parsed.hostname === "127.0.0.1") {
|
|
11
|
+
parsed.hostname = "localhost";
|
|
12
|
+
}
|
|
13
|
+
return parsed.toString();
|
|
14
|
+
}
|
|
8
15
|
export async function printDeviceCodePrompt(start, logger) {
|
|
9
|
-
const
|
|
16
|
+
const verificationUri = normalizeDeviceCodeUrl(start.verification_uri);
|
|
17
|
+
const verificationUriComplete = normalizeDeviceCodeUrl(start.verification_uri_complete);
|
|
18
|
+
const qr = await renderQr(verificationUriComplete);
|
|
10
19
|
const expiresInMin = Math.floor(start.expires_in / 60);
|
|
11
20
|
const lines = [
|
|
12
21
|
"",
|
|
13
22
|
"════════════════════════════════════════════════════════════",
|
|
14
23
|
" ClawHive: authorize this OpenClaw install.",
|
|
15
24
|
"",
|
|
16
|
-
` Visit: ${
|
|
25
|
+
` Visit: ${verificationUri}`,
|
|
17
26
|
` Enter the code: ${start.user_code}`,
|
|
18
27
|
"",
|
|
19
28
|
" Or scan this QR to jump straight to the approval page:",
|
|
@@ -28,15 +28,19 @@ export declare class RealtimeTransport implements TransportAdapter {
|
|
|
28
28
|
private reconnectTimer;
|
|
29
29
|
private session;
|
|
30
30
|
private readonly sessionHighWaterMark;
|
|
31
|
+
private readonly inFlightMessageSequences;
|
|
31
32
|
constructor(options: RealtimeTransportOptions);
|
|
32
33
|
getStatus(): TransportStatus;
|
|
33
34
|
getClient(): SupabaseClient;
|
|
34
35
|
start(handlers: TransportHandlers): Promise<void>;
|
|
35
36
|
stop(): Promise<void>;
|
|
36
37
|
private connect;
|
|
37
|
-
private
|
|
38
|
+
private handleMessageBroadcast;
|
|
38
39
|
private dispatchInbound;
|
|
39
40
|
catchUpSession(sessionId: string, afterSequence: number, beforeSequence?: number): Promise<void>;
|
|
41
|
+
recoverPendingUserMessages(): Promise<void>;
|
|
42
|
+
private recoverPendingSessionMessages;
|
|
43
|
+
private dispatchUserMessageOnce;
|
|
40
44
|
private ensureFreshSession;
|
|
41
45
|
/**
|
|
42
46
|
* Recover from an invalidated refresh token by re-registering the plugin node.
|
|
@@ -3,6 +3,8 @@ import WebSocket from "ws";
|
|
|
3
3
|
import { inboundMessageFromRow, } from "../transport.js";
|
|
4
4
|
import { addPluginBreadcrumb, capturePluginError } from "../telemetry.js";
|
|
5
5
|
const DEFAULT_MAX_BACKOFF_MS = 30_000;
|
|
6
|
+
const PENDING_SESSION_RECOVERY_LIMIT = 25;
|
|
7
|
+
const PENDING_MESSAGE_LOOKBACK_LIMIT = 50;
|
|
6
8
|
const NodeWebSocketTransport = WebSocket;
|
|
7
9
|
export class RealtimeTransport {
|
|
8
10
|
id = "clawhive-realtime";
|
|
@@ -16,6 +18,7 @@ export class RealtimeTransport {
|
|
|
16
18
|
reconnectTimer = null;
|
|
17
19
|
session;
|
|
18
20
|
sessionHighWaterMark = new Map();
|
|
21
|
+
inFlightMessageSequences = new Set();
|
|
19
22
|
constructor(options) {
|
|
20
23
|
this.options = options;
|
|
21
24
|
this.session = options.userSession;
|
|
@@ -79,14 +82,11 @@ export class RealtimeTransport {
|
|
|
79
82
|
});
|
|
80
83
|
await this.client.realtime.setAuth(this.session.access_token);
|
|
81
84
|
const channel = this.client
|
|
82
|
-
.channel(`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
filter: `user_id=eq.${this.options.userId}`,
|
|
88
|
-
}, (payload) => {
|
|
89
|
-
void this.handleRow(payload.new);
|
|
85
|
+
.channel(`user:${this.options.userId}`, {
|
|
86
|
+
config: { private: true },
|
|
87
|
+
})
|
|
88
|
+
.on("broadcast", { event: "message_insert" }, (payload) => {
|
|
89
|
+
void this.handleMessageBroadcast(payload);
|
|
90
90
|
});
|
|
91
91
|
this.channel = channel;
|
|
92
92
|
await new Promise((resolve, reject) => {
|
|
@@ -98,6 +98,7 @@ export class RealtimeTransport {
|
|
|
98
98
|
void (async () => {
|
|
99
99
|
try {
|
|
100
100
|
await this.options.onClientReady?.(this.getClient());
|
|
101
|
+
await this.recoverPendingUserMessages();
|
|
101
102
|
}
|
|
102
103
|
catch (error) {
|
|
103
104
|
reject(error);
|
|
@@ -138,20 +139,23 @@ export class RealtimeTransport {
|
|
|
138
139
|
});
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
|
-
async
|
|
142
|
+
async handleMessageBroadcast(envelope) {
|
|
142
143
|
if (!this.handlers)
|
|
143
144
|
return;
|
|
144
|
-
|
|
145
|
+
const messageRef = parseMessageBroadcastPayload(envelope);
|
|
146
|
+
if (!messageRef)
|
|
145
147
|
return;
|
|
146
|
-
|
|
147
|
-
if (row.sequence <= previousSequence)
|
|
148
|
+
if (messageRef.userId !== this.options.userId)
|
|
148
149
|
return;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
if (messageRef.role !== "user")
|
|
151
|
+
return;
|
|
152
|
+
const previousSequence = this.sessionHighWaterMark.get(messageRef.sessionId) ?? 0;
|
|
153
|
+
if (messageRef.sequence <= previousSequence)
|
|
154
|
+
return;
|
|
155
|
+
const afterSequence = previousSequence > 0
|
|
156
|
+
? previousSequence
|
|
157
|
+
: Math.max(0, messageRef.sequence - 1);
|
|
158
|
+
await this.catchUpSession(messageRef.sessionId, afterSequence);
|
|
155
159
|
}
|
|
156
160
|
async dispatchInbound(message) {
|
|
157
161
|
try {
|
|
@@ -178,8 +182,7 @@ export class RealtimeTransport {
|
|
|
178
182
|
continue;
|
|
179
183
|
if (item.role !== "user")
|
|
180
184
|
continue;
|
|
181
|
-
await this.
|
|
182
|
-
this.sessionHighWaterMark.set(item.session_id, item.sequence);
|
|
185
|
+
await this.dispatchUserMessageOnce(item);
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
catch (error) {
|
|
@@ -188,6 +191,73 @@ export class RealtimeTransport {
|
|
|
188
191
|
capturePluginError(error, "realtime-catch-up");
|
|
189
192
|
}
|
|
190
193
|
}
|
|
194
|
+
async recoverPendingUserMessages() {
|
|
195
|
+
if (!this.client || !this.handlers)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
const { data: sessions, error } = await this.client
|
|
199
|
+
.from("sessions")
|
|
200
|
+
.select("id")
|
|
201
|
+
.eq("user_id", this.options.userId)
|
|
202
|
+
.eq("session_type", "single_agent")
|
|
203
|
+
.is("archived_at", null)
|
|
204
|
+
.order("last_message_at", { ascending: false, nullsFirst: false })
|
|
205
|
+
.limit(PENDING_SESSION_RECOVERY_LIMIT);
|
|
206
|
+
if (error) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
for (const session of sessions ?? []) {
|
|
210
|
+
if (!isRecord(session) || typeof session.id !== "string") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
await this.recoverPendingSessionMessages(session.id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
218
|
+
this.options.logger?.warn(`ClawHive realtime pending-message recovery failed: ${reason}`);
|
|
219
|
+
capturePluginError(error, "realtime-pending-message-recovery");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async recoverPendingSessionMessages(sessionId) {
|
|
223
|
+
if (!this.client || !this.handlers)
|
|
224
|
+
return;
|
|
225
|
+
const { data, error } = await this.client
|
|
226
|
+
.from("messages")
|
|
227
|
+
.select("id, session_id, agent_id, user_id, sequence, client_message_id, role, type, content, metadata, created_at")
|
|
228
|
+
.eq("session_id", sessionId)
|
|
229
|
+
.order("sequence", { ascending: false })
|
|
230
|
+
.limit(PENDING_MESSAGE_LOOKBACK_LIMIT);
|
|
231
|
+
if (error) {
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const rows = (data ?? [])
|
|
235
|
+
.filter(isAppendedMessage)
|
|
236
|
+
.reverse();
|
|
237
|
+
let lastNonUserIndex = -1;
|
|
238
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
239
|
+
if (rows[index].role !== "user") {
|
|
240
|
+
lastNonUserIndex = index;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const pendingUserMessages = rows.slice(lastNonUserIndex + 1)
|
|
244
|
+
.filter((row) => row.role === "user");
|
|
245
|
+
for (const item of pendingUserMessages) {
|
|
246
|
+
await this.dispatchUserMessageOnce(item);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async dispatchUserMessageOnce(item) {
|
|
250
|
+
const currentHighWater = this.sessionHighWaterMark.get(item.session_id) ?? 0;
|
|
251
|
+
if (item.sequence <= currentHighWater)
|
|
252
|
+
return;
|
|
253
|
+
const inFlightKey = `${item.session_id}:${item.sequence}`;
|
|
254
|
+
if (this.inFlightMessageSequences.has(inFlightKey))
|
|
255
|
+
return;
|
|
256
|
+
this.inFlightMessageSequences.add(inFlightKey);
|
|
257
|
+
await this.dispatchInbound(inboundMessageFromRow(item));
|
|
258
|
+
this.inFlightMessageSequences.delete(inFlightKey);
|
|
259
|
+
this.sessionHighWaterMark.set(item.session_id, Math.max(this.sessionHighWaterMark.get(item.session_id) ?? 0, item.sequence));
|
|
260
|
+
}
|
|
191
261
|
async ensureFreshSession() {
|
|
192
262
|
const expiresAt = this.session.expires_at;
|
|
193
263
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
@@ -271,3 +341,43 @@ export class RealtimeTransport {
|
|
|
271
341
|
this.handlers?.onStatusChange?.(status);
|
|
272
342
|
}
|
|
273
343
|
}
|
|
344
|
+
function parseMessageBroadcastPayload(envelope) {
|
|
345
|
+
if (!isRecord(envelope)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const payload = envelope.payload;
|
|
349
|
+
if (!isRecord(payload)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const { messageId, sessionId, userId, sequence, role, createdAt } = payload;
|
|
353
|
+
if (typeof messageId !== "string" ||
|
|
354
|
+
typeof sessionId !== "string" ||
|
|
355
|
+
typeof userId !== "string" ||
|
|
356
|
+
typeof sequence !== "number" ||
|
|
357
|
+
typeof role !== "string" ||
|
|
358
|
+
typeof createdAt !== "string" ||
|
|
359
|
+
!isMessageRole(role)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return { messageId, sessionId, userId, sequence, role, createdAt };
|
|
363
|
+
}
|
|
364
|
+
function isMessageRole(role) {
|
|
365
|
+
return role === "user" || role === "agent" || role === "system";
|
|
366
|
+
}
|
|
367
|
+
function isAppendedMessage(value) {
|
|
368
|
+
if (!isRecord(value)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return (typeof value.id === "string" &&
|
|
372
|
+
typeof value.session_id === "string" &&
|
|
373
|
+
typeof value.agent_id === "string" &&
|
|
374
|
+
typeof value.user_id === "string" &&
|
|
375
|
+
typeof value.sequence === "number" &&
|
|
376
|
+
typeof value.role === "string" &&
|
|
377
|
+
typeof value.type === "string" &&
|
|
378
|
+
typeof value.content === "string" &&
|
|
379
|
+
typeof value.created_at === "string");
|
|
380
|
+
}
|
|
381
|
+
function isRecord(value) {
|
|
382
|
+
return typeof value === "object" && value !== null;
|
|
383
|
+
}
|
package/dist/src/wake.js
CHANGED
|
@@ -17,14 +17,11 @@ export class PluginWakeSubscription {
|
|
|
17
17
|
return this.startPromise;
|
|
18
18
|
}
|
|
19
19
|
const channel = this.options.client
|
|
20
|
-
.channel(`plugin-wake:${this.options.pluginNodeId}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
filter: `plugin_node_id=eq.${this.options.pluginNodeId}`,
|
|
26
|
-
}, (payload) => {
|
|
27
|
-
this.handleWakeRow(payload.new);
|
|
20
|
+
.channel(`plugin-wake:${this.options.pluginNodeId}`, {
|
|
21
|
+
config: { private: true },
|
|
22
|
+
})
|
|
23
|
+
.on("broadcast", { event: "wake" }, (payload) => {
|
|
24
|
+
this.handleWakeRow(payload.payload);
|
|
28
25
|
});
|
|
29
26
|
this.channel = channel;
|
|
30
27
|
this.startPromise = new Promise((resolve) => {
|
|
@@ -65,13 +62,13 @@ export class PluginWakeSubscription {
|
|
|
65
62
|
if (!isRecord(row)) {
|
|
66
63
|
return;
|
|
67
64
|
}
|
|
68
|
-
const taskType = row
|
|
65
|
+
const taskType = readString(row, "taskType", "task_type");
|
|
69
66
|
if (typeof taskType !== "string" || !VALID_TASK_TYPES.has(taskType)) {
|
|
70
67
|
return;
|
|
71
68
|
}
|
|
72
|
-
const taskId = row
|
|
73
|
-
const pluginNodeId = row
|
|
74
|
-
const userId = row
|
|
69
|
+
const taskId = readString(row, "taskId", "task_id");
|
|
70
|
+
const pluginNodeId = readString(row, "pluginNodeId", "plugin_node_id");
|
|
71
|
+
const userId = readString(row, "userId", "user_id");
|
|
75
72
|
if (!isNonEmptyString(taskId) || !isNonEmptyString(pluginNodeId) || !isNonEmptyString(userId)) {
|
|
76
73
|
return;
|
|
77
74
|
}
|
|
@@ -100,3 +97,6 @@ function isPlainObject(value) {
|
|
|
100
97
|
function isNonEmptyString(value) {
|
|
101
98
|
return typeof value === "string" && value.length > 0;
|
|
102
99
|
}
|
|
100
|
+
function readString(row, camelCaseKey, snakeCaseKey) {
|
|
101
|
+
return row[camelCaseKey] ?? row[snakeCaseKey];
|
|
102
|
+
}
|