@zaclinley/dsh-mailharbor 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +7 -4
- package/README.zh.md +7 -4
- package/lib/client.js +1229 -783
- package/lib/client.js.map +4 -4
- package/lib/index.js +342 -17
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -469,6 +469,7 @@ var init_imap = __esm({
|
|
|
469
469
|
|
|
470
470
|
// src/host/api.ts
|
|
471
471
|
init_address();
|
|
472
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
472
473
|
|
|
473
474
|
// src/host/config.ts
|
|
474
475
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1503,6 +1504,36 @@ var TRANSLATE_SYSTEM = (target) => `\u4F60\u662F\u90AE\u4EF6\u7FFB\u8BD1\u3002\u
|
|
|
1503
1504
|
- \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784
|
|
1504
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
|
|
1505
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
|
+
};
|
|
1506
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
|
|
1507
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
|
|
1508
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`;
|
|
@@ -1664,6 +1695,31 @@ var AiService = class {
|
|
|
1664
1695
|
if (text2 === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1");
|
|
1665
1696
|
return this.complete(TRANSLATE_SYSTEM(target), clip(text2, 2e4), signal);
|
|
1666
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
|
+
}
|
|
1667
1723
|
async learnStyle(account, samples, signal) {
|
|
1668
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");
|
|
1669
1725
|
const digest = samples.slice(0, 100).map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}
|
|
@@ -1697,6 +1753,21 @@ ${clip(sample.text, 800)}`).join("\n\n");
|
|
|
1697
1753
|
return catalog;
|
|
1698
1754
|
}
|
|
1699
1755
|
};
|
|
1756
|
+
function normalizeTranslated(value) {
|
|
1757
|
+
const raw = value.trim();
|
|
1758
|
+
if (raw === "") return { text: "", html: "" };
|
|
1759
|
+
if (!/<[a-z][\s\S]*>/i.test(raw)) return { text: raw, html: textToHtml(raw) };
|
|
1760
|
+
let body = raw;
|
|
1761
|
+
const match = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(raw);
|
|
1762
|
+
if (match !== null) body = match[1] ?? "";
|
|
1763
|
+
body = body.replace(/<!doctype[^>]*>/gi, "").replace(/<head[\s\S]*?<\/head>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").trim();
|
|
1764
|
+
const text2 = htmlToPlain(body);
|
|
1765
|
+
if (body === "" || text2 === "") return { text: raw, html: textToHtml(raw) };
|
|
1766
|
+
return { text: text2, html: body };
|
|
1767
|
+
}
|
|
1768
|
+
function htmlToPlain(html) {
|
|
1769
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
1770
|
+
}
|
|
1700
1771
|
function summaryBlocks(text2) {
|
|
1701
1772
|
const blocks = [];
|
|
1702
1773
|
let current = { title: "", lines: [] };
|
|
@@ -1724,7 +1795,7 @@ function textToHtml(text2) {
|
|
|
1724
1795
|
}
|
|
1725
1796
|
|
|
1726
1797
|
// src/host/version.ts
|
|
1727
|
-
var VERSION = "0.
|
|
1798
|
+
var VERSION = "0.4.1";
|
|
1728
1799
|
|
|
1729
1800
|
// src/host/api.ts
|
|
1730
1801
|
init_smtp();
|
|
@@ -2022,6 +2093,109 @@ function buildRouter(runtime) {
|
|
|
2022
2093
|
}
|
|
2023
2094
|
return { ok: true, imap, smtp };
|
|
2024
2095
|
});
|
|
2096
|
+
router.get("/ai/thread", ({ url }) => {
|
|
2097
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2098
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2099
|
+
if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2100
|
+
return { thread: runtime.aiThreads(account).get(key) };
|
|
2101
|
+
});
|
|
2102
|
+
router.delete("/ai/thread", ({ url }) => {
|
|
2103
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2104
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2105
|
+
if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2106
|
+
return { ok: runtime.aiThreads(account).clear(key) };
|
|
2107
|
+
});
|
|
2108
|
+
router.delete("/ai/cards", ({ url }) => {
|
|
2109
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2110
|
+
const key = (url.searchParams.get("key") ?? "").trim();
|
|
2111
|
+
const cardId = (url.searchParams.get("cardId") ?? "").trim();
|
|
2112
|
+
if (key === "" || cardId === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id");
|
|
2113
|
+
return { thread: runtime.aiThreads(account).removeCard(key, cardId) };
|
|
2114
|
+
});
|
|
2115
|
+
router.post("/ai/cards", async ({ body }) => {
|
|
2116
|
+
const payload = await body();
|
|
2117
|
+
const account = accountFromPayload(runtime, payload);
|
|
2118
|
+
const key = String(payload.key ?? "").trim();
|
|
2119
|
+
if (key === "" || key.length > 200) throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
|
|
2120
|
+
const kind = String(payload.kind ?? "");
|
|
2121
|
+
if (!["summary", "reply", "translate", "answer"].includes(kind)) {
|
|
2122
|
+
throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`);
|
|
2123
|
+
}
|
|
2124
|
+
const detail = await loadDetail(
|
|
2125
|
+
runtime,
|
|
2126
|
+
account,
|
|
2127
|
+
sourceOfPayload(payload),
|
|
2128
|
+
String(payload.id ?? ""),
|
|
2129
|
+
mailboxOfPayload(payload)
|
|
2130
|
+
);
|
|
2131
|
+
const config = runtime.current;
|
|
2132
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2133
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2134
|
+
const target = payload.target === "en" ? "en" : "zh";
|
|
2135
|
+
const ai = runtime.ai();
|
|
2136
|
+
try {
|
|
2137
|
+
let output = "";
|
|
2138
|
+
let html;
|
|
2139
|
+
let meta;
|
|
2140
|
+
if (kind === "summary") {
|
|
2141
|
+
output = await ai.summarize(detail);
|
|
2142
|
+
} else if (kind === "reply") {
|
|
2143
|
+
output = await ai.reply(account, detail, { instruction, language });
|
|
2144
|
+
html = textToHtml(output);
|
|
2145
|
+
meta = {
|
|
2146
|
+
to: detail.from.address,
|
|
2147
|
+
subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,
|
|
2148
|
+
inReplyTo: detail.messageId ?? "",
|
|
2149
|
+
references: [detail.references.join(" "), detail.messageId ?? ""].join(" ").trim(),
|
|
2150
|
+
language
|
|
2151
|
+
};
|
|
2152
|
+
} else if (kind === "translate") {
|
|
2153
|
+
const source = detail.html !== null && detail.html !== "" ? detail.html : detail.text;
|
|
2154
|
+
const translated = normalizeTranslated(await ai.translate(source, target));
|
|
2155
|
+
output = translated.text;
|
|
2156
|
+
html = translated.html;
|
|
2157
|
+
meta = { target };
|
|
2158
|
+
} else {
|
|
2159
|
+
const earlier = runtime.aiThreads(account).get(key)?.cards.filter((card2) => card2.kind === "answer").map((card2) => card2.output) ?? [];
|
|
2160
|
+
output = await ai.ask(detail, instruction, earlier);
|
|
2161
|
+
}
|
|
2162
|
+
const card = {
|
|
2163
|
+
id: randomUUID2(),
|
|
2164
|
+
kind,
|
|
2165
|
+
instruction,
|
|
2166
|
+
output,
|
|
2167
|
+
...html === void 0 ? {} : { html },
|
|
2168
|
+
...meta === void 0 ? {} : { meta },
|
|
2169
|
+
provider: config.ai.provider,
|
|
2170
|
+
model: config.ai.model,
|
|
2171
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2172
|
+
};
|
|
2173
|
+
const thread = runtime.aiThreads(account).appendCard({
|
|
2174
|
+
key,
|
|
2175
|
+
accountId: account.id,
|
|
2176
|
+
subject: detail.subject,
|
|
2177
|
+
messageId: detail.messageId ?? "",
|
|
2178
|
+
card
|
|
2179
|
+
});
|
|
2180
|
+
return { card, thread };
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
throw aiHttpError(error);
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
router.post("/ai/compose", async ({ body }) => {
|
|
2186
|
+
const payload = await body();
|
|
2187
|
+
const account = accountFromPayload(runtime, payload);
|
|
2188
|
+
const mode = payload.mode === "polish" ? "polish" : payload.mode === "shorten" ? "shorten" : "draft";
|
|
2189
|
+
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2190
|
+
const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
|
|
2191
|
+
const language = payload.language === "en" ? "en" : "zh";
|
|
2192
|
+
try {
|
|
2193
|
+
const text2 = await runtime.ai().compose(account, { mode, instruction, html, language });
|
|
2194
|
+
return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
throw aiHttpError(error);
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2025
2199
|
router.get("/ai/status", ({ url }) => {
|
|
2026
2200
|
const requested = url.searchParams.get("accountId");
|
|
2027
2201
|
const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
|
|
@@ -2069,8 +2243,8 @@ function buildRouter(runtime) {
|
|
|
2069
2243
|
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2070
2244
|
const target = payload.target === "en" ? "en" : "zh";
|
|
2071
2245
|
try {
|
|
2072
|
-
const
|
|
2073
|
-
return { text:
|
|
2246
|
+
const translated = normalizeTranslated(await runtime.ai().translate(html, target));
|
|
2247
|
+
return { text: translated.text, html: translated.html };
|
|
2074
2248
|
} catch (error) {
|
|
2075
2249
|
throw aiHttpError(error);
|
|
2076
2250
|
}
|
|
@@ -2582,10 +2756,150 @@ var CredentialManager = class {
|
|
|
2582
2756
|
}
|
|
2583
2757
|
};
|
|
2584
2758
|
|
|
2759
|
+
// src/host/ai-store.ts
|
|
2760
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2761
|
+
import { dirname as dirname2 } from "node:path";
|
|
2762
|
+
var MAX_THREADS = 50;
|
|
2763
|
+
var MAX_CARDS_PER_THREAD = 50;
|
|
2764
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
2765
|
+
var AiThreadStore = class {
|
|
2766
|
+
file;
|
|
2767
|
+
cache = null;
|
|
2768
|
+
constructor(file) {
|
|
2769
|
+
this.file = file;
|
|
2770
|
+
}
|
|
2771
|
+
load() {
|
|
2772
|
+
if (this.cache !== null) return this.cache;
|
|
2773
|
+
let parsed = null;
|
|
2774
|
+
try {
|
|
2775
|
+
if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
|
|
2776
|
+
} catch {
|
|
2777
|
+
parsed = null;
|
|
2778
|
+
}
|
|
2779
|
+
const threads = parsed !== null && typeof parsed === "object" && parsed.threads !== void 0 ? parsed.threads : {};
|
|
2780
|
+
this.cache = { version: 1, threads: sanitize(threads) };
|
|
2781
|
+
return this.cache;
|
|
2782
|
+
}
|
|
2783
|
+
flush() {
|
|
2784
|
+
const data = this.load();
|
|
2785
|
+
prune(data);
|
|
2786
|
+
const text2 = `${JSON.stringify(data, null, 2)}
|
|
2787
|
+
`;
|
|
2788
|
+
mkdirSync4(dirname2(this.file), { recursive: true });
|
|
2789
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2790
|
+
writeFileSync4(temp, text2, { mode: 384 });
|
|
2791
|
+
renameSync3(temp, this.file);
|
|
2792
|
+
}
|
|
2793
|
+
get(key) {
|
|
2794
|
+
const thread = this.load().threads[key];
|
|
2795
|
+
return thread === void 0 ? null : { ...thread, cards: [...thread.cards] };
|
|
2796
|
+
}
|
|
2797
|
+
appendCard(input) {
|
|
2798
|
+
const data = this.load();
|
|
2799
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2800
|
+
const existing = data.threads[input.key];
|
|
2801
|
+
const thread = existing ?? {
|
|
2802
|
+
key: input.key,
|
|
2803
|
+
accountId: input.accountId,
|
|
2804
|
+
subject: input.subject,
|
|
2805
|
+
messageId: input.messageId ?? "",
|
|
2806
|
+
cards: [],
|
|
2807
|
+
updatedAt: now
|
|
2808
|
+
};
|
|
2809
|
+
thread.accountId = input.accountId;
|
|
2810
|
+
thread.subject = input.subject;
|
|
2811
|
+
if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
|
|
2812
|
+
thread.cards.push(input.card);
|
|
2813
|
+
thread.updatedAt = now;
|
|
2814
|
+
data.threads[input.key] = thread;
|
|
2815
|
+
this.flush();
|
|
2816
|
+
return { ...thread, cards: [...thread.cards] };
|
|
2817
|
+
}
|
|
2818
|
+
removeCard(key, cardId) {
|
|
2819
|
+
const data = this.load();
|
|
2820
|
+
const thread = data.threads[key];
|
|
2821
|
+
if (thread === void 0) return null;
|
|
2822
|
+
thread.cards = thread.cards.filter((card) => card.id !== cardId);
|
|
2823
|
+
thread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2824
|
+
if (thread.cards.length === 0) {
|
|
2825
|
+
delete data.threads[key];
|
|
2826
|
+
this.flush();
|
|
2827
|
+
return null;
|
|
2828
|
+
}
|
|
2829
|
+
this.flush();
|
|
2830
|
+
return { ...thread, cards: [...thread.cards] };
|
|
2831
|
+
}
|
|
2832
|
+
clear(key) {
|
|
2833
|
+
const data = this.load();
|
|
2834
|
+
if (data.threads[key] === void 0) return false;
|
|
2835
|
+
delete data.threads[key];
|
|
2836
|
+
this.flush();
|
|
2837
|
+
return true;
|
|
2838
|
+
}
|
|
2839
|
+
/** Number of threads, for the settings page. */
|
|
2840
|
+
size() {
|
|
2841
|
+
return Object.keys(this.load().threads).length;
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
function sanitize(input) {
|
|
2845
|
+
if (input === null || typeof input !== "object") return {};
|
|
2846
|
+
const source = input;
|
|
2847
|
+
const result = {};
|
|
2848
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
2849
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
2850
|
+
const thread = raw;
|
|
2851
|
+
if (typeof thread.accountId !== "string" || !Array.isArray(thread.cards)) continue;
|
|
2852
|
+
const cards = thread.cards.filter(
|
|
2853
|
+
(card) => card !== null && typeof card === "object" && typeof card.id === "string"
|
|
2854
|
+
).map((card) => ({
|
|
2855
|
+
id: card.id,
|
|
2856
|
+
kind: card.kind,
|
|
2857
|
+
instruction: String(card.instruction ?? ""),
|
|
2858
|
+
output: String(card.output ?? ""),
|
|
2859
|
+
...typeof card.html === "string" ? { html: card.html } : {},
|
|
2860
|
+
...card.meta !== void 0 ? { meta: card.meta } : {},
|
|
2861
|
+
provider: String(card.provider ?? ""),
|
|
2862
|
+
model: String(card.model ?? ""),
|
|
2863
|
+
createdAt: String(card.createdAt ?? "")
|
|
2864
|
+
}));
|
|
2865
|
+
result[key] = {
|
|
2866
|
+
key,
|
|
2867
|
+
accountId: thread.accountId,
|
|
2868
|
+
subject: String(thread.subject ?? ""),
|
|
2869
|
+
messageId: String(thread.messageId ?? ""),
|
|
2870
|
+
cards,
|
|
2871
|
+
updatedAt: String(thread.updatedAt ?? "")
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
return result;
|
|
2875
|
+
}
|
|
2876
|
+
function prune(data) {
|
|
2877
|
+
const entries = Object.entries(data.threads).sort(
|
|
2878
|
+
(left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt)
|
|
2879
|
+
);
|
|
2880
|
+
const kept = new Map(entries.slice(0, MAX_THREADS));
|
|
2881
|
+
for (const thread of kept.values()) {
|
|
2882
|
+
if (thread.cards.length > MAX_CARDS_PER_THREAD) {
|
|
2883
|
+
thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
data.threads = Object.fromEntries(kept);
|
|
2887
|
+
let size = JSON.stringify(data).length;
|
|
2888
|
+
while (size > MAX_FILE_BYTES && kept.size > 1) {
|
|
2889
|
+
const oldest = [...kept.entries()].sort(
|
|
2890
|
+
(left, right) => left[1].updatedAt.localeCompare(right[1].updatedAt)
|
|
2891
|
+
)[0];
|
|
2892
|
+
if (oldest === void 0) break;
|
|
2893
|
+
kept.delete(oldest[0]);
|
|
2894
|
+
data.threads = Object.fromEntries(kept);
|
|
2895
|
+
size = JSON.stringify(data).length;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2585
2899
|
// src/host/drafts.ts
|
|
2586
|
-
import { randomUUID as
|
|
2587
|
-
import { mkdirSync as
|
|
2588
|
-
import { dirname as
|
|
2900
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2901
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2902
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2589
2903
|
function text(value) {
|
|
2590
2904
|
return value === void 0 || value === null ? "" : String(value);
|
|
2591
2905
|
}
|
|
@@ -2609,18 +2923,18 @@ var DraftStore = class {
|
|
|
2609
2923
|
}
|
|
2610
2924
|
load() {
|
|
2611
2925
|
try {
|
|
2612
|
-
const parsed = JSON.parse(
|
|
2926
|
+
const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2613
2927
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2614
2928
|
} catch {
|
|
2615
2929
|
this.items = [];
|
|
2616
2930
|
}
|
|
2617
2931
|
}
|
|
2618
2932
|
flush() {
|
|
2619
|
-
|
|
2933
|
+
mkdirSync5(dirname3(this.file), { recursive: true });
|
|
2620
2934
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2621
|
-
|
|
2935
|
+
writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2622
2936
|
`, { encoding: "utf8", mode: 384 });
|
|
2623
|
-
|
|
2937
|
+
renameSync4(temp, this.file);
|
|
2624
2938
|
}
|
|
2625
2939
|
list() {
|
|
2626
2940
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2652,7 +2966,7 @@ var DraftStore = class {
|
|
|
2652
2966
|
return { ...existing };
|
|
2653
2967
|
}
|
|
2654
2968
|
const draft = {
|
|
2655
|
-
id:
|
|
2969
|
+
id: randomUUID3(),
|
|
2656
2970
|
accountId: this.accountId,
|
|
2657
2971
|
...fields,
|
|
2658
2972
|
updatedAt: now
|
|
@@ -2677,8 +2991,8 @@ var DraftStore = class {
|
|
|
2677
2991
|
init_imap();
|
|
2678
2992
|
|
|
2679
2993
|
// src/host/read-state.ts
|
|
2680
|
-
import { mkdirSync as
|
|
2681
|
-
import { dirname as
|
|
2994
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2995
|
+
import { dirname as dirname4 } from "node:path";
|
|
2682
2996
|
var ReadStateStore = class {
|
|
2683
2997
|
file;
|
|
2684
2998
|
keys = null;
|
|
@@ -2688,7 +3002,7 @@ var ReadStateStore = class {
|
|
|
2688
3002
|
load() {
|
|
2689
3003
|
if (this.keys !== null) return this.keys;
|
|
2690
3004
|
try {
|
|
2691
|
-
const parsed = JSON.parse(
|
|
3005
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
2692
3006
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
2693
3007
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
2694
3008
|
} catch {
|
|
@@ -2697,14 +3011,14 @@ var ReadStateStore = class {
|
|
|
2697
3011
|
return this.keys;
|
|
2698
3012
|
}
|
|
2699
3013
|
flush() {
|
|
2700
|
-
|
|
3014
|
+
mkdirSync6(dirname4(this.file), { recursive: true });
|
|
2701
3015
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2702
|
-
|
|
3016
|
+
writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
2703
3017
|
`, {
|
|
2704
3018
|
encoding: "utf8",
|
|
2705
3019
|
mode: 384
|
|
2706
3020
|
});
|
|
2707
|
-
|
|
3021
|
+
renameSync5(temp, this.file);
|
|
2708
3022
|
}
|
|
2709
3023
|
isUnread(key) {
|
|
2710
3024
|
return key !== "" && this.load().has(key);
|
|
@@ -2760,6 +3074,7 @@ var MailHarborRuntime = class {
|
|
|
2760
3074
|
databases = /* @__PURE__ */ new Map();
|
|
2761
3075
|
draftStores = /* @__PURE__ */ new Map();
|
|
2762
3076
|
readStores = /* @__PURE__ */ new Map();
|
|
3077
|
+
aiStores = /* @__PURE__ */ new Map();
|
|
2763
3078
|
backups = /* @__PURE__ */ new Map();
|
|
2764
3079
|
llmProvider;
|
|
2765
3080
|
aiService = null;
|
|
@@ -2823,6 +3138,7 @@ var MailHarborRuntime = class {
|
|
|
2823
3138
|
}
|
|
2824
3139
|
for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id);
|
|
2825
3140
|
for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id);
|
|
3141
|
+
for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id);
|
|
2826
3142
|
}
|
|
2827
3143
|
/** Drop cached connections and stores for one account after a settings change. */
|
|
2828
3144
|
invalidate(accountId) {
|
|
@@ -2832,6 +3148,7 @@ var MailHarborRuntime = class {
|
|
|
2832
3148
|
this.databases.delete(accountId);
|
|
2833
3149
|
this.draftStores.delete(accountId);
|
|
2834
3150
|
this.readStores.delete(accountId);
|
|
3151
|
+
this.aiStores.delete(accountId);
|
|
2835
3152
|
this.refresh();
|
|
2836
3153
|
}
|
|
2837
3154
|
active() {
|
|
@@ -2890,6 +3207,14 @@ var MailHarborRuntime = class {
|
|
|
2890
3207
|
this.draftStores.set(account.id, { signature: file, value: created });
|
|
2891
3208
|
return created;
|
|
2892
3209
|
}
|
|
3210
|
+
aiThreads(account) {
|
|
3211
|
+
const file = join5(this.paths(account).root, "ai-threads.json");
|
|
3212
|
+
const existing = this.aiStores.get(account.id);
|
|
3213
|
+
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3214
|
+
const created = new AiThreadStore(file);
|
|
3215
|
+
this.aiStores.set(account.id, { signature: file, value: created });
|
|
3216
|
+
return created;
|
|
3217
|
+
}
|
|
2893
3218
|
readState(account) {
|
|
2894
3219
|
const file = join5(this.paths(account).root, "read-state.json");
|
|
2895
3220
|
const existing = this.readStores.get(account.id);
|