openclaw-seatalk 0.1.0
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/LICENSE +201 -0
- package/README.md +260 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +54 -0
- package/src/accounts.ts +94 -0
- package/src/bot.ts +698 -0
- package/src/channel.ts +359 -0
- package/src/client.ts +329 -0
- package/src/config-schema.ts +71 -0
- package/src/media.ts +163 -0
- package/src/monitor.ts +205 -0
- package/src/onboarding.ts +507 -0
- package/src/outbound.ts +120 -0
- package/src/probe.ts +34 -0
- package/src/relay-client.ts +204 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +71 -0
- package/src/targets.ts +27 -0
- package/src/tool-schema.ts +60 -0
- package/src/tool.ts +180 -0
- package/src/types.ts +94 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { listEnabledSeaTalkAccounts, resolveSeaTalkAccount } from "./accounts.js";
|
|
4
|
+
import { dispatchSeaTalkEvent } from "./bot.js";
|
|
5
|
+
import { resolveSeaTalkClient } from "./client.js";
|
|
6
|
+
import type { MonitorSeaTalkOpts } from "./monitor.js";
|
|
7
|
+
import type { ResolvedSeaTalkAccount, SeaTalkCallbackRequest } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const INITIAL_BACKOFF_MS = 1_000;
|
|
10
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
11
|
+
const BACKOFF_MULTIPLIER = 2;
|
|
12
|
+
|
|
13
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const onAbort = () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
resolve();
|
|
18
|
+
};
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
signal?.removeEventListener("abort", onAbort);
|
|
21
|
+
resolve();
|
|
22
|
+
}, ms);
|
|
23
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function connectSingleAccount(params: {
|
|
28
|
+
cfg: ClawdbotConfig;
|
|
29
|
+
account: ResolvedSeaTalkAccount;
|
|
30
|
+
relayUrl: string;
|
|
31
|
+
runtime?: RuntimeEnv;
|
|
32
|
+
abortSignal?: AbortSignal;
|
|
33
|
+
}): Promise<void> {
|
|
34
|
+
const { cfg, account, relayUrl, runtime, abortSignal } = params;
|
|
35
|
+
const { accountId } = account;
|
|
36
|
+
const log = runtime?.log ?? console.log;
|
|
37
|
+
const error = runtime?.error ?? console.error;
|
|
38
|
+
|
|
39
|
+
if (!account.appId || !account.appSecret || !account.signingSecret) {
|
|
40
|
+
throw new Error(`SeaTalk account "${accountId}" missing credentials for relay mode`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const client = resolveSeaTalkClient(account);
|
|
44
|
+
if (!client) {
|
|
45
|
+
throw new Error(`SeaTalk client not available for account "${accountId}"`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
49
|
+
|
|
50
|
+
while (!abortSignal?.aborted) {
|
|
51
|
+
try {
|
|
52
|
+
await new Promise<void>((resolve, reject) => {
|
|
53
|
+
if (abortSignal?.aborted) {
|
|
54
|
+
resolve();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log(`seatalk[${accountId}]: connecting to relay ${relayUrl}...`);
|
|
59
|
+
const ws = new WebSocket(relayUrl);
|
|
60
|
+
|
|
61
|
+
const handleAbort = () => {
|
|
62
|
+
ws.close();
|
|
63
|
+
resolve();
|
|
64
|
+
};
|
|
65
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
66
|
+
|
|
67
|
+
ws.on("open", () => {
|
|
68
|
+
log(`seatalk[${accountId}]: relay connected, authenticating...`);
|
|
69
|
+
ws.send(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
type: "auth",
|
|
72
|
+
appId: account.appId,
|
|
73
|
+
appSecret: account.appSecret,
|
|
74
|
+
signingSecret: account.signingSecret,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let authenticated = false;
|
|
80
|
+
|
|
81
|
+
ws.on("message", (raw) => {
|
|
82
|
+
let msg: { type: string; event?: SeaTalkCallbackRequest; error?: string };
|
|
83
|
+
try {
|
|
84
|
+
msg = JSON.parse(String(raw));
|
|
85
|
+
} catch {
|
|
86
|
+
error(`seatalk[${accountId}]: relay sent invalid JSON`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!authenticated) {
|
|
91
|
+
if (msg.type === "auth_ok") {
|
|
92
|
+
authenticated = true;
|
|
93
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
94
|
+
log(`seatalk[${accountId}]: relay authenticated`);
|
|
95
|
+
} else if (msg.type === "auth_fail") {
|
|
96
|
+
error(`seatalk[${accountId}]: relay auth failed: ${msg.error}`);
|
|
97
|
+
ws.close();
|
|
98
|
+
reject(new Error(`Relay auth failed: ${msg.error}`));
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
switch (msg.type) {
|
|
104
|
+
case "event":
|
|
105
|
+
if (msg.event && client) {
|
|
106
|
+
dispatchSeaTalkEvent({
|
|
107
|
+
cfg,
|
|
108
|
+
event: msg.event,
|
|
109
|
+
client,
|
|
110
|
+
runtime,
|
|
111
|
+
accountId,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case "ping":
|
|
116
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
117
|
+
break;
|
|
118
|
+
case "replaced":
|
|
119
|
+
log(`seatalk[${accountId}]: connection replaced by another instance`);
|
|
120
|
+
ws.close();
|
|
121
|
+
resolve();
|
|
122
|
+
return;
|
|
123
|
+
default:
|
|
124
|
+
log(`seatalk[${accountId}]: unknown relay message type: ${msg.type}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
ws.on("close", (code, reason) => {
|
|
129
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
130
|
+
if (authenticated) {
|
|
131
|
+
log(
|
|
132
|
+
`seatalk[${accountId}]: relay disconnected (code=${code}, reason=${String(reason)})`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
resolve();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
ws.on("error", (err) => {
|
|
139
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
140
|
+
error(`seatalk[${accountId}]: relay connection error: ${String(err)}`);
|
|
141
|
+
resolve();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = String(err);
|
|
146
|
+
if (msg.includes("Relay auth failed")) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
error(`seatalk[${accountId}]: relay error: ${msg}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (abortSignal?.aborted) break;
|
|
153
|
+
|
|
154
|
+
log(`seatalk[${accountId}]: reconnecting in ${backoff}ms...`);
|
|
155
|
+
await sleep(backoff, abortSignal);
|
|
156
|
+
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function connectSeaTalkRelay(
|
|
161
|
+
opts: MonitorSeaTalkOpts & { relayUrl: string },
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const cfg = opts.config;
|
|
164
|
+
if (!cfg) {
|
|
165
|
+
throw new Error("Config is required for SeaTalk relay client");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const log = opts.runtime?.log ?? console.log;
|
|
169
|
+
|
|
170
|
+
if (opts.accountId) {
|
|
171
|
+
const account = resolveSeaTalkAccount({ cfg, accountId: opts.accountId });
|
|
172
|
+
if (!account.enabled || !account.configured) {
|
|
173
|
+
throw new Error(`SeaTalk account "${opts.accountId}" not configured or disabled`);
|
|
174
|
+
}
|
|
175
|
+
return connectSingleAccount({
|
|
176
|
+
cfg,
|
|
177
|
+
account,
|
|
178
|
+
relayUrl: opts.relayUrl,
|
|
179
|
+
runtime: opts.runtime,
|
|
180
|
+
abortSignal: opts.abortSignal,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const accounts = listEnabledSeaTalkAccounts(cfg);
|
|
185
|
+
if (accounts.length === 0) {
|
|
186
|
+
throw new Error("No enabled SeaTalk accounts configured");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
log(
|
|
190
|
+
`seatalk: connecting ${accounts.length} account(s) to relay: ${accounts.map((a) => a.accountId).join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await Promise.all(
|
|
194
|
+
accounts.map((account) =>
|
|
195
|
+
connectSingleAccount({
|
|
196
|
+
cfg,
|
|
197
|
+
account,
|
|
198
|
+
relayUrl: opts.relayUrl,
|
|
199
|
+
runtime: opts.runtime,
|
|
200
|
+
abortSignal: opts.abortSignal,
|
|
201
|
+
}),
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setSeatalkRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSeatalkRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("SeaTalk runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
package/src/send.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { resolveSeaTalkAccount } from "./accounts.js";
|
|
3
|
+
import { type SeaTalkClient, resolveSeaTalkClient } from "./client.js";
|
|
4
|
+
|
|
5
|
+
export async function sendTextMessage(
|
|
6
|
+
client: SeaTalkClient,
|
|
7
|
+
employeeCode: string,
|
|
8
|
+
text: string,
|
|
9
|
+
format: 1 | 2 = 1,
|
|
10
|
+
threadId?: string,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
await client.sendSingleChat(
|
|
13
|
+
employeeCode,
|
|
14
|
+
{ tag: "text", text: { format, content: text } },
|
|
15
|
+
threadId,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function sendImageMessage(
|
|
20
|
+
client: SeaTalkClient,
|
|
21
|
+
employeeCode: string,
|
|
22
|
+
base64Data: string,
|
|
23
|
+
threadId?: string,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
await client.sendSingleChat(
|
|
26
|
+
employeeCode,
|
|
27
|
+
{ tag: "image", image: { content: base64Data } },
|
|
28
|
+
threadId,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function sendFileMessage(
|
|
33
|
+
client: SeaTalkClient,
|
|
34
|
+
employeeCode: string,
|
|
35
|
+
base64Data: string,
|
|
36
|
+
filename: string,
|
|
37
|
+
threadId?: string,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
await client.sendSingleChat(
|
|
40
|
+
employeeCode,
|
|
41
|
+
{ tag: "file", file: { content: base64Data, filename } },
|
|
42
|
+
threadId,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function sendGroupTextMessage(
|
|
47
|
+
client: SeaTalkClient,
|
|
48
|
+
groupId: string,
|
|
49
|
+
text: string,
|
|
50
|
+
format: 1 | 2 = 1,
|
|
51
|
+
threadId?: string,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await client.sendGroupChat(groupId, { tag: "text", text: { format, content: text } }, threadId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function sendSeaTalkMessage(params: {
|
|
57
|
+
cfg: ClawdbotConfig;
|
|
58
|
+
to: string;
|
|
59
|
+
text: string;
|
|
60
|
+
accountId?: string;
|
|
61
|
+
}): Promise<{ messageId?: string; chatId?: string }> {
|
|
62
|
+
const { cfg, to, text, accountId } = params;
|
|
63
|
+
const account = resolveSeaTalkAccount({ cfg, accountId });
|
|
64
|
+
const client = resolveSeaTalkClient(account);
|
|
65
|
+
if (!client) {
|
|
66
|
+
throw new Error(`SeaTalk client not available for account ${account.accountId}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await sendTextMessage(client, to, text, 1);
|
|
70
|
+
return { chatId: to };
|
|
71
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const GROUP_PREFIX = "group:";
|
|
2
|
+
|
|
3
|
+
export function normalizeSeaTalkTarget(raw: string): string | null {
|
|
4
|
+
const trimmed = raw.trim();
|
|
5
|
+
if (!trimmed) return null;
|
|
6
|
+
return trimmed;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isGroupTarget(to: string): boolean {
|
|
10
|
+
return to.startsWith(GROUP_PREFIX);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseGroupTarget(to: string): string {
|
|
14
|
+
return to.slice(GROUP_PREFIX.length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function looksLikeEmail(raw: string): boolean {
|
|
18
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw.trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function looksLikeSeaTalkId(raw: string): boolean {
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (!trimmed) return false;
|
|
24
|
+
if (looksLikeEmail(trimmed)) return true;
|
|
25
|
+
if (isGroupTarget(trimmed)) return parseGroupTarget(trimmed).length > 0;
|
|
26
|
+
return /^[a-zA-Z0-9_-]+$/.test(trimmed);
|
|
27
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
const CURSOR_DESC =
|
|
4
|
+
"Pagination cursor. Omit for the first request to get the latest messages. To fetch older messages, pass the next_cursor value from the previous response.";
|
|
5
|
+
|
|
6
|
+
export const SeaTalkToolSchema = Type.Union([
|
|
7
|
+
Type.Object({
|
|
8
|
+
action: Type.Literal("group_history", {
|
|
9
|
+
description:
|
|
10
|
+
"Get group chat message history (requires 'Get Chat History' app permission). Messages are returned in chronological order (oldest to newest). The first page (no cursor) contains the most recent messages. Use next_cursor from the response to fetch older pages.",
|
|
11
|
+
}),
|
|
12
|
+
group_id: Type.String({ description: "Group chat ID" }),
|
|
13
|
+
page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })),
|
|
14
|
+
cursor: Type.Optional(Type.String({ description: CURSOR_DESC })),
|
|
15
|
+
}),
|
|
16
|
+
Type.Object({
|
|
17
|
+
action: Type.Literal("group_info", { description: "Get group chat details" }),
|
|
18
|
+
group_id: Type.String({ description: "Group chat ID" }),
|
|
19
|
+
}),
|
|
20
|
+
Type.Object({
|
|
21
|
+
action: Type.Literal("group_list", {
|
|
22
|
+
description: "List groups the bot has joined",
|
|
23
|
+
}),
|
|
24
|
+
page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })),
|
|
25
|
+
cursor: Type.Optional(
|
|
26
|
+
Type.String({
|
|
27
|
+
description:
|
|
28
|
+
"Pagination cursor. Omit for the first request. To fetch more groups, pass the next_cursor value from the previous response.",
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
Type.Object({
|
|
33
|
+
action: Type.Literal("thread_history", {
|
|
34
|
+
description:
|
|
35
|
+
"Get thread messages in chronological order (oldest to newest). The first page (no cursor) contains the most recent replies. Use next_cursor to fetch older replies.",
|
|
36
|
+
}),
|
|
37
|
+
thread_id: Type.String({ description: "Thread ID" }),
|
|
38
|
+
group_id: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description: "Group chat ID (provide for group thread, omit for DM thread)",
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
employee_code: Type.Optional(
|
|
44
|
+
Type.String({
|
|
45
|
+
description: "Employee code (required for DM thread when group_id is omitted)",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })),
|
|
49
|
+
cursor: Type.Optional(Type.String({ description: CURSOR_DESC })),
|
|
50
|
+
}),
|
|
51
|
+
Type.Object({
|
|
52
|
+
action: Type.Literal("get_message", {
|
|
53
|
+
description:
|
|
54
|
+
"Get a message by its ID. Can resolve any message_id or quoted_message_id.",
|
|
55
|
+
}),
|
|
56
|
+
message_id: Type.String({ description: "The message ID to retrieve" }),
|
|
57
|
+
}),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export type SeaTalkToolParams = Static<typeof SeaTalkToolSchema>;
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { listEnabledSeaTalkAccounts } from "./accounts.js";
|
|
3
|
+
import type { SeaTalkClient } from "./client.js";
|
|
4
|
+
import { resolveSeaTalkClient } from "./client.js";
|
|
5
|
+
import { type SeaTalkToolParams, SeaTalkToolSchema } from "./tool-schema.js";
|
|
6
|
+
import type { SeaTalkToolsConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
function json(data: unknown) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
11
|
+
details: data,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ResolvedToolsConfig = Required<SeaTalkToolsConfig>;
|
|
16
|
+
|
|
17
|
+
function resolveToolsConfig(cfg?: SeaTalkToolsConfig): ResolvedToolsConfig {
|
|
18
|
+
return {
|
|
19
|
+
groupInfo: cfg?.groupInfo ?? true,
|
|
20
|
+
groupHistory: cfg?.groupHistory ?? true,
|
|
21
|
+
groupList: cfg?.groupList ?? true,
|
|
22
|
+
threadHistory: cfg?.threadHistory ?? true,
|
|
23
|
+
getMessage: cfg?.getMessage ?? true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function resolveQuotedMessages(
|
|
28
|
+
client: SeaTalkClient,
|
|
29
|
+
messages: Record<string, unknown>[],
|
|
30
|
+
log?: (msg: string) => void,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
for (const msg of messages) {
|
|
33
|
+
if (!msg || typeof msg !== "object") continue;
|
|
34
|
+
const qid = msg.quoted_message_id;
|
|
35
|
+
if (!qid || typeof qid !== "string") continue;
|
|
36
|
+
try {
|
|
37
|
+
const quoted = await client.getMessageByMessageId(qid);
|
|
38
|
+
msg.quoted_message = quoted;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log?.(`seatalk tool: failed to resolve quoted message ${qid}: ${String(err)}`);
|
|
41
|
+
msg.quoted_message = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reverseMessageArray(
|
|
47
|
+
data: Record<string, unknown>,
|
|
48
|
+
key: string,
|
|
49
|
+
): Record<string, unknown>[] {
|
|
50
|
+
const arr = data[key];
|
|
51
|
+
if (Array.isArray(arr)) {
|
|
52
|
+
const reversed = (arr as Record<string, unknown>[]).toReversed();
|
|
53
|
+
data[key] = reversed;
|
|
54
|
+
return reversed;
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function registerSeaTalkTool(api: OpenClawPluginApi) {
|
|
60
|
+
if (!api.config) {
|
|
61
|
+
api.logger.debug?.("seatalk tool: No config available, skipping");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const accounts = listEnabledSeaTalkAccounts(api.config);
|
|
66
|
+
if (accounts.length === 0) {
|
|
67
|
+
api.logger.debug?.("seatalk tool: No enabled SeaTalk accounts, skipping");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const firstAccount = accounts[0];
|
|
72
|
+
const toolsCfg = resolveToolsConfig(firstAccount.tools);
|
|
73
|
+
|
|
74
|
+
const anyEnabled =
|
|
75
|
+
toolsCfg.groupInfo ||
|
|
76
|
+
toolsCfg.groupHistory ||
|
|
77
|
+
toolsCfg.groupList ||
|
|
78
|
+
toolsCfg.threadHistory ||
|
|
79
|
+
toolsCfg.getMessage;
|
|
80
|
+
if (!anyEnabled) {
|
|
81
|
+
api.logger.debug?.("seatalk tool: All actions disabled, skipping");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const getClient = () => resolveSeaTalkClient(firstAccount);
|
|
86
|
+
const log = (msg: string) => api.logger.warn?.(msg);
|
|
87
|
+
|
|
88
|
+
api.registerTool(
|
|
89
|
+
{
|
|
90
|
+
name: "seatalk",
|
|
91
|
+
label: "SeaTalk",
|
|
92
|
+
description:
|
|
93
|
+
"SeaTalk operations. Actions: group_history (group chat messages, chronological order), group_info (group details), group_list (joined groups), thread_history (thread messages, chronological order), get_message (retrieve a single message by ID). History and thread results include resolved quoted_message for messages that quote another message.",
|
|
94
|
+
parameters: SeaTalkToolSchema,
|
|
95
|
+
async execute(_toolCallId, params) {
|
|
96
|
+
const p = params as SeaTalkToolParams;
|
|
97
|
+
try {
|
|
98
|
+
const client = getClient();
|
|
99
|
+
if (!client) {
|
|
100
|
+
return json({ error: "SeaTalk client not available" });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
switch (p.action) {
|
|
104
|
+
case "group_history": {
|
|
105
|
+
if (!toolsCfg.groupHistory) {
|
|
106
|
+
return json({ error: "groupHistory is disabled in config" });
|
|
107
|
+
}
|
|
108
|
+
const data = await client.getGroupChatHistory(p.group_id, {
|
|
109
|
+
pageSize: p.page_size,
|
|
110
|
+
cursor: p.cursor,
|
|
111
|
+
});
|
|
112
|
+
const msgs = reverseMessageArray(data, "group_chat_messages");
|
|
113
|
+
await resolveQuotedMessages(client, msgs, log);
|
|
114
|
+
return json(data);
|
|
115
|
+
}
|
|
116
|
+
case "group_info": {
|
|
117
|
+
if (!toolsCfg.groupInfo) {
|
|
118
|
+
return json({ error: "groupInfo is disabled in config" });
|
|
119
|
+
}
|
|
120
|
+
return json(await client.getGroupChatInfo(p.group_id));
|
|
121
|
+
}
|
|
122
|
+
case "group_list": {
|
|
123
|
+
if (!toolsCfg.groupList) {
|
|
124
|
+
return json({ error: "groupList is disabled in config" });
|
|
125
|
+
}
|
|
126
|
+
return json(
|
|
127
|
+
await client.getJoinedGroupChats({
|
|
128
|
+
pageSize: p.page_size,
|
|
129
|
+
cursor: p.cursor,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
case "thread_history": {
|
|
134
|
+
if (!toolsCfg.threadHistory) {
|
|
135
|
+
return json({ error: "threadHistory is disabled in config" });
|
|
136
|
+
}
|
|
137
|
+
let data: Record<string, unknown>;
|
|
138
|
+
if (p.group_id) {
|
|
139
|
+
data = await client.getGroupThread(p.group_id, p.thread_id, {
|
|
140
|
+
pageSize: p.page_size,
|
|
141
|
+
cursor: p.cursor,
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
if (!p.employee_code) {
|
|
145
|
+
return json({
|
|
146
|
+
error: "employee_code is required for DM thread (when group_id is absent)",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
data = await client.getDmThread(p.employee_code, p.thread_id, {
|
|
150
|
+
pageSize: p.page_size,
|
|
151
|
+
cursor: p.cursor,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const msgs = reverseMessageArray(data, "thread_messages");
|
|
155
|
+
await resolveQuotedMessages(client, msgs, log);
|
|
156
|
+
return json(data);
|
|
157
|
+
}
|
|
158
|
+
case "get_message": {
|
|
159
|
+
if (!toolsCfg.getMessage) {
|
|
160
|
+
return json({ error: "getMessage is disabled in config" });
|
|
161
|
+
}
|
|
162
|
+
return json(await client.getMessageByMessageId(p.message_id));
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
return json({
|
|
166
|
+
error: `Unknown action: ${String((p as Record<string, unknown>).action)}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return json({
|
|
171
|
+
error: err instanceof Error ? err.message : String(err),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{ name: "seatalk" },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
api.logger.info?.("seatalk tool: Registered");
|
|
180
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeaTalkAccountConfigSchema,
|
|
3
|
+
SeaTalkConfigSchema,
|
|
4
|
+
SeaTalkToolsConfigSchema,
|
|
5
|
+
z,
|
|
6
|
+
} from "./config-schema.js";
|
|
7
|
+
|
|
8
|
+
export type SeaTalkConfig = z.infer<typeof SeaTalkConfigSchema>;
|
|
9
|
+
export type SeaTalkAccountConfig = z.infer<typeof SeaTalkAccountConfigSchema>;
|
|
10
|
+
export type SeaTalkToolsConfig = z.infer<typeof SeaTalkToolsConfigSchema>;
|
|
11
|
+
|
|
12
|
+
export type GatewayMode = "webhook" | "relay";
|
|
13
|
+
|
|
14
|
+
export type ResolvedSeaTalkAccount = {
|
|
15
|
+
accountId: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
configured: boolean;
|
|
18
|
+
appId?: string;
|
|
19
|
+
appSecret?: string;
|
|
20
|
+
signingSecret?: string;
|
|
21
|
+
mode: GatewayMode;
|
|
22
|
+
relayUrl?: string;
|
|
23
|
+
webhookPort: number;
|
|
24
|
+
webhookPath: string;
|
|
25
|
+
tools?: SeaTalkToolsConfig;
|
|
26
|
+
config: SeaTalkConfig;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SeaTalkCallbackRequest = {
|
|
30
|
+
event_id: string;
|
|
31
|
+
event_type: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
app_id: string;
|
|
34
|
+
event: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SeaTalkMessageEvent = {
|
|
38
|
+
seatalk_id: string;
|
|
39
|
+
employee_code: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
message: SeaTalkMessage;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SeaTalkGroupMessageEvent = {
|
|
45
|
+
group_id: string;
|
|
46
|
+
message: SeaTalkGroupMessage;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type SeaTalkGroupMessage = SeaTalkMessage & {
|
|
50
|
+
sender: {
|
|
51
|
+
seatalk_id: string;
|
|
52
|
+
employee_code: string;
|
|
53
|
+
email?: string;
|
|
54
|
+
sender_type?: number;
|
|
55
|
+
};
|
|
56
|
+
message_sent_time?: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type SeaTalkMessage = {
|
|
60
|
+
message_id: string;
|
|
61
|
+
quoted_message_id?: string;
|
|
62
|
+
thread_id?: string;
|
|
63
|
+
tag: "text" | "image" | "file" | "video" | "combined_forwarded_chat_history";
|
|
64
|
+
text?: { content?: string; plain_text?: string };
|
|
65
|
+
image?: { content: string };
|
|
66
|
+
file?: { content: string; filename: string };
|
|
67
|
+
video?: { content: string };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SeaTalkMediaInfo = {
|
|
71
|
+
path: string;
|
|
72
|
+
contentType?: string;
|
|
73
|
+
filename?: string;
|
|
74
|
+
placeholder: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SeaTalkOutboundMedia = {
|
|
78
|
+
base64: string;
|
|
79
|
+
sendAs: "image" | "file";
|
|
80
|
+
filename?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type SeaTalkProbeResult = {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
error?: string;
|
|
86
|
+
appId?: string;
|
|
87
|
+
tokenExpire?: number;
|
|
88
|
+
latencyMs?: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type SeaTalkTokenInfo = {
|
|
92
|
+
token: string;
|
|
93
|
+
expireAt: number;
|
|
94
|
+
};
|