@symerian/symi 3.0.20 → 3.0.21
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/dist/{audio-preflight-BaCdNfrk.js → audio-preflight-D7BVT-ls.js} +4 -4
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-UfmVM0xR.js → chrome-B5CO2vB5.js} +7 -7
- package/dist/{deliver-BqXdac6W.js → deliver-CrwjsDwv.js} +1 -1
- package/dist/extensionAPI.js +7 -7
- package/dist/{image-DIWsXYcW.js → image-Csu7WcLW.js} +1 -1
- package/dist/{manager-DW3SxcPr.js → manager-BkkVjTO8.js} +1 -1
- package/dist/{pi-embedded-BNch0U5F.js → pi-embedded-Dhp64z5l.js} +16 -16
- package/dist/{pi-embedded-helpers-IkHl02JF.js → pi-embedded-helpers-840E4hop.js} +4 -4
- package/dist/{pw-ai-nMkA-oDJ.js → pw-ai-CBgJf_RR.js} +1 -1
- package/dist/{runner-DNEC58JI.js → runner-BbFKo1ne.js} +1 -1
- package/dist/{synthesis-BWAr0sZ9.js → synthesis-DoEM0E8_.js} +7 -7
- package/dist/{web-7a-m_UxL.js → web-BYXJn-Ps.js} +7 -7
- package/package.json +1 -1
- package/extensions/imessage/index.ts +0 -17
- package/extensions/imessage/node_modules/.bin/symi +0 -21
- package/extensions/imessage/package.json +0 -15
- package/extensions/imessage/src/channel.outbound.test.ts +0 -66
- package/extensions/imessage/src/channel.ts +0 -298
- package/extensions/imessage/src/runtime.ts +0 -14
- package/extensions/imessage/symi.plugin.json +0 -9
- package/extensions/line/index.ts +0 -19
- package/extensions/line/node_modules/.bin/symi +0 -21
- package/extensions/line/package.json +0 -30
- package/extensions/line/src/card-command.ts +0 -344
- package/extensions/line/src/channel.logout.test.ts +0 -133
- package/extensions/line/src/channel.sendPayload.test.ts +0 -312
- package/extensions/line/src/channel.startup.test.ts +0 -133
- package/extensions/line/src/channel.ts +0 -801
- package/extensions/line/src/runtime.ts +0 -14
- package/extensions/line/symi.plugin.json +0 -9
- package/extensions/signal/index.ts +0 -17
- package/extensions/signal/node_modules/.bin/symi +0 -21
- package/extensions/signal/package.json +0 -15
- package/extensions/signal/src/channel.ts +0 -302
- package/extensions/signal/src/runtime.ts +0 -14
- package/extensions/signal/symi.plugin.json +0 -9
- package/extensions/telegram/index.ts +0 -17
- package/extensions/telegram/node_modules/.bin/symi +0 -21
- package/extensions/telegram/package.json +0 -15
- package/extensions/telegram/src/channel.test.ts +0 -125
- package/extensions/telegram/src/channel.ts +0 -560
- package/extensions/telegram/src/runtime.ts +0 -14
- package/extensions/telegram/symi.plugin.json +0 -9
- package/extensions/whatsapp/index.ts +0 -17
- package/extensions/whatsapp/node_modules/.bin/symi +0 -21
- package/extensions/whatsapp/package.json +0 -15
- package/extensions/whatsapp/src/channel.ts +0 -465
- package/extensions/whatsapp/src/resolve-target.test.ts +0 -170
- package/extensions/whatsapp/src/runtime.ts +0 -14
- package/extensions/whatsapp/symi.plugin.json +0 -9
|
@@ -1,801 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildChannelConfigSchema,
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
LineConfigSchema,
|
|
5
|
-
processLineMessage,
|
|
6
|
-
type ChannelPlugin,
|
|
7
|
-
type ChannelStatusIssue,
|
|
8
|
-
type SymiConfig,
|
|
9
|
-
type LineConfig,
|
|
10
|
-
type LineChannelData,
|
|
11
|
-
type ResolvedLineAccount,
|
|
12
|
-
} from "symi/plugin-sdk";
|
|
13
|
-
import { getLineRuntime } from "./runtime.js";
|
|
14
|
-
|
|
15
|
-
// LINE channel metadata
|
|
16
|
-
const meta = {
|
|
17
|
-
id: "line",
|
|
18
|
-
label: "LINE",
|
|
19
|
-
selectionLabel: "LINE (Messaging API)",
|
|
20
|
-
detailLabel: "LINE Bot",
|
|
21
|
-
docsPath: "/channels/line",
|
|
22
|
-
docsLabel: "line",
|
|
23
|
-
blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.",
|
|
24
|
-
systemImage: "message.fill",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
|
28
|
-
id: "line",
|
|
29
|
-
meta: {
|
|
30
|
-
...meta,
|
|
31
|
-
quickstartAllowFrom: true,
|
|
32
|
-
},
|
|
33
|
-
pairing: {
|
|
34
|
-
idLabel: "lineUserId",
|
|
35
|
-
normalizeAllowEntry: (entry) => {
|
|
36
|
-
// LINE IDs are case-sensitive; only strip prefix variants (line: / line:user:).
|
|
37
|
-
return entry.replace(/^line:(?:user:)?/i, "");
|
|
38
|
-
},
|
|
39
|
-
notifyApproval: async ({ cfg, id }) => {
|
|
40
|
-
const line = getLineRuntime().channel.line;
|
|
41
|
-
const account = line.resolveLineAccount({ cfg });
|
|
42
|
-
if (!account.channelAccessToken) {
|
|
43
|
-
throw new Error("LINE channel access token not configured");
|
|
44
|
-
}
|
|
45
|
-
await line.pushMessageLine(id, "Symi: your access has been approved.", {
|
|
46
|
-
channelAccessToken: account.channelAccessToken,
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
capabilities: {
|
|
51
|
-
chatTypes: ["direct", "group"],
|
|
52
|
-
reactions: false,
|
|
53
|
-
threads: false,
|
|
54
|
-
media: true,
|
|
55
|
-
nativeCommands: false,
|
|
56
|
-
blockStreaming: true,
|
|
57
|
-
},
|
|
58
|
-
reload: { configPrefixes: ["channels.line"] },
|
|
59
|
-
configSchema: buildChannelConfigSchema(LineConfigSchema),
|
|
60
|
-
config: {
|
|
61
|
-
listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg),
|
|
62
|
-
resolveAccount: (cfg, accountId) =>
|
|
63
|
-
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
|
|
64
|
-
defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg),
|
|
65
|
-
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
66
|
-
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
67
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
68
|
-
return {
|
|
69
|
-
...cfg,
|
|
70
|
-
channels: {
|
|
71
|
-
...cfg.channels,
|
|
72
|
-
line: {
|
|
73
|
-
...lineConfig,
|
|
74
|
-
enabled,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
...cfg,
|
|
81
|
-
channels: {
|
|
82
|
-
...cfg.channels,
|
|
83
|
-
line: {
|
|
84
|
-
...lineConfig,
|
|
85
|
-
accounts: {
|
|
86
|
-
...lineConfig.accounts,
|
|
87
|
-
[accountId]: {
|
|
88
|
-
...lineConfig.accounts?.[accountId],
|
|
89
|
-
enabled,
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
deleteAccount: ({ cfg, accountId }) => {
|
|
97
|
-
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
98
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
99
|
-
// oxlint-disable-next-line no-unused-vars
|
|
100
|
-
const { channelSecret, tokenFile, secretFile, ...rest } = lineConfig;
|
|
101
|
-
return {
|
|
102
|
-
...cfg,
|
|
103
|
-
channels: {
|
|
104
|
-
...cfg.channels,
|
|
105
|
-
line: rest,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
const accounts = { ...lineConfig.accounts };
|
|
110
|
-
delete accounts[accountId];
|
|
111
|
-
return {
|
|
112
|
-
...cfg,
|
|
113
|
-
channels: {
|
|
114
|
-
...cfg.channels,
|
|
115
|
-
line: {
|
|
116
|
-
...lineConfig,
|
|
117
|
-
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
},
|
|
122
|
-
isConfigured: (account) =>
|
|
123
|
-
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
|
|
124
|
-
describeAccount: (account) => ({
|
|
125
|
-
accountId: account.accountId,
|
|
126
|
-
name: account.name,
|
|
127
|
-
enabled: account.enabled,
|
|
128
|
-
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
|
|
129
|
-
tokenSource: account.tokenSource ?? undefined,
|
|
130
|
-
}),
|
|
131
|
-
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
132
|
-
(
|
|
133
|
-
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined })
|
|
134
|
-
.config.allowFrom ?? []
|
|
135
|
-
).map((entry) => String(entry)),
|
|
136
|
-
formatAllowFrom: ({ allowFrom }) =>
|
|
137
|
-
allowFrom
|
|
138
|
-
.map((entry) => String(entry).trim())
|
|
139
|
-
.filter(Boolean)
|
|
140
|
-
.map((entry) => {
|
|
141
|
-
// LINE sender IDs are case-sensitive; keep original casing.
|
|
142
|
-
return entry.replace(/^line:(?:user:)?/i, "");
|
|
143
|
-
}),
|
|
144
|
-
},
|
|
145
|
-
security: {
|
|
146
|
-
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
147
|
-
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
148
|
-
const useAccountPath = Boolean(
|
|
149
|
-
(cfg.channels?.line as LineConfig | undefined)?.accounts?.[resolvedAccountId],
|
|
150
|
-
);
|
|
151
|
-
const basePath = useAccountPath
|
|
152
|
-
? `channels.line.accounts.${resolvedAccountId}.`
|
|
153
|
-
: "channels.line.";
|
|
154
|
-
return {
|
|
155
|
-
policy: account.config.dmPolicy ?? "pairing",
|
|
156
|
-
allowFrom: account.config.allowFrom ?? [],
|
|
157
|
-
policyPath: `${basePath}dmPolicy`,
|
|
158
|
-
allowFromPath: basePath,
|
|
159
|
-
approveHint: "symi pairing approve line <code>",
|
|
160
|
-
normalizeEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""),
|
|
161
|
-
};
|
|
162
|
-
},
|
|
163
|
-
collectWarnings: ({ account, cfg }) => {
|
|
164
|
-
const defaultGroupPolicy = (cfg.channels?.defaults as { groupPolicy?: string } | undefined)
|
|
165
|
-
?.groupPolicy;
|
|
166
|
-
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
167
|
-
if (groupPolicy !== "open") {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
return [
|
|
171
|
-
`- LINE groups: groupPolicy="open" allows any member in groups to trigger. Set channels.line.groupPolicy="allowlist" + channels.line.groupAllowFrom to restrict senders.`,
|
|
172
|
-
];
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
groups: {
|
|
176
|
-
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
177
|
-
const account = getLineRuntime().channel.line.resolveLineAccount({
|
|
178
|
-
cfg,
|
|
179
|
-
accountId: accountId ?? undefined,
|
|
180
|
-
});
|
|
181
|
-
const groups = account.config.groups;
|
|
182
|
-
if (!groups || !groupId) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
const groupConfig = groups[groupId] ?? groups["*"];
|
|
186
|
-
return groupConfig?.requireMention ?? false;
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
messaging: {
|
|
190
|
-
normalizeTarget: (target) => {
|
|
191
|
-
const trimmed = target.trim();
|
|
192
|
-
if (!trimmed) {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
|
|
196
|
-
},
|
|
197
|
-
targetResolver: {
|
|
198
|
-
looksLikeId: (id) => {
|
|
199
|
-
const trimmed = id?.trim();
|
|
200
|
-
if (!trimmed) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
// LINE user IDs are typically U followed by 32 hex characters
|
|
204
|
-
// Group IDs are C followed by 32 hex characters
|
|
205
|
-
// Room IDs are R followed by 32 hex characters
|
|
206
|
-
return /^[UCR][a-f0-9]{32}$/i.test(trimmed) || /^line:/i.test(trimmed);
|
|
207
|
-
},
|
|
208
|
-
hint: "<userId|groupId|roomId>",
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
directory: {
|
|
212
|
-
self: async () => null,
|
|
213
|
-
listPeers: async () => [],
|
|
214
|
-
listGroups: async () => [],
|
|
215
|
-
},
|
|
216
|
-
setup: {
|
|
217
|
-
resolveAccountId: ({ accountId }) =>
|
|
218
|
-
getLineRuntime().channel.line.normalizeAccountId(accountId),
|
|
219
|
-
applyAccountName: ({ cfg, accountId, name }) => {
|
|
220
|
-
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
221
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
222
|
-
return {
|
|
223
|
-
...cfg,
|
|
224
|
-
channels: {
|
|
225
|
-
...cfg.channels,
|
|
226
|
-
line: {
|
|
227
|
-
...lineConfig,
|
|
228
|
-
name,
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
...cfg,
|
|
235
|
-
channels: {
|
|
236
|
-
...cfg.channels,
|
|
237
|
-
line: {
|
|
238
|
-
...lineConfig,
|
|
239
|
-
accounts: {
|
|
240
|
-
...lineConfig.accounts,
|
|
241
|
-
[accountId]: {
|
|
242
|
-
...lineConfig.accounts?.[accountId],
|
|
243
|
-
name,
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
};
|
|
249
|
-
},
|
|
250
|
-
validateInput: ({ accountId, input }) => {
|
|
251
|
-
const typedInput = input as {
|
|
252
|
-
useEnv?: boolean;
|
|
253
|
-
channelAccessToken?: string;
|
|
254
|
-
channelSecret?: string;
|
|
255
|
-
tokenFile?: string;
|
|
256
|
-
secretFile?: string;
|
|
257
|
-
};
|
|
258
|
-
if (typedInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
259
|
-
return "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.";
|
|
260
|
-
}
|
|
261
|
-
if (!typedInput.useEnv && !typedInput.channelAccessToken && !typedInput.tokenFile) {
|
|
262
|
-
return "LINE requires channelAccessToken or --token-file (or --use-env).";
|
|
263
|
-
}
|
|
264
|
-
if (!typedInput.useEnv && !typedInput.channelSecret && !typedInput.secretFile) {
|
|
265
|
-
return "LINE requires channelSecret or --secret-file (or --use-env).";
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
},
|
|
269
|
-
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
270
|
-
const typedInput = input as {
|
|
271
|
-
name?: string;
|
|
272
|
-
useEnv?: boolean;
|
|
273
|
-
channelAccessToken?: string;
|
|
274
|
-
channelSecret?: string;
|
|
275
|
-
tokenFile?: string;
|
|
276
|
-
secretFile?: string;
|
|
277
|
-
};
|
|
278
|
-
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
279
|
-
|
|
280
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
281
|
-
return {
|
|
282
|
-
...cfg,
|
|
283
|
-
channels: {
|
|
284
|
-
...cfg.channels,
|
|
285
|
-
line: {
|
|
286
|
-
...lineConfig,
|
|
287
|
-
enabled: true,
|
|
288
|
-
...(typedInput.name ? { name: typedInput.name } : {}),
|
|
289
|
-
...(typedInput.useEnv
|
|
290
|
-
? {}
|
|
291
|
-
: typedInput.tokenFile
|
|
292
|
-
? { tokenFile: typedInput.tokenFile }
|
|
293
|
-
: typedInput.channelAccessToken
|
|
294
|
-
? { channelAccessToken: typedInput.channelAccessToken }
|
|
295
|
-
: {}),
|
|
296
|
-
...(typedInput.useEnv
|
|
297
|
-
? {}
|
|
298
|
-
: typedInput.secretFile
|
|
299
|
-
? { secretFile: typedInput.secretFile }
|
|
300
|
-
: typedInput.channelSecret
|
|
301
|
-
? { channelSecret: typedInput.channelSecret }
|
|
302
|
-
: {}),
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
...cfg,
|
|
310
|
-
channels: {
|
|
311
|
-
...cfg.channels,
|
|
312
|
-
line: {
|
|
313
|
-
...lineConfig,
|
|
314
|
-
enabled: true,
|
|
315
|
-
accounts: {
|
|
316
|
-
...lineConfig.accounts,
|
|
317
|
-
[accountId]: {
|
|
318
|
-
...lineConfig.accounts?.[accountId],
|
|
319
|
-
enabled: true,
|
|
320
|
-
...(typedInput.name ? { name: typedInput.name } : {}),
|
|
321
|
-
...(typedInput.tokenFile
|
|
322
|
-
? { tokenFile: typedInput.tokenFile }
|
|
323
|
-
: typedInput.channelAccessToken
|
|
324
|
-
? { channelAccessToken: typedInput.channelAccessToken }
|
|
325
|
-
: {}),
|
|
326
|
-
...(typedInput.secretFile
|
|
327
|
-
? { secretFile: typedInput.secretFile }
|
|
328
|
-
: typedInput.channelSecret
|
|
329
|
-
? { channelSecret: typedInput.channelSecret }
|
|
330
|
-
: {}),
|
|
331
|
-
},
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
outbound: {
|
|
339
|
-
deliveryMode: "direct",
|
|
340
|
-
chunker: (text, limit) => getLineRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
341
|
-
textChunkLimit: 5000, // LINE allows up to 5000 characters per text message
|
|
342
|
-
sendPayload: async ({ to, payload, accountId, cfg }) => {
|
|
343
|
-
const runtime = getLineRuntime();
|
|
344
|
-
const lineData = (payload.channelData?.line as LineChannelData | undefined) ?? {};
|
|
345
|
-
const sendText = runtime.channel.line.pushMessageLine;
|
|
346
|
-
const sendBatch = runtime.channel.line.pushMessagesLine;
|
|
347
|
-
const sendFlex = runtime.channel.line.pushFlexMessage;
|
|
348
|
-
const sendTemplate = runtime.channel.line.pushTemplateMessage;
|
|
349
|
-
const sendLocation = runtime.channel.line.pushLocationMessage;
|
|
350
|
-
const sendQuickReplies = runtime.channel.line.pushTextMessageWithQuickReplies;
|
|
351
|
-
const buildTemplate = runtime.channel.line.buildTemplateMessageFromPayload;
|
|
352
|
-
const createQuickReplyItems = runtime.channel.line.createQuickReplyItems;
|
|
353
|
-
|
|
354
|
-
let lastResult: { messageId: string; chatId: string } | null = null;
|
|
355
|
-
const quickReplies = lineData.quickReplies ?? [];
|
|
356
|
-
const hasQuickReplies = quickReplies.length > 0;
|
|
357
|
-
const quickReply = hasQuickReplies ? createQuickReplyItems(quickReplies) : undefined;
|
|
358
|
-
|
|
359
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
360
|
-
const sendMessageBatch = async (messages: Array<Record<string, unknown>>) => {
|
|
361
|
-
if (messages.length === 0) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
for (let i = 0; i < messages.length; i += 5) {
|
|
365
|
-
// LINE SDK expects Message[] but we build dynamically
|
|
366
|
-
const batch = messages.slice(i, i + 5) as unknown as Parameters<typeof sendBatch>[1];
|
|
367
|
-
const result = await sendBatch(to, batch, {
|
|
368
|
-
verbose: false,
|
|
369
|
-
accountId: accountId ?? undefined,
|
|
370
|
-
});
|
|
371
|
-
lastResult = { messageId: result.messageId, chatId: result.chatId };
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const processed = payload.text
|
|
376
|
-
? processLineMessage(payload.text)
|
|
377
|
-
: { text: "", flexMessages: [] };
|
|
378
|
-
|
|
379
|
-
const chunkLimit =
|
|
380
|
-
runtime.channel.text.resolveTextChunkLimit?.(cfg, "line", accountId ?? undefined, {
|
|
381
|
-
fallbackLimit: 5000,
|
|
382
|
-
}) ?? 5000;
|
|
383
|
-
|
|
384
|
-
const chunks = processed.text
|
|
385
|
-
? runtime.channel.text.chunkMarkdownText(processed.text, chunkLimit)
|
|
386
|
-
: [];
|
|
387
|
-
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
388
|
-
const shouldSendQuickRepliesInline = chunks.length === 0 && hasQuickReplies;
|
|
389
|
-
|
|
390
|
-
if (!shouldSendQuickRepliesInline) {
|
|
391
|
-
if (lineData.flexMessage) {
|
|
392
|
-
// LINE SDK expects FlexContainer but we receive contents as unknown
|
|
393
|
-
const flexContents = lineData.flexMessage.contents as Parameters<typeof sendFlex>[2];
|
|
394
|
-
lastResult = await sendFlex(to, lineData.flexMessage.altText, flexContents, {
|
|
395
|
-
verbose: false,
|
|
396
|
-
accountId: accountId ?? undefined,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (lineData.templateMessage) {
|
|
401
|
-
const template = buildTemplate(lineData.templateMessage);
|
|
402
|
-
if (template) {
|
|
403
|
-
lastResult = await sendTemplate(to, template, {
|
|
404
|
-
verbose: false,
|
|
405
|
-
accountId: accountId ?? undefined,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (lineData.location) {
|
|
411
|
-
lastResult = await sendLocation(to, lineData.location, {
|
|
412
|
-
verbose: false,
|
|
413
|
-
accountId: accountId ?? undefined,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
for (const flexMsg of processed.flexMessages) {
|
|
418
|
-
// LINE SDK expects FlexContainer but we receive contents as unknown
|
|
419
|
-
const flexContents = flexMsg.contents as Parameters<typeof sendFlex>[2];
|
|
420
|
-
lastResult = await sendFlex(to, flexMsg.altText, flexContents, {
|
|
421
|
-
verbose: false,
|
|
422
|
-
accountId: accountId ?? undefined,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const sendMediaAfterText = !(hasQuickReplies && chunks.length > 0);
|
|
428
|
-
if (mediaUrls.length > 0 && !shouldSendQuickRepliesInline && !sendMediaAfterText) {
|
|
429
|
-
for (const url of mediaUrls) {
|
|
430
|
-
lastResult = await runtime.channel.line.sendMessageLine(to, "", {
|
|
431
|
-
verbose: false,
|
|
432
|
-
mediaUrl: url,
|
|
433
|
-
accountId: accountId ?? undefined,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (chunks.length > 0) {
|
|
439
|
-
for (let i = 0; i < chunks.length; i += 1) {
|
|
440
|
-
const isLast = i === chunks.length - 1;
|
|
441
|
-
if (isLast && hasQuickReplies) {
|
|
442
|
-
lastResult = await sendQuickReplies(to, chunks[i], quickReplies, {
|
|
443
|
-
verbose: false,
|
|
444
|
-
accountId: accountId ?? undefined,
|
|
445
|
-
});
|
|
446
|
-
} else {
|
|
447
|
-
lastResult = await sendText(to, chunks[i], {
|
|
448
|
-
verbose: false,
|
|
449
|
-
accountId: accountId ?? undefined,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
} else if (shouldSendQuickRepliesInline) {
|
|
454
|
-
const quickReplyMessages: Array<Record<string, unknown>> = [];
|
|
455
|
-
if (lineData.flexMessage) {
|
|
456
|
-
quickReplyMessages.push({
|
|
457
|
-
type: "flex",
|
|
458
|
-
altText: lineData.flexMessage.altText.slice(0, 400),
|
|
459
|
-
contents: lineData.flexMessage.contents,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
if (lineData.templateMessage) {
|
|
463
|
-
const template = buildTemplate(lineData.templateMessage);
|
|
464
|
-
if (template) {
|
|
465
|
-
quickReplyMessages.push(template);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (lineData.location) {
|
|
469
|
-
quickReplyMessages.push({
|
|
470
|
-
type: "location",
|
|
471
|
-
title: lineData.location.title.slice(0, 100),
|
|
472
|
-
address: lineData.location.address.slice(0, 100),
|
|
473
|
-
latitude: lineData.location.latitude,
|
|
474
|
-
longitude: lineData.location.longitude,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
for (const flexMsg of processed.flexMessages) {
|
|
478
|
-
quickReplyMessages.push({
|
|
479
|
-
type: "flex",
|
|
480
|
-
altText: flexMsg.altText.slice(0, 400),
|
|
481
|
-
contents: flexMsg.contents,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
for (const url of mediaUrls) {
|
|
485
|
-
const trimmed = url?.trim();
|
|
486
|
-
if (!trimmed) {
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
quickReplyMessages.push({
|
|
490
|
-
type: "image",
|
|
491
|
-
originalContentUrl: trimmed,
|
|
492
|
-
previewImageUrl: trimmed,
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
if (quickReplyMessages.length > 0 && quickReply) {
|
|
496
|
-
const lastIndex = quickReplyMessages.length - 1;
|
|
497
|
-
quickReplyMessages[lastIndex] = {
|
|
498
|
-
...quickReplyMessages[lastIndex],
|
|
499
|
-
quickReply,
|
|
500
|
-
};
|
|
501
|
-
await sendMessageBatch(quickReplyMessages);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (mediaUrls.length > 0 && !shouldSendQuickRepliesInline && sendMediaAfterText) {
|
|
506
|
-
for (const url of mediaUrls) {
|
|
507
|
-
lastResult = await runtime.channel.line.sendMessageLine(to, "", {
|
|
508
|
-
verbose: false,
|
|
509
|
-
mediaUrl: url,
|
|
510
|
-
accountId: accountId ?? undefined,
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (lastResult) {
|
|
516
|
-
return { channel: "line", ...lastResult };
|
|
517
|
-
}
|
|
518
|
-
return { channel: "line", messageId: "empty", chatId: to };
|
|
519
|
-
},
|
|
520
|
-
sendText: async ({ to, text, accountId }) => {
|
|
521
|
-
const runtime = getLineRuntime();
|
|
522
|
-
const sendText = runtime.channel.line.pushMessageLine;
|
|
523
|
-
const sendFlex = runtime.channel.line.pushFlexMessage;
|
|
524
|
-
|
|
525
|
-
// Process markdown: extract tables/code blocks, strip formatting
|
|
526
|
-
const processed = processLineMessage(text);
|
|
527
|
-
|
|
528
|
-
// Send cleaned text first (if non-empty)
|
|
529
|
-
let result: { messageId: string; chatId: string };
|
|
530
|
-
if (processed.text.trim()) {
|
|
531
|
-
result = await sendText(to, processed.text, {
|
|
532
|
-
verbose: false,
|
|
533
|
-
accountId: accountId ?? undefined,
|
|
534
|
-
});
|
|
535
|
-
} else {
|
|
536
|
-
// If text is empty after processing, still need a result
|
|
537
|
-
result = { messageId: "processed", chatId: to };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Send flex messages for tables/code blocks
|
|
541
|
-
for (const flexMsg of processed.flexMessages) {
|
|
542
|
-
// LINE SDK expects FlexContainer but we receive contents as unknown
|
|
543
|
-
const flexContents = flexMsg.contents as Parameters<typeof sendFlex>[2];
|
|
544
|
-
await sendFlex(to, flexMsg.altText, flexContents, {
|
|
545
|
-
verbose: false,
|
|
546
|
-
accountId: accountId ?? undefined,
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return { channel: "line", ...result };
|
|
551
|
-
},
|
|
552
|
-
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
553
|
-
const send = getLineRuntime().channel.line.sendMessageLine;
|
|
554
|
-
const result = await send(to, text, {
|
|
555
|
-
verbose: false,
|
|
556
|
-
mediaUrl,
|
|
557
|
-
accountId: accountId ?? undefined,
|
|
558
|
-
});
|
|
559
|
-
return { channel: "line", ...result };
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
status: {
|
|
563
|
-
defaultRuntime: {
|
|
564
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
565
|
-
running: false,
|
|
566
|
-
lastStartAt: null,
|
|
567
|
-
lastStopAt: null,
|
|
568
|
-
lastError: null,
|
|
569
|
-
},
|
|
570
|
-
collectStatusIssues: (accounts) => {
|
|
571
|
-
const issues: ChannelStatusIssue[] = [];
|
|
572
|
-
for (const account of accounts) {
|
|
573
|
-
const accountId = account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
574
|
-
if (!account.channelAccessToken?.trim()) {
|
|
575
|
-
issues.push({
|
|
576
|
-
channel: "line",
|
|
577
|
-
accountId,
|
|
578
|
-
kind: "config",
|
|
579
|
-
message: "LINE channel access token not configured",
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
if (!account.channelSecret?.trim()) {
|
|
583
|
-
issues.push({
|
|
584
|
-
channel: "line",
|
|
585
|
-
accountId,
|
|
586
|
-
kind: "config",
|
|
587
|
-
message: "LINE channel secret not configured",
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return issues;
|
|
592
|
-
},
|
|
593
|
-
buildChannelSummary: ({ snapshot }) => ({
|
|
594
|
-
configured: snapshot.configured ?? false,
|
|
595
|
-
tokenSource: snapshot.tokenSource ?? "none",
|
|
596
|
-
running: snapshot.running ?? false,
|
|
597
|
-
mode: snapshot.mode ?? null,
|
|
598
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
599
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
600
|
-
lastError: snapshot.lastError ?? null,
|
|
601
|
-
probe: snapshot.probe,
|
|
602
|
-
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
603
|
-
}),
|
|
604
|
-
probeAccount: async ({ account, timeoutMs }) =>
|
|
605
|
-
getLineRuntime().channel.line.probeLineBot(account.channelAccessToken, timeoutMs),
|
|
606
|
-
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
607
|
-
const configured = Boolean(
|
|
608
|
-
account.channelAccessToken?.trim() && account.channelSecret?.trim(),
|
|
609
|
-
);
|
|
610
|
-
return {
|
|
611
|
-
accountId: account.accountId,
|
|
612
|
-
name: account.name,
|
|
613
|
-
enabled: account.enabled,
|
|
614
|
-
configured,
|
|
615
|
-
tokenSource: account.tokenSource,
|
|
616
|
-
running: runtime?.running ?? false,
|
|
617
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
618
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
619
|
-
lastError: runtime?.lastError ?? null,
|
|
620
|
-
mode: "webhook",
|
|
621
|
-
probe,
|
|
622
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
623
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
624
|
-
};
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
gateway: {
|
|
628
|
-
startAccount: async (ctx) => {
|
|
629
|
-
const account = ctx.account;
|
|
630
|
-
const token = account.channelAccessToken.trim();
|
|
631
|
-
const secret = account.channelSecret.trim();
|
|
632
|
-
if (!token) {
|
|
633
|
-
throw new Error(
|
|
634
|
-
`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`,
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
if (!secret) {
|
|
638
|
-
throw new Error(
|
|
639
|
-
`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`,
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
let lineBotLabel = "";
|
|
644
|
-
try {
|
|
645
|
-
const probe = await getLineRuntime().channel.line.probeLineBot(token, 2500);
|
|
646
|
-
const displayName = probe.ok ? probe.bot?.displayName?.trim() : null;
|
|
647
|
-
if (displayName) {
|
|
648
|
-
lineBotLabel = ` (${displayName})`;
|
|
649
|
-
}
|
|
650
|
-
} catch (err) {
|
|
651
|
-
if (getLineRuntime().logging.shouldLogVerbose()) {
|
|
652
|
-
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`);
|
|
657
|
-
|
|
658
|
-
return getLineRuntime().channel.line.monitorLineProvider({
|
|
659
|
-
channelAccessToken: token,
|
|
660
|
-
channelSecret: secret,
|
|
661
|
-
accountId: account.accountId,
|
|
662
|
-
config: ctx.cfg,
|
|
663
|
-
runtime: ctx.runtime,
|
|
664
|
-
abortSignal: ctx.abortSignal,
|
|
665
|
-
webhookPath: account.config.webhookPath,
|
|
666
|
-
});
|
|
667
|
-
},
|
|
668
|
-
logoutAccount: async ({ accountId, cfg }) => {
|
|
669
|
-
const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ?? "";
|
|
670
|
-
const nextCfg = { ...cfg } as SymiConfig;
|
|
671
|
-
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
672
|
-
const nextLine = { ...lineConfig };
|
|
673
|
-
let cleared = false;
|
|
674
|
-
let changed = false;
|
|
675
|
-
|
|
676
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
677
|
-
if (
|
|
678
|
-
nextLine.channelAccessToken ||
|
|
679
|
-
nextLine.channelSecret ||
|
|
680
|
-
nextLine.tokenFile ||
|
|
681
|
-
nextLine.secretFile
|
|
682
|
-
) {
|
|
683
|
-
delete nextLine.channelAccessToken;
|
|
684
|
-
delete nextLine.channelSecret;
|
|
685
|
-
delete nextLine.tokenFile;
|
|
686
|
-
delete nextLine.secretFile;
|
|
687
|
-
cleared = true;
|
|
688
|
-
changed = true;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const accounts = nextLine.accounts ? { ...nextLine.accounts } : undefined;
|
|
693
|
-
if (accounts && accountId in accounts) {
|
|
694
|
-
const entry = accounts[accountId];
|
|
695
|
-
if (entry && typeof entry === "object") {
|
|
696
|
-
const nextEntry = { ...entry } as Record<string, unknown>;
|
|
697
|
-
if (
|
|
698
|
-
"channelAccessToken" in nextEntry ||
|
|
699
|
-
"channelSecret" in nextEntry ||
|
|
700
|
-
"tokenFile" in nextEntry ||
|
|
701
|
-
"secretFile" in nextEntry
|
|
702
|
-
) {
|
|
703
|
-
cleared = true;
|
|
704
|
-
delete nextEntry.channelAccessToken;
|
|
705
|
-
delete nextEntry.channelSecret;
|
|
706
|
-
delete nextEntry.tokenFile;
|
|
707
|
-
delete nextEntry.secretFile;
|
|
708
|
-
changed = true;
|
|
709
|
-
}
|
|
710
|
-
if (Object.keys(nextEntry).length === 0) {
|
|
711
|
-
delete accounts[accountId];
|
|
712
|
-
changed = true;
|
|
713
|
-
} else {
|
|
714
|
-
accounts[accountId] = nextEntry as typeof entry;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (accounts) {
|
|
720
|
-
if (Object.keys(accounts).length === 0) {
|
|
721
|
-
delete nextLine.accounts;
|
|
722
|
-
changed = true;
|
|
723
|
-
} else {
|
|
724
|
-
nextLine.accounts = accounts;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (changed) {
|
|
729
|
-
if (Object.keys(nextLine).length > 0) {
|
|
730
|
-
nextCfg.channels = { ...nextCfg.channels, line: nextLine };
|
|
731
|
-
} else {
|
|
732
|
-
const nextChannels = { ...nextCfg.channels };
|
|
733
|
-
delete (nextChannels as Record<string, unknown>).line;
|
|
734
|
-
if (Object.keys(nextChannels).length > 0) {
|
|
735
|
-
nextCfg.channels = nextChannels;
|
|
736
|
-
} else {
|
|
737
|
-
delete nextCfg.channels;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
await getLineRuntime().config.writeConfigFile(nextCfg);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const resolved = getLineRuntime().channel.line.resolveLineAccount({
|
|
744
|
-
cfg: changed ? nextCfg : cfg,
|
|
745
|
-
accountId,
|
|
746
|
-
});
|
|
747
|
-
const loggedOut = resolved.tokenSource === "none";
|
|
748
|
-
|
|
749
|
-
return { cleared, envToken: Boolean(envToken), loggedOut };
|
|
750
|
-
},
|
|
751
|
-
},
|
|
752
|
-
agentPrompt: {
|
|
753
|
-
messageToolHints: () => [
|
|
754
|
-
"",
|
|
755
|
-
"### LINE Rich Messages",
|
|
756
|
-
"LINE supports rich visual messages. Use these directives in your reply when appropriate:",
|
|
757
|
-
"",
|
|
758
|
-
"**Quick Replies** (bottom button suggestions):",
|
|
759
|
-
" [[quick_replies: Option 1, Option 2, Option 3]]",
|
|
760
|
-
"",
|
|
761
|
-
"**Location** (map pin):",
|
|
762
|
-
" [[location: Place Name | Address | latitude | longitude]]",
|
|
763
|
-
"",
|
|
764
|
-
"**Confirm Dialog** (yes/no prompt):",
|
|
765
|
-
" [[confirm: Question text? | Yes Label | No Label]]",
|
|
766
|
-
"",
|
|
767
|
-
"**Button Menu** (title + text + buttons):",
|
|
768
|
-
" [[buttons: Title | Description | Btn1:action1, Btn2:https://url.com]]",
|
|
769
|
-
"",
|
|
770
|
-
"**Media Player Card** (music status):",
|
|
771
|
-
" [[media_player: Song Title | Artist Name | Source | https://albumart.url | playing]]",
|
|
772
|
-
" - Status: 'playing' or 'paused' (optional)",
|
|
773
|
-
"",
|
|
774
|
-
"**Event Card** (calendar events, meetings):",
|
|
775
|
-
" [[event: Event Title | Date | Time | Location | Description]]",
|
|
776
|
-
" - Time, Location, Description are optional",
|
|
777
|
-
"",
|
|
778
|
-
"**Agenda Card** (multiple events/schedule):",
|
|
779
|
-
" [[agenda: Schedule Title | Event1:9:00 AM, Event2:12:00 PM, Event3:3:00 PM]]",
|
|
780
|
-
"",
|
|
781
|
-
"**Device Control Card** (smart devices, TVs, etc.):",
|
|
782
|
-
" [[device: Device Name | Device Type | Status | Control1:data1, Control2:data2]]",
|
|
783
|
-
"",
|
|
784
|
-
"**Apple TV Remote** (full D-pad + transport):",
|
|
785
|
-
" [[appletv_remote: Apple TV | Playing]]",
|
|
786
|
-
"",
|
|
787
|
-
"**Auto-converted**: Markdown tables become Flex cards, code blocks become styled cards.",
|
|
788
|
-
"",
|
|
789
|
-
"When to use rich messages:",
|
|
790
|
-
"- Use [[quick_replies:...]] when offering 2-4 clear options",
|
|
791
|
-
"- Use [[confirm:...]] for yes/no decisions",
|
|
792
|
-
"- Use [[buttons:...]] for menus with actions/links",
|
|
793
|
-
"- Use [[location:...]] when sharing a place",
|
|
794
|
-
"- Use [[media_player:...]] when showing what's playing",
|
|
795
|
-
"- Use [[event:...]] for calendar event details",
|
|
796
|
-
"- Use [[agenda:...]] for a day's schedule or event list",
|
|
797
|
-
"- Use [[device:...]] for smart device status/controls",
|
|
798
|
-
"- Tables/code in your response auto-convert to visual cards",
|
|
799
|
-
],
|
|
800
|
-
},
|
|
801
|
-
};
|