@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/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,
@@ -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 bodyText = String(row.body_text ?? "");
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(bodyText, max),
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(/&nbsp;/g, " ").replace(/&amp;/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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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("&", "&amp;").replaceAll("<", "&lt;").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(account);
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
- return { items: runtime.database(account).mailboxes() };
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 (source === "server") {
1526
- const items = await listServer(runtime, account, mailbox, query, limit);
1527
- return { ...items, offset: 0, limit };
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 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)) };
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 = db.list({ mailbox: "all", query, offset, limit });
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
- 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);
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 { 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
- }
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
- var VERSION = "0.1.0";
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 bodyText = signature === "" ? text2 : `${text2}
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: bodyText,
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 join4 } from "node:path";
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 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";
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 join3(root, "drafts.json");
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(readFileSync3(this.file, "utf8"));
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
- mkdirSync3(dirname(this.file), { recursive: true });
2920
+ mkdirSync5(dirname3(this.file), { recursive: true });
2194
2921
  const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
2195
- writeFileSync3(temp, `${JSON.stringify(this.items, null, 2)}
2922
+ writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
2196
2923
  `, { encoding: "utf8", mode: 384 });
2197
- renameSync2(temp, this.file);
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: randomUUID2(),
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 mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2255
- import { dirname as dirname2 } from "node:path";
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(readFileSync4(this.file, "utf8"));
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
- mkdirSync4(dirname2(this.file), { recursive: true });
3001
+ mkdirSync6(dirname4(this.file), { recursive: true });
2275
3002
  const temp = `${this.file}.${String(process.pid)}.tmp`;
2276
- writeFileSync4(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
3003
+ writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
2277
3004
  `, {
2278
3005
  encoding: "utf8",
2279
3006
  mode: 384
2280
3007
  });
2281
- renameSync3(temp, this.file);
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 = join4(this.paths(account).root, "read-state.json");
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 clip(text2, max = MAX_BODY) {
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: clip(detail2.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: clip(detail.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
- VERSION2 as VERSION,
3703
+ VERSION,
2937
3704
  apply,
2938
3705
  inject,
2939
3706
  name