@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/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
- if (total === 0) return { total: 0, items: [] };
335
- const start = Math.max(1, total - limit + 1);
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 bodyText = String(row.body_text ?? "");
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(bodyText, max),
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(/&nbsp;/g, " ").replace(/&amp;/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("&", "&amp;").replaceAll("<", "&lt;").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(account);
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
- return { items: runtime.database(account).mailboxes() };
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 (source === "server") {
1526
- const items = await listServer(runtime, account, mailbox, query, limit);
1527
- return { ...items, offset: 0, limit };
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 db = runtime.database(account);
1530
- if (mailbox !== "all" && mailbox !== "") {
1531
- const page2 = db.list({ mailbox, query, offset, limit });
1532
- return { ...page2, items: page2.items.map((item) => localSummary(runtime, account, item)) };
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 = db.list({ mailbox: "all", query, offset, limit });
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
- const id = params.id ?? "";
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 { account } = accountFrom(runtime, url);
1621
- if (!runtime.hasCredentials(account)) {
1622
- return { unread: runtime.readState(account).count(), source: "local" };
1623
- }
1624
- try {
1625
- const provider = runtime.provider(account);
1626
- const result = await provider.unseen();
1627
- return { unread: result.count, source: "server" };
1628
- } catch {
1629
- return { unread: runtime.readState(account).count(), source: "local" };
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
- var VERSION = "0.1.0";
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 bodyText = signature === "" ? text2 : `${text2}
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: bodyText,
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 join4 } from "node:path";
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 mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
2162
- import { dirname, join as join3 } from "node:path";
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 join3(root, "drafts.json");
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(readFileSync3(this.file, "utf8"));
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
- mkdirSync3(dirname(this.file), { recursive: true });
2619
+ mkdirSync4(dirname2(this.file), { recursive: true });
2194
2620
  const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
2195
- writeFileSync3(temp, `${JSON.stringify(this.items, null, 2)}
2621
+ writeFileSync4(temp, `${JSON.stringify(this.items, null, 2)}
2196
2622
  `, { encoding: "utf8", mode: 384 });
2197
- renameSync2(temp, this.file);
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 mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2255
- import { dirname as dirname2 } from "node:path";
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(readFileSync4(this.file, "utf8"));
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
- mkdirSync4(dirname2(this.file), { recursive: true });
2700
+ mkdirSync5(dirname3(this.file), { recursive: true });
2275
2701
  const temp = `${this.file}.${String(process.pid)}.tmp`;
2276
- writeFileSync4(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
2702
+ writeFileSync5(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
2277
2703
  `, {
2278
2704
  encoding: "utf8",
2279
2705
  mode: 384
2280
2706
  });
2281
- renameSync3(temp, this.file);
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 = join4(this.paths(account).root, "read-state.json");
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 clip(text2, max = MAX_BODY) {
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: clip(detail2.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: clip(detail.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
- VERSION2 as VERSION,
3391
+ VERSION,
2937
3392
  apply,
2938
3393
  inject,
2939
3394
  name