@zhin.js/adapter-line 4.0.2 → 4.0.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # @zhin.js/adapter-line
2
2
 
3
+ ## 4.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6f9c366]
8
+ - Updated dependencies [373a56b]
9
+ - Updated dependencies [0de46a8]
10
+ - Updated dependencies [c106ecc]
11
+ - Updated dependencies [ca92e03]
12
+ - Updated dependencies [b0f37ae]
13
+ - Updated dependencies [ba08a2f]
14
+ - Updated dependencies [b08f7fe]
15
+ - Updated dependencies [daffd4c]
16
+ - Updated dependencies [36c7400]
17
+ - Updated dependencies [a9fa72e]
18
+ - Updated dependencies [c8de3ef]
19
+ - Updated dependencies [60f0fc8]
20
+ - Updated dependencies [574c990]
21
+ - Updated dependencies [d096f16]
22
+ - Updated dependencies [3f29623]
23
+ - Updated dependencies [2916852]
24
+ - Updated dependencies [d047869]
25
+ - Updated dependencies [162fa34]
26
+ - Updated dependencies [e62561e]
27
+ - Updated dependencies [3eeeb46]
28
+ - Updated dependencies [85d0f82]
29
+ - Updated dependencies [61bfc1c]
30
+ - Updated dependencies [04bad47]
31
+ - Updated dependencies [e40b048]
32
+ - Updated dependencies [5a5b1bb]
33
+ - Updated dependencies [f1708c3]
34
+ - Updated dependencies [d254a81]
35
+ - Updated dependencies [7123c47]
36
+ - Updated dependencies [05befc1]
37
+ - Updated dependencies [0663b6a]
38
+ - Updated dependencies [f28f9b3]
39
+ - Updated dependencies [9f340f7]
40
+ - Updated dependencies [e53444f]
41
+ - Updated dependencies [5eedd26]
42
+ - Updated dependencies [92b0dd7]
43
+ - Updated dependencies [f919b6f]
44
+ - Updated dependencies [098e411]
45
+ - Updated dependencies [e1b7c01]
46
+ - Updated dependencies [9b94f87]
47
+ - Updated dependencies [a7df753]
48
+ - @zhin.js/agent@1.1.5
49
+ - @zhin.js/host-http@1.0.8
50
+ - @zhin.js/im-contract@1.0.3
51
+ - @zhin.js/adapter@1.1.7
52
+ - @zhin.js/plugin-runtime@1.1.5
53
+ - @zhin.js/command@1.0.9
54
+ - @zhin.js/core@1.5.4
55
+ - zhin.js@6.0.4
56
+
57
+ ## 4.0.4
58
+
59
+ ### Patch Changes
60
+
61
+ - f8c7a54: fix: im
62
+ - Updated dependencies [f8c7a54]
63
+ - @zhin.js/logger@1.0.76
64
+ - @zhin.js/host-http@1.0.7
65
+ - @zhin.js/adapter@1.1.6
66
+ - @zhin.js/agent@1.1.4
67
+ - @zhin.js/command@1.0.8
68
+ - @zhin.js/core@1.5.3
69
+ - @zhin.js/im-contract@1.0.2
70
+ - @zhin.js/plugin-runtime@1.1.4
71
+ - zhin.js@6.0.3
72
+
73
+ ## 4.0.3
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [afc0e66]
78
+ - Updated dependencies [2e41ad5]
79
+ - Updated dependencies [9f57124]
80
+ - @zhin.js/core@1.5.2
81
+ - @zhin.js/adapter@1.1.5
82
+ - @zhin.js/im-contract@1.0.1
83
+ - @zhin.js/plugin-runtime@1.1.3
84
+ - @zhin.js/command@1.0.7
85
+ - @zhin.js/host-http@1.0.6
86
+ - @zhin.js/agent@1.1.3
87
+ - zhin.js@6.0.2
88
+
3
89
  ## 4.0.2
4
90
 
5
91
  ### Patch Changes
package/README.md CHANGED
@@ -24,8 +24,8 @@ pnpm add @zhin.js/adapter-line
24
24
  - `@zhin.js/plugin-runtime` — `plugin.ts`(`definePlugin`)
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
26
26
 
