@soyeht/soyeht 0.2.7 → 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.
@@ -5,7 +5,7 @@
5
5
  ],
6
6
  "name": "Soyeht",
7
7
  "description": "Channel plugin for the Soyeht Flutter mobile app",
8
- "version": "0.2.7",
8
+ "version": "0.2.8",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soyeht/soyeht",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "OpenClaw channel plugin for the Soyeht Flutter mobile app",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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
- // TODO: Push decrypted message to OpenClaw agent pipeline.
240
- // The exact API depends on the OpenClaw runtime (e.g. api.channel.pushInbound()).
241
- // For now the message is decrypted and the session is updated;
242
- // wiring to the agent conversation will be done when the runtime API is known.
243
- api.logger.info("[soyeht] Inbound message received (direct mode)", {
244
- accountId: result.accountId,
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
- sendJson(res, 200, { ok: true, received: true, accountId: result.accountId });
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/version.ts CHANGED
@@ -1 +1 @@
1
- export const PLUGIN_VERSION = "0.2.7";
1
+ export const PLUGIN_VERSION = "0.2.8";