manualofme-bridge 0.2.1 → 0.4.0
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/cli.js +1331 -544
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -34,6 +34,15 @@ import os from "node:os";
|
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
36
|
import { z } from "zod";
|
|
37
|
+
function bridgeInstance(cwd, name, hostname = os.hostname()) {
|
|
38
|
+
const machine = hostname.trim().slice(0, 100) || "PC";
|
|
39
|
+
return {
|
|
40
|
+
id: createHash("sha256").update(`${machine.toLowerCase()}
|
|
41
|
+
${sameCase(path.resolve(cwd))}`).digest("hex").slice(0, 16),
|
|
42
|
+
machine,
|
|
43
|
+
project: (name ?? path.basename(path.resolve(cwd))).slice(0, 200)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
37
46
|
function parseConfig(env) {
|
|
38
47
|
const set = Object.fromEntries(Object.entries(env).filter(([, v]) => v !== void 0 && v !== ""));
|
|
39
48
|
const r = Env.safeParse(set);
|
|
@@ -55,7 +64,8 @@ ${lines.join("\n")}`);
|
|
|
55
64
|
expoAccessToken: e.EXPO_ACCESS_TOKEN,
|
|
56
65
|
tunnel: e.BRIDGE_TUNNEL,
|
|
57
66
|
cloudflaredPath: e.CLOUDFLARED_PATH,
|
|
58
|
-
apiUrl: e.BRIDGE_API_URL.replace(/\/+$/, "")
|
|
67
|
+
apiUrl: e.BRIDGE_API_URL.replace(/\/+$/, ""),
|
|
68
|
+
instance: bridgeInstance(cwd, e.BRIDGE_NAME)
|
|
59
69
|
};
|
|
60
70
|
}
|
|
61
71
|
function loadEnvFiles() {
|
|
@@ -106,7 +116,9 @@ var init_config = __esm({
|
|
|
106
116
|
/** Đường dẫn cloudflared, khi không nằm trong PATH và không ở chỗ winget cài. */
|
|
107
117
|
CLOUDFLARED_PATH: z.string().optional(),
|
|
108
118
|
/** API server để gửi địa chỉ tunnel lên, app tự lấy về. */
|
|
109
|
-
BRIDGE_API_URL: z.url().default(DEFAULT_API_URL)
|
|
119
|
+
BRIDGE_API_URL: z.url().default(DEFAULT_API_URL),
|
|
120
|
+
/** Tên project hiện trong app. Mặc định là tên thư mục làm việc. */
|
|
121
|
+
BRIDGE_NAME: z.string().trim().min(1).max(200).optional()
|
|
110
122
|
});
|
|
111
123
|
envFiles = () => [...APP_DIR ? [path.join(APP_DIR, ".env")] : [], USER_ENV_FILE];
|
|
112
124
|
}
|
|
@@ -247,7 +259,7 @@ var init_sha256 = __esm({
|
|
|
247
259
|
|
|
248
260
|
// ../../packages/shared/src/enums.ts
|
|
249
261
|
import { z as z2 } from "zod";
|
|
250
|
-
var SOURCES, Source, TASK_STATUSES, TaskStatus, TASK_PRIORITIES, TaskPriority, REMINDER_STATUSES, ReminderStatus, TARGET_TYPES, TargetType, TX_TYPES, TxType, CATEGORY_KINDS, CategoryKind, AGENT_AUTONOMIES, AgentAutonomy, AI_PROVIDER_MODES, AiProviderMode, AI_PROVIDERS, AiProvider, CONVERSATION_STATUSES, ConversationStatus, DEVICE_PLATFORMS, DevicePlatform, NOTICE_KINDS, NoticeKind;
|
|
262
|
+
var SOURCES, Source, TASK_STATUSES, TaskStatus, TASK_PRIORITIES, TaskPriority, REPEAT_FROMS, RepeatFrom, PLANNED_BYS, PlannedBy, REMINDER_STATUSES, ReminderStatus, TARGET_TYPES, TargetType, TX_TYPES, TxType, EVENT_SCOPES, EventScope, CATEGORY_KINDS, CategoryKind, AGENT_AUTONOMIES, AgentAutonomy, AI_PROVIDER_MODES, AiProviderMode, AI_PROVIDERS, AiProvider, CONVERSATION_STATUSES, ConversationStatus, DEVICE_PLATFORMS, DevicePlatform, NOTICE_KINDS, NoticeKind;
|
|
251
263
|
var init_enums = __esm({
|
|
252
264
|
"../../packages/shared/src/enums.ts"() {
|
|
253
265
|
"use strict";
|
|
@@ -257,12 +269,27 @@ var init_enums = __esm({
|
|
|
257
269
|
TaskStatus = z2.enum(TASK_STATUSES);
|
|
258
270
|
TASK_PRIORITIES = ["low", "medium", "high", "urgent"];
|
|
259
271
|
TaskPriority = z2.enum(TASK_PRIORITIES);
|
|
272
|
+
REPEAT_FROMS = ["due", "completion"];
|
|
273
|
+
RepeatFrom = z2.enum(REPEAT_FROMS);
|
|
274
|
+
PLANNED_BYS = ["auto", "user"];
|
|
275
|
+
PlannedBy = z2.enum(PLANNED_BYS);
|
|
260
276
|
REMINDER_STATUSES = ["scheduled", "fired", "snoozed", "done", "cancelled"];
|
|
261
277
|
ReminderStatus = z2.enum(REMINDER_STATUSES);
|
|
262
|
-
TARGET_TYPES = [
|
|
278
|
+
TARGET_TYPES = [
|
|
279
|
+
"task",
|
|
280
|
+
"note",
|
|
281
|
+
"transaction",
|
|
282
|
+
"person",
|
|
283
|
+
"event",
|
|
284
|
+
"habit",
|
|
285
|
+
"bill",
|
|
286
|
+
"none"
|
|
287
|
+
];
|
|
263
288
|
TargetType = z2.enum(TARGET_TYPES);
|
|
264
289
|
TX_TYPES = ["expense", "income", "transfer", "debt_out", "debt_in"];
|
|
265
290
|
TxType = z2.enum(TX_TYPES);
|
|
291
|
+
EVENT_SCOPES = ["this", "following", "all"];
|
|
292
|
+
EventScope = z2.enum(EVENT_SCOPES);
|
|
266
293
|
CATEGORY_KINDS = ["expense", "income"];
|
|
267
294
|
CategoryKind = z2.enum(CATEGORY_KINDS);
|
|
268
295
|
AGENT_AUTONOMIES = ["ask_more", "balanced", "act_more"];
|
|
@@ -279,7 +306,21 @@ var init_enums = __esm({
|
|
|
279
306
|
"daily_briefing",
|
|
280
307
|
"debt_digest",
|
|
281
308
|
"budget_alert",
|
|
282
|
-
"ai_budget_alert"
|
|
309
|
+
"ai_budget_alert",
|
|
310
|
+
"google_sync_error",
|
|
311
|
+
"bill_logged",
|
|
312
|
+
"afternoon_briefing",
|
|
313
|
+
"evening_review",
|
|
314
|
+
"meeting_reminder",
|
|
315
|
+
"calendar_alert",
|
|
316
|
+
"weekly_preview",
|
|
317
|
+
// Mục tiêu (docs/10 §1.4)
|
|
318
|
+
"goal_plan",
|
|
319
|
+
"goal_risk",
|
|
320
|
+
"goal_review",
|
|
321
|
+
"goal_milestone",
|
|
322
|
+
"goal_block"
|
|
323
|
+
// nhắc trước khối giờ làm việc mục tiêu do bộ xếp lịch đặt
|
|
283
324
|
];
|
|
284
325
|
NoticeKind = z2.enum(NOTICE_KINDS);
|
|
285
326
|
}
|
|
@@ -290,12 +331,14 @@ import { z as z3 } from "zod";
|
|
|
290
331
|
function queryList(item) {
|
|
291
332
|
return z3.string().transform((v) => v.split(",").filter(Boolean)).pipe(z3.array(item).min(1));
|
|
292
333
|
}
|
|
293
|
-
var Id, IsoDateTime, QueryBool, PageQuery;
|
|
334
|
+
var Id, IsoDateTime, DateKey, HhMm, QueryBool, PageQuery;
|
|
294
335
|
var init_common = __esm({
|
|
295
336
|
"../../packages/shared/src/schemas/common.ts"() {
|
|
296
337
|
"use strict";
|
|
297
338
|
Id = z3.string().min(1).max(64);
|
|
298
339
|
IsoDateTime = z3.iso.datetime({ offset: true });
|
|
340
|
+
DateKey = z3.string().regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/, "C\u1EA7n d\u1EA1ng YYYY-MM-DD");
|
|
341
|
+
HhMm = z3.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "\u0110\u1ECBnh d\u1EA1ng HH:mm");
|
|
299
342
|
QueryBool = z3.enum(["true", "false"]).transform((v) => v === "true");
|
|
300
343
|
PageQuery = z3.object({
|
|
301
344
|
cursor: z3.string().max(500).optional(),
|
|
@@ -304,335 +347,6 @@ var init_common = __esm({
|
|
|
304
347
|
}
|
|
305
348
|
});
|
|
306
349
|
|
|
307
|
-
// ../../packages/shared/src/schemas/notify.ts
|
|
308
|
-
import { z as z4 } from "zod";
|
|
309
|
-
var ExpoToken, WebPushSubscription, WebToken, RegisterDevice, Device, Notice, ListNoticesQuery, Connector, ReadNotices;
|
|
310
|
-
var init_notify = __esm({
|
|
311
|
-
"../../packages/shared/src/schemas/notify.ts"() {
|
|
312
|
-
"use strict";
|
|
313
|
-
init_enums();
|
|
314
|
-
init_common();
|
|
315
|
-
ExpoToken = z4.string().regex(/^Expo(nent)?PushToken\[[^\]]+\]$/, "Kh\xF4ng ph\u1EA3i Expo push token");
|
|
316
|
-
WebPushSubscription = z4.object({
|
|
317
|
-
endpoint: z4.url({ protocol: /^https$/ }).max(1e3),
|
|
318
|
-
keys: z4.object({
|
|
319
|
-
p256dh: z4.string().min(80).max(100),
|
|
320
|
-
// khoá công khai P-256 dạng base64url (65 byte)
|
|
321
|
-
auth: z4.string().min(16).max(30)
|
|
322
|
-
// 16 byte base64url
|
|
323
|
-
})
|
|
324
|
-
});
|
|
325
|
-
WebToken = z4.union([z4.string().max(2e3), z4.record(z4.string(), z4.unknown())]).transform((v, ctx) => {
|
|
326
|
-
let raw = v;
|
|
327
|
-
if (typeof v === "string") {
|
|
328
|
-
try {
|
|
329
|
-
raw = JSON.parse(v);
|
|
330
|
-
} catch {
|
|
331
|
-
ctx.addIssue({ code: "custom", message: "pushToken kh\xF4ng ph\u1EA3i JSON" });
|
|
332
|
-
return z4.NEVER;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const sub = WebPushSubscription.safeParse(raw);
|
|
336
|
-
if (!sub.success) {
|
|
337
|
-
ctx.addIssue({ code: "custom", message: "Subscription Web Push kh\xF4ng h\u1EE3p l\u1EC7" });
|
|
338
|
-
return z4.NEVER;
|
|
339
|
-
}
|
|
340
|
-
const { endpoint, keys } = sub.data;
|
|
341
|
-
return JSON.stringify({ endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } });
|
|
342
|
-
});
|
|
343
|
-
RegisterDevice = z4.discriminatedUnion("platform", [
|
|
344
|
-
z4.object({ platform: z4.literal("android"), pushToken: ExpoToken }).strict(),
|
|
345
|
-
z4.object({ platform: z4.literal("web"), pushToken: WebToken }).strict()
|
|
346
|
-
]);
|
|
347
|
-
Device = z4.object({
|
|
348
|
-
id: z4.string(),
|
|
349
|
-
platform: DevicePlatform,
|
|
350
|
-
lastSeen: z4.string()
|
|
351
|
-
});
|
|
352
|
-
Notice = z4.object({
|
|
353
|
-
id: z4.string(),
|
|
354
|
-
kind: NoticeKind,
|
|
355
|
-
title: z4.string(),
|
|
356
|
-
body: z4.string(),
|
|
357
|
-
data: z4.record(z4.string(), z4.unknown()).nullable(),
|
|
358
|
-
// vd {budgetId, month} cho nút "Xem chi tiết"
|
|
359
|
-
createdAt: z4.string(),
|
|
360
|
-
readAt: z4.string().nullable()
|
|
361
|
-
});
|
|
362
|
-
ListNoticesQuery = PageQuery.extend({ unread: QueryBool.optional() });
|
|
363
|
-
Connector = z4.object({
|
|
364
|
-
clientId: z4.string(),
|
|
365
|
-
name: z4.string(),
|
|
366
|
-
uri: z4.string().nullable(),
|
|
367
|
-
icon: z4.string().nullable(),
|
|
368
|
-
scopes: z4.array(z4.string()),
|
|
369
|
-
connectedAt: z4.string(),
|
|
370
|
-
lastUsedAt: z4.string().nullable()
|
|
371
|
-
// lần cấp access token gần nhất (khoảng mỗi giờ khi đang dùng)
|
|
372
|
-
});
|
|
373
|
-
ReadNotices = z4.object({ ids: z4.array(z4.string()).min(1).max(200).optional() }).strict();
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ../../packages/shared/src/schemas/bridge.ts
|
|
378
|
-
import { z as z5 } from "zod";
|
|
379
|
-
function deriveBridgeKey(token) {
|
|
380
|
-
const digest2 = sha256(new TextEncoder().encode(`manualofme-bridge-url:${token}`));
|
|
381
|
-
return `${BRIDGE_KEY_PREFIX}${base64url(digest2)}`;
|
|
382
|
-
}
|
|
383
|
-
var BRIDGE_SUBPROTOCOL, BRIDGE_CLOSE_UNAUTHORIZED, BRIDGE_PERMISSION_MODES, BridgePermissionMode, BridgeModel, BRIDGE_MAX_ATTACHMENTS, BridgeAttachment, BridgeClientMessage, BridgeChatItem, BridgePermissionRequest, BridgeSessionInfo, BridgeServerMessage, BRIDGE_KEY_PREFIX, RegisterBridgeKey, BridgePublicUrl, PublishBridgeUrl, BridgeEndpoint;
|
|
384
|
-
var init_bridge = __esm({
|
|
385
|
-
"../../packages/shared/src/schemas/bridge.ts"() {
|
|
386
|
-
"use strict";
|
|
387
|
-
init_sha256();
|
|
388
|
-
init_notify();
|
|
389
|
-
BRIDGE_SUBPROTOCOL = "manualofme-bridge";
|
|
390
|
-
BRIDGE_CLOSE_UNAUTHORIZED = 4401;
|
|
391
|
-
BRIDGE_PERMISSION_MODES = [
|
|
392
|
-
"default",
|
|
393
|
-
"acceptEdits",
|
|
394
|
-
"plan",
|
|
395
|
-
"bypassPermissions"
|
|
396
|
-
];
|
|
397
|
-
BridgePermissionMode = z5.enum(BRIDGE_PERMISSION_MODES);
|
|
398
|
-
BridgeModel = z5.string().max(100).regex(/^[A-Za-z0-9._[\]-]+$/, "T\xEAn model kh\xF4ng h\u1EE3p l\u1EC7").nullable();
|
|
399
|
-
BRIDGE_MAX_ATTACHMENTS = 4;
|
|
400
|
-
BridgeAttachment = z5.object({
|
|
401
|
-
name: z5.string().trim().min(1).max(200),
|
|
402
|
-
mimeType: z5.string().max(200),
|
|
403
|
-
data: z5.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "data ph\u1EA3i l\xE0 base64")
|
|
404
|
-
}).strict();
|
|
405
|
-
BridgeClientMessage = z5.discriminatedUnion("type", [
|
|
406
|
-
/** Liệt kê các session của project (cả session mở từ VS Code). */
|
|
407
|
-
z5.object({ type: z5.literal("list_sessions") }).strict(),
|
|
408
|
-
/** Chuyển sang session khác. `null` = session mới (tạo khi gửi tin đầu tiên). */
|
|
409
|
-
z5.object({ type: z5.literal("open_session"), sessionId: z5.uuid().nullable() }).strict(),
|
|
410
|
-
z5.object({
|
|
411
|
-
type: z5.literal("send"),
|
|
412
|
-
/** Rỗng được khi có tệp đính kèm. */
|
|
413
|
-
text: z5.string().trim().max(2e4),
|
|
414
|
-
clientId: z5.string().max(64).optional(),
|
|
415
|
-
// bridge trả lại trong item user để app khử trùng
|
|
416
|
-
attachments: z5.array(BridgeAttachment).max(BRIDGE_MAX_ATTACHMENTS).optional()
|
|
417
|
-
}).strict(),
|
|
418
|
-
/**
|
|
419
|
-
* Đổi chế độ duyệt / model, áp cho mọi app và nhớ qua lần khởi động lại. Đổi chế độ khi đang
|
|
420
|
-
* chạy thì các yêu cầu duyệt mà chế độ mới tự cho phép được duyệt luôn; model áp từ lượt sau.
|
|
421
|
-
*/
|
|
422
|
-
z5.object({
|
|
423
|
-
type: z5.literal("set_options"),
|
|
424
|
-
permissionMode: BridgePermissionMode.optional(),
|
|
425
|
-
model: BridgeModel.optional()
|
|
426
|
-
}).strict(),
|
|
427
|
-
/** Dừng lượt đang chạy. */
|
|
428
|
-
z5.object({ type: z5.literal("interrupt") }).strict(),
|
|
429
|
-
z5.object({
|
|
430
|
-
type: z5.literal("permission_reply"),
|
|
431
|
-
requestId: z5.string().min(1).max(200),
|
|
432
|
-
allow: z5.boolean(),
|
|
433
|
-
message: z5.string().max(1e3).optional()
|
|
434
|
-
// lý do từ chối, gửi lại cho Claude
|
|
435
|
-
}).strict(),
|
|
436
|
-
/** Đăng ký Expo push token để nhận thông báo khi xong việc / cần duyệt. */
|
|
437
|
-
z5.object({ type: z5.literal("register_push"), token: ExpoToken }).strict(),
|
|
438
|
-
z5.object({ type: z5.literal("unregister_push"), token: ExpoToken }).strict()
|
|
439
|
-
]);
|
|
440
|
-
BridgeChatItem = z5.discriminatedUnion("kind", [
|
|
441
|
-
z5.object({
|
|
442
|
-
kind: z5.literal("user"),
|
|
443
|
-
id: z5.string(),
|
|
444
|
-
text: z5.string(),
|
|
445
|
-
clientId: z5.string().optional()
|
|
446
|
-
// chỉ có với tin vừa gửi từ app
|
|
447
|
-
}),
|
|
448
|
-
z5.object({ kind: z5.literal("assistant"), id: z5.string(), text: z5.string() }),
|
|
449
|
-
/** Claude gọi tool. `summary` là phần dễ đọc của input: lệnh bash, đường dẫn file... */
|
|
450
|
-
z5.object({ kind: z5.literal("tool"), id: z5.string(), name: z5.string(), summary: z5.string() })
|
|
451
|
-
]);
|
|
452
|
-
BridgePermissionRequest = z5.object({
|
|
453
|
-
requestId: z5.string(),
|
|
454
|
-
toolName: z5.string(),
|
|
455
|
-
title: z5.string(),
|
|
456
|
-
// câu mô tả sẵn, vd "Claude wants to run: pnpm test"
|
|
457
|
-
input: z5.record(z5.string(), z5.unknown())
|
|
458
|
-
});
|
|
459
|
-
BridgeSessionInfo = z5.object({
|
|
460
|
-
sessionId: z5.string(),
|
|
461
|
-
title: z5.string(),
|
|
462
|
-
updatedAt: z5.string()
|
|
463
|
-
// ISO
|
|
464
|
-
});
|
|
465
|
-
BridgeServerMessage = z5.discriminatedUnion("type", [
|
|
466
|
-
/** Gửi khi vừa kết nối và mỗi khi trạng thái đổi. */
|
|
467
|
-
z5.object({
|
|
468
|
-
type: z5.literal("state"),
|
|
469
|
-
sessionId: z5.string().nullable(),
|
|
470
|
-
busy: z5.boolean(),
|
|
471
|
-
// đang có lượt chạy
|
|
472
|
-
pendingPermissions: z5.array(BridgePermissionRequest),
|
|
473
|
-
// Bridge bản cũ không gửi hai trường này (chưa đổi được chế độ / model từ app)
|
|
474
|
-
permissionMode: BridgePermissionMode.optional(),
|
|
475
|
-
model: BridgeModel.optional()
|
|
476
|
-
}),
|
|
477
|
-
/** Toàn bộ lịch sử (đã cắt bớt) của session hiện tại: khi kết nối và khi đổi session. */
|
|
478
|
-
z5.object({
|
|
479
|
-
type: z5.literal("history"),
|
|
480
|
-
sessionId: z5.string().nullable(),
|
|
481
|
-
items: z5.array(BridgeChatItem)
|
|
482
|
-
}),
|
|
483
|
-
z5.object({ type: z5.literal("sessions"), sessions: z5.array(BridgeSessionInfo) }),
|
|
484
|
-
/** Dòng mới trong lượt đang chạy. */
|
|
485
|
-
z5.object({ type: z5.literal("item"), sessionId: z5.string().nullable(), item: BridgeChatItem }),
|
|
486
|
-
z5.object({ type: z5.literal("permission_request"), request: BridgePermissionRequest }),
|
|
487
|
-
z5.object({
|
|
488
|
-
type: z5.literal("permission_resolved"),
|
|
489
|
-
requestId: z5.string(),
|
|
490
|
-
allow: z5.boolean()
|
|
491
|
-
}),
|
|
492
|
-
z5.object({
|
|
493
|
-
type: z5.literal("turn_end"),
|
|
494
|
-
sessionId: z5.string().nullable(),
|
|
495
|
-
ok: z5.boolean(),
|
|
496
|
-
text: z5.string(),
|
|
497
|
-
// câu trả lời cuối, hoặc lỗi khi ok = false
|
|
498
|
-
costUsd: z5.number(),
|
|
499
|
-
durationMs: z5.number()
|
|
500
|
-
}),
|
|
501
|
-
z5.object({
|
|
502
|
-
type: z5.literal("error"),
|
|
503
|
-
message: z5.string(),
|
|
504
|
-
clientId: z5.string().optional()
|
|
505
|
-
// lỗi do tin `send` có clientId này
|
|
506
|
-
})
|
|
507
|
-
]);
|
|
508
|
-
BRIDGE_KEY_PREFIX = "mobk_";
|
|
509
|
-
RegisterBridgeKey = z5.object({ key: z5.string().regex(/^mobk_[A-Za-z0-9_-]{43}$/, "Kho\xE1 bridge kh\xF4ng h\u1EE3p l\u1EC7") }).strict();
|
|
510
|
-
BridgePublicUrl = z5.string().max(500).regex(/^wss:\/\/[a-z0-9.-]+(:\d+)?\/[^\s?#]*$/i, "Ph\u1EA3i c\xF3 d\u1EA1ng wss://<host>/ws");
|
|
511
|
-
PublishBridgeUrl = z5.object({ url: BridgePublicUrl.nullable(), ifUrl: BridgePublicUrl.nullable().optional() }).strict();
|
|
512
|
-
BridgeEndpoint = z5.object({
|
|
513
|
-
url: z5.string().nullable(),
|
|
514
|
-
// null = bridge chưa gửi địa chỉ hoặc đã tắt
|
|
515
|
-
updatedAt: z5.string().nullable(),
|
|
516
|
-
hasKey: z5.boolean()
|
|
517
|
-
// app đã đăng ký khoá bridge chưa
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// src/announce.ts
|
|
523
|
-
var RETRY_MIN_MS, RETRY_MAX_MS, UNREGISTERED_RETRY_MS, UrlAnnouncer;
|
|
524
|
-
var init_announce = __esm({
|
|
525
|
-
"src/announce.ts"() {
|
|
526
|
-
"use strict";
|
|
527
|
-
RETRY_MIN_MS = 2e3;
|
|
528
|
-
RETRY_MAX_MS = 6e4;
|
|
529
|
-
UNREGISTERED_RETRY_MS = 1e4;
|
|
530
|
-
UrlAnnouncer = class {
|
|
531
|
-
constructor(opts) {
|
|
532
|
-
this.opts = opts;
|
|
533
|
-
this.fetchFn = opts.fetchFn ?? fetch;
|
|
534
|
-
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref()));
|
|
535
|
-
}
|
|
536
|
-
/** Địa chỉ còn chờ gửi; `undefined` = không có gì chờ. */
|
|
537
|
-
want = void 0;
|
|
538
|
-
/** Địa chỉ server đang giữ do bridge này gửi; `undefined` = chưa gửi được lần nào. */
|
|
539
|
-
published = void 0;
|
|
540
|
-
replaced = false;
|
|
541
|
-
running = false;
|
|
542
|
-
/** Đã báo khoá chưa đăng ký, để không in lặp mỗi lần thử. */
|
|
543
|
-
warned = false;
|
|
544
|
-
fetchFn;
|
|
545
|
-
sleep;
|
|
546
|
-
/** Gửi địa chỉ mới (`null` = bridge tắt). Lỗi mạng thì tự thử lại, địa chỉ mới nhất thắng. */
|
|
547
|
-
announce(url) {
|
|
548
|
-
if (this.replaced) return Promise.resolve();
|
|
549
|
-
this.want = url;
|
|
550
|
-
return this.run();
|
|
551
|
-
}
|
|
552
|
-
/** Gọi định kỳ: hỏi server còn giữ địa chỉ của bridge này không, để biết sớm khi bị máy khác thay. */
|
|
553
|
-
async check() {
|
|
554
|
-
if (this.running || this.replaced || typeof this.published !== "string") return;
|
|
555
|
-
if (await this.send(this.published, this.published) === "replaced") this.markReplaced();
|
|
556
|
-
}
|
|
557
|
-
/** Lúc tắt bridge: báo server bridge đã tắt, không chờ quá `timeoutMs`. */
|
|
558
|
-
async clear(timeoutMs = 2e3) {
|
|
559
|
-
this.want = void 0;
|
|
560
|
-
if (this.replaced || !this.published) return;
|
|
561
|
-
await this.send(null, this.published, AbortSignal.timeout(timeoutMs));
|
|
562
|
-
}
|
|
563
|
-
markReplaced() {
|
|
564
|
-
if (this.replaced) return;
|
|
565
|
-
this.replaced = true;
|
|
566
|
-
this.want = void 0;
|
|
567
|
-
console.log("Bridge \u1EDF m\xE1y kh\xE1c v\u1EEBa b\u1EADt v\xE0 \u0111\xE3 nh\u1EADn k\u1EBFt n\u1ED1i, bridge n\xE0y t\u1EF1 t\u1EAFt.");
|
|
568
|
-
this.opts.onReplaced?.();
|
|
569
|
-
}
|
|
570
|
-
async run() {
|
|
571
|
-
if (this.running) return;
|
|
572
|
-
this.running = true;
|
|
573
|
-
let delay = RETRY_MIN_MS;
|
|
574
|
-
try {
|
|
575
|
-
while (this.want !== void 0) {
|
|
576
|
-
const url = this.want;
|
|
577
|
-
if (url === null && this.published === void 0) {
|
|
578
|
-
this.want = void 0;
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
const result = await this.send(url, this.published);
|
|
582
|
-
if (result === "replaced") {
|
|
583
|
-
this.markReplaced();
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
if (result === "unauthorized") {
|
|
587
|
-
if (!this.warned) {
|
|
588
|
-
this.warned = true;
|
|
589
|
-
console.log(
|
|
590
|
-
"Server ch\u01B0a nh\u1EADn kho\xE1 c\u1EE7a bridge: m\u1EDF web ho\u1EB7c app \u2192 Claude PC \u2192 C\xE0i \u0111\u1EB7t k\u1EBFt n\u1ED1i, d\xE1n BRIDGE_TOKEN, \u0111\u1EC3 tr\u1ED1ng URL r\u1ED3i L\u01B0u. Bridge t\u1EF1 th\u1EED l\u1EA1i."
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
await this.sleep(UNREGISTERED_RETRY_MS);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
if (result === "ok") {
|
|
597
|
-
this.published = url;
|
|
598
|
-
if (this.want === url) this.want = void 0;
|
|
599
|
-
delay = RETRY_MIN_MS;
|
|
600
|
-
this.warned = false;
|
|
601
|
-
if (url) console.log("\u0110\xE3 g\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server, app s\u1EBD t\u1EF1 k\u1EBFt n\u1ED1i.");
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
await this.sleep(delay);
|
|
605
|
-
delay = Math.min(RETRY_MAX_MS, delay * 2);
|
|
606
|
-
}
|
|
607
|
-
} finally {
|
|
608
|
-
this.running = false;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/** `ifUrl` khác `undefined` thì server chỉ ghi khi địa chỉ đang lưu đúng bằng nó. */
|
|
612
|
-
async send(url, ifUrl, signal) {
|
|
613
|
-
try {
|
|
614
|
-
const res = await this.fetchFn(`${this.opts.apiUrl}/v1/bridge/url`, {
|
|
615
|
-
method: "PUT",
|
|
616
|
-
headers: {
|
|
617
|
-
Authorization: `Bearer ${this.opts.key}`,
|
|
618
|
-
"Content-Type": "application/json"
|
|
619
|
-
},
|
|
620
|
-
body: JSON.stringify(ifUrl === void 0 ? { url } : { url, ifUrl }),
|
|
621
|
-
signal
|
|
622
|
-
});
|
|
623
|
-
if (res.ok) return "ok";
|
|
624
|
-
if (res.status === 401) return "unauthorized";
|
|
625
|
-
if (res.status === 409) return "replaced";
|
|
626
|
-
console.error(`G\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server l\u1ED7i HTTP ${res.status}, th\u1EED l\u1EA1i sau`);
|
|
627
|
-
return "retry";
|
|
628
|
-
} catch {
|
|
629
|
-
return "retry";
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
|
|
636
350
|
// ../../node_modules/.pnpm/rrule@2.8.1/node_modules/rrule/dist/es5/rrule.js
|
|
637
351
|
var require_rrule = __commonJS({
|
|
638
352
|
"../../node_modules/.pnpm/rrule@2.8.1/node_modules/rrule/dist/es5/rrule.js"(exports, module) {
|
|
@@ -722,8 +436,8 @@ var require_rrule = __commonJS({
|
|
|
722
436
|
this.weekday = weekday;
|
|
723
437
|
this.n = n;
|
|
724
438
|
}
|
|
725
|
-
Weekday2.fromStr = function(
|
|
726
|
-
return new Weekday2(ALL_WEEKDAYS.indexOf(
|
|
439
|
+
Weekday2.fromStr = function(str2) {
|
|
440
|
+
return new Weekday2(ALL_WEEKDAYS.indexOf(str2));
|
|
727
441
|
};
|
|
728
442
|
Weekday2.prototype.nth = function(n) {
|
|
729
443
|
return this.n === n ? this : new Weekday2(this.weekday, n);
|
|
@@ -792,20 +506,20 @@ var require_rrule = __commonJS({
|
|
|
792
506
|
if (padString === void 0) {
|
|
793
507
|
padString = " ";
|
|
794
508
|
}
|
|
795
|
-
var
|
|
509
|
+
var str2 = String(item);
|
|
796
510
|
targetLength = targetLength >> 0;
|
|
797
|
-
if (
|
|
798
|
-
return String(
|
|
511
|
+
if (str2.length > targetLength) {
|
|
512
|
+
return String(str2);
|
|
799
513
|
}
|
|
800
|
-
targetLength = targetLength -
|
|
514
|
+
targetLength = targetLength - str2.length;
|
|
801
515
|
if (targetLength > padString.length) {
|
|
802
516
|
padString += repeat(padString, targetLength / padString.length);
|
|
803
517
|
}
|
|
804
|
-
return padString.slice(0, targetLength) + String(
|
|
518
|
+
return padString.slice(0, targetLength) + String(str2);
|
|
805
519
|
}
|
|
806
|
-
var split = function(
|
|
807
|
-
var splits =
|
|
808
|
-
return
|
|
520
|
+
var split = function(str2, sep, num3) {
|
|
521
|
+
var splits = str2.split(sep);
|
|
522
|
+
return num3 ? splits.slice(0, num3).concat([splits.slice(num3).join(sep)]) : splits;
|
|
809
523
|
};
|
|
810
524
|
var pymod = function(a, b) {
|
|
811
525
|
var r = a % b;
|
|
@@ -2545,9 +2259,9 @@ var require_rrule = __commonJS({
|
|
|
2545
2259
|
case "BYHOUR":
|
|
2546
2260
|
case "BYMINUTE":
|
|
2547
2261
|
case "BYSECOND":
|
|
2548
|
-
var
|
|
2262
|
+
var num3 = parseNumber(value);
|
|
2549
2263
|
var optionKey = key.toLowerCase();
|
|
2550
|
-
options[optionKey] =
|
|
2264
|
+
options[optionKey] = num3;
|
|
2551
2265
|
break;
|
|
2552
2266
|
case "BYWEEKDAY":
|
|
2553
2267
|
case "BYDAY":
|
|
@@ -3359,8 +3073,8 @@ var require_rrule = __commonJS({
|
|
|
3359
3073
|
RRule3.fromText = function(text, language) {
|
|
3360
3074
|
return fromText(text, language);
|
|
3361
3075
|
};
|
|
3362
|
-
RRule3.fromString = function(
|
|
3363
|
-
return new RRule3(RRule3.parseString(
|
|
3076
|
+
RRule3.fromString = function(str2) {
|
|
3077
|
+
return new RRule3(RRule3.parseString(str2) || void 0);
|
|
3364
3078
|
};
|
|
3365
3079
|
RRule3.prototype._iter = function(iterResult) {
|
|
3366
3080
|
return iter(iterResult, this.options);
|
|
@@ -3864,6 +3578,13 @@ var require_rrule = __commonJS({
|
|
|
3864
3578
|
}
|
|
3865
3579
|
});
|
|
3866
3580
|
|
|
3581
|
+
// ../../packages/shared/src/lunar.ts
|
|
3582
|
+
var init_lunar = __esm({
|
|
3583
|
+
"../../packages/shared/src/lunar.ts"() {
|
|
3584
|
+
"use strict";
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3867
3588
|
// ../../packages/shared/src/time.ts
|
|
3868
3589
|
var init_time = __esm({
|
|
3869
3590
|
"../../packages/shared/src/time.ts"() {
|
|
@@ -3889,34 +3610,55 @@ function parseUntil(value) {
|
|
|
3889
3610
|
}
|
|
3890
3611
|
return { date, dateOnly: m[4] === void 0 };
|
|
3891
3612
|
}
|
|
3892
|
-
function
|
|
3893
|
-
const
|
|
3894
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3613
|
+
function parseParts(body, rules, lunar) {
|
|
3614
|
+
const parts = /* @__PURE__ */ new Map();
|
|
3895
3615
|
for (const part of body.split(";")) {
|
|
3896
3616
|
const [key, value, ...rest] = part.split("=");
|
|
3897
3617
|
if (!key || value === void 0 || rest.length > 0) {
|
|
3898
3618
|
throw new RRuleError(`"${part}" kh\xF4ng \u0111\xFAng d\u1EA1ng KEY=VALUE`);
|
|
3899
3619
|
}
|
|
3900
|
-
const rule =
|
|
3901
|
-
if (!rule)
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3620
|
+
const rule = rules[key];
|
|
3621
|
+
if (!rule) {
|
|
3622
|
+
throw new RRuleError(lunar ? `L\u1ECBch \xE2m kh\xF4ng h\u1ED7 tr\u1EE3 ${key}` : `Kh\xF4ng h\u1ED7 tr\u1EE3 ${key}`);
|
|
3623
|
+
}
|
|
3624
|
+
if (parts.has(key)) throw new RRuleError(`${key} xu\u1EA5t hi\u1EC7n nhi\u1EC1u l\u1EA7n`);
|
|
3625
|
+
if (!rule.test(value)) {
|
|
3626
|
+
throw new RRuleError(
|
|
3627
|
+
`${key}=${value} kh\xF4ng h\u1EE3p l\u1EC7${lunar ? " (l\u1ECBch \xE2m)" : ""}, c\u1EA7n ${rule.hint}`
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
parts.set(key, value);
|
|
3905
3631
|
}
|
|
3906
|
-
if (!
|
|
3907
|
-
if (
|
|
3632
|
+
if (!parts.has("FREQ")) throw new RRuleError("Thi\u1EBFu FREQ");
|
|
3633
|
+
if (parts.has("COUNT") && parts.has("UNTIL")) {
|
|
3908
3634
|
throw new RRuleError("Kh\xF4ng d\xF9ng COUNT v\xE0 UNTIL c\xF9ng l\xFAc");
|
|
3909
3635
|
}
|
|
3636
|
+
if (lunar && parts.has("BYMONTH") && parts.get("FREQ") !== "YEARLY") {
|
|
3637
|
+
throw new RRuleError("L\u1ECBch \xE2m: BYMONTH ch\u1EC9 d\xF9ng v\u1EDBi FREQ=YEARLY");
|
|
3638
|
+
}
|
|
3639
|
+
return parts;
|
|
3640
|
+
}
|
|
3641
|
+
function normalizeRRule(input) {
|
|
3642
|
+
const text = input.trim().toUpperCase();
|
|
3643
|
+
if (text.startsWith(LUNAR_PREFIX)) {
|
|
3644
|
+
const body2 = text.slice(LUNAR_PREFIX.length).replace(/^RRULE:/, "");
|
|
3645
|
+
parseParts(body2, LUNAR_RULES, true);
|
|
3646
|
+
return LUNAR_PREFIX + body2;
|
|
3647
|
+
}
|
|
3648
|
+
const body = text.replace(/^RRULE:/, "");
|
|
3649
|
+
parseParts(body, RULES, false);
|
|
3910
3650
|
return body;
|
|
3911
3651
|
}
|
|
3912
|
-
var import_rrule, RRuleError, WEEKDAY, BYDAY_ITEM, RULES;
|
|
3652
|
+
var import_rrule, RRuleError, LUNAR_PREFIX, WEEKDAY, BYDAY_ITEM, RULES, LUNAR_RULES;
|
|
3913
3653
|
var init_recurrence = __esm({
|
|
3914
3654
|
"../../packages/shared/src/recurrence.ts"() {
|
|
3915
3655
|
"use strict";
|
|
3916
3656
|
import_rrule = __toESM(require_rrule(), 1);
|
|
3657
|
+
init_lunar();
|
|
3917
3658
|
init_time();
|
|
3918
3659
|
RRuleError = class extends Error {
|
|
3919
3660
|
};
|
|
3661
|
+
LUNAR_PREFIX = "LUNAR:";
|
|
3920
3662
|
WEEKDAY = "(?:MO|TU|WE|TH|FR|SA|SU)";
|
|
3921
3663
|
BYDAY_ITEM = new RegExp(`^(?:[+-]?(?:[1-9]|[1-4]\\d|5[0-3]))?${WEEKDAY}$`);
|
|
3922
3664
|
RULES = {
|
|
@@ -3938,12 +3680,22 @@ var init_recurrence = __esm({
|
|
|
3938
3680
|
BYSETPOS: { test: (v) => intList(v, 1, 366, true), hint: "1..366 ho\u1EB7c -366..-1" },
|
|
3939
3681
|
WKST: { test: (v) => new RegExp(`^${WEEKDAY}$`).test(v), hint: "MO..SU" }
|
|
3940
3682
|
};
|
|
3683
|
+
LUNAR_RULES = {
|
|
3684
|
+
FREQ: { test: (v) => /^(?:YEARLY|MONTHLY)$/.test(v), hint: "MONTHLY ho\u1EB7c YEARLY" },
|
|
3685
|
+
INTERVAL: RULES.INTERVAL,
|
|
3686
|
+
COUNT: RULES.COUNT,
|
|
3687
|
+
UNTIL: { test: RULES.UNTIL.test, hint: "ng\xE0y d\u01B0\u01A1ng YYYYMMDD ho\u1EB7c YYYYMMDDTHHMMSSZ" },
|
|
3688
|
+
BYMONTH: RULES.BYMONTH,
|
|
3689
|
+
BYMONTHDAY: { test: (v) => intList(v, 1, 30), hint: "ng\xE0y \xE2m 1..30" },
|
|
3690
|
+
BYHOUR: RULES.BYHOUR,
|
|
3691
|
+
BYMINUTE: RULES.BYMINUTE
|
|
3692
|
+
};
|
|
3941
3693
|
}
|
|
3942
3694
|
});
|
|
3943
3695
|
|
|
3944
3696
|
// ../../packages/shared/src/schemas/reminders.ts
|
|
3945
|
-
import { z as
|
|
3946
|
-
var MAX_SNOOZE_MIN, RRuleString, Reminder, ReminderFields, targetConsistent, CreateReminder, UpdateReminder, SnoozeReminder, ListRemindersQuery;
|
|
3697
|
+
import { z as z4 } from "zod";
|
|
3698
|
+
var MAX_SNOOZE_MIN, MAX_REMINDER_OFFSET_MIN, MAX_CONDITION_LENGTH, ReminderOffsetMin, RRuleString, Reminder, ReminderFields, targetConsistent, CreateReminder, UpdateReminder, SnoozeReminder, ListRemindersQuery;
|
|
3947
3699
|
var init_reminders = __esm({
|
|
3948
3700
|
"../../packages/shared/src/schemas/reminders.ts"() {
|
|
3949
3701
|
"use strict";
|
|
@@ -3951,8 +3703,11 @@ var init_reminders = __esm({
|
|
|
3951
3703
|
init_recurrence();
|
|
3952
3704
|
init_common();
|
|
3953
3705
|
MAX_SNOOZE_MIN = 24 * 60;
|
|
3954
|
-
|
|
3955
|
-
|
|
3706
|
+
MAX_REMINDER_OFFSET_MIN = 30 * 24 * 60;
|
|
3707
|
+
MAX_CONDITION_LENGTH = 500;
|
|
3708
|
+
ReminderOffsetMin = z4.number().int().min(0).max(MAX_REMINDER_OFFSET_MIN);
|
|
3709
|
+
RRuleString = z4.string().max(500).overwrite(
|
|
3710
|
+
(s) => s.trim().toUpperCase().replace(/^(LUNAR:)?RRULE:/, "$1")
|
|
3956
3711
|
).superRefine((s, ctx) => {
|
|
3957
3712
|
try {
|
|
3958
3713
|
normalizeRRule(s);
|
|
@@ -3961,43 +3716,61 @@ var init_reminders = __esm({
|
|
|
3961
3716
|
ctx.addIssue({ code: "custom", message: e.message });
|
|
3962
3717
|
}
|
|
3963
3718
|
});
|
|
3964
|
-
Reminder =
|
|
3965
|
-
id:
|
|
3966
|
-
title:
|
|
3967
|
-
body:
|
|
3719
|
+
Reminder = z4.object({
|
|
3720
|
+
id: z4.string(),
|
|
3721
|
+
title: z4.string(),
|
|
3722
|
+
body: z4.string().nullable(),
|
|
3968
3723
|
targetType: TargetType,
|
|
3969
|
-
targetId:
|
|
3970
|
-
fireAt:
|
|
3724
|
+
targetId: z4.string().nullable(),
|
|
3725
|
+
fireAt: z4.string(),
|
|
3971
3726
|
// lần nhắc kế tiếp
|
|
3972
|
-
rrule:
|
|
3727
|
+
rrule: z4.string().nullable(),
|
|
3728
|
+
offsetMin: z4.number().int().nullable(),
|
|
3729
|
+
// có giá trị = nhắc tương đối, fireAt tự tính theo item
|
|
3730
|
+
// Có giá trị = trợ lý tự kiểm tra: đến giờ Gemini đọc dữ liệu, chỉ nhắc khi điều kiện đúng
|
|
3731
|
+
condition: z4.string().nullable(),
|
|
3973
3732
|
status: ReminderStatus,
|
|
3974
3733
|
source: Source,
|
|
3975
|
-
createdAt:
|
|
3976
|
-
updatedAt:
|
|
3734
|
+
createdAt: z4.string(),
|
|
3735
|
+
updatedAt: z4.string()
|
|
3977
3736
|
});
|
|
3978
|
-
ReminderFields =
|
|
3979
|
-
title:
|
|
3980
|
-
body:
|
|
3737
|
+
ReminderFields = z4.object({
|
|
3738
|
+
title: z4.string().trim().min(1).max(200),
|
|
3739
|
+
body: z4.string().max(2e3).nullable(),
|
|
3981
3740
|
targetType: TargetType,
|
|
3982
3741
|
targetId: Id.nullable(),
|
|
3983
3742
|
fireAt: IsoDateTime,
|
|
3984
|
-
rrule: RRuleString.nullable()
|
|
3743
|
+
rrule: RRuleString.nullable(),
|
|
3744
|
+
offsetMin: ReminderOffsetMin.nullable(),
|
|
3745
|
+
condition: z4.string().trim().min(1).max(MAX_CONDITION_LENGTH).nullable()
|
|
3985
3746
|
});
|
|
3986
3747
|
targetConsistent = (v) => (v.targetType ?? "none") === "none" ? v.targetId == null : v.targetId != null;
|
|
3987
3748
|
CreateReminder = ReminderFields.extend({
|
|
3988
3749
|
body: ReminderFields.shape.body.optional(),
|
|
3989
3750
|
targetType: TargetType.default("none"),
|
|
3990
3751
|
targetId: ReminderFields.shape.targetId.optional(),
|
|
3991
|
-
|
|
3752
|
+
fireAt: ReminderFields.shape.fireAt.optional(),
|
|
3753
|
+
rrule: ReminderFields.shape.rrule.optional(),
|
|
3754
|
+
offsetMin: ReminderFields.shape.offsetMin.optional(),
|
|
3755
|
+
condition: ReminderFields.shape.condition.optional()
|
|
3992
3756
|
}).strict().refine(targetConsistent, {
|
|
3993
3757
|
message: "targetId b\u1EAFt bu\u1ED9c khi c\xF3 targetType, v\xE0 ph\u1EA3i b\u1ECF tr\u1ED1ng khi targetType l\xE0 none",
|
|
3994
3758
|
path: ["targetId"]
|
|
3759
|
+
}).refine((v) => v.offsetMin != null || v.fireAt != null, {
|
|
3760
|
+
message: "C\u1EA7n fireAt (gi\u1EDD c\u1ED1 \u0111\u1ECBnh) ho\u1EB7c offsetMin (nh\u1EAFc tr\u01B0\u1EDBc gi\u1EDD c\u1EE7a item)",
|
|
3761
|
+
path: ["fireAt"]
|
|
3762
|
+
}).refine((v) => v.offsetMin == null || v.targetType !== "none", {
|
|
3763
|
+
message: "Nh\u1EAFc t\u01B0\u01A1ng \u0111\u1ED1i (offsetMin) ph\u1EA3i g\u1EAFn v\u1EDBi m\u1ED9t item",
|
|
3764
|
+
path: ["offsetMin"]
|
|
3765
|
+
}).refine((v) => v.condition == null || v.offsetMin == null, {
|
|
3766
|
+
message: "Nh\u1EAFc c\xF3 \u0111i\u1EC1u ki\u1EC7n d\xF9ng fireAt (+ rrule), kh\xF4ng d\xF9ng offsetMin",
|
|
3767
|
+
path: ["condition"]
|
|
3995
3768
|
});
|
|
3996
3769
|
UpdateReminder = ReminderFields.extend({
|
|
3997
3770
|
// fired / snoozed do hệ thống đặt, người dùng chỉ bật lại, đánh dấu xong hoặc huỷ
|
|
3998
|
-
status:
|
|
3771
|
+
status: z4.enum(["scheduled", "done", "cancelled"])
|
|
3999
3772
|
}).partial().strict();
|
|
4000
|
-
SnoozeReminder =
|
|
3773
|
+
SnoozeReminder = z4.object({ minutes: z4.number().int().min(1).max(MAX_SNOOZE_MIN) }).strict();
|
|
4001
3774
|
ListRemindersQuery = PageQuery.extend({
|
|
4002
3775
|
from: IsoDateTime.optional(),
|
|
4003
3776
|
// fireAt >= from
|
|
@@ -4010,22 +3783,635 @@ var init_reminders = __esm({
|
|
|
4010
3783
|
}
|
|
4011
3784
|
});
|
|
4012
3785
|
|
|
4013
|
-
// ../../packages/shared/src/
|
|
4014
|
-
|
|
4015
|
-
var
|
|
4016
|
-
|
|
3786
|
+
// ../../packages/shared/src/schemas/tasks.ts
|
|
3787
|
+
import { z as z5 } from "zod";
|
|
3788
|
+
var MAX_TASKS_PER_CREATE, Task, TaskTree, TaskFields, RepeatFields, CreateSubtask, CreateTask, UpdateTask, UpdateTaskResult, ListTasksQuery, PostponedTask, MAX_POSTPONE, PostponeTasks, PostponeTasksResult, UndoPostpone, ReorderTasks;
|
|
3789
|
+
var init_tasks = __esm({
|
|
3790
|
+
"../../packages/shared/src/schemas/tasks.ts"() {
|
|
4017
3791
|
"use strict";
|
|
3792
|
+
init_enums();
|
|
3793
|
+
init_common();
|
|
4018
3794
|
init_reminders();
|
|
4019
|
-
|
|
4020
|
-
|
|
3795
|
+
MAX_TASKS_PER_CREATE = 100;
|
|
3796
|
+
Task = z5.object({
|
|
3797
|
+
id: z5.string(),
|
|
3798
|
+
parentId: z5.string().nullable(),
|
|
3799
|
+
title: z5.string(),
|
|
3800
|
+
description: z5.string().nullable(),
|
|
3801
|
+
status: TaskStatus,
|
|
3802
|
+
priority: TaskPriority,
|
|
3803
|
+
dueAt: z5.string().nullable(),
|
|
3804
|
+
allDay: z5.boolean(),
|
|
3805
|
+
estimateMin: z5.number().int().nullable(),
|
|
3806
|
+
sortOrder: z5.number().int(),
|
|
3807
|
+
completedAt: z5.string().nullable(),
|
|
3808
|
+
areaId: z5.string().nullable(),
|
|
3809
|
+
// subtask luôn cùng mảng với task gốc
|
|
3810
|
+
rrule: z5.string().nullable(),
|
|
3811
|
+
// chỉ ở task gốc đang mở của chuỗi lặp
|
|
3812
|
+
repeatFrom: RepeatFrom.nullable(),
|
|
3813
|
+
seriesId: z5.string().nullable(),
|
|
3814
|
+
// id task đầu chuỗi, lần đã xong vẫn giữ để xem lịch sử
|
|
3815
|
+
scheduledAt: z5.string().nullable(),
|
|
3816
|
+
// giờ định làm, khác hạn dueAt
|
|
3817
|
+
// Mục tiêu (docs/10 §1.2): task gốc có goalId là mốc, subtask là việc
|
|
3818
|
+
goalId: z5.string().nullable(),
|
|
3819
|
+
optional: z5.boolean(),
|
|
3820
|
+
// "nên có", thiếu giờ thì cắt trước
|
|
3821
|
+
deferred: z5.boolean(),
|
|
3822
|
+
// việc nên có đã dồn ra sau hạn, không xếp lịch
|
|
3823
|
+
plannedBy: PlannedBy.nullable(),
|
|
3824
|
+
slipCount: z5.number().int(),
|
|
3825
|
+
source: Source,
|
|
3826
|
+
createdAt: z5.string(),
|
|
3827
|
+
updatedAt: z5.string(),
|
|
3828
|
+
childrenCount: z5.number().int()
|
|
3829
|
+
});
|
|
3830
|
+
TaskTree = Task.extend({
|
|
3831
|
+
get children() {
|
|
3832
|
+
return z5.array(TaskTree);
|
|
3833
|
+
}
|
|
3834
|
+
});
|
|
3835
|
+
TaskFields = z5.object({
|
|
3836
|
+
title: z5.string().trim().min(1).max(300),
|
|
3837
|
+
description: z5.string().max(1e4).nullable(),
|
|
3838
|
+
status: TaskStatus,
|
|
3839
|
+
priority: TaskPriority,
|
|
3840
|
+
dueAt: IsoDateTime.nullable(),
|
|
3841
|
+
allDay: z5.boolean(),
|
|
3842
|
+
estimateMin: z5.number().int().min(1).max(1e5).nullable(),
|
|
3843
|
+
scheduledAt: IsoDateTime.nullable(),
|
|
3844
|
+
optional: z5.boolean()
|
|
3845
|
+
});
|
|
3846
|
+
RepeatFields = z5.object({
|
|
3847
|
+
rrule: RRuleString.nullable(),
|
|
3848
|
+
repeatFrom: RepeatFrom
|
|
3849
|
+
});
|
|
3850
|
+
CreateSubtask = TaskFields.partial().extend({
|
|
3851
|
+
title: TaskFields.shape.title,
|
|
3852
|
+
get subtasks() {
|
|
3853
|
+
return z5.array(CreateSubtask).max(MAX_TASKS_PER_CREATE).optional();
|
|
3854
|
+
}
|
|
3855
|
+
}).strict();
|
|
3856
|
+
CreateTask = TaskFields.partial().extend(RepeatFields.partial().shape).extend({
|
|
3857
|
+
title: TaskFields.shape.title,
|
|
3858
|
+
parentId: Id.nullable().optional(),
|
|
3859
|
+
// Như mảng: subtask theo mục tiêu của task gốc
|
|
3860
|
+
goalId: Id.nullable().optional(),
|
|
3861
|
+
// Subtask lấy mảng của task gốc; gửi khác mảng gốc thì 400 area_follows_root
|
|
3862
|
+
areaId: Id.nullable().optional(),
|
|
3863
|
+
subtasks: z5.array(CreateSubtask).max(MAX_TASKS_PER_CREATE).optional()
|
|
3864
|
+
}).strict();
|
|
3865
|
+
UpdateTask = TaskFields.extend(RepeatFields.shape).extend({
|
|
3866
|
+
parentId: Id.nullable(),
|
|
3867
|
+
// chuyển sang cha khác, null = thành task gốc
|
|
3868
|
+
areaId: Id.nullable(),
|
|
3869
|
+
// đổi ở task gốc thì cả cây đổi theo
|
|
3870
|
+
goalId: Id.nullable(),
|
|
3871
|
+
// như areaId: chỉ đổi ở task gốc
|
|
3872
|
+
deferred: z5.boolean(),
|
|
3873
|
+
sortOrder: z5.number().int()
|
|
3874
|
+
}).partial().strict();
|
|
3875
|
+
UpdateTaskResult = Task.extend({ nextTask: Task.nullable() });
|
|
3876
|
+
ListTasksQuery = PageQuery.extend({
|
|
3877
|
+
status: queryList(TaskStatus).optional(),
|
|
3878
|
+
dueFrom: IsoDateTime.optional(),
|
|
3879
|
+
dueTo: IsoDateTime.optional(),
|
|
3880
|
+
parentId: Id.optional(),
|
|
3881
|
+
q: z5.string().trim().max(200).optional(),
|
|
3882
|
+
areaId: queryList(Id).optional(),
|
|
3883
|
+
// "a,b", "none" = chưa xếp mảng
|
|
3884
|
+
seriesId: Id.optional()
|
|
3885
|
+
// lịch sử một chuỗi lặp (mọi trạng thái, mới nhất trước)
|
|
3886
|
+
});
|
|
3887
|
+
PostponedTask = z5.object({ id: Id, dueAt: IsoDateTime, scheduledAt: IsoDateTime.nullable() }).strict();
|
|
3888
|
+
MAX_POSTPONE = 50;
|
|
3889
|
+
PostponeTasks = z5.object({
|
|
3890
|
+
ids: z5.array(Id).min(1).max(MAX_POSTPONE).optional(),
|
|
3891
|
+
noticeId: Id.optional(),
|
|
3892
|
+
to: z5.literal("tomorrow")
|
|
3893
|
+
}).strict().refine((v) => v.ids || v.noticeId, { message: "C\u1EA7n ids ho\u1EB7c noticeId", path: ["ids"] });
|
|
3894
|
+
PostponeTasksResult = z5.object({
|
|
3895
|
+
postponed: z5.number().int(),
|
|
3896
|
+
before: z5.array(PostponedTask)
|
|
3897
|
+
});
|
|
3898
|
+
UndoPostpone = z5.object({ before: z5.array(PostponedTask).min(1).max(MAX_POSTPONE), noticeId: Id.optional() }).strict();
|
|
3899
|
+
ReorderTasks = z5.array(z5.object({ id: Id, sortOrder: z5.number().int(), parentId: Id.nullable() }).strict()).min(1).max(500);
|
|
3900
|
+
}
|
|
3901
|
+
});
|
|
3902
|
+
|
|
3903
|
+
// ../../packages/shared/src/schemas/notify.ts
|
|
3904
|
+
import { z as z6 } from "zod";
|
|
3905
|
+
var ExpoToken, WebPushSubscription, WebToken, RegisterDevice, Device, Notice, BriefingSuggestion, BriefingData, ListNoticesQuery, Connector, ReadNotices;
|
|
3906
|
+
var init_notify = __esm({
|
|
3907
|
+
"../../packages/shared/src/schemas/notify.ts"() {
|
|
3908
|
+
"use strict";
|
|
3909
|
+
init_enums();
|
|
3910
|
+
init_common();
|
|
3911
|
+
init_tasks();
|
|
3912
|
+
ExpoToken = z6.string().regex(/^Expo(nent)?PushToken\[[^\]]+\]$/, "Kh\xF4ng ph\u1EA3i Expo push token");
|
|
3913
|
+
WebPushSubscription = z6.object({
|
|
3914
|
+
endpoint: z6.url({ protocol: /^https$/ }).max(1e3),
|
|
3915
|
+
keys: z6.object({
|
|
3916
|
+
p256dh: z6.string().min(80).max(100),
|
|
3917
|
+
// khoá công khai P-256 dạng base64url (65 byte)
|
|
3918
|
+
auth: z6.string().min(16).max(30)
|
|
3919
|
+
// 16 byte base64url
|
|
3920
|
+
})
|
|
3921
|
+
});
|
|
3922
|
+
WebToken = z6.union([z6.string().max(2e3), z6.record(z6.string(), z6.unknown())]).transform((v, ctx) => {
|
|
3923
|
+
let raw = v;
|
|
3924
|
+
if (typeof v === "string") {
|
|
3925
|
+
try {
|
|
3926
|
+
raw = JSON.parse(v);
|
|
3927
|
+
} catch {
|
|
3928
|
+
ctx.addIssue({ code: "custom", message: "pushToken kh\xF4ng ph\u1EA3i JSON" });
|
|
3929
|
+
return z6.NEVER;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
const sub = WebPushSubscription.safeParse(raw);
|
|
3933
|
+
if (!sub.success) {
|
|
3934
|
+
ctx.addIssue({ code: "custom", message: "Subscription Web Push kh\xF4ng h\u1EE3p l\u1EC7" });
|
|
3935
|
+
return z6.NEVER;
|
|
3936
|
+
}
|
|
3937
|
+
const { endpoint, keys } = sub.data;
|
|
3938
|
+
return JSON.stringify({ endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } });
|
|
3939
|
+
});
|
|
3940
|
+
RegisterDevice = z6.discriminatedUnion("platform", [
|
|
3941
|
+
z6.object({ platform: z6.literal("android"), pushToken: ExpoToken }).strict(),
|
|
3942
|
+
z6.object({ platform: z6.literal("web"), pushToken: WebToken }).strict()
|
|
3943
|
+
]);
|
|
3944
|
+
Device = z6.object({
|
|
3945
|
+
id: z6.string(),
|
|
3946
|
+
platform: DevicePlatform,
|
|
3947
|
+
lastSeen: z6.string()
|
|
3948
|
+
});
|
|
3949
|
+
Notice = z6.object({
|
|
3950
|
+
id: z6.string(),
|
|
3951
|
+
kind: NoticeKind,
|
|
3952
|
+
title: z6.string(),
|
|
3953
|
+
body: z6.string(),
|
|
3954
|
+
data: z6.record(z6.string(), z6.unknown()).nullable(),
|
|
3955
|
+
// vd {budgetId, month} cho nút "Xem chi tiết"
|
|
3956
|
+
createdAt: z6.string(),
|
|
3957
|
+
readAt: z6.string().nullable()
|
|
3958
|
+
});
|
|
3959
|
+
BriefingSuggestion = z6.object({
|
|
3960
|
+
text: z6.string().max(200),
|
|
3961
|
+
taskId: z6.string().max(64).optional(),
|
|
3962
|
+
slotStart: z6.iso.datetime({ offset: true }).optional()
|
|
3963
|
+
});
|
|
3964
|
+
BriefingData = z6.object({
|
|
3965
|
+
taskIds: z6.array(z6.string()).default([]),
|
|
3966
|
+
suggestions: z6.array(BriefingSuggestion).default([]),
|
|
3967
|
+
postponed: z6.object({ count: z6.number().int(), before: z6.array(PostponedTask) }).nullable().default(null)
|
|
3968
|
+
});
|
|
3969
|
+
ListNoticesQuery = PageQuery.extend({ unread: QueryBool.optional() });
|
|
3970
|
+
Connector = z6.object({
|
|
3971
|
+
clientId: z6.string(),
|
|
3972
|
+
name: z6.string(),
|
|
3973
|
+
uri: z6.string().nullable(),
|
|
3974
|
+
icon: z6.string().nullable(),
|
|
3975
|
+
scopes: z6.array(z6.string()),
|
|
3976
|
+
connectedAt: z6.string(),
|
|
3977
|
+
lastUsedAt: z6.string().nullable()
|
|
3978
|
+
// lần cấp access token gần nhất (khoảng mỗi giờ khi đang dùng)
|
|
3979
|
+
});
|
|
3980
|
+
ReadNotices = z6.object({ ids: z6.array(z6.string()).min(1).max(200).optional() }).strict();
|
|
3981
|
+
}
|
|
3982
|
+
});
|
|
3983
|
+
|
|
3984
|
+
// ../../packages/shared/src/schemas/bridge.ts
|
|
3985
|
+
import { z as z7 } from "zod";
|
|
3986
|
+
function deriveBridgeKey(token) {
|
|
3987
|
+
const digest2 = sha256(new TextEncoder().encode(`manualofme-bridge-url:${token}`));
|
|
3988
|
+
return `${BRIDGE_KEY_PREFIX}${base64url(digest2)}`;
|
|
3989
|
+
}
|
|
3990
|
+
var BRIDGE_SUBPROTOCOL, BRIDGE_CLOSE_UNAUTHORIZED, BRIDGE_PERMISSION_MODES, BridgePermissionMode, BridgeModel, BRIDGE_MAX_ATTACHMENTS, BridgeAttachment, BRIDGE_MAX_WINDOWS, BridgeClientMessage, BridgeFileRef, BridgeToolDetail, BridgeChatItem, BridgePermissionRequest, BridgeSessionInfo, BridgeWindow, BridgeServerMessage, BRIDGE_KEY_PREFIX, RegisterBridgeKey, BridgePublicUrl, BridgeInstance, PublishBridgeUrl, BridgeEndpointInstance, BridgeEndpoint;
|
|
3991
|
+
var init_bridge = __esm({
|
|
3992
|
+
"../../packages/shared/src/schemas/bridge.ts"() {
|
|
3993
|
+
"use strict";
|
|
3994
|
+
init_sha256();
|
|
3995
|
+
init_notify();
|
|
3996
|
+
BRIDGE_SUBPROTOCOL = "manualofme-bridge";
|
|
3997
|
+
BRIDGE_CLOSE_UNAUTHORIZED = 4401;
|
|
3998
|
+
BRIDGE_PERMISSION_MODES = [
|
|
3999
|
+
"default",
|
|
4000
|
+
"acceptEdits",
|
|
4001
|
+
"plan",
|
|
4002
|
+
"bypassPermissions"
|
|
4003
|
+
];
|
|
4004
|
+
BridgePermissionMode = z7.enum(BRIDGE_PERMISSION_MODES);
|
|
4005
|
+
BridgeModel = z7.string().max(100).regex(/^[A-Za-z0-9._[\]-]+$/, "T\xEAn model kh\xF4ng h\u1EE3p l\u1EC7").nullable();
|
|
4006
|
+
BRIDGE_MAX_ATTACHMENTS = 4;
|
|
4007
|
+
BridgeAttachment = z7.object({
|
|
4008
|
+
name: z7.string().trim().min(1).max(200),
|
|
4009
|
+
mimeType: z7.string().max(200),
|
|
4010
|
+
data: z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "data ph\u1EA3i l\xE0 base64")
|
|
4011
|
+
}).strict();
|
|
4012
|
+
BRIDGE_MAX_WINDOWS = 10;
|
|
4013
|
+
BridgeClientMessage = z7.discriminatedUnion("type", [
|
|
4014
|
+
/** Liệt kê các session của project (cả session mở từ VS Code). */
|
|
4015
|
+
z7.object({ type: z7.literal("list_sessions") }).strict(),
|
|
4016
|
+
/** App bản cũ: đổi cửa sổ hiện tại sang session khác. `null` = session mới. */
|
|
4017
|
+
z7.object({ type: z7.literal("open_session"), sessionId: z7.uuid().nullable() }).strict(),
|
|
4018
|
+
/**
|
|
4019
|
+
* Mở cửa sổ cho một session (đã mở rồi thì thôi). `new: true` = cửa sổ trống, `sessionId` do app
|
|
4020
|
+
* tự sinh, thành id của session Claude khi gửi tin đầu tiên.
|
|
4021
|
+
*/
|
|
4022
|
+
z7.object({ type: z7.literal("open_window"), sessionId: z7.uuid(), new: z7.boolean().optional() }).strict(),
|
|
4023
|
+
/** Đóng cửa sổ (session vẫn còn, mở lại từ Lịch sử). Đang chạy thì không đóng được. */
|
|
4024
|
+
z7.object({ type: z7.literal("close_window"), sessionId: z7.uuid() }).strict(),
|
|
4025
|
+
/** Lấy lịch sử một session, bridge trả `history` chỉ cho app hỏi. */
|
|
4026
|
+
z7.object({ type: z7.literal("get_history"), sessionId: z7.uuid() }).strict(),
|
|
4027
|
+
z7.object({
|
|
4028
|
+
type: z7.literal("send"),
|
|
4029
|
+
sessionId: z7.uuid().optional(),
|
|
4030
|
+
/** Rỗng được khi có tệp đính kèm. */
|
|
4031
|
+
text: z7.string().trim().max(2e4),
|
|
4032
|
+
clientId: z7.string().max(64).optional(),
|
|
4033
|
+
// bridge trả lại trong item user để app khử trùng
|
|
4034
|
+
attachments: z7.array(BridgeAttachment).max(BRIDGE_MAX_ATTACHMENTS).optional()
|
|
4035
|
+
}).strict(),
|
|
4036
|
+
/**
|
|
4037
|
+
* Đổi chế độ duyệt / model, áp cho mọi app và nhớ qua lần khởi động lại. Đổi chế độ khi đang
|
|
4038
|
+
* chạy thì các yêu cầu duyệt mà chế độ mới tự cho phép được duyệt luôn; model áp từ lượt sau.
|
|
4039
|
+
*/
|
|
4040
|
+
z7.object({
|
|
4041
|
+
type: z7.literal("set_options"),
|
|
4042
|
+
permissionMode: BridgePermissionMode.optional(),
|
|
4043
|
+
model: BridgeModel.optional()
|
|
4044
|
+
}).strict(),
|
|
4045
|
+
/** Dừng lượt đang chạy. */
|
|
4046
|
+
z7.object({ type: z7.literal("interrupt"), sessionId: z7.uuid().optional() }).strict(),
|
|
4047
|
+
z7.object({
|
|
4048
|
+
type: z7.literal("permission_reply"),
|
|
4049
|
+
requestId: z7.string().min(1).max(200),
|
|
4050
|
+
allow: z7.boolean(),
|
|
4051
|
+
message: z7.string().max(1e3).optional()
|
|
4052
|
+
// lý do từ chối, gửi lại cho Claude
|
|
4053
|
+
}).strict(),
|
|
4054
|
+
/** Đăng ký Expo push token để nhận thông báo khi xong việc / cần duyệt. */
|
|
4055
|
+
z7.object({ type: z7.literal("register_push"), token: ExpoToken }).strict(),
|
|
4056
|
+
z7.object({ type: z7.literal("unregister_push"), token: ExpoToken }).strict(),
|
|
4057
|
+
/** Lấy ảnh theo `ref` trong item (`user.files`, `image`). Bridge trả `image_data`. */
|
|
4058
|
+
z7.object({ type: z7.literal("get_image"), ref: z7.string().min(1).max(300) }).strict()
|
|
4059
|
+
]);
|
|
4060
|
+
BridgeFileRef = z7.object({
|
|
4061
|
+
ref: z7.string().max(300),
|
|
4062
|
+
name: z7.string().max(200),
|
|
4063
|
+
mimeType: z7.string().max(200)
|
|
4064
|
+
});
|
|
4065
|
+
BridgeToolDetail = z7.object({
|
|
4066
|
+
edits: z7.array(z7.object({ old: z7.string(), new: z7.string() })).optional(),
|
|
4067
|
+
content: z7.string().optional()
|
|
4068
|
+
});
|
|
4069
|
+
BridgeChatItem = z7.discriminatedUnion("kind", [
|
|
4070
|
+
z7.object({
|
|
4071
|
+
kind: z7.literal("user"),
|
|
4072
|
+
id: z7.string(),
|
|
4073
|
+
text: z7.string(),
|
|
4074
|
+
// không gồm danh sách tệp gửi kèm
|
|
4075
|
+
clientId: z7.string().optional(),
|
|
4076
|
+
// chỉ có với tin vừa gửi từ app
|
|
4077
|
+
files: z7.array(BridgeFileRef).optional()
|
|
4078
|
+
}),
|
|
4079
|
+
z7.object({ kind: z7.literal("assistant"), id: z7.string(), text: z7.string() }),
|
|
4080
|
+
/** Claude gọi tool. `summary` là phần dễ đọc của input: lệnh bash, đường dẫn file... */
|
|
4081
|
+
z7.object({
|
|
4082
|
+
kind: z7.literal("tool"),
|
|
4083
|
+
id: z7.string(),
|
|
4084
|
+
name: z7.string(),
|
|
4085
|
+
summary: z7.string(),
|
|
4086
|
+
detail: BridgeToolDetail.optional()
|
|
4087
|
+
}),
|
|
4088
|
+
/** Ảnh Claude nhận từ tool (đọc file ảnh, chụp màn hình...). */
|
|
4089
|
+
z7.object({ kind: z7.literal("image"), id: z7.string(), file: BridgeFileRef }),
|
|
4090
|
+
/** Mốc nén hội thoại. Bridge không đọc được số token thì để trống. */
|
|
4091
|
+
z7.object({
|
|
4092
|
+
kind: z7.literal("compact"),
|
|
4093
|
+
id: z7.string(),
|
|
4094
|
+
trigger: z7.enum(["auto", "manual"]),
|
|
4095
|
+
preTokens: z7.number().optional(),
|
|
4096
|
+
postTokens: z7.number().optional()
|
|
4097
|
+
})
|
|
4098
|
+
]);
|
|
4099
|
+
BridgePermissionRequest = z7.object({
|
|
4100
|
+
requestId: z7.string(),
|
|
4101
|
+
toolName: z7.string(),
|
|
4102
|
+
title: z7.string(),
|
|
4103
|
+
// câu mô tả sẵn, vd "Claude wants to run: pnpm test"
|
|
4104
|
+
input: z7.record(z7.string(), z7.unknown()),
|
|
4105
|
+
sessionId: z7.string().nullable().optional()
|
|
4106
|
+
// cửa sổ hỏi; bridge bản cũ không gửi
|
|
4107
|
+
});
|
|
4108
|
+
BridgeSessionInfo = z7.object({
|
|
4109
|
+
sessionId: z7.string(),
|
|
4110
|
+
title: z7.string(),
|
|
4111
|
+
updatedAt: z7.string()
|
|
4112
|
+
// ISO
|
|
4113
|
+
});
|
|
4114
|
+
BridgeWindow = z7.object({
|
|
4115
|
+
sessionId: z7.string(),
|
|
4116
|
+
title: z7.string(),
|
|
4117
|
+
busy: z7.boolean()
|
|
4118
|
+
});
|
|
4119
|
+
BridgeServerMessage = z7.discriminatedUnion("type", [
|
|
4120
|
+
/** Gửi khi vừa kết nối và mỗi khi trạng thái đổi. */
|
|
4121
|
+
z7.object({
|
|
4122
|
+
type: z7.literal("state"),
|
|
4123
|
+
sessionId: z7.string().nullable(),
|
|
4124
|
+
busy: z7.boolean(),
|
|
4125
|
+
// đang có lượt chạy
|
|
4126
|
+
pendingPermissions: z7.array(BridgePermissionRequest),
|
|
4127
|
+
// Bridge bản cũ không gửi hai trường này (chưa đổi được chế độ / model từ app)
|
|
4128
|
+
permissionMode: BridgePermissionMode.optional(),
|
|
4129
|
+
model: BridgeModel.optional(),
|
|
4130
|
+
/** Các cửa sổ đang mở. Bridge bản cũ không gửi: chỉ có một cửa sổ là `sessionId`. */
|
|
4131
|
+
windows: z7.array(BridgeWindow).optional()
|
|
4132
|
+
}),
|
|
4133
|
+
/** Toàn bộ lịch sử (đã cắt bớt) của session hiện tại: khi kết nối và khi đổi session. */
|
|
4134
|
+
z7.object({
|
|
4135
|
+
type: z7.literal("history"),
|
|
4136
|
+
sessionId: z7.string().nullable(),
|
|
4137
|
+
items: z7.array(BridgeChatItem)
|
|
4138
|
+
}),
|
|
4139
|
+
z7.object({ type: z7.literal("sessions"), sessions: z7.array(BridgeSessionInfo) }),
|
|
4140
|
+
/** Dòng mới trong lượt đang chạy. */
|
|
4141
|
+
z7.object({ type: z7.literal("item"), sessionId: z7.string().nullable(), item: BridgeChatItem }),
|
|
4142
|
+
z7.object({ type: z7.literal("permission_request"), request: BridgePermissionRequest }),
|
|
4143
|
+
z7.object({
|
|
4144
|
+
type: z7.literal("permission_resolved"),
|
|
4145
|
+
requestId: z7.string(),
|
|
4146
|
+
allow: z7.boolean()
|
|
4147
|
+
}),
|
|
4148
|
+
z7.object({
|
|
4149
|
+
type: z7.literal("turn_end"),
|
|
4150
|
+
sessionId: z7.string().nullable(),
|
|
4151
|
+
ok: z7.boolean(),
|
|
4152
|
+
text: z7.string(),
|
|
4153
|
+
// câu trả lời cuối, hoặc lỗi khi ok = false
|
|
4154
|
+
costUsd: z7.number(),
|
|
4155
|
+
durationMs: z7.number()
|
|
4156
|
+
}),
|
|
4157
|
+
z7.object({
|
|
4158
|
+
type: z7.literal("error"),
|
|
4159
|
+
message: z7.string(),
|
|
4160
|
+
clientId: z7.string().optional()
|
|
4161
|
+
// lỗi do tin `send` có clientId này
|
|
4162
|
+
}),
|
|
4163
|
+
/** Trả lời `get_image`, chỉ gửi cho app hỏi. `data: null` = ảnh không còn hoặc không được đọc. */
|
|
4164
|
+
z7.object({
|
|
4165
|
+
type: z7.literal("image_data"),
|
|
4166
|
+
ref: z7.string(),
|
|
4167
|
+
mimeType: z7.string().optional(),
|
|
4168
|
+
data: z7.string().nullable()
|
|
4169
|
+
})
|
|
4170
|
+
]);
|
|
4171
|
+
BRIDGE_KEY_PREFIX = "mobk_";
|
|
4172
|
+
RegisterBridgeKey = z7.object({ key: z7.string().regex(/^mobk_[A-Za-z0-9_-]{43}$/, "Kho\xE1 bridge kh\xF4ng h\u1EE3p l\u1EC7") }).strict();
|
|
4173
|
+
BridgePublicUrl = z7.string().max(500).regex(/^wss:\/\/[a-z0-9.-]+(:\d+)?\/[^\s?#]*$/i, "Ph\u1EA3i c\xF3 d\u1EA1ng wss://<host>/ws");
|
|
4174
|
+
BridgeInstance = z7.object({
|
|
4175
|
+
id: z7.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "id bridge kh\xF4ng h\u1EE3p l\u1EC7"),
|
|
4176
|
+
machine: z7.string().trim().min(1).max(100),
|
|
4177
|
+
project: z7.string().trim().max(200)
|
|
4178
|
+
}).strict();
|
|
4179
|
+
PublishBridgeUrl = z7.object({
|
|
4180
|
+
url: BridgePublicUrl.nullable(),
|
|
4181
|
+
ifUrl: BridgePublicUrl.nullable().optional(),
|
|
4182
|
+
instance: BridgeInstance.optional()
|
|
4183
|
+
}).strict();
|
|
4184
|
+
BridgeEndpointInstance = BridgeInstance.extend({
|
|
4185
|
+
url: z7.string().nullable(),
|
|
4186
|
+
updatedAt: z7.string().nullable()
|
|
4187
|
+
});
|
|
4188
|
+
BridgeEndpoint = z7.object({
|
|
4189
|
+
/** Địa chỉ bridge bật gần nhất, cho app bản cũ chỉ nối một bridge. */
|
|
4190
|
+
url: z7.string().nullable(),
|
|
4191
|
+
// null = bridge chưa gửi địa chỉ hoặc đã tắt
|
|
4192
|
+
updatedAt: z7.string().nullable(),
|
|
4193
|
+
hasKey: z7.boolean(),
|
|
4194
|
+
// app đã đăng ký khoá bridge chưa
|
|
4195
|
+
/** Mọi bridge (máy · project), bật gần nhất trước. Server bản cũ không có. */
|
|
4196
|
+
bridges: z7.array(BridgeEndpointInstance).optional()
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
// src/announce.ts
|
|
4202
|
+
var RETRY_MIN_MS, RETRY_MAX_MS, UNREGISTERED_RETRY_MS, UrlAnnouncer;
|
|
4203
|
+
var init_announce = __esm({
|
|
4204
|
+
"src/announce.ts"() {
|
|
4205
|
+
"use strict";
|
|
4206
|
+
RETRY_MIN_MS = 2e3;
|
|
4207
|
+
RETRY_MAX_MS = 6e4;
|
|
4208
|
+
UNREGISTERED_RETRY_MS = 1e4;
|
|
4209
|
+
UrlAnnouncer = class {
|
|
4210
|
+
constructor(opts) {
|
|
4211
|
+
this.opts = opts;
|
|
4212
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
4213
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref()));
|
|
4214
|
+
}
|
|
4215
|
+
/** Địa chỉ còn chờ gửi; `undefined` = không có gì chờ. */
|
|
4216
|
+
want = void 0;
|
|
4217
|
+
/** Địa chỉ server đang giữ do bridge này gửi; `undefined` = chưa gửi được lần nào. */
|
|
4218
|
+
published = void 0;
|
|
4219
|
+
replaced = false;
|
|
4220
|
+
running = false;
|
|
4221
|
+
/** Đã báo khoá chưa đăng ký, để không in lặp mỗi lần thử. */
|
|
4222
|
+
warned = false;
|
|
4223
|
+
fetchFn;
|
|
4224
|
+
sleep;
|
|
4225
|
+
/** Gửi địa chỉ mới (`null` = bridge tắt). Lỗi mạng thì tự thử lại, địa chỉ mới nhất thắng. */
|
|
4226
|
+
announce(url) {
|
|
4227
|
+
if (this.replaced) return Promise.resolve();
|
|
4228
|
+
this.want = url;
|
|
4229
|
+
return this.run();
|
|
4230
|
+
}
|
|
4231
|
+
/** Gọi định kỳ: hỏi server còn giữ địa chỉ của bridge này không, để biết sớm khi bị máy khác thay. */
|
|
4232
|
+
async check() {
|
|
4233
|
+
if (this.running || this.replaced || typeof this.published !== "string") return;
|
|
4234
|
+
if (await this.send(this.published, this.published) === "replaced") this.markReplaced();
|
|
4235
|
+
}
|
|
4236
|
+
/** Lúc tắt bridge: báo server bridge đã tắt, không chờ quá `timeoutMs`. */
|
|
4237
|
+
async clear(timeoutMs = 2e3) {
|
|
4238
|
+
this.want = void 0;
|
|
4239
|
+
if (this.replaced || !this.published) return;
|
|
4240
|
+
await this.send(null, this.published, AbortSignal.timeout(timeoutMs));
|
|
4241
|
+
}
|
|
4242
|
+
markReplaced() {
|
|
4243
|
+
if (this.replaced) return;
|
|
4244
|
+
this.replaced = true;
|
|
4245
|
+
this.want = void 0;
|
|
4246
|
+
console.log("Project n\xE0y v\u1EEBa \u0111\u01B0\u1EE3c b\u1EADt bridge m\u1EDBi v\xE0 \u0111\xE3 nh\u1EADn k\u1EBFt n\u1ED1i, bridge n\xE0y t\u1EF1 t\u1EAFt.");
|
|
4247
|
+
this.opts.onReplaced?.();
|
|
4248
|
+
}
|
|
4249
|
+
async run() {
|
|
4250
|
+
if (this.running) return;
|
|
4251
|
+
this.running = true;
|
|
4252
|
+
let delay = RETRY_MIN_MS;
|
|
4253
|
+
try {
|
|
4254
|
+
while (this.want !== void 0) {
|
|
4255
|
+
const url = this.want;
|
|
4256
|
+
if (url === null && this.published === void 0) {
|
|
4257
|
+
this.want = void 0;
|
|
4258
|
+
break;
|
|
4259
|
+
}
|
|
4260
|
+
const result = await this.send(url, this.published);
|
|
4261
|
+
if (result === "replaced") {
|
|
4262
|
+
this.markReplaced();
|
|
4263
|
+
break;
|
|
4264
|
+
}
|
|
4265
|
+
if (result === "unauthorized") {
|
|
4266
|
+
if (!this.warned) {
|
|
4267
|
+
this.warned = true;
|
|
4268
|
+
console.log(
|
|
4269
|
+
"Server ch\u01B0a nh\u1EADn kho\xE1 c\u1EE7a bridge: m\u1EDF web ho\u1EB7c app \u2192 Claude PC \u2192 C\xE0i \u0111\u1EB7t k\u1EBFt n\u1ED1i, d\xE1n BRIDGE_TOKEN, \u0111\u1EC3 tr\u1ED1ng URL r\u1ED3i L\u01B0u. Bridge t\u1EF1 th\u1EED l\u1EA1i."
|
|
4270
|
+
);
|
|
4271
|
+
}
|
|
4272
|
+
await this.sleep(UNREGISTERED_RETRY_MS);
|
|
4273
|
+
continue;
|
|
4274
|
+
}
|
|
4275
|
+
if (result === "ok") {
|
|
4276
|
+
this.published = url;
|
|
4277
|
+
if (this.want === url) this.want = void 0;
|
|
4278
|
+
delay = RETRY_MIN_MS;
|
|
4279
|
+
this.warned = false;
|
|
4280
|
+
if (url) console.log("\u0110\xE3 g\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server, app s\u1EBD t\u1EF1 k\u1EBFt n\u1ED1i.");
|
|
4281
|
+
continue;
|
|
4282
|
+
}
|
|
4283
|
+
await this.sleep(delay);
|
|
4284
|
+
delay = Math.min(RETRY_MAX_MS, delay * 2);
|
|
4285
|
+
}
|
|
4286
|
+
} finally {
|
|
4287
|
+
this.running = false;
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
/** `ifUrl` khác `undefined` thì server chỉ ghi khi địa chỉ đang lưu đúng bằng nó. */
|
|
4291
|
+
async send(url, ifUrl, signal) {
|
|
4292
|
+
try {
|
|
4293
|
+
const res = await this.fetchFn(`${this.opts.apiUrl}/v1/bridge/url`, {
|
|
4294
|
+
method: "PUT",
|
|
4295
|
+
headers: {
|
|
4296
|
+
Authorization: `Bearer ${this.opts.key}`,
|
|
4297
|
+
"Content-Type": "application/json"
|
|
4298
|
+
},
|
|
4299
|
+
body: JSON.stringify({
|
|
4300
|
+
url,
|
|
4301
|
+
...ifUrl === void 0 ? {} : { ifUrl },
|
|
4302
|
+
...this.opts.instance ? { instance: this.opts.instance } : {}
|
|
4303
|
+
}),
|
|
4304
|
+
signal
|
|
4305
|
+
});
|
|
4306
|
+
if (res.ok) return "ok";
|
|
4307
|
+
if (res.status === 401) return "unauthorized";
|
|
4308
|
+
if (res.status === 409) return "replaced";
|
|
4309
|
+
console.error(`G\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server l\u1ED7i HTTP ${res.status}, th\u1EED l\u1EA1i sau`);
|
|
4310
|
+
return "retry";
|
|
4311
|
+
} catch {
|
|
4312
|
+
return "retry";
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
});
|
|
4318
|
+
|
|
4319
|
+
// ../../packages/shared/src/push.ts
|
|
4320
|
+
var SNOOZE_CUSTOM, HABIT_DONE, HABIT_SKIP, BILL_PAID, TASK_DONE, PLAN_DAY, POSTPONE_ALL, OPEN_MEET, PLAN_WEEK, GOAL_DONE, GOAL_SKIP, REMINDER_ACTIONS, PUSH_ACTIONS, AGENT_PERMISSION_CATEGORY;
|
|
4321
|
+
var init_push = __esm({
|
|
4322
|
+
"../../packages/shared/src/push.ts"() {
|
|
4323
|
+
"use strict";
|
|
4324
|
+
init_reminders();
|
|
4325
|
+
SNOOZE_CUSTOM = "snooze_custom";
|
|
4326
|
+
HABIT_DONE = "habit_done";
|
|
4327
|
+
HABIT_SKIP = "habit_skip";
|
|
4328
|
+
BILL_PAID = "bill_paid";
|
|
4329
|
+
TASK_DONE = "task_done";
|
|
4330
|
+
PLAN_DAY = "plan_day";
|
|
4331
|
+
POSTPONE_ALL = "postpone_all";
|
|
4332
|
+
OPEN_MEET = "open_meet";
|
|
4333
|
+
PLAN_WEEK = "plan_week";
|
|
4334
|
+
GOAL_DONE = "goal_done";
|
|
4335
|
+
GOAL_SKIP = "goal_skip";
|
|
4336
|
+
REMINDER_ACTIONS = [
|
|
4337
|
+
{ action: "snooze_10", title: "10 ph\xFAt" },
|
|
4338
|
+
{
|
|
4339
|
+
action: SNOOZE_CUSTOM,
|
|
4340
|
+
title: "Nh\u1EADp s\u1ED1 ph\xFAt",
|
|
4341
|
+
input: { placeholder: "S\u1ED1 ph\xFAt, vd 45", submit: "Ho\xE3n" }
|
|
4342
|
+
},
|
|
4343
|
+
// Nhắc một lần thì thôi nhắc, nhắc lặp lại thì chờ lần sau (completeReminder)
|
|
4344
|
+
{ action: "done", title: "B\u1ECF qua" }
|
|
4345
|
+
];
|
|
4346
|
+
PUSH_ACTIONS = {
|
|
4347
|
+
// Android tối đa 3 nút: "Xong việc" thay "Bỏ qua" (bỏ qua thì gạt thông báo đi)
|
|
4348
|
+
task_reminder: [{ action: TASK_DONE, title: "Xong vi\u1EC7c" }, ...REMINDER_ACTIONS.slice(0, 2)],
|
|
4349
|
+
// Nhắc trước giờ họp / hẹn: thường chỉ cần biết, hoặc nhắc lại sát giờ
|
|
4350
|
+
event_reminder: [
|
|
4351
|
+
{ action: "done", title: "OK" },
|
|
4352
|
+
{ action: "snooze_5", title: "Ho\xE3n 5'" }
|
|
4353
|
+
],
|
|
4354
|
+
reminder: REMINDER_ACTIONS,
|
|
4355
|
+
// Xong = log {delta: 1}; nhắc lặp nên không gọi done (done sẽ bỏ luôn lần kế)
|
|
4356
|
+
habit_reminder: [
|
|
4357
|
+
{ action: HABIT_DONE, title: "Xong" },
|
|
4358
|
+
{ action: HABIT_SKIP, title: "B\u1ECF qua h\xF4m nay" }
|
|
4359
|
+
],
|
|
4360
|
+
bill_reminder: [
|
|
4361
|
+
{ action: BILL_PAID, title: "\u0110\xE3 \u0111\xF3ng" },
|
|
4362
|
+
{ action: "snooze_1440", title: "Ho\xE3n 1 ng\xE0y" }
|
|
4363
|
+
],
|
|
4364
|
+
// Hoá đơn mỗi kỳ một khác: gõ số tiền ngay trên thông báo (parseVnd), web mở màn hoá đơn
|
|
4365
|
+
bill_amount_reminder: [
|
|
4366
|
+
{
|
|
4367
|
+
action: BILL_PAID,
|
|
4368
|
+
title: "Nh\u1EADp s\u1ED1 ti\u1EC1n",
|
|
4369
|
+
input: { placeholder: "S\u1ED1 ti\u1EC1n, vd 650k", submit: "\u0110\xE3 \u0111\xF3ng" }
|
|
4370
|
+
},
|
|
4371
|
+
{ action: "snooze_1440", title: "Ho\xE3n 1 ng\xE0y" }
|
|
4372
|
+
],
|
|
4373
|
+
budget_alert: [{ action: "open", title: "Xem chi ti\u1EBFt" }],
|
|
4374
|
+
// Lên kế hoạch: mở app, tạo hội thoại với tin nhắn dựng sẵn (POST /v1/notices/:id/plan)
|
|
4375
|
+
daily_briefing: [{ action: PLAN_DAY, title: "L\xEAn k\u1EBF ho\u1EA1ch" }],
|
|
4376
|
+
afternoon_briefing: [
|
|
4377
|
+
{ action: PLAN_DAY, title: "L\xEAn k\u1EBF ho\u1EA1ch" },
|
|
4378
|
+
{ action: "open", title: "M\u1EDF" }
|
|
4379
|
+
],
|
|
4380
|
+
// Dời hết sang mai: POST /v1/tasks/postpone {noticeId}, chạy nền không mở app
|
|
4381
|
+
evening_review: [
|
|
4382
|
+
{ action: POSTPONE_ALL, title: "D\u1EDDi h\u1EBFt sang mai" },
|
|
4383
|
+
{ action: "open", title: "M\u1EDF" }
|
|
4384
|
+
],
|
|
4385
|
+
// Lên kế hoạch tuần: như PLAN_DAY, tin nhắn dựng sẵn cho cả tuần
|
|
4386
|
+
weekly_preview: [
|
|
4387
|
+
{ action: PLAN_WEEK, title: "L\xEAn k\u1EBF ho\u1EA1ch tu\u1EA7n" },
|
|
4388
|
+
{ action: "open", title: "M\u1EDF" }
|
|
4389
|
+
],
|
|
4390
|
+
// Họp không có meetLink thì server gửi bằng category `calendar_alert` (chỉ nút Xem lịch)
|
|
4391
|
+
meeting_reminder: [
|
|
4392
|
+
{ action: OPEN_MEET, title: "V\xE0o Meet" },
|
|
4393
|
+
{ action: "open", title: "M\u1EDF" }
|
|
4394
|
+
],
|
|
4395
|
+
// Mở màn Lịch ở ngày bị đổi (data.date)
|
|
4396
|
+
calendar_alert: [{ action: "open", title: "Xem l\u1ECBch" }],
|
|
4397
|
+
// Xong: PATCH /v1/tasks/:taskId done; Dời: POST /v1/planner/skip/:taskId (data.taskId)
|
|
4398
|
+
goal_block: [
|
|
4399
|
+
{ action: GOAL_DONE, title: "Xong" },
|
|
4400
|
+
{ action: GOAL_SKIP, title: "D\u1EDDi" }
|
|
4401
|
+
],
|
|
4402
|
+
notice: []
|
|
4403
|
+
};
|
|
4404
|
+
AGENT_PERMISSION_CATEGORY = "agent_permission";
|
|
4405
|
+
}
|
|
4021
4406
|
});
|
|
4022
4407
|
|
|
4023
4408
|
// src/push.ts
|
|
4024
|
-
function createPusher(state2, accessToken, fetchFn = fetch) {
|
|
4409
|
+
function createPusher(state2, accessToken, fetchFn = fetch, origin) {
|
|
4025
4410
|
return async (notice) => {
|
|
4026
4411
|
const tokens = [...state2.pushTokens];
|
|
4027
4412
|
if (tokens.length === 0) return;
|
|
4028
4413
|
const body = notice.body.replace(/\s+/g, " ").trim();
|
|
4414
|
+
const title = origin?.project ? `${origin.project} \xB7 ${notice.title}` : notice.title;
|
|
4029
4415
|
const messages = tokens.map((to) => ({
|
|
4030
4416
|
to,
|
|
4031
4417
|
priority: "high",
|
|
@@ -4034,8 +4420,9 @@ function createPusher(state2, accessToken, fetchFn = fetch) {
|
|
|
4034
4420
|
event: notice.event,
|
|
4035
4421
|
sessionId: notice.sessionId,
|
|
4036
4422
|
...notice.requestId ? { requestId: notice.requestId } : {},
|
|
4423
|
+
...origin ? { bridgeId: origin.bridgeId, project: origin.project } : {},
|
|
4037
4424
|
push: {
|
|
4038
|
-
title
|
|
4425
|
+
title,
|
|
4039
4426
|
body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY)}\u2026` : body,
|
|
4040
4427
|
category: notice.event === "permission_request" ? AGENT_PERMISSION_CATEGORY : "notice",
|
|
4041
4428
|
channelId: "alerts"
|
|
@@ -4105,6 +4492,198 @@ var init_auth = __esm({
|
|
|
4105
4492
|
}
|
|
4106
4493
|
});
|
|
4107
4494
|
|
|
4495
|
+
// src/uploads.ts
|
|
4496
|
+
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4497
|
+
import os2 from "node:os";
|
|
4498
|
+
import path2 from "node:path";
|
|
4499
|
+
function safeFileName(name) {
|
|
4500
|
+
const base = path2.basename(name.replace(/\\/g, "/"));
|
|
4501
|
+
const clean = base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").trim().slice(0, 120);
|
|
4502
|
+
return clean || "tep";
|
|
4503
|
+
}
|
|
4504
|
+
function saveAttachments(files, root = UPLOAD_DIR) {
|
|
4505
|
+
if (!files.length) return [];
|
|
4506
|
+
cleanOld(root);
|
|
4507
|
+
const dir = path2.join(root, `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`);
|
|
4508
|
+
mkdirSync(dir, { recursive: true });
|
|
4509
|
+
const used = /* @__PURE__ */ new Set();
|
|
4510
|
+
return files.map((f) => {
|
|
4511
|
+
let name = safeFileName(f.name);
|
|
4512
|
+
for (let i = 2; used.has(name.toLowerCase()); i++) {
|
|
4513
|
+
const ext = path2.extname(name);
|
|
4514
|
+
name = `${path2.basename(safeFileName(f.name), ext)} (${i})${ext}`;
|
|
4515
|
+
}
|
|
4516
|
+
used.add(name.toLowerCase());
|
|
4517
|
+
const file = path2.join(dir, name);
|
|
4518
|
+
writeFileSync(file, Buffer.from(f.data, "base64"));
|
|
4519
|
+
return file;
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
function promptWithFiles(text, paths) {
|
|
4523
|
+
if (!paths.length) return text;
|
|
4524
|
+
const list = paths.map((p) => `- ${p}`).join("\n");
|
|
4525
|
+
return text ? `${text}
|
|
4526
|
+
|
|
4527
|
+
${FILES_HEAD}
|
|
4528
|
+
${list}` : `${FILES_HEAD}
|
|
4529
|
+
${list}`;
|
|
4530
|
+
}
|
|
4531
|
+
function splitFiles(prompt) {
|
|
4532
|
+
const at = prompt.lastIndexOf(FILES_HEAD);
|
|
4533
|
+
if (at < 0 || at > 0 && !prompt.slice(0, at).endsWith("\n\n")) return { text: prompt, paths: [] };
|
|
4534
|
+
const lines = prompt.slice(at + FILES_HEAD.length).split("\n").filter(Boolean);
|
|
4535
|
+
if (!lines.length || !lines.every((l) => l.startsWith("- "))) return { text: prompt, paths: [] };
|
|
4536
|
+
return { text: prompt.slice(0, at).trim(), paths: lines.map((l) => l.slice(2).trim()) };
|
|
4537
|
+
}
|
|
4538
|
+
function fileRef(file, root = UPLOAD_DIR) {
|
|
4539
|
+
const rel = path2.relative(root, file);
|
|
4540
|
+
if (!rel || rel.startsWith("..") || path2.isAbsolute(rel)) return null;
|
|
4541
|
+
const name = path2.basename(file);
|
|
4542
|
+
return { ref: `u/${rel.split(path2.sep).join("/")}`, name, mimeType: mimeOf(name) };
|
|
4543
|
+
}
|
|
4544
|
+
function cleanOld(root, keepMs = KEEP_MS) {
|
|
4545
|
+
let names;
|
|
4546
|
+
try {
|
|
4547
|
+
names = readdirSync(root);
|
|
4548
|
+
} catch {
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
const cutoff = Date.now() - keepMs;
|
|
4552
|
+
for (const name of names) {
|
|
4553
|
+
const p = path2.join(root, name);
|
|
4554
|
+
try {
|
|
4555
|
+
if (statSync(p).mtimeMs < cutoff) rmSync(p, { recursive: true, force: true });
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
var UPLOAD_DIR, KEEP_MS, FILES_HEAD, MIME, mimeOf;
|
|
4561
|
+
var init_uploads = __esm({
|
|
4562
|
+
"src/uploads.ts"() {
|
|
4563
|
+
"use strict";
|
|
4564
|
+
UPLOAD_DIR = path2.join(os2.tmpdir(), "manualofme-bridge-uploads");
|
|
4565
|
+
KEEP_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
4566
|
+
FILES_HEAD = "T\u1EC7p ng\u01B0\u1EDDi d\xF9ng g\u1EEDi k\xE8m (\u0111\u1ECDc b\u1EB1ng tool Read):";
|
|
4567
|
+
MIME = {
|
|
4568
|
+
".png": "image/png",
|
|
4569
|
+
".jpg": "image/jpeg",
|
|
4570
|
+
".jpeg": "image/jpeg",
|
|
4571
|
+
".gif": "image/gif",
|
|
4572
|
+
".webp": "image/webp",
|
|
4573
|
+
".pdf": "application/pdf"
|
|
4574
|
+
};
|
|
4575
|
+
mimeOf = (name) => MIME[path2.extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
4576
|
+
}
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
// src/images.ts
|
|
4580
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4581
|
+
import os3 from "node:os";
|
|
4582
|
+
import path3 from "node:path";
|
|
4583
|
+
function saveToolImage(id, mimeType, data, root = IMAGE_DIR) {
|
|
4584
|
+
const ext = EXT[mimeType];
|
|
4585
|
+
if (!ext) return null;
|
|
4586
|
+
const name = `${id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 120)}${ext}`;
|
|
4587
|
+
const file = path3.join(root, name);
|
|
4588
|
+
try {
|
|
4589
|
+
if (!existsSync(file)) {
|
|
4590
|
+
mkdirSync2(root, { recursive: true });
|
|
4591
|
+
cleanOld(root, KEEP_MS2);
|
|
4592
|
+
writeFileSync2(file, Buffer.from(data, "base64"));
|
|
4593
|
+
}
|
|
4594
|
+
} catch (e) {
|
|
4595
|
+
console.error("[bridge] l\u01B0u \u1EA3nh", e);
|
|
4596
|
+
return null;
|
|
4597
|
+
}
|
|
4598
|
+
return { ref: `i/${name}`, name, mimeType };
|
|
4599
|
+
}
|
|
4600
|
+
function readImage(ref, roots = { u: UPLOAD_DIR, i: IMAGE_DIR }) {
|
|
4601
|
+
const m = /^([ui])\/(.+)$/.exec(ref);
|
|
4602
|
+
if (!m) return null;
|
|
4603
|
+
const root = roots[m[1]];
|
|
4604
|
+
const parts = m[2].split("/");
|
|
4605
|
+
if (parts.some((p) => !p || p === "." || p === ".." || p.includes("\\") || p.includes(":")))
|
|
4606
|
+
return null;
|
|
4607
|
+
const file = path3.resolve(root, ...parts);
|
|
4608
|
+
const rel = path3.relative(root, file);
|
|
4609
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) return null;
|
|
4610
|
+
if (!IMAGE_EXTS.has(path3.extname(file).toLowerCase())) return null;
|
|
4611
|
+
try {
|
|
4612
|
+
const st = statSync2(file);
|
|
4613
|
+
if (!st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
|
|
4614
|
+
return { mimeType: mimeOf(file), data: readFileSync(file).toString("base64") };
|
|
4615
|
+
} catch {
|
|
4616
|
+
return null;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
var IMAGE_DIR, KEEP_MS2, MAX_IMAGE_BYTES, EXT, IMAGE_EXTS;
|
|
4620
|
+
var init_images = __esm({
|
|
4621
|
+
"src/images.ts"() {
|
|
4622
|
+
"use strict";
|
|
4623
|
+
init_uploads();
|
|
4624
|
+
IMAGE_DIR = path3.join(os3.tmpdir(), "manualofme-bridge-images");
|
|
4625
|
+
KEEP_MS2 = 15 * 24 * 60 * 60 * 1e3;
|
|
4626
|
+
MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
4627
|
+
EXT = {
|
|
4628
|
+
"image/png": ".png",
|
|
4629
|
+
"image/jpeg": ".jpg",
|
|
4630
|
+
"image/gif": ".gif",
|
|
4631
|
+
"image/webp": ".webp"
|
|
4632
|
+
};
|
|
4633
|
+
IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
4634
|
+
}
|
|
4635
|
+
});
|
|
4636
|
+
|
|
4637
|
+
// src/compact.ts
|
|
4638
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
4639
|
+
import { readFile } from "node:fs/promises";
|
|
4640
|
+
import os4 from "node:os";
|
|
4641
|
+
import path4 from "node:path";
|
|
4642
|
+
function transcriptPath(cwd, sessionId, home = claudeHome()) {
|
|
4643
|
+
return path4.join(home, "projects", cwd.replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`);
|
|
4644
|
+
}
|
|
4645
|
+
function findTranscript(cwd, sessionId, home = claudeHome()) {
|
|
4646
|
+
if (!/^[A-Za-z0-9-]+$/.test(sessionId)) return null;
|
|
4647
|
+
const direct = transcriptPath(cwd, sessionId, home);
|
|
4648
|
+
if (existsSync2(direct)) return direct;
|
|
4649
|
+
const projects = path4.join(home, "projects");
|
|
4650
|
+
try {
|
|
4651
|
+
for (const dir of readdirSync2(projects)) {
|
|
4652
|
+
const file = path4.join(projects, dir, `${sessionId}.jsonl`);
|
|
4653
|
+
if (existsSync2(file)) return file;
|
|
4654
|
+
}
|
|
4655
|
+
} catch {
|
|
4656
|
+
}
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4659
|
+
async function readCompactMeta(file) {
|
|
4660
|
+
const out = /* @__PURE__ */ new Map();
|
|
4661
|
+
const text = await readFile(file, "utf8");
|
|
4662
|
+
for (const line of text.split("\n")) {
|
|
4663
|
+
if (!line.includes('"compact_boundary"')) continue;
|
|
4664
|
+
try {
|
|
4665
|
+
const row = JSON.parse(line);
|
|
4666
|
+
if (row.subtype !== "compact_boundary" || typeof row.uuid !== "string") continue;
|
|
4667
|
+
const m = row.compactMetadata;
|
|
4668
|
+
out.set(row.uuid, {
|
|
4669
|
+
trigger: m?.trigger === "manual" ? "manual" : "auto",
|
|
4670
|
+
preTokens: num(m?.preTokens),
|
|
4671
|
+
postTokens: num(m?.postTokens)
|
|
4672
|
+
});
|
|
4673
|
+
} catch {
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
return out;
|
|
4677
|
+
}
|
|
4678
|
+
var claudeHome, num;
|
|
4679
|
+
var init_compact = __esm({
|
|
4680
|
+
"src/compact.ts"() {
|
|
4681
|
+
"use strict";
|
|
4682
|
+
claudeHome = () => process.env.CLAUDE_CONFIG_DIR || path4.join(os4.homedir(), ".claude");
|
|
4683
|
+
num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
4684
|
+
}
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4108
4687
|
// src/transcript.ts
|
|
4109
4688
|
function toolSummary(input) {
|
|
4110
4689
|
if (!input || typeof input !== "object") return "";
|
|
@@ -4120,14 +4699,79 @@ function blocksOf(message) {
|
|
|
4120
4699
|
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
4121
4700
|
return Array.isArray(content) ? content : [];
|
|
4122
4701
|
}
|
|
4123
|
-
function
|
|
4702
|
+
function toolDetail(name, input) {
|
|
4703
|
+
if (!input || typeof input !== "object") return void 0;
|
|
4704
|
+
const obj = input;
|
|
4705
|
+
if (name === "Write") {
|
|
4706
|
+
const content = str(obj.content);
|
|
4707
|
+
return content ? { content: clip(content, MAX_DETAIL) } : void 0;
|
|
4708
|
+
}
|
|
4709
|
+
const raw = name === "Edit" ? [obj] : name === "MultiEdit" && Array.isArray(obj.edits) ? obj.edits : [];
|
|
4710
|
+
const edits = raw.filter((e) => e && typeof e === "object").slice(0, MAX_EDITS);
|
|
4711
|
+
if (!edits.length) return void 0;
|
|
4712
|
+
const each = Math.floor(MAX_DETAIL / (2 * edits.length));
|
|
4713
|
+
return {
|
|
4714
|
+
edits: edits.map((e) => ({ old: clip(str(e.old_string), each), new: clip(str(e.new_string), each) }))
|
|
4715
|
+
};
|
|
4716
|
+
}
|
|
4717
|
+
function compactItem(id, meta) {
|
|
4718
|
+
return {
|
|
4719
|
+
kind: "compact",
|
|
4720
|
+
id,
|
|
4721
|
+
trigger: meta.trigger,
|
|
4722
|
+
...meta.preTokens !== void 0 ? { preTokens: meta.preTokens } : {},
|
|
4723
|
+
...meta.postTokens !== void 0 ? { postTokens: meta.postTokens } : {}
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
function toolImages(blocks, save, skip) {
|
|
4727
|
+
return blocks.flatMap((b) => {
|
|
4728
|
+
if (b.type !== "tool_result" || typeof b.tool_use_id !== "string" || !Array.isArray(b.content))
|
|
4729
|
+
return [];
|
|
4730
|
+
const toolId = b.tool_use_id;
|
|
4731
|
+
if (skip?.has(toolId)) return [];
|
|
4732
|
+
return b.content.flatMap((c, n) => {
|
|
4733
|
+
const src = c.source;
|
|
4734
|
+
if (c.type !== "image" || src?.type !== "base64") return [];
|
|
4735
|
+
if (typeof src.media_type !== "string" || typeof src.data !== "string") return [];
|
|
4736
|
+
const id = `${toolId}:img${n}`;
|
|
4737
|
+
const file = save(id, src.media_type, src.data);
|
|
4738
|
+
return file ? [{ kind: "image", id, file }] : [];
|
|
4739
|
+
});
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
function toChatItems(msg, opts = {}) {
|
|
4124
4743
|
if (msg.parent_tool_use_id) return [];
|
|
4125
4744
|
const id = msg.uuid ?? crypto.randomUUID();
|
|
4745
|
+
if (msg.type === "system") {
|
|
4746
|
+
if (msg.subtype === "compact_boundary") {
|
|
4747
|
+
const m = msg.compact_metadata;
|
|
4748
|
+
return [
|
|
4749
|
+
compactItem(id, {
|
|
4750
|
+
trigger: m?.trigger === "manual" ? "manual" : "auto",
|
|
4751
|
+
preTokens: num2(m?.pre_tokens),
|
|
4752
|
+
postTokens: num2(m?.post_tokens)
|
|
4753
|
+
})
|
|
4754
|
+
];
|
|
4755
|
+
}
|
|
4756
|
+
const meta = msg.uuid ? opts.compacts?.get(msg.uuid) : void 0;
|
|
4757
|
+
return meta ? [compactItem(id, meta)] : [];
|
|
4758
|
+
}
|
|
4126
4759
|
const blocks = blocksOf(msg.message);
|
|
4127
4760
|
if (msg.type === "user") {
|
|
4128
4761
|
if (msg.isSynthetic) return [];
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4762
|
+
const images = opts.saveImage ? toolImages(blocks, opts.saveImage, opts.sentReads) : [];
|
|
4763
|
+
const raw = blocks.flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text.trim()] : []).filter((t) => t && !NOISE.test(t)).join("\n\n");
|
|
4764
|
+
if (raw.startsWith(COMPACT_SUMMARY_PREFIX)) return images;
|
|
4765
|
+
const { text, paths } = splitFiles(raw);
|
|
4766
|
+
const files = paths.flatMap((p) => fileRef(p) ?? []);
|
|
4767
|
+
if (!text && !files.length) return images;
|
|
4768
|
+
const item = {
|
|
4769
|
+
kind: "user",
|
|
4770
|
+
id,
|
|
4771
|
+
text: clip(text, MAX_TEXT),
|
|
4772
|
+
...files.length ? { files } : {}
|
|
4773
|
+
};
|
|
4774
|
+
return [item, ...images];
|
|
4131
4775
|
}
|
|
4132
4776
|
if (msg.type === "assistant") {
|
|
4133
4777
|
return blocks.flatMap((b, i) => {
|
|
@@ -4136,27 +4780,44 @@ function toChatItems(msg) {
|
|
|
4136
4780
|
return [{ kind: "assistant", id: itemId, text: clip(b.text.trim(), MAX_TEXT) }];
|
|
4137
4781
|
}
|
|
4138
4782
|
if (b.type === "tool_use" && typeof b.name === "string") {
|
|
4139
|
-
|
|
4783
|
+
const detail = toolDetail(b.name, b.input);
|
|
4784
|
+
const file = str(b.input?.file_path);
|
|
4785
|
+
if (b.name === "Read" && typeof b.id === "string" && file && fileRef(file)) {
|
|
4786
|
+
opts.sentReads?.add(b.id);
|
|
4787
|
+
}
|
|
4788
|
+
return [
|
|
4789
|
+
{
|
|
4790
|
+
kind: "tool",
|
|
4791
|
+
id: itemId,
|
|
4792
|
+
name: b.name,
|
|
4793
|
+
summary: toolSummary(b.input),
|
|
4794
|
+
...detail ? { detail } : {}
|
|
4795
|
+
}
|
|
4796
|
+
];
|
|
4140
4797
|
}
|
|
4141
4798
|
return [];
|
|
4142
4799
|
});
|
|
4143
4800
|
}
|
|
4144
4801
|
return [];
|
|
4145
4802
|
}
|
|
4146
|
-
function toHistory(messages, limit = HISTORY_LIMIT) {
|
|
4147
|
-
|
|
4803
|
+
function toHistory(messages, opts = {}, limit = HISTORY_LIMIT) {
|
|
4804
|
+
const all = { sentReads: /* @__PURE__ */ new Set(), ...opts };
|
|
4805
|
+
return messages.flatMap((m) => toChatItems(m, all)).slice(-limit);
|
|
4148
4806
|
}
|
|
4149
4807
|
function clipInput(input, max = 2e3) {
|
|
4150
4808
|
return Object.fromEntries(
|
|
4151
4809
|
Object.entries(input).map(([k, v]) => [k, typeof v === "string" ? clip(v, max) : v])
|
|
4152
4810
|
);
|
|
4153
4811
|
}
|
|
4154
|
-
var MAX_TEXT, MAX_SUMMARY, HISTORY_LIMIT, SUMMARY_KEYS, NOISE, clip;
|
|
4812
|
+
var MAX_TEXT, MAX_SUMMARY, MAX_DETAIL, MAX_EDITS, HISTORY_LIMIT, SUMMARY_KEYS, NOISE, COMPACT_SUMMARY_PREFIX, clip, str, num2;
|
|
4155
4813
|
var init_transcript = __esm({
|
|
4156
4814
|
"src/transcript.ts"() {
|
|
4157
4815
|
"use strict";
|
|
4816
|
+
init_uploads();
|
|
4158
4817
|
MAX_TEXT = 8e3;
|
|
4159
4818
|
MAX_SUMMARY = 200;
|
|
4819
|
+
MAX_DETAIL = 3e3;
|
|
4820
|
+
MAX_EDITS = 10;
|
|
4160
4821
|
HISTORY_LIMIT = 300;
|
|
4161
4822
|
SUMMARY_KEYS = [
|
|
4162
4823
|
"command",
|
|
@@ -4170,78 +4831,22 @@ var init_transcript = __esm({
|
|
|
4170
4831
|
"prompt"
|
|
4171
4832
|
];
|
|
4172
4833
|
NOISE = /^<(system-reminder|ide_[a-z_]+|command-[a-z]+|local-command-[a-z]+)>/;
|
|
4834
|
+
COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation";
|
|
4173
4835
|
clip = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
// src/uploads.ts
|
|
4178
|
-
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4179
|
-
import os2 from "node:os";
|
|
4180
|
-
import path2 from "node:path";
|
|
4181
|
-
function safeFileName(name) {
|
|
4182
|
-
const base = path2.basename(name.replace(/\\/g, "/"));
|
|
4183
|
-
const clean = base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").trim().slice(0, 120);
|
|
4184
|
-
return clean || "tep";
|
|
4185
|
-
}
|
|
4186
|
-
function saveAttachments(files, root = UPLOAD_DIR) {
|
|
4187
|
-
if (!files.length) return [];
|
|
4188
|
-
cleanOld(root);
|
|
4189
|
-
const dir = path2.join(root, `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`);
|
|
4190
|
-
mkdirSync(dir, { recursive: true });
|
|
4191
|
-
const used = /* @__PURE__ */ new Set();
|
|
4192
|
-
return files.map((f) => {
|
|
4193
|
-
let name = safeFileName(f.name);
|
|
4194
|
-
for (let i = 2; used.has(name.toLowerCase()); i++) {
|
|
4195
|
-
const ext = path2.extname(name);
|
|
4196
|
-
name = `${path2.basename(safeFileName(f.name), ext)} (${i})${ext}`;
|
|
4197
|
-
}
|
|
4198
|
-
used.add(name.toLowerCase());
|
|
4199
|
-
const file = path2.join(dir, name);
|
|
4200
|
-
writeFileSync(file, Buffer.from(f.data, "base64"));
|
|
4201
|
-
return file;
|
|
4202
|
-
});
|
|
4203
|
-
}
|
|
4204
|
-
function promptWithFiles(text, paths) {
|
|
4205
|
-
if (!paths.length) return text;
|
|
4206
|
-
const list = paths.map((p) => `- ${p}`).join("\n");
|
|
4207
|
-
const head = "T\u1EC7p ng\u01B0\u1EDDi d\xF9ng g\u1EEDi k\xE8m (\u0111\u1ECDc b\u1EB1ng tool Read):";
|
|
4208
|
-
return text ? `${text}
|
|
4209
|
-
|
|
4210
|
-
${head}
|
|
4211
|
-
${list}` : `${head}
|
|
4212
|
-
${list}`;
|
|
4213
|
-
}
|
|
4214
|
-
function cleanOld(root) {
|
|
4215
|
-
let names;
|
|
4216
|
-
try {
|
|
4217
|
-
names = readdirSync(root);
|
|
4218
|
-
} catch {
|
|
4219
|
-
return;
|
|
4220
|
-
}
|
|
4221
|
-
const cutoff = Date.now() - KEEP_MS;
|
|
4222
|
-
for (const name of names) {
|
|
4223
|
-
const p = path2.join(root, name);
|
|
4224
|
-
try {
|
|
4225
|
-
if (statSync(p).mtimeMs < cutoff) rmSync(p, { recursive: true, force: true });
|
|
4226
|
-
} catch {
|
|
4227
|
-
}
|
|
4228
|
-
}
|
|
4229
|
-
}
|
|
4230
|
-
var UPLOAD_DIR, KEEP_MS;
|
|
4231
|
-
var init_uploads = __esm({
|
|
4232
|
-
"src/uploads.ts"() {
|
|
4233
|
-
"use strict";
|
|
4234
|
-
UPLOAD_DIR = path2.join(os2.tmpdir(), "manualofme-bridge-uploads");
|
|
4235
|
-
KEEP_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
4836
|
+
str = (v) => typeof v === "string" ? v : "";
|
|
4837
|
+
num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
4236
4838
|
}
|
|
4237
4839
|
});
|
|
4238
4840
|
|
|
4239
4841
|
// src/session.ts
|
|
4240
4842
|
import * as sdk from "@anthropic-ai/claude-agent-sdk";
|
|
4241
|
-
var EDIT_TOOLS, autoAllows, BridgeError, APPEND_PROMPT, errorText, AgentSession;
|
|
4843
|
+
var EDIT_TOOLS, autoAllows, BridgeError, APPEND_PROMPT, sessionTitle, errorText, TITLE_MAX, titleFrom, withWindow, AgentSession;
|
|
4242
4844
|
var init_session = __esm({
|
|
4243
4845
|
"src/session.ts"() {
|
|
4244
4846
|
"use strict";
|
|
4847
|
+
init_bridge();
|
|
4848
|
+
init_compact();
|
|
4849
|
+
init_images();
|
|
4245
4850
|
init_transcript();
|
|
4246
4851
|
init_uploads();
|
|
4247
4852
|
EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
|
|
@@ -4249,38 +4854,53 @@ var init_session = __esm({
|
|
|
4249
4854
|
BridgeError = class extends Error {
|
|
4250
4855
|
};
|
|
4251
4856
|
APPEND_PROMPT = "Ng\u01B0\u1EDDi d\xF9ng \u0111ang nh\u1EAFn t\u1EEB app \u0111i\u1EC7n tho\u1EA1i qua agent bridge, kh\xF4ng nh\xECn th\u1EA5y editor. Tr\u1EA3 l\u1EDDi g\u1ECDn, n\xF3i r\xF5 \u0111\xE3 s\u1EEDa file n\xE0o. C\u1EA7n h\u1ECFi th\xEC h\u1ECFi b\u1EB1ng c\xE2u tr\u1EA3 l\u1EDDi th\u01B0\u1EDDng.";
|
|
4857
|
+
sessionTitle = (s) => s.customTitle || s.summary || s.firstPrompt || "(ch\u01B0a c\xF3 ti\xEAu \u0111\u1EC1)";
|
|
4252
4858
|
errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
4859
|
+
TITLE_MAX = 60;
|
|
4860
|
+
titleFrom = (text) => {
|
|
4861
|
+
const line = text.trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
4862
|
+
return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX)}\u2026` : line;
|
|
4863
|
+
};
|
|
4864
|
+
withWindow = (win, text) => win.title ? `${win.title}: ${text}` : text;
|
|
4253
4865
|
AgentSession = class {
|
|
4254
4866
|
constructor(deps) {
|
|
4255
4867
|
this.deps = deps;
|
|
4256
4868
|
this.sdk = deps.sdk ?? sdk;
|
|
4869
|
+
this.saveImage = deps.saveImage ?? saveToolImage;
|
|
4870
|
+
const { state: state2 } = deps;
|
|
4871
|
+
this.wins = state2.windows.map((w) => ({ ...w, abort: null, tail: null }));
|
|
4872
|
+
if (!this.wins.length && state2.sessionId) {
|
|
4873
|
+
this.wins.push({ sessionId: state2.sessionId, title: "", fresh: false, abort: null, tail: null });
|
|
4874
|
+
}
|
|
4257
4875
|
}
|
|
4258
4876
|
sdk;
|
|
4259
|
-
abort = null;
|
|
4260
|
-
/**
|
|
4261
|
-
* Query của lượt đã xong nhưng chưa đóng: Claude bật tác vụ nền (server, tunnel...) thì SDK
|
|
4262
|
-
* còn chạy tiếp tới khi tác vụ đó tắt. Gửi tin mới / đổi session thì đóng nó trước.
|
|
4263
|
-
*/
|
|
4264
|
-
tail = null;
|
|
4265
4877
|
pending = /* @__PURE__ */ new Map();
|
|
4878
|
+
wins;
|
|
4879
|
+
saveImage;
|
|
4880
|
+
/** Cửa sổ dùng gần nhất. App bản cũ chỉ thấy cửa sổ này. */
|
|
4266
4881
|
get sessionId() {
|
|
4267
4882
|
return this.deps.state.sessionId;
|
|
4268
4883
|
}
|
|
4884
|
+
/** Có cửa sổ nào đang chạy không. */
|
|
4269
4885
|
get busy() {
|
|
4270
|
-
return this.abort !== null;
|
|
4886
|
+
return this.wins.some((w) => w.abort !== null);
|
|
4271
4887
|
}
|
|
4272
4888
|
/** Chế độ duyệt đang dùng: chọn từ app, chưa chọn thì theo .env. */
|
|
4273
4889
|
get permissionMode() {
|
|
4274
4890
|
return this.deps.state.permissionMode ?? this.deps.config.permissionMode;
|
|
4275
4891
|
}
|
|
4892
|
+
windows() {
|
|
4893
|
+
return this.wins.map((w) => ({ sessionId: w.sessionId, title: w.title, busy: w.abort !== null }));
|
|
4894
|
+
}
|
|
4276
4895
|
stateMessage() {
|
|
4277
4896
|
return {
|
|
4278
4897
|
type: "state",
|
|
4279
4898
|
sessionId: this.sessionId,
|
|
4280
|
-
busy: this.
|
|
4899
|
+
busy: !!this.find(this.sessionId)?.abort,
|
|
4281
4900
|
pendingPermissions: [...this.pending.values()].map((p) => p.request),
|
|
4282
4901
|
permissionMode: this.permissionMode,
|
|
4283
|
-
model: this.deps.state.model
|
|
4902
|
+
model: this.deps.state.model,
|
|
4903
|
+
windows: this.windows()
|
|
4284
4904
|
};
|
|
4285
4905
|
}
|
|
4286
4906
|
/**
|
|
@@ -4298,32 +4918,102 @@ var init_session = __esm({
|
|
|
4298
4918
|
}
|
|
4299
4919
|
this.deps.broadcast(this.stateMessage());
|
|
4300
4920
|
}
|
|
4301
|
-
/**
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4921
|
+
/**
|
|
4922
|
+
* Lịch sử một session, mặc định cửa sổ dùng gần nhất. `null` khi cửa sổ dùng gần nhất đã đổi
|
|
4923
|
+
* trong lúc đọc file: bản lịch sử này cũ rồi, không gửi.
|
|
4924
|
+
*/
|
|
4925
|
+
async historyMessage(sessionId) {
|
|
4926
|
+
const current = sessionId === void 0;
|
|
4927
|
+
const id = current ? this.sessionId : sessionId;
|
|
4928
|
+
if (!id || this.find(id)?.fresh) return { type: "history", sessionId: id, items: [] };
|
|
4929
|
+
const messages = await this.sdk.getSessionMessages(id, {
|
|
4930
|
+
dir: this.deps.config.cwd,
|
|
4931
|
+
includeSystemMessages: true
|
|
4932
|
+
});
|
|
4933
|
+
const compacts = messages.some((m) => m.type === "system") ? await this.compacts(id) : void 0;
|
|
4934
|
+
if (current && this.sessionId !== id) return null;
|
|
4935
|
+
return {
|
|
4936
|
+
type: "history",
|
|
4937
|
+
sessionId: id,
|
|
4938
|
+
items: toHistory(messages, { compacts, saveImage: this.saveImage })
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
async compacts(sessionId) {
|
|
4942
|
+
try {
|
|
4943
|
+
if (this.deps.readCompacts) return await this.deps.readCompacts(sessionId);
|
|
4944
|
+
const file = findTranscript(this.deps.config.cwd, sessionId);
|
|
4945
|
+
return file ? await readCompactMeta(file) : void 0;
|
|
4946
|
+
} catch (e) {
|
|
4947
|
+
console.error("[bridge] \u0111\u1ECDc m\u1ED1c n\xE9n", e);
|
|
4948
|
+
return void 0;
|
|
4949
|
+
}
|
|
4308
4950
|
}
|
|
4309
4951
|
async sessions() {
|
|
4310
4952
|
const list = await this.sdk.listSessions({ dir: this.deps.config.cwd, limit: 30 });
|
|
4311
4953
|
return list.map((s) => ({
|
|
4312
4954
|
sessionId: s.sessionId,
|
|
4313
|
-
title: s
|
|
4955
|
+
title: sessionTitle(s),
|
|
4314
4956
|
updatedAt: new Date(s.lastModified).toISOString()
|
|
4315
4957
|
}));
|
|
4316
4958
|
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Mở cửa sổ cho một session, đã mở thì thôi. `fresh` = cửa sổ trống, `sessionId` thành id của
|
|
4961
|
+
* session Claude khi gửi tin đầu tiên.
|
|
4962
|
+
*/
|
|
4963
|
+
openWindow(sessionId, fresh = false) {
|
|
4964
|
+
if (this.find(sessionId)) return;
|
|
4965
|
+
if (this.wins.length >= BRIDGE_MAX_WINDOWS) {
|
|
4966
|
+
throw new BridgeError(`\u0110\xE3 m\u1EDF ${BRIDGE_MAX_WINDOWS} c\u1EEDa s\u1ED5, \u0111\xF3ng b\u1EDBt c\u1EEDa s\u1ED5 c\u0169 tr\u01B0\u1EDBc`);
|
|
4967
|
+
}
|
|
4968
|
+
const win = { sessionId, title: "", fresh, abort: null, tail: null };
|
|
4969
|
+
this.wins.push(win);
|
|
4970
|
+
this.saveWindows();
|
|
4971
|
+
this.deps.broadcast(this.stateMessage());
|
|
4972
|
+
if (!fresh) void this.fillTitle(win);
|
|
4973
|
+
}
|
|
4974
|
+
/** Đóng cửa sổ, session vẫn còn (mở lại từ Lịch sử). Đang chạy thì không đóng được. */
|
|
4975
|
+
closeWindow(sessionId) {
|
|
4976
|
+
const win = this.find(sessionId);
|
|
4977
|
+
if (!win) return;
|
|
4978
|
+
if (win.abort) throw new BridgeError("C\u1EEDa s\u1ED5 n\xE0y \u0111ang ch\u1EA1y, d\u1EEBng l\u01B0\u1EE3t tr\u01B0\u1EDBc khi \u0111\xF3ng");
|
|
4979
|
+
this.closeTail(win);
|
|
4980
|
+
this.wins.splice(this.wins.indexOf(win), 1);
|
|
4981
|
+
for (const p of [...this.pending.values()])
|
|
4982
|
+
if (p.win === win) p.finish({ behavior: "deny", message: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng" });
|
|
4983
|
+
if (this.sessionId === sessionId) this.deps.state.sessionId = this.wins.at(-1)?.sessionId ?? null;
|
|
4984
|
+
this.saveWindows();
|
|
4985
|
+
this.deps.broadcast(this.stateMessage());
|
|
4986
|
+
}
|
|
4987
|
+
/** Cửa sổ chưa có tiêu đề (mở từ Lịch sử, file trạng thái bản cũ): lấy tiêu đề session. */
|
|
4988
|
+
async fillTitles() {
|
|
4989
|
+
await Promise.all(this.wins.filter((w) => !w.title && !w.fresh).map((w) => this.fillTitle(w)));
|
|
4990
|
+
}
|
|
4991
|
+
async fillTitle(win) {
|
|
4992
|
+
try {
|
|
4993
|
+
const info = await this.sdk.getSessionInfo?.(win.sessionId, { dir: this.deps.config.cwd });
|
|
4994
|
+
if (!info || win.title) return;
|
|
4995
|
+
win.title = titleFrom(sessionTitle(info));
|
|
4996
|
+
if (!this.wins.includes(win)) return;
|
|
4997
|
+
this.saveWindows();
|
|
4998
|
+
this.deps.broadcast(this.stateMessage());
|
|
4999
|
+
} catch (e) {
|
|
5000
|
+
console.error("[bridge] \u0111\u1ECDc ti\xEAu \u0111\u1EC1 session", e);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
/** App bản cũ: chuyển cửa sổ dùng gần nhất sang session khác (`null` = cửa sổ mới). */
|
|
4317
5004
|
async open(sessionId) {
|
|
4318
|
-
|
|
4319
|
-
this.
|
|
4320
|
-
this.deps.state.sessionId =
|
|
5005
|
+
const id = sessionId ?? crypto.randomUUID();
|
|
5006
|
+
this.openWindow(id, sessionId === null);
|
|
5007
|
+
this.deps.state.sessionId = id;
|
|
4321
5008
|
this.deps.broadcast(this.stateMessage());
|
|
4322
5009
|
const history = await this.historyMessage();
|
|
4323
5010
|
if (history) this.deps.broadcast(history);
|
|
4324
5011
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
5012
|
+
/** Gửi tin vào một cửa sổ. Không chỉ rõ cửa sổ (app bản cũ) thì dùng cửa sổ dùng gần nhất. */
|
|
5013
|
+
send(input, clientId, attachments = [], sessionId) {
|
|
5014
|
+
const win = sessionId === void 0 ? this.currentWindow() : this.find(sessionId);
|
|
5015
|
+
if (!win) throw new BridgeError("C\u1EEDa s\u1ED5 n\xE0y \u0111\xE3 \u0111\xF3ng, m\u1EDF c\u1EEDa s\u1ED5 kh\xE1c \u0111\u1EC3 g\u1EEDi");
|
|
5016
|
+
if (win.abort) throw new BridgeError("Claude \u0111ang ch\u1EA1y \u1EDF c\u1EEDa s\u1ED5 n\xE0y, \u0111\u1EE3i xong ho\u1EB7c b\u1EA5m d\u1EEBng");
|
|
4327
5017
|
if (!input && !attachments.length) throw new BridgeError("Tin nh\u1EAFn tr\u1ED1ng");
|
|
4328
5018
|
let paths;
|
|
4329
5019
|
try {
|
|
@@ -4333,19 +5023,34 @@ var init_session = __esm({
|
|
|
4333
5023
|
throw new BridgeError("Kh\xF4ng l\u01B0u \u0111\u01B0\u1EE3c t\u1EC7p \u0111\xEDnh k\xE8m tr\xEAn PC");
|
|
4334
5024
|
}
|
|
4335
5025
|
const text = promptWithFiles(input, paths);
|
|
4336
|
-
|
|
5026
|
+
const files = paths.flatMap((p) => fileRef(p) ?? []);
|
|
5027
|
+
this.closeTail(win);
|
|
4337
5028
|
const abort = new AbortController();
|
|
4338
|
-
|
|
5029
|
+
win.abort = abort;
|
|
5030
|
+
if (!win.title) win.title = titleFrom(input) || (files[0]?.name ?? "");
|
|
5031
|
+
this.deps.state.sessionId = win.sessionId;
|
|
5032
|
+
this.saveWindows();
|
|
4339
5033
|
this.deps.broadcast(this.stateMessage());
|
|
4340
5034
|
this.deps.broadcast({
|
|
4341
5035
|
type: "item",
|
|
4342
|
-
sessionId:
|
|
4343
|
-
item: {
|
|
5036
|
+
sessionId: win.sessionId,
|
|
5037
|
+
item: {
|
|
5038
|
+
kind: "user",
|
|
5039
|
+
id: crypto.randomUUID(),
|
|
5040
|
+
text: input,
|
|
5041
|
+
...clientId ? { clientId } : {},
|
|
5042
|
+
...files.length ? { files } : {}
|
|
5043
|
+
}
|
|
4344
5044
|
});
|
|
4345
|
-
void this.run(text, abort);
|
|
5045
|
+
void this.run(win, text, abort);
|
|
4346
5046
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
5047
|
+
/** Dừng lượt của một cửa sổ, mặc định cửa sổ dùng gần nhất. */
|
|
5048
|
+
interrupt(sessionId) {
|
|
5049
|
+
this.find(sessionId === void 0 ? this.sessionId : sessionId)?.abort?.abort();
|
|
5050
|
+
}
|
|
5051
|
+
/** Dừng mọi cửa sổ, lúc tắt bridge. */
|
|
5052
|
+
interruptAll() {
|
|
5053
|
+
for (const w of this.wins) w.abort?.abort();
|
|
4349
5054
|
}
|
|
4350
5055
|
replyPermission(requestId, allow, message) {
|
|
4351
5056
|
const p = this.pending.get(requestId);
|
|
@@ -4357,12 +5062,31 @@ var init_session = __esm({
|
|
|
4357
5062
|
this.setOptions({ permissionMode: "default" });
|
|
4358
5063
|
}
|
|
4359
5064
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
5065
|
+
find(sessionId) {
|
|
5066
|
+
return sessionId ? this.wins.find((w) => w.sessionId === sessionId) : void 0;
|
|
5067
|
+
}
|
|
5068
|
+
/** Cửa sổ dùng gần nhất, chưa có thì mở (app bản cũ chỉ biết cửa sổ này). */
|
|
5069
|
+
currentWindow() {
|
|
5070
|
+
const id = this.sessionId;
|
|
5071
|
+
const win = this.find(id);
|
|
5072
|
+
if (win) return win;
|
|
5073
|
+
if (id) this.openWindow(id);
|
|
5074
|
+
else {
|
|
5075
|
+
const fresh = crypto.randomUUID();
|
|
5076
|
+
this.openWindow(fresh, true);
|
|
5077
|
+
this.deps.state.sessionId = fresh;
|
|
5078
|
+
}
|
|
5079
|
+
return this.find(this.sessionId);
|
|
5080
|
+
}
|
|
5081
|
+
saveWindows() {
|
|
5082
|
+
this.deps.state.windows = this.wins;
|
|
5083
|
+
}
|
|
5084
|
+
closeTail(win) {
|
|
5085
|
+
const tail = win.tail;
|
|
5086
|
+
win.tail = null;
|
|
4363
5087
|
tail?.close();
|
|
4364
5088
|
}
|
|
4365
|
-
async run(text, abort) {
|
|
5089
|
+
async run(win, text, abort) {
|
|
4366
5090
|
const { config: config2, broadcast, push } = this.deps;
|
|
4367
5091
|
const started = Date.now();
|
|
4368
5092
|
let ok = false;
|
|
@@ -4371,22 +5095,26 @@ var init_session = __esm({
|
|
|
4371
5095
|
let durationMs = 0;
|
|
4372
5096
|
let ended = false;
|
|
4373
5097
|
const mode = this.permissionMode;
|
|
5098
|
+
const denyPending = () => {
|
|
5099
|
+
for (const p of [...this.pending.values()])
|
|
5100
|
+
if (p.win === win) p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
|
|
5101
|
+
};
|
|
4374
5102
|
const endTurn = () => {
|
|
4375
5103
|
if (ended) return;
|
|
4376
5104
|
ended = true;
|
|
4377
|
-
if (
|
|
4378
|
-
|
|
4379
|
-
p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
|
|
5105
|
+
if (win.abort === abort) win.abort = null;
|
|
5106
|
+
denyPending();
|
|
4380
5107
|
if (!durationMs) durationMs = Date.now() - started;
|
|
4381
5108
|
if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
|
|
4382
|
-
const sessionId =
|
|
5109
|
+
const { sessionId } = win;
|
|
4383
5110
|
broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
|
|
4384
5111
|
broadcast(this.stateMessage());
|
|
4385
5112
|
if (!abort.signal.aborted) {
|
|
4386
5113
|
void push({
|
|
4387
5114
|
event: "turn_end",
|
|
4388
5115
|
title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
|
|
4389
|
-
|
|
5116
|
+
// Nhiều cửa sổ chạy song song: ghi tên cửa sổ để biết cái nào xong
|
|
5117
|
+
body: withWindow(win, resultText || "(kh\xF4ng c\xF3 n\u1ED9i dung)"),
|
|
4390
5118
|
sessionId
|
|
4391
5119
|
});
|
|
4392
5120
|
}
|
|
@@ -4397,9 +5125,10 @@ var init_session = __esm({
|
|
|
4397
5125
|
prompt: text,
|
|
4398
5126
|
options: {
|
|
4399
5127
|
cwd: config2.cwd,
|
|
4400
|
-
|
|
5128
|
+
// Cửa sổ mới: tạo session đúng id app đã biết; còn lại nối tiếp session cũ
|
|
5129
|
+
...win.fresh ? { sessionId: win.sessionId } : { resume: win.sessionId },
|
|
4401
5130
|
abortController: abort,
|
|
4402
|
-
canUseTool: this.canUseTool,
|
|
5131
|
+
canUseTool: this.canUseTool(win),
|
|
4403
5132
|
permissionMode: mode,
|
|
4404
5133
|
...mode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
|
|
4405
5134
|
model: this.deps.state.model ?? config2.model,
|
|
@@ -4410,17 +5139,22 @@ var init_session = __esm({
|
|
|
4410
5139
|
systemPrompt: { type: "preset", preset: "claude_code", append: APPEND_PROMPT }
|
|
4411
5140
|
}
|
|
4412
5141
|
});
|
|
4413
|
-
|
|
5142
|
+
win.tail = q;
|
|
5143
|
+
const sentReads = /* @__PURE__ */ new Set();
|
|
4414
5144
|
for await (const msg of q) {
|
|
4415
5145
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
4416
|
-
|
|
4417
|
-
this.deps.state.sessionId = msg.session_id;
|
|
4418
|
-
broadcast(this.stateMessage());
|
|
4419
|
-
}
|
|
5146
|
+
this.adoptSession(win, msg.session_id);
|
|
4420
5147
|
} else if (msg.type === "assistant") {
|
|
5148
|
+
for (const item of toChatItems(msg, { sentReads }))
|
|
5149
|
+
broadcast({ type: "item", sessionId: win.sessionId, item });
|
|
5150
|
+
} else if (msg.type === "user") {
|
|
5151
|
+
for (const item of toChatItems(msg, { saveImage: this.saveImage, sentReads }))
|
|
5152
|
+
if (item.kind === "image") broadcast({ type: "item", sessionId: win.sessionId, item });
|
|
5153
|
+
} else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
4421
5154
|
for (const item of toChatItems(msg))
|
|
4422
|
-
broadcast({ type: "item", sessionId:
|
|
5155
|
+
broadcast({ type: "item", sessionId: win.sessionId, item });
|
|
4423
5156
|
} else if (msg.type === "result" && !ended) {
|
|
5157
|
+
this.adoptSession(win, msg.session_id || win.sessionId);
|
|
4424
5158
|
ok = msg.subtype === "success" && !msg.is_error;
|
|
4425
5159
|
resultText = msg.subtype === "success" ? msg.result : msg.errors.join("\n") || msg.subtype;
|
|
4426
5160
|
costUsd = msg.total_cost_usd;
|
|
@@ -4434,14 +5168,22 @@ var init_session = __esm({
|
|
|
4434
5168
|
resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : errorText(e);
|
|
4435
5169
|
}
|
|
4436
5170
|
} finally {
|
|
4437
|
-
if (q &&
|
|
5171
|
+
if (q && win.tail === q) win.tail = null;
|
|
4438
5172
|
endTurn();
|
|
4439
|
-
if (!
|
|
4440
|
-
for (const p of this.pending.values())
|
|
4441
|
-
p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
|
|
5173
|
+
if (!win.abort) denyPending();
|
|
4442
5174
|
}
|
|
4443
5175
|
}
|
|
4444
|
-
|
|
5176
|
+
/** Session Claude đã được tạo: cửa sổ thôi là cửa sổ trống, id khác thì đổi theo. */
|
|
5177
|
+
adoptSession(win, sessionId) {
|
|
5178
|
+
const changed = win.sessionId !== sessionId;
|
|
5179
|
+
if (!changed && !win.fresh) return;
|
|
5180
|
+
if (changed && this.sessionId === win.sessionId) this.deps.state.sessionId = sessionId;
|
|
5181
|
+
win.sessionId = sessionId;
|
|
5182
|
+
win.fresh = false;
|
|
5183
|
+
this.saveWindows();
|
|
5184
|
+
if (changed) this.deps.broadcast(this.stateMessage());
|
|
5185
|
+
}
|
|
5186
|
+
canUseTool = (win) => (toolName, input, opts) => new Promise((resolve) => {
|
|
4445
5187
|
if (autoAllows(this.permissionMode, toolName)) {
|
|
4446
5188
|
resolve({ behavior: "allow" });
|
|
4447
5189
|
return;
|
|
@@ -4451,7 +5193,8 @@ var init_session = __esm({
|
|
|
4451
5193
|
requestId,
|
|
4452
5194
|
toolName,
|
|
4453
5195
|
title: opts.title ?? `${toolName}: ${toolSummary(input) || "(kh\xF4ng c\xF3 chi ti\u1EBFt)"}`,
|
|
4454
|
-
input: clipInput(input)
|
|
5196
|
+
input: clipInput(input),
|
|
5197
|
+
sessionId: win.sessionId
|
|
4455
5198
|
};
|
|
4456
5199
|
const finish = (result) => {
|
|
4457
5200
|
if (!this.pending.delete(requestId)) return;
|
|
@@ -4471,13 +5214,13 @@ var init_session = __esm({
|
|
|
4471
5214
|
this.deps.config.permissionTimeoutMs
|
|
4472
5215
|
);
|
|
4473
5216
|
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
4474
|
-
this.pending.set(requestId, { request, finish });
|
|
5217
|
+
this.pending.set(requestId, { request, finish, win });
|
|
4475
5218
|
this.deps.broadcast({ type: "permission_request", request });
|
|
4476
5219
|
void this.deps.push({
|
|
4477
5220
|
event: "permission_request",
|
|
4478
5221
|
title: "Claude c\u1EA7n b\u1EA1n duy\u1EC7t",
|
|
4479
|
-
body: request.title,
|
|
4480
|
-
sessionId:
|
|
5222
|
+
body: withWindow(win, request.title),
|
|
5223
|
+
sessionId: win.sessionId,
|
|
4481
5224
|
requestId
|
|
4482
5225
|
});
|
|
4483
5226
|
});
|
|
@@ -4488,7 +5231,13 @@ var init_session = __esm({
|
|
|
4488
5231
|
// src/server.ts
|
|
4489
5232
|
import { createServer } from "node:http";
|
|
4490
5233
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4491
|
-
function createBridgeServer({
|
|
5234
|
+
function createBridgeServer({
|
|
5235
|
+
token,
|
|
5236
|
+
hub: hub2,
|
|
5237
|
+
session: session2,
|
|
5238
|
+
state: state2,
|
|
5239
|
+
readImage: read = readImage
|
|
5240
|
+
}) {
|
|
4492
5241
|
const http = createServer((req, res) => {
|
|
4493
5242
|
if (req.method === "GET" && req.url === "/health") {
|
|
4494
5243
|
res.writeHead(200, { "content-type": "application/json" }).end('{"ok":true}');
|
|
@@ -4503,8 +5252,8 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4503
5252
|
handleProtocols: (protocols) => protocols.has(BRIDGE_SUBPROTOCOL) ? BRIDGE_SUBPROTOCOL : false
|
|
4504
5253
|
});
|
|
4505
5254
|
http.on("upgrade", (req, socket, head) => {
|
|
4506
|
-
const
|
|
4507
|
-
if (
|
|
5255
|
+
const path8 = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
5256
|
+
if (path8 !== WS_PATH) {
|
|
4508
5257
|
socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
4509
5258
|
return;
|
|
4510
5259
|
}
|
|
@@ -4526,14 +5275,25 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4526
5275
|
case "open_session":
|
|
4527
5276
|
await session2.open(msg.sessionId);
|
|
4528
5277
|
return;
|
|
5278
|
+
case "open_window":
|
|
5279
|
+
session2.openWindow(msg.sessionId, msg.new);
|
|
5280
|
+
return;
|
|
5281
|
+
case "close_window":
|
|
5282
|
+
session2.closeWindow(msg.sessionId);
|
|
5283
|
+
return;
|
|
5284
|
+
case "get_history": {
|
|
5285
|
+
const history = await session2.historyMessage(msg.sessionId);
|
|
5286
|
+
if (history) sendTo(ws, history);
|
|
5287
|
+
return;
|
|
5288
|
+
}
|
|
4529
5289
|
case "send":
|
|
4530
|
-
session2.send(msg.text, msg.clientId, msg.attachments);
|
|
5290
|
+
session2.send(msg.text, msg.clientId, msg.attachments, msg.sessionId);
|
|
4531
5291
|
return;
|
|
4532
5292
|
case "set_options":
|
|
4533
5293
|
session2.setOptions(msg);
|
|
4534
5294
|
return;
|
|
4535
5295
|
case "interrupt":
|
|
4536
|
-
session2.interrupt();
|
|
5296
|
+
session2.interrupt(msg.sessionId);
|
|
4537
5297
|
return;
|
|
4538
5298
|
case "permission_reply":
|
|
4539
5299
|
session2.replyPermission(msg.requestId, msg.allow, msg.message);
|
|
@@ -4544,6 +5304,9 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4544
5304
|
case "unregister_push":
|
|
4545
5305
|
state2.removePushToken(msg.token);
|
|
4546
5306
|
return;
|
|
5307
|
+
case "get_image":
|
|
5308
|
+
sendTo(ws, { type: "image_data", ref: msg.ref, ...read(msg.ref) ?? { data: null } });
|
|
5309
|
+
return;
|
|
4547
5310
|
}
|
|
4548
5311
|
}
|
|
4549
5312
|
wss.on("connection", (ws) => {
|
|
@@ -4606,6 +5369,7 @@ var init_server = __esm({
|
|
|
4606
5369
|
"use strict";
|
|
4607
5370
|
init_bridge();
|
|
4608
5371
|
init_auth();
|
|
5372
|
+
init_images();
|
|
4609
5373
|
init_session();
|
|
4610
5374
|
WS_PATH = "/ws";
|
|
4611
5375
|
PING_MS = 3e4;
|
|
@@ -4624,18 +5388,25 @@ var init_server = __esm({
|
|
|
4624
5388
|
});
|
|
4625
5389
|
|
|
4626
5390
|
// src/state.ts
|
|
4627
|
-
import { mkdirSync as
|
|
4628
|
-
import
|
|
4629
|
-
import { z as
|
|
4630
|
-
var MAX_PUSH_TOKENS, Saved, StateStore;
|
|
5391
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5392
|
+
import path5 from "node:path";
|
|
5393
|
+
import { z as z8 } from "zod";
|
|
5394
|
+
var MAX_PUSH_TOKENS, SavedWindow, Saved, StateStore;
|
|
4631
5395
|
var init_state = __esm({
|
|
4632
5396
|
"src/state.ts"() {
|
|
4633
5397
|
"use strict";
|
|
4634
5398
|
init_bridge();
|
|
4635
5399
|
MAX_PUSH_TOKENS = 10;
|
|
4636
|
-
|
|
4637
|
-
sessionId:
|
|
4638
|
-
|
|
5400
|
+
SavedWindow = z8.object({
|
|
5401
|
+
sessionId: z8.string(),
|
|
5402
|
+
title: z8.string().catch(""),
|
|
5403
|
+
fresh: z8.boolean().catch(false)
|
|
5404
|
+
});
|
|
5405
|
+
Saved = z8.object({
|
|
5406
|
+
/** Cửa sổ dùng gần nhất: app bản cũ chỉ thấy cửa sổ này, app mới mở lên cũng hiện nó trước. */
|
|
5407
|
+
sessionId: z8.string().nullable().catch(null),
|
|
5408
|
+
windows: z8.array(SavedWindow).catch([]),
|
|
5409
|
+
pushTokens: z8.array(z8.string()).catch([]),
|
|
4639
5410
|
/** Chọn từ app. `null` = theo .env. */
|
|
4640
5411
|
permissionMode: BridgePermissionMode.nullable().catch(null),
|
|
4641
5412
|
model: BridgeModel.catch(null)
|
|
@@ -4643,9 +5414,9 @@ var init_state = __esm({
|
|
|
4643
5414
|
StateStore = class {
|
|
4644
5415
|
constructor(file) {
|
|
4645
5416
|
this.file = file;
|
|
4646
|
-
this.data = { sessionId: null, pushTokens: [], permissionMode: null, model: null };
|
|
5417
|
+
this.data = { sessionId: null, windows: [], pushTokens: [], permissionMode: null, model: null };
|
|
4647
5418
|
try {
|
|
4648
|
-
const r = Saved.safeParse(JSON.parse(
|
|
5419
|
+
const r = Saved.safeParse(JSON.parse(readFileSync2(file, "utf8")));
|
|
4649
5420
|
if (r.success) this.data = r.data;
|
|
4650
5421
|
} catch {
|
|
4651
5422
|
}
|
|
@@ -4659,6 +5430,15 @@ var init_state = __esm({
|
|
|
4659
5430
|
this.data.sessionId = id;
|
|
4660
5431
|
this.save();
|
|
4661
5432
|
}
|
|
5433
|
+
get windows() {
|
|
5434
|
+
return this.data.windows;
|
|
5435
|
+
}
|
|
5436
|
+
set windows(list) {
|
|
5437
|
+
const next = list.map(({ sessionId, title, fresh }) => ({ sessionId, title, fresh }));
|
|
5438
|
+
if (JSON.stringify(next) === JSON.stringify(this.data.windows)) return;
|
|
5439
|
+
this.data.windows = next;
|
|
5440
|
+
this.save();
|
|
5441
|
+
}
|
|
4662
5442
|
get permissionMode() {
|
|
4663
5443
|
return this.data.permissionMode;
|
|
4664
5444
|
}
|
|
@@ -4690,9 +5470,9 @@ var init_state = __esm({
|
|
|
4690
5470
|
this.save();
|
|
4691
5471
|
}
|
|
4692
5472
|
save() {
|
|
4693
|
-
|
|
5473
|
+
mkdirSync3(path5.dirname(this.file), { recursive: true });
|
|
4694
5474
|
const tmp = `${this.file}.tmp`;
|
|
4695
|
-
|
|
5475
|
+
writeFileSync3(tmp, JSON.stringify(this.data, null, 2));
|
|
4696
5476
|
renameSync(tmp, this.file);
|
|
4697
5477
|
}
|
|
4698
5478
|
};
|
|
@@ -4701,15 +5481,15 @@ var init_state = __esm({
|
|
|
4701
5481
|
|
|
4702
5482
|
// src/tunnel.ts
|
|
4703
5483
|
import { spawn } from "node:child_process";
|
|
4704
|
-
import { existsSync } from "node:fs";
|
|
4705
|
-
import
|
|
5484
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5485
|
+
import path6 from "node:path";
|
|
4706
5486
|
import { createInterface } from "node:readline";
|
|
4707
|
-
function findCloudflared(configured, env = process.env, exists =
|
|
5487
|
+
function findCloudflared(configured, env = process.env, exists = existsSync3, platform = process.platform) {
|
|
4708
5488
|
if (configured) return configured;
|
|
4709
5489
|
if (platform === "win32") {
|
|
4710
5490
|
for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
|
|
4711
5491
|
if (!base) continue;
|
|
4712
|
-
const p =
|
|
5492
|
+
const p = path6.win32.join(base, "cloudflared", "cloudflared.exe");
|
|
4713
5493
|
if (exists(p)) return p;
|
|
4714
5494
|
}
|
|
4715
5495
|
}
|
|
@@ -4822,12 +5602,17 @@ var init_index = __esm({
|
|
|
4822
5602
|
config,
|
|
4823
5603
|
state,
|
|
4824
5604
|
broadcast: (msg) => hub.broadcast(msg),
|
|
4825
|
-
push: createPusher(state, config.expoAccessToken
|
|
5605
|
+
push: createPusher(state, config.expoAccessToken, fetch, {
|
|
5606
|
+
bridgeId: config.instance.id,
|
|
5607
|
+
project: config.instance.project
|
|
5608
|
+
})
|
|
4826
5609
|
});
|
|
5610
|
+
void session.fillTitles();
|
|
4827
5611
|
server = createBridgeServer({ token: config.token, hub, session, state });
|
|
4828
5612
|
announcer = new UrlAnnouncer({
|
|
4829
5613
|
apiUrl: config.apiUrl,
|
|
4830
5614
|
key: deriveBridgeKey(config.token),
|
|
5615
|
+
instance: config.instance,
|
|
4831
5616
|
onReplaced: () => void shutdown()
|
|
4832
5617
|
});
|
|
4833
5618
|
REPLACED_CHECK_MS = 6e4;
|
|
@@ -4838,6 +5623,7 @@ var init_index = __esm({
|
|
|
4838
5623
|
const port = typeof address === "object" && address ? address.port : config.port;
|
|
4839
5624
|
console.log(`Agent bridge: ws://127.0.0.1:${port}/ws`);
|
|
4840
5625
|
console.log(`Th\u01B0 m\u1EE5c l\xE0m vi\u1EC7c: ${config.cwd}`);
|
|
5626
|
+
console.log(`Project: ${config.instance.project} @ ${config.instance.machine}`);
|
|
4841
5627
|
console.log(`Ch\u1EBF \u0111\u1ED9 quy\u1EC1n: ${config.permissionMode}`);
|
|
4842
5628
|
if (cloudflared) {
|
|
4843
5629
|
console.log("\u0110ang m\u1EDF Cloudflare tunnel...");
|
|
@@ -4867,7 +5653,7 @@ var init_index = __esm({
|
|
|
4867
5653
|
shutdown = async () => {
|
|
4868
5654
|
if (closing) return;
|
|
4869
5655
|
closing = true;
|
|
4870
|
-
session.
|
|
5656
|
+
session.interruptAll();
|
|
4871
5657
|
for (const ws of hub.clients) ws.close(1001, "bridge t\u1EAFt");
|
|
4872
5658
|
server.close();
|
|
4873
5659
|
tunnel?.stop();
|
|
@@ -4883,13 +5669,14 @@ var init_index = __esm({
|
|
|
4883
5669
|
// src/cli.ts
|
|
4884
5670
|
init_config();
|
|
4885
5671
|
import { randomBytes } from "node:crypto";
|
|
4886
|
-
import { existsSync as
|
|
4887
|
-
import
|
|
5672
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5673
|
+
import path7 from "node:path";
|
|
4888
5674
|
var HELP = `C\xE1ch d\xF9ng: manualofme-bridge [th\u01B0 m\u1EE5c]
|
|
4889
5675
|
|
|
4890
5676
|
M\u1EDF Claude Code tr\xEAn m\xE1y n\xE0y cho web/app ManualOfMe \u0111i\u1EC1u khi\u1EC3n. Claude l\xE0m vi\u1EC7c trong
|
|
4891
|
-
[th\u01B0 m\u1EE5c], b\u1ECF tr\u1ED1ng th\xEC l\xE0 th\u01B0 m\u1EE5c \u0111ang \u0111\u1EE9ng.
|
|
4892
|
-
|
|
5677
|
+
[th\u01B0 m\u1EE5c], b\u1ECF tr\u1ED1ng th\xEC l\xE0 th\u01B0 m\u1EE5c \u0111ang \u0111\u1EE9ng. Ch\u1EA1y \u0111\u01B0\u1EE3c nhi\u1EC1u bridge c\xF9ng token, m\u1ED7i
|
|
5678
|
+
project m\u1ED7i m\xE1y m\u1ED9t bridge (t\xEAn project: BRIDGE_NAME, m\u1EB7c \u0111\u1ECBnh t\xEAn th\u01B0 m\u1EE5c). B\u1EADt l\u1EA1i
|
|
5679
|
+
c\xF9ng th\u01B0 m\u1EE5c th\xEC bridge c\u0169 t\u1EF1 t\u1EAFt.
|
|
4893
5680
|
|
|
4894
5681
|
--token Nh\u1EADp l\u1EA1i token (d\xE1n nh\u1EA7m, \u0111\u1ED5i token), l\u01B0u \u0111\xE8 token c\u0169 r\u1ED3i ch\u1EA1y bridge
|
|
4895
5682
|
|
|
@@ -4904,8 +5691,8 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
4904
5691
|
var resetToken = args.includes("--token");
|
|
4905
5692
|
var dirArg = args.find((a) => !a.startsWith("-"));
|
|
4906
5693
|
if (dirArg) {
|
|
4907
|
-
const dir =
|
|
4908
|
-
if (!
|
|
5694
|
+
const dir = path7.resolve(dirArg);
|
|
5695
|
+
if (!existsSync4(dir) || !statSync3(dir).isDirectory()) {
|
|
4909
5696
|
console.error(`Kh\xF4ng c\xF3 th\u01B0 m\u1EE5c ${dir}`);
|
|
4910
5697
|
process.exit(1);
|
|
4911
5698
|
}
|
|
@@ -4949,10 +5736,10 @@ async function askToken(reset) {
|
|
|
4949
5736
|
}
|
|
4950
5737
|
}
|
|
4951
5738
|
function saveToken(token) {
|
|
4952
|
-
|
|
4953
|
-
const old =
|
|
5739
|
+
mkdirSync4(CONFIG_DIR, { recursive: true });
|
|
5740
|
+
const old = existsSync4(USER_ENV_FILE) ? readFileSync3(USER_ENV_FILE, "utf8") : "";
|
|
4954
5741
|
const lines = old.split(/\r?\n/).filter((l) => l && !/^\s*BRIDGE_TOKEN\s*=/.test(l));
|
|
4955
|
-
|
|
5742
|
+
writeFileSync4(USER_ENV_FILE, [`BRIDGE_TOKEN=${token}`, ...lines, ""].join("\n"), { mode: 384 });
|
|
4956
5743
|
}
|
|
4957
5744
|
function readHidden(prompt) {
|
|
4958
5745
|
return new Promise((resolve) => {
|