@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 +86 -0
- package/README.md +2 -2
- package/adapters/line.js +2 -2
- package/adapters/line.ts +2 -2
- package/lib/endpoint.d.ts +2 -5
- package/lib/endpoint.js +22 -20
- package/lib/line-agent-deps.d.ts +1 -1
- package/lib/line-agent-deps.js +4 -4
- package/lib/protocol.d.ts +8 -2
- package/lib/protocol.js +15 -3
- package/package.json +15 -14
- package/schema.json +41 -5
- package/src/endpoint.ts +24 -22
- package/src/line-agent-deps.ts +4 -4
- package/src/protocol.ts +19 -5
- /package/commands/endpoint/add/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/add/{[name:string].ts → [id].ts} +0 -0
- /package/commands/endpoint/remove/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name:string].ts → [id].ts} +0 -0
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({
|
|
28
|
-
出站:`send({
|
|
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.
|
|
23
|
-
|
|
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.
|
|
28
|
-
|
|
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({
|
|
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,
|
|
1
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
2
2
|
import { registerLineAgentEndpoint } from './line-agent-deps.js';
|
|
3
|
-
import { formatInboundContent, formatOutboundMessages, generateMessageId, isMessageEvent, isValidLineRecipientId,
|
|
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.
|
|
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.
|
|
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'
|
|
70
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
70
71
|
}
|
|
71
|
-
async send({
|
|
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.
|
|
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
|
|
107
|
+
const conversation = lineInboundConversation(String(this.#options.id), event.source);
|
|
105
108
|
if ('replyToken' in event && typeof event.replyToken === 'string') {
|
|
106
|
-
this.#replyTokenCache.set(
|
|
109
|
+
this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
|
|
107
110
|
}
|
|
108
111
|
void this.#options.gateway.receive({
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
conversation,
|
|
113
|
+
message: { conversation, id: generateMessageId(event) },
|
|
111
114
|
content: formatInboundContent(event),
|
|
112
|
-
sender: event.source.userId ||
|
|
113
|
-
|
|
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:
|
|
126
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
125
127
|
error: err instanceof Error ? err.message : String(err),
|
|
126
128
|
}));
|
|
127
129
|
});
|
package/lib/line-agent-deps.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface LineAgentDeps {
|
|
|
14
14
|
apiBaseUrl: string;
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
export declare function registerLineAgentEndpoint(
|
|
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;
|
package/lib/line-agent-deps.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const endpoints = new Map();
|
|
6
6
|
let override = null;
|
|
7
|
-
export function registerLineAgentEndpoint(
|
|
8
|
-
endpoints.set(
|
|
7
|
+
export function registerLineAgentEndpoint(endpointKey, endpoint) {
|
|
8
|
+
endpoints.set(endpointKey, endpoint);
|
|
9
9
|
return () => {
|
|
10
|
-
if (endpoints.get(
|
|
11
|
-
endpoints.delete(
|
|
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
|
|
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
|
|
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
|
|
21
|
-
|| (typeof entry?.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
36
|
-
"@zhin.js/command": "1.0.
|
|
37
|
-
"@zhin.js/core": "1.5.
|
|
38
|
-
"@zhin.js/host-http": "1.0.
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
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.
|
|
45
|
-
"@zhin.js/agent": "1.1.
|
|
46
|
-
"@zhin.js/core": "1.5.
|
|
47
|
-
"@zhin.js/host-http": "1.0.
|
|
48
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
49
|
-
"zhin.js": "6.0.
|
|
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.
|
|
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。每项与顶层字段同构(
|
|
34
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
17
35
|
"items": {
|
|
18
36
|
"type": "object",
|
|
19
37
|
"additionalProperties": true,
|
|
20
38
|
"properties": {
|
|
21
|
-
"
|
|
22
|
-
"type":
|
|
23
|
-
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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'
|
|
118
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
async send({
|
|
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.
|
|
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
|
|
158
|
+
const conversation = lineInboundConversation(String(this.#options.id), event.source);
|
|
156
159
|
if ('replyToken' in event && typeof event.replyToken === 'string') {
|
|
157
|
-
this.#replyTokenCache.set(
|
|
160
|
+
this.#replyTokenCache.set(conversation.id, { token: event.replyToken, timestamp: Date.now() });
|
|
158
161
|
}
|
|
159
162
|
void this.#options.gateway.receive({
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
conversation,
|
|
164
|
+
message: { conversation, id: generateMessageId(event) },
|
|
162
165
|
content: formatInboundContent(event),
|
|
163
|
-
sender: event.source.userId ||
|
|
164
|
-
|
|
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:
|
|
177
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
176
178
|
error: err instanceof Error ? err.message : String(err),
|
|
177
179
|
}));
|
|
178
180
|
});
|
package/src/line-agent-deps.ts
CHANGED
|
@@ -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
|
-
|
|
18
|
+
endpointKey: string,
|
|
19
19
|
endpoint: LineAgentEndpoint,
|
|
20
20
|
): () => void {
|
|
21
|
-
endpoints.set(
|
|
21
|
+
endpoints.set(endpointKey, endpoint);
|
|
22
22
|
return () => {
|
|
23
|
-
if (endpoints.get(
|
|
24
|
-
endpoints.delete(
|
|
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
|
|
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
|
|
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
|
|
177
|
-
|| (typeof entry?.
|
|
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
|
-
|
|
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}`;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|