@zhin.js/adapter-line 5.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @zhin.js/adapter-line
2
2
 
3
+ ## 5.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1fc78bc: Unify native platform Client access behind the literal `adapter` discriminant. Handlers infer both native events and Clients, while command, inbound/outbound middleware, and both Agent tool authoring surfaces expose the exact operation-scoped Client through a lazy `$client` getter. Definitions without `adapter` keep `$client` typed as `unknown`, and runtime dispatch rejects adapter mismatches before resolving the Client. Bundled platform tools now use this single path instead of model-provided endpoint ids and adapter-specific dependency wrappers. Every adapter registers one Client/EventMap contract, and protocol adapters including NapCat, Milky, OneBot and Satori now produce transport-independent Client objects rather than letting Endpoint instances impersonate Clients.
8
+ - Updated dependencies [e9c6a73]
9
+ - Updated dependencies [4e8117c]
10
+ - Updated dependencies [902fa35]
11
+ - Updated dependencies [54bfd6b]
12
+ - Updated dependencies [12025ee]
13
+ - Updated dependencies [09b14d6]
14
+ - Updated dependencies [1fc78bc]
15
+ - @zhin.js/agent@1.1.16
16
+ - @zhin.js/adapter@1.2.1
17
+ - @zhin.js/core@1.5.14
18
+ - @zhin.js/host-http@1.0.13
19
+ - @zhin.js/command@1.0.16
20
+ - @zhin.js/logger@1.0.77
21
+ - zhin.js@6.0.14
22
+ - @zhin.js/feature-kit@1.0.13
23
+
3
24
  ## 5.0.0
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -19,7 +19,7 @@ pnpm add @zhin.js/adapter-line
19
19
  ## Plugin Runtime
20
20
 
21
21
  - `@zhin.js/adapter` — 约定式 `adapters/line.ts`(`defineAdapter`)
