@zhin.js/adapter-line 2.0.1 → 2.0.3

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +50 -19
  3. package/adapters/line.ts +26 -0
  4. package/agent/tools/get_group_members.ts +24 -0
  5. package/agent/tools/get_profile.ts +24 -0
  6. package/lib/endpoint.d.ts +45 -37
  7. package/lib/endpoint.js +114 -506
  8. package/lib/index.d.ts +4 -15
  9. package/lib/index.js +4 -83
  10. package/lib/line-agent-deps.d.ts +24 -0
  11. package/lib/line-agent-deps.js +33 -0
  12. package/lib/protocol.d.ts +151 -0
  13. package/lib/protocol.js +212 -0
  14. package/lib/webhook.d.ts +13 -0
  15. package/lib/webhook.js +50 -0
  16. package/package.json +48 -21
  17. package/plugin.ts +8 -0
  18. package/schema.json +22 -0
  19. package/src/endpoint.ts +148 -554
  20. package/src/index.ts +53 -100
  21. package/src/line-agent-deps.ts +47 -0
  22. package/src/protocol.ts +384 -0
  23. package/src/webhook.ts +79 -0
  24. package/lib/adapter.d.ts +0 -17
  25. package/lib/adapter.d.ts.map +0 -1
  26. package/lib/adapter.js +0 -22
  27. package/lib/adapter.js.map +0 -1
  28. package/lib/endpoint.d.ts.map +0 -1
  29. package/lib/endpoint.js.map +0 -1
  30. package/lib/index.d.ts.map +0 -1
  31. package/lib/index.js.map +0 -1
  32. package/lib/segment-mapper.d.ts +0 -2
  33. package/lib/segment-mapper.d.ts.map +0 -1
  34. package/lib/segment-mapper.js +0 -2
  35. package/lib/segment-mapper.js.map +0 -1
  36. package/lib/types.d.ts +0 -112
  37. package/lib/types.d.ts.map +0 -1
  38. package/lib/types.js +0 -5
  39. package/lib/types.js.map +0 -1
  40. package/plugin.yml +0 -3
  41. package/src/adapter.ts +0 -29
  42. package/src/segment-mapper.ts +0 -1
  43. package/src/types.ts +0 -130
  44. /package/{skills/line → agent}/PERMITS.md +0 -0
  45. /package/{skills/line/SKILL.md → agent/skills/line.md} +0 -0
