@zhin.js/adapter-line 5.0.0 → 5.0.2
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 +29 -0
- package/README.md +1 -1
- package/adapters/line.js +0 -3
- package/adapters/line.ts +0 -3
- package/agent/tools/get_group_members.ts +3 -11
- package/agent/tools/get_profile.ts +3 -11
- package/lib/client.d.ts +33 -0
- package/lib/client.js +54 -0
- package/lib/endpoint.d.ts +5 -19
- package/lib/endpoint.js +16 -55
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/line-endpoint-commands.d.ts +1 -1
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +1 -1
- package/lib/side-event-dispatch.d.ts +2 -2
- package/lib/side-event-dispatch.js +3 -3
- package/package.json +14 -13
- package/src/client.ts +87 -0
- package/src/endpoint.ts +17 -64
- package/src/index.ts +5 -7
- package/src/protocol.ts +1 -1
- package/src/side-event-dispatch.ts +4 -4
- package/lib/line-agent-deps.d.ts +0 -24
- package/lib/line-agent-deps.js +0 -33
- package/src/line-agent-deps.ts +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @zhin.js/adapter-line
|
|
2
2
|
|
|
3
|
+
## 5.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [736fa04]
|
|
8
|
+
- @zhin.js/agent@1.1.17
|
|
9
|
+
- zhin.js@6.0.14
|
|
10
|
+
|
|
11
|
+
## 5.0.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- Updated dependencies [e9c6a73]
|
|
17
|
+
- Updated dependencies [4e8117c]
|
|
18
|
+
- Updated dependencies [902fa35]
|
|
19
|
+
- Updated dependencies [54bfd6b]
|
|
20
|
+
- Updated dependencies [12025ee]
|
|
21
|
+
- Updated dependencies [09b14d6]
|
|
22
|
+
- Updated dependencies [1fc78bc]
|
|
23
|
+
- @zhin.js/agent@1.1.16
|
|
24
|
+
- @zhin.js/adapter@1.2.1
|
|
25
|
+
- @zhin.js/core@1.5.14
|
|
26
|
+
- @zhin.js/host-http@1.0.13
|
|
27
|
+
- @zhin.js/command@1.0.16
|
|
28
|
+
- @zhin.js/logger@1.0.77
|
|
29
|
+
- zhin.js@6.0.14
|
|
30
|
+
- @zhin.js/feature-kit@1.0.13
|
|
31
|
+
|
|
3
32
|
## 5.0.0
|
|
4
33
|
|
|
5
34
|
### 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` — `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/lib/client.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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(
|
|
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(
|
|
19
|
-
if (!
|
|
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
|
|
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.
|
|
3
|
+
"version": "5.0.2",
|
|
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.
|
|
36
|
-
"@zhin.js/core": "1.5.
|
|
37
|
-
"@zhin.js/
|
|
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.
|
|
40
|
+
"@zhin.js/logger": "1.0.77"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"zod": "^4.0.0",
|
|
43
|
-
"@zhin.js/adapter": "1.2.
|
|
44
|
-
"@zhin.js/agent": "1.1.
|
|
45
|
-
"@zhin.js/command": "1.0.
|
|
46
|
-
"@zhin.js/core": "1.5.
|
|
47
|
-
"@zhin.js/host-http": "1.0.
|
|
48
|
-
"zhin.js": "6.0.
|
|
44
|
+
"@zhin.js/adapter": "1.2.1",
|
|
45
|
+
"@zhin.js/agent": "1.1.17",
|
|
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.
|
|
70
|
-
"zhin.js": "6.0.
|
|
70
|
+
"@zhin.js/agent": "1.1.17",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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,
|
package/lib/line-agent-deps.d.ts
DELETED
|
@@ -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
|
-
};
|
package/lib/line-agent-deps.js
DELETED
|
@@ -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
|
-
}
|
package/src/line-agent-deps.ts
DELETED
|
@@ -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
|
-
}
|