@soyeht/soyeht 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/http.ts +147 -10
- package/src/openclaw-plugin-sdk.d.ts +100 -0
- package/src/service.ts +8 -3
- package/src/version.ts +1 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/http.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi, PluginRuntimeChannel } from "openclaw/plugin-sdk";
|
|
3
3
|
import { normalizeAccountId, resolveSoyehtAccount } from "./config.js";
|
|
4
|
-
import { decryptEnvelopeV2, validateEnvelopeV2, type EnvelopeV2 } from "./envelope-v2.js";
|
|
4
|
+
import { decryptEnvelopeV2, encryptEnvelopeV2, validateEnvelopeV2, type EnvelopeV2 } from "./envelope-v2.js";
|
|
5
|
+
import { buildOutboundEnvelope } from "./outbound.js";
|
|
5
6
|
import { cloneRatchetSession, zeroBuffer, type RatchetState } from "./ratchet.js";
|
|
6
7
|
import type { SecurityV2Deps } from "./service.js";
|
|
7
8
|
import { PLUGIN_VERSION } from "./version.js";
|
|
@@ -236,15 +237,38 @@ export function inboundHandler(
|
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
const { plaintext, accountId } = result;
|
|
241
|
+
|
|
242
|
+
// Parse the decrypted message (EnvelopeV2 plaintext is JSON with contentType/text)
|
|
243
|
+
let messageText: string;
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(plaintext);
|
|
246
|
+
messageText = parsed.text ?? parsed.body ?? plaintext;
|
|
247
|
+
} catch {
|
|
248
|
+
messageText = plaintext;
|
|
249
|
+
}
|
|
246
250
|
|
|
247
|
-
|
|
251
|
+
api.logger.info("[soyeht] Inbound message received (direct mode)", { accountId });
|
|
252
|
+
|
|
253
|
+
// Respond 200 immediately — agent dispatch happens in background
|
|
254
|
+
sendJson(res, 200, { ok: true, received: true, accountId });
|
|
255
|
+
|
|
256
|
+
// Fire-and-forget: dispatch to agent pipeline
|
|
257
|
+
const channelRuntime = (api.runtime as Record<string, unknown>).channel as
|
|
258
|
+
| PluginRuntimeChannel
|
|
259
|
+
| undefined;
|
|
260
|
+
if (channelRuntime?.routing && channelRuntime?.reply) {
|
|
261
|
+
dispatchToAgent(api, v2deps, channelRuntime, accountId, messageText).catch((err) => {
|
|
262
|
+
api.logger.error("[soyeht] Agent dispatch failed", {
|
|
263
|
+
accountId,
|
|
264
|
+
error: err instanceof Error ? err.message : String(err),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
api.logger.warn(
|
|
269
|
+
"[soyeht] channelRuntime not available — message decrypted but not delivered to agent",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
248
272
|
} catch (err) {
|
|
249
273
|
const message = err instanceof Error ? err.message : "unknown_error";
|
|
250
274
|
sendJson(res, 400, { ok: false, error: message });
|
|
@@ -252,6 +276,119 @@ export function inboundHandler(
|
|
|
252
276
|
};
|
|
253
277
|
}
|
|
254
278
|
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Dispatch inbound message to OpenClaw agent pipeline (fire-and-forget)
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
async function dispatchToAgent(
|
|
284
|
+
api: OpenClawPluginApi,
|
|
285
|
+
v2deps: SecurityV2Deps,
|
|
286
|
+
channelRuntime: PluginRuntimeChannel,
|
|
287
|
+
accountId: string,
|
|
288
|
+
messageText: string,
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
291
|
+
const account = resolveSoyehtAccount(cfg, accountId);
|
|
292
|
+
|
|
293
|
+
const route = channelRuntime.routing.resolveAgentRoute({
|
|
294
|
+
cfg,
|
|
295
|
+
channel: "soyeht",
|
|
296
|
+
accountId: account.accountId,
|
|
297
|
+
peer: { kind: "direct", id: accountId },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const sessionCfg = (cfg as Record<string, unknown>).session as
|
|
301
|
+
| Record<string, unknown>
|
|
302
|
+
| undefined;
|
|
303
|
+
const storePath = channelRuntime.session.resolveStorePath(
|
|
304
|
+
sessionCfg?.store as string | undefined,
|
|
305
|
+
{ agentId: route.agentId },
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const envelopeOptions = channelRuntime.reply.resolveEnvelopeFormatOptions(cfg);
|
|
309
|
+
const previousTimestamp = channelRuntime.session.readSessionUpdatedAt({
|
|
310
|
+
storePath,
|
|
311
|
+
sessionKey: route.sessionKey,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const body = channelRuntime.reply.formatAgentEnvelope({
|
|
315
|
+
channel: "Soyeht",
|
|
316
|
+
from: `soyeht:${accountId}`,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
previousTimestamp,
|
|
319
|
+
envelope: envelopeOptions,
|
|
320
|
+
body: messageText,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const ctxPayload = channelRuntime.reply.finalizeInboundContext({
|
|
324
|
+
Body: body,
|
|
325
|
+
BodyForAgent: messageText,
|
|
326
|
+
RawBody: messageText,
|
|
327
|
+
From: `soyeht:${accountId}`,
|
|
328
|
+
To: `soyeht:${accountId}`,
|
|
329
|
+
SessionKey: route.sessionKey,
|
|
330
|
+
AccountId: route.accountId,
|
|
331
|
+
ChatType: "direct",
|
|
332
|
+
ConversationLabel: `soyeht:${accountId}`,
|
|
333
|
+
SenderName: accountId,
|
|
334
|
+
SenderId: accountId,
|
|
335
|
+
CommandAuthorized: true,
|
|
336
|
+
Provider: "soyeht",
|
|
337
|
+
Surface: "soyeht",
|
|
338
|
+
OriginatingChannel: "soyeht",
|
|
339
|
+
OriginatingTo: `soyeht:${accountId}`,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await channelRuntime.session.recordInboundSession({
|
|
343
|
+
storePath,
|
|
344
|
+
sessionKey: (ctxPayload as Record<string, unknown>).SessionKey as string ?? route.sessionKey,
|
|
345
|
+
ctx: ctxPayload,
|
|
346
|
+
onRecordError: (err: unknown) => {
|
|
347
|
+
api.logger.error("[soyeht] Failed updating session meta", { error: String(err) });
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
352
|
+
ctx: ctxPayload,
|
|
353
|
+
cfg,
|
|
354
|
+
dispatcherOptions: {
|
|
355
|
+
deliver: async (payload: { text?: string; mediaUrl?: string }) => {
|
|
356
|
+
const text = payload.text;
|
|
357
|
+
if (!text) return;
|
|
358
|
+
|
|
359
|
+
const ratchetSession = v2deps.sessions.get(accountId);
|
|
360
|
+
if (!ratchetSession) {
|
|
361
|
+
api.logger.warn("[soyeht] No ratchet session for reply delivery", { accountId });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const envelope = buildOutboundEnvelope(accountId, accountId, {
|
|
366
|
+
contentType: "text",
|
|
367
|
+
text,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const { envelope: v2env, updatedSession } = encryptEnvelopeV2({
|
|
371
|
+
session: ratchetSession,
|
|
372
|
+
accountId,
|
|
373
|
+
plaintext: JSON.stringify(envelope),
|
|
374
|
+
dhRatchetCfg: {
|
|
375
|
+
intervalMessages: account.security.dhRatchetIntervalMessages,
|
|
376
|
+
intervalMs: account.security.dhRatchetIntervalMs,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
v2deps.sessions.set(accountId, updatedSession);
|
|
380
|
+
v2deps.outboundQueue.enqueue(accountId, v2env);
|
|
381
|
+
},
|
|
382
|
+
onError: (err: unknown, info: { kind: string }) => {
|
|
383
|
+
api.logger.error(`[soyeht] ${info.kind} reply failed`, {
|
|
384
|
+
accountId,
|
|
385
|
+
error: err instanceof Error ? err.message : String(err),
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
255
392
|
// ---------------------------------------------------------------------------
|
|
256
393
|
// GET /soyeht/events/:accountId (SSE — plugin → app outbound stream)
|
|
257
394
|
// ---------------------------------------------------------------------------
|
|
@@ -140,6 +140,105 @@ declare module "openclaw/plugin-sdk" {
|
|
|
140
140
|
sampleRate?: number;
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// ---- Channel runtime types (subset used by soyeht inbound dispatch) ----
|
|
144
|
+
|
|
145
|
+
export type ResolvedAgentRoute = {
|
|
146
|
+
agentId: string;
|
|
147
|
+
channel: string;
|
|
148
|
+
accountId: string;
|
|
149
|
+
sessionKey: string;
|
|
150
|
+
mainSessionKey: string;
|
|
151
|
+
lastRoutePolicy: "main" | "session";
|
|
152
|
+
matchedBy: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type EnvelopeFormatOptions = {
|
|
156
|
+
timezone?: string;
|
|
157
|
+
includeTimestamp?: boolean;
|
|
158
|
+
includeElapsed?: boolean;
|
|
159
|
+
userTimezone?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type ReplyPayload = {
|
|
163
|
+
text?: string;
|
|
164
|
+
mediaUrl?: string;
|
|
165
|
+
mediaUrls?: string[];
|
|
166
|
+
isError?: boolean;
|
|
167
|
+
isReasoning?: boolean;
|
|
168
|
+
[key: string]: unknown;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type ReplyDispatchKind = "tool" | "block" | "final";
|
|
172
|
+
|
|
173
|
+
export type PluginRuntimeChannel = {
|
|
174
|
+
routing: {
|
|
175
|
+
resolveAgentRoute: (params: {
|
|
176
|
+
cfg: OpenClawConfig;
|
|
177
|
+
channel: string;
|
|
178
|
+
accountId?: string | null;
|
|
179
|
+
peer?: { kind: ChatType; id: string } | null;
|
|
180
|
+
parentPeer?: { kind: ChatType; id: string } | null;
|
|
181
|
+
guildId?: string | null;
|
|
182
|
+
teamId?: string | null;
|
|
183
|
+
memberRoleIds?: string[];
|
|
184
|
+
}) => ResolvedAgentRoute;
|
|
185
|
+
buildAgentSessionKey: (...args: unknown[]) => string;
|
|
186
|
+
};
|
|
187
|
+
reply: {
|
|
188
|
+
finalizeInboundContext: <T extends Record<string, unknown>>(
|
|
189
|
+
ctx: T,
|
|
190
|
+
opts?: Record<string, unknown>,
|
|
191
|
+
) => T;
|
|
192
|
+
formatAgentEnvelope: (params: {
|
|
193
|
+
channel: string;
|
|
194
|
+
from?: string;
|
|
195
|
+
timestamp?: number | Date;
|
|
196
|
+
previousTimestamp?: number | Date;
|
|
197
|
+
envelope?: EnvelopeFormatOptions;
|
|
198
|
+
body: string;
|
|
199
|
+
host?: string;
|
|
200
|
+
ip?: string;
|
|
201
|
+
}) => string;
|
|
202
|
+
resolveEnvelopeFormatOptions: (cfg?: OpenClawConfig) => EnvelopeFormatOptions;
|
|
203
|
+
dispatchReplyWithBufferedBlockDispatcher: (params: {
|
|
204
|
+
ctx: Record<string, unknown>;
|
|
205
|
+
cfg: OpenClawConfig;
|
|
206
|
+
dispatcherOptions: {
|
|
207
|
+
deliver: (
|
|
208
|
+
payload: ReplyPayload,
|
|
209
|
+
info?: { kind: ReplyDispatchKind },
|
|
210
|
+
) => Promise<void>;
|
|
211
|
+
onError?: (err: unknown, info: { kind: string }) => void;
|
|
212
|
+
[key: string]: unknown;
|
|
213
|
+
};
|
|
214
|
+
replyOptions?: Record<string, unknown>;
|
|
215
|
+
}) => Promise<unknown>;
|
|
216
|
+
dispatchReplyFromConfig: (...args: unknown[]) => Promise<unknown>;
|
|
217
|
+
[key: string]: unknown;
|
|
218
|
+
};
|
|
219
|
+
session: {
|
|
220
|
+
resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
|
|
221
|
+
readSessionUpdatedAt: (params: {
|
|
222
|
+
storePath: string;
|
|
223
|
+
sessionKey: string;
|
|
224
|
+
}) => number | undefined;
|
|
225
|
+
recordInboundSession: (params: {
|
|
226
|
+
storePath: string;
|
|
227
|
+
sessionKey: string;
|
|
228
|
+
ctx: Record<string, unknown>;
|
|
229
|
+
onRecordError: (err: unknown) => void;
|
|
230
|
+
[key: string]: unknown;
|
|
231
|
+
}) => Promise<void>;
|
|
232
|
+
[key: string]: unknown;
|
|
233
|
+
};
|
|
234
|
+
activity: {
|
|
235
|
+
record: (...args: unknown[]) => void;
|
|
236
|
+
[key: string]: unknown;
|
|
237
|
+
};
|
|
238
|
+
text: { [key: string]: unknown };
|
|
239
|
+
[key: string]: unknown;
|
|
240
|
+
};
|
|
241
|
+
|
|
143
242
|
export type PluginRuntime = {
|
|
144
243
|
config: {
|
|
145
244
|
loadConfig: () => Promise<OpenClawConfig>;
|
|
@@ -160,6 +259,7 @@ declare module "openclaw/plugin-sdk" {
|
|
|
160
259
|
prefsPath?: string;
|
|
161
260
|
}) => Promise<TtsTelephonyResult>;
|
|
162
261
|
};
|
|
262
|
+
channel?: PluginRuntimeChannel;
|
|
163
263
|
[key: string]: unknown;
|
|
164
264
|
};
|
|
165
265
|
|
package/src/service.ts
CHANGED
|
@@ -78,15 +78,20 @@ const DEFAULT_GATEWAY_PORT = 18789;
|
|
|
78
78
|
|
|
79
79
|
function detectLanIp(): string | null {
|
|
80
80
|
const nets = networkInterfaces();
|
|
81
|
+
const candidates: string[] = [];
|
|
81
82
|
for (const ifaces of Object.values(nets)) {
|
|
82
83
|
if (!ifaces) continue;
|
|
83
84
|
for (const iface of ifaces) {
|
|
84
|
-
// Skip loopback and internal, only IPv4
|
|
85
85
|
if (iface.internal || iface.family !== "IPv4") continue;
|
|
86
|
-
|
|
86
|
+
candidates.push(iface.address);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
return null;
|
|
89
|
+
if (candidates.length === 0) return null;
|
|
90
|
+
// Prefer typical LAN ranges over VPN/container ranges
|
|
91
|
+
const preferred = candidates.find((ip) => ip.startsWith("192.168."))
|
|
92
|
+
?? candidates.find((ip) => ip.startsWith("10."))
|
|
93
|
+
?? candidates[0];
|
|
94
|
+
return preferred;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
function detectGatewayPort(cfg: Record<string, unknown>): number {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "0.2.
|
|
1
|
+
export const PLUGIN_VERSION = "0.2.8";
|