@zhin.js/adapter-line 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +42 -21
- package/adapters/line.ts +26 -0
- package/agent/tools/get_group_members.ts +2 -2
- package/agent/tools/get_profile.ts +2 -2
- package/lib/endpoint.d.ts +46 -0
- package/lib/endpoint.js +144 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/line-agent-deps.d.ts +24 -0
- package/lib/line-agent-deps.js +33 -0
- package/lib/protocol.d.ts +151 -0
- package/lib/protocol.js +212 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +50 -0
- package/package.json +42 -25
- package/plugin.ts +8 -0
- package/schema.json +22 -0
- package/src/endpoint.ts +148 -554
- package/src/index.ts +50 -41
- package/src/line-agent-deps.ts +32 -23
- package/src/protocol.ts +384 -0
- package/src/webhook.ts +79 -0
- package/lib/agent/tools/get_group_members.js +0 -24
- package/lib/agent/tools/get_group_members.js.map +0 -1
- package/lib/agent/tools/get_profile.js +0 -24
- package/lib/agent/tools/get_profile.js.map +0 -1
- package/lib/src/adapter.js +0 -22
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -536
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/index.js +0 -30
- package/lib/src/index.js.map +0 -1
- package/lib/src/line-agent-deps.js +0 -26
- package/lib/src/line-agent-deps.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js +0 -5
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -29
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -130
package/src/index.ts
CHANGED
|
@@ -1,44 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export {
|
|
2
|
+
formatInboundContent,
|
|
3
|
+
formatOutboundMessages,
|
|
4
|
+
generateMessageId,
|
|
5
|
+
isMessageEvent,
|
|
6
|
+
isPostbackEvent,
|
|
7
|
+
isValidLineRecipientId,
|
|
8
|
+
normalizeWebhookPath,
|
|
9
|
+
readTextBody,
|
|
10
|
+
resolveChannel,
|
|
11
|
+
resolveLineConfig,
|
|
12
|
+
verifySignature,
|
|
13
|
+
type LineAdapterConfig,
|
|
14
|
+
type LineApiResponse,
|
|
15
|
+
type LineChannel,
|
|
16
|
+
type LineEvent,
|
|
17
|
+
type LineFollowEvent,
|
|
18
|
+
type LineJoinEvent,
|
|
19
|
+
type LineLeaveEvent,
|
|
20
|
+
type LineMessage,
|
|
21
|
+
type LineMessageEvent,
|
|
22
|
+
type LinePostbackEvent,
|
|
23
|
+
type LinePushRequest,
|
|
24
|
+
type LineReplyMessage,
|
|
25
|
+
type LineReplyRequest,
|
|
26
|
+
type LineSource,
|
|
27
|
+
type LineUnfollowEvent,
|
|
28
|
+
type LineUser,
|
|
29
|
+
type LineWebhookBody,
|
|
30
|
+
type LineWireSegment,
|
|
31
|
+
type ResolvedLineConfig,
|
|
32
|
+
} from './protocol.js';
|
|
7
33
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
interface Adapters {
|
|
15
|
-
line: LineAdapter;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
34
|
+
export {
|
|
35
|
+
LineEndpoint,
|
|
36
|
+
type LineEndpointOptions,
|
|
37
|
+
type LineFetch,
|
|
38
|
+
} from './endpoint.js';
|
|
18
39
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
export {
|
|
41
|
+
registerLineWebhookRoutes,
|
|
42
|
+
handleLineWebhookRequest,
|
|
43
|
+
type LineWebhookHandler,
|
|
44
|
+
} from './webhook.js';
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const adapter = new LineAdapter(p, router);
|
|
32
|
-
await adapter.start();
|
|
33
|
-
return adapter;
|
|
34
|
-
},
|
|
35
|
-
dispose: async (adapter: LineAdapter) => {
|
|
36
|
-
await adapter.stop();
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
useContext('tool', 'line', (_toolService, lineAdapter: LineAdapter) => {
|
|
42
|
-
setLineAgentDeps({ getAdapter: () => lineAdapter });
|
|
43
|
-
return () => {};
|
|
44
|
-
});
|
|
46
|
+
export {
|
|
47
|
+
getLineAgentDeps,
|
|
48
|
+
getLineApiConfig,
|
|
49
|
+
registerLineAgentEndpoint,
|
|
50
|
+
setLineAgentDeps,
|
|
51
|
+
type LineAgentDeps,
|
|
52
|
+
type LineAgentEndpoint,
|
|
53
|
+
} from './line-agent-deps.js';
|
package/src/line-agent-deps.ts
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Agent tool deps for line (get_profile / get_group_members).
|
|
3
|
+
* Endpoints register themselves on start; tools look up the active API config.
|
|
3
4
|
*/
|
|
4
|
-
import type { LineAdapter } from './adapter.js';
|
|
5
|
-
import type { LineEndpoint } from './endpoint.js';
|
|
6
5
|
|
|
7
|
-
export interface
|
|
8
|
-
|
|
6
|
+
export interface LineAgentEndpoint {
|
|
7
|
+
getApiConfig(): { accessToken: string; apiBaseUrl: string };
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export function setLineAgentDeps(deps: LineAgentDeps): void {
|
|
14
|
-
_deps = deps;
|
|
10
|
+
export interface LineAgentDeps {
|
|
11
|
+
getApiConfig: () => { accessToken: string; apiBaseUrl: string };
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return _deps;
|
|
20
|
-
}
|
|
14
|
+
const endpoints = new Map<string, LineAgentEndpoint>();
|
|
15
|
+
let override: LineAgentDeps | null = null;
|
|
21
16
|
|
|
22
|
-
export function
|
|
23
|
-
|
|
17
|
+
export function registerLineAgentEndpoint(
|
|
18
|
+
endpointId: string,
|
|
19
|
+
endpoint: LineAgentEndpoint,
|
|
20
|
+
): () => void {
|
|
21
|
+
endpoints.set(endpointId, endpoint);
|
|
22
|
+
return () => {
|
|
23
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
24
|
+
endpoints.delete(endpointId);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
30
|
+
export function setLineAgentDeps(deps: LineAgentDeps | null): void {
|
|
31
|
+
override = deps;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
const accessToken = endpoint?.$config?.channelAccessToken;
|
|
33
|
-
if (!accessToken) throw new Error('LINE channel access token not configured');
|
|
34
|
+
export function getLineAgentDeps(): LineAgentDeps {
|
|
35
|
+
if (override) return override;
|
|
34
36
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
},
|
|
37
42
|
};
|
|
38
43
|
}
|
|
44
|
+
|
|
45
|
+
export function getLineApiConfig(): { accessToken: string; apiBaseUrl: string } {
|
|
46
|
+
return getLineAgentDeps().getApiConfig();
|
|
47
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE Messaging API protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import type { IncomingMessage } from 'node:http';
|
|
8
|
+
|
|
9
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
10
|
+
export interface LineAdapterConfig {
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
readonly channelSecret?: string;
|
|
13
|
+
readonly channelAccessToken?: string;
|
|
14
|
+
readonly webhookPath?: string;
|
|
15
|
+
readonly apiBaseUrl?: string;
|
|
16
|
+
/** Transitional: legacy root `endpoints[]` with `context: line`. */
|
|
17
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedLineConfig> & {
|
|
18
|
+
readonly context?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvedLineConfig {
|
|
23
|
+
readonly context: 'line';
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly channelSecret: string;
|
|
26
|
+
readonly channelAccessToken: string;
|
|
27
|
+
readonly webhookPath: string;
|
|
28
|
+
readonly apiBaseUrl: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LineUser {
|
|
32
|
+
readonly userId: string;
|
|
33
|
+
readonly displayName?: string;
|
|
34
|
+
readonly pictureUrl?: string;
|
|
35
|
+
readonly statusMessage?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LineMessageEvent {
|
|
39
|
+
readonly type: 'message';
|
|
40
|
+
readonly replyToken: string;
|
|
41
|
+
readonly source: LineSource;
|
|
42
|
+
readonly timestamp: number;
|
|
43
|
+
readonly message: LineMessage;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LineFollowEvent {
|
|
47
|
+
readonly type: 'follow';
|
|
48
|
+
readonly replyToken: string;
|
|
49
|
+
readonly source: LineSource;
|
|
50
|
+
readonly timestamp: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LineUnfollowEvent {
|
|
54
|
+
readonly type: 'unfollow';
|
|
55
|
+
readonly source: LineSource;
|
|
56
|
+
readonly timestamp: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LineJoinEvent {
|
|
60
|
+
readonly type: 'join';
|
|
61
|
+
readonly replyToken: string;
|
|
62
|
+
readonly source: LineSource;
|
|
63
|
+
readonly timestamp: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LineLeaveEvent {
|
|
67
|
+
readonly type: 'leave';
|
|
68
|
+
readonly source: LineSource;
|
|
69
|
+
readonly timestamp: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LinePostbackEvent {
|
|
73
|
+
readonly type: 'postback';
|
|
74
|
+
readonly replyToken: string;
|
|
75
|
+
readonly source: LineSource;
|
|
76
|
+
readonly timestamp: number;
|
|
77
|
+
readonly postback: {
|
|
78
|
+
readonly data: string;
|
|
79
|
+
readonly params?: Record<string, string>;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type LineEvent =
|
|
84
|
+
| LineMessageEvent
|
|
85
|
+
| LineFollowEvent
|
|
86
|
+
| LineUnfollowEvent
|
|
87
|
+
| LineJoinEvent
|
|
88
|
+
| LineLeaveEvent
|
|
89
|
+
| LinePostbackEvent;
|
|
90
|
+
|
|
91
|
+
export interface LineSource {
|
|
92
|
+
readonly type: 'user' | 'group' | 'room';
|
|
93
|
+
readonly userId?: string;
|
|
94
|
+
readonly groupId?: string;
|
|
95
|
+
readonly roomId?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LineMessage {
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly type: 'text' | 'image' | 'video' | 'audio' | 'file' | 'location' | 'sticker';
|
|
101
|
+
readonly text?: string;
|
|
102
|
+
readonly fileName?: string;
|
|
103
|
+
readonly fileSize?: number;
|
|
104
|
+
readonly title?: string;
|
|
105
|
+
readonly address?: string;
|
|
106
|
+
readonly latitude?: number;
|
|
107
|
+
readonly longitude?: number;
|
|
108
|
+
readonly packageId?: string;
|
|
109
|
+
readonly stickerId?: string;
|
|
110
|
+
readonly stickerResourceType?: string;
|
|
111
|
+
readonly duration?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LineWebhookBody {
|
|
115
|
+
readonly destination: string;
|
|
116
|
+
readonly events: readonly LineEvent[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface LineReplyMessage {
|
|
120
|
+
readonly type: 'text' | 'image' | 'video' | 'audio' | 'location' | 'sticker' | 'flex';
|
|
121
|
+
readonly text?: string;
|
|
122
|
+
readonly originalContentUrl?: string;
|
|
123
|
+
readonly previewImageUrl?: string;
|
|
124
|
+
readonly packageId?: string;
|
|
125
|
+
readonly stickerId?: string;
|
|
126
|
+
readonly title?: string;
|
|
127
|
+
readonly address?: string;
|
|
128
|
+
readonly latitude?: number;
|
|
129
|
+
readonly longitude?: number;
|
|
130
|
+
readonly duration?: number;
|
|
131
|
+
readonly altText?: string;
|
|
132
|
+
readonly contents?: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface LineReplyRequest {
|
|
136
|
+
readonly replyToken: string;
|
|
137
|
+
readonly messages: readonly LineReplyMessage[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface LinePushRequest {
|
|
141
|
+
readonly to: string;
|
|
142
|
+
readonly messages: readonly LineReplyMessage[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface LineApiResponse {
|
|
146
|
+
readonly sentMessages?: ReadonlyArray<{ readonly id: string; readonly quoteToken?: string }>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface LineWireSegment {
|
|
150
|
+
readonly type: string;
|
|
151
|
+
readonly data?: Record<string, unknown>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface LineChannel {
|
|
155
|
+
readonly channelType: 'private' | 'group' | 'channel';
|
|
156
|
+
readonly channelId: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveLineConfig(config: LineAdapterConfig = {}): ResolvedLineConfig {
|
|
160
|
+
const entry = config.endpoints?.find((item) => item.context === 'line');
|
|
161
|
+
const channelSecret = config.channelSecret
|
|
162
|
+
?? entry?.channelSecret
|
|
163
|
+
?? process.env.LINE_CHANNEL_SECRET;
|
|
164
|
+
const channelAccessToken = config.channelAccessToken
|
|
165
|
+
?? entry?.channelAccessToken
|
|
166
|
+
?? process.env.LINE_CHANNEL_ACCESS_TOKEN;
|
|
167
|
+
if (!channelSecret || !channelAccessToken) {
|
|
168
|
+
throw new TypeError(
|
|
169
|
+
'LINE adapter requires channelSecret + channelAccessToken (plugins.<key> or endpoints with context: line)',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
173
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
174
|
+
|| process.env.LINE_BOT_NAME
|
|
175
|
+
|| 'line-bot';
|
|
176
|
+
const webhookPath = normalizeWebhookPath(
|
|
177
|
+
config.webhookPath ?? entry?.webhookPath ?? '/line/webhook',
|
|
178
|
+
);
|
|
179
|
+
const apiBaseUrl = (config.apiBaseUrl ?? entry?.apiBaseUrl ?? 'https://api.line.me').replace(/\/$/, '');
|
|
180
|
+
return {
|
|
181
|
+
context: 'line',
|
|
182
|
+
name,
|
|
183
|
+
channelSecret,
|
|
184
|
+
channelAccessToken,
|
|
185
|
+
webhookPath,
|
|
186
|
+
apiBaseUrl,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function normalizeWebhookPath(path: string): string {
|
|
191
|
+
const trimmed = path.trim() || '/line/webhook';
|
|
192
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function isMessageEvent(event: LineEvent): event is LineMessageEvent {
|
|
196
|
+
return event.type === 'message' && 'message' in event && (event as LineMessageEvent).message != null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function isPostbackEvent(event: LineEvent): event is LinePostbackEvent {
|
|
200
|
+
return event.type === 'postback' && 'postback' in event;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveChannel(source: LineSource): LineChannel {
|
|
204
|
+
switch (source.type) {
|
|
205
|
+
case 'user':
|
|
206
|
+
return { channelType: 'private', channelId: source.userId || '' };
|
|
207
|
+
case 'group':
|
|
208
|
+
return { channelType: 'group', channelId: source.groupId || '' };
|
|
209
|
+
case 'room':
|
|
210
|
+
return { channelType: 'channel', channelId: source.roomId || '' };
|
|
211
|
+
default:
|
|
212
|
+
return { channelType: 'private', channelId: '' };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function generateMessageId(event: LineEvent): string {
|
|
217
|
+
if (isMessageEvent(event) && event.message?.id) return event.message.id;
|
|
218
|
+
return `${event.type}-${event.timestamp}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
222
|
+
export function formatInboundContent(event: LineEvent): string {
|
|
223
|
+
if (isMessageEvent(event)) {
|
|
224
|
+
const msg = event.message;
|
|
225
|
+
switch (msg.type) {
|
|
226
|
+
case 'text':
|
|
227
|
+
return msg.text || '';
|
|
228
|
+
case 'location':
|
|
229
|
+
return msg.address || `[location: ${msg.latitude},${msg.longitude}]`;
|
|
230
|
+
case 'image':
|
|
231
|
+
return '[image]';
|
|
232
|
+
case 'video':
|
|
233
|
+
return '[video]';
|
|
234
|
+
case 'audio':
|
|
235
|
+
return '[audio]';
|
|
236
|
+
case 'file':
|
|
237
|
+
return msg.fileName ? `[file: ${msg.fileName}]` : '[file]';
|
|
238
|
+
case 'sticker':
|
|
239
|
+
return `[sticker: ${msg.packageId}/${msg.stickerId}]`;
|
|
240
|
+
default:
|
|
241
|
+
return `[${(msg as LineMessage).type}]`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (event.type === 'follow') return '[follow]';
|
|
245
|
+
if (event.type === 'join') return '[join]';
|
|
246
|
+
if (event.type === 'unfollow') return '[unfollow]';
|
|
247
|
+
if (event.type === 'leave') return '[leave]';
|
|
248
|
+
if (event.type === 'postback') return `[postback: ${event.postback.data}]`;
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function verifySignature(
|
|
253
|
+
channelSecret: string,
|
|
254
|
+
body: string,
|
|
255
|
+
signature: string,
|
|
256
|
+
): boolean {
|
|
257
|
+
const hmac = createHmac('sha256', channelSecret);
|
|
258
|
+
hmac.update(body, 'utf-8');
|
|
259
|
+
const computedSignature = hmac.digest('base64');
|
|
260
|
+
const sigBuf = Buffer.from(signature);
|
|
261
|
+
const computedBuf = Buffer.from(computedSignature);
|
|
262
|
+
if (sigBuf.length !== computedBuf.length) return false;
|
|
263
|
+
return timingSafeEqual(sigBuf, computedBuf);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function isValidLineRecipientId(id: string): boolean {
|
|
267
|
+
return /^[UGR]/.test(id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildTextMessage(text: string): LineReplyMessage {
|
|
271
|
+
const truncated = text.length > 5000 ? `${text.slice(0, 4997)}...` : text;
|
|
272
|
+
return { type: 'text', text: truncated };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Wire-encode an already-rendered outbound payload into LINE Reply/Push messages.
|
|
277
|
+
* Segment canonicalization is intentionally not done here.
|
|
278
|
+
*/
|
|
279
|
+
export function formatOutboundMessages(payload: unknown): LineReplyMessage[] {
|
|
280
|
+
if (typeof payload === 'string') {
|
|
281
|
+
return [buildTextMessage(payload)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const items: Array<string | LineWireSegment> = Array.isArray(payload)
|
|
285
|
+
? payload as Array<string | LineWireSegment>
|
|
286
|
+
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
287
|
+
? [payload as LineWireSegment]
|
|
288
|
+
: [];
|
|
289
|
+
|
|
290
|
+
if (items.length === 0) {
|
|
291
|
+
const text = payload == null
|
|
292
|
+
? ''
|
|
293
|
+
: typeof payload === 'object'
|
|
294
|
+
? JSON.stringify(payload)
|
|
295
|
+
: String(payload);
|
|
296
|
+
return [buildTextMessage(text)];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const messages: LineReplyMessage[] = [];
|
|
300
|
+
for (const item of items) {
|
|
301
|
+
if (typeof item === 'string') {
|
|
302
|
+
messages.push(buildTextMessage(item));
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const data = item.data ?? {};
|
|
306
|
+
switch (item.type) {
|
|
307
|
+
case 'text':
|
|
308
|
+
messages.push(buildTextMessage(String(data.text ?? data.content ?? '')));
|
|
309
|
+
break;
|
|
310
|
+
case 'at':
|
|
311
|
+
if (data.id) {
|
|
312
|
+
messages.push(buildTextMessage(`@${String(data.name || data.id)}`));
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
case 'image':
|
|
316
|
+
if (typeof data.url === 'string' && data.url) {
|
|
317
|
+
messages.push({
|
|
318
|
+
type: 'image',
|
|
319
|
+
originalContentUrl: data.url,
|
|
320
|
+
previewImageUrl: data.url,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
case 'video':
|
|
325
|
+
if (typeof data.url === 'string' && data.url) {
|
|
326
|
+
messages.push({
|
|
327
|
+
type: 'video',
|
|
328
|
+
originalContentUrl: data.url,
|
|
329
|
+
previewImageUrl: typeof data.previewUrl === 'string' ? data.previewUrl : data.url,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
case 'audio':
|
|
334
|
+
if (typeof data.url === 'string' && data.url) {
|
|
335
|
+
messages.push({
|
|
336
|
+
type: 'audio',
|
|
337
|
+
originalContentUrl: data.url,
|
|
338
|
+
duration: typeof data.duration === 'number' ? data.duration : 0,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
case 'location':
|
|
343
|
+
messages.push({
|
|
344
|
+
type: 'location',
|
|
345
|
+
title: String(data.title || 'Location'),
|
|
346
|
+
address: String(data.address || ''),
|
|
347
|
+
latitude: typeof data.latitude === 'number' ? data.latitude : 0,
|
|
348
|
+
longitude: typeof data.longitude === 'number' ? data.longitude : 0,
|
|
349
|
+
});
|
|
350
|
+
break;
|
|
351
|
+
case 'sticker':
|
|
352
|
+
messages.push({
|
|
353
|
+
type: 'sticker',
|
|
354
|
+
packageId: String(data.package_id || '1'),
|
|
355
|
+
stickerId: String(data.sticker_id || '1'),
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
default:
|
|
359
|
+
messages.push(buildTextMessage(`[${item.type}]`));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// LINE allows at most 5 messages per Reply/Push request.
|
|
364
|
+
return messages.slice(0, 5);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function readTextBody(
|
|
368
|
+
request: IncomingMessage,
|
|
369
|
+
options: { readonly limit?: number } = {},
|
|
370
|
+
): Promise<string> {
|
|
371
|
+
const limit = options.limit ?? 1_048_576;
|
|
372
|
+
const chunks: Buffer[] = [];
|
|
373
|
+
let size = 0;
|
|
374
|
+
for await (const chunk of request) {
|
|
375
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
376
|
+
size += buffer.length;
|
|
377
|
+
if (size > limit) {
|
|
378
|
+
request.destroy();
|
|
379
|
+
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
380
|
+
}
|
|
381
|
+
chunks.push(buffer);
|
|
382
|
+
}
|
|
383
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
384
|
+
}
|
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE webhook HTTP: signature → parse → admit.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import {
|
|
8
|
+
readTextBody,
|
|
9
|
+
verifySignature,
|
|
10
|
+
type LineEvent,
|
|
11
|
+
type LineWebhookBody,
|
|
12
|
+
type ResolvedLineConfig,
|
|
13
|
+
} from './protocol.js';
|
|
14
|
+
|
|
15
|
+
const logger = getLogger('line');
|
|
16
|
+
|
|
17
|
+
export interface LineWebhookHandler {
|
|
18
|
+
readonly config: ResolvedLineConfig;
|
|
19
|
+
readonly isOpen: boolean;
|
|
20
|
+
admit(event: LineEvent): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerLineWebhookRoutes(
|
|
24
|
+
http: HttpHost,
|
|
25
|
+
handler: LineWebhookHandler,
|
|
26
|
+
): HttpRouteRegistration[] {
|
|
27
|
+
const path = handler.config.webhookPath;
|
|
28
|
+
return [
|
|
29
|
+
http.route('POST', path, async (request, response) => {
|
|
30
|
+
await handleLineWebhookRequest(request, response, handler);
|
|
31
|
+
}, { summary: 'LINE Messaging API webhook', tags: ['line'] }),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function handleLineWebhookRequest(
|
|
36
|
+
request: IncomingMessage,
|
|
37
|
+
response: ServerResponse,
|
|
38
|
+
handler: LineWebhookHandler,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
const signature = request.headers['x-line-signature'];
|
|
42
|
+
const signatureValue = Array.isArray(signature) ? signature[0] : signature;
|
|
43
|
+
if (!signatureValue) {
|
|
44
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
45
|
+
response.end(JSON.stringify({ message: 'Missing signature' }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rawBody = await readTextBody(request);
|
|
50
|
+
if (!verifySignature(handler.config.channelSecret, rawBody, signatureValue)) {
|
|
51
|
+
logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid signature' }));
|
|
52
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
53
|
+
response.end(JSON.stringify({ message: 'Invalid signature' }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let body: LineWebhookBody;
|
|
58
|
+
try {
|
|
59
|
+
body = JSON.parse(rawBody) as LineWebhookBody;
|
|
60
|
+
} catch {
|
|
61
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
62
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (handler.isOpen && Array.isArray(body.events)) {
|
|
67
|
+
for (const event of body.events) {
|
|
68
|
+
handler.admit(event);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error('LINE webhook error:', error);
|
|
76
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
77
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { defineTool } from '@zhin.js/agent/tools';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getLineApiConfig } from '../../src/line-agent-deps.js';
|
|
4
|
-
export default defineTool({
|
|
5
|
-
description: 'Get LINE group member IDs',
|
|
6
|
-
inputSchema: z.object({
|
|
7
|
-
groupId: z.string().min(1),
|
|
8
|
-
}),
|
|
9
|
-
async execute({ groupId }) {
|
|
10
|
-
if (!groupId.startsWith('G')) {
|
|
11
|
-
throw new Error(`Invalid groupId "${groupId}": must start with G`);
|
|
12
|
-
}
|
|
13
|
-
const { accessToken, apiBaseUrl } = getLineApiConfig();
|
|
14
|
-
const response = await fetch(`${apiBaseUrl}/v2/bot/group/${groupId}/members/ids`, {
|
|
15
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
16
|
-
});
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
const errorText = await response.text();
|
|
19
|
-
throw new Error(`LINE Group Members API error ${response.status}: ${errorText}`);
|
|
20
|
-
}
|
|
21
|
-
return await response.json();
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
//# sourceMappingURL=get_group_members.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get_group_members.js","sourceRoot":"","sources":["../../../agent/tools/get_group_members.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,eAAe,UAAU,CAAsB;IAC7C,WAAW,EAAE,2BAA2B;IACxC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC3B,CAAC;IACF,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE;QACvB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,oBAAoB,OAAO,sBAAsB,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,gBAAgB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,UAAU,iBAAiB,OAAO,cAAc,EAAE;YAChF,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;SACpD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { defineTool } from '@zhin.js/agent/tools';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getLineApiConfig } from '../../src/line-agent-deps.js';
|
|
4
|
-
export default defineTool({
|
|
5
|
-
description: 'Get LINE user profile by userId',
|
|
6
|
-
inputSchema: z.object({
|
|
7
|
-
userId: z.string().min(1),
|
|
8
|
-
}),
|
|
9
|
-
async execute({ userId }) {
|
|
10
|
-
if (!userId.startsWith('U')) {
|
|
11
|
-
throw new Error(`Invalid userId "${userId}": must start with U`);
|
|
12
|
-
}
|
|
13
|
-
const { accessToken, apiBaseUrl } = getLineApiConfig();
|
|
14
|
-
const response = await fetch(`${apiBaseUrl}/v2/profile/${userId}`, {
|
|
15
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
16
|
-
});
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
const errorText = await response.text();
|
|
19
|
-
throw new Error(`LINE Profile API error ${response.status}: ${errorText}`);
|
|
20
|
-
}
|
|
21
|
-
return await response.json();
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
//# sourceMappingURL=get_profile.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get_profile.js","sourceRoot":"","sources":["../../../agent/tools/get_profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,eAAe,UAAU,CAAqB;IAC5C,WAAW,EAAE,iCAAiC;IAC9C,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC1B,CAAC;IACF,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,sBAAsB,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,gBAAgB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,UAAU,eAAe,MAAM,EAAE,EAAE;YACjE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;SACpD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;CACF,CAAC,CAAC"}
|