manualofme-bridge 0.2.0 → 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 +1358 -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,33 +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
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. */
|
|
4261
4881
|
get sessionId() {
|
|
4262
4882
|
return this.deps.state.sessionId;
|
|
4263
4883
|
}
|
|
4884
|
+
/** Có cửa sổ nào đang chạy không. */
|
|
4264
4885
|
get busy() {
|
|
4265
|
-
return this.abort !== null;
|
|
4886
|
+
return this.wins.some((w) => w.abort !== null);
|
|
4266
4887
|
}
|
|
4267
4888
|
/** Chế độ duyệt đang dùng: chọn từ app, chưa chọn thì theo .env. */
|
|
4268
4889
|
get permissionMode() {
|
|
4269
4890
|
return this.deps.state.permissionMode ?? this.deps.config.permissionMode;
|
|
4270
4891
|
}
|
|
4892
|
+
windows() {
|
|
4893
|
+
return this.wins.map((w) => ({ sessionId: w.sessionId, title: w.title, busy: w.abort !== null }));
|
|
4894
|
+
}
|
|
4271
4895
|
stateMessage() {
|
|
4272
4896
|
return {
|
|
4273
4897
|
type: "state",
|
|
4274
4898
|
sessionId: this.sessionId,
|
|
4275
|
-
busy: this.
|
|
4899
|
+
busy: !!this.find(this.sessionId)?.abort,
|
|
4276
4900
|
pendingPermissions: [...this.pending.values()].map((p) => p.request),
|
|
4277
4901
|
permissionMode: this.permissionMode,
|
|
4278
|
-
model: this.deps.state.model
|
|
4902
|
+
model: this.deps.state.model,
|
|
4903
|
+
windows: this.windows()
|
|
4279
4904
|
};
|
|
4280
4905
|
}
|
|
4281
4906
|
/**
|
|
@@ -4293,31 +4918,102 @@ var init_session = __esm({
|
|
|
4293
4918
|
}
|
|
4294
4919
|
this.deps.broadcast(this.stateMessage());
|
|
4295
4920
|
}
|
|
4296
|
-
/**
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
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
|
+
}
|
|
4303
4950
|
}
|
|
4304
4951
|
async sessions() {
|
|
4305
4952
|
const list = await this.sdk.listSessions({ dir: this.deps.config.cwd, limit: 30 });
|
|
4306
4953
|
return list.map((s) => ({
|
|
4307
4954
|
sessionId: s.sessionId,
|
|
4308
|
-
title: s
|
|
4955
|
+
title: sessionTitle(s),
|
|
4309
4956
|
updatedAt: new Date(s.lastModified).toISOString()
|
|
4310
4957
|
}));
|
|
4311
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). */
|
|
4312
5004
|
async open(sessionId) {
|
|
4313
|
-
|
|
4314
|
-
this.
|
|
5005
|
+
const id = sessionId ?? crypto.randomUUID();
|
|
5006
|
+
this.openWindow(id, sessionId === null);
|
|
5007
|
+
this.deps.state.sessionId = id;
|
|
4315
5008
|
this.deps.broadcast(this.stateMessage());
|
|
4316
5009
|
const history = await this.historyMessage();
|
|
4317
5010
|
if (history) this.deps.broadcast(history);
|
|
4318
5011
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
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");
|
|
4321
5017
|
if (!input && !attachments.length) throw new BridgeError("Tin nh\u1EAFn tr\u1ED1ng");
|
|
4322
5018
|
let paths;
|
|
4323
5019
|
try {
|
|
@@ -4327,18 +5023,34 @@ var init_session = __esm({
|
|
|
4327
5023
|
throw new BridgeError("Kh\xF4ng l\u01B0u \u0111\u01B0\u1EE3c t\u1EC7p \u0111\xEDnh k\xE8m tr\xEAn PC");
|
|
4328
5024
|
}
|
|
4329
5025
|
const text = promptWithFiles(input, paths);
|
|
5026
|
+
const files = paths.flatMap((p) => fileRef(p) ?? []);
|
|
5027
|
+
this.closeTail(win);
|
|
4330
5028
|
const abort = new AbortController();
|
|
4331
|
-
|
|
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();
|
|
4332
5033
|
this.deps.broadcast(this.stateMessage());
|
|
4333
5034
|
this.deps.broadcast({
|
|
4334
5035
|
type: "item",
|
|
4335
|
-
sessionId:
|
|
4336
|
-
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
|
+
}
|
|
4337
5044
|
});
|
|
4338
|
-
void this.run(text, abort);
|
|
5045
|
+
void this.run(win, text, abort);
|
|
5046
|
+
}
|
|
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();
|
|
4339
5050
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
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();
|
|
4342
5054
|
}
|
|
4343
5055
|
replyPermission(requestId, allow, message) {
|
|
4344
5056
|
const p = this.pending.get(requestId);
|
|
@@ -4350,22 +5062,73 @@ var init_session = __esm({
|
|
|
4350
5062
|
this.setOptions({ permissionMode: "default" });
|
|
4351
5063
|
}
|
|
4352
5064
|
}
|
|
4353
|
-
|
|
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;
|
|
5087
|
+
tail?.close();
|
|
5088
|
+
}
|
|
5089
|
+
async run(win, text, abort) {
|
|
4354
5090
|
const { config: config2, broadcast, push } = this.deps;
|
|
4355
5091
|
const started = Date.now();
|
|
4356
5092
|
let ok = false;
|
|
4357
5093
|
let resultText = "";
|
|
4358
5094
|
let costUsd = 0;
|
|
4359
5095
|
let durationMs = 0;
|
|
5096
|
+
let ended = false;
|
|
4360
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
|
+
};
|
|
5102
|
+
const endTurn = () => {
|
|
5103
|
+
if (ended) return;
|
|
5104
|
+
ended = true;
|
|
5105
|
+
if (win.abort === abort) win.abort = null;
|
|
5106
|
+
denyPending();
|
|
5107
|
+
if (!durationMs) durationMs = Date.now() - started;
|
|
5108
|
+
if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
|
|
5109
|
+
const { sessionId } = win;
|
|
5110
|
+
broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
|
|
5111
|
+
broadcast(this.stateMessage());
|
|
5112
|
+
if (!abort.signal.aborted) {
|
|
5113
|
+
void push({
|
|
5114
|
+
event: "turn_end",
|
|
5115
|
+
title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
|
|
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)"),
|
|
5118
|
+
sessionId
|
|
5119
|
+
});
|
|
5120
|
+
}
|
|
5121
|
+
};
|
|
5122
|
+
let q = null;
|
|
4361
5123
|
try {
|
|
4362
|
-
|
|
5124
|
+
q = this.sdk.query({
|
|
4363
5125
|
prompt: text,
|
|
4364
5126
|
options: {
|
|
4365
5127
|
cwd: config2.cwd,
|
|
4366
|
-
|
|
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 },
|
|
4367
5130
|
abortController: abort,
|
|
4368
|
-
canUseTool: this.canUseTool,
|
|
5131
|
+
canUseTool: this.canUseTool(win),
|
|
4369
5132
|
permissionMode: mode,
|
|
4370
5133
|
...mode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
|
|
4371
5134
|
model: this.deps.state.model ?? config2.model,
|
|
@@ -4376,45 +5139,51 @@ var init_session = __esm({
|
|
|
4376
5139
|
systemPrompt: { type: "preset", preset: "claude_code", append: APPEND_PROMPT }
|
|
4377
5140
|
}
|
|
4378
5141
|
});
|
|
5142
|
+
win.tail = q;
|
|
5143
|
+
const sentReads = /* @__PURE__ */ new Set();
|
|
4379
5144
|
for await (const msg of q) {
|
|
4380
5145
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
4381
|
-
|
|
4382
|
-
this.deps.state.sessionId = msg.session_id;
|
|
4383
|
-
broadcast(this.stateMessage());
|
|
4384
|
-
}
|
|
5146
|
+
this.adoptSession(win, msg.session_id);
|
|
4385
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") {
|
|
4386
5154
|
for (const item of toChatItems(msg))
|
|
4387
|
-
broadcast({ type: "item", sessionId:
|
|
4388
|
-
} else if (msg.type === "result") {
|
|
5155
|
+
broadcast({ type: "item", sessionId: win.sessionId, item });
|
|
5156
|
+
} else if (msg.type === "result" && !ended) {
|
|
5157
|
+
this.adoptSession(win, msg.session_id || win.sessionId);
|
|
4389
5158
|
ok = msg.subtype === "success" && !msg.is_error;
|
|
4390
5159
|
resultText = msg.subtype === "success" ? msg.result : msg.errors.join("\n") || msg.subtype;
|
|
4391
5160
|
costUsd = msg.total_cost_usd;
|
|
4392
5161
|
durationMs = msg.duration_ms;
|
|
5162
|
+
endTurn();
|
|
4393
5163
|
}
|
|
4394
5164
|
}
|
|
4395
5165
|
} catch (e) {
|
|
4396
|
-
|
|
4397
|
-
|
|
5166
|
+
if (!ended) {
|
|
5167
|
+
ok = false;
|
|
5168
|
+
resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : errorText(e);
|
|
5169
|
+
}
|
|
4398
5170
|
} finally {
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
}
|
|
4403
|
-
if (!durationMs) durationMs = Date.now() - started;
|
|
4404
|
-
if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
|
|
4405
|
-
const sessionId = this.sessionId;
|
|
4406
|
-
broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
|
|
4407
|
-
broadcast(this.stateMessage());
|
|
4408
|
-
if (!abort.signal.aborted) {
|
|
4409
|
-
void push({
|
|
4410
|
-
event: "turn_end",
|
|
4411
|
-
title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
|
|
4412
|
-
body: resultText || "(kh\xF4ng c\xF3 n\u1ED9i dung)",
|
|
4413
|
-
sessionId
|
|
4414
|
-
});
|
|
5171
|
+
if (q && win.tail === q) win.tail = null;
|
|
5172
|
+
endTurn();
|
|
5173
|
+
if (!win.abort) denyPending();
|
|
4415
5174
|
}
|
|
4416
5175
|
}
|
|
4417
|
-
|
|
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) => {
|
|
4418
5187
|
if (autoAllows(this.permissionMode, toolName)) {
|
|
4419
5188
|
resolve({ behavior: "allow" });
|
|
4420
5189
|
return;
|
|
@@ -4424,7 +5193,8 @@ var init_session = __esm({
|
|
|
4424
5193
|
requestId,
|
|
4425
5194
|
toolName,
|
|
4426
5195
|
title: opts.title ?? `${toolName}: ${toolSummary(input) || "(kh\xF4ng c\xF3 chi ti\u1EBFt)"}`,
|
|
4427
|
-
input: clipInput(input)
|
|
5196
|
+
input: clipInput(input),
|
|
5197
|
+
sessionId: win.sessionId
|
|
4428
5198
|
};
|
|
4429
5199
|
const finish = (result) => {
|
|
4430
5200
|
if (!this.pending.delete(requestId)) return;
|
|
@@ -4444,13 +5214,13 @@ var init_session = __esm({
|
|
|
4444
5214
|
this.deps.config.permissionTimeoutMs
|
|
4445
5215
|
);
|
|
4446
5216
|
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
4447
|
-
this.pending.set(requestId, { request, finish });
|
|
5217
|
+
this.pending.set(requestId, { request, finish, win });
|
|
4448
5218
|
this.deps.broadcast({ type: "permission_request", request });
|
|
4449
5219
|
void this.deps.push({
|
|
4450
5220
|
event: "permission_request",
|
|
4451
5221
|
title: "Claude c\u1EA7n b\u1EA1n duy\u1EC7t",
|
|
4452
|
-
body: request.title,
|
|
4453
|
-
sessionId:
|
|
5222
|
+
body: withWindow(win, request.title),
|
|
5223
|
+
sessionId: win.sessionId,
|
|
4454
5224
|
requestId
|
|
4455
5225
|
});
|
|
4456
5226
|
});
|
|
@@ -4461,7 +5231,13 @@ var init_session = __esm({
|
|
|
4461
5231
|
// src/server.ts
|
|
4462
5232
|
import { createServer } from "node:http";
|
|
4463
5233
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4464
|
-
function createBridgeServer({
|
|
5234
|
+
function createBridgeServer({
|
|
5235
|
+
token,
|
|
5236
|
+
hub: hub2,
|
|
5237
|
+
session: session2,
|
|
5238
|
+
state: state2,
|
|
5239
|
+
readImage: read = readImage
|
|
5240
|
+
}) {
|
|
4465
5241
|
const http = createServer((req, res) => {
|
|
4466
5242
|
if (req.method === "GET" && req.url === "/health") {
|
|
4467
5243
|
res.writeHead(200, { "content-type": "application/json" }).end('{"ok":true}');
|
|
@@ -4476,8 +5252,8 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4476
5252
|
handleProtocols: (protocols) => protocols.has(BRIDGE_SUBPROTOCOL) ? BRIDGE_SUBPROTOCOL : false
|
|
4477
5253
|
});
|
|
4478
5254
|
http.on("upgrade", (req, socket, head) => {
|
|
4479
|
-
const
|
|
4480
|
-
if (
|
|
5255
|
+
const path8 = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
5256
|
+
if (path8 !== WS_PATH) {
|
|
4481
5257
|
socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
4482
5258
|
return;
|
|
4483
5259
|
}
|
|
@@ -4499,14 +5275,25 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4499
5275
|
case "open_session":
|
|
4500
5276
|
await session2.open(msg.sessionId);
|
|
4501
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
|
+
}
|
|
4502
5289
|
case "send":
|
|
4503
|
-
session2.send(msg.text, msg.clientId, msg.attachments);
|
|
5290
|
+
session2.send(msg.text, msg.clientId, msg.attachments, msg.sessionId);
|
|
4504
5291
|
return;
|
|
4505
5292
|
case "set_options":
|
|
4506
5293
|
session2.setOptions(msg);
|
|
4507
5294
|
return;
|
|
4508
5295
|
case "interrupt":
|
|
4509
|
-
session2.interrupt();
|
|
5296
|
+
session2.interrupt(msg.sessionId);
|
|
4510
5297
|
return;
|
|
4511
5298
|
case "permission_reply":
|
|
4512
5299
|
session2.replyPermission(msg.requestId, msg.allow, msg.message);
|
|
@@ -4517,6 +5304,9 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
|
|
|
4517
5304
|
case "unregister_push":
|
|
4518
5305
|
state2.removePushToken(msg.token);
|
|
4519
5306
|
return;
|
|
5307
|
+
case "get_image":
|
|
5308
|
+
sendTo(ws, { type: "image_data", ref: msg.ref, ...read(msg.ref) ?? { data: null } });
|
|
5309
|
+
return;
|
|
4520
5310
|
}
|
|
4521
5311
|
}
|
|
4522
5312
|
wss.on("connection", (ws) => {
|
|
@@ -4579,6 +5369,7 @@ var init_server = __esm({
|
|
|
4579
5369
|
"use strict";
|
|
4580
5370
|
init_bridge();
|
|
4581
5371
|
init_auth();
|
|
5372
|
+
init_images();
|
|
4582
5373
|
init_session();
|
|
4583
5374
|
WS_PATH = "/ws";
|
|
4584
5375
|
PING_MS = 3e4;
|
|
@@ -4597,18 +5388,25 @@ var init_server = __esm({
|
|
|
4597
5388
|
});
|
|
4598
5389
|
|
|
4599
5390
|
// src/state.ts
|
|
4600
|
-
import { mkdirSync as
|
|
4601
|
-
import
|
|
4602
|
-
import { z as
|
|
4603
|
-
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;
|
|
4604
5395
|
var init_state = __esm({
|
|
4605
5396
|
"src/state.ts"() {
|
|
4606
5397
|
"use strict";
|
|
4607
5398
|
init_bridge();
|
|
4608
5399
|
MAX_PUSH_TOKENS = 10;
|
|
4609
|
-
|
|
4610
|
-
sessionId:
|
|
4611
|
-
|
|
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([]),
|
|
4612
5410
|
/** Chọn từ app. `null` = theo .env. */
|
|
4613
5411
|
permissionMode: BridgePermissionMode.nullable().catch(null),
|
|
4614
5412
|
model: BridgeModel.catch(null)
|
|
@@ -4616,9 +5414,9 @@ var init_state = __esm({
|
|
|
4616
5414
|
StateStore = class {
|
|
4617
5415
|
constructor(file) {
|
|
4618
5416
|
this.file = file;
|
|
4619
|
-
this.data = { sessionId: null, pushTokens: [], permissionMode: null, model: null };
|
|
5417
|
+
this.data = { sessionId: null, windows: [], pushTokens: [], permissionMode: null, model: null };
|
|
4620
5418
|
try {
|
|
4621
|
-
const r = Saved.safeParse(JSON.parse(
|
|
5419
|
+
const r = Saved.safeParse(JSON.parse(readFileSync2(file, "utf8")));
|
|
4622
5420
|
if (r.success) this.data = r.data;
|
|
4623
5421
|
} catch {
|
|
4624
5422
|
}
|
|
@@ -4632,6 +5430,15 @@ var init_state = __esm({
|
|
|
4632
5430
|
this.data.sessionId = id;
|
|
4633
5431
|
this.save();
|
|
4634
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
|
+
}
|
|
4635
5442
|
get permissionMode() {
|
|
4636
5443
|
return this.data.permissionMode;
|
|
4637
5444
|
}
|
|
@@ -4663,9 +5470,9 @@ var init_state = __esm({
|
|
|
4663
5470
|
this.save();
|
|
4664
5471
|
}
|
|
4665
5472
|
save() {
|
|
4666
|
-
|
|
5473
|
+
mkdirSync3(path5.dirname(this.file), { recursive: true });
|
|
4667
5474
|
const tmp = `${this.file}.tmp`;
|
|
4668
|
-
|
|
5475
|
+
writeFileSync3(tmp, JSON.stringify(this.data, null, 2));
|
|
4669
5476
|
renameSync(tmp, this.file);
|
|
4670
5477
|
}
|
|
4671
5478
|
};
|
|
@@ -4674,15 +5481,15 @@ var init_state = __esm({
|
|
|
4674
5481
|
|
|
4675
5482
|
// src/tunnel.ts
|
|
4676
5483
|
import { spawn } from "node:child_process";
|
|
4677
|
-
import { existsSync } from "node:fs";
|
|
4678
|
-
import
|
|
5484
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5485
|
+
import path6 from "node:path";
|
|
4679
5486
|
import { createInterface } from "node:readline";
|
|
4680
|
-
function findCloudflared(configured, env = process.env, exists =
|
|
5487
|
+
function findCloudflared(configured, env = process.env, exists = existsSync3, platform = process.platform) {
|
|
4681
5488
|
if (configured) return configured;
|
|
4682
5489
|
if (platform === "win32") {
|
|
4683
5490
|
for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
|
|
4684
5491
|
if (!base) continue;
|
|
4685
|
-
const p =
|
|
5492
|
+
const p = path6.win32.join(base, "cloudflared", "cloudflared.exe");
|
|
4686
5493
|
if (exists(p)) return p;
|
|
4687
5494
|
}
|
|
4688
5495
|
}
|
|
@@ -4795,12 +5602,17 @@ var init_index = __esm({
|
|
|
4795
5602
|
config,
|
|
4796
5603
|
state,
|
|
4797
5604
|
broadcast: (msg) => hub.broadcast(msg),
|
|
4798
|
-
push: createPusher(state, config.expoAccessToken
|
|
5605
|
+
push: createPusher(state, config.expoAccessToken, fetch, {
|
|
5606
|
+
bridgeId: config.instance.id,
|
|
5607
|
+
project: config.instance.project
|
|
5608
|
+
})
|
|
4799
5609
|
});
|
|
5610
|
+
void session.fillTitles();
|
|
4800
5611
|
server = createBridgeServer({ token: config.token, hub, session, state });
|
|
4801
5612
|
announcer = new UrlAnnouncer({
|
|
4802
5613
|
apiUrl: config.apiUrl,
|
|
4803
5614
|
key: deriveBridgeKey(config.token),
|
|
5615
|
+
instance: config.instance,
|
|
4804
5616
|
onReplaced: () => void shutdown()
|
|
4805
5617
|
});
|
|
4806
5618
|
REPLACED_CHECK_MS = 6e4;
|
|
@@ -4811,6 +5623,7 @@ var init_index = __esm({
|
|
|
4811
5623
|
const port = typeof address === "object" && address ? address.port : config.port;
|
|
4812
5624
|
console.log(`Agent bridge: ws://127.0.0.1:${port}/ws`);
|
|
4813
5625
|
console.log(`Th\u01B0 m\u1EE5c l\xE0m vi\u1EC7c: ${config.cwd}`);
|
|
5626
|
+
console.log(`Project: ${config.instance.project} @ ${config.instance.machine}`);
|
|
4814
5627
|
console.log(`Ch\u1EBF \u0111\u1ED9 quy\u1EC1n: ${config.permissionMode}`);
|
|
4815
5628
|
if (cloudflared) {
|
|
4816
5629
|
console.log("\u0110ang m\u1EDF Cloudflare tunnel...");
|
|
@@ -4840,7 +5653,7 @@ var init_index = __esm({
|
|
|
4840
5653
|
shutdown = async () => {
|
|
4841
5654
|
if (closing) return;
|
|
4842
5655
|
closing = true;
|
|
4843
|
-
session.
|
|
5656
|
+
session.interruptAll();
|
|
4844
5657
|
for (const ws of hub.clients) ws.close(1001, "bridge t\u1EAFt");
|
|
4845
5658
|
server.close();
|
|
4846
5659
|
tunnel?.stop();
|
|
@@ -4856,13 +5669,14 @@ var init_index = __esm({
|
|
|
4856
5669
|
// src/cli.ts
|
|
4857
5670
|
init_config();
|
|
4858
5671
|
import { randomBytes } from "node:crypto";
|
|
4859
|
-
import { existsSync as
|
|
4860
|
-
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";
|
|
4861
5674
|
var HELP = `C\xE1ch d\xF9ng: manualofme-bridge [th\u01B0 m\u1EE5c]
|
|
4862
5675
|
|
|
4863
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
|
|
4864
|
-
[th\u01B0 m\u1EE5c], b\u1ECF tr\u1ED1ng th\xEC l\xE0 th\u01B0 m\u1EE5c \u0111ang \u0111\u1EE9ng.
|
|
4865
|
-
|
|
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.
|
|
4866
5680
|
|
|
4867
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
|
|
4868
5682
|
|
|
@@ -4877,8 +5691,8 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
4877
5691
|
var resetToken = args.includes("--token");
|
|
4878
5692
|
var dirArg = args.find((a) => !a.startsWith("-"));
|
|
4879
5693
|
if (dirArg) {
|
|
4880
|
-
const dir =
|
|
4881
|
-
if (!
|
|
5694
|
+
const dir = path7.resolve(dirArg);
|
|
5695
|
+
if (!existsSync4(dir) || !statSync3(dir).isDirectory()) {
|
|
4882
5696
|
console.error(`Kh\xF4ng c\xF3 th\u01B0 m\u1EE5c ${dir}`);
|
|
4883
5697
|
process.exit(1);
|
|
4884
5698
|
}
|
|
@@ -4922,10 +5736,10 @@ async function askToken(reset) {
|
|
|
4922
5736
|
}
|
|
4923
5737
|
}
|
|
4924
5738
|
function saveToken(token) {
|
|
4925
|
-
|
|
4926
|
-
const old =
|
|
5739
|
+
mkdirSync4(CONFIG_DIR, { recursive: true });
|
|
5740
|
+
const old = existsSync4(USER_ENV_FILE) ? readFileSync3(USER_ENV_FILE, "utf8") : "";
|
|
4927
5741
|
const lines = old.split(/\r?\n/).filter((l) => l && !/^\s*BRIDGE_TOKEN\s*=/.test(l));
|
|
4928
|
-
|
|
5742
|
+
writeFileSync4(USER_ENV_FILE, [`BRIDGE_TOKEN=${token}`, ...lines, ""].join("\n"), { mode: 384 });
|
|
4929
5743
|
}
|
|
4930
5744
|
function readHidden(prompt) {
|
|
4931
5745
|
return new Promise((resolve) => {
|