@zaclinley/dsh-mailharbor 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +7 -4
- package/README.zh.md +7 -4
- package/lib/client.js +1205 -779
- package/lib/client.js.map +4 -4
- package/lib/index.js +327 -15
- 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,9 @@ ${clip(sample.text, 800)}`).join("\n\n");
|
|
|
1697
1753
|
return catalog;
|
|
1698
1754
|
}
|
|
1699
1755
|
};
|
|
1756
|
+
function htmlToPlain(html) {
|
|
1757
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
1758
|
+
}
|
|
1700
1759
|
function summaryBlocks(text2) {
|
|
1701
1760
|
const blocks = [];
|
|
1702
1761
|
let current = { title: "", lines: [] };
|
|
@@ -1724,7 +1783,7 @@ function textToHtml(text2) {
|
|
|
1724
1783
|
}
|
|
1725
1784
|
|
|
1726
1785
|
// src/host/version.ts
|
|
1727
|
-
var VERSION = "0.
|
|
1786
|
+
var VERSION = "0.4.0";
|
|
1728
1787
|
|
|
1729
1788
|
// src/host/api.ts
|
|
1730
1789
|
init_smtp();
|
|
@@ -2022,6 +2081,108 @@ function buildRouter(runtime) {
|
|
|
2022
2081
|
}
|
|
2023
2082
|
return { ok: true, imap, smtp };
|
|
2024
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
|
+
});
|
|
2025
2186
|
router.get("/ai/status", ({ url }) => {
|
|
2026
2187
|
const requested = url.searchParams.get("accountId");
|
|
2027
2188
|
const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
|
|
@@ -2582,10 +2743,150 @@ var CredentialManager = class {
|
|
|
2582
2743
|
}
|
|
2583
2744
|
};
|
|
2584
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
|
+
|
|
2585
2886
|
// src/host/drafts.ts
|
|
2586
|
-
import { randomUUID as
|
|
2587
|
-
import { mkdirSync as
|
|
2588
|
-
import { dirname as
|
|
2887
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2888
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2889
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2589
2890
|
function text(value) {
|
|
2590
2891
|
return value === void 0 || value === null ? "" : String(value);
|
|
2591
2892
|
}
|
|
@@ -2609,18 +2910,18 @@ var DraftStore = class {
|
|
|
2609
2910
|
}
|
|
2610
2911
|
load() {
|
|
2611
2912
|
try {
|
|
2612
|
-
const parsed = JSON.parse(
|
|
2913
|
+
const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2613
2914
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2614
2915
|
} catch {
|
|
2615
2916
|
this.items = [];
|
|
2616
2917
|
}
|
|
2617
2918
|
}
|
|
2618
2919
|
flush() {
|
|
2619
|
-
|
|
2920
|
+
mkdirSync5(dirname3(this.file), { recursive: true });
|
|
2620
2921
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2621
|
-
|
|
2922
|
+
writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2622
2923
|
`, { encoding: "utf8", mode: 384 });
|
|
2623
|
-
|
|
2924
|
+
renameSync4(temp, this.file);
|
|
2624
2925
|
}
|
|
2625
2926
|
list() {
|
|
2626
2927
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2652,7 +2953,7 @@ var DraftStore = class {
|
|
|
2652
2953
|
return { ...existing };
|
|
2653
2954
|
}
|
|
2654
2955
|
const draft = {
|
|
2655
|
-
id:
|
|
2956
|
+
id: randomUUID3(),
|
|
2656
2957
|
accountId: this.accountId,
|
|
2657
2958
|
...fields,
|
|
2658
2959
|
updatedAt: now
|
|
@@ -2677,8 +2978,8 @@ var DraftStore = class {
|
|
|
2677
2978
|
init_imap();
|
|
2678
2979
|
|
|
2679
2980
|
// src/host/read-state.ts
|
|
2680
|
-
import { mkdirSync as
|
|
2681
|
-
import { dirname as
|
|
2981
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2982
|
+
import { dirname as dirname4 } from "node:path";
|
|
2682
2983
|
var ReadStateStore = class {
|
|
2683
2984
|
file;
|
|
2684
2985
|
keys = null;
|
|
@@ -2688,7 +2989,7 @@ var ReadStateStore = class {
|
|
|
2688
2989
|
load() {
|
|
2689
2990
|
if (this.keys !== null) return this.keys;
|
|
2690
2991
|
try {
|
|
2691
|
-
const parsed = JSON.parse(
|
|
2992
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
2692
2993
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
2693
2994
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
2694
2995
|
} catch {
|
|
@@ -2697,14 +2998,14 @@ var ReadStateStore = class {
|
|
|
2697
2998
|
return this.keys;
|
|
2698
2999
|
}
|
|
2699
3000
|
flush() {
|
|
2700
|
-
|
|
3001
|
+
mkdirSync6(dirname4(this.file), { recursive: true });
|
|
2701
3002
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2702
|
-
|
|
3003
|
+
writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
2703
3004
|
`, {
|
|
2704
3005
|
encoding: "utf8",
|
|
2705
3006
|
mode: 384
|
|
2706
3007
|
});
|
|
2707
|
-
|
|
3008
|
+
renameSync5(temp, this.file);
|
|
2708
3009
|
}
|
|
2709
3010
|
isUnread(key) {
|
|
2710
3011
|
return key !== "" && this.load().has(key);
|
|
@@ -2760,6 +3061,7 @@ var MailHarborRuntime = class {
|
|
|
2760
3061
|
databases = /* @__PURE__ */ new Map();
|
|
2761
3062
|
draftStores = /* @__PURE__ */ new Map();
|
|
2762
3063
|
readStores = /* @__PURE__ */ new Map();
|
|
3064
|
+
aiStores = /* @__PURE__ */ new Map();
|
|
2763
3065
|
backups = /* @__PURE__ */ new Map();
|
|
2764
3066
|
llmProvider;
|
|
2765
3067
|
aiService = null;
|
|
@@ -2823,6 +3125,7 @@ var MailHarborRuntime = class {
|
|
|
2823
3125
|
}
|
|
2824
3126
|
for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id);
|
|
2825
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);
|
|
2826
3129
|
}
|
|
2827
3130
|
/** Drop cached connections and stores for one account after a settings change. */
|
|
2828
3131
|
invalidate(accountId) {
|
|
@@ -2832,6 +3135,7 @@ var MailHarborRuntime = class {
|
|
|
2832
3135
|
this.databases.delete(accountId);
|
|
2833
3136
|
this.draftStores.delete(accountId);
|
|
2834
3137
|
this.readStores.delete(accountId);
|
|
3138
|
+
this.aiStores.delete(accountId);
|
|
2835
3139
|
this.refresh();
|
|
2836
3140
|
}
|
|
2837
3141
|
active() {
|
|
@@ -2890,6 +3194,14 @@ var MailHarborRuntime = class {
|
|
|
2890
3194
|
this.draftStores.set(account.id, { signature: file, value: created });
|
|
2891
3195
|
return created;
|
|
2892
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
|
+
}
|
|
2893
3205
|
readState(account) {
|
|
2894
3206
|
const file = join5(this.paths(account).root, "read-state.json");
|
|
2895
3207
|
const existing = this.readStores.get(account.id);
|