@zoulabo/line-hive 0.1.20 → 0.1.21
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/README.md +8 -6
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/line/webhook.js +146 -41
- package/dist/line/webhook.js.map +1 -1
- package/dist/server.js +7 -5
- package/dist/server.js.map +1 -1
- package/dist/store/messageStore.js +112 -22
- package/dist/store/messageStore.js.map +1 -1
- package/dist/tools/ask.js +20 -4
- package/dist/tools/ask.js.map +1 -1
- package/dist/tools/readFile.js +18 -6
- package/dist/tools/readFile.js.map +1 -1
- package/dist/tools/renderMarkdown.js +120 -9
- package/dist/tools/renderMarkdown.js.map +1 -1
- package/dist/tools/sendMessage.js +12 -4
- package/dist/tools/sendMessage.js.map +1 -1
- package/dist/util/toolHelpers.js +267 -18
- package/dist/util/toolHelpers.js.map +1 -1
- package/package.json +1 -1
- package/templates/line-notification.instructions.md +28 -9
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<a href="https://www.npmjs.com/package/@zoulabo/line-hive"><img src="https://img.shields.io/npm/v/@zoulabo/line-hive?color=cb3837&label=npm" alt="npm version"></a>
|
|
14
14
|
<img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js ≥18">
|
|
15
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License"></a>
|
|
16
|
-
<img src="https://img.shields.io/badge/tests-
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-339%20passing-brightgreen" alt="339 tests passing">
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
- 🔍 **PDF intelligence** — text extraction, scanned PDF→image rendering, per-page navigation
|
|
35
35
|
- 🌐 **Multi-language** — English and Japanese, auto-detected from LINE profile
|
|
36
36
|
- 🔧 **Zero config tunnel** — built-in ngrok management (or bring your own tunnel)
|
|
37
|
-
- 🛡️ **Security hardened** —
|
|
37
|
+
- 🛡️ **Security hardened** — workspace-scoped file access, path containment, sandboxed PDF parsing
|
|
38
|
+
- 🔐 **Access control** — first user is auto-registered as primary admin; restricted mode activates on follow or status check, blocking other users by default
|
|
38
39
|
|
|
39
40
|
## Contents
|
|
40
41
|
|
|
@@ -266,7 +267,7 @@ npx @zoulabo/line-hive init --editor cursor # switch editor type
|
|
|
266
267
|
| `line_check_messages` | Fetch unclaimed inbound messages | No |
|
|
267
268
|
| `line_list_agents` | List all active agents across editor windows | No |
|
|
268
269
|
| `line_read_file` | Read content from a received file (PDF text, scanned PDF→image, image resize, text decode) | No |
|
|
269
|
-
| `line_render_markdown` | Convert Markdown
|
|
270
|
+
| `line_render_markdown` | Convert Markdown → HTML with auto-injected CDN (Mermaid, highlight.js, KaTeX). TTL-managed. | No |
|
|
270
271
|
|
|
271
272
|
### Recommended Pattern
|
|
272
273
|
|
|
@@ -304,8 +305,8 @@ Auto-detected from the user's LINE profile. Currently **English** (default) and
|
|
|
304
305
|
|
|
305
306
|
- **Push quota** — free tier: 200/month. Status queries via `?` use reply tokens (free). Send `?` periodically to replenish the token pool.
|
|
306
307
|
- **Reply token expiry** — ~15 min. Falls back to push when exhausted.
|
|
307
|
-
- **Files** — receive images & files (stored
|
|
308
|
-
- **Single user** — designed for one
|
|
308
|
+
- **Files** — receive images & files (stored on disk in `.line-hive-tmp/incoming/`, 24h TTL). Send via `imageBase64`, `imageFilePath`, or `filePath`. Served files have a configurable TTL (default 2h, max 24h) with auto-cleanup. Stickers are currently ignored; video and audio are stored as files.
|
|
309
|
+
- **Single user** — designed for one primary user (admin) per LINE channel. The first follow or status check auto-activates restricted mode; additional users require manual allowlisting via store methods.
|
|
309
310
|
- **Single webhook URL** — all agent instances share one tunnel endpoint.
|
|
310
311
|
- **Multi-workspace** — only the first editor claims the webhook port/tunnel. Others piggyback via shared SQLite.
|
|
311
312
|
- **Claude Code** — `line_ask` can block up to 2 hours; ensure `MCP_TOOL_TIMEOUT` is not too short. Always use `--editor claude-code` for init.
|
|
@@ -350,7 +351,7 @@ For standalone `npm start`:
|
|
|
350
351
|
|
|
351
352
|
Designed for **AI-agent-driven development**. The repo includes skill files (`.github/skills/`) that teach agents the codebase. Point your agent at the repo and let it work.
|
|
352
353
|
|
|
353
|
-
- `npm test` must pass (~
|
|
354
|
+
- `npm test` must pass (~339 tests, <1s)
|
|
354
355
|
- `npx tsc --noEmit` must be clean
|
|
355
356
|
- Let the agents read skills first, then write the code
|
|
356
357
|
|
|
@@ -400,6 +401,7 @@ Don't use `--editor vscode` — that's for GitHub Copilot.
|
|
|
400
401
|
- **SQLite** (WAL mode) shared across all agent instances at `~/.line-hive/line-hive.db`
|
|
401
402
|
- **Heartbeat** every 10s — dead agents auto-cleaned after 2 min
|
|
402
403
|
- **Reply token pooling** — webhook saves tokens for free replies (saves push quota)
|
|
404
|
+
- **TTL-managed file serving** — images, HTML, and documents served from disk with configurable TTL (default 2h, max 24h); expired files auto-cleaned every 60s
|
|
403
405
|
- **Session correlation** — `line_ask` creates a session, webhook fulfills it on reply
|
|
404
406
|
- **Multi-agent routing** — user selects agent via quick reply buttons, or auto-routes to `needs_input` agent
|
|
405
407
|
- **Embedded tunnel** — ngrok auto-managed as child process (optional; any tunnel to localhost:19780 works)
|
package/dist/constants.js
CHANGED
|
@@ -10,6 +10,12 @@ export const MAX_TIMEOUT_MS = 86400000; // 24 hours
|
|
|
10
10
|
export const POLL_INTERVAL_MS = 2000;
|
|
11
11
|
// Reply token pool
|
|
12
12
|
export const REPLY_TOKEN_MAX_AGE_MS = 900000; // 15 min
|
|
13
|
+
// Served file TTL
|
|
14
|
+
export const DEFAULT_FILE_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
15
|
+
export const MAX_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
16
|
+
// Incoming media disk storage
|
|
17
|
+
export const INCOMING_MEDIA_DIR = ".line-hive-tmp/incoming";
|
|
13
18
|
// Config keys
|
|
14
19
|
export const CONFIG_TARGETED_AGENT = "targeted_agent_id";
|
|
20
|
+
export const CONFIG_ALLOWED_USERS = "allowed_users";
|
|
15
21
|
//# sourceMappingURL=constants.js.map
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kBAAkB;AAClB,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,mBAAmB;AACnB,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,WAAW;AAEnD,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,mBAAmB;AACnB,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,CAAC,SAAS;AAEvD,cAAc;AACd,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kBAAkB;AAClB,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,mBAAmB;AACnB,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,WAAW;AAEnD,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,mBAAmB;AACnB,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,CAAC,SAAS;AAEvD,kBAAkB;AAClB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAW,UAAU;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAS,WAAW;AAEvE,8BAA8B;AAC9B,MAAM,CAAC,MAAM,kBAAkB,GAAG,yBAAyB,CAAC;AAE5D,cAAc;AACd,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAC;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -82,8 +82,8 @@ async function main() {
|
|
|
82
82
|
webhookServer = null;
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
-
catch {
|
|
86
|
-
|
|
85
|
+
catch (err) {
|
|
86
|
+
logger.debug({ error: err instanceof Error ? err.message : err }, "Port still in use — will retry on next heartbeat");
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
// Initial attempt to claim the port
|
|
@@ -147,9 +147,39 @@ async function main() {
|
|
|
147
147
|
}
|
|
148
148
|
}, config.heartbeatIntervalMs);
|
|
149
149
|
const cleanupTimer = setInterval(() => {
|
|
150
|
-
messageStore.cleanup();
|
|
150
|
+
const incomingMediaPaths = messageStore.cleanup();
|
|
151
151
|
messageStore.expireSessions(Date.now());
|
|
152
152
|
messageStore.cleanupDeadAgentSessions(config.heartbeatTimeoutMs);
|
|
153
|
+
// Clean up incoming media files from disk (returned by cleanup())
|
|
154
|
+
let incomingDeleted = 0;
|
|
155
|
+
for (const diskPath of incomingMediaPaths) {
|
|
156
|
+
try {
|
|
157
|
+
const fullPath = path.resolve(diskPath);
|
|
158
|
+
if (fs.existsSync(fullPath)) {
|
|
159
|
+
fs.unlinkSync(fullPath);
|
|
160
|
+
incomingDeleted++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { /* ignore cleanup errors */ }
|
|
164
|
+
}
|
|
165
|
+
if (incomingDeleted > 0) {
|
|
166
|
+
logger.info({ count: incomingDeleted }, "Cleaned up incoming media files");
|
|
167
|
+
}
|
|
168
|
+
// Clean up expired served files from disk
|
|
169
|
+
const expiredFiles = messageStore.cleanupExpiredServedFiles();
|
|
170
|
+
const serveDir = path.resolve(".line-hive-tmp");
|
|
171
|
+
for (const filename of expiredFiles) {
|
|
172
|
+
try {
|
|
173
|
+
const filePath = path.join(serveDir, filename);
|
|
174
|
+
if (fs.existsSync(filePath)) {
|
|
175
|
+
fs.unlinkSync(filePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { /* ignore cleanup errors */ }
|
|
179
|
+
}
|
|
180
|
+
if (expiredFiles.length > 0) {
|
|
181
|
+
logger.info({ count: expiredFiles.length }, "Cleaned up expired served files");
|
|
182
|
+
}
|
|
153
183
|
}, config.cleanupIntervalMs);
|
|
154
184
|
let shuttingDown = false;
|
|
155
185
|
const shutdown = () => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/line/webhook.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
3
5
|
import { buildDeleteQueueReply, errorMessage, parsePostbackData, textWithQuickReply, textWithQueueQuickReply, formatTimeAgo, STATUS_ICONS, getAgentDisplayName, } from "./messages.js";
|
|
4
6
|
import { sendWithTokenPool } from "../util/sendWithTokenPool.js";
|
|
5
|
-
import { CONFIG_TARGETED_AGENT } from "../constants.js";
|
|
7
|
+
import { CONFIG_TARGETED_AGENT, INCOMING_MEDIA_DIR } from "../constants.js";
|
|
6
8
|
import { t, resolveLocale } from "../i18n.js";
|
|
7
9
|
export function verifySignature(secret, body, signature) {
|
|
8
10
|
if (!secret || !signature) {
|
|
@@ -42,6 +44,18 @@ async function resolveUserLocale(deps, userId) {
|
|
|
42
44
|
}
|
|
43
45
|
return "en";
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Register the user as default (if first user) and auto-enable restricted mode.
|
|
49
|
+
* When the first user interacts with the bot and no allowed_users list exists yet,
|
|
50
|
+
* this switches from open mode to restricted mode with the first user auto-allowed.
|
|
51
|
+
*/
|
|
52
|
+
function registerAndAutoAllow(deps, userId) {
|
|
53
|
+
const isFirst = deps.messageStore.registerDefaultUserIfMissing(userId);
|
|
54
|
+
if (isFirst && deps.messageStore.getAllowedUsers().length === 0) {
|
|
55
|
+
deps.messageStore.addAllowedUser(userId);
|
|
56
|
+
deps.logger?.info({ userId: userId.slice(0, 8) }, "First user auto-allowed, restricted mode enabled");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
45
59
|
/** Format a single agent's status into a line of text */
|
|
46
60
|
export function formatAgentStatus(agent, locale = "en") {
|
|
47
61
|
const m = t(locale);
|
|
@@ -136,8 +150,8 @@ async function replyViaPool(deps, replyToken, message, eventTimestamp) {
|
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
/**
|
|
139
|
-
* Download an image
|
|
140
|
-
* Uses the
|
|
153
|
+
* Download an image from the LINE Content API at full resolution.
|
|
154
|
+
* Uses the full content endpoint (not preview) to get the original image.
|
|
141
155
|
* For external images (forwarded), fetches from the original URL directly.
|
|
142
156
|
*/
|
|
143
157
|
async function downloadImagePreview(deps, event) {
|
|
@@ -154,7 +168,8 @@ async function downloadImagePreview(deps, event) {
|
|
|
154
168
|
url = provider.originalContentUrl;
|
|
155
169
|
}
|
|
156
170
|
else if (messageId) {
|
|
157
|
-
|
|
171
|
+
// Full-resolution content endpoint (not /preview)
|
|
172
|
+
url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
|
|
158
173
|
}
|
|
159
174
|
else {
|
|
160
175
|
return null;
|
|
@@ -168,16 +183,15 @@ async function downloadImagePreview(deps, event) {
|
|
|
168
183
|
deps.logger?.warn({ status: res.status, messageId }, "Image download failed");
|
|
169
184
|
return null;
|
|
170
185
|
}
|
|
171
|
-
// Limit download size to
|
|
186
|
+
// Limit download size to 10MB to prevent memory exhaustion
|
|
172
187
|
const contentLength = res.headers.get("content-length");
|
|
173
|
-
if (contentLength && parseInt(contentLength, 10) >
|
|
188
|
+
if (contentLength && parseInt(contentLength, 10) > 10 * 1024 * 1024) {
|
|
174
189
|
deps.logger?.warn({ contentLength }, "Image too large, skipping download");
|
|
175
190
|
return null;
|
|
176
191
|
}
|
|
177
192
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
178
|
-
const base64 = buffer.toString("base64");
|
|
179
193
|
const mimeType = res.headers.get("content-type") || "image/jpeg";
|
|
180
|
-
return {
|
|
194
|
+
return { buffer, mimeType };
|
|
181
195
|
}
|
|
182
196
|
catch (error) {
|
|
183
197
|
deps.logger?.warn({ error }, "Image download error");
|
|
@@ -209,10 +223,9 @@ async function downloadFileContent(deps, event) {
|
|
|
209
223
|
return null;
|
|
210
224
|
}
|
|
211
225
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
212
|
-
const base64 = buffer.toString("base64");
|
|
213
226
|
const mimeType = res.headers.get("content-type") || "application/octet-stream";
|
|
214
227
|
return {
|
|
215
|
-
|
|
228
|
+
buffer,
|
|
216
229
|
mimeType,
|
|
217
230
|
fileName: event.message?.fileName,
|
|
218
231
|
fileSize: event.message?.fileSize,
|
|
@@ -223,6 +236,36 @@ async function downloadFileContent(deps, event) {
|
|
|
223
236
|
return null;
|
|
224
237
|
}
|
|
225
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Write downloaded media to disk under `.line-hive-tmp/incoming/`.
|
|
241
|
+
* Returns the relative disk path (from project root) for storage in media_json.
|
|
242
|
+
*
|
|
243
|
+
* Naming: `{lineMessageId}-{sanitizedFilename}` or `{lineMessageId}.{ext}`
|
|
244
|
+
*/
|
|
245
|
+
function writeIncomingMedia(lineMessageId, buffer, mimeType, fileName) {
|
|
246
|
+
const dir = path.resolve(INCOMING_MEDIA_DIR);
|
|
247
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
248
|
+
// Derive extension from filename or mime type
|
|
249
|
+
const mimeToExt = {
|
|
250
|
+
"image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif",
|
|
251
|
+
"image/webp": ".webp", "audio/mpeg": ".mp3", "audio/m4a": ".m4a",
|
|
252
|
+
"audio/ogg": ".ogg", "video/mp4": ".mp4", "application/pdf": ".pdf",
|
|
253
|
+
};
|
|
254
|
+
let safeName;
|
|
255
|
+
if (fileName) {
|
|
256
|
+
// Sanitize user-provided filename: keep alphanumeric, dots, hyphens, underscores
|
|
257
|
+
safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const ext = mimeToExt[mimeType] || "";
|
|
261
|
+
safeName = `${lineMessageId}${ext}`;
|
|
262
|
+
}
|
|
263
|
+
const diskName = `${lineMessageId}-${safeName}`;
|
|
264
|
+
const diskPath = path.join(dir, diskName);
|
|
265
|
+
fs.writeFileSync(diskPath, buffer);
|
|
266
|
+
// Return path relative to project root (for storage in media_json)
|
|
267
|
+
return path.join(INCOMING_MEDIA_DIR, diskName);
|
|
268
|
+
}
|
|
226
269
|
async function handleFileEvent(deps, event, userId) {
|
|
227
270
|
const replyToken = event.replyToken;
|
|
228
271
|
const webhookEventId = getWebhookEventId(event);
|
|
@@ -230,7 +273,17 @@ async function handleFileEvent(deps, event, userId) {
|
|
|
230
273
|
const fileSize = event.message?.fileSize;
|
|
231
274
|
// Download file content immediately (LINE auto-deletes content after a period)
|
|
232
275
|
const media = await downloadFileContent(deps, event);
|
|
233
|
-
|
|
276
|
+
let mediaJson = null;
|
|
277
|
+
if (media) {
|
|
278
|
+
const lineMessageId = event.message?.id ?? webhookEventId;
|
|
279
|
+
const diskPath = writeIncomingMedia(lineMessageId, media.buffer, media.mimeType, media.fileName);
|
|
280
|
+
mediaJson = JSON.stringify({
|
|
281
|
+
mimeType: media.mimeType,
|
|
282
|
+
diskPath,
|
|
283
|
+
fileName: media.fileName,
|
|
284
|
+
fileSize: media.fileSize,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
234
287
|
const locale = await resolveUserLocale(deps, userId);
|
|
235
288
|
const chatTargetedAgentId = deps.messageStore.getConfig(CONFIG_TARGETED_AGENT);
|
|
236
289
|
const sizeStr = fileSize ? ` (${Math.round(fileSize / 1024)}KB)` : "";
|
|
@@ -261,7 +314,12 @@ async function handleImageEvent(deps, event, userId) {
|
|
|
261
314
|
const webhookEventId = getWebhookEventId(event);
|
|
262
315
|
// Download image preview immediately (LINE auto-deletes content after a period)
|
|
263
316
|
const media = await downloadImagePreview(deps, event);
|
|
264
|
-
|
|
317
|
+
let mediaJson = null;
|
|
318
|
+
if (media) {
|
|
319
|
+
const lineMessageId = event.message?.id ?? webhookEventId;
|
|
320
|
+
const diskPath = writeIncomingMedia(lineMessageId, media.buffer, media.mimeType);
|
|
321
|
+
mediaJson = JSON.stringify({ mimeType: media.mimeType, diskPath });
|
|
322
|
+
}
|
|
265
323
|
// Resolve user locale
|
|
266
324
|
const locale = await resolveUserLocale(deps, userId);
|
|
267
325
|
// For chat messages, use targeted routing if user has selected an agent
|
|
@@ -277,7 +335,7 @@ async function handleImageEvent(deps, event, userId) {
|
|
|
277
335
|
}, { targetedAgentId: chatTargetedAgentId });
|
|
278
336
|
if (messageResult.deduped)
|
|
279
337
|
return;
|
|
280
|
-
// Auto-register userId
|
|
338
|
+
// Auto-register userId (no auto-allow for image events)
|
|
281
339
|
deps.messageStore.registerDefaultUserIfMissing(userId);
|
|
282
340
|
if (!replyToken)
|
|
283
341
|
return;
|
|
@@ -331,8 +389,13 @@ async function handleTextEvent(deps, event, text, userId) {
|
|
|
331
389
|
if (isControlCommand && messageResult.messageId) {
|
|
332
390
|
deps.messageStore.markMessageConsumed(messageResult.messageId);
|
|
333
391
|
}
|
|
334
|
-
// Auto-register
|
|
335
|
-
|
|
392
|
+
// Auto-register: only auto-allow on status command ("?"), not on chat/other
|
|
393
|
+
if (command.type === "status") {
|
|
394
|
+
registerAndAutoAllow(deps, userId);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
deps.messageStore.registerDefaultUserIfMissing(userId);
|
|
398
|
+
}
|
|
336
399
|
if (!replyToken) {
|
|
337
400
|
return;
|
|
338
401
|
}
|
|
@@ -356,7 +419,7 @@ async function handlePostbackEvent(deps, event, data, userId) {
|
|
|
356
419
|
const replyToken = event.replyToken;
|
|
357
420
|
if (!replyToken)
|
|
358
421
|
return;
|
|
359
|
-
// Auto-register userId
|
|
422
|
+
// Auto-register userId (no auto-allow for postback events)
|
|
360
423
|
deps.messageStore.registerDefaultUserIfMissing(userId);
|
|
361
424
|
// Resolve user locale
|
|
362
425
|
const locale = await resolveUserLocale(deps, userId);
|
|
@@ -600,7 +663,8 @@ async function handleCommandReply(deps, command, replyToken, messageId, eventTim
|
|
|
600
663
|
deps.logger?.info({ targetAgent: agentDisplayName }, "Chat routed to targeted agent");
|
|
601
664
|
// If agent is actively waiting (has active session), save token silently —
|
|
602
665
|
// agent will respond within seconds via line_ask polling.
|
|
603
|
-
|
|
666
|
+
// Scope to THIS agent to avoid stale sessions from dead agents suppressing acks.
|
|
667
|
+
if (deps.messageStore.getWaitingSessionForAgent(targetAgent.agentId)) {
|
|
604
668
|
deps.messageStore.saveReplyToken(replyToken, eventTimestamp ?? Date.now());
|
|
605
669
|
return;
|
|
606
670
|
}
|
|
@@ -645,6 +709,11 @@ export async function handleWebhook(deps, body, signature) {
|
|
|
645
709
|
const userId = event.source?.userId;
|
|
646
710
|
if (!userId)
|
|
647
711
|
continue;
|
|
712
|
+
// Access control: reject messages from unauthorized users
|
|
713
|
+
if (!deps.messageStore.isUserAllowed(userId)) {
|
|
714
|
+
deps.logger?.info({ userId: userId.slice(0, 8) }, "Blocked message from unauthorized user");
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
648
717
|
if (event.type === "message" && event.message?.type === "text") {
|
|
649
718
|
const text = event.message?.text ?? "";
|
|
650
719
|
await handleTextEvent(deps, event, text, userId);
|
|
@@ -667,6 +736,14 @@ export async function handleWebhook(deps, body, signature) {
|
|
|
667
736
|
const removed = deps.messageStore.deleteByLineMessageId(event.unsend.messageId);
|
|
668
737
|
deps.logger?.info({ lineMessageId: event.unsend.messageId, removed }, "Unsend: message deleted");
|
|
669
738
|
}
|
|
739
|
+
else if (event.type === "follow") {
|
|
740
|
+
// New user added bot as friend — auto-register and auto-allow
|
|
741
|
+
registerAndAutoAllow(deps, userId);
|
|
742
|
+
// Save reply token if available
|
|
743
|
+
if (event.replyToken) {
|
|
744
|
+
deps.messageStore.saveReplyToken(event.replyToken, event.timestamp);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
670
747
|
}
|
|
671
748
|
return { statusCode: 200, body: "OK" };
|
|
672
749
|
}
|
|
@@ -681,37 +758,65 @@ export function startWebhookServer(deps) {
|
|
|
681
758
|
const prefix = url.startsWith("/files/") ? "/files/" : "/images/";
|
|
682
759
|
// Strip query string and decode percent-encoding for clean ID extraction
|
|
683
760
|
const rawId = url.slice(prefix.length).split("?")[0];
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (!imageId || imageId.includes("/") || imageId.includes("..")) {
|
|
761
|
+
const decodedRawId = decodeURIComponent(rawId);
|
|
762
|
+
if (!decodedRawId || decodedRawId.includes("/") || decodedRawId.includes("..")) {
|
|
687
763
|
res.statusCode = 404;
|
|
688
764
|
res.end("Not found");
|
|
689
765
|
return;
|
|
690
766
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
767
|
+
// Check TTL — all disk-served files MUST be tracked.
|
|
768
|
+
// Untracked files on disk return 404 (strict mode).
|
|
769
|
+
// Race: cleanup may delete between check and readFile → 404 (acceptable).
|
|
770
|
+
const fileExpiry = deps.messageStore.getServedFileExpiry(decodedRawId);
|
|
771
|
+
if (fileExpiry?.isExpired) {
|
|
772
|
+
res.statusCode = 410;
|
|
773
|
+
res.end("Gone — file expired");
|
|
695
774
|
return;
|
|
696
775
|
}
|
|
697
|
-
//
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
776
|
+
// Try disk (files MUST be tracked in served_files table)
|
|
777
|
+
const serveDir = path.resolve(".line-hive-tmp");
|
|
778
|
+
const diskPath = path.resolve(".line-hive-tmp", decodedRawId);
|
|
779
|
+
if (fileExpiry && diskPath.startsWith(serveDir + path.sep) && fs.existsSync(diskPath)) {
|
|
780
|
+
try {
|
|
781
|
+
const fileBuffer = fs.readFileSync(diskPath);
|
|
782
|
+
const ext = path.extname(diskPath).toLowerCase();
|
|
783
|
+
const mimeMap = {
|
|
784
|
+
".html": "text/html",
|
|
785
|
+
".css": "text/css",
|
|
786
|
+
".js": "application/javascript",
|
|
787
|
+
".json": "application/json",
|
|
788
|
+
".png": "image/png",
|
|
789
|
+
".jpg": "image/jpeg",
|
|
790
|
+
".jpeg": "image/jpeg",
|
|
791
|
+
".gif": "image/gif",
|
|
792
|
+
".webp": "image/webp",
|
|
793
|
+
".svg": "image/svg+xml",
|
|
794
|
+
".pdf": "application/pdf",
|
|
795
|
+
};
|
|
796
|
+
const mimeType = mimeMap[ext] || "application/octet-stream";
|
|
797
|
+
const contentType = mimeType.startsWith("text/") ? `${mimeType}; charset=utf-8` : mimeType;
|
|
798
|
+
res.setHeader("Content-Type", contentType);
|
|
799
|
+
res.setHeader("Content-Length", fileBuffer.length);
|
|
800
|
+
res.setHeader("Cache-Control", "public, max-age=60");
|
|
801
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
802
|
+
if (mimeType.startsWith("image/") || mimeType === "text/html" || mimeType === "application/pdf") {
|
|
803
|
+
res.setHeader("Content-Disposition", "inline");
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
res.setHeader("Content-Disposition", "attachment");
|
|
807
|
+
}
|
|
808
|
+
res.statusCode = 200;
|
|
809
|
+
res.end(fileBuffer);
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
deps.logger?.warn({ error: err instanceof Error ? err.message : err, file: decodedRawId }, "Failed to serve file");
|
|
813
|
+
res.statusCode = 500;
|
|
814
|
+
res.end("Internal error");
|
|
815
|
+
}
|
|
816
|
+
return;
|
|
712
817
|
}
|
|
713
|
-
res.statusCode =
|
|
714
|
-
res.end(
|
|
818
|
+
res.statusCode = 404;
|
|
819
|
+
res.end("Not found");
|
|
715
820
|
return;
|
|
716
821
|
}
|
|
717
822
|
if (method !== "POST" || url !== deps.config.webhookPath) {
|