openclaw-seatalk 0.1.6 → 0.2.1

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/README.md CHANGED
@@ -63,16 +63,15 @@ openclaw plugins install openclaw-seatalk
63
63
 
64
64
  OpenClaw downloads the package, installs dependencies, and registers the plugin automatically. The plugin will appear in the `openclaw onboard` channel selection.
65
65
 
66
- To install and pin a specific version:
66
+ | Plugin version | OpenClaw version |
67
+ |---------------|-----------------|
68
+ | 0.2.x | >= 2026.3.22 |
69
+ | 0.1.x | < 2026.3.22 |
67
70
 
68
- ```bash
69
- openclaw plugins install openclaw-seatalk --pin
70
- ```
71
-
72
- To update to the latest version (restart the gateway afterwards to apply):
71
+ v0.2.0 migrated to the new plugin SDK (`openclaw/plugin-sdk/*`). If you are running OpenClaw < 2026.3.22, pin to 0.1.x:
73
72
 
74
73
  ```bash
75
- openclaw plugins update openclaw-seatalk
74
+ openclaw plugins install openclaw-seatalk@0.1.6
76
75
  ```
77
76
 
78
77
  ### From source (development)
@@ -86,6 +85,25 @@ npm install
86
85
  openclaw plugins install -l .
87
86
  ```
88
87
 
88
+ ## Upgrading
89
+
90
+ ```bash
91
+ openclaw plugins update openclaw-seatalk
92
+ openclaw gateway restart
93
+ ```
94
+
95
+ Upgrading OpenClaw across the 2026.3.22 SDK boundary (e.g. 2026.3.13 -> 2026.3.22):
96
+
97
+ ```bash
98
+ openclaw plugins disable openclaw-seatalk
99
+ openclaw update
100
+ openclaw plugins update openclaw-seatalk
101
+ openclaw plugins enable openclaw-seatalk
102
+ openclaw gateway restart
103
+ ```
104
+
105
+ The plugin must be disabled before upgrading because the old plugin (0.1.x) imports SDK exports removed in OpenClaw >= 2026.3.22. Disabling prevents it from loading during the upgrade.
106
+
89
107
  ## Gateway Modes
90
108
 
91
109
  The plugin supports two gateway modes for receiving SeaTalk events:
@@ -132,7 +150,7 @@ Or edit the OpenClaw config file directly (`~/.openclaw/openclaw.json`).
132
150
  webhookPort: 3210,
133
151
  webhookPath: "/callback",
134
152
  dmPolicy: "open", // or "allowlist"
135
- // allowFrom: ["e_12345678", "alice@company.com"],
153
+ // allowFrom: ["12345678", "alice@company.com"],
136
154
  },
137
155
  },
138
156
  }
package/index.ts CHANGED
@@ -1,29 +1,22 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
3
2
  import { seatalkPlugin } from "./src/channel.js";
4
3
  import { setSeatalkRuntime } from "./src/runtime.js";
5
4
  import { registerSeaTalkTool } from "./src/tool.js";
6
5
 
7
- export { monitorSeaTalkProvider } from "./src/monitor.js";
8
6
  export {
9
- sendSeaTalkMessage,
10
7
  sendTextMessage,
11
8
  sendImageMessage,
12
9
  sendFileMessage,
13
10
  } from "./src/send.js";
14
11
  export { probeSeaTalk } from "./src/probe.js";
12
+ export { monitorSeaTalkProvider } from "./src/monitor.js";
15
13
  export { seatalkPlugin } from "./src/channel.js";
16
14
 
17
- const plugin = {
15
+ export default defineChannelPluginEntry({
18
16
  id: "openclaw-seatalk",
19
17
  name: "SeaTalk",
20
18
  description: "SeaTalk channel plugin",
21
- configSchema: emptyPluginConfigSchema(),
22
- register(api: OpenClawPluginApi) {
23
- setSeatalkRuntime(api.runtime);
24
- api.registerChannel({ plugin: seatalkPlugin });
25
- registerSeaTalkTool(api);
26
- },
27
- };
28
-
29
- export default plugin;
19
+ plugin: seatalkPlugin,
20
+ setRuntime: setSeatalkRuntime,
21
+ registerFull: (api) => registerSeaTalkTool(api),
22
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-seatalk",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "OpenClaw SeaTalk channel plugin",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,7 +14,7 @@
14
14
  "engines": {
15
15
  "node": ">=22.16.0"
16
16
  },
17
- "files": ["index.ts", "src/", "openclaw.plugin.json"],
17
+ "files": ["index.ts", "setup-entry.ts", "src/", "openclaw.plugin.json"],
18
18
  "scripts": {
19
19
  "format": "biome format --write .",
20
20
  "format:check": "biome format .",
@@ -33,6 +33,14 @@
33
33
  "@types/ws": "^8.5.0",
34
34
  "openclaw": "latest"
35
35
  },
36
+ "peerDependencies": {
37
+ "openclaw": ">=2026.3.22"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "openclaw": {
41
+ "optional": true
42
+ }
43
+ },
36
44
  "pnpm": {
37
45
  "onlyBuiltDependencies": ["@biomejs/biome"]
38
46
  },
@@ -49,6 +57,9 @@
49
57
  "install": {
50
58
  "npmSpec": "openclaw-seatalk",
51
59
  "defaultChoice": "npm"
60
+ },
61
+ "compat": {
62
+ "pluginApi": ">=2026.3.22"
52
63
  }
53
64
  }
54
65
  }
package/setup-entry.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { seatalkPlugin } from "./src/channel.js";
3
+
4
+ export default defineSetupPluginEntry(seatalkPlugin);
package/src/accounts.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/core";
3
3
  import type { ResolvedSeaTalkAccount, SeaTalkAccountConfig, SeaTalkConfig } from "./types.js";
4
4
 
5
- function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
5
+ function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
6
6
  const accounts = (cfg.channels?.seatalk as SeaTalkConfig)?.accounts;
7
7
  if (!accounts || typeof accounts !== "object") {
8
8
  return [];
@@ -10,7 +10,7 @@ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
10
10
  return Object.keys(accounts).filter(Boolean);
11
11
  }
12
12
 
13
- export function listSeaTalkAccountIds(cfg: ClawdbotConfig): string[] {
13
+ export function listSeaTalkAccountIds(cfg: OpenClawConfig): string[] {
14
14
  const ids = listConfiguredAccountIds(cfg);
15
15
  if (ids.length === 0) {
16
16
  return [DEFAULT_ACCOUNT_ID];
@@ -18,7 +18,7 @@ export function listSeaTalkAccountIds(cfg: ClawdbotConfig): string[] {
18
18
  return [...ids].toSorted((a, b) => a.localeCompare(b));
19
19
  }
20
20
 
21
- export function resolveDefaultSeaTalkAccountId(cfg: ClawdbotConfig): string {
21
+ export function resolveDefaultSeaTalkAccountId(cfg: OpenClawConfig): string {
22
22
  const ids = listSeaTalkAccountIds(cfg);
23
23
  if (ids.includes(DEFAULT_ACCOUNT_ID)) {
24
24
  return DEFAULT_ACCOUNT_ID;
@@ -27,7 +27,7 @@ export function resolveDefaultSeaTalkAccountId(cfg: ClawdbotConfig): string {
27
27
  }
28
28
 
29
29
  function resolveAccountConfig(
30
- cfg: ClawdbotConfig,
30
+ cfg: OpenClawConfig,
31
31
  accountId: string,
32
32
  ): SeaTalkAccountConfig | undefined {
33
33
  const accounts = (cfg.channels?.seatalk as SeaTalkConfig)?.accounts;
@@ -37,7 +37,7 @@ function resolveAccountConfig(
37
37
  return accounts[accountId];
38
38
  }
39
39
 
40
- function mergeSeaTalkAccountConfig(cfg: ClawdbotConfig, accountId: string): SeaTalkConfig {
40
+ function mergeSeaTalkAccountConfig(cfg: OpenClawConfig, accountId: string): SeaTalkConfig {
41
41
  const seatalkCfg = cfg.channels?.seatalk as SeaTalkConfig | undefined;
42
42
  const { accounts: _ignored, ...base } = seatalkCfg ?? {};
43
43
  const account = resolveAccountConfig(cfg, accountId) ?? {};
@@ -59,7 +59,7 @@ export function resolveSeaTalkCredentials(cfg?: SeaTalkConfig): {
59
59
  }
60
60
 
61
61
  export function resolveSeaTalkAccount(params: {
62
- cfg: ClawdbotConfig;
62
+ cfg: OpenClawConfig;
63
63
  accountId?: string | null;
64
64
  }): ResolvedSeaTalkAccount {
65
65
  const accountId = normalizeAccountId(params.accountId);
@@ -86,7 +86,7 @@ export function resolveSeaTalkAccount(params: {
86
86
  };
87
87
  }
88
88
 
89
- export function listEnabledSeaTalkAccounts(cfg: ClawdbotConfig): ResolvedSeaTalkAccount[] {
89
+ export function listEnabledSeaTalkAccounts(cfg: OpenClawConfig): ResolvedSeaTalkAccount[] {
90
90
  return listSeaTalkAccountIds(cfg)
91
91
  .map((accountId) => resolveSeaTalkAccount({ cfg, accountId }))
92
92
  .filter((account) => account.enabled && account.configured);
package/src/bot.ts CHANGED
@@ -1,9 +1,13 @@
1
- import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import {
3
+ resolveSendableOutboundReplyParts,
4
+ sendMediaWithLeadingCaption,
5
+ } from "openclaw/plugin-sdk/reply-payload";
2
6
  import { resolveSeaTalkAccount } from "./accounts.js";
3
7
  import type { SeaTalkClient } from "./client.js";
4
8
  import { buildSeaTalkMediaPayload, resolveInboundMedia } from "./media.js";
5
9
  import { getSeatalkRuntime } from "./runtime.js";
6
- import { sendGroupTextMessage, sendTextMessage } from "./send.js";
10
+ import { sendGroupTextMessage, sendMediaToTarget, sendTextMessage } from "./send.js";
7
11
  import type {
8
12
  SeaTalkCallbackRequest,
9
13
  SeaTalkGroupMessageEvent,
@@ -13,7 +17,7 @@ import type {
13
17
  } from "./types.js";
14
18
 
15
19
  export function dispatchSeaTalkEvent(params: {
16
- cfg: ClawdbotConfig;
20
+ cfg: OpenClawConfig;
17
21
  event: SeaTalkCallbackRequest;
18
22
  client: SeaTalkClient;
19
23
  runtime?: RuntimeEnv;
@@ -83,11 +87,22 @@ function tryRecordEvent(eventId: string): boolean {
83
87
  const DEBOUNCE_SLIDE_MS = 1500;
84
88
  const DEBOUNCE_HARD_CAP_MS = 5000;
85
89
 
86
- type BufferEntry = {
90
+ type DmBufferEntry = {
91
+ kind: "dm";
87
92
  event: SeaTalkCallbackRequest;
88
93
  parsedEvent: SeaTalkMessageEvent;
89
94
  };
90
95
 
96
+ type GroupBufferEntry = {
97
+ kind: "group";
98
+ event: SeaTalkCallbackRequest;
99
+ groupEvent: SeaTalkGroupMessageEvent;
100
+ groupId: string;
101
+ eventType: string;
102
+ };
103
+
104
+ type BufferEntry = DmBufferEntry | GroupBufferEntry;
105
+
91
106
  type DebounceState = {
92
107
  entries: BufferEntry[];
93
108
  timer: ReturnType<typeof setTimeout>;
@@ -96,7 +111,7 @@ type DebounceState = {
96
111
  };
97
112
 
98
113
  type DebounceContext = {
99
- cfg: ClawdbotConfig;
114
+ cfg: OpenClawConfig;
100
115
  client: SeaTalkClient;
101
116
  runtime?: RuntimeEnv;
102
117
  accountId: string;
@@ -110,6 +125,17 @@ function dmDebounceKey(accountId: string, employeeCode: string, threadId?: strin
110
125
  : `${accountId}:dm:${employeeCode}`;
111
126
  }
112
127
 
128
+ function groupDebounceKey(
129
+ accountId: string,
130
+ groupId: string,
131
+ employeeCode: string,
132
+ threadId?: string,
133
+ ): string {
134
+ return threadId
135
+ ? `${accountId}:grp:${groupId}:${employeeCode}:t:${threadId}`
136
+ : `${accountId}:grp:${groupId}:${employeeCode}`;
137
+ }
138
+
113
139
  function scheduleFlush(key: string, state: DebounceState): void {
114
140
  clearTimeout(state.timer);
115
141
 
@@ -133,10 +159,20 @@ function flushBuffer(key: string): void {
133
159
  const entries = state.entries;
134
160
  if (entries.length === 0) return;
135
161
 
136
- processBufferedEvents(entries, state.context).catch((err) => {
137
- const error = state.context.runtime?.error ?? console.error;
138
- error(`seatalk[${state.context.accountId}]: flush error: ${String(err)}`);
139
- });
162
+ const first = entries[0];
163
+ if (first.kind === "dm") {
164
+ const dmEntries = entries as DmBufferEntry[];
165
+ processBufferedDmEvents(dmEntries, state.context).catch((err) => {
166
+ const error = state.context.runtime?.error ?? console.error;
167
+ error(`seatalk[${state.context.accountId}]: flush error: ${String(err)}`);
168
+ });
169
+ } else {
170
+ const groupEntries = entries as GroupBufferEntry[];
171
+ processBufferedGroupEvents(groupEntries, state.context).catch((err) => {
172
+ const error = state.context.runtime?.error ?? console.error;
173
+ error(`seatalk[${state.context.accountId}]: group flush error: ${String(err)}`);
174
+ });
175
+ }
140
176
  }
141
177
 
142
178
  function pushToBuffer(key: string, entry: BufferEntry, context: DebounceContext): void {
@@ -204,8 +240,29 @@ async function resolveQuotedMessage(params: {
204
240
  }
205
241
  }
206
242
 
207
- async function processBufferedEvents(
208
- entries: BufferEntry[],
243
+ async function deliverMediaReplies(params: {
244
+ mediaUrls: string[];
245
+ client: SeaTalkClient;
246
+ to: string;
247
+ threadId?: string;
248
+ isGroup: boolean;
249
+ log: (msg: string) => void;
250
+ }): Promise<void> {
251
+ const { mediaUrls, client, to, threadId, isGroup, log } = params;
252
+ await sendMediaWithLeadingCaption({
253
+ mediaUrls,
254
+ caption: "",
255
+ send: async ({ mediaUrl }) => {
256
+ await sendMediaToTarget({ client, to, mediaUrl, threadId, isGroup });
257
+ },
258
+ onError: async ({ error, mediaUrl }) => {
259
+ log(`seatalk: failed to send media ${mediaUrl}: ${String(error)}`);
260
+ },
261
+ });
262
+ }
263
+
264
+ async function processBufferedDmEvents(
265
+ entries: DmBufferEntry[],
209
266
  context: DebounceContext,
210
267
  ): Promise<void> {
211
268
  const { cfg, client, runtime, accountId } = context;
@@ -267,12 +324,15 @@ async function processBufferedEvents(
267
324
  if (media) mediaList.push(media);
268
325
  }
269
326
 
270
- const quotedMessageId = first.message.quoted_message_id;
271
- let quotedText: string | null = null;
272
- if (quotedMessageId) {
273
- const quoted = await resolveQuotedMessage({ client, quotedMessageId, log });
327
+ const seenQuotedIds = new Set<string>();
328
+ const quotedTexts: string[] = [];
329
+ for (const { parsedEvent } of entries) {
330
+ const qid = parsedEvent.message.quoted_message_id;
331
+ if (!qid || seenQuotedIds.has(qid)) continue;
332
+ seenQuotedIds.add(qid);
333
+ const quoted = await resolveQuotedMessage({ client, quotedMessageId: qid, log });
274
334
  if (quoted) {
275
- quotedText = quoted.text;
335
+ quotedTexts.push(quoted.text);
276
336
  mediaList.push(...quoted.media);
277
337
  }
278
338
  }
@@ -280,8 +340,9 @@ async function processBufferedEvents(
280
340
  const mediaPayload = buildSeaTalkMediaPayload(mediaList);
281
341
 
282
342
  let messageText = textParts.join("\n");
283
- if (quotedText) {
284
- messageText = messageText ? `${quotedText}\n${messageText}` : quotedText;
343
+ if (quotedTexts.length > 0) {
344
+ const quotedBlock = quotedTexts.join("\n");
345
+ messageText = messageText ? `${quotedBlock}\n${messageText}` : quotedBlock;
285
346
  }
286
347
  if (!messageText && mediaList.length > 0) {
287
348
  messageText = mediaList.map((m) => m.placeholder).join(" ");
@@ -331,7 +392,8 @@ async function processBufferedEvents(
331
392
 
332
393
  const metadata: Record<string, string> = {};
333
394
  if (threadId) metadata.threadId = threadId;
334
- if (quotedMessageId) metadata.quotedMessageId = quotedMessageId;
395
+ const firstQuotedId = first.message.quoted_message_id;
396
+ if (firstQuotedId) metadata.quotedMessageId = firstQuotedId;
335
397
 
336
398
  const ctxPayload = core.channel.reply.finalizeInboundContext({
337
399
  Body: body,
@@ -368,12 +430,25 @@ async function processBufferedEvents(
368
430
  const typingResult = core.channel.reply.createReplyDispatcherWithTyping({
369
431
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
370
432
  deliver: async (payload) => {
371
- const text = payload.text?.trim();
372
- if (text) {
433
+ const reply = resolveSendableOutboundReplyParts(payload);
434
+ if (!reply.hasText && !reply.hasMedia) return;
435
+
436
+ if (reply.hasText) {
373
437
  log(
374
438
  `seatalk[${accountId}]: inline deliver DM to ${employeeCode} threadId=${threadId || "none"}`,
375
439
  );
376
- await sendTextMessage(client, employeeCode, text, 1, threadId);
440
+ await sendTextMessage(client, employeeCode, reply.trimmedText, 1, threadId);
441
+ }
442
+
443
+ if (reply.hasMedia) {
444
+ await deliverMediaReplies({
445
+ mediaUrls: reply.mediaUrls,
446
+ client,
447
+ to: employeeCode,
448
+ threadId,
449
+ isGroup: false,
450
+ log,
451
+ });
377
452
  }
378
453
  },
379
454
  onError: (err) => {
@@ -408,7 +483,7 @@ async function processBufferedEvents(
408
483
  }
409
484
 
410
485
  export async function handleSeaTalkMessage(params: {
411
- cfg: ClawdbotConfig;
486
+ cfg: OpenClawConfig;
412
487
  event: SeaTalkCallbackRequest;
413
488
  client: SeaTalkClient;
414
489
  runtime?: RuntimeEnv;
@@ -417,7 +492,7 @@ export async function handleSeaTalkMessage(params: {
417
492
  const { cfg, event, client, runtime, accountId } = params;
418
493
  const log = runtime?.log ?? console.log;
419
494
 
420
- if (!tryRecordEvent(event.event_id)) {
495
+ if (!tryRecordEvent(`${accountId}:${event.event_id}`)) {
421
496
  log(`seatalk[${accountId}]: skipping duplicate event ${event.event_id}`);
422
497
  return;
423
498
  }
@@ -433,14 +508,11 @@ export async function handleSeaTalkMessage(params: {
433
508
  );
434
509
 
435
510
  const key = dmDebounceKey(accountId, msgEvent.employee_code, msgEvent.message.thread_id);
436
- pushToBuffer(key, { event, parsedEvent: msgEvent }, { cfg, client, runtime, accountId });
437
- }
438
-
439
- function extractGroupText(msg: SeaTalkMessage): string {
440
- if (msg.tag === "text") {
441
- return msg.text?.plain_text ?? msg.text?.content ?? "";
442
- }
443
- return "";
511
+ pushToBuffer(
512
+ key,
513
+ { kind: "dm", event, parsedEvent: msgEvent },
514
+ { cfg, client, runtime, accountId },
515
+ );
444
516
  }
445
517
 
446
518
  function checkGroupAccess(params: {
@@ -491,7 +563,7 @@ function checkGroupAccess(params: {
491
563
  }
492
564
 
493
565
  export async function handleSeaTalkGroupMessage(params: {
494
- cfg: ClawdbotConfig;
566
+ cfg: OpenClawConfig;
495
567
  event: SeaTalkCallbackRequest;
496
568
  client: SeaTalkClient;
497
569
  runtime?: RuntimeEnv;
@@ -499,9 +571,8 @@ export async function handleSeaTalkGroupMessage(params: {
499
571
  }): Promise<void> {
500
572
  const { cfg, event, client, runtime, accountId } = params;
501
573
  const log = runtime?.log ?? console.log;
502
- const error = runtime?.error ?? console.error;
503
574
 
504
- if (!tryRecordEvent(event.event_id)) {
575
+ if (!tryRecordEvent(`${accountId}:${event.event_id}`)) {
505
576
  log(`seatalk[${accountId}]: skipping duplicate group event ${event.event_id}`);
506
577
  return;
507
578
  }
@@ -546,16 +617,51 @@ export async function handleSeaTalkGroupMessage(params: {
546
617
  return;
547
618
  }
548
619
 
620
+ const key = groupDebounceKey(accountId, groupId, employeeCode, threadId);
621
+ pushToBuffer(
622
+ key,
623
+ { kind: "group", event, groupEvent, groupId, eventType: event.event_type },
624
+ { cfg, client, runtime, accountId },
625
+ );
626
+ }
627
+
628
+ async function processBufferedGroupEvents(
629
+ entries: GroupBufferEntry[],
630
+ context: DebounceContext,
631
+ ): Promise<void> {
632
+ const { cfg, client, runtime, accountId } = context;
633
+ const log = runtime?.log ?? console.log;
634
+ const error = runtime?.error ?? console.error;
635
+
636
+ const first = entries[0];
637
+ const groupId = first.groupId;
638
+ const msg = first.groupEvent.message;
639
+ const sender = msg.sender;
640
+ const employeeCode = sender.employee_code;
641
+ const senderEmail = sender.email;
642
+ const threadId = msg.thread_id;
643
+
644
+ const textParts: string[] = [];
549
645
  const mediaMessages: SeaTalkMessage[] = [];
550
- let messageText = "";
551
-
552
- if (msg.tag === "text") {
553
- messageText = extractGroupText(msg);
554
- } else if (msg.tag === "image" || msg.tag === "file" || msg.tag === "video") {
555
- mediaMessages.push(msg);
556
- } else if (msg.tag === "combined_forwarded_chat_history") {
557
- log(`seatalk[${accountId}]: skipping combined_forwarded_chat_history in group ${groupId}`);
558
- return;
646
+
647
+ for (const { groupEvent } of entries) {
648
+ const m = groupEvent.message;
649
+ switch (m.tag) {
650
+ case "text":
651
+ if (m.text?.plain_text || m.text?.content)
652
+ textParts.push(m.text.plain_text ?? m.text.content ?? "");
653
+ break;
654
+ case "image":
655
+ case "file":
656
+ case "video":
657
+ mediaMessages.push(m);
658
+ break;
659
+ case "combined_forwarded_chat_history":
660
+ log(
661
+ `seatalk[${accountId}]: skipping combined_forwarded_chat_history in group ${groupId}`,
662
+ );
663
+ break;
664
+ }
559
665
  }
560
666
 
561
667
  const mediaList: SeaTalkMediaInfo[] = [];
@@ -564,7 +670,7 @@ export async function handleSeaTalkGroupMessage(params: {
564
670
  if (media) mediaList.push(media);
565
671
  }
566
672
 
567
- const quotedMessageId = msg.quoted_message_id;
673
+ const quotedMessageId = first.groupEvent.message.quoted_message_id;
568
674
  let quotedText: string | null = null;
569
675
  if (quotedMessageId) {
570
676
  const quoted = await resolveQuotedMessage({ client, quotedMessageId, log });
@@ -576,6 +682,7 @@ export async function handleSeaTalkGroupMessage(params: {
576
682
 
577
683
  const mediaPayload = buildSeaTalkMediaPayload(mediaList);
578
684
 
685
+ let messageText = textParts.join("\n");
579
686
  if (quotedText) {
580
687
  messageText = messageText ? `${quotedText}\n${messageText}` : quotedText;
581
688
  }
@@ -591,6 +698,9 @@ export async function handleSeaTalkGroupMessage(params: {
591
698
 
592
699
  const senderName = employeeCode + (senderEmail ? ` (${senderEmail})` : "");
593
700
  const messageId = msg.message_id;
701
+ const wasMentioned = entries.some(
702
+ (e) => e.eventType === "new_mentioned_message_received_from_group_chat",
703
+ );
594
704
 
595
705
  try {
596
706
  const core = getSeatalkRuntime();
@@ -620,6 +730,9 @@ export async function handleSeaTalkGroupMessage(params: {
620
730
  body: `${senderName}: ${messageText}`,
621
731
  });
622
732
 
733
+ const account = resolveSeaTalkAccount({ cfg, accountId });
734
+ const seatalkCfg = account.config;
735
+
623
736
  const metadata: Record<string, string> = { groupId };
624
737
  if (threadId) metadata.threadId = threadId;
625
738
  if (quotedMessageId) metadata.quotedMessageId = quotedMessageId;
@@ -641,7 +754,7 @@ export async function handleSeaTalkGroupMessage(params: {
641
754
  MessageSid: messageId,
642
755
  MessageThreadId: threadId || undefined,
643
756
  Timestamp: Date.now(),
644
- WasMentioned: event.event_type === "new_mentioned_message_received_from_group_chat",
757
+ WasMentioned: wasMentioned,
645
758
  CommandAuthorized: true,
646
759
  OriginatingChannel: "seatalk" as const,
647
760
  OriginatingTo: `group:${groupId}`,
@@ -661,9 +774,28 @@ export async function handleSeaTalkGroupMessage(params: {
661
774
  const typingResult = core.channel.reply.createReplyDispatcherWithTyping({
662
775
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
663
776
  deliver: async (payload) => {
664
- const text = payload.text?.trim();
665
- if (text) {
666
- await sendGroupTextMessage(client, groupId, text, 1, replyThreadId);
777
+ const reply = resolveSendableOutboundReplyParts(payload);
778
+ if (!reply.hasText && !reply.hasMedia) return;
779
+
780
+ if (reply.hasText) {
781
+ await sendGroupTextMessage(
782
+ client,
783
+ groupId,
784
+ reply.trimmedText,
785
+ 1,
786
+ replyThreadId,
787
+ );
788
+ }
789
+
790
+ if (reply.hasMedia) {
791
+ await deliverMediaReplies({
792
+ mediaUrls: reply.mediaUrls,
793
+ client,
794
+ to: groupId,
795
+ threadId: replyThreadId,
796
+ isGroup: true,
797
+ log,
798
+ });
667
799
  }
668
800
  },
669
801
  onError: (err) => {
package/src/channel.ts CHANGED
@@ -1,18 +1,18 @@
1
- import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
1
+ import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
3
3
  import {
4
4
  listSeaTalkAccountIds,
5
5
  resolveDefaultSeaTalkAccountId,
6
6
  resolveSeaTalkAccount,
7
7
  } from "./accounts.js";
8
8
  import { resolveSeaTalkClient } from "./client.js";
9
- import { seatalkOnboardingAdapter } from "./onboarding.js";
10
9
  import { seatalkOutbound } from "./outbound.js";
11
10
  import { probeSeaTalk } from "./probe.js";
11
+ import { seatalkSetupWizard } from "./setup-surface.js";
12
12
  import { looksLikeEmail, looksLikeSeaTalkId, normalizeSeaTalkTarget } from "./targets.js";
13
13
  import type { ResolvedSeaTalkAccount, SeaTalkConfig } from "./types.js";
14
14
 
15
- const meta: ChannelMeta = {
15
+ const meta = {
16
16
  id: "seatalk",
17
17
  label: "SeaTalk",
18
18
  selectionLabel: "SeaTalk (plugin)",
@@ -135,7 +135,7 @@ export const seatalkPlugin: ChannelPlugin<ResolvedSeaTalkAccount> = {
135
135
  const isDefault = accountId === DEFAULT_ACCOUNT_ID;
136
136
 
137
137
  if (isDefault) {
138
- const next = { ...cfg } as ClawdbotConfig;
138
+ const next = { ...cfg } as OpenClawConfig;
139
139
  const nextChannels = { ...cfg.channels } as Record<string, unknown>;
140
140
  nextChannels.seatalk = undefined;
141
141
  const hasOtherChannels = Object.values(nextChannels).some((v) => v !== undefined);
@@ -224,7 +224,7 @@ export const seatalkPlugin: ChannelPlugin<ResolvedSeaTalkAccount> = {
224
224
  };
225
225
  },
226
226
  },
227
- onboarding: seatalkOnboardingAdapter,
227
+ setupWizard: seatalkSetupWizard,
228
228
  messaging: {
229
229
  normalizeTarget: (raw) => normalizeSeaTalkTarget(raw) ?? undefined,
230
230
  targetResolver: {
package/src/monitor.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as http from "node:http";
3
- import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
3
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
4
4
  import {
5
5
  listEnabledSeaTalkAccounts,
6
6
  resolveSeaTalkAccount,
@@ -11,7 +11,7 @@ import { resolveSeaTalkClient } from "./client.js";
11
11
  import type { ResolvedSeaTalkAccount, SeaTalkCallbackRequest } from "./types.js";
12
12
 
13
13
  export type MonitorSeaTalkOpts = {
14
- config?: ClawdbotConfig;
14
+ config?: OpenClawConfig;
15
15
  runtime?: RuntimeEnv;
16
16
  abortSignal?: AbortSignal;
17
17
  accountId?: string;
@@ -61,7 +61,7 @@ function readBody(req: http.IncomingMessage): Promise<Buffer> {
61
61
  }
62
62
 
63
63
  async function monitorSingleAccount(params: {
64
- cfg: ClawdbotConfig;
64
+ cfg: OpenClawConfig;
65
65
  account: ResolvedSeaTalkAccount;
66
66
  runtime?: RuntimeEnv;
67
67
  abortSignal?: AbortSignal;