22
- - `@zhin.js/core` — `messageGatewayToken` 入站/出站
22
+ - `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
23
23
  - `@zhin.js/host-http` — `httpHostToken` 注册 Webhook 路由(**非** legacy host-router/Koa)
24
24
  - `zhin.js` — `plugin.ts`(`definePlugin`)
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
package/adapters/line.js CHANGED
@@ -3,7 +3,6 @@
3
3
  * Convention entry: discover `adapters/line.ts` → defineAdapter.
4
4
  */
5
5
  import { defineAdapter } from 'zhin.js/adapter';
6
- import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
7
6
  import { httpHostToken } from '@zhin.js/host-http';
8
7
  import { LineEndpoint } from "../lib/endpoint.js";
9
8
  import { resolveLineConfig, } from "../lib/protocol.js";
@@ -25,8 +24,6 @@ export default defineAdapter({
25
24
  });
26
25
  return new LineEndpoint({
27
26
  id: context.id,
28
- gateway: context.use(messageGatewayToken),
29
- sideEvents: context.use(sideEventGatewayToken),
30
27
  http: context.use(httpHostToken),
31
28
  config,
32
29
  });
package/adapters/line.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  * Convention entry: discover `adapters/line.ts` → defineAdapter.
3
3
  */
4
4
  import { defineAdapter } from 'zhin.js/adapter';
5
- import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
6
5
  import { httpHostToken } from '@zhin.js/host-http';
7
6
  import { LineEndpoint } from '../src/endpoint.js';
8
7
  import {
@@ -30,8 +29,6 @@ export default defineAdapter<LineAdapterConfig>({
30
29
  });
31
30
  return new LineEndpoint({
32
31
  id: context.id,
33
- gateway: context.use(messageGatewayToken),
34
- sideEvents: context.use(sideEventGatewayToken),
35
32
  http: context.use(httpHostToken),
36
33
  config,
37
34
  });
@@ -1,24 +1,16 @@
1
1
  import { defineAgentTool } from '@zhin.js/agent/tools';
2
2
  import { z } from 'zod';
3
- import { getLineApiConfig } from '../../src/line-agent-deps.js';
4
3
 
5
4
  export default defineAgentTool<{ groupId: string }>({
6
5
  description: 'Get LINE group member IDs',
6
+ adapter: 'line',
7
7
  inputSchema: z.object({
8
8
  groupId: z.string().min(1),
9
9
  }),
10
- async execute({ groupId }) {
10
+ async execute({ groupId }, context) {
11
11
  if (!groupId.startsWith('G')) {
12
12
  throw new Error(`Invalid groupId "${groupId}": must start with G`);
13
13
  }
14
- const { accessToken, apiBaseUrl } = getLineApiConfig();
15
- const response = await fetch(`${apiBaseUrl}/v2/bot/group/${groupId}/members/ids`, {
16
- headers: { Authorization: `Bearer ${accessToken}` },
17
- });
18
- if (!response.ok) {
19
- const errorText = await response.text();
20
- throw new Error(`LINE Group Members API error ${response.status}: ${errorText}`);
21
- }
22
- return await response.json();
14
+ return context.$client.getGroupMemberIds(groupId);
23
15
  },
24
16
  });
@@ -1,24 +1,16 @@
1
1
  import { defineAgentTool } from '@zhin.js/agent/tools';
2
2
  import { z } from 'zod';
3
- import { getLineApiConfig } from '../../src/line-agent-deps.js';
4
3
 
5
4
  export default defineAgentTool<{ userId: string }>({
6
5
  description: 'Get LINE user profile by userId',
6
+ adapter: 'line',
7
7
  inputSchema: z.object({
8
8
  userId: z.string().min(1),
9
9
  }),
10
- async execute({ userId }) {
10
+ async execute({ userId }, context) {
11
11
  if (!userId.startsWith('U')) {
12
12
  throw new Error(`Invalid userId "${userId}": must start with U`);
13
13
  }
14
- const { accessToken, apiBaseUrl } = getLineApiConfig();
15
- const response = await fetch(`${apiBaseUrl}/v2/profile/${userId}`, {
16
- headers: { Authorization: `Bearer ${accessToken}` },
17
- });
18
- if (!response.ok) {
19
- const errorText = await response.text();
20
- throw new Error(`LINE Profile API error ${response.status}: ${errorText}`);
21
- }
22
- return await response.json();
14
+ return context.$client.getProfile(userId);
23
15
  },
24
16
  });
@@ -0,0 +1,33 @@
1
+ import type { LineFetch } from './endpoint.js';
2
+ import type { LineEvent, ResolvedLineConfig } from './protocol.js';
3
+ export interface LineGroupMember {
4
+ readonly user_id: string;
5
+ readonly nickname: string;
6
+ }
7
+ /** Direct LINE Messaging API client; it has no Endpoint lifecycle methods. */
8
+ export declare class LineClient {
9
+ readonly config: ResolvedLineConfig;
10
+ readonly fetch: LineFetch;
11
+ constructor(config: ResolvedLineConfig, fetch: LineFetch);
12
+ request<T = unknown>(path: string, init?: {
13
+ readonly method?: string;
14
+ readonly body?: string;
15
+ readonly signal?: AbortSignal;
16
+ }): Promise<T>;
17
+ getProfile(userId: string): Promise<unknown>;
18
+ getGroupMemberIds(groupId: string, start?: string): Promise<{
19
+ readonly memberIds?: string[];
20
+ readonly next?: string;
21
+ }>;
22
+ getGroupMembers(groupId: string): Promise<LineGroupMember[]>;
23
+ }
24
+ export type LineClientEventMap = Record<string, LineEvent>;
25
+ declare module '@zhin.js/feature-kit' {
26
+ interface AdapterClientRegistry {
27
+ readonly line: {
28
+ readonly client: LineClient;
29
+ readonly events: LineClientEventMap;
30
+ };
31
+ }
32
+ }
33
+ export declare const lineClient: import("@zhin.js/adapter").EndpointClientToken<LineClient, LineClientEventMap>;
package/lib/client.js ADDED
@@ -0,0 +1,54 @@
1
+ import { defineEndpointClient } from 'zhin.js/adapter';
2
+ /** Direct LINE Messaging API client; it has no Endpoint lifecycle methods. */
3
+ export class LineClient {
4
+ config;
5
+ fetch;
6
+ constructor(config, fetch) {
7
+ this.config = config;
8
+ this.fetch = fetch;
9
+ }
10
+ async request(path, init = {}) {
11
+ const response = await this.fetch(`${this.config.apiBaseUrl}${path}`, {
12
+ method: init.method ?? 'GET',
13
+ headers: {
14
+ Authorization: `Bearer ${this.config.channelAccessToken}`,
15
+ ...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
16
+ },
17
+ ...(init.body === undefined ? {} : { body: init.body }),
18
+ signal: init.signal ?? AbortSignal.timeout(30_000),
19
+ });
20
+ if (!response.ok) {
21
+ const text = await response.text();
22
+ throw new Error(`LINE API error ${response.status}: ${text}`);
23
+ }
24
+ return await response.json();
25
+ }
26
+ getProfile(userId) {
27
+ return this.request(`/v2/profile/${encodeURIComponent(userId)}`);
28
+ }
29
+ getGroupMemberIds(groupId, start) {
30
+ const kind = groupId.startsWith('R') ? 'room' : 'group';
31
+ const query = start ? `?start=${encodeURIComponent(start)}` : '';
32
+ return this.request(`/v2/bot/${kind}/${encodeURIComponent(groupId)}/members/ids${query}`);
33
+ }
34
+ async getGroupMembers(groupId) {
35
+ const kind = groupId.startsWith('R') ? 'room' : 'group';
36
+ const ids = [];
37
+ let next;
38
+ do {
39
+ const page = await this.getGroupMemberIds(groupId, next);
40
+ ids.push(...(page.memberIds ?? []).filter((id) => typeof id === 'string' && id.length > 0));
41
+ next = page.next || undefined;
42
+ } while (next);
43
+ return Promise.all(ids.map(async (userId) => {
44
+ try {
45
+ const profile = await this.request(`/v2/bot/${kind}/${encodeURIComponent(groupId)}/member/${encodeURIComponent(userId)}`);
46
+ return { user_id: userId, nickname: String(profile.displayName ?? userId) };
47
+ }
48
+ catch {
49
+ return { user_id: userId, nickname: userId };
50
+ }
51
+ }));
52
+ }
53
+ }
54
+ export const lineClient = defineEndpointClient('line');
package/lib/endpoint.d.ts CHANGED
@@ -1,11 +1,12 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * LineEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
3
4
  */
4
- import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
5
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
5
+ import { type EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
6
6
  import type { HttpHost } from '@zhin.js/host-http';
7
7
  import type { CapabilityId } from 'zhin.js';
8
8
  import { type LineEvent, type ResolvedLineConfig } from './protocol.js';
9
+ import { LineClient } from './client.js';
9
10
  export type LineFetch = (url: string, init?: {
10
11
  readonly method?: string;
11
12
  readonly headers?: Record<string, string>;
@@ -19,23 +20,18 @@ export type LineFetch = (url: string, init?: {
19
20
  }>;
20
21
  export interface LineEndpointOptions {
21
22
  readonly id: CapabilityId;
22
- readonly gateway: MessageGateway;
23
- readonly sideEvents?: SideEventGateway;
24
23
  readonly http: HttpHost;
25
24
  readonly config: ResolvedLineConfig;
26
25
  readonly fetch?: LineFetch;
27
26
  }
28
- export declare class LineEndpoint implements EndpointInstance {
27
+ export declare class LineEndpoint extends Endpoint<LineClient> {
29
28
  #private;
29
+ readonly client: LineClient;
30
30
  readonly management: EndpointManagement;
31
31
  constructor(options: LineEndpointOptions);
32
32
  /** Used by webhook handler. */
33
33
  get isOpen(): boolean;
34
34
  get config(): ResolvedLineConfig;
35
- getApiConfig(): {
36
- accessToken: string;
37
- apiBaseUrl: string;
38
- };
39
35
  start(): Promise<void>;
40
36
  open(): void;
41
37
  close(): void;
@@ -43,14 +39,4 @@ export declare class LineEndpoint implements EndpointInstance {
43
39
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
44
40
  /** Test / internal: admit a parsed event when open (non-webhook path). */
45
41
  admit(event: LineEvent): void;
46
- /**
47
- * group/room 成员列表:Bot API 无群列表,只能按已知 groupId/roomId 拉成员。
48
- * members/ids 分页(next continuation token)后逐个取 profile 归一 nickname;
49
- * 单个 profile 失败(用户已退群等)时回退 userId 占位,不拖垮整批。
50
- */
51
- getGroupMembers(groupId: string): Promise<LineGroupMember[]>;
52
- }
53
- export interface LineGroupMember {
54
- readonly user_id: string;
55
- readonly nickname: string;
56
42
  }
package/lib/endpoint.js CHANGED
@@ -1,13 +1,15 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
2
- import { registerLineAgentEndpoint } from './line-agent-deps.js';
3
3
  import { formatInboundContent, formatOutboundMessages, generateMessageId, isLineLifecycleEvent, isMessageEvent, isValidLineRecipientId, lineInboundConversation, } from './protocol.js';
4
4
  import { registerLineWebhookRoutes } from './webhook.js';
5
5
  import { receiveLineSideEvent } from './side-event-dispatch.js';
6
+ import { LineClient } from './client.js';
6
7
  /** LINE replyToken 有效期短,过期后 reply 必 400;缓存带时间戳,超时弃用改走 push。 */
7
8
  const REPLY_TOKEN_TTL_MS = 60_000;
8
9
  /** 出站 HTTP 调用统一 30s 超时。 */
9
10
  const OUTBOUND_TIMEOUT_MS = 30_000;
10
- export class LineEndpoint {
11
+ export class LineEndpoint extends Endpoint {
12
+ client;
11
13
  #logger;
12
14
  #options;
13
15
  #fetch;
@@ -15,12 +17,13 @@ export class LineEndpoint {
15
17
  #replyTokenCache = new Map();
16
18
  #open = false;
17
19
  #started = false;
18
- #unregisterAgent;
19
20
  management = createLineEndpointManagement(this);
20
21
  constructor(options) {
22
+ super();
21
23
  this.#logger = getAdapterLogger('line', options.config.id);
22
24
  this.#options = options;
23
25
  this.#fetch = options.fetch ?? globalThis.fetch;
26
+ this.client = new LineClient(options.config, this.#fetch);
24
27
  }
25
28
  /** Used by webhook handler. */
26
29
  get isOpen() {
@@ -29,18 +32,11 @@ export class LineEndpoint {
29
32
  get config() {
30
33
  return this.#options.config;
31
34
  }
32
- getApiConfig() {
33
- return {
34
- accessToken: this.#options.config.channelAccessToken,
35
- apiBaseUrl: this.#options.config.apiBaseUrl,
36
- };
37
- }
38
35
  async start() {
39
36
  if (this.#started)
40
37
  return;
41
38
  this.#started = true;
42
39
  try {
43
- this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.id, this);
44
40
  this.#routeReleases.push(...registerLineWebhookRoutes(this.#options.http, this));
45
41
  this.#logger.debug(formatCompact({
46
42
  endpoint: this.#options.config.id,
@@ -65,8 +61,6 @@ export class LineEndpoint {
65
61
  this.#replyTokenCache.clear();
66
62
  for (const release of this.#routeReleases.splice(0))
67
63
  release();
68
- this.#unregisterAgent?.();
69
- this.#unregisterAgent = undefined;
70
64
  this.#started = false;
71
65
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
72
66
  }
@@ -105,15 +99,22 @@ export class LineEndpoint {
105
99
  admit(event) {
106
100
  if (!this.#open)
107
101
  return;
102
+ void this.emitPlatform(event.type || 'event', event).catch((error) => {
103
+ this.#logger.warn(formatCompact({
104
+ op: 'line_platform_event_failed',
105
+ event: event.type,
106
+ error: error instanceof Error ? error.message : String(error),
107
+ }));
108
+ });
108
109
  if (isLineLifecycleEvent(event)) {
109
- receiveLineSideEvent(this.#options.sideEvents, String(this.#options.id), this.#options.config.id, event, this.#logger);
110
+ receiveLineSideEvent((name, payload) => this.emit(name, payload), String(this.#options.id), this.#options.config.id, event, this.#logger);
110
111
  return;
111
112
  }
112
113
  const conversation = lineInboundConversation(String(this.#options.id), event.source);
113
114
  if ('replyToken' in event && typeof event.replyToken === 'string') {
114
115
  this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
115
116
  }
116
- void this.#options.gateway.receive({
117
+ void this.emit('message.receive', {
117
118
  conversation,
118
119
  message: { conversation, id: generateMessageId(event) },
119
120
  content: formatInboundContent(event),
@@ -171,50 +172,10 @@ export class LineEndpoint {
171
172
  const result = await response.json();
172
173
  return result.sentMessages?.[0]?.id || `push-${Date.now()}`;
173
174
  }
174
- /**
175
- * group/room 成员列表:Bot API 无群列表,只能按已知 groupId/roomId 拉成员。
176
- * members/ids 分页(next continuation token)后逐个取 profile 归一 nickname;
177
- * 单个 profile 失败(用户已退群等)时回退 userId 占位,不拖垮整批。
178
- */
179
- async getGroupMembers(groupId) {
180
- const kind = groupId.startsWith('R') ? 'room' : 'group';
181
- const memberIds = [];
182
- let start;
183
- do {
184
- const query = start ? `?start=${encodeURIComponent(start)}` : '';
185
- const data = await this.#get(`${this.#options.config.apiBaseUrl}/v2/bot/${kind}/${encodeURIComponent(groupId)}/members/ids${query}`);
186
- for (const id of data.memberIds ?? []) {
187
- if (typeof id === 'string' && id)
188
- memberIds.push(id);
189
- }
190
- start = data.next || undefined;
191
- } while (start);
192
- return Promise.all(memberIds.map(async (userId) => {
193
- try {
194
- const profile = await this.#get(`${this.#options.config.apiBaseUrl}/v2/bot/${kind}/${encodeURIComponent(groupId)}/member/${encodeURIComponent(userId)}`);
195
- return { user_id: userId, nickname: String(profile.displayName ?? userId) };
196
- }
197
- catch {
198
- return { user_id: userId, nickname: userId };
199
- }
200
- }));
201
- }
202
- async #get(url) {
203
- const response = await this.#fetch(url, {
204
- method: 'GET',
205
- headers: { Authorization: `Bearer ${this.#options.config.channelAccessToken}` },
206
- signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
207
- });
208
- if (!response.ok) {
209
- const errorText = await response.text();
210
- throw new Error(`LINE API error ${response.status}: ${errorText}`);
211
- }
212
- return response.json();
213
- }
214
175
  }
