@zaclinley/dsh-mailharbor 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +41 -21
- package/README.zh.md +34 -15
- package/lib/client.js +1552 -557
- package/lib/client.js.map +4 -4
- package/lib/index.js +833 -66
- package/lib/index.js.map +4 -4
- package/package.json +3 -2
- package/scripts/migrate-legacy.mjs +2 -1
package/lib/index.js
CHANGED
|
@@ -326,15 +326,16 @@ var init_imap = __esm({
|
|
|
326
326
|
}
|
|
327
327
|
return result;
|
|
328
328
|
}
|
|
329
|
-
async list(mailbox, limit) {
|
|
329
|
+
async list(mailbox, limit, offset = 0) {
|
|
330
330
|
return this.run(async (client) => {
|
|
331
331
|
const lock = await client.getMailboxLock(mailbox);
|
|
332
332
|
try {
|
|
333
333
|
const total = Number(client.mailbox?.exists ?? 0);
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
const end = total - offset;
|
|
335
|
+
if (total === 0 || end < 1) return { total, items: [] };
|
|
336
|
+
const start = Math.max(1, end - limit + 1);
|
|
336
337
|
const items = [];
|
|
337
|
-
for await (const message of client.fetch(`${String(start)}
|
|
338
|
+
for await (const message of client.fetch(`${String(start)}:${String(end)}`, {
|
|
338
339
|
uid: true,
|
|
339
340
|
envelope: true,
|
|
340
341
|
flags: true,
|
|
@@ -468,6 +469,7 @@ var init_imap = __esm({
|
|
|
468
469
|
|
|
469
470
|
// src/host/api.ts
|
|
470
471
|
init_address();
|
|
472
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
471
473
|
|
|
472
474
|
// src/host/config.ts
|
|
473
475
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -927,13 +929,13 @@ var MailDatabase = class {
|
|
|
927
929
|
size: Number(item.size ?? 0)
|
|
928
930
|
}));
|
|
929
931
|
const max = this.options.maxBodyChars ?? 4e5;
|
|
930
|
-
const
|
|
932
|
+
const bodyText2 = String(row.body_text ?? "");
|
|
931
933
|
const bodyHtml = String(row.body_html ?? "");
|
|
932
934
|
return {
|
|
933
935
|
...rowToSummary(row),
|
|
934
936
|
cc: parseAddressList(String(row.cc ?? "")),
|
|
935
937
|
replyTo: [],
|
|
936
|
-
text: truncate(
|
|
938
|
+
text: truncate(bodyText2, max),
|
|
937
939
|
html: bodyHtml === "" ? null : truncate(bodyHtml, max * 2),
|
|
938
940
|
attachments,
|
|
939
941
|
references: []
|
|
@@ -969,6 +971,18 @@ var MailDatabase = class {
|
|
|
969
971
|
return false;
|
|
970
972
|
}
|
|
971
973
|
}
|
|
974
|
+
/** Recent sent messages, for learning the user's writing style. */
|
|
975
|
+
sentMessages(limit = 100) {
|
|
976
|
+
const rows = this.open().prepare(
|
|
977
|
+
`SELECT subject, body_text FROM emails
|
|
978
|
+
WHERE lower(mailbox) LIKE '%sent%' OR mailbox LIKE '%\u5DF2\u53D1\u9001%'
|
|
979
|
+
ORDER BY date DESC, id DESC LIMIT ?`
|
|
980
|
+
).all(limit);
|
|
981
|
+
return rows.map((row) => ({
|
|
982
|
+
subject: String(row.subject ?? ""),
|
|
983
|
+
text: String(row.body_text ?? "")
|
|
984
|
+
}));
|
|
985
|
+
}
|
|
972
986
|
/** Latest backup runs, for the settings page. */
|
|
973
987
|
backupSummary() {
|
|
974
988
|
try {
|
|
@@ -1425,6 +1439,9 @@ var Router = class {
|
|
|
1425
1439
|
patch(path, handler) {
|
|
1426
1440
|
this.add("PATCH", path, handler);
|
|
1427
1441
|
}
|
|
1442
|
+
put(path, handler) {
|
|
1443
|
+
this.add("PUT", path, handler);
|
|
1444
|
+
}
|
|
1428
1445
|
delete(path, handler) {
|
|
1429
1446
|
this.add("DELETE", path, handler);
|
|
1430
1447
|
}
|
|
@@ -1450,6 +1467,324 @@ var Router = class {
|
|
|
1450
1467
|
}
|
|
1451
1468
|
};
|
|
1452
1469
|
|
|
1470
|
+
// src/host/ai.ts
|
|
1471
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1472
|
+
import { dirname, join as join3 } from "node:path";
|
|
1473
|
+
var TIMEOUT_MS = 9e4;
|
|
1474
|
+
var MAX_PROMPT_CHARS = 4e4;
|
|
1475
|
+
var SUMMARY_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u7406\uFF0C\u670D\u52A1\u4E8E\u4E2D\u6587\u7528\u6237\u3002\u8BF7\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u90AE\u4EF6\uFF0C\u4E25\u683C\u6309\u4E0B\u9762\u56DB\u6BB5\u8F93\u51FA\uFF0C\u4F7F\u7528\u7ED9\u5B9A\u7684\u5C0F\u6807\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9\uFF1A
|
|
1476
|
+
|
|
1477
|
+
\u7ED3\u8BBA
|
|
1478
|
+
\uFF081-2 \u53E5\u8BDD\u8BF4\u660E\u8FD9\u5C01\u90AE\u4EF6\u662F\u4EC0\u4E48\u4E8B\uFF09
|
|
1479
|
+
|
|
1480
|
+
\u9700\u8981\u4F60\u505A\u4EC0\u4E48
|
|
1481
|
+
\uFF08\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u9700\u64CD\u4F5C\u300D\uFF1B\u6709\u5C31\u5217 1-3 \u6761\uFF0C\u9879\u76EE\u7B26\u53F7\uFF09
|
|
1482
|
+
|
|
1483
|
+
\u5173\u952E\u4FE1\u606F
|
|
1484
|
+
\uFF08\u65F6\u95F4\u3001\u91D1\u989D\u3001\u4EBA\u540D\u3001\u5355\u53F7\u3001\u5730\u5740\u7B49\uFF0C\u9879\u76EE\u7B26\u53F7\uFF1B\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u300D\uFF09
|
|
1485
|
+
|
|
1486
|
+
\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9
|
|
1487
|
+
\uFF081-3 \u6761\u4E2D\u6587\u8981\u70B9\uFF09
|
|
1488
|
+
|
|
1489
|
+
\u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\uFF0C\u4E0D\u8981\u7F16\u9020\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B\u3002`;
|
|
1490
|
+
var REPLY_SYSTEM = (language, style) => `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u539F\u59CB\u90AE\u4EF6\u548C\u7528\u6237\u7684\u8981\u6C42\uFF0C\u5199\u4E00\u5C01\u53EF\u4EE5\u76F4\u63A5\u53D1\u9001\u7684\u56DE\u590D\u6B63\u6587\u3002
|
|
1491
|
+
|
|
1492
|
+
\u8981\u6C42\uFF1A
|
|
1493
|
+
- \u8BED\u8A00\uFF1A${language === "en" ? "\u82F1\u6587" : "\u7B80\u4F53\u4E2D\u6587"}
|
|
1494
|
+
- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48
|
|
1495
|
+
- \u8BED\u6C14\u4E13\u4E1A\u3001\u7B80\u6D01\u3001\u793C\u8C8C\uFF1B\u4E2D\u6587\u7528\u300C\u60A8\u300D\uFF0C\u82F1\u6587\u907F\u514D\u8FC7\u5EA6\u6B63\u5F0F
|
|
1496
|
+
- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB\uFF08\u79F0\u547C\u3001\u6B63\u6587\u3001\u7ED3\u5C3E\uFF09\uFF0C\u4E0D\u8981\u5F15\u53F7\u5305\u88F9
|
|
1497
|
+
${style === "" ? "" : `
|
|
1498
|
+
\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A
|
|
1499
|
+
${style}`}`;
|
|
1500
|
+
var TRANSLATE_SYSTEM = (target) => `\u4F60\u662F\u90AE\u4EF6\u7FFB\u8BD1\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7FFB\u8BD1\u6210${target === "en" ? "\u82F1\u6587" : "\u7B80\u4F53\u4E2D\u6587"}\u3002
|
|
1501
|
+
|
|
1502
|
+
\u89C4\u5219\uFF1A
|
|
1503
|
+
- \u53EA\u8F93\u51FA\u8BD1\u6587\uFF0C\u4E0D\u6DFB\u52A0\u89E3\u91CA
|
|
1504
|
+
- \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784
|
|
1505
|
+
- \u5982\u679C\u6B63\u6587\u662F HTML\uFF0C\u4FDD\u7559\u539F\u6709\u6807\u7B7E\u3001\u94FE\u63A5\u548C\u56FE\u7247\uFF0C\u53EA\u7FFB\u8BD1\u53EF\u89C1\u6587\u5B57
|
|
1506
|
+
- \u4E13\u6709\u540D\u8BCD\u3001\u90AE\u7BB1\u5730\u5740\u3001\u94FE\u63A5\u3001\u5355\u53F7\u4FDD\u6301\u4E0D\u53D8`;
|
|
1507
|
+
var ASK_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u624B\u3002\u57FA\u4E8E\u7528\u6237\u63D0\u4F9B\u7684\u90AE\u4EF6\u548C\u5DF2\u6709\u7684 AI \u8BB0\u5F55\u56DE\u7B54\u95EE\u9898\u3002
|
|
1508
|
+
|
|
1509
|
+
\u8981\u6C42\uFF1A
|
|
1510
|
+
- \u7528\u7B80\u4F53\u4E2D\u6587\u56DE\u7B54\uFF0C\u7B80\u6D01\u76F4\u63A5
|
|
1511
|
+
- \u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\u548C\u5DF2\u6709\u8BB0\u5F55\uFF0C\u4E0D\u786E\u5B9A\u5C31\u8BF4\u660E\u300C\u90AE\u4EF6\u91CC\u6CA1\u6709\u63D0\u5230\u300D
|
|
1512
|
+
- \u4E0D\u8981\u590D\u8FF0\u95EE\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B`;
|
|
1513
|
+
var COMPOSE_SYSTEM = (mode, language, style) => {
|
|
1514
|
+
const shared = `- \u8BED\u8A00\uFF1A${language === "en" ? "\u82F1\u6587" : "\u7B80\u4F53\u4E2D\u6587"}
|
|
1515
|
+
- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48
|
|
1516
|
+
- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB`;
|
|
1517
|
+
const styleLine = style === "" ? "" : `
|
|
1518
|
+
\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A
|
|
1519
|
+
${style}`;
|
|
1520
|
+
if (mode === "polish") {
|
|
1521
|
+
return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6DA6\u8272\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\uFF0C\u4FDD\u6301\u539F\u610F\u548C\u4E8B\u5B9E\uFF0C\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u81EA\u7136\u3002
|
|
1522
|
+
|
|
1523
|
+
\u8981\u6C42\uFF1A
|
|
1524
|
+
${shared}${styleLine}`;
|
|
1525
|
+
}
|
|
1526
|
+
if (mode === "shorten") {
|
|
1527
|
+
return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7F29\u77ED\u5230\u7EA6\u4E00\u534A\uFF0C\u4FDD\u7559\u5173\u952E\u4FE1\u606F\u548C\u4E8B\u5B9E\u3002
|
|
1528
|
+
|
|
1529
|
+
\u8981\u6C42\uFF1A
|
|
1530
|
+
${shared}${styleLine}`;
|
|
1531
|
+
}
|
|
1532
|
+
return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u7528\u6237\u7ED9\u51FA\u7684\u8981\u70B9\u5199\u4E00\u5C01\u90AE\u4EF6\u6B63\u6587\u3002
|
|
1533
|
+
|
|
1534
|
+
\u8981\u6C42\uFF1A
|
|
1535
|
+
${shared}${styleLine}`;
|
|
1536
|
+
};
|
|
1537
|
+
var STYLE_SYSTEM = `\u4F60\u662F\u5199\u4F5C\u98CE\u683C\u5206\u6790\u5E08\u3002\u9605\u8BFB\u7528\u6237\u6700\u8FD1\u53D1\u51FA\u7684\u90AE\u4EF6\uFF0C\u7528\u7B80\u4F53\u4E2D\u6587\u5199\u4E00\u6BB5 150-300 \u5B57\u7684\u5199\u4F5C\u98CE\u683C\u6863\u6848\uFF0C\u8986\u76D6\uFF1A
|
|
1538
|
+
\u5E38\u7528\u79F0\u547C\u3001\u5F00\u5934\u65B9\u5F0F\u3001\u7ED3\u5C3E\u4E0E\u7F72\u540D\u3001\u8BED\u6C14\uFF08\u6B63\u5F0F/\u4EB2\u5207/\u76F4\u63A5\uFF09\u3001\u53E5\u5B50\u957F\u77ED\u3001\u5E38\u7528\u8868\u8FBE\u6216\u53E3\u5934\u7985\u3002
|
|
1539
|
+
\u53EA\u8F93\u51FA\u6863\u6848\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u6807\u9898\u3001\u5217\u8868\u7B26\u53F7\u6216\u5206\u6790\u8FC7\u7A0B\u3002`;
|
|
1540
|
+
function stylePath(root) {
|
|
1541
|
+
return join3(root, "style.json");
|
|
1542
|
+
}
|
|
1543
|
+
function readStyle(root) {
|
|
1544
|
+
try {
|
|
1545
|
+
const parsed = JSON.parse(readFileSync3(stylePath(root), "utf8"));
|
|
1546
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
1547
|
+
const value = parsed;
|
|
1548
|
+
if (typeof value.profile !== "string" || value.profile.trim() === "") return null;
|
|
1549
|
+
return {
|
|
1550
|
+
profile: value.profile,
|
|
1551
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : "",
|
|
1552
|
+
sampleCount: typeof value.sampleCount === "number" ? value.sampleCount : 0
|
|
1553
|
+
};
|
|
1554
|
+
} catch {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function writeStyle(root, profile) {
|
|
1559
|
+
const file = stylePath(root);
|
|
1560
|
+
mkdirSync3(dirname(file), { recursive: true });
|
|
1561
|
+
const temp = `${file}.${String(process.pid)}.tmp`;
|
|
1562
|
+
writeFileSync3(temp, `${JSON.stringify(profile, null, 2)}
|
|
1563
|
+
`, { mode: 384 });
|
|
1564
|
+
renameSync2(temp, file);
|
|
1565
|
+
}
|
|
1566
|
+
function clip(value, max = MAX_PROMPT_CHARS) {
|
|
1567
|
+
return value.length <= max ? value : `${value.slice(0, max - 2e3)}
|
|
1568
|
+
\u2026\uFF08\u4E2D\u95F4\u7701\u7565\uFF09\u2026
|
|
1569
|
+
${value.slice(-2e3)}`;
|
|
1570
|
+
}
|
|
1571
|
+
function bodyText(message) {
|
|
1572
|
+
if (message.text.trim() !== "") return message.text;
|
|
1573
|
+
return (message.html ?? "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").trim();
|
|
1574
|
+
}
|
|
1575
|
+
function contextOf(message) {
|
|
1576
|
+
const lines = [
|
|
1577
|
+
`\u4E3B\u9898: ${message.subject}`,
|
|
1578
|
+
`\u53D1\u4EF6\u4EBA: ${message.from.name !== "" ? `${message.from.name} <${message.from.address}>` : message.from.address}`,
|
|
1579
|
+
`\u6536\u4EF6\u4EBA: ${message.to.map((item) => item.name !== "" ? `${item.name} <${item.address}>` : item.address).join(", ")}`,
|
|
1580
|
+
...message.cc.length === 0 ? [] : [`\u6284\u9001: ${message.cc.map((item) => item.address).join(", ")}`],
|
|
1581
|
+
`\u65F6\u95F4: ${message.date}`,
|
|
1582
|
+
"",
|
|
1583
|
+
clip(bodyText(message))
|
|
1584
|
+
];
|
|
1585
|
+
return lines.join("\n");
|
|
1586
|
+
}
|
|
1587
|
+
var AiService = class {
|
|
1588
|
+
deps;
|
|
1589
|
+
constructor(deps) {
|
|
1590
|
+
this.deps = deps;
|
|
1591
|
+
}
|
|
1592
|
+
llm() {
|
|
1593
|
+
try {
|
|
1594
|
+
return this.deps.llm();
|
|
1595
|
+
} catch {
|
|
1596
|
+
return void 0;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
status(account) {
|
|
1600
|
+
const config = this.deps.config();
|
|
1601
|
+
const llm = this.llm();
|
|
1602
|
+
const style = account === null ? null : this.style(account);
|
|
1603
|
+
return {
|
|
1604
|
+
available: llm !== void 0,
|
|
1605
|
+
enabled: config.ai.enabled,
|
|
1606
|
+
provider: config.ai.provider,
|
|
1607
|
+
model: config.ai.model,
|
|
1608
|
+
styleUpdatedAt: style?.updatedAt ?? "",
|
|
1609
|
+
styleSampleCount: style?.sampleCount ?? 0,
|
|
1610
|
+
...llm === void 0 ? { error: "\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1" } : {}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
style(account) {
|
|
1614
|
+
return readStyle(this.deps.accountRoot(account));
|
|
1615
|
+
}
|
|
1616
|
+
saveStyle(account, profile) {
|
|
1617
|
+
const text2 = profile.trim();
|
|
1618
|
+
if (text2 === "") throw new Error("\u5199\u4F5C\u98CE\u683C\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1619
|
+
if (text2.length > 8e3) throw new Error("\u5199\u4F5C\u98CE\u683C\u8FC7\u957F\uFF08\u6700\u591A 8000 \u5B57\uFF09");
|
|
1620
|
+
const previous = this.style(account);
|
|
1621
|
+
const next = {
|
|
1622
|
+
profile: text2,
|
|
1623
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1624
|
+
sampleCount: previous?.sampleCount ?? 0
|
|
1625
|
+
};
|
|
1626
|
+
writeStyle(this.deps.accountRoot(account), next);
|
|
1627
|
+
return next;
|
|
1628
|
+
}
|
|
1629
|
+
requireReady() {
|
|
1630
|
+
const config = this.deps.config();
|
|
1631
|
+
if (!config.ai.enabled) throw new Error("AI \u529F\u80FD\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5230\u8BBE\u7F6E\u91CC\u5F00\u542F");
|
|
1632
|
+
const llm = this.llm();
|
|
1633
|
+
if (llm === void 0) throw new Error("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1");
|
|
1634
|
+
if (config.ai.provider === "" || config.ai.model === "") throw new Error("\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 AI \u6A21\u578B");
|
|
1635
|
+
return { provider: config.ai.provider, model: config.ai.model };
|
|
1636
|
+
}
|
|
1637
|
+
async complete(system, user, signal) {
|
|
1638
|
+
const { provider, model } = this.requireReady();
|
|
1639
|
+
const llm = this.llm();
|
|
1640
|
+
if (llm === void 0) throw new Error("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1");
|
|
1641
|
+
const controller = new AbortController();
|
|
1642
|
+
const timer = setTimeout(() => {
|
|
1643
|
+
controller.abort();
|
|
1644
|
+
}, TIMEOUT_MS);
|
|
1645
|
+
const onAbort = () => {
|
|
1646
|
+
controller.abort();
|
|
1647
|
+
};
|
|
1648
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1649
|
+
let text2 = "";
|
|
1650
|
+
try {
|
|
1651
|
+
const stream = llm.stream({
|
|
1652
|
+
provider,
|
|
1653
|
+
model,
|
|
1654
|
+
messages: [
|
|
1655
|
+
{ role: "system", content: [{ type: "text", text: system }] },
|
|
1656
|
+
{ role: "user", content: [{ type: "text", text: user }] }
|
|
1657
|
+
],
|
|
1658
|
+
signal: controller.signal
|
|
1659
|
+
});
|
|
1660
|
+
for await (const chunk of stream) {
|
|
1661
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") {
|
|
1662
|
+
text2 += chunk.text;
|
|
1663
|
+
} else if (chunk.type === "finish") {
|
|
1664
|
+
const reason = chunk.reason;
|
|
1665
|
+
if (reason?.kind === "error") {
|
|
1666
|
+
throw new Error(reason.failure?.message ?? reason.failure?.code ?? "\u6A21\u578B\u8C03\u7528\u5931\u8D25");
|
|
1667
|
+
}
|
|
1668
|
+
if (reason?.kind === "aborted") throw new Error("\u6A21\u578B\u8C03\u7528\u5DF2\u53D6\u6D88");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
} finally {
|
|
1672
|
+
clearTimeout(timer);
|
|
1673
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1674
|
+
}
|
|
1675
|
+
const trimmed = text2.trim();
|
|
1676
|
+
if (trimmed === "") throw new Error("\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u5185\u5BB9");
|
|
1677
|
+
return trimmed;
|
|
1678
|
+
}
|
|
1679
|
+
async summarize(message, signal) {
|
|
1680
|
+
return this.complete(SUMMARY_SYSTEM, contextOf(message), signal);
|
|
1681
|
+
}
|
|
1682
|
+
async reply(account, message, options = {}, signal) {
|
|
1683
|
+
const style = this.style(account)?.profile ?? "";
|
|
1684
|
+
const instruction = (options.instruction ?? "").trim();
|
|
1685
|
+
const user = [
|
|
1686
|
+
"\u539F\u59CB\u90AE\u4EF6\uFF1A",
|
|
1687
|
+
contextOf(message),
|
|
1688
|
+
"",
|
|
1689
|
+
instruction === "" ? "\u7528\u6237\u6CA1\u6709\u989D\u5916\u8981\u6C42\uFF0C\u8BF7\u5199\u4E00\u5C01\u5F97\u4F53\u7684\u56DE\u590D\u3002" : `\u7528\u6237\u7684\u8981\u6C42\uFF1A${instruction}`
|
|
1690
|
+
].join("\n");
|
|
1691
|
+
return this.complete(REPLY_SYSTEM(options.language ?? "zh", style), user, signal);
|
|
1692
|
+
}
|
|
1693
|
+
async translate(html, target, signal) {
|
|
1694
|
+
const text2 = html.trim();
|
|
1695
|
+
if (text2 === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1");
|
|
1696
|
+
return this.complete(TRANSLATE_SYSTEM(target), clip(text2, 2e4), signal);
|
|
1697
|
+
}
|
|
1698
|
+
/** Free-form question about a mail, with up to three earlier AI outputs as context. */
|
|
1699
|
+
async ask(message, question, history, signal) {
|
|
1700
|
+
const prompt = (question ?? "").trim();
|
|
1701
|
+
if (prompt === "") throw new Error("\u95EE\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1702
|
+
const earlier = history.filter((item) => item.trim() !== "").slice(-3).map((item, index) => `\u3010\u5DF2\u6709\u8BB0\u5F55 ${String(index + 1)}\u3011
|
|
1703
|
+
${clip(item, 4e3)}`).join("\n\n");
|
|
1704
|
+
const user = [
|
|
1705
|
+
"\u90AE\u4EF6\uFF1A",
|
|
1706
|
+
contextOf(message),
|
|
1707
|
+
...earlier === "" ? [] : ["", "\u5DF2\u6709 AI \u8BB0\u5F55\uFF1A", earlier],
|
|
1708
|
+
"",
|
|
1709
|
+
`\u7528\u6237\u7684\u95EE\u9898\uFF1A${prompt}`
|
|
1710
|
+
].join("\n");
|
|
1711
|
+
return this.complete(ASK_SYSTEM, user, signal);
|
|
1712
|
+
}
|
|
1713
|
+
/** Compose-body assistance: draft from points, polish, or shorten. */
|
|
1714
|
+
async compose(account, options, signal) {
|
|
1715
|
+
const style = this.style(account)?.profile ?? "";
|
|
1716
|
+
const language = options.language ?? "zh";
|
|
1717
|
+
const body = options.html.trim();
|
|
1718
|
+
if (options.mode !== "draft" && body === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A");
|
|
1719
|
+
const source = options.mode === "draft" ? `\u7528\u6237\u8981\u70B9\uFF1A${(options.instruction ?? "").trim() === "" ? "\u5199\u4E00\u5C01\u7B80\u77ED\u7684\u90AE\u4EF6" : (options.instruction ?? "").trim()}` : `\u9700\u8981\u5904\u7406\u7684\u6B63\u6587\uFF1A
|
|
1720
|
+
${clip(htmlToPlain(body), 12e3)}`;
|
|
1721
|
+
return this.complete(COMPOSE_SYSTEM(options.mode, language, style), source, signal);
|
|
1722
|
+
}
|
|
1723
|
+
async learnStyle(account, samples, signal) {
|
|
1724
|
+
if (samples.length === 0) throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u5DF2\u53D1\u9001\u90AE\u4EF6\uFF0C\u5148\u5907\u4EFD\u4E00\u6B21\u90AE\u7BB1");
|
|
1725
|
+
const digest = samples.slice(0, 100).map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}
|
|
1726
|
+
${clip(sample.text, 800)}`).join("\n\n");
|
|
1727
|
+
const profile = await this.complete(STYLE_SYSTEM, clip(digest, 35e3), signal);
|
|
1728
|
+
const next = {
|
|
1729
|
+
profile,
|
|
1730
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1731
|
+
sampleCount: samples.length
|
|
1732
|
+
};
|
|
1733
|
+
writeStyle(this.deps.accountRoot(account), next);
|
|
1734
|
+
return next;
|
|
1735
|
+
}
|
|
1736
|
+
async models() {
|
|
1737
|
+
const llm = this.llm();
|
|
1738
|
+
if (llm === void 0) return [];
|
|
1739
|
+
const providers = llm.listProviders();
|
|
1740
|
+
const catalog = [];
|
|
1741
|
+
for (const provider of providers) {
|
|
1742
|
+
try {
|
|
1743
|
+
const models = await llm.listModels(provider.id);
|
|
1744
|
+
catalog.push({
|
|
1745
|
+
provider: provider.id,
|
|
1746
|
+
name: provider.name ?? provider.id,
|
|
1747
|
+
models: models.map((model) => ({ id: model.id, name: model.name ?? model.id }))
|
|
1748
|
+
});
|
|
1749
|
+
} catch {
|
|
1750
|
+
catalog.push({ provider: provider.id, name: provider.name ?? provider.id, models: [] });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return catalog;
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
function htmlToPlain(html) {
|
|
1757
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
1758
|
+
}
|
|
1759
|
+
function summaryBlocks(text2) {
|
|
1760
|
+
const blocks = [];
|
|
1761
|
+
let current = { title: "", lines: [] };
|
|
1762
|
+
const push = () => {
|
|
1763
|
+
const body = current.lines.join("\n").trim();
|
|
1764
|
+
if (current.title !== "" || body !== "") blocks.push({ title: current.title, body });
|
|
1765
|
+
current = { title: "", lines: [] };
|
|
1766
|
+
};
|
|
1767
|
+
for (const line2 of text2.split("\n")) {
|
|
1768
|
+
const heading = /^#{1,4}\s+(.*)$/.exec(line2) ?? /^(结论|需要你做什么|关键信息|建议回复要点)\s*$/.exec(line2);
|
|
1769
|
+
if (heading !== null) {
|
|
1770
|
+
push();
|
|
1771
|
+
current.title = heading[1] ?? "";
|
|
1772
|
+
} else {
|
|
1773
|
+
current.lines.push(line2);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
push();
|
|
1777
|
+
return blocks;
|
|
1778
|
+
}
|
|
1779
|
+
function textToHtml(text2) {
|
|
1780
|
+
return text2.split(/\n{2,}/).map(
|
|
1781
|
+
(paragraph) => `<p>${paragraph.trim().replaceAll("&", "&").replaceAll("<", "<").replaceAll("\n", "<br>")}</p>`
|
|
1782
|
+
).join("");
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/host/version.ts
|
|
1786
|
+
var VERSION = "0.4.0";
|
|
1787
|
+
|
|
1453
1788
|
// src/host/api.ts
|
|
1454
1789
|
init_smtp();
|
|
1455
1790
|
|
|
@@ -1504,49 +1839,61 @@ function buildRouter(runtime) {
|
|
|
1504
1839
|
router.get("/health", () => ({ ok: true, version: VERSION }));
|
|
1505
1840
|
router.get("/state", () => appState(runtime));
|
|
1506
1841
|
router.get("/folders", async ({ url }) => {
|
|
1507
|
-
const { account } = accountFrom(runtime, url);
|
|
1508
1842
|
const source = sourceOf(url);
|
|
1843
|
+
const requested = url.searchParams.get("accountId");
|
|
1844
|
+
if (source === "server" && requested === "all") {
|
|
1845
|
+
throw new HttpError(400, "\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD");
|
|
1846
|
+
}
|
|
1847
|
+
const accounts = requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountFrom(runtime, url).account];
|
|
1509
1848
|
if (source === "server") {
|
|
1510
|
-
const provider = runtime.provider(
|
|
1849
|
+
const provider = runtime.provider(accounts[0]);
|
|
1511
1850
|
const boxes = await provider.mailboxes();
|
|
1512
1851
|
return {
|
|
1513
1852
|
items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages }))
|
|
1514
1853
|
};
|
|
1515
1854
|
}
|
|
1516
|
-
|
|
1855
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1856
|
+
let drafts = 0;
|
|
1857
|
+
for (const account of accounts) {
|
|
1858
|
+
try {
|
|
1859
|
+
for (const box of runtime.database(account).mailboxes()) {
|
|
1860
|
+
counts.set(box.name, (counts.get(box.name) ?? 0) + box.count);
|
|
1861
|
+
}
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
try {
|
|
1865
|
+
drafts += runtime.drafts(account).list().length;
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const items = [...counts.entries()].map(([name2, count]) => ({ name: name2, count })).sort((left, right) => right.count - left.count);
|
|
1870
|
+
items.unshift({ name: "__drafts", count: drafts });
|
|
1871
|
+
return { items };
|
|
1517
1872
|
});
|
|
1518
1873
|
router.get("/messages", async ({ url }) => {
|
|
1519
|
-
const { account } = accountFrom(runtime, url);
|
|
1520
1874
|
const source = sourceOf(url);
|
|
1875
|
+
const requested = url.searchParams.get("accountId");
|
|
1521
1876
|
const mailbox = url.searchParams.get("mailbox") ?? "all";
|
|
1522
1877
|
const query = url.searchParams.get("query") ?? "";
|
|
1523
1878
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1524
1879
|
const limit = Number(url.searchParams.get("limit") ?? runtime.current.render.listLimit);
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
return {
|
|
1880
|
+
if (requested === "all") {
|
|
1881
|
+
if (source === "server") throw new HttpError(400, "\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD");
|
|
1882
|
+
return listAllAccounts(runtime, { mailbox, query, offset, limit });
|
|
1528
1883
|
}
|
|
1529
|
-
const
|
|
1530
|
-
if (
|
|
1531
|
-
const
|
|
1532
|
-
return { ...
|
|
1884
|
+
const { account } = accountFrom(runtime, url);
|
|
1885
|
+
if (source === "server") {
|
|
1886
|
+
const items = await listServer(runtime, account, mailbox, query, limit, offset);
|
|
1887
|
+
return { ...items, offset, limit };
|
|
1533
1888
|
}
|
|
1534
|
-
const page =
|
|
1889
|
+
const page = runtime.database(account).list({ mailbox: mailbox === "" ? "all" : mailbox, query, offset, limit });
|
|
1535
1890
|
return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) };
|
|
1536
1891
|
});
|
|
1537
1892
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
1538
1893
|
const { account } = accountFrom(runtime, url);
|
|
1539
1894
|
const source = sourceOf(url);
|
|
1540
1895
|
const mailbox = url.searchParams.get("mailbox") ?? "INBOX";
|
|
1541
|
-
|
|
1542
|
-
if (source === "server") {
|
|
1543
|
-
const provider = runtime.provider(account);
|
|
1544
|
-
const message = await provider.read(mailbox, Number(id));
|
|
1545
|
-
return serverDetail(runtime, account, message, mailbox);
|
|
1546
|
-
}
|
|
1547
|
-
const detail = runtime.database(account).get(Number(id));
|
|
1548
|
-
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
1549
|
-
return localDetail(runtime, account, detail);
|
|
1896
|
+
return loadDetail(runtime, account, source, params.id ?? "", mailbox);
|
|
1550
1897
|
});
|
|
1551
1898
|
router.post("/messages/:id/read", async ({ url, params, body }) => {
|
|
1552
1899
|
const { account } = accountFrom(runtime, url);
|
|
@@ -1617,17 +1964,24 @@ function buildRouter(runtime) {
|
|
|
1617
1964
|
return result;
|
|
1618
1965
|
});
|
|
1619
1966
|
router.get("/unread", async ({ url }) => {
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1967
|
+
const requested = url.searchParams.get("accountId");
|
|
1968
|
+
const accounts = requested === null || requested === "" || requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountById(runtime, requested)];
|
|
1969
|
+
let server = 0;
|
|
1970
|
+
let answered = 0;
|
|
1971
|
+
await Promise.all(
|
|
1972
|
+
accounts.map(async (account) => {
|
|
1973
|
+
if (!runtime.hasCredentials(account)) return;
|
|
1974
|
+
try {
|
|
1975
|
+
const result = await runtime.provider(account).unseen();
|
|
1976
|
+
server += result.count;
|
|
1977
|
+
answered += 1;
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
})
|
|
1981
|
+
);
|
|
1982
|
+
if (answered > 0) return { unread: server, source: "server" };
|
|
1983
|
+
const local = accounts.reduce((sum, account) => sum + runtime.readState(account).count(), 0);
|
|
1984
|
+
return { unread: local, source: "local" };
|
|
1631
1985
|
});
|
|
1632
1986
|
router.get("/accounts", () => ({ accounts: runtime.current.accounts }));
|
|
1633
1987
|
router.post("/accounts", async ({ body }) => {
|
|
@@ -1727,6 +2081,186 @@ function buildRouter(runtime) {
|
|
|
1727
2081
|
}
|
|
1728
2082
|
return { ok: true, imap, smtp };
|
|
1729
2083
|
});
|
|
2084
|
+
router.get("/ai/thread", ({ url }) => {
|
|
2085
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2086
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2087
|
+
if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2088
|
+
return { thread: runtime.aiThreads(account).get(key) };
|
|
2089
|
+
});
|
|
2090
|
+
router.delete("/ai/thread", ({ url }) => {
|
|
2091
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2092
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2093
|
+
if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2094
|
+
return { ok: runtime.aiThreads(account).clear(key) };
|
|
2095
|
+
});
|
|
2096
|
+
router.delete("/ai/cards", ({ url }) => {
|
|
2097
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2098
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2099
|
+
const cardId = (url.searchParams.get("cardId") ?? "").trim();
|
|
2100
|
+
if (key === "" || cardId === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id");
|
|
2101
|
+
return { thread: runtime.aiThreads(account).removeCard(key, cardId) };
|
|
2102
|
+
});
|
|
2103
|
+
router.post("/ai/cards", async ({ body }) => {
|
|
2104
|
+
const payload = await body();
|
|
2105
|
+
const account = accountFromPayload(runtime, payload);
|
|
2106
|
+
const key = String(payload.key ?? "").trim();
|
|
2107
|
+
if (key === "" || key.length > 200) throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2108
|
+
const kind = String(payload.kind ?? "");
|
|
2109
|
+
if (!["summary", "reply", "translate", "answer"].includes(kind)) {
|
|
2110
|
+
throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`);
|
|
2111
|
+
}
|
|
2112
|
+
const detail = await loadDetail(
|
|
2113
|
+
runtime,
|
|
2114
|
+
account,
|
|
2115
|
+
sourceOfPayload(payload),
|
|
2116
|
+
String(payload.id ?? ""),
|
|
2117
|
+
mailboxOfPayload(payload)
|
|
2118
|
+
);
|
|
2119
|
+
const config = runtime.current;
|
|
2120
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2121
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2122
|
+
const target = payload.target === "en" ? "en" : "zh";
|
|
2123
|
+
const ai = runtime.ai();
|
|
2124
|
+
try {
|
|
2125
|
+
let output = "";
|
|
2126
|
+
let html;
|
|
2127
|
+
let meta;
|
|
2128
|
+
if (kind === "summary") {
|
|
2129
|
+
output = await ai.summarize(detail);
|
|
2130
|
+
} else if (kind === "reply") {
|
|
2131
|
+
output = await ai.reply(account, detail, { instruction, language });
|
|
2132
|
+
html = textToHtml(output);
|
|
2133
|
+
meta = {
|
|
2134
|
+
to: detail.from.address,
|
|
2135
|
+
subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,
|
|
2136
|
+
inReplyTo: detail.messageId ?? "",
|
|
2137
|
+
references: [detail.references.join(" "), detail.messageId ?? ""].join(" ").trim(),
|
|
2138
|
+
language
|
|
2139
|
+
};
|
|
2140
|
+
} else if (kind === "translate") {
|
|
2141
|
+
const source = detail.html !== null && detail.html !== "" ? detail.html : detail.text;
|
|
2142
|
+
output = await ai.translate(source, target);
|
|
2143
|
+
html = /<[a-z][\s\S]*>/i.test(output) ? output : textToHtml(output);
|
|
2144
|
+
meta = { target };
|
|
2145
|
+
} else {
|
|
2146
|
+
const earlier = runtime.aiThreads(account).get(key)?.cards.filter((card2) => card2.kind === "answer").map((card2) => card2.output) ?? [];
|
|
2147
|
+
output = await ai.ask(detail, instruction, earlier);
|
|
2148
|
+
}
|
|
2149
|
+
const card = {
|
|
2150
|
+
id: randomUUID2(),
|
|
2151
|
+
kind,
|
|
2152
|
+
instruction,
|
|
2153
|
+
output,
|
|
2154
|
+
...html === void 0 ? {} : { html },
|
|
2155
|
+
...meta === void 0 ? {} : { meta },
|
|
2156
|
+
provider: config.ai.provider,
|
|
2157
|
+
model: config.ai.model,
|
|
2158
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2159
|
+
};
|
|
2160
|
+
const thread = runtime.aiThreads(account).appendCard({
|
|
2161
|
+
key,
|
|
2162
|
+
accountId: account.id,
|
|
2163
|
+
subject: detail.subject,
|
|
2164
|
+
messageId: detail.messageId ?? "",
|
|
2165
|
+
card
|
|
2166
|
+
});
|
|
2167
|
+
return { card, thread };
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
throw aiHttpError(error);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
router.post("/ai/compose", async ({ body }) => {
|
|
2173
|
+
const payload = await body();
|
|
2174
|
+
const account = accountFromPayload(runtime, payload);
|
|
2175
|
+
const mode = payload.mode === "polish" ? "polish" : payload.mode === "shorten" ? "shorten" : "draft";
|
|
2176
|
+
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2177
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2178
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2179
|
+
try {
|
|
2180
|
+
const text2 = await runtime.ai().compose(account, { mode, instruction, html, language });
|
|
2181
|
+
return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
throw aiHttpError(error);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
router.get("/ai/status", ({ url }) => {
|
|
2187
|
+
const requested = url.searchParams.get("accountId");
|
|
2188
|
+
const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
|
|
2189
|
+
return runtime.ai().status(account ?? null);
|
|
2190
|
+
});
|
|
2191
|
+
router.get("/ai/models", async () => ({ providers: await runtime.ai().models() }));
|
|
2192
|
+
router.post("/ai/summary", async ({ body }) => {
|
|
2193
|
+
const payload = await body();
|
|
2194
|
+
const account = accountFromPayload(runtime, payload);
|
|
2195
|
+
const detail = await loadDetail(
|
|
2196
|
+
runtime,
|
|
2197
|
+
account,
|
|
2198
|
+
sourceOfPayload(payload),
|
|
2199
|
+
String(payload.id ?? ""),
|
|
2200
|
+
mailboxOfPayload(payload)
|
|
2201
|
+
);
|
|
2202
|
+
try {
|
|
2203
|
+
const summary = await runtime.ai().summarize(detail);
|
|
2204
|
+
return { summary, blocks: summaryBlocks(summary) };
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
throw aiHttpError(error);
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
router.post("/ai/reply", async ({ body }) => {
|
|
2210
|
+
const payload = await body();
|
|
2211
|
+
const account = accountFromPayload(runtime, payload);
|
|
2212
|
+
const detail = await loadDetail(
|
|
2213
|
+
runtime,
|
|
2214
|
+
account,
|
|
2215
|
+
sourceOfPayload(payload),
|
|
2216
|
+
String(payload.id ?? ""),
|
|
2217
|
+
mailboxOfPayload(payload)
|
|
2218
|
+
);
|
|
2219
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2220
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2221
|
+
try {
|
|
2222
|
+
const text2 = await runtime.ai().reply(account, detail, { instruction, language });
|
|
2223
|
+
return { text: text2, html: textToHtml(text2) };
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
throw aiHttpError(error);
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
router.post("/ai/translate", async ({ body }) => {
|
|
2229
|
+
const payload = await body();
|
|
2230
|
+
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2231
|
+
const target = payload.target === "en" ? "en" : "zh";
|
|
2232
|
+
try {
|
|
2233
|
+
const text2 = await runtime.ai().translate(html, target);
|
|
2234
|
+
return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
throw aiHttpError(error);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
router.get("/ai/style", ({ url }) => {
|
|
2240
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2241
|
+
return { style: runtime.ai().style(account) };
|
|
2242
|
+
});
|
|
2243
|
+
router.put("/ai/style", async ({ body }) => {
|
|
2244
|
+
const payload = await body();
|
|
2245
|
+
const account = accountFromPayload(runtime, payload);
|
|
2246
|
+
try {
|
|
2247
|
+
const style = runtime.ai().saveStyle(account, String(payload.profile ?? ""));
|
|
2248
|
+
return { style };
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
throw aiHttpError(error);
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
router.post("/ai/style/learn", async ({ body }) => {
|
|
2254
|
+
const payload = await body();
|
|
2255
|
+
const account = accountFromPayload(runtime, payload);
|
|
2256
|
+
try {
|
|
2257
|
+
const samples = runtime.database(account).sentMessages(100);
|
|
2258
|
+
const style = await runtime.ai().learnStyle(account, samples);
|
|
2259
|
+
return { style };
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
throw aiHttpError(error);
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
1730
2264
|
router.get("/settings", () => settingsView(runtime.current));
|
|
1731
2265
|
router.patch("/settings", async ({ body }) => {
|
|
1732
2266
|
const payload = await body();
|
|
@@ -1755,7 +2289,60 @@ function buildRouter(runtime) {
|
|
|
1755
2289
|
});
|
|
1756
2290
|
return router;
|
|
1757
2291
|
}
|
|
1758
|
-
|
|
2292
|
+
async function loadDetail(runtime, account, source, id, mailbox) {
|
|
2293
|
+
if (source === "server") {
|
|
2294
|
+
const message = await runtime.provider(account).read(mailbox, Number(id));
|
|
2295
|
+
return serverDetail(runtime, account, message, mailbox);
|
|
2296
|
+
}
|
|
2297
|
+
const detail = runtime.database(account).get(Number(id));
|
|
2298
|
+
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
2299
|
+
return localDetail(runtime, account, detail);
|
|
2300
|
+
}
|
|
2301
|
+
function listAllAccounts(runtime, options) {
|
|
2302
|
+
const accounts = runtime.current.accounts.filter((item) => item.enabled !== false);
|
|
2303
|
+
const collected = [];
|
|
2304
|
+
let total = 0;
|
|
2305
|
+
for (const account of accounts) {
|
|
2306
|
+
try {
|
|
2307
|
+
const page = runtime.database(account).list({
|
|
2308
|
+
mailbox: options.mailbox === "" ? "all" : options.mailbox,
|
|
2309
|
+
query: options.query,
|
|
2310
|
+
offset: 0,
|
|
2311
|
+
limit: options.offset + options.limit
|
|
2312
|
+
});
|
|
2313
|
+
total += page.total;
|
|
2314
|
+
for (const item of page.items) collected.push(localSummary(runtime, account, item));
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
collected.sort((left, right) => right.date.localeCompare(left.date));
|
|
2319
|
+
return {
|
|
2320
|
+
items: collected.slice(options.offset, options.offset + options.limit),
|
|
2321
|
+
total,
|
|
2322
|
+
offset: options.offset,
|
|
2323
|
+
limit: options.limit
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
function accountFromPayload(runtime, payload) {
|
|
2327
|
+
const requested = typeof payload.accountId === "string" ? payload.accountId : "";
|
|
2328
|
+
if (requested !== "" && requested !== "all") {
|
|
2329
|
+
const found = runtime.current.accounts.find((item) => item.id === requested);
|
|
2330
|
+
if (found !== void 0) return found;
|
|
2331
|
+
}
|
|
2332
|
+
const active = runtime.active();
|
|
2333
|
+
if (active !== null) return active;
|
|
2334
|
+
throw new HttpError(400, "\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7");
|
|
2335
|
+
}
|
|
2336
|
+
function sourceOfPayload(payload) {
|
|
2337
|
+
return payload.source === "server" ? "server" : "local";
|
|
2338
|
+
}
|
|
2339
|
+
function mailboxOfPayload(payload) {
|
|
2340
|
+
return typeof payload.mailbox === "string" && payload.mailbox !== "" ? payload.mailbox : "INBOX";
|
|
2341
|
+
}
|
|
2342
|
+
function aiHttpError(error) {
|
|
2343
|
+
if (error instanceof HttpError) return error;
|
|
2344
|
+
return new HttpError(400, error instanceof Error ? error.message : String(error));
|
|
2345
|
+
}
|
|
1759
2346
|
function accountById(runtime, id) {
|
|
1760
2347
|
const account = runtime.current.accounts.find((item) => item.id === id);
|
|
1761
2348
|
if (account === void 0) throw new HttpError(404, "\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
@@ -1885,9 +2472,9 @@ function serverDetail(runtime, account, message, mailbox) {
|
|
|
1885
2472
|
references: message.references
|
|
1886
2473
|
};
|
|
1887
2474
|
}
|
|
1888
|
-
async function listServer(runtime, account, mailbox, query, limit) {
|
|
2475
|
+
async function listServer(runtime, account, mailbox, query, limit, offset = 0) {
|
|
1889
2476
|
const provider = runtime.provider(account);
|
|
1890
|
-
const result = await provider.list(mailbox === "all" ? "INBOX" : mailbox, Math.min(200, limit * 2));
|
|
2477
|
+
const result = await provider.list(mailbox === "all" ? "INBOX" : mailbox, Math.min(200, limit * 2), offset);
|
|
1891
2478
|
let database = null;
|
|
1892
2479
|
try {
|
|
1893
2480
|
database = runtime.database(account);
|
|
@@ -1952,7 +2539,7 @@ async function sendFromPayload(runtime, account, payload) {
|
|
|
1952
2539
|
if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, "\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB");
|
|
1953
2540
|
const { sendMail: sendMail2 } = await Promise.resolve().then(() => (init_smtp(), smtp_exports));
|
|
1954
2541
|
const signature = runtime.current.signature;
|
|
1955
|
-
const
|
|
2542
|
+
const bodyText2 = signature === "" ? text2 : `${text2}
|
|
1956
2543
|
|
|
1957
2544
|
--
|
|
1958
2545
|
${signature}`;
|
|
@@ -1965,7 +2552,7 @@ ${signature}`;
|
|
|
1965
2552
|
to,
|
|
1966
2553
|
cc: stringOf(payload.cc),
|
|
1967
2554
|
subject,
|
|
1968
|
-
text:
|
|
2555
|
+
text: bodyText2,
|
|
1969
2556
|
html: bodyHtml,
|
|
1970
2557
|
...typeof payload.inReplyTo === "string" ? { inReplyTo: payload.inReplyTo } : {},
|
|
1971
2558
|
...typeof payload.references === "string" ? { references: payload.references } : {},
|
|
@@ -1977,7 +2564,7 @@ ${signature}`;
|
|
|
1977
2564
|
|
|
1978
2565
|
// src/host/runtime.ts
|
|
1979
2566
|
import { statSync } from "node:fs";
|
|
1980
|
-
import { join as
|
|
2567
|
+
import { join as join5 } from "node:path";
|
|
1981
2568
|
|
|
1982
2569
|
// src/host/secrets.ts
|
|
1983
2570
|
import { spawnSync } from "node:child_process";
|
|
@@ -2156,10 +2743,150 @@ var CredentialManager = class {
|
|
|
2156
2743
|
}
|
|
2157
2744
|
};
|
|
2158
2745
|
|
|
2746
|
+
// src/host/ai-store.ts
|
|
2747
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2748
|
+
import { dirname as dirname2 } from "node:path";
|
|
2749
|
+
var MAX_THREADS = 50;
|
|
2750
|
+
var MAX_CARDS_PER_THREAD = 50;
|
|
2751
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
2752
|
+
var AiThreadStore = class {
|
|
2753
|
+
file;
|
|
2754
|
+
cache = null;
|
|
2755
|
+
constructor(file) {
|
|
2756
|
+
this.file = file;
|
|
2757
|
+
}
|
|
2758
|
+
load() {
|
|
2759
|
+
if (this.cache !== null) return this.cache;
|
|
2760
|
+
let parsed = null;
|
|
2761
|
+
try {
|
|
2762
|
+
if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
|
|
2763
|
+
} catch {
|
|
2764
|
+
parsed = null;
|
|
2765
|
+
}
|
|
2766
|
+
const threads = parsed !== null && typeof parsed === "object" && parsed.threads !== void 0 ? parsed.threads : {};
|
|
2767
|
+
this.cache = { version: 1, threads: sanitize(threads) };
|
|
2768
|
+
return this.cache;
|
|
2769
|
+
}
|
|
2770
|
+
flush() {
|
|
2771
|
+
const data = this.load();
|
|
2772
|
+
prune(data);
|
|
2773
|
+
const text2 = `${JSON.stringify(data, null, 2)}
|
|
2774
|
+
`;
|
|
2775
|
+
mkdirSync4(dirname2(this.file), { recursive: true });
|
|
2776
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2777
|
+
writeFileSync4(temp, text2, { mode: 384 });
|
|
2778
|
+
renameSync3(temp, this.file);
|
|
2779
|
+
}
|
|
2780
|
+
get(key) {
|
|
2781
|
+
const thread = this.load().threads[key];
|
|
2782
|
+
return thread === void 0 ? null : { ...thread, cards: [...thread.cards] };
|
|
2783
|
+
}
|
|
2784
|
+
appendCard(input) {
|
|
2785
|
+
const data = this.load();
|
|
2786
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2787
|
+
const existing = data.threads[input.key];
|
|
2788
|
+
const thread = existing ?? {
|
|
2789
|
+
key: input.key,
|
|
2790
|
+
accountId: input.accountId,
|
|
2791
|
+
subject: input.subject,
|
|
2792
|
+
messageId: input.messageId ?? "",
|
|
2793
|
+
cards: [],
|
|
2794
|
+
updatedAt: now
|
|
2795
|
+
};
|
|
2796
|
+
thread.accountId = input.accountId;
|
|
2797
|
+
thread.subject = input.subject;
|
|
2798
|
+
if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
|
|
2799
|
+
thread.cards.push(input.card);
|
|
2800
|
+
thread.updatedAt = now;
|
|
2801
|
+
data.threads[input.key] = thread;
|
|
2802
|
+
this.flush();
|
|
2803
|
+
return { ...thread, cards: [...thread.cards] };
|
|
2804
|
+
}
|
|
2805
|
+
removeCard(key, cardId) {
|
|
2806
|
+
const data = this.load();
|
|
2807
|
+
const thread = data.threads[key];
|
|
2808
|
+
if (thread === void 0) return null;
|
|
2809
|
+
thread.cards = thread.cards.filter((card) => card.id !== cardId);
|
|
2810
|
+
thread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2811
|
+
if (thread.cards.length === 0) {
|
|
2812
|
+
delete data.threads[key];
|
|
2813
|
+
this.flush();
|
|
2814
|
+
return null;
|
|
2815
|
+
}
|
|
2816
|
+
this.flush();
|
|
2817
|
+
return { ...thread, cards: [...thread.cards] };
|
|
2818
|
+
}
|
|
2819
|
+
clear(key) {
|
|
2820
|
+
const data = this.load();
|
|
2821
|
+
if (data.threads[key] === void 0) return false;
|
|
2822
|
+
delete data.threads[key];
|
|
2823
|
+
this.flush();
|
|
2824
|
+
return true;
|
|
2825
|
+
}
|
|
2826
|
+
/** Number of threads, for the settings page. */
|
|
2827
|
+
size() {
|
|
2828
|
+
return Object.keys(this.load().threads).length;
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
function sanitize(input) {
|
|
2832
|
+
if (input === null || typeof input !== "object") return {};
|
|
2833
|
+
const source = input;
|
|
2834
|
+
const result = {};
|
|
2835
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
2836
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
2837
|
+
const thread = raw;
|
|
2838
|
+
if (typeof thread.accountId !== "string" || !Array.isArray(thread.cards)) continue;
|
|
2839
|
+
const cards = thread.cards.filter(
|
|
2840
|
+
(card) => card !== null && typeof card === "object" && typeof card.id === "string"
|
|
2841
|
+
).map((card) => ({
|
|
2842
|
+
id: card.id,
|
|
2843
|
+
kind: card.kind,
|
|
2844
|
+
instruction: String(card.instruction ?? ""),
|
|
2845
|
+
output: String(card.output ?? ""),
|
|
2846
|
+
...typeof card.html === "string" ? { html: card.html } : {},
|
|
2847
|
+
...card.meta !== void 0 ? { meta: card.meta } : {},
|
|
2848
|
+
provider: String(card.provider ?? ""),
|
|
2849
|
+
model: String(card.model ?? ""),
|
|
2850
|
+
createdAt: String(card.createdAt ?? "")
|
|
2851
|
+
}));
|
|
2852
|
+
result[key] = {
|
|
2853
|
+
key,
|
|
2854
|
+
accountId: thread.accountId,
|
|
2855
|
+
subject: String(thread.subject ?? ""),
|
|
2856
|
+
messageId: String(thread.messageId ?? ""),
|
|
2857
|
+
cards,
|
|
2858
|
+
updatedAt: String(thread.updatedAt ?? "")
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
return result;
|
|
2862
|
+
}
|
|
2863
|
+
function prune(data) {
|
|
2864
|
+
const entries = Object.entries(data.threads).sort(
|
|
2865
|
+
(left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt)
|
|
2866
|
+
);
|
|
2867
|
+
const kept = new Map(entries.slice(0, MAX_THREADS));
|
|
2868
|
+
for (const thread of kept.values()) {
|
|
2869
|
+
if (thread.cards.length > MAX_CARDS_PER_THREAD) {
|
|
2870
|
+
thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
data.threads = Object.fromEntries(kept);
|
|
2874
|
+
let size = JSON.stringify(data).length;
|
|
2875
|
+
while (size > MAX_FILE_BYTES && kept.size > 1) {
|
|
2876
|
+
const oldest = [...kept.entries()].sort(
|
|
2877
|
+
(left, right) => left[1].updatedAt.localeCompare(right[1].updatedAt)
|
|
2878
|
+
)[0];
|
|
2879
|
+
if (oldest === void 0) break;
|
|
2880
|
+
kept.delete(oldest[0]);
|
|
2881
|
+
data.threads = Object.fromEntries(kept);
|
|
2882
|
+
size = JSON.stringify(data).length;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2159
2886
|
// src/host/drafts.ts
|
|
2160
|
-
import { randomUUID as
|
|
2161
|
-
import { mkdirSync as
|
|
2162
|
-
import { dirname, join as
|
|
2887
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2888
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2889
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2163
2890
|
function text(value) {
|
|
2164
2891
|
return value === void 0 || value === null ? "" : String(value);
|
|
2165
2892
|
}
|
|
@@ -2170,7 +2897,7 @@ function isDraft(value) {
|
|
|
2170
2897
|
return value !== null && typeof value === "object" && typeof value.id === "string";
|
|
2171
2898
|
}
|
|
2172
2899
|
function draftPath(root) {
|
|
2173
|
-
return
|
|
2900
|
+
return join4(root, "drafts.json");
|
|
2174
2901
|
}
|
|
2175
2902
|
var DraftStore = class {
|
|
2176
2903
|
file;
|
|
@@ -2183,18 +2910,18 @@ var DraftStore = class {
|
|
|
2183
2910
|
}
|
|
2184
2911
|
load() {
|
|
2185
2912
|
try {
|
|
2186
|
-
const parsed = JSON.parse(
|
|
2913
|
+
const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2187
2914
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2188
2915
|
} catch {
|
|
2189
2916
|
this.items = [];
|
|
2190
2917
|
}
|
|
2191
2918
|
}
|
|
2192
2919
|
flush() {
|
|
2193
|
-
|
|
2920
|
+
mkdirSync5(dirname3(this.file), { recursive: true });
|
|
2194
2921
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2195
|
-
|
|
2922
|
+
writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2196
2923
|
`, { encoding: "utf8", mode: 384 });
|
|
2197
|
-
|
|
2924
|
+
renameSync4(temp, this.file);
|
|
2198
2925
|
}
|
|
2199
2926
|
list() {
|
|
2200
2927
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2226,7 +2953,7 @@ var DraftStore = class {
|
|
|
2226
2953
|
return { ...existing };
|
|
2227
2954
|
}
|
|
2228
2955
|
const draft = {
|
|
2229
|
-
id:
|
|
2956
|
+
id: randomUUID3(),
|
|
2230
2957
|
accountId: this.accountId,
|
|
2231
2958
|
...fields,
|
|
2232
2959
|
updatedAt: now
|
|
@@ -2251,8 +2978,8 @@ var DraftStore = class {
|
|
|
2251
2978
|
init_imap();
|
|
2252
2979
|
|
|
2253
2980
|
// src/host/read-state.ts
|
|
2254
|
-
import { mkdirSync as
|
|
2255
|
-
import { dirname as
|
|
2981
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2982
|
+
import { dirname as dirname4 } from "node:path";
|
|
2256
2983
|
var ReadStateStore = class {
|
|
2257
2984
|
file;
|
|
2258
2985
|
keys = null;
|
|
@@ -2262,7 +2989,7 @@ var ReadStateStore = class {
|
|
|
2262
2989
|
load() {
|
|
2263
2990
|
if (this.keys !== null) return this.keys;
|
|
2264
2991
|
try {
|
|
2265
|
-
const parsed = JSON.parse(
|
|
2992
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
2266
2993
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
2267
2994
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
2268
2995
|
} catch {
|
|
@@ -2271,14 +2998,14 @@ var ReadStateStore = class {
|
|
|
2271
2998
|
return this.keys;
|
|
2272
2999
|
}
|
|
2273
3000
|
flush() {
|
|
2274
|
-
|
|
3001
|
+
mkdirSync6(dirname4(this.file), { recursive: true });
|
|
2275
3002
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2276
|
-
|
|
3003
|
+
writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
2277
3004
|
`, {
|
|
2278
3005
|
encoding: "utf8",
|
|
2279
3006
|
mode: 384
|
|
2280
3007
|
});
|
|
2281
|
-
|
|
3008
|
+
renameSync5(temp, this.file);
|
|
2282
3009
|
}
|
|
2283
3010
|
isUnread(key) {
|
|
2284
3011
|
return key !== "" && this.load().has(key);
|
|
@@ -2334,11 +3061,33 @@ var MailHarborRuntime = class {
|
|
|
2334
3061
|
databases = /* @__PURE__ */ new Map();
|
|
2335
3062
|
draftStores = /* @__PURE__ */ new Map();
|
|
2336
3063
|
readStores = /* @__PURE__ */ new Map();
|
|
3064
|
+
aiStores = /* @__PURE__ */ new Map();
|
|
2337
3065
|
backups = /* @__PURE__ */ new Map();
|
|
3066
|
+
llmProvider;
|
|
3067
|
+
aiService = null;
|
|
2338
3068
|
disposed = false;
|
|
2339
3069
|
constructor(options = {}) {
|
|
2340
3070
|
this.config = loadConfig();
|
|
2341
3071
|
this.credentials = options.credentials ?? new CredentialManager();
|
|
3072
|
+
this.llmProvider = options.llm ?? (() => void 0);
|
|
3073
|
+
}
|
|
3074
|
+
/** The Host model service, when the composition provides one. */
|
|
3075
|
+
llm() {
|
|
3076
|
+
try {
|
|
3077
|
+
return this.llmProvider();
|
|
3078
|
+
} catch {
|
|
3079
|
+
return void 0;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
ai() {
|
|
3083
|
+
if (this.aiService === null) {
|
|
3084
|
+
this.aiService = new AiService({
|
|
3085
|
+
config: () => this.config,
|
|
3086
|
+
llm: () => this.llm(),
|
|
3087
|
+
accountRoot: (account) => accountDataDir(account)
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
return this.aiService;
|
|
2342
3091
|
}
|
|
2343
3092
|
get current() {
|
|
2344
3093
|
return this.config;
|
|
@@ -2376,6 +3125,7 @@ var MailHarborRuntime = class {
|
|
|
2376
3125
|
}
|
|
2377
3126
|
for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id);
|
|
2378
3127
|
for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id);
|
|
3128
|
+
for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id);
|
|
2379
3129
|
}
|
|
2380
3130
|
/** Drop cached connections and stores for one account after a settings change. */
|
|
2381
3131
|
invalidate(accountId) {
|
|
@@ -2385,6 +3135,7 @@ var MailHarborRuntime = class {
|
|
|
2385
3135
|
this.databases.delete(accountId);
|
|
2386
3136
|
this.draftStores.delete(accountId);
|
|
2387
3137
|
this.readStores.delete(accountId);
|
|
3138
|
+
this.aiStores.delete(accountId);
|
|
2388
3139
|
this.refresh();
|
|
2389
3140
|
}
|
|
2390
3141
|
active() {
|
|
@@ -2443,8 +3194,16 @@ var MailHarborRuntime = class {
|
|
|
2443
3194
|
this.draftStores.set(account.id, { signature: file, value: created });
|
|
2444
3195
|
return created;
|
|
2445
3196
|
}
|
|
3197
|
+
aiThreads(account) {
|
|
3198
|
+
const file = join5(this.paths(account).root, "ai-threads.json");
|
|
3199
|
+
const existing = this.aiStores.get(account.id);
|
|
3200
|
+
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3201
|
+
const created = new AiThreadStore(file);
|
|
3202
|
+
this.aiStores.set(account.id, { signature: file, value: created });
|
|
3203
|
+
return created;
|
|
3204
|
+
}
|
|
2446
3205
|
readState(account) {
|
|
2447
|
-
const file =
|
|
3206
|
+
const file = join5(this.paths(account).root, "read-state.json");
|
|
2448
3207
|
const existing = this.readStores.get(account.id);
|
|
2449
3208
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
2450
3209
|
const created = new ReadStateStore(file);
|
|
@@ -2540,7 +3299,7 @@ function numberOf(args, key) {
|
|
|
2540
3299
|
const value = args[key];
|
|
2541
3300
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2542
3301
|
}
|
|
2543
|
-
function
|
|
3302
|
+
function clip2(text2, max = MAX_BODY) {
|
|
2544
3303
|
return text2.length <= max ? text2 : `${text2.slice(0, max)}
|
|
2545
3304
|
\u2026\uFF08\u5DF2\u622A\u65AD\uFF0C\u5171 ${text2.length} \u5B57\u7B26\uFF09`;
|
|
2546
3305
|
}
|
|
@@ -2761,7 +3520,7 @@ ${items.map(summaryLine).join("\n")}`
|
|
|
2761
3520
|
subject: detail2.subject,
|
|
2762
3521
|
from: `${detail2.from.name} <${detail2.from.address}>`.trim(),
|
|
2763
3522
|
date: stamp2(detail2.date),
|
|
2764
|
-
text:
|
|
3523
|
+
text: clip2(detail2.text),
|
|
2765
3524
|
attachments: detail2.attachments.map((file) => ({
|
|
2766
3525
|
filename: file.filename,
|
|
2767
3526
|
contentType: file.contentType,
|
|
@@ -2775,7 +3534,7 @@ ${items.map(summaryLine).join("\n")}`
|
|
|
2775
3534
|
subject: detail.subject,
|
|
2776
3535
|
from: `${detail.from.name} <${detail.from.address}>`.trim(),
|
|
2777
3536
|
date: stamp2(detail.date),
|
|
2778
|
-
text:
|
|
3537
|
+
text: clip2(detail.text),
|
|
2779
3538
|
attachments: detail.attachments.map((file) => ({
|
|
2780
3539
|
filename: file.filename,
|
|
2781
3540
|
contentType: file.contentType,
|
|
@@ -2904,10 +3663,18 @@ ${page.items.map(
|
|
|
2904
3663
|
// src/host/index.ts
|
|
2905
3664
|
var name = "mailharbor";
|
|
2906
3665
|
var inject = ["webServer", "tools"];
|
|
2907
|
-
var VERSION2 = "0.2.1";
|
|
2908
3666
|
var ROUTE = "/mail-plugin";
|
|
2909
3667
|
function apply(ctx) {
|
|
2910
|
-
const runtime = new MailHarborRuntime(
|
|
3668
|
+
const runtime = new MailHarborRuntime({
|
|
3669
|
+
llm: () => {
|
|
3670
|
+
if (typeof ctx.get !== "function") return void 0;
|
|
3671
|
+
try {
|
|
3672
|
+
return ctx.get.call(ctx, "llm");
|
|
3673
|
+
} catch {
|
|
3674
|
+
return void 0;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
});
|
|
2911
3678
|
const handler = createApiHandler(runtime);
|
|
2912
3679
|
const disposers = [];
|
|
2913
3680
|
const start = () => {
|
|
@@ -2933,7 +3700,7 @@ function apply(ctx) {
|
|
|
2933
3700
|
}
|
|
2934
3701
|
}
|
|
2935
3702
|
export {
|
|
2936
|
-
|
|
3703
|
+
VERSION,
|
|
2937
3704
|
apply,
|
|
2938
3705
|
inject,
|
|
2939
3706
|
name
|