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 +26 -8
- package/index.ts +7 -14
- package/package.json +13 -2
- package/setup-entry.ts +4 -0
- package/src/accounts.ts +9 -9
- package/src/bot.ts +181 -49
- package/src/channel.ts +6 -6
- package/src/monitor.ts +3 -3
- package/src/outbound.ts +19 -58
- package/src/probe.ts +1 -1
- package/src/relay-client.ts +2 -2
- package/src/send.ts +35 -16
- package/src/{onboarding.ts → setup-surface.ts} +96 -142
- package/src/tool.ts +90 -78
- package/src/types.ts +0 -1
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
|
-
|
|
66
|
+
| Plugin version | OpenClaw version |
|
|
67
|
+
|---------------|-----------------|
|
|
68
|
+
| 0.2.x | >= 2026.3.22 |
|
|
69
|
+
| 0.1.x | < 2026.3.22 |
|
|
67
70
|
|
|
68
|
-
|
|
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
|
|
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: ["
|
|
153
|
+
// allowFrom: ["12345678", "alice@company.com"],
|
|
136
154
|
},
|
|
137
155
|
},
|
|
138
156
|
}
|
package/index.ts
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
15
|
+
export default defineChannelPluginEntry({
|
|
18
16
|
id: "openclaw-seatalk",
|
|
19
17
|
name: "SeaTalk",
|
|
20
18
|
description: "SeaTalk channel plugin",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
package/src/accounts.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
208
|
-
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
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
|
-
|
|
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 (
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
372
|
-
if (
|
|
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,
|
|
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:
|
|
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(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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:
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
665
|
-
if (
|
|
666
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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?:
|
|
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:
|
|
64
|
+
cfg: OpenClawConfig;
|
|
65
65
|
account: ResolvedSeaTalkAccount;
|
|
66
66
|
runtime?: RuntimeEnv;
|
|
67
67
|
abortSignal?: AbortSignal;
|