215
176
  function createLineEndpointManagement(endpoint) {
216
177
  return Object.freeze({
217
178
  // listGroups 不接:LINE Bot API 没有"我加入了哪些群"的接口,群 id 只能来自入站事件。
218
- listGroupMembers: (groupId) => endpoint.getGroupMembers(groupId),
179
+ listGroupMembers: (groupId) => endpoint.client.getGroupMembers(groupId),
219
180
  });
220
181
  }
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
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
2
  export { LineEndpoint, type LineEndpointOptions, type LineFetch, } from './endpoint.js';
3
3
  export { registerLineWebhookRoutes, handleLineWebhookRequest, type LineWebhookHandler, } from './webhook.js';
4
- export { getLineAgentDeps, getLineApiConfig, registerLineAgentEndpoint, setLineAgentDeps, type LineAgentDeps, type LineAgentEndpoint, } from './line-agent-deps.js';
4
+ export { LineClient, lineClient, type LineClientEventMap, type LineGroupMember, } from './client.js';
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isPostbackEvent, isValidLineRecipientId, normalizeWebhookPath, readTextBody, resolveChannel, resolveLineConfig, verifySignature, } from './protocol.js';
2
2
  export { LineEndpoint, } from './endpoint.js';
3
3
  export { registerLineWebhookRoutes, handleLineWebhookRequest, } from './webhook.js';