package/lib/endpoint.js CHANGED
@@ -1,441 +1,138 @@
1
- /**
2
- * LINE Endpoint 实现
3
- *
4
- * 使用 Webhook 模式接收消息,通过 LINE Messaging API 发送消息。
5
- * HMAC-SHA256 签名验证确保请求来自 LINE 平台。
6
- */
7
- import { createHmac, timingSafeEqual } from "node:crypto";
8
- import { Message, segment, formatCompact, expandInteractiveSegmentsInContent, } from 'zhin.js';
9
- import { registerFetchRoute } from "@zhin.js/host-router/router";
10
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
11
- /** Type guard: narrows a LineEvent to a message event */
12
- function isMessageEvent(e) {
13
- return e.type === "message" && "message" in e && e.message != null;
14
- }
15
- /** Type guard: narrows a LineEvent to a postback event */
16
- function isPostbackEvent(e) {
17
- return e.type === "postback" && "postback" in e;
18
- }
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { registerLineAgentEndpoint } from './line-agent-deps.js';
3
+ import { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isValidLineRecipientId, resolveChannel, } from './protocol.js';
4
+ import { registerLineWebhookRoutes } from './webhook.js';
5
+ const logger = getLogger('line');
19
6
  export class LineEndpoint {
20
- adapter;
21
- router;
22
- $config;
23
- $connected = false;
24
- get pluginLogger() {
25
- return this.adapter.plugin.logger;
26
- }
27
- get $id() {
28
- return this.$config.name;
29
- }
30
- constructor(adapter, router, $config) {
31
- this.adapter = adapter;
32
- this.router = router;
33
- this.$config = $config;
34
- }
35
- async $connect() {
7
+ #options;
8
+ #fetch;
9
+ #routeReleases = [];
10
+ #replyTokenCache = new Map();
11
+ #open = false;
12
+ #started = false;
13
+ #unregisterAgent;
14
+ constructor(options) {
15
+ this.#options = options;
16
+ this.#fetch = options.fetch ?? globalThis.fetch;
17
+ }
18
+ /** Used by webhook handler. */
19
+ get isOpen() {
20
+ return this.#open;
21
+ }
22
+ get config() {
23
+ return this.#options.config;
24
+ }
25
+ getApiConfig() {
26
+ return {
27
+ accessToken: this.#options.config.channelAccessToken,
28
+ apiBaseUrl: this.#options.config.apiBaseUrl,
29
+ };
30
+ }
31
+ async start() {
32
+ if (this.#started)
33
+ return;
34
+ this.#started = true;
36
35
  try {
37
- const path = this.$config.webhookPath || "/line/webhook";
38
- const cleanPath = path.startsWith("/") ? path : `/${path}`;
39
- registerFetchRoute(this.router, "POST", cleanPath, async (ctx) => {
40
- await this.handleWebhook(ctx);
41
- });
42
- this.$connected = true;
43
- this.pluginLogger.info(formatCompact({ op: "webhook", path: cleanPath }));
36
+ this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.name, this);
37
+ this.#routeReleases.push(...registerLineWebhookRoutes(this.#options.http, this));
38
+ logger.debug(formatCompact({
39
+ endpoint: this.#options.config.name,
40
+ op: 'webhook',
41
+ path: this.#options.config.webhookPath,
42
+ }));
44
43
  }
45
44
  catch (error) {
46
- this.pluginLogger.error("Failed to connect LINE endpoint:", error);
47
- this.$connected = false;
45
+ await this.stop();
46
+ logger.error('Failed to connect LINE endpoint:', error);
48
47
  throw error;
49
48
  }
50
49
  }
51
- async $disconnect() {
52
- try {
53
- this.$connected = false;
54
- this.replyTokenCache.clear();
55
- this.pluginLogger.info(`LINE endpoint ${this.$config.name} disconnected`);
56
- }
57
- catch (error) {
58
- this.pluginLogger.error("Error disconnecting LINE endpoint:", error);
59
- // L02: Log and swallow instead of re-throwing
60
- }
61
- }
62
- // ── Webhook 处理 ────────────────────────────────────────────────────
63
- async handleWebhook(ctx) {
64
- try {
65
- // 1. 签名验证
66
- const signature = ctx.get("x-line-signature");
67
- if (!signature) {
68
- ctx.status = 403;
69
- ctx.body = { message: "Missing signature" };
70
- return;
71
- }
72
- // L05: 获取原始请求体用于签名验证
73
- // Koa ctx.req 是 Node.js IncomingMessage,koa-body 可能已经消费了流
74
- // 因此优先使用已解析的 body 并序列化,记录警告说明可能不精确
75
- let rawBody;
76
- if (typeof ctx.request.body === "string") {
77
- rawBody = ctx.request.body;
78
- }
79
- else {
80
- rawBody = JSON.stringify(ctx.request.body);
81
- this.pluginLogger.debug("Signature verification using JSON.stringify(body) — may differ from original raw body");
82
- }
83
- if (!this.verifySignature(rawBody, signature)) {
84
- this.pluginLogger.warn(formatCompact({ op: "webhook", ok: false, error: "invalid signature" }));
85
- ctx.status = 403;
86
- ctx.body = { message: "Invalid signature" };
87
- return;
88
- }
89
- // 2. 解析事件
90
- const body = typeof ctx.request.body === "string"
91
- ? JSON.parse(ctx.request.body)
92
- : ctx.request.body;
93
- if (!body.events || !Array.isArray(body.events)) {
94
- ctx.status = 200;
95
- ctx.body = { message: "OK" };
96
- return;
97
- }
98
- // 3. 处理每个事件
99
- for (const event of body.events) {
100
- await this.handleEvent(event);
101
- }
102
- ctx.status = 200;
103
- ctx.body = { message: "OK" };
104
- }
105
- catch (error) {
106
- this.pluginLogger.error("LINE webhook error:", error);
107
- ctx.status = 200;
108
- ctx.body = { message: "OK" };
109
- }
110
- }
111
- // L07: Use timing-safe comparison for signature
112
- verifySignature(body, signature) {
113
- const channelSecret = this.$config.channelSecret;
114
- const hmac = createHmac("sha256", channelSecret);
115
- hmac.update(body, "utf-8");
116
- const computedSignature = hmac.digest("base64");
117
- const sigBuf = Buffer.from(signature);
118
- const computedBuf = Buffer.from(computedSignature);
119
- if (sigBuf.length !== computedBuf.length)
120
- return false;
121
- return timingSafeEqual(sigBuf, computedBuf);
122
- }
123
- // L04: Use type guards instead of "in" checks + `as any` casts
124
- async handleEvent(event) {
125
- switch (event.type) {
126
- case "message":
127
- if (isMessageEvent(event)) {
128
- await this.handleMessageEvent(event);
129
- }
130
- break;
131
- case "follow":
132
- await this.handleFollowEvent(event);
133
- break;
134
- case "unfollow":
135
- this.pluginLogger.debug(formatCompact({
136
- op: "unfollow",
137
- endpoint: this.$config.name,
138
- userId: event.source.userId,
139
- }));
140
- break;
141
- case "join":
142
- await this.handleJoinEvent(event);
143
- break;
144
- case "leave":
145
- this.pluginLogger.debug(formatCompact({
146
- op: "leave",
147
- endpoint: this.$config.name,
148
- sourceType: event.source.type,
149
- groupId: event.source.groupId,
150
- roomId: event.source.roomId,
151
- }));
152
- break;
153
- case "postback":
154
- if (isPostbackEvent(event)) {
155
- this.pluginLogger.debug(formatCompact({
156
- op: "postback",
157
- endpoint: this.$config.name,
158
- data: event.postback.data,
159
- }));
160
- }
161
- break;
162
- default:
163
- this.pluginLogger.debug(formatCompact({
164
- op: "unknown_event",
165
- endpoint: this.$config.name,
166
- type: event.type,
167
- }));
168
- }
169
- }
170
- // L01: Cache replyToken from webhook events before emitting
171
- async handleMessageEvent(event) {
172
- const { channelId } = this.resolveChannel(event.source);
173
- this.cacheReplyToken(channelId, event.replyToken);
174
- const message = this.$formatMessage(event);
175
- this.adapter.emit("message.receive", message);
176
- this.pluginLogger.debug(formatCompact({
177
- op: "recv",
178
- endpoint: this.$config.name,
179
- channel: message.$channel.type,
180
- id: message.$channel.id,
181
- len: segment.raw(message.$content).length,
182
- }));
183
- }
184
- async handleFollowEvent(event) {
185
- const { channelId } = this.resolveChannel(event.source);
186
- this.cacheReplyToken(channelId, event.replyToken);
187
- const message = this.$formatMessage(event);
188
- this.adapter.emit("message.receive", message);
189
- this.pluginLogger.debug(formatCompact({
190
- op: "follow",
191
- endpoint: this.$config.name,
192
- userId: event.source.userId,
193
- }));
194
- }
195
- async handleJoinEvent(event) {
196
- const { channelId } = this.resolveChannel(event.source);
197
- this.cacheReplyToken(channelId, event.replyToken);
198
- const message = this.$formatMessage(event);
199
- this.adapter.emit("message.receive", message);
200
- this.pluginLogger.debug(formatCompact({
201
- op: "join",
202
- endpoint: this.$config.name,
203
- sourceType: event.source.type,
204
- groupId: event.source.groupId,
205
- roomId: event.source.roomId,
206
- }));
207
- }
208
- // ── 消息格式化 ────────────────────────────────────────────────────
209
- $formatMessage(event) {
210
- const { channelType, channelId } = this.resolveChannel(event.source);
211
- const wire = this.parseMessageContent(event);
212
- const quoteId = Message.quoteIdFromContent(wire);
213
- Message.alignReplySegments(wire, quoteId);
214
- const content = toCanonicalSegments(wire);
215
- const userId = event.source.userId || "";
216
- const timestamp = event.timestamp || Date.now();
217
- const rawText = this.extractRawText(event);
218
- return Message.from(event, {
219
- $id: this.generateMessageId(event),
220
- $adapter: "line",
221
- $endpoint: this.$config.name,
222
- $sender: {
223
- id: userId,
224
- name: userId,
225
- },
226
- $channel: {
227
- id: channelId,
228
- type: channelType,
229
- },
230
- $content: content,
231
- $quote_id: quoteId,
232
- $raw: rawText,
233
- $timestamp: timestamp,
234
- $recall: async () => {
235
- // LINE 不支持消息撤回
236
- this.pluginLogger.warn("LINE does not support message recall");
237
- },
238
- $reply: async (content, quote) => {
239
- if (!Array.isArray(content))
240
- content = [content];
241
- if (quote) {
242
- const replyToMessageId = typeof quote === "boolean"
243
- ? (isMessageEvent(event) && event.message?.id) || ""
244
- : quote;
245
- content.unshift({ type: "reply", data: { id: replyToMessageId } });
246
- }
247
- return await this.adapter.sendMessage({
248
- context: "line",
249
- endpoint: this.$config.name,
250
- id: channelId,
251
- type: channelType,
252
- content: content,
253
- });
254
- },
50
+ open() {
51
+ this.#open = true;
52
+ }
53
+ close() {
54
+ this.#open = false;
55
+ }
56
+ async stop() {
57
+ this.#open = false;
58
+ this.#replyTokenCache.clear();
59
+ for (const release of this.#routeReleases.splice(0))
60
+ release();
61
+ this.#unregisterAgent?.();
62
+ this.#unregisterAgent = undefined;
63
+ this.#started = false;
64
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
65
+ }
66
+ async send({ target, payload }) {
67
+ const messages = formatOutboundMessages(payload);
68
+ if (messages.length === 0) {
69
+ throw new Error('No valid LINE messages to send');
70
+ }
71
+ const replyToken = this.#replyTokenCache.get(target);
72
+ if (replyToken) {
73
+ this.#replyTokenCache.delete(target);
74
+ return this.#replyMessage(replyToken, messages);
75
+ }
76
+ if (!isValidLineRecipientId(target)) {
77
+ throw new Error(`Invalid LINE recipient ID "${target}": must start with U (user), G (group), or R (room)`);
78
+ }
79
+ return this.#pushMessage(target, messages);
80
+ }
81
+ /** Test / internal: admit a parsed event when open (non-webhook path). */
82
+ admit(event) {
83
+ if (!this.#open)
84
+ return;
85
+ const { channelId } = resolveChannel(event.source);
86
+ if ('replyToken' in event && typeof event.replyToken === 'string') {
87
+ this.#replyTokenCache.set(channelId, event.replyToken);
88
+ }
89
+ void this.#options.gateway.receive({
90
+ adapter: this.#options.id,
91
+ target: channelId,
92
+ content: formatInboundContent(event),
93
+ sender: event.source.userId || channelId,
94
+ id: generateMessageId(event),
95
+ metadata: Object.freeze({
96
+ eventType: event.type,
97
+ sourceType: event.source.type,
98
+ endpoint: this.#options.config.name,
99
+ timestamp: event.timestamp,
100
+ ...(isMessageEvent(event) ? { messageType: event.message.type } : {}),
101
+ }),
102
+ }).catch((err) => {
103
+ logger.warn(formatCompact({
104
+ op: 'line_gateway_receive_failed',
105
+ target: channelId,
106
+ error: err instanceof Error ? err.message : String(err),
107
+ }));
255
108
  });
256
109
  }
257
- generateMessageId(event) {
258
- if (isMessageEvent(event) && event.message?.id) {
259
- return event.message.id;
260
- }
261
- return `${event.type}-${event.timestamp}`;
262
- }
263
- resolveChannel(source) {
264
- switch (source.type) {
265
- case "user":
266
- return { channelType: "private", channelId: source.userId || "" };
267
- case "group":
268
- return { channelType: "group", channelId: source.groupId || "" };
269
- case "room":
270
- return { channelType: "channel", channelId: source.roomId || "" };
271
- default:
272
- return { channelType: "private", channelId: "" };
273
- }
274
- }
275
- extractRawText(event) {
276
- if (isMessageEvent(event)) {
277
- const msg = event.message;
278
- if (msg.type === "text" && msg.text)
279
- return msg.text;
280
- if (msg.type === "location" && msg.address)
281
- return msg.address;
282
- return `[${msg.type}]`;
283
- }
284
- if (event.type === "follow")
285
- return "[follow]";
286
- if (event.type === "join")
287
- return "[join]";
288
- return "";
289
- }
290
- parseMessageContent(event) {
291
- const segments = [];
292
- if (isMessageEvent(event)) {
293
- const msg = event.message;
294
- switch (msg.type) {
295
- case "text":
296
- if (msg.text) {
297
- segments.push({ type: "text", data: { text: msg.text } });
298
- }
299
- break;
300
- case "image":
301
- segments.push({
302
- type: "image",
303
- data: {
304
- message_id: msg.id,
305
- platform: "line",
306
- },
307
- });
308
- break;
309
- case "video":
310
- segments.push({
311
- type: "video",
312
- data: {
313
- message_id: msg.id,
314
- platform: "line",
315
- },
316
- });
317
- break;
318
- case "audio":
319
- segments.push({
320
- type: "audio",
321
- data: {
322
- message_id: msg.id,
323
- duration: msg.duration || 0,
324
- platform: "line",
325
- },
326
- });
327
- break;
328
- case "file":
329
- segments.push({
330
- type: "file",
331
- data: {
332
- message_id: msg.id,
333
- file_name: msg.fileName,
334
- file_size: msg.fileSize,
335
- platform: "line",
336
- },
337
- });
338
- break;
339
- case "location":
340
- segments.push({
341
- type: "location",
342
- data: {
343
- title: msg.title,
344
- address: msg.address,
345
- latitude: msg.latitude,
346
- longitude: msg.longitude,
347
- },
348
- });
349
- break;
350
- case "sticker":
351
- segments.push({
352
- type: "sticker",
353
- data: {
354
- package_id: msg.packageId,
355
- sticker_id: msg.stickerId,
356
- resource_type: msg.stickerResourceType,
357
- },
358
- });
359
- break;
360
- default:
361
- segments.push({ type: "text", data: { text: `[unsupported message type]` } });
362
- }
363
- }
364
- else {
365
- // 系统事件(follow/unfollow/join/leave)
366
- const text = event.type === "follow" ? "[follow event]"
367
- : event.type === "join" ? "[join event]"
368
- : event.type === "unfollow" ? "[unfollow event]"
369
- : event.type === "leave" ? "[leave event]"
370
- : `[${event.type} event]`;
371
- segments.push({ type: "text", data: { text } });
372
- }
373
- return segments.length > 0 ? segments : [{ type: "text", data: { text: "" } }];
374
- }
375
- // ── 发送消息 ──────────────────────────────────────────────────────
376
- async $sendMessage(options) {
377
- try {
378
- const canonical = expandInteractiveSegmentsInContent(options.content);
379
- const wire = fromCanonicalSegments(canonical);
380
- const messages = this.buildLineMessages(wire);
381
- if (messages.length === 0) {
382
- throw new Error("No valid LINE messages to send");
383
- }
384
- // 优先使用 Reply API(如果存在 replyToken)
385
- const replyToken = this.replyTokenCache.get(options.id);
386
- if (replyToken) {
387
- this.replyTokenCache.delete(options.id);
388
- return await this.replyMessage(replyToken, messages);
389
- }
390
- // L06: Validate Push API `to` field
391
- if (!/^[UGR]/.test(options.id)) {
392
- throw new Error(`Invalid LINE recipient ID "${options.id}": must start with U (user), G (group), or R (room)`);
393
- }
394
- // 使用 Push API
395
- return await this.pushMessage(options.id, messages);
396
- }
397
- catch (error) {
398
- this.pluginLogger.error("Failed to send LINE message:", error);
399
- throw error;
400
- }
401
- }
402
- replyTokenCache = new Map();
403
- /**
404
- * 缓存 replyToken,用于后续发送回复消息
405
- */
406
- cacheReplyToken(channelId, replyToken) {
407
- this.replyTokenCache.set(channelId, replyToken);
408
- }
409
- // L15: Parse Reply API response for message ID
410
- async replyMessage(replyToken, messages) {
411
- const baseUrl = this.$config.apiBaseUrl || "https://api.line.me";
412
- const request = { replyToken, messages };
413
- const response = await fetch(`${baseUrl}/v2/bot/message/reply`, {
414
- method: "POST",
110
+ async #replyMessage(replyToken, messages) {
111
+ const url = `${this.#options.config.apiBaseUrl}/v2/bot/message/reply`;
112
+ const response = await this.#fetch(url, {
113
+ method: 'POST',
415
114
  headers: {
416
- "Content-Type": "application/json",
417
- "Authorization": `Bearer ${this.$config.channelAccessToken}`,
115
+ 'Content-Type': 'application/json',
116
+ Authorization: `Bearer ${this.#options.config.channelAccessToken}`,
418
117
  },
419
- body: JSON.stringify(request),
118
+ body: JSON.stringify({ replyToken, messages }),
420
119
  });
421
120
  if (!response.ok) {
422
121
  const errorText = await response.text();
423
122
  throw new Error(`LINE Reply API error ${response.status}: ${errorText}`);
424
123
  }
425
- // Reply API now returns sentMessages in the response body
426
124
  const result = await response.json();
427
125
  return result.sentMessages?.[0]?.id || `reply-${Date.now()}`;
428
126
  }
429
- async pushMessage(to, messages) {
430
- const baseUrl = this.$config.apiBaseUrl || "https://api.line.me";
431
- const request = { to, messages };
432
- const response = await fetch(`${baseUrl}/v2/bot/message/push`, {
433
- method: "POST",
127
+ async #pushMessage(to, messages) {
128
+ const url = `${this.#options.config.apiBaseUrl}/v2/bot/message/push`;
129
+ const response = await this.#fetch(url, {
130
+ method: 'POST',
434
131
  headers: {
435
- "Content-Type": "application/json",
436
- "Authorization": `Bearer ${this.$config.channelAccessToken}`,
132
+ 'Content-Type': 'application/json',
133
+ Authorization: `Bearer ${this.#options.config.channelAccessToken}`,
437
134
  },
438
- body: JSON.stringify(request),
135
+ body: JSON.stringify({ to, messages }),
439
136
  });
440
137
  if (!response.ok) {
441
138
  const errorText = await response.text();
@@ -444,93 +141,4 @@ export class LineEndpoint {
444
141
  const result = await response.json();
445
142
  return result.sentMessages?.[0]?.id || `push-${Date.now()}`;
446
143
  }
447
- buildLineMessages(content) {
448
- if (!Array.isArray(content))
449
- content = [content];
450
- const messages = [];
451
- for (const item of content) {
452
- if (typeof item === "string") {
453
- messages.push(this.buildTextMessage(item));
454
- continue;
455
- }
456
- const seg = item;
457
- switch (seg.type) {
458
- case "text":
459
- messages.push(this.buildTextMessage(seg.data.text || ""));
460
- break;
461
- case "at":
462
- // LINE 没有 @ 语法,转为文本
463
- if (seg.data.id) {
464
- messages.push(this.buildTextMessage(`@${seg.data.name || seg.data.id}`));
465
- }
466
- break;
467
- case "image":
468
- if (seg.data.url) {
469
- messages.push({
470
- type: "image",
471
- originalContentUrl: seg.data.url,
472
- previewImageUrl: seg.data.url,
473
- });
474
- }
475
- break;
476
- case "video":
477
- if (seg.data.url) {
478
- messages.push({
479
- type: "video",
480
- originalContentUrl: seg.data.url,
481
- previewImageUrl: seg.data.previewUrl || seg.data.url,
482
- });
483
- }
484
- break;
485
- case "audio":
486
- if (seg.data.url) {
487
- messages.push({
488
- type: "audio",
489
- originalContentUrl: seg.data.url,
490
- duration: seg.data.duration || 0,
491
- });
492
- }
493
- break;
494
- case "location":
495
- messages.push({
496
- type: "location",
497
- title: seg.data.title || "Location",
498
- address: seg.data.address || "",
499
- latitude: seg.data.latitude || 0,
500
- longitude: seg.data.longitude || 0,
501
- });
502
- break;
503
- case "sticker":
504
- messages.push({
505
- type: "sticker",
506
- packageId: seg.data.package_id || "1",
507
- stickerId: seg.data.sticker_id || "1",
508
- });
509
- break;
510
- default:
511
- messages.push(this.buildTextMessage(`[${seg.type}]`));
512
- }
513
- }
514
- // L14: Log warning when messages are sliced to 5 (LINE limit)
515
- if (messages.length > 5) {
516
- this.pluginLogger.warn(`LINE messages truncated from ${messages.length} to 5 (platform limit)`);
517
- }
518
- // LINE 单次最多发送 5 条消息
519
- return messages.slice(0, 5);
520
- }
521
- buildTextMessage(text) {
522
- // L13: Log warning on text truncation
523
- if (text.length > 5000) {
524
- this.pluginLogger.warn(`LINE text message truncated from ${text.length} to 5000 characters`);
525
- }
526
- // LINE 消息文本限制 5000 字符
527
- const truncated = text.length > 5000 ? text.slice(0, 4997) + "..." : text;
528
- return { type: "text", text: truncated };
529
- }
530
- // ── 消息撤回 ──────────────────────────────────────────────────────
531
- // L16: Log warning and return gracefully (matching WeCom/DingTalk pattern)
532
- async $recallMessage(_id) {
533
- this.pluginLogger.warn(formatCompact({ op: "recall", ok: false, error: "LINE Messaging API does not support message recall" }));
534
- }
535
144
  }
536
- //# sourceMappingURL=endpoint.js.map
package/lib/index.d.ts CHANGED
@@ -1,15 +1,4 @@
1
- import { LineAdapter } from './adapter.js';
2
- declare module 'zhin.js' {
3
- namespace Plugin {
4
- interface Contexts {
5
- router: import('@zhin.js/host-router').Router;
6
- }
7
- }
8
- interface Adapters {
9
- line: LineAdapter;
10
- }
11
- }
12
- export * from './types.js';
13
- export { LineEndpoint } from './endpoint.js';
14
- export { LineAdapter } from './adapter.js';
15
- //# sourceMappingURL=index.d.ts.map
1
+ export { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isPostbackEvent, isValidLineRecipientId, normalizeWebhookPath, readTextBody, resolveChannel, resolveLineConfig, verifySignature, type LineAdapterConfig, type LineApiResponse, type LineChannel, type LineEvent, type LineFollowEvent, type LineJoinEvent, type LineLeaveEvent, type LineMessage, type LineMessageEvent, type LinePostbackEvent, type LinePushRequest, type LineReplyMessage, type LineReplyRequest, type LineSource, type LineUnfollowEvent, type LineUser, type LineWebhookBody, type LineWireSegment, type ResolvedLineConfig, } from './protocol.js';
2
+ export { LineEndpoint, type LineEndpointOptions, type LineFetch, } from './endpoint.js';
3
+ export { registerLineWebhookRoutes, handleLineWebhookRequest, type LineWebhookHandler, } from './webhook.js';
4
+ export { getLineAgentDeps, getLineApiConfig, registerLineAgentEndpoint, setLineAgentDeps, type LineAgentDeps, type LineAgentEndpoint, } from './line-agent-deps.js';