@zaclinley/dsh-mailharbor 0.2.1 → 0.3.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 +15 -0
- package/README.md +38 -21
- package/README.zh.md +31 -15
- package/lib/client.js +649 -80
- package/lib/client.js.map +3 -3
- package/lib/index.js +519 -64
- 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,
|
|
@@ -927,13 +928,13 @@ var MailDatabase = class {
|
|
|
927
928
|
size: Number(item.size ?? 0)
|
|
928
929
|
}));
|
|
929
930
|
const max = this.options.maxBodyChars ?? 4e5;
|
|
930
|
-
const
|
|
931
|
+
const bodyText2 = String(row.body_text ?? "");
|
|
931
932
|
const bodyHtml = String(row.body_html ?? "");
|
|
932
933
|
return {
|
|
933
934
|
...rowToSummary(row),
|
|
934
935
|
cc: parseAddressList(String(row.cc ?? "")),
|
|
935
936
|
replyTo: [],
|
|
936
|
-
text: truncate(
|
|
937
|
+
text: truncate(bodyText2, max),
|
|
937
938
|
html: bodyHtml === "" ? null : truncate(bodyHtml, max * 2),
|
|
938
939
|
attachments,
|
|
939
940
|
references: []
|
|
@@ -969,6 +970,18 @@ var MailDatabase = class {
|
|
|
969
970
|
return false;
|
|
970
971
|
}
|
|
971
972
|
}
|
|
973
|
+
/** Recent sent messages, for learning the user's writing style. */
|
|
974
|
+
sentMessages(limit = 100) {
|
|
975
|
+
const rows = this.open().prepare(
|
|
976
|
+
`SELECT subject, body_text FROM emails
|
|
977
|
+
WHERE lower(mailbox) LIKE '%sent%' OR mailbox LIKE '%\u5DF2\u53D1\u9001%'
|
|
978
|
+
ORDER BY date DESC, id DESC LIMIT ?`
|
|
979
|
+
).all(limit);
|
|
980
|
+
return rows.map((row) => ({
|
|
981
|
+
subject: String(row.subject ?? ""),
|
|
982
|
+
text: String(row.body_text ?? "")
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
972
985
|
/** Latest backup runs, for the settings page. */
|
|
973
986
|
backupSummary() {
|
|
974
987
|
try {
|
|
@@ -1425,6 +1438,9 @@ var Router = class {
|
|
|
1425
1438
|
patch(path, handler) {
|
|
1426
1439
|
this.add("PATCH", path, handler);
|
|
1427
1440
|
}
|
|
1441
|
+
put(path, handler) {
|
|
1442
|
+
this.add("PUT", path, handler);
|
|
1443
|
+
}
|
|
1428
1444
|
delete(path, handler) {
|
|
1429
1445
|
this.add("DELETE", path, handler);
|
|
1430
1446
|
}
|
|
@@ -1450,6 +1466,266 @@ var Router = class {
|
|
|
1450
1466
|
}
|
|
1451
1467
|
};
|
|
1452
1468
|
|
|
1469
|
+
// src/host/ai.ts
|
|
1470
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1471
|
+
import { dirname, join as join3 } from "node:path";
|
|
1472
|
+
var TIMEOUT_MS = 9e4;
|
|
1473
|
+
var MAX_PROMPT_CHARS = 4e4;
|
|
1474
|
+
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
|
|
1475
|
+
|
|
1476
|
+
\u7ED3\u8BBA
|
|
1477
|
+
\uFF081-2 \u53E5\u8BDD\u8BF4\u660E\u8FD9\u5C01\u90AE\u4EF6\u662F\u4EC0\u4E48\u4E8B\uFF09
|
|
1478
|
+
|
|
1479
|
+
\u9700\u8981\u4F60\u505A\u4EC0\u4E48
|
|
1480
|
+
\uFF08\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u9700\u64CD\u4F5C\u300D\uFF1B\u6709\u5C31\u5217 1-3 \u6761\uFF0C\u9879\u76EE\u7B26\u53F7\uFF09
|
|
1481
|
+
|
|
1482
|
+
\u5173\u952E\u4FE1\u606F
|
|
1483
|
+
\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
|
|
1484
|
+
|
|
1485
|
+
\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9
|
|
1486
|
+
\uFF081-3 \u6761\u4E2D\u6587\u8981\u70B9\uFF09
|
|
1487
|
+
|
|
1488
|
+
\u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\uFF0C\u4E0D\u8981\u7F16\u9020\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B\u3002`;
|
|
1489
|
+
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
|
|
1490
|
+
|
|
1491
|
+
\u8981\u6C42\uFF1A
|
|
1492
|
+
- \u8BED\u8A00\uFF1A${language === "en" ? "\u82F1\u6587" : "\u7B80\u4F53\u4E2D\u6587"}
|
|
1493
|
+
- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48
|
|
1494
|
+
- \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
|
|
1495
|
+
- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB\uFF08\u79F0\u547C\u3001\u6B63\u6587\u3001\u7ED3\u5C3E\uFF09\uFF0C\u4E0D\u8981\u5F15\u53F7\u5305\u88F9
|
|
1496
|
+
${style === "" ? "" : `
|
|
1497
|
+
\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A
|
|
1498
|
+
${style}`}`;
|
|
1499
|
+
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
|
|
1500
|
+
|
|
1501
|
+
\u89C4\u5219\uFF1A
|
|
1502
|
+
- \u53EA\u8F93\u51FA\u8BD1\u6587\uFF0C\u4E0D\u6DFB\u52A0\u89E3\u91CA
|
|
1503
|
+
- \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784
|
|
1504
|
+
- \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
|
|
1505
|
+
- \u4E13\u6709\u540D\u8BCD\u3001\u90AE\u7BB1\u5730\u5740\u3001\u94FE\u63A5\u3001\u5355\u53F7\u4FDD\u6301\u4E0D\u53D8`;
|
|
1506
|
+
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
|
|
1507
|
+
\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
|
|
1508
|
+
\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`;
|
|
1509
|
+
function stylePath(root) {
|
|
1510
|
+
return join3(root, "style.json");
|
|
1511
|
+
}
|
|
1512
|
+
function readStyle(root) {
|
|
1513
|
+
try {
|
|
1514
|
+
const parsed = JSON.parse(readFileSync3(stylePath(root), "utf8"));
|
|
1515
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
1516
|
+
const value = parsed;
|
|
1517
|
+
if (typeof value.profile !== "string" || value.profile.trim() === "") return null;
|
|
1518
|
+
return {
|
|
1519
|
+
profile: value.profile,
|
|
1520
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : "",
|
|
1521
|
+
sampleCount: typeof value.sampleCount === "number" ? value.sampleCount : 0
|
|
1522
|
+
};
|
|
1523
|
+
} catch {
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function writeStyle(root, profile) {
|
|
1528
|
+
const file = stylePath(root);
|
|
1529
|
+
mkdirSync3(dirname(file), { recursive: true });
|
|
1530
|
+
const temp = `${file}.${String(process.pid)}.tmp`;
|
|
1531
|
+
writeFileSync3(temp, `${JSON.stringify(profile, null, 2)}
|
|
1532
|
+
`, { mode: 384 });
|
|
1533
|
+
renameSync2(temp, file);
|
|
1534
|
+
}
|
|
1535
|
+
function clip(value, max = MAX_PROMPT_CHARS) {
|
|
1536
|
+
return value.length <= max ? value : `${value.slice(0, max - 2e3)}
|
|
1537
|
+
\u2026\uFF08\u4E2D\u95F4\u7701\u7565\uFF09\u2026
|
|
1538
|
+
${value.slice(-2e3)}`;
|
|
1539
|
+
}
|
|
1540
|
+
function bodyText(message) {
|
|
1541
|
+
if (message.text.trim() !== "") return message.text;
|
|
1542
|
+
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();
|
|
1543
|
+
}
|
|
1544
|
+
function contextOf(message) {
|
|
1545
|
+
const lines = [
|
|
1546
|
+
`\u4E3B\u9898: ${message.subject}`,
|
|
1547
|
+
`\u53D1\u4EF6\u4EBA: ${message.from.name !== "" ? `${message.from.name} <${message.from.address}>` : message.from.address}`,
|
|
1548
|
+
`\u6536\u4EF6\u4EBA: ${message.to.map((item) => item.name !== "" ? `${item.name} <${item.address}>` : item.address).join(", ")}`,
|
|
1549
|
+
...message.cc.length === 0 ? [] : [`\u6284\u9001: ${message.cc.map((item) => item.address).join(", ")}`],
|
|
1550
|
+
`\u65F6\u95F4: ${message.date}`,
|
|
1551
|
+
"",
|
|
1552
|
+
clip(bodyText(message))
|
|
1553
|
+
];
|
|
1554
|
+
return lines.join("\n");
|
|
1555
|
+
}
|
|
1556
|
+
var AiService = class {
|
|
1557
|
+
deps;
|
|
1558
|
+
constructor(deps) {
|
|
1559
|
+
this.deps = deps;
|
|
1560
|
+
}
|
|
1561
|
+
llm() {
|
|
1562
|
+
try {
|
|
1563
|
+
return this.deps.llm();
|
|
1564
|
+
} catch {
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
status(account) {
|
|
1569
|
+
const config = this.deps.config();
|
|
1570
|
+
const llm = this.llm();
|
|
1571
|
+
const style = account === null ? null : this.style(account);
|
|
1572
|
+
return {
|
|
1573
|
+
available: llm !== void 0,
|
|
1574
|
+
enabled: config.ai.enabled,
|
|
1575
|
+
provider: config.ai.provider,
|
|
1576
|
+
model: config.ai.model,
|
|
1577
|
+
styleUpdatedAt: style?.updatedAt ?? "",
|
|
1578
|
+
styleSampleCount: style?.sampleCount ?? 0,
|
|
1579
|
+
...llm === void 0 ? { error: "\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1" } : {}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
style(account) {
|
|
1583
|
+
return readStyle(this.deps.accountRoot(account));
|
|
1584
|
+
}
|
|
1585
|
+
saveStyle(account, profile) {
|
|
1586
|
+
const text2 = profile.trim();
|
|
1587
|
+
if (text2 === "") throw new Error("\u5199\u4F5C\u98CE\u683C\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1588
|
+
if (text2.length > 8e3) throw new Error("\u5199\u4F5C\u98CE\u683C\u8FC7\u957F\uFF08\u6700\u591A 8000 \u5B57\uFF09");
|
|
1589
|
+
const previous = this.style(account);
|
|
1590
|
+
const next = {
|
|
1591
|
+
profile: text2,
|
|
1592
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1593
|
+
sampleCount: previous?.sampleCount ?? 0
|
|
1594
|
+
};
|
|
1595
|
+
writeStyle(this.deps.accountRoot(account), next);
|
|
1596
|
+
return next;
|
|
1597
|
+
}
|
|
1598
|
+
requireReady() {
|
|
1599
|
+
const config = this.deps.config();
|
|
1600
|
+
if (!config.ai.enabled) throw new Error("AI \u529F\u80FD\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5230\u8BBE\u7F6E\u91CC\u5F00\u542F");
|
|
1601
|
+
const llm = this.llm();
|
|
1602
|
+
if (llm === void 0) throw new Error("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1");
|
|
1603
|
+
if (config.ai.provider === "" || config.ai.model === "") throw new Error("\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 AI \u6A21\u578B");
|
|
1604
|
+
return { provider: config.ai.provider, model: config.ai.model };
|
|
1605
|
+
}
|
|
1606
|
+
async complete(system, user, signal) {
|
|
1607
|
+
const { provider, model } = this.requireReady();
|
|
1608
|
+
const llm = this.llm();
|
|
1609
|
+
if (llm === void 0) throw new Error("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1");
|
|
1610
|
+
const controller = new AbortController();
|
|
1611
|
+
const timer = setTimeout(() => {
|
|
1612
|
+
controller.abort();
|
|
1613
|
+
}, TIMEOUT_MS);
|
|
1614
|
+
const onAbort = () => {
|
|
1615
|
+
controller.abort();
|
|
1616
|
+
};
|
|
1617
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1618
|
+
let text2 = "";
|
|
1619
|
+
try {
|
|
1620
|
+
const stream = llm.stream({
|
|
1621
|
+
provider,
|
|
1622
|
+
model,
|
|
1623
|
+
messages: [
|
|
1624
|
+
{ role: "system", content: [{ type: "text", text: system }] },
|
|
1625
|
+
{ role: "user", content: [{ type: "text", text: user }] }
|
|
1626
|
+
],
|
|
1627
|
+
signal: controller.signal
|
|
1628
|
+
});
|
|
1629
|
+
for await (const chunk of stream) {
|
|
1630
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") {
|
|
1631
|
+
text2 += chunk.text;
|
|
1632
|
+
} else if (chunk.type === "finish") {
|
|
1633
|
+
const reason = chunk.reason;
|
|
1634
|
+
if (reason?.kind === "error") {
|
|
1635
|
+
throw new Error(reason.failure?.message ?? reason.failure?.code ?? "\u6A21\u578B\u8C03\u7528\u5931\u8D25");
|
|
1636
|
+
}
|
|
1637
|
+
if (reason?.kind === "aborted") throw new Error("\u6A21\u578B\u8C03\u7528\u5DF2\u53D6\u6D88");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
} finally {
|
|
1641
|
+
clearTimeout(timer);
|
|
1642
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1643
|
+
}
|
|
1644
|
+
const trimmed = text2.trim();
|
|
1645
|
+
if (trimmed === "") throw new Error("\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u5185\u5BB9");
|
|
1646
|
+
return trimmed;
|
|
1647
|
+
}
|
|
1648
|
+
async summarize(message, signal) {
|
|
1649
|
+
return this.complete(SUMMARY_SYSTEM, contextOf(message), signal);
|
|
1650
|
+
}
|
|
1651
|
+
async reply(account, message, options = {}, signal) {
|
|
1652
|
+
const style = this.style(account)?.profile ?? "";
|
|
1653
|
+
const instruction = (options.instruction ?? "").trim();
|
|
1654
|
+
const user = [
|
|
1655
|
+
"\u539F\u59CB\u90AE\u4EF6\uFF1A",
|
|
1656
|
+
contextOf(message),
|
|
1657
|
+
"",
|
|
1658
|
+
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}`
|
|
1659
|
+
].join("\n");
|
|
1660
|
+
return this.complete(REPLY_SYSTEM(options.language ?? "zh", style), user, signal);
|
|
1661
|
+
}
|
|
1662
|
+
async translate(html, target, signal) {
|
|
1663
|
+
const text2 = html.trim();
|
|
1664
|
+
if (text2 === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1");
|
|
1665
|
+
return this.complete(TRANSLATE_SYSTEM(target), clip(text2, 2e4), signal);
|
|
1666
|
+
}
|
|
1667
|
+
async learnStyle(account, samples, signal) {
|
|
1668
|
+
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");
|
|
1669
|
+
const digest = samples.slice(0, 100).map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}
|
|
1670
|
+
${clip(sample.text, 800)}`).join("\n\n");
|
|
1671
|
+
const profile = await this.complete(STYLE_SYSTEM, clip(digest, 35e3), signal);
|
|
1672
|
+
const next = {
|
|
1673
|
+
profile,
|
|
1674
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1675
|
+
sampleCount: samples.length
|
|
1676
|
+
};
|
|
1677
|
+
writeStyle(this.deps.accountRoot(account), next);
|
|
1678
|
+
return next;
|
|
1679
|
+
}
|
|
1680
|
+
async models() {
|
|
1681
|
+
const llm = this.llm();
|
|
1682
|
+
if (llm === void 0) return [];
|
|
1683
|
+
const providers = llm.listProviders();
|
|
1684
|
+
const catalog = [];
|
|
1685
|
+
for (const provider of providers) {
|
|
1686
|
+
try {
|
|
1687
|
+
const models = await llm.listModels(provider.id);
|
|
1688
|
+
catalog.push({
|
|
1689
|
+
provider: provider.id,
|
|
1690
|
+
name: provider.name ?? provider.id,
|
|
1691
|
+
models: models.map((model) => ({ id: model.id, name: model.name ?? model.id }))
|
|
1692
|
+
});
|
|
1693
|
+
} catch {
|
|
1694
|
+
catalog.push({ provider: provider.id, name: provider.name ?? provider.id, models: [] });
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return catalog;
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
function summaryBlocks(text2) {
|
|
1701
|
+
const blocks = [];
|
|
1702
|
+
let current = { title: "", lines: [] };
|
|
1703
|
+
const push = () => {
|
|
1704
|
+
const body = current.lines.join("\n").trim();
|
|
1705
|
+
if (current.title !== "" || body !== "") blocks.push({ title: current.title, body });
|
|
1706
|
+
current = { title: "", lines: [] };
|
|
1707
|
+
};
|
|
1708
|
+
for (const line2 of text2.split("\n")) {
|
|
1709
|
+
const heading = /^#{1,4}\s+(.*)$/.exec(line2) ?? /^(结论|需要你做什么|关键信息|建议回复要点)\s*$/.exec(line2);
|
|
1710
|
+
if (heading !== null) {
|
|
1711
|
+
push();
|
|
1712
|
+
current.title = heading[1] ?? "";
|
|
1713
|
+
} else {
|
|
1714
|
+
current.lines.push(line2);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
push();
|
|
1718
|
+
return blocks;
|
|
1719
|
+
}
|
|
1720
|
+
function textToHtml(text2) {
|
|
1721
|
+
return text2.split(/\n{2,}/).map(
|
|
1722
|
+
(paragraph) => `<p>${paragraph.trim().replaceAll("&", "&").replaceAll("<", "<").replaceAll("\n", "<br>")}</p>`
|
|
1723
|
+
).join("");
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/host/version.ts
|
|
1727
|
+
var VERSION = "0.3.0";
|
|
1728
|
+
|
|
1453
1729
|
// src/host/api.ts
|
|
1454
1730
|
init_smtp();
|
|
1455
1731
|
|
|
@@ -1504,49 +1780,61 @@ function buildRouter(runtime) {
|
|
|
1504
1780
|
router.get("/health", () => ({ ok: true, version: VERSION }));
|
|
1505
1781
|
router.get("/state", () => appState(runtime));
|
|
1506
1782
|
router.get("/folders", async ({ url }) => {
|
|
1507
|
-
const { account } = accountFrom(runtime, url);
|
|
1508
1783
|
const source = sourceOf(url);
|
|
1784
|
+
const requested = url.searchParams.get("accountId");
|
|
1785
|
+
if (source === "server" && requested === "all") {
|
|
1786
|
+
throw new HttpError(400, "\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD");
|
|
1787
|
+
}
|
|
1788
|
+
const accounts = requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountFrom(runtime, url).account];
|
|
1509
1789
|
if (source === "server") {
|
|
1510
|
-
const provider = runtime.provider(
|
|
1790
|
+
const provider = runtime.provider(accounts[0]);
|
|
1511
1791
|
const boxes = await provider.mailboxes();
|
|
1512
1792
|
return {
|
|
1513
1793
|
items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages }))
|
|
1514
1794
|
};
|
|
1515
1795
|
}
|
|
1516
|
-
|
|
1796
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1797
|
+
let drafts = 0;
|
|
1798
|
+
for (const account of accounts) {
|
|
1799
|
+
try {
|
|
1800
|
+
for (const box of runtime.database(account).mailboxes()) {
|
|
1801
|
+
counts.set(box.name, (counts.get(box.name) ?? 0) + box.count);
|
|
1802
|
+
}
|
|
1803
|
+
} catch {
|
|
1804
|
+
}
|
|
1805
|
+
try {
|
|
1806
|
+
drafts += runtime.drafts(account).list().length;
|
|
1807
|
+
} catch {
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const items = [...counts.entries()].map(([name2, count]) => ({ name: name2, count })).sort((left, right) => right.count - left.count);
|
|
1811
|
+
items.unshift({ name: "__drafts", count: drafts });
|
|
1812
|
+
return { items };
|
|
1517
1813
|
});
|
|
1518
1814
|
router.get("/messages", async ({ url }) => {
|
|
1519
|
-
const { account } = accountFrom(runtime, url);
|
|
1520
1815
|
const source = sourceOf(url);
|
|
1816
|
+
const requested = url.searchParams.get("accountId");
|
|
1521
1817
|
const mailbox = url.searchParams.get("mailbox") ?? "all";
|
|
1522
1818
|
const query = url.searchParams.get("query") ?? "";
|
|
1523
1819
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1524
1820
|
const limit = Number(url.searchParams.get("limit") ?? runtime.current.render.listLimit);
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
return {
|
|
1821
|
+
if (requested === "all") {
|
|
1822
|
+
if (source === "server") throw new HttpError(400, "\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD");
|
|
1823
|
+
return listAllAccounts(runtime, { mailbox, query, offset, limit });
|
|
1528
1824
|
}
|
|
1529
|
-
const
|
|
1530
|
-
if (
|
|
1531
|
-
const
|
|
1532
|
-
return { ...
|
|
1825
|
+
const { account } = accountFrom(runtime, url);
|
|
1826
|
+
if (source === "server") {
|
|
1827
|
+
const items = await listServer(runtime, account, mailbox, query, limit, offset);
|
|
1828
|
+
return { ...items, offset, limit };
|
|
1533
1829
|
}
|
|
1534
|
-
const page =
|
|
1830
|
+
const page = runtime.database(account).list({ mailbox: mailbox === "" ? "all" : mailbox, query, offset, limit });
|
|
1535
1831
|
return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) };
|
|
1536
1832
|
});
|
|
1537
1833
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
1538
1834
|
const { account } = accountFrom(runtime, url);
|
|
1539
1835
|
const source = sourceOf(url);
|
|
1540
1836
|
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);
|
|
1837
|
+
return loadDetail(runtime, account, source, params.id ?? "", mailbox);
|
|
1550
1838
|
});
|
|
1551
1839
|
router.post("/messages/:id/read", async ({ url, params, body }) => {
|
|
1552
1840
|
const { account } = accountFrom(runtime, url);
|
|
@@ -1617,17 +1905,24 @@ function buildRouter(runtime) {
|
|
|
1617
1905
|
return result;
|
|
1618
1906
|
});
|
|
1619
1907
|
router.get("/unread", async ({ url }) => {
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1908
|
+
const requested = url.searchParams.get("accountId");
|
|
1909
|
+
const accounts = requested === null || requested === "" || requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountById(runtime, requested)];
|
|
1910
|
+
let server = 0;
|
|
1911
|
+
let answered = 0;
|
|
1912
|
+
await Promise.all(
|
|
1913
|
+
accounts.map(async (account) => {
|
|
1914
|
+
if (!runtime.hasCredentials(account)) return;
|
|
1915
|
+
try {
|
|
1916
|
+
const result = await runtime.provider(account).unseen();
|
|
1917
|
+
server += result.count;
|
|
1918
|
+
answered += 1;
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
if (answered > 0) return { unread: server, source: "server" };
|
|
1924
|
+
const local = accounts.reduce((sum, account) => sum + runtime.readState(account).count(), 0);
|
|
1925
|
+
return { unread: local, source: "local" };
|
|
1631
1926
|
});
|
|
1632
1927
|
router.get("/accounts", () => ({ accounts: runtime.current.accounts }));
|
|
1633
1928
|
router.post("/accounts", async ({ body }) => {
|
|
@@ -1727,6 +2022,84 @@ function buildRouter(runtime) {
|
|
|
1727
2022
|
}
|
|
1728
2023
|
return { ok: true, imap, smtp };
|
|
1729
2024
|
});
|
|
2025
|
+
router.get("/ai/status", ({ url }) => {
|
|
2026
|
+
const requested = url.searchParams.get("accountId");
|
|
2027
|
+
const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
|
|
2028
|
+
return runtime.ai().status(account ?? null);
|
|
2029
|
+
});
|
|
2030
|
+
router.get("/ai/models", async () => ({ providers: await runtime.ai().models() }));
|
|
2031
|
+
router.post("/ai/summary", async ({ body }) => {
|
|
2032
|
+
const payload = await body();
|
|
2033
|
+
const account = accountFromPayload(runtime, payload);
|
|
2034
|
+
const detail = await loadDetail(
|
|
2035
|
+
runtime,
|
|
2036
|
+
account,
|
|
2037
|
+
sourceOfPayload(payload),
|
|
2038
|
+
String(payload.id ?? ""),
|
|
2039
|
+
mailboxOfPayload(payload)
|
|
2040
|
+
);
|
|
2041
|
+
try {
|
|
2042
|
+
const summary = await runtime.ai().summarize(detail);
|
|
2043
|
+
return { summary, blocks: summaryBlocks(summary) };
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
throw aiHttpError(error);
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
router.post("/ai/reply", async ({ body }) => {
|
|
2049
|
+
const payload = await body();
|
|
2050
|
+
const account = accountFromPayload(runtime, payload);
|
|
2051
|
+
const detail = await loadDetail(
|
|
2052
|
+
runtime,
|
|
2053
|
+
account,
|
|
2054
|
+
sourceOfPayload(payload),
|
|
2055
|
+
String(payload.id ?? ""),
|
|
2056
|
+
mailboxOfPayload(payload)
|
|
2057
|
+
);
|
|
2058
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2059
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2060
|
+
try {
|
|
2061
|
+
const text2 = await runtime.ai().reply(account, detail, { instruction, language });
|
|
2062
|
+
return { text: text2, html: textToHtml(text2) };
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
throw aiHttpError(error);
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
router.post("/ai/translate", async ({ body }) => {
|
|
2068
|
+
const payload = await body();
|
|
2069
|
+
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2070
|
+
const target = payload.target === "en" ? "en" : "zh";
|
|
2071
|
+
try {
|
|
2072
|
+
const text2 = await runtime.ai().translate(html, target);
|
|
2073
|
+
return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
throw aiHttpError(error);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
router.get("/ai/style", ({ url }) => {
|
|
2079
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2080
|
+
return { style: runtime.ai().style(account) };
|
|
2081
|
+
});
|
|
2082
|
+
router.put("/ai/style", async ({ body }) => {
|
|
2083
|
+
const payload = await body();
|
|
2084
|
+
const account = accountFromPayload(runtime, payload);
|
|
2085
|
+
try {
|
|
2086
|
+
const style = runtime.ai().saveStyle(account, String(payload.profile ?? ""));
|
|
2087
|
+
return { style };
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
throw aiHttpError(error);
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
router.post("/ai/style/learn", async ({ body }) => {
|
|
2093
|
+
const payload = await body();
|
|
2094
|
+
const account = accountFromPayload(runtime, payload);
|
|
2095
|
+
try {
|
|
2096
|
+
const samples = runtime.database(account).sentMessages(100);
|
|
2097
|
+
const style = await runtime.ai().learnStyle(account, samples);
|
|
2098
|
+
return { style };
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
throw aiHttpError(error);
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
1730
2103
|
router.get("/settings", () => settingsView(runtime.current));
|
|
1731
2104
|
router.patch("/settings", async ({ body }) => {
|
|
1732
2105
|
const payload = await body();
|
|
@@ -1755,7 +2128,60 @@ function buildRouter(runtime) {
|
|
|
1755
2128
|
});
|
|
1756
2129
|
return router;
|
|
1757
2130
|
}
|
|
1758
|
-
|
|
2131
|
+
async function loadDetail(runtime, account, source, id, mailbox) {
|
|
2132
|
+
if (source === "server") {
|
|
2133
|
+
const message = await runtime.provider(account).read(mailbox, Number(id));
|
|
2134
|
+
return serverDetail(runtime, account, message, mailbox);
|
|
2135
|
+
}
|
|
2136
|
+
const detail = runtime.database(account).get(Number(id));
|
|
2137
|
+
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
2138
|
+
return localDetail(runtime, account, detail);
|
|
2139
|
+
}
|
|
2140
|
+
function listAllAccounts(runtime, options) {
|
|
2141
|
+
const accounts = runtime.current.accounts.filter((item) => item.enabled !== false);
|
|
2142
|
+
const collected = [];
|
|
2143
|
+
let total = 0;
|
|
2144
|
+
for (const account of accounts) {
|
|
2145
|
+
try {
|
|
2146
|
+
const page = runtime.database(account).list({
|
|
2147
|
+
mailbox: options.mailbox === "" ? "all" : options.mailbox,
|
|
2148
|
+
query: options.query,
|
|
2149
|
+
offset: 0,
|
|
2150
|
+
limit: options.offset + options.limit
|
|
2151
|
+
});
|
|
2152
|
+
total += page.total;
|
|
2153
|
+
for (const item of page.items) collected.push(localSummary(runtime, account, item));
|
|
2154
|
+
} catch {
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
collected.sort((left, right) => right.date.localeCompare(left.date));
|
|
2158
|
+
return {
|
|
2159
|
+
items: collected.slice(options.offset, options.offset + options.limit),
|
|
2160
|
+
total,
|
|
2161
|
+
offset: options.offset,
|
|
2162
|
+
limit: options.limit
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
function accountFromPayload(runtime, payload) {
|
|
2166
|
+
const requested = typeof payload.accountId === "string" ? payload.accountId : "";
|
|
2167
|
+
if (requested !== "" && requested !== "all") {
|
|
2168
|
+
const found = runtime.current.accounts.find((item) => item.id === requested);
|
|
2169
|
+
if (found !== void 0) return found;
|
|
2170
|
+
}
|
|
2171
|
+
const active = runtime.active();
|
|
2172
|
+
if (active !== null) return active;
|
|
2173
|
+
throw new HttpError(400, "\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7");
|
|
2174
|
+
}
|
|
2175
|
+
function sourceOfPayload(payload) {
|
|
2176
|
+
return payload.source === "server" ? "server" : "local";
|
|
2177
|
+
}
|
|
2178
|
+
function mailboxOfPayload(payload) {
|
|
2179
|
+
return typeof payload.mailbox === "string" && payload.mailbox !== "" ? payload.mailbox : "INBOX";
|
|
2180
|
+
}
|
|
2181
|
+
function aiHttpError(error) {
|
|
2182
|
+
if (error instanceof HttpError) return error;
|
|
2183
|
+
return new HttpError(400, error instanceof Error ? error.message : String(error));
|
|
2184
|
+
}
|
|
1759
2185
|
function accountById(runtime, id) {
|
|
1760
2186
|
const account = runtime.current.accounts.find((item) => item.id === id);
|
|
1761
2187
|
if (account === void 0) throw new HttpError(404, "\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
@@ -1885,9 +2311,9 @@ function serverDetail(runtime, account, message, mailbox) {
|
|
|
1885
2311
|
references: message.references
|
|
1886
2312
|
};
|
|
1887
2313
|
}
|
|
1888
|
-
async function listServer(runtime, account, mailbox, query, limit) {
|
|
2314
|
+
async function listServer(runtime, account, mailbox, query, limit, offset = 0) {
|
|
1889
2315
|
const provider = runtime.provider(account);
|
|
1890
|
-
const result = await provider.list(mailbox === "all" ? "INBOX" : mailbox, Math.min(200, limit * 2));
|
|
2316
|
+
const result = await provider.list(mailbox === "all" ? "INBOX" : mailbox, Math.min(200, limit * 2), offset);
|
|
1891
2317
|
let database = null;
|
|
1892
2318
|
try {
|
|
1893
2319
|
database = runtime.database(account);
|
|
@@ -1952,7 +2378,7 @@ async function sendFromPayload(runtime, account, payload) {
|
|
|
1952
2378
|
if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, "\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB");
|
|
1953
2379
|
const { sendMail: sendMail2 } = await Promise.resolve().then(() => (init_smtp(), smtp_exports));
|
|
1954
2380
|
const signature = runtime.current.signature;
|
|
1955
|
-
const
|
|
2381
|
+
const bodyText2 = signature === "" ? text2 : `${text2}
|
|
1956
2382
|
|
|
1957
2383
|
--
|
|
1958
2384
|
${signature}`;
|
|
@@ -1965,7 +2391,7 @@ ${signature}`;
|
|
|
1965
2391
|
to,
|
|
1966
2392
|
cc: stringOf(payload.cc),
|
|
1967
2393
|
subject,
|
|
1968
|
-
text:
|
|
2394
|
+
text: bodyText2,
|
|
1969
2395
|
html: bodyHtml,
|
|
1970
2396
|
...typeof payload.inReplyTo === "string" ? { inReplyTo: payload.inReplyTo } : {},
|
|
1971
2397
|
...typeof payload.references === "string" ? { references: payload.references } : {},
|
|
@@ -1977,7 +2403,7 @@ ${signature}`;
|
|
|
1977
2403
|
|
|
1978
2404
|
// src/host/runtime.ts
|
|
1979
2405
|
import { statSync } from "node:fs";
|
|
1980
|
-
import { join as
|
|
2406
|
+
import { join as join5 } from "node:path";
|
|
1981
2407
|
|
|
1982
2408
|
// src/host/secrets.ts
|
|
1983
2409
|
import { spawnSync } from "node:child_process";
|
|
@@ -2158,8 +2584,8 @@ var CredentialManager = class {
|
|
|
2158
2584
|
|
|
2159
2585
|
// src/host/drafts.ts
|
|
2160
2586
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2161
|
-
import { mkdirSync as
|
|
2162
|
-
import { dirname, join as
|
|
2587
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2588
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
2163
2589
|
function text(value) {
|
|
2164
2590
|
return value === void 0 || value === null ? "" : String(value);
|
|
2165
2591
|
}
|
|
@@ -2170,7 +2596,7 @@ function isDraft(value) {
|
|
|
2170
2596
|
return value !== null && typeof value === "object" && typeof value.id === "string";
|
|
2171
2597
|
}
|
|
2172
2598
|
function draftPath(root) {
|
|
2173
|
-
return
|
|
2599
|
+
return join4(root, "drafts.json");
|
|
2174
2600
|
}
|
|
2175
2601
|
var DraftStore = class {
|
|
2176
2602
|
file;
|
|
@@ -2183,18 +2609,18 @@ var DraftStore = class {
|
|
|
2183
2609
|
}
|
|
2184
2610
|
load() {
|
|
2185
2611
|
try {
|
|
2186
|
-
const parsed = JSON.parse(
|
|
2612
|
+
const parsed = JSON.parse(readFileSync4(this.file, "utf8"));
|
|
2187
2613
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2188
2614
|
} catch {
|
|
2189
2615
|
this.items = [];
|
|
2190
2616
|
}
|
|
2191
2617
|
}
|
|
2192
2618
|
flush() {
|
|
2193
|
-
|
|
2619
|
+
mkdirSync4(dirname2(this.file), { recursive: true });
|
|
2194
2620
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2195
|
-
|
|
2621
|
+
writeFileSync4(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2196
2622
|
`, { encoding: "utf8", mode: 384 });
|
|
2197
|
-
|
|
2623
|
+
renameSync3(temp, this.file);
|
|
2198
2624
|
}
|
|
2199
2625
|
list() {
|
|
2200
2626
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2251,8 +2677,8 @@ var DraftStore = class {
|
|
|
2251
2677
|
init_imap();
|
|
2252
2678
|
|
|
2253
2679
|
// src/host/read-state.ts
|
|
2254
|
-
import { mkdirSync as
|
|
2255
|
-
import { dirname as
|
|
2680
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2681
|
+
import { dirname as dirname3 } from "node:path";
|
|
2256
2682
|
var ReadStateStore = class {
|
|
2257
2683
|
file;
|
|
2258
2684
|
keys = null;
|
|
@@ -2262,7 +2688,7 @@ var ReadStateStore = class {
|
|
|
2262
2688
|
load() {
|
|
2263
2689
|
if (this.keys !== null) return this.keys;
|
|
2264
2690
|
try {
|
|
2265
|
-
const parsed = JSON.parse(
|
|
2691
|
+
const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2266
2692
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
2267
2693
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
2268
2694
|
} catch {
|
|
@@ -2271,14 +2697,14 @@ var ReadStateStore = class {
|
|
|
2271
2697
|
return this.keys;
|
|
2272
2698
|
}
|
|
2273
2699
|
flush() {
|
|
2274
|
-
|
|
2700
|
+
mkdirSync5(dirname3(this.file), { recursive: true });
|
|
2275
2701
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2276
|
-
|
|
2702
|
+
writeFileSync5(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
2277
2703
|
`, {
|
|
2278
2704
|
encoding: "utf8",
|
|
2279
2705
|
mode: 384
|
|
2280
2706
|
});
|
|
2281
|
-
|
|
2707
|
+
renameSync4(temp, this.file);
|
|
2282
2708
|
}
|
|
2283
2709
|
isUnread(key) {
|
|
2284
2710
|
return key !== "" && this.load().has(key);
|
|
@@ -2335,10 +2761,31 @@ var MailHarborRuntime = class {
|
|
|
2335
2761
|
draftStores = /* @__PURE__ */ new Map();
|
|
2336
2762
|
readStores = /* @__PURE__ */ new Map();
|
|
2337
2763
|
backups = /* @__PURE__ */ new Map();
|
|
2764
|
+
llmProvider;
|
|
2765
|
+
aiService = null;
|
|
2338
2766
|
disposed = false;
|
|
2339
2767
|
constructor(options = {}) {
|
|
2340
2768
|
this.config = loadConfig();
|
|
2341
2769
|
this.credentials = options.credentials ?? new CredentialManager();
|
|
2770
|
+
this.llmProvider = options.llm ?? (() => void 0);
|
|
2771
|
+
}
|
|
2772
|
+
/** The Host model service, when the composition provides one. */
|
|
2773
|
+
llm() {
|
|
2774
|
+
try {
|
|
2775
|
+
return this.llmProvider();
|
|
2776
|
+
} catch {
|
|
2777
|
+
return void 0;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
ai() {
|
|
2781
|
+
if (this.aiService === null) {
|
|
2782
|
+
this.aiService = new AiService({
|
|
2783
|
+
config: () => this.config,
|
|
2784
|
+
llm: () => this.llm(),
|
|
2785
|
+
accountRoot: (account) => accountDataDir(account)
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
return this.aiService;
|
|
2342
2789
|
}
|
|
2343
2790
|
get current() {
|
|
2344
2791
|
return this.config;
|
|
@@ -2444,7 +2891,7 @@ var MailHarborRuntime = class {
|
|
|
2444
2891
|
return created;
|
|
2445
2892
|
}
|
|
2446
2893
|
readState(account) {
|
|
2447
|
-
const file =
|
|
2894
|
+
const file = join5(this.paths(account).root, "read-state.json");
|
|
2448
2895
|
const existing = this.readStores.get(account.id);
|
|
2449
2896
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
2450
2897
|
const created = new ReadStateStore(file);
|
|
@@ -2540,7 +2987,7 @@ function numberOf(args, key) {
|
|
|
2540
2987
|
const value = args[key];
|
|
2541
2988
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2542
2989
|
}
|
|
2543
|
-
function
|
|
2990
|
+
function clip2(text2, max = MAX_BODY) {
|
|
2544
2991
|
return text2.length <= max ? text2 : `${text2.slice(0, max)}
|
|
2545
2992
|
\u2026\uFF08\u5DF2\u622A\u65AD\uFF0C\u5171 ${text2.length} \u5B57\u7B26\uFF09`;
|
|
2546
2993
|
}
|
|
@@ -2761,7 +3208,7 @@ ${items.map(summaryLine).join("\n")}`
|
|
|
2761
3208
|
subject: detail2.subject,
|
|
2762
3209
|
from: `${detail2.from.name} <${detail2.from.address}>`.trim(),
|
|
2763
3210
|
date: stamp2(detail2.date),
|
|
2764
|
-
text:
|
|
3211
|
+
text: clip2(detail2.text),
|
|
2765
3212
|
attachments: detail2.attachments.map((file) => ({
|
|
2766
3213
|
filename: file.filename,
|
|
2767
3214
|
contentType: file.contentType,
|
|
@@ -2775,7 +3222,7 @@ ${items.map(summaryLine).join("\n")}`
|
|
|
2775
3222
|
subject: detail.subject,
|
|
2776
3223
|
from: `${detail.from.name} <${detail.from.address}>`.trim(),
|
|
2777
3224
|
date: stamp2(detail.date),
|
|
2778
|
-
text:
|
|
3225
|
+
text: clip2(detail.text),
|
|
2779
3226
|
attachments: detail.attachments.map((file) => ({
|
|
2780
3227
|
filename: file.filename,
|
|
2781
3228
|
contentType: file.contentType,
|
|
@@ -2904,10 +3351,18 @@ ${page.items.map(
|
|
|
2904
3351
|
// src/host/index.ts
|
|
2905
3352
|
var name = "mailharbor";
|
|
2906
3353
|
var inject = ["webServer", "tools"];
|
|
2907
|
-
var VERSION2 = "0.2.1";
|
|
2908
3354
|
var ROUTE = "/mail-plugin";
|
|
2909
3355
|
function apply(ctx) {
|
|
2910
|
-
const runtime = new MailHarborRuntime(
|
|
3356
|
+
const runtime = new MailHarborRuntime({
|
|
3357
|
+
llm: () => {
|
|
3358
|
+
if (typeof ctx.get !== "function") return void 0;
|
|
3359
|
+
try {
|
|
3360
|
+
return ctx.get.call(ctx, "llm");
|
|
3361
|
+
} catch {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
2911
3366
|
const handler = createApiHandler(runtime);
|
|
2912
3367
|
const disposers = [];
|
|
2913
3368
|
const start = () => {
|
|
@@ -2933,7 +3388,7 @@ function apply(ctx) {
|
|
|
2933
3388
|
}
|
|
2934
3389
|
}
|
|
2935
3390
|
export {
|
|
2936
|
-
|
|
3391
|
+
VERSION,
|
|
2937
3392
|
apply,
|
|
2938
3393
|
inject,
|
|
2939
3394
|
name
|