4
- export { getLineAgentDeps, getLineApiConfig, registerLineAgentEndpoint, setLineAgentDeps, } from './line-agent-deps.js';
4
+ export { LineClient, lineClient, } from './client.js';
@@ -1 +1 @@
1
- export declare const lineEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
1
+ export declare const lineEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
package/lib/protocol.d.ts CHANGED
@@ -144,7 +144,7 @@ export declare function resolveChannel(source: LineSource): LineChannel;
144
144
  */
145
145
  export declare function lineInboundConversation(endpointKey: string, source: LineSource): ConversationRef;
146
146
  export declare function generateMessageId(event: LineEvent): string;
147
- /** Build inbound text for MessageGateway.receive. */
147
+ /** Build inbound text for OutboundMessageService.receive. */
148
148
  export declare function formatInboundContent(event: LineEvent): string;
149
149
  export declare function verifySignature(channelSecret: string, body: string, signature: string): boolean;
150
150
  export declare function isValidLineRecipientId(id: string): boolean;
package/lib/protocol.js CHANGED
@@ -77,7 +77,7 @@ export function generateMessageId(event) {
77
77
  return event.message.id;
78
78
  return `${event.type}-${event.timestamp}`;
79
79
  }
80
- /** Build inbound text for MessageGateway.receive. */
80
+ /** Build inbound text for OutboundMessageService.receive. */
81
81
  export function formatInboundContent(event) {
82
82
  if (isMessageEvent(event)) {
83
83
  const msg = event.message;
@@ -1,4 +1,4 @@
1
- import type { SideEventGateway } from '@zhin.js/core/runtime';
1
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
2
2
  import { type getAdapterLogger } from '@zhin.js/logger';
3
3
  import { type LineEvent } from './protocol.js';
4
- export declare function receiveLineSideEvent(sideEvents: SideEventGateway | undefined, endpointKey: string, configId: string, event: LineEvent, logger: ReturnType<typeof getAdapterLogger>): void;
4
+ export declare function receiveLineSideEvent(emit: EndpointEventEmitter, endpointKey: string, configId: string, event: LineEvent, logger: ReturnType<typeof getAdapterLogger>): void;
@@ -15,13 +15,13 @@ function mapLineLifecycleParts(type) {
15
15
  return { scene_type: 'line', sub_type: type };
16
16
  }
17
17
  }
18
- export function receiveLineSideEvent(sideEvents, endpointKey, configId, event, logger) {
19
- if (!sideEvents || !isLineLifecycleEvent(event))
18
+ export function receiveLineSideEvent(emit, endpointKey, configId, event, logger) {
19
+ if (!isLineLifecycleEvent(event))
20
20
  return;
21
21
  const parts = mapLineLifecycleParts(event.type);
22
22
  const conversation = lineInboundConversation(endpointKey, event.source);
23
23
  const userId = event.source.userId || conversation.id;
24
- void sideEvents.receiveNotice(buildNotice(event, {
24
+ void emit('notice.receive', buildNotice(event, {
25
25
  $id: `line:${event.type}:${event.timestamp}:${userId}`,
26
26
  $adapter: 'line',
27
27
  $endpoint: configId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-line",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
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,20 +32,21 @@
32
32
  "directory": "plugins/adapters/line"
33
33
  },
34
34
  "dependencies": {
35
- "@zhin.js/adapter": "1.2.0",
36
- "@zhin.js/core": "1.5.13",
37
- "@zhin.js/host-http": "1.0.12",
35
+ "@zhin.js/adapter": "1.2.1",
36
+ "@zhin.js/core": "1.5.14",
37
+ "@zhin.js/feature-kit": "1.0.13",
38
+ "@zhin.js/host-http": "1.0.13",
38
39
  "@zhin.js/im-contract": "1.0.4",
39
- "@zhin.js/logger": "1.0.76"
40
+ "@zhin.js/logger": "1.0.77"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "zod": "^4.0.0",
43
- "@zhin.js/adapter": "1.2.0",
44
- "@zhin.js/agent": "1.1.15",
45
- "@zhin.js/command": "1.0.15",
46
- "@zhin.js/core": "1.5.13",
47
- "@zhin.js/host-http": "1.0.12",
48
- "zhin.js": "6.0.13"
44
+ "@zhin.js/adapter": "1.2.1",
45
+ "@zhin.js/agent": "1.1.16",
46
+ "@zhin.js/command": "1.0.16",
47
+ "@zhin.js/core": "1.5.14",
48
+ "@zhin.js/host-http": "1.0.13",
49
+ "zhin.js": "6.0.14"
49
50
  },
50
51
  "peerDependenciesMeta": {
51
52
  "@zhin.js/agent": {
@@ -66,8 +67,8 @@
66
67
  "typescript": "^6.0.3",
67
68
  "vitest": "^4.1.10",
68
69
  "zod": "^4.4.3",
69
- "@zhin.js/agent": "1.1.15",
70
- "zhin.js": "6.0.13"
70
+ "@zhin.js/agent": "1.1.16",
71
+ "zhin.js": "6.0.14"
71
72
  },
72
73
  "files": [
73
74
  "adapters",
package/src/client.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { LineFetch } from './endpoint.js';
2
+ import { defineEndpointClient } from 'zhin.js/adapter';
3
+ import type { LineEvent, ResolvedLineConfig } from './protocol.js';
4
+
5
+ export interface LineGroupMember {
6
+ readonly user_id: string;
7
+ readonly nickname: string;
8
+ }
9
+
10
+ /** Direct LINE Messaging API client; it has no Endpoint lifecycle methods. */
11
+ export class LineClient {
12
+ constructor(
13
+ readonly config: ResolvedLineConfig,
14
+ readonly fetch: LineFetch,
15
+ ) {}
16
+
17
+ async request<T = unknown>(path: string, init: {
18
+ readonly method?: string;
19
+ readonly body?: string;
20
+ readonly signal?: AbortSignal;
21
+ } = {}): Promise<T> {
22
+ const response = await this.fetch(`${this.config.apiBaseUrl}${path}`, {
23
+ method: init.method ?? 'GET',
24
+ headers: {
25
+ Authorization: `Bearer ${this.config.channelAccessToken}`,
26
+ ...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
27
+ },
28
+ ...(init.body === undefined ? {} : { body: init.body }),
29
+ signal: init.signal ?? AbortSignal.timeout(30_000),
30
+ });
31
+ if (!response.ok) {
32
+ const text = await response.text();
33
+ throw new Error(`LINE API error ${response.status}: ${text}`);
34
+ }
35
+ return await response.json() as T;
36
+ }
37
+
38
+ getProfile(userId: string): Promise<unknown> {
39
+ return this.request(`/v2/profile/${encodeURIComponent(userId)}`);
40
+ }
41
+
42
+ getGroupMemberIds(groupId: string, start?: string): Promise<{
43
+ readonly memberIds?: string[];
44
+ readonly next?: string;
45
+ }> {
46
+ const kind = groupId.startsWith('R') ? 'room' : 'group';
47
+ const query = start ? `?start=${encodeURIComponent(start)}` : '';
48
+ return this.request(
49
+ `/v2/bot/${kind}/${encodeURIComponent(groupId)}/members/ids${query}`,
50
+ );
51
+ }
52
+
53
+ async getGroupMembers(groupId: string): Promise<LineGroupMember[]> {
54
+ const kind = groupId.startsWith('R') ? 'room' : 'group';
55
+ const ids: string[] = [];
56
+ let next: string | undefined;
57
+ do {
58
+ const page = await this.getGroupMemberIds(groupId, next);
59
+ ids.push(...(page.memberIds ?? []).filter((id) => typeof id === 'string' && id.length > 0));
60
+ next = page.next || undefined;
61
+ } while (next);
62
+
63
+ return Promise.all(ids.map(async (userId) => {
64
+ try {
65
+ const profile = await this.request<{ displayName?: string }>(
66
+ `/v2/bot/${kind}/${encodeURIComponent(groupId)}/member/${encodeURIComponent(userId)}`,
67
+ );
68
+ return { user_id: userId, nickname: String(profile.displayName ?? userId) };
69
+ } catch {
70
+ return { user_id: userId, nickname: userId };
71
+ }
72
+ }));
73
+ }
74
+ }
75
+
76
+ export type LineClientEventMap = Record<string, LineEvent>;
77
+
78
+ declare module '@zhin.js/feature-kit' {
79
+ interface AdapterClientRegistry {
80
+ readonly line: {
81
+ readonly client: LineClient;
82
+ readonly events: LineClientEventMap;
83
+ };
84
+ }
85
+ }
86
+
87
+ export const lineClient = defineEndpointClient<LineClient, LineClientEventMap>('line');
package/src/endpoint.ts CHANGED
@@ -1,12 +1,11 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * LineEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
3
4
  */
4
- import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
5
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
5
+ import { type EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
6
6
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
7
7
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
8
  import type { CapabilityId } from 'zhin.js';
9
- import { registerLineAgentEndpoint } from './line-agent-deps.js';
10
9
  import {
11
10
  formatInboundContent,
12
11
  formatOutboundMessages,
@@ -21,6 +20,7 @@ import {
21
20
  } from './protocol.js';
22
21
  import { registerLineWebhookRoutes } from './webhook.js';
23
22
  import { receiveLineSideEvent } from './side-event-dispatch.js';
23
+ import { LineClient } from './client.js';
24
24
 
25
25
  /** LINE replyToken 有效期短,过期后 reply 必 400;缓存带时间戳,超时弃用改走 push。 */
26
26
  const REPLY_TOKEN_TTL_MS = 60_000;
@@ -44,14 +44,13 @@ export type LineFetch = (
44
44
 
45
45
  export interface LineEndpointOptions {
46
46
  readonly id: CapabilityId;
47
- readonly gateway: MessageGateway;
48
- readonly sideEvents?: SideEventGateway;
49
47
  readonly http: HttpHost;
50
48
  readonly config: ResolvedLineConfig;
51
49
  readonly fetch?: LineFetch;
52
50
  }
53
51
 
54
- export class LineEndpoint implements EndpointInstance {
52
+ export class LineEndpoint extends Endpoint<LineClient> {
53
+ readonly client: LineClient;
55
54
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
56
55
 
57
56
  readonly #options: LineEndpointOptions;
@@ -60,13 +59,14 @@ export class LineEndpoint implements EndpointInstance {
60
59
  #replyTokenCache = new Map<string, { token: string; timestamp: number }>();
61
60
  #open = false;
62
61
  #started = false;
63
- #unregisterAgent?: () => void;
64
62
  readonly management: EndpointManagement = createLineEndpointManagement(this);
65
63
 
66
64
  constructor(options: LineEndpointOptions) {
65
+ super();
67
66
  this.#logger = getAdapterLogger('line', options.config.id);
68
67
  this.#options = options;
69
68
  this.#fetch = options.fetch ?? globalThis.fetch;
69
+ this.client = new LineClient(options.config, this.#fetch);
70
70
  }
71
71
 
72
72
  /** Used by webhook handler. */
@@ -78,18 +78,10 @@ export class LineEndpoint implements EndpointInstance {
78
78
  return this.#options.config;
79
79
  }
80
80
 
81
- getApiConfig(): { accessToken: string; apiBaseUrl: string } {
82
- return {
83
- accessToken: this.#options.config.channelAccessToken,
84
- apiBaseUrl: this.#options.config.apiBaseUrl,
85
- };
86
- }
87
-
88
81
  async start(): Promise<void> {
89
82
  if (this.#started) return;
90
83
  this.#started = true;
91
84
  try {
92
- this.#unregisterAgent = registerLineAgentEndpoint(this.#options.config.id, this);
93
85
  this.#routeReleases.push(...registerLineWebhookRoutes(this.#options.http, this));
94
86
  this.#logger.debug(formatCompact({
95
87
  endpoint: this.#options.config.id,
@@ -115,8 +107,6 @@ export class LineEndpoint implements EndpointInstance {
115
107
  this.#open = false;
116
108
  this.#replyTokenCache.clear();
117
109
  for (const release of this.#routeReleases.splice(0)) release();
118
- this.#unregisterAgent?.();
119
- this.#unregisterAgent = undefined;
120
110
  this.#started = false;
121
111
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
122
112
  }
@@ -158,9 +148,16 @@ export class LineEndpoint implements EndpointInstance {
158
148
  /** Test / internal: admit a parsed event when open (non-webhook path). */
159
149
  admit(event: LineEvent): void {
160
150
  if (!this.#open) return;
151
+ void this.emitPlatform(event.type || 'event', event).catch((error) => {
152
+ this.#logger.warn(formatCompact({
153
+ op: 'line_platform_event_failed',
154
+ event: event.type,
155
+ error: error instanceof Error ? error.message : String(error),
156
+ }));
157
+ });
161
158
  if (isLineLifecycleEvent(event)) {
162
159
  receiveLineSideEvent(
163
- this.#options.sideEvents,
160
+ (name, payload) => this.emit(name, payload),
164
161
  String(this.#options.id),
165
162
  this.#options.config.id,
166
163
  event,
@@ -172,7 +169,7 @@ export class LineEndpoint implements EndpointInstance {
172
169
  if ('replyToken' in event && typeof event.replyToken === 'string') {
173
170
  this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
174
171
  }
175
- void this.#options.gateway.receive({
172
+ void this.emit('message.receive', {
176
173
  conversation,
177
174
  message: { conversation, id: generateMessageId(event) },
178
175
  content: formatInboundContent(event),
@@ -244,55 +241,11 @@ export class LineEndpoint implements EndpointInstance {
244
241
  * members/ids 分页(next continuation token)后逐个取 profile 归一 nickname;
245
242
  * 单个 profile 失败(用户已退群等)时回退 userId 占位,不拖垮整批。
246
243
  */
247
- async getGroupMembers(groupId: string): Promise<LineGroupMember[]> {
248
- const kind = groupId.startsWith('R') ? 'room' : 'group';
249
- const memberIds: string[] = [];
250
- let start: string | undefined;
251
- do {
252
- const query = start ? `?start=${encodeURIComponent(start)}` : '';
253
- const data = await this.#get(
254
- `${this.#options.config.apiBaseUrl}/v2/bot/${kind}/${encodeURIComponent(groupId)}/members/ids${query}`,
255
- ) as { memberIds?: string[]; next?: string };
256
- for (const id of data.memberIds ?? []) {
257
- if (typeof id === 'string' && id) memberIds.push(id);
258
- }
259
- start = data.next || undefined;
260
- } while (start);
261
-
262
- return Promise.all(memberIds.map(async (userId) => {
263
- try {
264
- const profile = await this.#get(
265
- `${this.#options.config.apiBaseUrl}/v2/bot/${kind}/${encodeURIComponent(groupId)}/member/${encodeURIComponent(userId)}`,
266
- ) as { displayName?: string };
267
- return { user_id: userId, nickname: String(profile.displayName ?? userId) };
268
- } catch {
269
- return { user_id: userId, nickname: userId };
270
- }
271
- }));
272
- }
273
-
274
- async #get(url: string): Promise<unknown> {
275
- const response = await this.#fetch(url, {
276
- method: 'GET',
277
- headers: { Authorization: `Bearer ${this.#options.config.channelAccessToken}` },
278
- signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
279
- });
280
- if (!response.ok) {
281
- const errorText = await response.text();
282
- throw new Error(`LINE API error ${response.status}: ${errorText}`);
283
- }
284
- return response.json();
285
- }
286
- }
287
-
288
- export interface LineGroupMember {
289
- readonly user_id: string;
290
- readonly nickname: string;
291
244
  }
292
245
 
293
246
  function createLineEndpointManagement(endpoint: LineEndpoint): EndpointManagement {
294
247
  return Object.freeze<EndpointManagement>({
295
248
  // listGroups 不接:LINE Bot API 没有"我加入了哪些群"的接口,群 id 只能来自入站事件。
296
- listGroupMembers: (groupId) => endpoint.getGroupMembers(groupId),
249
+ listGroupMembers: (groupId) => endpoint.client.getGroupMembers(groupId),
297
250
  });
298
251
  }
package/src/index.ts CHANGED
@@ -44,10 +44,8 @@ export {
44
44
  } from './webhook.js';
45
45
 
46
46
  export {
47
- getLineAgentDeps,
48
- getLineApiConfig,
49
- registerLineAgentEndpoint,
50
- setLineAgentDeps,
51
- type LineAgentDeps,
52
- type LineAgentEndpoint,
53
- } from './line-agent-deps.js';
47
+ LineClient,
48
+ lineClient,
49
+ type LineClientEventMap,
50
+ type LineGroupMember,
51
+ } from './client.js';
package/src/protocol.ts CHANGED
@@ -243,7 +243,7 @@ export function generateMessageId(event: LineEvent): string {
243
243
  return `${event.type}-${event.timestamp}`;
244
244
  }
245
245
 
246
- /** Build inbound text for MessageGateway.receive. */
246
+ /** Build inbound text for OutboundMessageService.receive. */
247
247
  export function formatInboundContent(event: LineEvent): string {
248
248
  if (isMessageEvent(event)) {
249
249
  const msg = event.message;
@@ -1,5 +1,5 @@
1
1
  import { buildNotice, senderFromId } from '@zhin.js/core';
2
- import type { SideEventGateway } from '@zhin.js/core/runtime';
2
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
3
3
  import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
4
4
  import {
5
5
  isLineLifecycleEvent,
@@ -23,17 +23,17 @@ function mapLineLifecycleParts(type: LineEvent['type']): { scene_type: string; s
23
23
  }
24
24
 
25
25
  export function receiveLineSideEvent(
26
- sideEvents: SideEventGateway | undefined,
26
+ emit: EndpointEventEmitter,
27
27
  endpointKey: string,
28
28
  configId: string,
29
29
  event: LineEvent,
30
30
  logger: ReturnType<typeof getAdapterLogger>,
31
31
  ): void {
32
- if (!sideEvents || !isLineLifecycleEvent(event)) return;
32
+ if (!isLineLifecycleEvent(event)) return;
33
33
  const parts = mapLineLifecycleParts(event.type);
34
34
  const conversation = lineInboundConversation(endpointKey, event.source);
35
35
  const userId = event.source.userId || conversation.id;
36
- void sideEvents.receiveNotice(buildNotice(event, {
36
+ void emit('notice.receive', buildNotice(event, {
37
37
  $id: `line:${event.type}:${event.timestamp}:${userId}`,
38
38
  $adapter: 'line' as never,
39
39
  $endpoint: configId,
@@ -1,24 +0,0 @@
1
- /**
2
- * Agent tool deps for line (get_profile / get_group_members).
3
- * Endpoints register themselves on start; tools look up the active API config.
4
- */
5
- export interface LineAgentEndpoint {
6
- getApiConfig(): {
7
- accessToken: string;
8
- apiBaseUrl: string;
9
- };
10
- }
11
- export interface LineAgentDeps {
12
- getApiConfig: () => {
13
- accessToken: string;
14
- apiBaseUrl: string;
15
- };
16
- }
17
- export declare function registerLineAgentEndpoint(endpointKey: string, endpoint: LineAgentEndpoint): () => void;
18
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
19
- export declare function setLineAgentDeps(deps: LineAgentDeps | null): void;
20
- export declare function getLineAgentDeps(): LineAgentDeps;
21
- export declare function getLineApiConfig(): {
22
- accessToken: string;
23
- apiBaseUrl: string;
24
- };
@@ -1,33 +0,0 @@
1
- /**
2
- * Agent tool deps for line (get_profile / get_group_members).
3
- * Endpoints register themselves on start; tools look up the active API config.
4
- */
5
- const endpoints = new Map();
6
- let override = null;
7
- export function registerLineAgentEndpoint(endpointKey, endpoint) {
8
- endpoints.set(endpointKey, endpoint);
9
- return () => {
10
- if (endpoints.get(endpointKey) === endpoint) {
11
- endpoints.delete(endpointKey);
12
- }
13
- };
14
- }
15
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
- export function setLineAgentDeps(deps) {
17
- override = deps;
18
- }
19
- export function getLineAgentDeps() {
20
- if (override)
21
- return override;
22
- return {
23
- getApiConfig() {
24
- const first = endpoints.values().next().value;
25
- if (!first)
26
- throw new Error('LINE channel access token not configured');
27
- return first.getApiConfig();
28
- },
29
- };
30
- }
31
- export function getLineApiConfig() {
32
- return getLineAgentDeps().getApiConfig();
33
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Agent tool deps for line (get_profile / get_group_members).
3
- * Endpoints register themselves on start; tools look up the active API config.
4
- */
5
-
6
- export interface LineAgentEndpoint {
7
- getApiConfig(): { accessToken: string; apiBaseUrl: string };
8
- }
9
-
10
- export interface LineAgentDeps {
11
- getApiConfig: () => { accessToken: string; apiBaseUrl: string };
12
- }
13
-
14
- const endpoints = new Map<string, LineAgentEndpoint>();
15
- let override: LineAgentDeps | null = null;
16
-
17
- export function registerLineAgentEndpoint(
18
- endpointKey: string,
19
- endpoint: LineAgentEndpoint,
20
- ): () => void {
21
- endpoints.set(endpointKey, endpoint);
22
- return () => {
23
- if (endpoints.get(endpointKey) === endpoint) {
24
- endpoints.delete(endpointKey);
25
- }
26
- };
27
- }
28
-
29
- /** Optional override used by tests / transitional callers. Pass `null` to clear. */
30
- export function setLineAgentDeps(deps: LineAgentDeps | null): void {
31
- override = deps;
32
- }
33
-
34
- export function getLineAgentDeps(): LineAgentDeps {
35
- if (override) return override;
36
- return {
37
- getApiConfig() {
38
- const first = endpoints.values().next().value as LineAgentEndpoint | undefined;
39
- if (!first) throw new Error('LINE channel access token not configured');
40
- return first.getApiConfig();
41
- },
42
- };
43
- }
44
-
45
- export function getLineApiConfig(): { accessToken: string; apiBaseUrl: string } {
46
- return getLineAgentDeps().getApiConfig();
47
- }