27
- 入站:`gateway.receive({ adapter, target: channelId, content: text, sender, metadata })`
28
- 出站:`send({ target, payload })` → Reply API(缓存 replyToken)或 Push API
27
+ 入站:`gateway.receive({ conversation, message: { conversation, id }, content: text, sender, metadata })`
28
+ 出站:`send({ conversation, payload })` → Reply API(缓存 replyToken)或 Push API
29
29
 
30
30
  ## 前置条件
31
31
 
package/adapters/line.js CHANGED
@@ -19,8 +19,8 @@ export default defineAdapter({
19
19
  create(context) {
20
20
  const config = resolveLineConfig(context.config);
21
21
  // 注册到插件运行时状态(line.endpoint list 的"运行中"数据源)
22
- context.use(lineRuntimeStateToken).endpoints.set(config.name, {
23
- name: config.name,
22
+ context.use(lineRuntimeStateToken).endpoints.set(config.id, {
23
+ id: config.id,
24
24
  mode: 'webhook',
25
25
  });
26
26
  return new LineEndpoint({
package/adapters/line.ts CHANGED
@@ -24,8 +24,8 @@ export default defineAdapter<LineAdapterConfig>({
24
24
  create(context) {
25
25
  const config = resolveLineConfig(context.config);
26
26
  // 注册到插件运行时状态(line.endpoint list 的"运行中"数据源)
27
- context.use(lineRuntimeStateToken).endpoints.set(config.name, {
28
- name: config.name,
27
+ context.use(lineRuntimeStateToken).endpoints.set(config.id, {
28
+ id: config.id,
29
29
  mode: 'webhook',
30
30
  });
31
31
  return new LineEndpoint({
package/lib/endpoint.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LineEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
3
3
  */
4
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
4
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import type { HttpHost } from '@zhin.js/host-http';
7
7
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -39,10 +39,7 @@ export declare class LineEndpoint implements EndpointInstance {
39
39
  open(): void;
40
40
  close(): void;
41
41
  stop(): Promise<void>;
42
- send({ target, payload }: {
43
- readonly target: string;
44
- readonly payload: unknown;
45
- }): Promise<string>;
42
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
46
43
  /** Test / internal: admit a parsed event when open (non-webhook path). */
47
44
  admit(event: LineEvent): void;
48
45
  /**
package/lib/endpoint.js CHANGED
@@ -1,13 +1,13 @@
1
- import { formatCompact, getLogger } from '@zhin.js/logger';
1
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
2
2
  import { registerLineAgentEndpoint } from './line-agent-deps.js';
3
- import { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isValidLineRecipientId, resolveChannel, } from './protocol.js';
3
+ import { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isValidLineRecipientId, lineInboundConversation, } from './protocol.js';
4
4
  import { registerLineWebhookRoutes } from './webhook.js';
5
- const logger = getLogger('line');
6
5
  /** LINE replyToken 有效期短,过期后 reply 必 400;缓存带时间戳,超时弃用改走 push。 */
7
6
  const REPLY_TOKEN_TTL_MS = 60_000;
8
7
  /** 出站 HTTP 调用统一 30s 超时。 */
9
8
  const OUTBOUND_TIMEOUT_MS = 30_000;
10
9
  export class LineEndpoint {
10
+ #logger;
11
11
  #options;
12
12
  #fetch;
13
13
  #routeReleases = [];
@@ -17,6 +17,7 @@ export class LineEndpoint {
17
17
  #unregisterAgent;
18
18
  management = createLineEndpointManagement(this);
19
19
  constructor(options) {
20
+ this.#logger = getAdapterLogger('line', options.config.id);
20
21
  this.#options = options;
21
22
  this.#fetch = options.fetch ?? globalThis.fetch;
22
23
  }
@@ -38,17 +39,17 @@ export class LineEndpoint {
38
39
  return;
39
40
  this.#started = true;
40
41
  try {
41
- this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.name, this);
42
+ this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.id, this);
42
43
  this.#routeReleases.push(...registerLineWebhookRoutes(this.#options.http, this));
43
- logger.debug(formatCompact({
44
- endpoint: this.#options.config.name,
44
+ this.#logger.debug(formatCompact({
45
+ endpoint: this.#options.config.id,
45
46
  op: 'webhook',
46
47
  path: this.#options.config.webhookPath,
47
48
  }));
48
49
  }
49
50
  catch (error) {
50
51
  await this.stop();
51
- logger.error('Failed to connect LINE endpoint:', error);
52
+ this.#logger.error('Failed to connect LINE endpoint:', error);
52
53
  throw error;
53
54
  }
54
55
  }
@@ -66,13 +67,15 @@ export class LineEndpoint {
66
67
  this.#unregisterAgent?.();
67
68
  this.#unregisterAgent = undefined;
68
69
  this.#started = false;
69
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
70
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
70
71
  }
71
- async send({ target, payload }) {
72
+ async send({ conversation, payload }) {
72
73
  const messages = formatOutboundMessages(payload);
73
74
  if (messages.length === 0) {
74
75
  throw new Error('No valid LINE messages to send');
75
76
  }
77
+ // LINE recipient id 前缀(U/G/R)自带场景信息,原生 id 即投递地址。
78
+ const target = conversation.id;
76
79
  const cached = this.#replyTokenCache.get(target);
77
80
  if (cached) {
78
81
  this.#replyTokenCache.delete(target);
@@ -84,9 +87,9 @@ export class LineEndpoint {
84
87
  // replyToken 过期/失效时 LINE 返回 400,回退 push 保证消息不丢
85
88
  if (error.status !== 400)
86
89
  throw error;
87
- logger.warn(formatCompact({
90
+ this.#logger.warn(formatCompact({
88
91
  op: 'line_reply_fallback_push',
89
- endpoint: this.#options.config.name,
92
+ endpoint: this.#options.config.id,
90
93
  target,
91
94
  }));
92
95
  }
@@ -101,27 +104,26 @@ export class LineEndpoint {
101
104
  admit(event) {
102
105
  if (!this.#open)
103
106
  return;
104
- const { channelId } = resolveChannel(event.source);
107
+ const conversation = lineInboundConversation(String(this.#options.id), event.source);
105
108
  if ('replyToken' in event && typeof event.replyToken === 'string') {
106
- this.#replyTokenCache.set(channelId, { token: event.replyToken, timestamp: Date.now() });
109
+ this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
107
110
  }
108
111
  void this.#options.gateway.receive({
109
- adapter: this.#options.id,
110
- target: channelId,
112
+ conversation,
113
+ message: { conversation, id: generateMessageId(event) },
111
114
  content: formatInboundContent(event),
112
- sender: event.source.userId || channelId,
113
- id: generateMessageId(event),
115
+ sender: { id: event.source.userId || conversation.id },
116
+ endpointId: this.#options.config.id,
114
117
  metadata: Object.freeze({
115
118
  eventType: event.type,
116
119
  sourceType: event.source.type,
117
- endpoint: this.#options.config.name,
118
120
  timestamp: event.timestamp,
119
121
  ...(isMessageEvent(event) ? { messageType: event.message.type } : {}),
120
122
  }),
121
123
  }).catch((err) => {
122
- logger.warn(formatCompact({
124
+ this.#logger.warn(formatCompact({
123
125
  op: 'line_gateway_receive_failed',
124
- target: channelId,
126
+ target: `${conversation.kind}:${conversation.id}`,
125
127
  error: err instanceof Error ? err.message : String(err),
126
128
  }));
127
129
  });
@@ -14,7 +14,7 @@ export interface LineAgentDeps {
14
14
  apiBaseUrl: string;
15
15
  };
16
16
  }
17
- export declare function registerLineAgentEndpoint(endpointId: string, endpoint: LineAgentEndpoint): () => void;
17
+ export declare function registerLineAgentEndpoint(endpointKey: string, endpoint: LineAgentEndpoint): () => void;
18
18
  /** Optional override used by tests / transitional callers. Pass `null` to clear. */
19
19
  export declare function setLineAgentDeps(deps: LineAgentDeps | null): void;
20
20
  export declare function getLineAgentDeps(): LineAgentDeps;
@@ -4,11 +4,11 @@
4
4
  */
5
5
  const endpoints = new Map();
6
6
  let override = null;
7
- export function registerLineAgentEndpoint(endpointId, endpoint) {
8
- endpoints.set(endpointId, endpoint);
7
+ export function registerLineAgentEndpoint(endpointKey, endpoint) {
8
+ endpoints.set(endpointKey, endpoint);
9
9
  return () => {
10
- if (endpoints.get(endpointId) === endpoint) {
11
- endpoints.delete(endpointId);
10
+ if (endpoints.get(endpointKey) === endpoint) {
11
+ endpoints.delete(endpointKey);
12
12
  }
13
13
  };
14
14
  }
package/lib/protocol.d.ts CHANGED
@@ -3,9 +3,10 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
5
  import type { IncomingMessage } from 'node:http';
6
+ import type { ConversationRef } from '@zhin.js/im-contract';
6
7
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
7
8
  export interface LineAdapterConfig {
8
- readonly name?: string;
9
+ readonly id?: string;
9
10
  readonly channelSecret?: string;
10
11
  readonly channelAccessToken?: string;
11
12
  readonly webhookPath?: string;
@@ -17,7 +18,7 @@ export interface LineAdapterConfig {
17
18
  }
18
19
  export interface ResolvedLineConfig {
19
20
  readonly context: 'line';
20
- readonly name: string;
21
+ readonly id: string;
21
22
  readonly channelSecret: string;
22
23
  readonly channelAccessToken: string;
23
24
  readonly webhookPath: string;
@@ -136,6 +137,11 @@ export declare function normalizeWebhookPath(path: string): string;
136
137
  export declare function isMessageEvent(event: LineEvent): event is LineMessageEvent;
137
138
  export declare function isPostbackEvent(event: LineEvent): event is LinePostbackEvent;
138
139
  export declare function resolveChannel(source: LineSource): LineChannel;
140
+ /**
141
+ * 入站归一化 → ConversationRef:LINE user → private、group → group、room → channel。
142
+ * LINE 没有 guild/频道容器概念,无 parent;recipient id 前缀(U/G/R)自带场景信息。
143
+ */
144
+ export declare function lineInboundConversation(endpointKey: string, source: LineSource): ConversationRef;
139
145
  export declare function generateMessageId(event: LineEvent): string;
140
146
  /** Build inbound text for MessageGateway.receive. */
141
147
  export declare function formatInboundContent(event: LineEvent): string;
package/lib/protocol.js CHANGED
@@ -17,15 +17,15 @@ export function resolveLineConfig(config = {}) {
17
17
  if (!channelSecret || !channelAccessToken) {
18
18
  throw new TypeError('LINE adapter requires channelSecret + channelAccessToken (plugins.<key> or endpoints with context: line)');
19
19
  }
20
- const name = (typeof config.name === 'string' && config.name)
21
- || (typeof entry?.name === 'string' && entry.name)
20
+ const id = (typeof config.id === 'string' && config.id)
21
+ || (typeof entry?.id === 'string' && entry.id)
22
22
  || process.env.LINE_BOT_NAME
23
23
  || 'line-bot';
24
24
  const webhookPath = normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? '/line/webhook');
25
25
  const apiBaseUrl = (config.apiBaseUrl ?? entry?.apiBaseUrl ?? 'https://api.line.me').replace(/\/$/, '');
26
26
  return {
27
27
  context: 'line',
28
- name,
28
+ id,
29
29
  channelSecret,
30
30
  channelAccessToken,
31
31
  webhookPath,
@@ -54,6 +54,18 @@ export function resolveChannel(source) {
54
54
  return { channelType: 'private', channelId: '' };
55
55
  }
56
56
  }
57
+ /**
58
+ * 入站归一化 → ConversationRef:LINE user → private、group → group、room → channel。
59
+ * LINE 没有 guild/频道容器概念,无 parent;recipient id 前缀(U/G/R)自带场景信息。
60
+ */
61
+ export function lineInboundConversation(endpointKey, source) {
62
+ const channel = resolveChannel(source);
63
+ return {
64
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
65
+ kind: channel.channelType,
66
+ id: channel.channelId,
67
+ };
68
+ }
57
69
  export function generateMessageId(event) {
58
70
  if (isMessageEvent(event) && event.message?.id)
59
71
  return event.message.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-line",
3
- "version": "4.0.2",
3
+ "version": "4.0.5",
4
4
  "description": "Zhin.js LINE Messaging API adapter for Plugin Runtime (HTTP webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,21 +32,22 @@
32
32
  "directory": "plugins/adapters/line"
33
33
  },
34
34
  "dependencies": {
35
- "@zhin.js/adapter": "1.1.4",
36
- "@zhin.js/command": "1.0.6",
37
- "@zhin.js/core": "1.5.1",
38
- "@zhin.js/host-http": "1.0.5",
39
- "@zhin.js/logger": "1.0.75",
40
- "@zhin.js/plugin-runtime": "1.1.2"
35
+ "@zhin.js/adapter": "1.1.7",
36
+ "@zhin.js/command": "1.0.9",
37
+ "@zhin.js/core": "1.5.4",
38
+ "@zhin.js/host-http": "1.0.8",
39
+ "@zhin.js/im-contract": "1.0.3",
40
+ "@zhin.js/logger": "1.0.76",
41
+ "@zhin.js/plugin-runtime": "1.1.5"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "zod": "^4.0.0",
44
- "@zhin.js/adapter": "1.1.4",
45
- "@zhin.js/agent": "1.1.2",
46
- "@zhin.js/core": "1.5.1",
47
- "@zhin.js/host-http": "1.0.5",
48
- "@zhin.js/plugin-runtime": "1.1.2",
49
- "zhin.js": "6.0.1"
45
+ "@zhin.js/adapter": "1.1.7",
46
+ "@zhin.js/agent": "1.1.5",
47
+ "@zhin.js/core": "1.5.4",
48
+ "@zhin.js/host-http": "1.0.8",
49
+ "@zhin.js/plugin-runtime": "1.1.5",
50
+ "zhin.js": "6.0.4"
50
51
  },
51
52
  "peerDependenciesMeta": {
52
53
  "zhin.js": {
@@ -64,7 +65,7 @@
64
65
  "typescript": "^6.0.3",
65
66
  "vitest": "^4.1.10",
66
67
  "zod": "^4.4.3",
67
- "@zhin.js/agent": "1.1.2"
68
+ "@zhin.js/agent": "1.1.5"
68
69
  },
69
70
  "files": [
70
71
  "adapters",
package/schema.json CHANGED
@@ -11,16 +11,48 @@
11
11
  "type": "string",
12
12
  "default": "https://api.line.me"
13
13
  },
14
+ "master": {
15
+ "type": [
16
+ "string",
17
+ "number"
18
+ ],
19
+ "description": "框架 master(LINE user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
20
+ },
21
+ "trusted": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": [
25
+ "string",
26
+ "number"
27
+ ],
28
+ "description": "Trusted LINE user id"
29
+ },
30
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
31
+ },
14
32
  "endpoints": {
15
33
  "type": "array",
16
- "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
34
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
17
35
  "items": {
18
36
  "type": "object",
19
37
  "additionalProperties": true,
20
38
  "properties": {
21
- "name": {
22
- "type": "string",
23
- "description": "LINE bot name"
39
+ "master": {
40
+ "type": [
41
+ "string",
42
+ "number"
43
+ ],
44
+ "description": "本 endpoint 的框架 master(LINE user id);覆盖顶层 master"
45
+ },
46
+ "trusted": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": [
50
+ "string",
51
+ "number"
52
+ ],
53
+ "description": "Trusted LINE user id"
54
+ },
55
+ "description": "本 endpoint 的 trusted 列表"
24
56
  },
25
57
  "channelSecret": {
26
58
  "type": "string",
@@ -29,10 +61,14 @@
29
61
  "channelAccessToken": {
30
62
  "type": "string",
31
63
  "description": "LINE channel access token"
64
+ },
65
+ "id": {
66
+ "type": "string",
67
+ "description": "LINE bot name"
32
68
  }
33
69
  },
34
70
  "required": [
35
- "name",
71
+ "id",
36
72
  "channelSecret",
37
73
  "channelAccessToken"
38
74
  ]
package/src/endpoint.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * LineEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
3
3
  */
4
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
4
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
7
- import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
8
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
9
9
  import { registerLineAgentEndpoint } from './line-agent-deps.js';
10
10
  import {
@@ -13,15 +13,13 @@ import {
13
13
  generateMessageId,
14
14
  isMessageEvent,
15
15
  isValidLineRecipientId,
16
- resolveChannel,
16
+ lineInboundConversation,
17
17
  type LineApiResponse,
18
18
  type LineEvent,
19
19
  type ResolvedLineConfig,
20
20
  } from './protocol.js';
21
21
  import { registerLineWebhookRoutes } from './webhook.js';
22
22
 
23
- const logger = getLogger('line');
24
-
25
23
  /** LINE replyToken 有效期短,过期后 reply 必 400;缓存带时间戳,超时弃用改走 push。 */
26
24
  const REPLY_TOKEN_TTL_MS = 60_000;
27
25
  /** 出站 HTTP 调用统一 30s 超时。 */
@@ -51,6 +49,8 @@ export interface LineEndpointOptions {
51
49
  }
52
50
 
53
51
  export class LineEndpoint implements EndpointInstance {
52
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
53
+
54
54
  readonly #options: LineEndpointOptions;
55
55
  readonly #fetch: LineFetch;
56
56
  #routeReleases: HttpRouteRegistration[] = [];
@@ -61,6 +61,7 @@ export class LineEndpoint implements EndpointInstance {
61
61
  readonly management: EndpointManagement = createLineEndpointManagement(this);
62
62
 
63
63
  constructor(options: LineEndpointOptions) {
64
+ this.#logger = getAdapterLogger('line', options.config.id);
64
65
  this.#options = options;
65
66
  this.#fetch = options.fetch ?? globalThis.fetch;
66
67
  }
@@ -85,16 +86,16 @@ export class LineEndpoint implements EndpointInstance {
85
86
  if (this.#started) return;
86
87
  this.#started = true;
87
88
  try {
88
- this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.name, this);
89
+ this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.id, this);
89
90
  this.#routeReleases.push(...registerLineWebhookRoutes(this.#options.http, this));
90
- logger.debug(formatCompact({
91
- endpoint: this.#options.config.name,
91
+ this.#logger.debug(formatCompact({
92
+ endpoint: this.#options.config.id,
92
93
  op: 'webhook',
93
94
  path: this.#options.config.webhookPath,
94
95
  }));
95
96
  } catch (error) {
96
97
  await this.stop();
97
- logger.error('Failed to connect LINE endpoint:', error);
98
+ this.#logger.error('Failed to connect LINE endpoint:', error);
98
99
  throw error;
99
100
  }
100
101
  }
@@ -114,14 +115,16 @@ export class LineEndpoint implements EndpointInstance {
114
115
  this.#unregisterAgent?.();
115
116
  this.#unregisterAgent = undefined;
116
117
  this.#started = false;
117
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
118
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
118
119
  }
119
120
 
120
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
121
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
121
122
  const messages = formatOutboundMessages(payload);
122
123
  if (messages.length === 0) {
123
124
  throw new Error('No valid LINE messages to send');
124
125
  }
126
+ // LINE recipient id 前缀(U/G/R)自带场景信息,原生 id 即投递地址。
127
+ const target = conversation.id;
125
128
 
126
129
  const cached = this.#replyTokenCache.get(target);
127
130
  if (cached) {
@@ -132,9 +135,9 @@ export class LineEndpoint implements EndpointInstance {
132
135
  } catch (error) {
133
136
  // replyToken 过期/失效时 LINE 返回 400,回退 push 保证消息不丢
134
137
  if ((error as { status?: number }).status !== 400) throw error;
135
- logger.warn(formatCompact({
138
+ this.#logger.warn(formatCompact({
136
139
  op: 'line_reply_fallback_push',
137
- endpoint: this.#options.config.name,
140
+ endpoint: this.#options.config.id,
138
141
  target,
139
142
  }));
140
143
  }
@@ -152,27 +155,26 @@ export class LineEndpoint implements EndpointInstance {
152
155
  /** Test / internal: admit a parsed event when open (non-webhook path). */
153
156
  admit(event: LineEvent): void {
154
157
  if (!this.#open) return;
155
- const { channelId } = resolveChannel(event.source);
158
+ const conversation = lineInboundConversation(String(this.#options.id), event.source);
156
159
  if ('replyToken' in event && typeof event.replyToken === 'string') {
157
- this.#replyTokenCache.set(channelId, { token: event.replyToken, timestamp: Date.now() });
160
+ this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
158
161
  }
159
162
  void this.#options.gateway.receive({
160
- adapter: this.#options.id,
161
- target: channelId,
163
+ conversation,
164
+ message: { conversation, id: generateMessageId(event) },
162
165
  content: formatInboundContent(event),
163
- sender: event.source.userId || channelId,
164
- id: generateMessageId(event),
166
+ sender: { id: event.source.userId || conversation.id },
167
+ endpointId: this.#options.config.id,
165
168
  metadata: Object.freeze({
166
169
  eventType: event.type,
167
170
  sourceType: event.source.type,
168
- endpoint: this.#options.config.name,
169
171
  timestamp: event.timestamp,
170
172
  ...(isMessageEvent(event) ? { messageType: event.message.type } : {}),
171
173
  }),
172
174
  }).catch((err) => {
173
- logger.warn(formatCompact({
175
+ this.#logger.warn(formatCompact({
174
176
  op: 'line_gateway_receive_failed',
175
- target: channelId,
177
+ target: `${conversation.kind}:${conversation.id}`,
176
178
  error: err instanceof Error ? err.message : String(err),
177
179
  }));
178
180
  });
@@ -15,13 +15,13 @@ const endpoints = new Map<string, LineAgentEndpoint>();
15
15
  let override: LineAgentDeps | null = null;
16
16
 
17
17
  export function registerLineAgentEndpoint(
18
- endpointId: string,
18
+ endpointKey: string,
19
19
  endpoint: LineAgentEndpoint,
20
20
  ): () => void {
21
- endpoints.set(endpointId, endpoint);
21
+ endpoints.set(endpointKey, endpoint);
22
22
  return () => {
23
- if (endpoints.get(endpointId) === endpoint) {
24
- endpoints.delete(endpointId);
23
+ if (endpoints.get(endpointKey) === endpoint) {
24
+ endpoints.delete(endpointKey);
25
25
  }
26
26
  };
27
27
  }
package/src/protocol.ts CHANGED
@@ -6,13 +6,14 @@
6
6
  import { createHmac, timingSafeEqual } from 'node:crypto';
7
7
  import type { IncomingMessage } from 'node:http';
8
8
  import { isMediaRef } from '@zhin.js/core';
9
+ import type { ConversationRef } from '@zhin.js/im-contract';
9
10
  import { formatCompact, getLogger } from '@zhin.js/logger';
10
11
 
11
12
  const logger = getLogger('line');
12
13
 
13
14
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
14
15
  export interface LineAdapterConfig {
15
- readonly name?: string;
16
+ readonly id?: string;
16
17
  readonly channelSecret?: string;
17
18
  readonly channelAccessToken?: string;
18
19
  readonly webhookPath?: string;
@@ -25,7 +26,7 @@ export interface LineAdapterConfig {
25
26
 
26
27
  export interface ResolvedLineConfig {
27
28
  readonly context: 'line';
28
- readonly name: string;
29
+ readonly id: string;
29
30
  readonly channelSecret: string;
30
31
  readonly channelAccessToken: string;
31
32
  readonly webhookPath: string;
@@ -173,8 +174,8 @@ export function resolveLineConfig(config: LineAdapterConfig = {}): ResolvedLineC
173
174
  'LINE adapter requires channelSecret + channelAccessToken (plugins.<key> or endpoints with context: line)',
174
175
  );
175
176
  }
176
- const name = (typeof config.name === 'string' && config.name)
177
- || (typeof entry?.name === 'string' && entry.name)
177
+ const id = (typeof config.id === 'string' && config.id)
178
+ || (typeof entry?.id === 'string' && entry.id)
178
179
  || process.env.LINE_BOT_NAME
179
180
  || 'line-bot';
180
181
  const webhookPath = normalizeWebhookPath(
@@ -183,7 +184,7 @@ export function resolveLineConfig(config: LineAdapterConfig = {}): ResolvedLineC
183
184
  const apiBaseUrl = (config.apiBaseUrl ?? entry?.apiBaseUrl ?? 'https://api.line.me').replace(/\/$/, '');
184
185
  return {
185
186
  context: 'line',
186
- name,
187
+ id,
187
188
  channelSecret,
188
189
  channelAccessToken,
189
190
  webhookPath,
@@ -217,6 +218,19 @@ export function resolveChannel(source: LineSource): LineChannel {
217
218
  }
218
219
  }
219
220
 
221
+ /**
222
+ * 入站归一化 → ConversationRef:LINE user → private、group → group、room → channel。
223
+ * LINE 没有 guild/频道容器概念,无 parent;recipient id 前缀(U/G/R)自带场景信息。
224
+ */
225
+ export function lineInboundConversation(endpointKey: string, source: LineSource): ConversationRef {
226
+ const channel = resolveChannel(source);
227
+ return {
228
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
229
+ kind: channel.channelType,
230
+ id: channel.channelId,
231
+ };
232
+ }
233
+
220
234
  export function generateMessageId(event: LineEvent): string {
221
235
  if (isMessageEvent(event) && event.message?.id) return event.message.id;
222
236
  return `${event.type}-${event.timestamp}`;