@zaclinley/dsh-mailharbor 0.4.0 → 0.5.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 +28 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +2050 -749
- package/lib/client.js.map +4 -4
- package/lib/index.js +1296 -44
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -469,11 +469,13 @@ var init_imap = __esm({
|
|
|
469
469
|
|
|
470
470
|
// src/host/api.ts
|
|
471
471
|
init_address();
|
|
472
|
-
import { randomUUID as
|
|
472
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
473
473
|
|
|
474
474
|
// src/host/config.ts
|
|
475
475
|
import { createHash, randomUUID } from "node:crypto";
|
|
476
476
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
477
|
+
import { homedir as homedir2 } from "node:os";
|
|
478
|
+
import { join as join2 } from "node:path";
|
|
477
479
|
|
|
478
480
|
// src/host/paths.ts
|
|
479
481
|
import { homedir } from "node:os";
|
|
@@ -484,6 +486,14 @@ var ACCOUNTS_DIR = join(DATA_ROOT, "accounts");
|
|
|
484
486
|
var STATE_DIR = join(DATA_ROOT, "state");
|
|
485
487
|
var REPORTS_DIR = join(DATA_ROOT, "reports");
|
|
486
488
|
var LOGS_DIR = join(DATA_ROOT, "logs");
|
|
489
|
+
function resolveUserPath(value, label) {
|
|
490
|
+
const text2 = value.trim();
|
|
491
|
+
if (text2 === "") throw new Error(`${label}\u4E0D\u80FD\u4E3A\u7A7A`);
|
|
492
|
+
if (text2.length > 4096) throw new Error(`${label}\u8FC7\u957F`);
|
|
493
|
+
const expanded = text2 === "~" ? homedir() : text2.startsWith("~/") ? join(homedir(), text2.slice(2)) : text2;
|
|
494
|
+
if (!isAbsolute(expanded)) throw new Error(`${label}\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84`);
|
|
495
|
+
return expanded;
|
|
496
|
+
}
|
|
487
497
|
function accountRoot(id) {
|
|
488
498
|
const safe = id.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
489
499
|
return join(ACCOUNTS_DIR, safe);
|
|
@@ -512,6 +522,7 @@ var DEFAULTS = {
|
|
|
512
522
|
},
|
|
513
523
|
read: { markOnOpen: true, pollSeconds: 60 },
|
|
514
524
|
render: { listLimit: 40 },
|
|
525
|
+
projects: { root: join2(homedir2(), "work", "projects") },
|
|
515
526
|
oauth: {
|
|
516
527
|
google: { clientId: "", redirectPort: 8765, scopes: ["https://mail.google.com/"] },
|
|
517
528
|
microsoft: {
|
|
@@ -625,6 +636,7 @@ function loadConfig() {
|
|
|
625
636
|
const aiRaw = file.ai ?? {};
|
|
626
637
|
const readRaw = file.read ?? {};
|
|
627
638
|
const renderRaw = file.render ?? {};
|
|
639
|
+
const projectsRaw = file.projects ?? {};
|
|
628
640
|
const oauthRaw = file.oauth ?? {};
|
|
629
641
|
const googleRaw = oauthRaw.google ?? {};
|
|
630
642
|
const microsoftRaw = oauthRaw.microsoft ?? {};
|
|
@@ -648,6 +660,9 @@ function loadConfig() {
|
|
|
648
660
|
render: {
|
|
649
661
|
listLimit: intOr(renderRaw.listLimit, DEFAULTS.render.listLimit, 10, 200)
|
|
650
662
|
},
|
|
663
|
+
projects: {
|
|
664
|
+
root: stringOr(projectsRaw.root, DEFAULTS.projects.root)
|
|
665
|
+
},
|
|
651
666
|
oauth: {
|
|
652
667
|
google: {
|
|
653
668
|
clientId: stringOr(googleRaw.clientId, DEFAULTS.oauth.google.clientId),
|
|
@@ -731,7 +746,7 @@ function activeAccount(config) {
|
|
|
731
746
|
init_address();
|
|
732
747
|
import { createHash as createHash2 } from "node:crypto";
|
|
733
748
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
734
|
-
import { join as
|
|
749
|
+
import { join as join3 } from "node:path";
|
|
735
750
|
import { ImapFlow } from "imapflow";
|
|
736
751
|
import { simpleParser } from "mailparser";
|
|
737
752
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
@@ -961,6 +976,14 @@ var MailDatabase = class {
|
|
|
961
976
|
data
|
|
962
977
|
};
|
|
963
978
|
}
|
|
979
|
+
/** Locate a local row by Message-ID, for links from tasks and projects. */
|
|
980
|
+
findByMessageId(messageId) {
|
|
981
|
+
const id = messageId.trim().replace(/^<|>$/g, "");
|
|
982
|
+
if (id === "") return void 0;
|
|
983
|
+
const row = this.open().prepare("SELECT id, mailbox FROM emails WHERE message_id = ? LIMIT 1").get(id);
|
|
984
|
+
if (row === void 0) return void 0;
|
|
985
|
+
return { id: Number(row.id), mailbox: String(row.mailbox ?? "") };
|
|
986
|
+
}
|
|
964
987
|
/** Whether the backup already holds a message with this Message-ID. */
|
|
965
988
|
hasMessageId(messageId) {
|
|
966
989
|
const id = messageId.trim().replace(/^<|>$/g, "");
|
|
@@ -1058,8 +1081,8 @@ function normalizeId(value) {
|
|
|
1058
1081
|
function stamp(value) {
|
|
1059
1082
|
const date = value instanceof Date ? value : value === void 0 || value === null ? void 0 : new Date(String(value));
|
|
1060
1083
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return "";
|
|
1061
|
-
const
|
|
1062
|
-
return `${String(date.getFullYear())}-${
|
|
1084
|
+
const pad2 = (number) => String(number).padStart(2, "0");
|
|
1085
|
+
return `${String(date.getFullYear())}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
|
|
1063
1086
|
}
|
|
1064
1087
|
function abortIfNeeded(signal) {
|
|
1065
1088
|
if (signal?.aborted === true) {
|
|
@@ -1090,8 +1113,8 @@ var BackupEngine = class {
|
|
|
1090
1113
|
}
|
|
1091
1114
|
writeAttachment(data) {
|
|
1092
1115
|
const hash = sha256(data);
|
|
1093
|
-
const dir =
|
|
1094
|
-
const file =
|
|
1116
|
+
const dir = join3(this.attachmentsDir(), hash.slice(0, 2), hash.slice(2, 4));
|
|
1117
|
+
const file = join3(dir, hash);
|
|
1095
1118
|
mkdirSync2(dir, { recursive: true });
|
|
1096
1119
|
if (!existsSync3(file)) writeFileSync2(file, data, { mode: 384 });
|
|
1097
1120
|
return { hash, size: data.length };
|
|
@@ -1469,7 +1492,7 @@ var Router = class {
|
|
|
1469
1492
|
|
|
1470
1493
|
// src/host/ai.ts
|
|
1471
1494
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1472
|
-
import { dirname, join as
|
|
1495
|
+
import { dirname, join as join4 } from "node:path";
|
|
1473
1496
|
var TIMEOUT_MS = 9e4;
|
|
1474
1497
|
var MAX_PROMPT_CHARS = 4e4;
|
|
1475
1498
|
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
|
|
@@ -1534,11 +1557,21 @@ ${shared}${styleLine}`;
|
|
|
1534
1557
|
\u8981\u6C42\uFF1A
|
|
1535
1558
|
${shared}${styleLine}`;
|
|
1536
1559
|
};
|
|
1560
|
+
var TASK_SYSTEM = `\u4F60\u662F\u4EFB\u52A1\u63D0\u53D6\u52A9\u624B\u3002\u4ECE\u90AE\u4EF6\u4E2D\u63D0\u53D6\u9700\u8981\u7528\u6237\u5904\u7406\u7684\u5177\u4F53\u4E8B\u9879\u3002
|
|
1561
|
+
|
|
1562
|
+
\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001Markdown \u6216\u4EE3\u7801\u5757\u6807\u8BB0\u3002\u6BCF\u4E2A\u5143\u7D20\uFF1A
|
|
1563
|
+
{"title": "\u4EFB\u52A1\u6807\u9898\uFF08\u52A8\u8BCD\u5F00\u5934\uFF0C\u5C3D\u91CF\u5177\u4F53\uFF09", "due": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "priority": 0 \u5230 9 \u7684\u6574\u6570, "owner": "\u8D1F\u8D23\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32", "notes": "\u4E00\u53E5\u6765\u6E90\u4F9D\u636E"}
|
|
1564
|
+
|
|
1565
|
+
\u89C4\u5219\uFF1A
|
|
1566
|
+
- \u53EA\u63D0\u53D6\u90AE\u4EF6\u91CC\u660E\u786E\u8981\u6C42\u3001\u6216\u660E\u663E\u9700\u8981\u8DDF\u8FDB\u7684\u4E8B\u9879
|
|
1567
|
+
- \u622A\u6B62\u65F6\u95F4\u80FD\u786E\u5B9A\u624D\u586B\uFF0C\u5426\u5219\u7559\u7A7A
|
|
1568
|
+
- \u6CA1\u6709\u53EF\u6267\u884C\u4E8B\u9879\u65F6\u8F93\u51FA []
|
|
1569
|
+
- \u4E0D\u8981\u7F16\u9020\u90AE\u4EF6\u91CC\u6CA1\u6709\u7684\u4FE1\u606F`;
|
|
1537
1570
|
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
1571
|
\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
1572
|
\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
1573
|
function stylePath(root) {
|
|
1541
|
-
return
|
|
1574
|
+
return join4(root, "style.json");
|
|
1542
1575
|
}
|
|
1543
1576
|
function readStyle(root) {
|
|
1544
1577
|
try {
|
|
@@ -1710,6 +1743,11 @@ ${clip(item, 4e3)}`).join("\n\n");
|
|
|
1710
1743
|
].join("\n");
|
|
1711
1744
|
return this.complete(ASK_SYSTEM, user, signal);
|
|
1712
1745
|
}
|
|
1746
|
+
/** Extract actionable tasks from one mail. */
|
|
1747
|
+
async extractTasks(message, signal) {
|
|
1748
|
+
const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
|
|
1749
|
+
return parseTaskJson(text2);
|
|
1750
|
+
}
|
|
1713
1751
|
/** Compose-body assistance: draft from points, polish, or shorten. */
|
|
1714
1752
|
async compose(account, options, signal) {
|
|
1715
1753
|
const style = this.style(account)?.profile ?? "";
|
|
@@ -1753,9 +1791,48 @@ ${clip(sample.text, 800)}`).join("\n\n");
|
|
|
1753
1791
|
return catalog;
|
|
1754
1792
|
}
|
|
1755
1793
|
};
|
|
1794
|
+
function normalizeTranslated(value) {
|
|
1795
|
+
const raw = value.trim();
|
|
1796
|
+
if (raw === "") return { text: "", html: "" };
|
|
1797
|
+
if (!/<[a-z][\s\S]*>/i.test(raw)) return { text: raw, html: textToHtml(raw) };
|
|
1798
|
+
let body = raw;
|
|
1799
|
+
const match = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(raw);
|
|
1800
|
+
if (match !== null) body = match[1] ?? "";
|
|
1801
|
+
body = body.replace(/<!doctype[^>]*>/gi, "").replace(/<head[\s\S]*?<\/head>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").trim();
|
|
1802
|
+
const text2 = htmlToPlain(body);
|
|
1803
|
+
if (body === "" || text2 === "") return { text: raw, html: textToHtml(raw) };
|
|
1804
|
+
return { text: text2, html: body };
|
|
1805
|
+
}
|
|
1756
1806
|
function htmlToPlain(html) {
|
|
1757
1807
|
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
1808
|
}
|
|
1809
|
+
function parseTaskJson(text2) {
|
|
1810
|
+
const start = text2.indexOf("[");
|
|
1811
|
+
const end = text2.lastIndexOf("]");
|
|
1812
|
+
if (start < 0 || end <= start) return [];
|
|
1813
|
+
let parsed;
|
|
1814
|
+
try {
|
|
1815
|
+
parsed = JSON.parse(text2.slice(start, end + 1));
|
|
1816
|
+
} catch {
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
if (!Array.isArray(parsed)) return [];
|
|
1820
|
+
const result = [];
|
|
1821
|
+
for (const item of parsed) {
|
|
1822
|
+
if (item === null || typeof item !== "object") continue;
|
|
1823
|
+
const value = item;
|
|
1824
|
+
const title = String(value.title ?? "").trim();
|
|
1825
|
+
if (title === "") continue;
|
|
1826
|
+
result.push({
|
|
1827
|
+
title: title.slice(0, 300),
|
|
1828
|
+
due: /^\d{4}-\d{2}-\d{2}/.test(String(value.due ?? "")) ? String(value.due).slice(0, 10) : "",
|
|
1829
|
+
priority: Math.max(0, Math.min(9, Number(value.priority ?? 0) || 0)),
|
|
1830
|
+
owner: String(value.owner ?? "").slice(0, 120),
|
|
1831
|
+
notes: String(value.notes ?? "").slice(0, 1e3)
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return result.slice(0, 30);
|
|
1835
|
+
}
|
|
1759
1836
|
function summaryBlocks(text2) {
|
|
1760
1837
|
const blocks = [];
|
|
1761
1838
|
let current = { title: "", lines: [] };
|
|
@@ -1782,8 +1859,578 @@ function textToHtml(text2) {
|
|
|
1782
1859
|
).join("");
|
|
1783
1860
|
}
|
|
1784
1861
|
|
|
1862
|
+
// src/host/ics.ts
|
|
1863
|
+
var CRLF = "\r\n";
|
|
1864
|
+
function escapeText(value) {
|
|
1865
|
+
return String(value ?? "").replaceAll("\\", "\\\\").replaceAll(";", "\\;").replaceAll(",", "\\,").replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
1866
|
+
}
|
|
1867
|
+
function unescapeText(value) {
|
|
1868
|
+
return String(value ?? "").replaceAll("\\n", "\n").replaceAll("\\N", "\n").replaceAll("\\,", ",").replaceAll("\\;", ";").replaceAll("\\\\", "\\");
|
|
1869
|
+
}
|
|
1870
|
+
function fold(line2) {
|
|
1871
|
+
const text2 = String(line2);
|
|
1872
|
+
if (text2.length <= 74) return text2;
|
|
1873
|
+
const chunks = [];
|
|
1874
|
+
let rest = text2;
|
|
1875
|
+
while (rest.length > 74) {
|
|
1876
|
+
chunks.push(rest.slice(0, 74));
|
|
1877
|
+
rest = ` ${rest.slice(74)}`;
|
|
1878
|
+
}
|
|
1879
|
+
chunks.push(rest);
|
|
1880
|
+
return chunks.join(CRLF);
|
|
1881
|
+
}
|
|
1882
|
+
function utcStamp(date) {
|
|
1883
|
+
const pad2 = (value) => String(value).padStart(2, "0");
|
|
1884
|
+
return `${String(date.getUTCFullYear())}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}T${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
|
|
1885
|
+
}
|
|
1886
|
+
function pad(value) {
|
|
1887
|
+
return String(value).padStart(2, "0");
|
|
1888
|
+
}
|
|
1889
|
+
function dueProperty(due) {
|
|
1890
|
+
const text2 = String(due ?? "").trim();
|
|
1891
|
+
if (text2 === "") return null;
|
|
1892
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text2)) return `DUE;VALUE=DATE:${text2.replaceAll("-", "")}`;
|
|
1893
|
+
const parsed = new Date(text2);
|
|
1894
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
1895
|
+
return `DUE:${utcStamp(parsed)}`;
|
|
1896
|
+
}
|
|
1897
|
+
function serializeTodos(tasks) {
|
|
1898
|
+
const lines = [
|
|
1899
|
+
"BEGIN:VCALENDAR",
|
|
1900
|
+
"VERSION:2.0",
|
|
1901
|
+
"PRODID:-//dsh-mailharbor//VTODO//EN",
|
|
1902
|
+
"CALSCALE:GREGORIAN"
|
|
1903
|
+
];
|
|
1904
|
+
for (const task of tasks) {
|
|
1905
|
+
lines.push("BEGIN:VTODO");
|
|
1906
|
+
lines.push(`UID:${String(task.uid ?? task.id ?? "")}`);
|
|
1907
|
+
lines.push(`DTSTAMP:${utcStamp(/* @__PURE__ */ new Date())}`);
|
|
1908
|
+
lines.push(`SUMMARY:${escapeText(task.title ?? "(\u65E0\u6807\u9898)")}`);
|
|
1909
|
+
if (task.notes !== void 0 && task.notes !== null && String(task.notes) !== "") {
|
|
1910
|
+
lines.push(`DESCRIPTION:${escapeText(task.notes)}`);
|
|
1911
|
+
}
|
|
1912
|
+
const due = dueProperty(String(task.due ?? ""));
|
|
1913
|
+
if (due !== null) lines.push(due);
|
|
1914
|
+
lines.push(`STATUS:${String(task.status ?? "NEEDS-ACTION").toUpperCase()}`);
|
|
1915
|
+
if (Number(task.priority ?? 0) > 0)
|
|
1916
|
+
lines.push(`PRIORITY:${String(Math.max(0, Math.min(9, Number(task.priority))))}`);
|
|
1917
|
+
if (String(task.status ?? "").toUpperCase() === "COMPLETED") {
|
|
1918
|
+
const completed = task.completedAt === void 0 || task.completedAt === null ? /* @__PURE__ */ new Date() : new Date(task.completedAt);
|
|
1919
|
+
if (!Number.isNaN(completed.getTime())) lines.push(`COMPLETED:${utcStamp(completed)}`);
|
|
1920
|
+
}
|
|
1921
|
+
lines.push("END:VTODO");
|
|
1922
|
+
}
|
|
1923
|
+
lines.push("END:VCALENDAR");
|
|
1924
|
+
return `${lines.map(fold).join(CRLF)}${CRLF}`;
|
|
1925
|
+
}
|
|
1926
|
+
function unfold(text2) {
|
|
1927
|
+
return String(text2 ?? "").replace(/\r?\n[ \t]/g, "");
|
|
1928
|
+
}
|
|
1929
|
+
function decodeDue(value, params) {
|
|
1930
|
+
const text2 = String(value ?? "").trim();
|
|
1931
|
+
if (params.includes("VALUE=DATE") || /^\d{8}$/.test(text2)) {
|
|
1932
|
+
if (/^\d{8}$/.test(text2)) return `${text2.slice(0, 4)}-${text2.slice(4, 6)}-${text2.slice(6, 8)}`;
|
|
1933
|
+
return "";
|
|
1934
|
+
}
|
|
1935
|
+
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(text2);
|
|
1936
|
+
if (match !== null) {
|
|
1937
|
+
const date = new Date(
|
|
1938
|
+
Date.UTC(
|
|
1939
|
+
Number(match[1]),
|
|
1940
|
+
Number(match[2]) - 1,
|
|
1941
|
+
Number(match[3]),
|
|
1942
|
+
Number(match[4]),
|
|
1943
|
+
Number(match[5]),
|
|
1944
|
+
Number(match[6])
|
|
1945
|
+
)
|
|
1946
|
+
);
|
|
1947
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1948
|
+
}
|
|
1949
|
+
return "";
|
|
1950
|
+
}
|
|
1951
|
+
function parseTodos(ics) {
|
|
1952
|
+
const lines = unfold(ics).split(/\r?\n/);
|
|
1953
|
+
const todos = [];
|
|
1954
|
+
let current = null;
|
|
1955
|
+
for (const line2 of lines) {
|
|
1956
|
+
const trimmed = line2.trim();
|
|
1957
|
+
if (trimmed === "BEGIN:VTODO") {
|
|
1958
|
+
current = {};
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (trimmed === "END:VTODO") {
|
|
1962
|
+
if (current !== null && String(current.uid ?? "") !== "") {
|
|
1963
|
+
todos.push({
|
|
1964
|
+
uid: String(current.uid),
|
|
1965
|
+
title: String(current.title ?? "(\u65E0\u6807\u9898)"),
|
|
1966
|
+
notes: String(current.notes ?? ""),
|
|
1967
|
+
due: String(current.due ?? ""),
|
|
1968
|
+
status: String(current.status ?? "NEEDS-ACTION"),
|
|
1969
|
+
priority: Number(current.priority ?? 0),
|
|
1970
|
+
completedAt: current.completedAt ?? null
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
current = null;
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
if (current === null) continue;
|
|
1977
|
+
const colon = trimmed.indexOf(":");
|
|
1978
|
+
if (colon < 0) continue;
|
|
1979
|
+
const left = trimmed.slice(0, colon);
|
|
1980
|
+
const value = trimmed.slice(colon + 1);
|
|
1981
|
+
const semicolon = left.indexOf(";");
|
|
1982
|
+
const name2 = (semicolon < 0 ? left : left.slice(0, semicolon)).toUpperCase();
|
|
1983
|
+
const params = semicolon < 0 ? "" : left.slice(semicolon + 1).toUpperCase();
|
|
1984
|
+
if (name2 === "UID") current.uid = unescapeText(value).trim();
|
|
1985
|
+
else if (name2 === "SUMMARY") current.title = unescapeText(value);
|
|
1986
|
+
else if (name2 === "DESCRIPTION") current.notes = unescapeText(value);
|
|
1987
|
+
else if (name2 === "STATUS") current.status = value.trim().toUpperCase();
|
|
1988
|
+
else if (name2 === "PRIORITY") current.priority = Number(value) || 0;
|
|
1989
|
+
else if (name2 === "DUE") current.due = decodeDue(value, params);
|
|
1990
|
+
else if (name2 === "COMPLETED") {
|
|
1991
|
+
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(value.trim());
|
|
1992
|
+
if (match !== null) {
|
|
1993
|
+
current.completedAt = new Date(
|
|
1994
|
+
Date.UTC(
|
|
1995
|
+
Number(match[1]),
|
|
1996
|
+
Number(match[2]) - 1,
|
|
1997
|
+
Number(match[3]),
|
|
1998
|
+
Number(match[4]),
|
|
1999
|
+
Number(match[5]),
|
|
2000
|
+
Number(match[6])
|
|
2001
|
+
)
|
|
2002
|
+
).toISOString();
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return todos;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/host/caldav.ts
|
|
2010
|
+
function decodeXml(value) {
|
|
2011
|
+
return String(value ?? "").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&");
|
|
2012
|
+
}
|
|
2013
|
+
function tagValue(xml, name2) {
|
|
2014
|
+
const pattern = new RegExp(`<(?:[\\w-]+:)?${name2}\\b[^>]*>([\\s\\S]*?)</(?:[\\w-]+:)?${name2}>`, "i");
|
|
2015
|
+
const match = pattern.exec(String(xml ?? ""));
|
|
2016
|
+
return match === null ? "" : decodeXml(match[1] ?? "");
|
|
2017
|
+
}
|
|
2018
|
+
function responseBlocks(xml) {
|
|
2019
|
+
return [
|
|
2020
|
+
...String(xml ?? "").matchAll(/<(?:[\w-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?response>/gi)
|
|
2021
|
+
].map((match) => match[1] ?? "");
|
|
2022
|
+
}
|
|
2023
|
+
function resolveHref(base, href) {
|
|
2024
|
+
const text2 = String(href ?? "").trim();
|
|
2025
|
+
if (text2 === "") return "";
|
|
2026
|
+
return new URL(text2, base).href;
|
|
2027
|
+
}
|
|
2028
|
+
function withSlash(url) {
|
|
2029
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
2030
|
+
}
|
|
2031
|
+
var PROPFIND_ROOT = `<?xml version="1.0" encoding="utf-8"?>
|
|
2032
|
+
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2033
|
+
<d:prop><d:current-user-principal/><c:calendar-home-set/></d:prop>
|
|
2034
|
+
</d:propfind>`;
|
|
2035
|
+
var PROPFIND_HOME = `<?xml version="1.0" encoding="utf-8"?>
|
|
2036
|
+
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2037
|
+
<d:prop><d:resourcetype/><d:displayname/><c:supported-calendar-component-set/></d:prop>
|
|
2038
|
+
</d:propfind>`;
|
|
2039
|
+
var REPORT_TODOS = `<?xml version="1.0" encoding="utf-8"?>
|
|
2040
|
+
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2041
|
+
<d:prop><d:getetag/><c:calendar-data/></d:prop>
|
|
2042
|
+
<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VTODO"/></c:comp-filter></c:filter>
|
|
2043
|
+
</c:calendar-query>`;
|
|
2044
|
+
var CalDavClient = class {
|
|
2045
|
+
url;
|
|
2046
|
+
username;
|
|
2047
|
+
password;
|
|
2048
|
+
calendarHref;
|
|
2049
|
+
constructor(config, password) {
|
|
2050
|
+
this.url = withSlash(String(config.url ?? "").trim());
|
|
2051
|
+
this.username = String(config.username ?? "");
|
|
2052
|
+
this.password = String(password ?? "");
|
|
2053
|
+
this.calendarHref = String(config.calendarHref ?? "").trim();
|
|
2054
|
+
}
|
|
2055
|
+
headers(extra = {}) {
|
|
2056
|
+
const auth = Buffer.from(`${this.username}:${this.password}`, "utf8").toString("base64");
|
|
2057
|
+
return { Authorization: `Basic ${auth}`, ...extra };
|
|
2058
|
+
}
|
|
2059
|
+
async request(method, url, options = {}) {
|
|
2060
|
+
if (this.url === "") throw new Error("CalDAV \u5730\u5740\u672A\u914D\u7F6E");
|
|
2061
|
+
const controller = new AbortController();
|
|
2062
|
+
const timer = setTimeout(() => {
|
|
2063
|
+
controller.abort();
|
|
2064
|
+
}, 2e4);
|
|
2065
|
+
let response;
|
|
2066
|
+
try {
|
|
2067
|
+
response = await fetch(url, {
|
|
2068
|
+
method,
|
|
2069
|
+
headers: this.headers({
|
|
2070
|
+
...options.depth === void 0 ? {} : { Depth: String(options.depth) },
|
|
2071
|
+
...options.body === void 0 ? {} : { "Content-Type": "application/xml; charset=utf-8" },
|
|
2072
|
+
...options.headers ?? {}
|
|
2073
|
+
}),
|
|
2074
|
+
body: options.body,
|
|
2075
|
+
signal: controller.signal,
|
|
2076
|
+
redirect: "follow"
|
|
2077
|
+
});
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error("CalDAV \u8FDE\u63A5\u8D85\u65F6");
|
|
2080
|
+
throw new Error(`CalDAV \u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2081
|
+
} finally {
|
|
2082
|
+
clearTimeout(timer);
|
|
2083
|
+
}
|
|
2084
|
+
const text2 = await response.text();
|
|
2085
|
+
if (response.status === 401 || response.status === 403)
|
|
2086
|
+
throw new Error("CalDAV \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7528\u6237\u540D\u548C\u5E94\u7528\u5BC6\u7801");
|
|
2087
|
+
if (response.status === 404) throw new Error("CalDAV \u5730\u5740\u6216\u65E5\u5386\u4E0D\u5B58\u5728");
|
|
2088
|
+
if (response.status === 412) throw new Error("CalDAV \u4EFB\u52A1\u5DF2\u88AB\u5176\u4ED6\u5BA2\u6237\u7AEF\u4FEE\u6539\uFF0C\u8BF7\u5148\u540C\u6B65");
|
|
2089
|
+
if (!response.ok && response.status !== 207) {
|
|
2090
|
+
throw new Error(`CalDAV \u8FD4\u56DE ${String(response.status)}${text2 === "" ? "" : `\uFF1A${text2.slice(0, 160)}`}`);
|
|
2091
|
+
}
|
|
2092
|
+
return { status: response.status, text: text2, headers: response.headers };
|
|
2093
|
+
}
|
|
2094
|
+
async discover() {
|
|
2095
|
+
if (this.calendarHref !== "") return this.calendarHref;
|
|
2096
|
+
const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
|
|
2097
|
+
const principalBlock = tagValue(root.text, "current-user-principal");
|
|
2098
|
+
const principal = tagValue(principalBlock, "href") || (principalBlock.trim().startsWith("/") ? principalBlock.trim() : "");
|
|
2099
|
+
const homeBlock = tagValue(root.text, "calendar-home-set");
|
|
2100
|
+
let home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
|
|
2101
|
+
if (home === "") {
|
|
2102
|
+
if (principal === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE calendar-home-set");
|
|
2103
|
+
const principalUrl = resolveHref(this.url, principal);
|
|
2104
|
+
const principalResponse = await this.request("PROPFIND", principalUrl, {
|
|
2105
|
+
body: PROPFIND_ROOT,
|
|
2106
|
+
depth: 0
|
|
2107
|
+
});
|
|
2108
|
+
const principalHome = tagValue(principalResponse.text, "calendar-home-set");
|
|
2109
|
+
home = tagValue(principalHome, "href") || (principalHome.trim().startsWith("/") ? principalHome.trim() : "");
|
|
2110
|
+
if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
|
|
2111
|
+
}
|
|
2112
|
+
const homeUrl = withSlash(resolveHref(this.url, home));
|
|
2113
|
+
const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
|
|
2114
|
+
const calendars = [];
|
|
2115
|
+
for (const block of responseBlocks(listing.text)) {
|
|
2116
|
+
const href = tagValue(block, "href");
|
|
2117
|
+
if (href === "") continue;
|
|
2118
|
+
if (!/calendar/i.test(tagValue(block, "resourcetype"))) continue;
|
|
2119
|
+
const supported = tagValue(block, "supported-calendar-component-set");
|
|
2120
|
+
if (supported !== "" && !/VTODO/i.test(supported)) continue;
|
|
2121
|
+
calendars.push({
|
|
2122
|
+
href: withSlash(resolveHref(homeUrl, href)),
|
|
2123
|
+
displayName: tagValue(block, "displayname") || href
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
if (calendars.length === 0) throw new Error("\u6CA1\u6709\u627E\u5230\u652F\u6301 VTODO \u7684\u4EFB\u52A1\u65E5\u5386");
|
|
2127
|
+
this.calendarHref = calendars[0]?.href ?? "";
|
|
2128
|
+
return this.calendarHref;
|
|
2129
|
+
}
|
|
2130
|
+
async listCalendars() {
|
|
2131
|
+
if (this.calendarHref !== "") return [{ href: this.calendarHref, displayName: "" }];
|
|
2132
|
+
const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
|
|
2133
|
+
const homeBlock = tagValue(root.text, "calendar-home-set");
|
|
2134
|
+
const home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
|
|
2135
|
+
if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
|
|
2136
|
+
const homeUrl = withSlash(resolveHref(this.url, home));
|
|
2137
|
+
const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
|
|
2138
|
+
return responseBlocks(listing.text).filter((block) => /calendar/i.test(tagValue(block, "resourcetype"))).map((block) => ({
|
|
2139
|
+
href: withSlash(resolveHref(homeUrl, tagValue(block, "href"))),
|
|
2140
|
+
displayName: tagValue(block, "displayname")
|
|
2141
|
+
}));
|
|
2142
|
+
}
|
|
2143
|
+
async fetchTodos(collectionHref) {
|
|
2144
|
+
const collection = withSlash(resolveHref(this.url, collectionHref));
|
|
2145
|
+
const response = await this.request("REPORT", collection, { body: REPORT_TODOS, depth: 1 });
|
|
2146
|
+
const items = [];
|
|
2147
|
+
for (const block of responseBlocks(response.text)) {
|
|
2148
|
+
const href = tagValue(block, "href");
|
|
2149
|
+
if (href === "") continue;
|
|
2150
|
+
const etag = tagValue(block, "getetag") || null;
|
|
2151
|
+
const data = tagValue(block, "calendar-data");
|
|
2152
|
+
if (data === "") continue;
|
|
2153
|
+
for (const todo of parseTodos(data)) {
|
|
2154
|
+
if (todo.uid === "") continue;
|
|
2155
|
+
items.push({ href: resolveHref(collection, href), etag, todo });
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return items;
|
|
2159
|
+
}
|
|
2160
|
+
async putTodo(collectionHref, task) {
|
|
2161
|
+
const collection = withSlash(resolveHref(this.url, collectionHref));
|
|
2162
|
+
const href = task.href !== null && task.href !== "" ? task.href : new URL(`${encodeURIComponent(task.uid)}.ics`, collection).href;
|
|
2163
|
+
const headers = { "Content-Type": "text/calendar; charset=utf-8" };
|
|
2164
|
+
if (task.etag !== null && task.etag !== "") headers["If-Match"] = task.etag;
|
|
2165
|
+
else headers["If-None-Match"] = "*";
|
|
2166
|
+
const response = await this.request("PUT", href, { body: serializeTodos([task]), headers });
|
|
2167
|
+
return { href, etag: response.headers.get("etag") ?? task.etag, uid: task.uid };
|
|
2168
|
+
}
|
|
2169
|
+
async deleteTodo(href, etag) {
|
|
2170
|
+
const headers = {};
|
|
2171
|
+
if (etag !== null && etag !== "") headers["If-Match"] = etag;
|
|
2172
|
+
await this.request("DELETE", resolveHref(this.url, href), { headers });
|
|
2173
|
+
}
|
|
2174
|
+
/** Pull remote VTODOs, then push local dirty/tombstoned tasks. */
|
|
2175
|
+
async sync(store, accountId) {
|
|
2176
|
+
const collection = await this.discover();
|
|
2177
|
+
const remote = await this.fetchTodos(collection);
|
|
2178
|
+
let pulled = 0;
|
|
2179
|
+
for (const item of remote) {
|
|
2180
|
+
store.applyRemote({
|
|
2181
|
+
accountId,
|
|
2182
|
+
calendarHref: collection,
|
|
2183
|
+
href: item.href,
|
|
2184
|
+
etag: item.etag,
|
|
2185
|
+
todo: item.todo
|
|
2186
|
+
});
|
|
2187
|
+
pulled += 1;
|
|
2188
|
+
}
|
|
2189
|
+
const removed = store.pruneMissingRemote(
|
|
2190
|
+
accountId,
|
|
2191
|
+
collection,
|
|
2192
|
+
remote.map((item) => item.href)
|
|
2193
|
+
);
|
|
2194
|
+
let pushed = 0;
|
|
2195
|
+
let deleted = 0;
|
|
2196
|
+
for (const task of store.pending(accountId)) {
|
|
2197
|
+
if (task.deleted) {
|
|
2198
|
+
if (task.href !== null && task.href !== "") {
|
|
2199
|
+
await this.deleteTodo(task.href, task.etag);
|
|
2200
|
+
deleted += 1;
|
|
2201
|
+
}
|
|
2202
|
+
store.purge(task.id);
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
const result = await this.putTodo(
|
|
2206
|
+
task.calendarHref !== null && task.calendarHref !== "" ? task.calendarHref : collection,
|
|
2207
|
+
task
|
|
2208
|
+
);
|
|
2209
|
+
store.markPushed(task.id, result);
|
|
2210
|
+
pushed += 1;
|
|
2211
|
+
}
|
|
2212
|
+
store.setCalendarState(accountId, {
|
|
2213
|
+
calendarHref: collection,
|
|
2214
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2215
|
+
error: null
|
|
2216
|
+
});
|
|
2217
|
+
return { pulled, pushed, deleted, removed, calendarHref: collection, total: remote.length };
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
// src/host/projects.ts
|
|
2222
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2223
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2224
|
+
import { join as join5 } from "node:path";
|
|
2225
|
+
var ProjectStore = class {
|
|
2226
|
+
file;
|
|
2227
|
+
cache = null;
|
|
2228
|
+
constructor(file) {
|
|
2229
|
+
this.file = file;
|
|
2230
|
+
}
|
|
2231
|
+
load() {
|
|
2232
|
+
if (this.cache !== null) return this.cache;
|
|
2233
|
+
let parsed = null;
|
|
2234
|
+
try {
|
|
2235
|
+
if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
|
|
2236
|
+
} catch {
|
|
2237
|
+
parsed = null;
|
|
2238
|
+
}
|
|
2239
|
+
const projects = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
2240
|
+
this.cache = { version: 1, projects: projects.filter(isProject) };
|
|
2241
|
+
return this.cache;
|
|
2242
|
+
}
|
|
2243
|
+
flush() {
|
|
2244
|
+
const data = this.load();
|
|
2245
|
+
mkdirSync4(join5(this.file, ".."), { recursive: true });
|
|
2246
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2247
|
+
writeFileSync4(temp, `${JSON.stringify(data, null, 2)}
|
|
2248
|
+
`, { mode: 384 });
|
|
2249
|
+
renameSync3(temp, this.file);
|
|
2250
|
+
}
|
|
2251
|
+
list() {
|
|
2252
|
+
return [...this.load().projects].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
2253
|
+
}
|
|
2254
|
+
get(id) {
|
|
2255
|
+
return this.load().projects.find((project) => project.id === id);
|
|
2256
|
+
}
|
|
2257
|
+
findByMessage(accountId, key) {
|
|
2258
|
+
return this.load().projects.find(
|
|
2259
|
+
(project) => project.messages.some((message) => message.accountId === accountId && message.key === key)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
create(record) {
|
|
2263
|
+
const data = this.load();
|
|
2264
|
+
data.projects.push(record);
|
|
2265
|
+
this.flush();
|
|
2266
|
+
return record;
|
|
2267
|
+
}
|
|
2268
|
+
update(id, patch) {
|
|
2269
|
+
const project = this.get(id);
|
|
2270
|
+
if (project === void 0) return void 0;
|
|
2271
|
+
Object.assign(project, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2272
|
+
this.flush();
|
|
2273
|
+
return project;
|
|
2274
|
+
}
|
|
2275
|
+
addMessage(id, message) {
|
|
2276
|
+
const project = this.get(id);
|
|
2277
|
+
if (project === void 0) return void 0;
|
|
2278
|
+
const exists = project.messages.some(
|
|
2279
|
+
(item) => item.accountId === message.accountId && item.key === message.key
|
|
2280
|
+
);
|
|
2281
|
+
if (!exists) project.messages.push(message);
|
|
2282
|
+
if (!project.accountIds.includes(message.accountId)) project.accountIds.push(message.accountId);
|
|
2283
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2284
|
+
this.flush();
|
|
2285
|
+
return project;
|
|
2286
|
+
}
|
|
2287
|
+
removeMessage(id, accountId, key) {
|
|
2288
|
+
const project = this.get(id);
|
|
2289
|
+
if (project === void 0) return void 0;
|
|
2290
|
+
project.messages = project.messages.filter((item) => !(item.accountId === accountId && item.key === key));
|
|
2291
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2292
|
+
this.flush();
|
|
2293
|
+
return project;
|
|
2294
|
+
}
|
|
2295
|
+
addThread(id, thread) {
|
|
2296
|
+
const project = this.get(id);
|
|
2297
|
+
if (project === void 0) return void 0;
|
|
2298
|
+
if (!project.threads.some((item) => item.accountId === thread.accountId && item.key === thread.key)) {
|
|
2299
|
+
project.threads.push(thread);
|
|
2300
|
+
}
|
|
2301
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2302
|
+
this.flush();
|
|
2303
|
+
return project;
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
function isProject(value) {
|
|
2307
|
+
return value !== null && typeof value === "object" && typeof value.id === "string" && typeof value.path === "string";
|
|
2308
|
+
}
|
|
2309
|
+
function slugify(name2) {
|
|
2310
|
+
const text2 = name2.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2311
|
+
return text2 === "" ? "project" : text2.slice(0, 60);
|
|
2312
|
+
}
|
|
2313
|
+
async function createProject(options) {
|
|
2314
|
+
const { store, input } = options;
|
|
2315
|
+
const name2 = input.name.trim();
|
|
2316
|
+
if (name2 === "") throw new Error("\u9879\u76EE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2317
|
+
if (name2.length > 120) throw new Error("\u9879\u76EE\u540D\u79F0\u8FC7\u957F");
|
|
2318
|
+
const root = input.root.trim();
|
|
2319
|
+
if (root === "") throw new Error("\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2320
|
+
const warnings = [];
|
|
2321
|
+
const id = `prj_${randomUUID2().slice(0, 8)}`;
|
|
2322
|
+
const base = slugify(name2);
|
|
2323
|
+
let path = join5(root, base);
|
|
2324
|
+
let attempt = 2;
|
|
2325
|
+
while (existsSync5(path)) {
|
|
2326
|
+
path = join5(root, `${base}-${String(attempt)}`);
|
|
2327
|
+
attempt += 1;
|
|
2328
|
+
}
|
|
2329
|
+
mkdirSync4(path, { recursive: true });
|
|
2330
|
+
const created = (/* @__PURE__ */ new Date()).toISOString();
|
|
2331
|
+
const brief = renderBrief(name2, input);
|
|
2332
|
+
writeFileSync4(join5(path, "BRIEF.md"), brief, { mode: 384 });
|
|
2333
|
+
writeFileSync4(join5(path, "CONVERSATION.md"), renderConversation(input.conversation, created), {
|
|
2334
|
+
mode: 384
|
|
2335
|
+
});
|
|
2336
|
+
let workspaceId = "";
|
|
2337
|
+
if (options.registry !== void 0) {
|
|
2338
|
+
try {
|
|
2339
|
+
const workspace = await options.registry.create(path, name2);
|
|
2340
|
+
workspaceId = String(workspace?.id ?? "");
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
warnings.push(`\u5DE5\u4F5C\u7A7A\u95F4\u6CE8\u518C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2343
|
+
}
|
|
2344
|
+
} else {
|
|
2345
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 workspaceRegistry\uFF0C\u9879\u76EE\u76EE\u5F55\u5DF2\u521B\u5EFA\u4F46\u6CA1\u6709\u6CE8\u518C\u5230\u4FA7\u680F");
|
|
2346
|
+
}
|
|
2347
|
+
let sessionId = "";
|
|
2348
|
+
if (options.sessions !== void 0) {
|
|
2349
|
+
try {
|
|
2350
|
+
sessionId = randomUUID2();
|
|
2351
|
+
const session = options.sessions.create(sessionId, { meta: { cwd: path } });
|
|
2352
|
+
session.append(
|
|
2353
|
+
"user/message",
|
|
2354
|
+
{
|
|
2355
|
+
role: "user",
|
|
2356
|
+
content: [{ type: "text", text: `\u6211\u521B\u5EFA\u4E86\u4E00\u4E2A\u90AE\u4EF6\u9879\u76EE\u300C${name2}\u300D\u3002
|
|
2357
|
+
|
|
2358
|
+
${brief}` }],
|
|
2359
|
+
source: { kind: "user" }
|
|
2360
|
+
},
|
|
2361
|
+
{ surfaceOp: "append" }
|
|
2362
|
+
);
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
warnings.push(`\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2365
|
+
sessionId = "";
|
|
2366
|
+
}
|
|
2367
|
+
} else {
|
|
2368
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 sessions \u670D\u52A1\uFF0C\u672A\u521B\u5EFA\u9879\u76EE\u4F1A\u8BDD");
|
|
2369
|
+
}
|
|
2370
|
+
const project = {
|
|
2371
|
+
id,
|
|
2372
|
+
name: name2,
|
|
2373
|
+
path,
|
|
2374
|
+
workspaceId,
|
|
2375
|
+
sessionId,
|
|
2376
|
+
accountIds: [input.accountId],
|
|
2377
|
+
messages: input.message === void 0 ? [] : [input.message],
|
|
2378
|
+
threads: input.thread === void 0 ? [] : [input.thread],
|
|
2379
|
+
createdAt: created,
|
|
2380
|
+
updatedAt: created
|
|
2381
|
+
};
|
|
2382
|
+
writeFileSync4(
|
|
2383
|
+
join5(path, "project.json"),
|
|
2384
|
+
`${JSON.stringify({ ...project, conversation: void 0 }, null, 2)}
|
|
2385
|
+
`,
|
|
2386
|
+
{
|
|
2387
|
+
mode: 384
|
|
2388
|
+
}
|
|
2389
|
+
);
|
|
2390
|
+
store.create(project);
|
|
2391
|
+
return { project, warnings };
|
|
2392
|
+
}
|
|
2393
|
+
function appendConversation(project, conversation) {
|
|
2394
|
+
const stamp3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2395
|
+
const file = join5(project.path, "CONVERSATION.md");
|
|
2396
|
+
const block = `
|
|
2397
|
+
|
|
2398
|
+
## \u8FFD\u52A0\u8BB0\u5F55 \xB7 ${stamp3}
|
|
2399
|
+
|
|
2400
|
+
${conversation.trim()}
|
|
2401
|
+
`;
|
|
2402
|
+
writeFileSync4(file, block, { flag: "a", mode: 384 });
|
|
2403
|
+
return file;
|
|
2404
|
+
}
|
|
2405
|
+
function renderBrief(name2, input) {
|
|
2406
|
+
const lines = [`# ${name2}`, "", `- \u521B\u5EFA\u65F6\u95F4\uFF1A${(/* @__PURE__ */ new Date()).toISOString()}`, `- \u6765\u6E90\u8D26\u53F7\uFF1A${input.accountId}`];
|
|
2407
|
+
if (input.message !== void 0) {
|
|
2408
|
+
lines.push(`- \u6765\u6E90\u90AE\u4EF6\uFF1A${input.message.subject}\uFF08${input.message.date}\uFF09`);
|
|
2409
|
+
}
|
|
2410
|
+
lines.push(
|
|
2411
|
+
"",
|
|
2412
|
+
"## \u80CC\u666F",
|
|
2413
|
+
"",
|
|
2414
|
+
"\u7531 MailHarbor \u4ECE\u90AE\u4EF6\u751F\u6210\u3002\u4EE5\u4E0B\u662F\u5F53\u65F6 AI \u5DE5\u4F5C\u53F0\u7684\u8BB0\u5F55\u3002",
|
|
2415
|
+
"",
|
|
2416
|
+
"## AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55",
|
|
2417
|
+
"",
|
|
2418
|
+
input.conversation.trim(),
|
|
2419
|
+
""
|
|
2420
|
+
);
|
|
2421
|
+
return lines.join("\n");
|
|
2422
|
+
}
|
|
2423
|
+
function renderConversation(conversation, created) {
|
|
2424
|
+
return `# AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55
|
|
2425
|
+
|
|
2426
|
+
- \u8F6C\u79FB\u5230\u9879\u76EE\uFF1A${created}
|
|
2427
|
+
|
|
2428
|
+
${conversation.trim()}
|
|
2429
|
+
`;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
1785
2432
|
// src/host/version.ts
|
|
1786
|
-
var VERSION = "0.
|
|
2433
|
+
var VERSION = "0.5.0";
|
|
1787
2434
|
|
|
1788
2435
|
// src/host/api.ts
|
|
1789
2436
|
init_smtp();
|
|
@@ -1889,6 +2536,22 @@ function buildRouter(runtime) {
|
|
|
1889
2536
|
const page = runtime.database(account).list({ mailbox: mailbox === "" ? "all" : mailbox, query, offset, limit });
|
|
1890
2537
|
return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) };
|
|
1891
2538
|
});
|
|
2539
|
+
router.get("/messages/lookup", ({ url }) => {
|
|
2540
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2541
|
+
const key = url.searchParams.get("key") ?? "";
|
|
2542
|
+
if (key.startsWith("local:")) {
|
|
2543
|
+
const id = Number(key.slice(6));
|
|
2544
|
+
const detail = runtime.database(account).get(id);
|
|
2545
|
+
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
2546
|
+
return { id: String(id), mailbox: detail.mailbox, subject: detail.subject };
|
|
2547
|
+
}
|
|
2548
|
+
if (key.startsWith("mid:")) {
|
|
2549
|
+
const found = runtime.database(account).findByMessageId(key.slice(4));
|
|
2550
|
+
if (found === void 0) throw new HttpError(404, "\u672C\u5730\u5907\u4EFD\u91CC\u627E\u4E0D\u5230\u8FD9\u5C01\u90AE\u4EF6");
|
|
2551
|
+
return { id: String(found.id), mailbox: found.mailbox, subject: "" };
|
|
2552
|
+
}
|
|
2553
|
+
throw new HttpError(400, "\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6");
|
|
2554
|
+
});
|
|
1892
2555
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
1893
2556
|
const { account } = accountFrom(runtime, url);
|
|
1894
2557
|
const source = sourceOf(url);
|
|
@@ -2139,15 +2802,16 @@ function buildRouter(runtime) {
|
|
|
2139
2802
|
};
|
|
2140
2803
|
} else if (kind === "translate") {
|
|
2141
2804
|
const source = detail.html !== null && detail.html !== "" ? detail.html : detail.text;
|
|
2142
|
-
|
|
2143
|
-
|
|
2805
|
+
const translated = normalizeTranslated(await ai.translate(source, target));
|
|
2806
|
+
output = translated.text;
|
|
2807
|
+
html = translated.html;
|
|
2144
2808
|
meta = { target };
|
|
2145
2809
|
} else {
|
|
2146
2810
|
const earlier = runtime.aiThreads(account).get(key)?.cards.filter((card2) => card2.kind === "answer").map((card2) => card2.output) ?? [];
|
|
2147
2811
|
output = await ai.ask(detail, instruction, earlier);
|
|
2148
2812
|
}
|
|
2149
2813
|
const card = {
|
|
2150
|
-
id:
|
|
2814
|
+
id: randomUUID3(),
|
|
2151
2815
|
kind,
|
|
2152
2816
|
instruction,
|
|
2153
2817
|
output,
|
|
@@ -2230,8 +2894,8 @@ function buildRouter(runtime) {
|
|
|
2230
2894
|
const html = typeof payload.html === "string" ? payload.html : "";
|
|
2231
2895
|
const target = payload.target === "en" ? "en" : "zh";
|
|
2232
2896
|
try {
|
|
2233
|
-
const
|
|
2234
|
-
return { text:
|
|
2897
|
+
const translated = normalizeTranslated(await runtime.ai().translate(html, target));
|
|
2898
|
+
return { text: translated.text, html: translated.html };
|
|
2235
2899
|
} catch (error) {
|
|
2236
2900
|
throw aiHttpError(error);
|
|
2237
2901
|
}
|
|
@@ -2261,6 +2925,192 @@ function buildRouter(runtime) {
|
|
|
2261
2925
|
throw aiHttpError(error);
|
|
2262
2926
|
}
|
|
2263
2927
|
});
|
|
2928
|
+
router.get("/tasks", ({ url }) => {
|
|
2929
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2930
|
+
const items = runtime.tasks().list({
|
|
2931
|
+
accountId: account.id,
|
|
2932
|
+
status: url.searchParams.get("status") ?? "",
|
|
2933
|
+
projectId: url.searchParams.get("projectId") ?? "",
|
|
2934
|
+
query: url.searchParams.get("query") ?? ""
|
|
2935
|
+
});
|
|
2936
|
+
return { items: items.map(taskView), calendar: runtime.tasks().calendarState(account.id) };
|
|
2937
|
+
});
|
|
2938
|
+
router.post("/tasks", async ({ body }) => {
|
|
2939
|
+
const payload = await body();
|
|
2940
|
+
const account = accountFromPayload(runtime, payload);
|
|
2941
|
+
const task = runtime.tasks().create({
|
|
2942
|
+
accountId: account.id,
|
|
2943
|
+
title: String(payload.title ?? ""),
|
|
2944
|
+
notes: typeof payload.notes === "string" ? payload.notes : "",
|
|
2945
|
+
due: typeof payload.due === "string" ? payload.due : "",
|
|
2946
|
+
status: typeof payload.status === "string" ? payload.status : "NEEDS-ACTION",
|
|
2947
|
+
priority: Number(payload.priority ?? 0),
|
|
2948
|
+
owner: typeof payload.owner === "string" ? payload.owner : "",
|
|
2949
|
+
projectId: typeof payload.projectId === "string" ? payload.projectId : null,
|
|
2950
|
+
messageKey: typeof payload.messageKey === "string" ? payload.messageKey : "",
|
|
2951
|
+
messageSubject: typeof payload.messageSubject === "string" ? payload.messageSubject : ""
|
|
2952
|
+
});
|
|
2953
|
+
return { task: taskView(task) };
|
|
2954
|
+
});
|
|
2955
|
+
router.patch("/tasks/:id", async ({ params, body }) => {
|
|
2956
|
+
const payload = await body();
|
|
2957
|
+
const task = runtime.tasks().update(String(params.id ?? ""), {
|
|
2958
|
+
...typeof payload.title === "string" ? { title: payload.title } : {},
|
|
2959
|
+
...typeof payload.notes === "string" ? { notes: payload.notes } : {},
|
|
2960
|
+
...typeof payload.due === "string" ? { due: payload.due } : {},
|
|
2961
|
+
...typeof payload.status === "string" ? { status: payload.status } : {},
|
|
2962
|
+
...typeof payload.priority === "number" ? { priority: payload.priority } : {},
|
|
2963
|
+
...typeof payload.owner === "string" ? { owner: payload.owner } : {},
|
|
2964
|
+
...payload.projectId === null || typeof payload.projectId === "string" ? { projectId: payload.projectId } : {}
|
|
2965
|
+
});
|
|
2966
|
+
return { task: taskView(task) };
|
|
2967
|
+
});
|
|
2968
|
+
router.delete("/tasks/:id", ({ params }) => {
|
|
2969
|
+
const removed = runtime.tasks().remove(String(params.id ?? ""));
|
|
2970
|
+
if (!removed) throw new HttpError(404, "\u4EFB\u52A1\u4E0D\u5B58\u5728");
|
|
2971
|
+
return { ok: true };
|
|
2972
|
+
});
|
|
2973
|
+
router.post("/tasks/extract", async ({ body }) => {
|
|
2974
|
+
const payload = await body();
|
|
2975
|
+
const account = accountFromPayload(runtime, payload);
|
|
2976
|
+
const detail = await loadDetail(
|
|
2977
|
+
runtime,
|
|
2978
|
+
account,
|
|
2979
|
+
sourceOfPayload(payload),
|
|
2980
|
+
String(payload.id ?? ""),
|
|
2981
|
+
mailboxOfPayload(payload)
|
|
2982
|
+
);
|
|
2983
|
+
try {
|
|
2984
|
+
const suggestions = await runtime.ai().extractTasks(detail);
|
|
2985
|
+
return { suggestions };
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
throw aiHttpError(error);
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
router.get("/tasks/caldav", ({ url }) => {
|
|
2991
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2992
|
+
return {
|
|
2993
|
+
caldav: account.caldav,
|
|
2994
|
+
calendar: runtime.tasks().calendarState(account.id),
|
|
2995
|
+
hasPassword: runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email) !== void 0
|
|
2996
|
+
};
|
|
2997
|
+
});
|
|
2998
|
+
router.put("/tasks/caldav", async ({ body }) => {
|
|
2999
|
+
const payload = await body();
|
|
3000
|
+
const account = accountFromPayload(runtime, payload);
|
|
3001
|
+
const url = String(payload.url ?? "").trim();
|
|
3002
|
+
const username = String(payload.username ?? "").trim();
|
|
3003
|
+
const calendarHref = String(payload.calendarHref ?? "").trim();
|
|
3004
|
+
const caldav = url === "" ? null : { url, username, ...calendarHref === "" ? {} : { calendarHref } };
|
|
3005
|
+
const accounts = runtime.current.accounts.map(
|
|
3006
|
+
(item) => item.id === account.id ? { ...item, caldav } : item
|
|
3007
|
+
);
|
|
3008
|
+
saveAccounts(accounts, runtime.current.activeAccountId);
|
|
3009
|
+
if (typeof payload.password === "string" && payload.password !== "") {
|
|
3010
|
+
runtime.credentials.writeSecret(`${account.keychainService}:caldav`, account.email, payload.password);
|
|
3011
|
+
}
|
|
3012
|
+
runtime.refresh();
|
|
3013
|
+
const updated = runtime.current.accounts.find((item) => item.id === account.id) ?? account;
|
|
3014
|
+
return {
|
|
3015
|
+
caldav: updated.caldav,
|
|
3016
|
+
title: "",
|
|
3017
|
+
hasPassword: runtime.credentials.readSecret(`${updated.keychainService}:caldav`, updated.email) !== void 0
|
|
3018
|
+
};
|
|
3019
|
+
});
|
|
3020
|
+
router.post("/tasks/sync", async ({ body }) => {
|
|
3021
|
+
const payload = await body();
|
|
3022
|
+
const account = accountFromPayload(runtime, payload);
|
|
3023
|
+
const caldav = account.caldav;
|
|
3024
|
+
if (caldav === null) throw new HttpError(400, "\u8BF7\u5148\u914D\u7F6E CalDAV \u5730\u5740\u548C\u8D26\u53F7");
|
|
3025
|
+
const password = runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email);
|
|
3026
|
+
if (password === void 0 || password === "") throw new HttpError(400, "\u8BF7\u5148\u4FDD\u5B58 CalDAV \u5E94\u7528\u5BC6\u7801");
|
|
3027
|
+
try {
|
|
3028
|
+
const client = new CalDavClient(caldav, password);
|
|
3029
|
+
const result = await runtime.tasks().sync(account.id, client);
|
|
3030
|
+
if (result.calendarHref !== "" && result.calendarHref !== (caldav.calendarHref ?? "")) {
|
|
3031
|
+
const accounts = runtime.current.accounts.map(
|
|
3032
|
+
(item) => item.id === account.id ? { ...item, caldav: { ...caldav, calendarHref: result.calendarHref } } : item
|
|
3033
|
+
);
|
|
3034
|
+
saveAccounts(accounts, runtime.current.activeAccountId);
|
|
3035
|
+
runtime.refresh();
|
|
3036
|
+
}
|
|
3037
|
+
return { result, calendar: runtime.tasks().calendarState(account.id) };
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
throw new HttpError(400, error instanceof Error ? error.message : String(error));
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
router.get("/projects", () => ({ items: runtime.projects().list().map(projectView) }));
|
|
3043
|
+
router.get("/projects/for", ({ url }) => {
|
|
3044
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
3045
|
+
const key = url.searchParams.get("key") ?? "";
|
|
3046
|
+
return {
|
|
3047
|
+
project: key === "" ? null : (() => {
|
|
3048
|
+
const found = runtime.projects().findByMessage(account.id, key);
|
|
3049
|
+
return found === void 0 ? null : projectView(found);
|
|
3050
|
+
})()
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
router.post("/projects", async ({ body }) => {
|
|
3054
|
+
const payload = await body();
|
|
3055
|
+
const account = accountFromPayload(runtime, payload);
|
|
3056
|
+
const name2 = String(payload.name ?? "").trim();
|
|
3057
|
+
const root = String(payload.root ?? "").trim() || runtime.current.projects.root;
|
|
3058
|
+
const conversation = typeof payload.conversation === "string" ? payload.conversation : "";
|
|
3059
|
+
const message = asObject(payload.message);
|
|
3060
|
+
const key = typeof message.key === "string" ? message.key : "";
|
|
3061
|
+
const result = await createProject({
|
|
3062
|
+
store: runtime.projects(),
|
|
3063
|
+
input: {
|
|
3064
|
+
name: name2,
|
|
3065
|
+
root,
|
|
3066
|
+
conversation,
|
|
3067
|
+
accountId: account.id,
|
|
3068
|
+
...key === "" ? {} : {
|
|
3069
|
+
message: {
|
|
3070
|
+
accountId: account.id,
|
|
3071
|
+
key,
|
|
3072
|
+
subject: String(message.subject ?? ""),
|
|
3073
|
+
date: String(message.date ?? "")
|
|
3074
|
+
}
|
|
3075
|
+
},
|
|
3076
|
+
...key === "" ? {} : { thread: { accountId: account.id, key } }
|
|
3077
|
+
},
|
|
3078
|
+
registry: runtime.workspaceRegistry,
|
|
3079
|
+
sessions: runtime.sessions
|
|
3080
|
+
});
|
|
3081
|
+
return { project: projectView(result.project), warnings: result.warnings };
|
|
3082
|
+
});
|
|
3083
|
+
router.post("/projects/:id/messages", async ({ params, body }) => {
|
|
3084
|
+
const payload = await body();
|
|
3085
|
+
const account = accountFromPayload(runtime, payload);
|
|
3086
|
+
const project = runtime.projects().addMessage(String(params.id ?? ""), {
|
|
3087
|
+
accountId: account.id,
|
|
3088
|
+
key: String(payload.key ?? ""),
|
|
3089
|
+
subject: String(payload.subject ?? ""),
|
|
3090
|
+
date: String(payload.date ?? "")
|
|
3091
|
+
});
|
|
3092
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3093
|
+
return { project: projectView(project) };
|
|
3094
|
+
});
|
|
3095
|
+
router.delete("/projects/:id/messages", ({ params, url }) => {
|
|
3096
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
3097
|
+
const key = url.searchParams.get("key") ?? "";
|
|
3098
|
+
const project = runtime.projects().removeMessage(String(params.id ?? ""), account.id, key);
|
|
3099
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3100
|
+
return { project: projectView(project) };
|
|
3101
|
+
});
|
|
3102
|
+
router.post("/projects/:id/transfer", async ({ params, body }) => {
|
|
3103
|
+
const payload = await body();
|
|
3104
|
+
const account = accountFromPayload(runtime, payload);
|
|
3105
|
+
const project = runtime.projects().get(String(params.id ?? ""));
|
|
3106
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3107
|
+
const conversation = typeof payload.conversation === "string" ? payload.conversation : "";
|
|
3108
|
+
if (conversation.trim() === "") throw new HttpError(400, "\u6CA1\u6709\u53EF\u8F6C\u79FB\u7684 AI \u8BB0\u5F55");
|
|
3109
|
+
appendConversation(project, conversation);
|
|
3110
|
+
const key = typeof payload.key === "string" ? payload.key : "";
|
|
3111
|
+
const updated = key === "" ? runtime.projects().update(project.id, {}) : runtime.projects().addThread(project.id, { accountId: account.id, key });
|
|
3112
|
+
return { project: projectView(updated ?? project) };
|
|
3113
|
+
});
|
|
2264
3114
|
router.get("/settings", () => settingsView(runtime.current));
|
|
2265
3115
|
router.patch("/settings", async ({ body }) => {
|
|
2266
3116
|
const payload = await body();
|
|
@@ -2269,6 +3119,11 @@ function buildRouter(runtime) {
|
|
|
2269
3119
|
if (payload.ai !== void 0) patch.ai = asObject(payload.ai);
|
|
2270
3120
|
if (payload.read !== void 0) patch.read = asObject(payload.read);
|
|
2271
3121
|
if (payload.render !== void 0) patch.render = asObject(payload.render);
|
|
3122
|
+
if (payload.projects !== void 0) {
|
|
3123
|
+
const root = String(asObject(payload.projects).root ?? "").trim();
|
|
3124
|
+
if (root === "") throw new HttpError(400, "\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3125
|
+
patch.projects = { root: resolveUserPath(root, "\u9879\u76EE\u76EE\u5F55") };
|
|
3126
|
+
}
|
|
2272
3127
|
saveSettings(patch);
|
|
2273
3128
|
runtime.refresh();
|
|
2274
3129
|
return { settings: settingsView(runtime.current) };
|
|
@@ -2410,7 +3265,41 @@ function settingsView(config) {
|
|
|
2410
3265
|
styleSampleCount: config.ai.styleSampleCount
|
|
2411
3266
|
},
|
|
2412
3267
|
read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },
|
|
2413
|
-
render: { listLimit: config.render.listLimit }
|
|
3268
|
+
render: { listLimit: config.render.listLimit },
|
|
3269
|
+
projects: { root: config.projects.root }
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function taskView(task) {
|
|
3273
|
+
return {
|
|
3274
|
+
id: task.id,
|
|
3275
|
+
accountId: task.accountId,
|
|
3276
|
+
projectId: task.projectId,
|
|
3277
|
+
title: task.title,
|
|
3278
|
+
notes: task.notes,
|
|
3279
|
+
due: task.due,
|
|
3280
|
+
status: task.status,
|
|
3281
|
+
priority: task.priority,
|
|
3282
|
+
owner: task.owner,
|
|
3283
|
+
messageKey: task.messageKey,
|
|
3284
|
+
messageSubject: task.messageSubject,
|
|
3285
|
+
dirty: task.dirty,
|
|
3286
|
+
createdAt: task.createdAt,
|
|
3287
|
+
updatedAt: task.updatedAt,
|
|
3288
|
+
completedAt: task.completedAt
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function projectView(project) {
|
|
3292
|
+
return {
|
|
3293
|
+
id: project.id,
|
|
3294
|
+
name: project.name,
|
|
3295
|
+
path: project.path,
|
|
3296
|
+
workspaceId: project.workspaceId,
|
|
3297
|
+
sessionId: project.sessionId,
|
|
3298
|
+
accountIds: [...project.accountIds],
|
|
3299
|
+
messages: project.messages.map((message) => ({ ...message })),
|
|
3300
|
+
threads: project.threads.map((thread) => ({ ...thread })),
|
|
3301
|
+
createdAt: project.createdAt,
|
|
3302
|
+
updatedAt: project.updatedAt
|
|
2414
3303
|
};
|
|
2415
3304
|
}
|
|
2416
3305
|
function localSummary(runtime, account, row) {
|
|
@@ -2564,7 +3453,7 @@ ${signature}`;
|
|
|
2564
3453
|
|
|
2565
3454
|
// src/host/runtime.ts
|
|
2566
3455
|
import { statSync } from "node:fs";
|
|
2567
|
-
import { join as
|
|
3456
|
+
import { join as join7 } from "node:path";
|
|
2568
3457
|
|
|
2569
3458
|
// src/host/secrets.ts
|
|
2570
3459
|
import { spawnSync } from "node:child_process";
|
|
@@ -2733,6 +3622,16 @@ var CredentialManager = class {
|
|
|
2733
3622
|
this.session.delete(account.id);
|
|
2734
3623
|
this.secrets.delete(account.keychainService, account.email);
|
|
2735
3624
|
}
|
|
3625
|
+
/** Read an arbitrary secret (for example a CalDAV app password). */
|
|
3626
|
+
readSecret(service, account) {
|
|
3627
|
+
return this.secrets.read(service, account);
|
|
3628
|
+
}
|
|
3629
|
+
writeSecret(service, account, secret) {
|
|
3630
|
+
return this.secrets.write(service, account, secret);
|
|
3631
|
+
}
|
|
3632
|
+
deleteSecret(service, account) {
|
|
3633
|
+
return this.secrets.delete(service, account);
|
|
3634
|
+
}
|
|
2736
3635
|
status(account) {
|
|
2737
3636
|
const resolved = this.resolve(account);
|
|
2738
3637
|
return {
|
|
@@ -2744,7 +3643,7 @@ var CredentialManager = class {
|
|
|
2744
3643
|
};
|
|
2745
3644
|
|
|
2746
3645
|
// src/host/ai-store.ts
|
|
2747
|
-
import { existsSync as
|
|
3646
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2748
3647
|
import { dirname as dirname2 } from "node:path";
|
|
2749
3648
|
var MAX_THREADS = 50;
|
|
2750
3649
|
var MAX_CARDS_PER_THREAD = 50;
|
|
@@ -2759,7 +3658,7 @@ var AiThreadStore = class {
|
|
|
2759
3658
|
if (this.cache !== null) return this.cache;
|
|
2760
3659
|
let parsed = null;
|
|
2761
3660
|
try {
|
|
2762
|
-
if (
|
|
3661
|
+
if (existsSync6(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2763
3662
|
} catch {
|
|
2764
3663
|
parsed = null;
|
|
2765
3664
|
}
|
|
@@ -2772,10 +3671,10 @@ var AiThreadStore = class {
|
|
|
2772
3671
|
prune(data);
|
|
2773
3672
|
const text2 = `${JSON.stringify(data, null, 2)}
|
|
2774
3673
|
`;
|
|
2775
|
-
|
|
3674
|
+
mkdirSync5(dirname2(this.file), { recursive: true });
|
|
2776
3675
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2777
|
-
|
|
2778
|
-
|
|
3676
|
+
writeFileSync5(temp, text2, { mode: 384 });
|
|
3677
|
+
renameSync4(temp, this.file);
|
|
2779
3678
|
}
|
|
2780
3679
|
get(key) {
|
|
2781
3680
|
const thread = this.load().threads[key];
|
|
@@ -2783,7 +3682,7 @@ var AiThreadStore = class {
|
|
|
2783
3682
|
}
|
|
2784
3683
|
appendCard(input) {
|
|
2785
3684
|
const data = this.load();
|
|
2786
|
-
const
|
|
3685
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2787
3686
|
const existing = data.threads[input.key];
|
|
2788
3687
|
const thread = existing ?? {
|
|
2789
3688
|
key: input.key,
|
|
@@ -2791,13 +3690,13 @@ var AiThreadStore = class {
|
|
|
2791
3690
|
subject: input.subject,
|
|
2792
3691
|
messageId: input.messageId ?? "",
|
|
2793
3692
|
cards: [],
|
|
2794
|
-
updatedAt:
|
|
3693
|
+
updatedAt: now2
|
|
2795
3694
|
};
|
|
2796
3695
|
thread.accountId = input.accountId;
|
|
2797
3696
|
thread.subject = input.subject;
|
|
2798
3697
|
if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
|
|
2799
3698
|
thread.cards.push(input.card);
|
|
2800
|
-
thread.updatedAt =
|
|
3699
|
+
thread.updatedAt = now2;
|
|
2801
3700
|
data.threads[input.key] = thread;
|
|
2802
3701
|
this.flush();
|
|
2803
3702
|
return { ...thread, cards: [...thread.cards] };
|
|
@@ -2883,10 +3782,330 @@ function prune(data) {
|
|
|
2883
3782
|
}
|
|
2884
3783
|
}
|
|
2885
3784
|
|
|
3785
|
+
// src/host/tasks.ts
|
|
3786
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3787
|
+
import { mkdirSync as mkdirSync6 } from "node:fs";
|
|
3788
|
+
import { dirname as dirname3 } from "node:path";
|
|
3789
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
3790
|
+
var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
|
|
3791
|
+
function now() {
|
|
3792
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3793
|
+
}
|
|
3794
|
+
function rowToTask(row) {
|
|
3795
|
+
if (row === void 0) return void 0;
|
|
3796
|
+
return {
|
|
3797
|
+
id: String(row.id),
|
|
3798
|
+
accountId: String(row.account_id),
|
|
3799
|
+
projectId: row.project_id === null || row.project_id === void 0 ? null : String(row.project_id),
|
|
3800
|
+
calendarHref: row.calendar_href === null || row.calendar_href === void 0 ? null : String(row.calendar_href),
|
|
3801
|
+
href: row.href === null || row.href === void 0 ? null : String(row.href),
|
|
3802
|
+
etag: row.etag === null || row.etag === void 0 ? null : String(row.etag),
|
|
3803
|
+
uid: String(row.uid),
|
|
3804
|
+
title: String(row.title),
|
|
3805
|
+
notes: String(row.notes ?? ""),
|
|
3806
|
+
due: String(row.due ?? ""),
|
|
3807
|
+
status: String(row.status ?? "NEEDS-ACTION"),
|
|
3808
|
+
priority: Number(row.priority ?? 0),
|
|
3809
|
+
owner: String(row.owner ?? ""),
|
|
3810
|
+
messageKey: String(row.message_key ?? ""),
|
|
3811
|
+
messageSubject: String(row.message_subject ?? ""),
|
|
3812
|
+
completedAt: row.completed_at === null || row.completed_at === void 0 ? null : String(row.completed_at),
|
|
3813
|
+
dirty: Number(row.dirty) === 1,
|
|
3814
|
+
deleted: Number(row.deleted) === 1,
|
|
3815
|
+
createdAt: String(row.created_at ?? ""),
|
|
3816
|
+
updatedAt: String(row.updated_at ?? "")
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
function statusOf(value) {
|
|
3820
|
+
const text2 = String(value ?? "NEEDS-ACTION").toUpperCase();
|
|
3821
|
+
return STATUSES.has(text2) ? text2 : "NEEDS-ACTION";
|
|
3822
|
+
}
|
|
3823
|
+
var TaskStore = class {
|
|
3824
|
+
file;
|
|
3825
|
+
db = null;
|
|
3826
|
+
constructor(file) {
|
|
3827
|
+
this.file = file;
|
|
3828
|
+
}
|
|
3829
|
+
close() {
|
|
3830
|
+
if (this.db !== null) {
|
|
3831
|
+
this.db.close();
|
|
3832
|
+
this.db = null;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
open() {
|
|
3836
|
+
if (this.db !== null) return this.db;
|
|
3837
|
+
mkdirSync6(dirname3(this.file), { recursive: true });
|
|
3838
|
+
const db = new DatabaseSync3(this.file);
|
|
3839
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
3840
|
+
db.exec("PRAGMA busy_timeout=10000");
|
|
3841
|
+
db.exec(`
|
|
3842
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
3843
|
+
id TEXT PRIMARY KEY,
|
|
3844
|
+
account_id TEXT NOT NULL,
|
|
3845
|
+
project_id TEXT,
|
|
3846
|
+
calendar_href TEXT,
|
|
3847
|
+
href TEXT,
|
|
3848
|
+
etag TEXT,
|
|
3849
|
+
uid TEXT NOT NULL,
|
|
3850
|
+
title TEXT NOT NULL,
|
|
3851
|
+
notes TEXT,
|
|
3852
|
+
due TEXT,
|
|
3853
|
+
status TEXT NOT NULL DEFAULT 'NEEDS-ACTION',
|
|
3854
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
3855
|
+
owner TEXT,
|
|
3856
|
+
message_key TEXT,
|
|
3857
|
+
message_subject TEXT,
|
|
3858
|
+
completed_at TEXT,
|
|
3859
|
+
dirty INTEGER NOT NULL DEFAULT 1,
|
|
3860
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
3861
|
+
created_at TEXT NOT NULL,
|
|
3862
|
+
updated_at TEXT NOT NULL
|
|
3863
|
+
);
|
|
3864
|
+
CREATE INDEX IF NOT EXISTS tasks_account_due ON tasks (account_id, deleted, status, due);
|
|
3865
|
+
CREATE INDEX IF NOT EXISTS tasks_project ON tasks (project_id);
|
|
3866
|
+
CREATE TABLE IF NOT EXISTS calendar_state (
|
|
3867
|
+
account_id TEXT PRIMARY KEY,
|
|
3868
|
+
calendar_href TEXT,
|
|
3869
|
+
display_name TEXT,
|
|
3870
|
+
ctag TEXT,
|
|
3871
|
+
last_sync TEXT,
|
|
3872
|
+
error TEXT
|
|
3873
|
+
);
|
|
3874
|
+
`);
|
|
3875
|
+
this.db = db;
|
|
3876
|
+
return db;
|
|
3877
|
+
}
|
|
3878
|
+
list(options) {
|
|
3879
|
+
const db = this.open();
|
|
3880
|
+
const where = ["account_id = ?", "deleted = 0"];
|
|
3881
|
+
const params = [options.accountId];
|
|
3882
|
+
if (options.status !== void 0 && options.status !== "") {
|
|
3883
|
+
where.push("status = ?");
|
|
3884
|
+
params.push(statusOf(options.status));
|
|
3885
|
+
}
|
|
3886
|
+
if (options.projectId !== void 0 && options.projectId !== "") {
|
|
3887
|
+
where.push("project_id = ?");
|
|
3888
|
+
params.push(options.projectId);
|
|
3889
|
+
}
|
|
3890
|
+
const text2 = String(options.query ?? "").trim();
|
|
3891
|
+
if (text2 !== "") {
|
|
3892
|
+
where.push("(title LIKE ? OR notes LIKE ? OR owner LIKE ?)");
|
|
3893
|
+
const like = `%${text2}%`;
|
|
3894
|
+
params.push(like, like, like);
|
|
3895
|
+
}
|
|
3896
|
+
const rows = db.prepare(
|
|
3897
|
+
`SELECT * FROM tasks WHERE ${where.join(" AND ")}
|
|
3898
|
+
ORDER BY CASE WHEN completed_at IS NOT NULL THEN 1 ELSE 0 END,
|
|
3899
|
+
CASE WHEN due IS NULL OR due = '' THEN 1 ELSE 0 END, due ASC, created_at DESC
|
|
3900
|
+
LIMIT ?`
|
|
3901
|
+
).all(...params, Math.max(1, Math.min(1e3, options.limit ?? 500)));
|
|
3902
|
+
return rows.map((row) => rowToTask(row)).filter((task) => task !== void 0);
|
|
3903
|
+
}
|
|
3904
|
+
get(id) {
|
|
3905
|
+
return rowToTask(
|
|
3906
|
+
this.open().prepare("SELECT * FROM tasks WHERE id = ?").get(id)
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
create(input) {
|
|
3910
|
+
const title = String(input.title ?? "").trim();
|
|
3911
|
+
if (title === "") throw new Error("\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3912
|
+
if (title.length > 300) throw new Error("\u4EFB\u52A1\u6807\u9898\u8FC7\u957F");
|
|
3913
|
+
const status = statusOf(input.status);
|
|
3914
|
+
const id = `task_${randomUUID4()}`;
|
|
3915
|
+
const stamp3 = now();
|
|
3916
|
+
this.open().prepare(
|
|
3917
|
+
`INSERT INTO tasks (id, account_id, project_id, uid, title, notes, due, status, priority, owner,
|
|
3918
|
+
message_key, message_subject, completed_at, dirty, deleted, created_at, updated_at)
|
|
3919
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)`
|
|
3920
|
+
).run(
|
|
3921
|
+
id,
|
|
3922
|
+
input.accountId,
|
|
3923
|
+
input.projectId ?? null,
|
|
3924
|
+
`${randomUUID4()}@dsh-mailharbor`,
|
|
3925
|
+
title,
|
|
3926
|
+
String(input.notes ?? ""),
|
|
3927
|
+
String(input.due ?? ""),
|
|
3928
|
+
status,
|
|
3929
|
+
Math.max(0, Math.min(9, Number(input.priority ?? 0))),
|
|
3930
|
+
String(input.owner ?? ""),
|
|
3931
|
+
String(input.messageKey ?? ""),
|
|
3932
|
+
String(input.messageSubject ?? ""),
|
|
3933
|
+
status === "COMPLETED" ? stamp3 : null,
|
|
3934
|
+
stamp3,
|
|
3935
|
+
stamp3
|
|
3936
|
+
);
|
|
3937
|
+
const task = this.get(id);
|
|
3938
|
+
if (task === void 0) throw new Error("\u4EFB\u52A1\u521B\u5EFA\u5931\u8D25");
|
|
3939
|
+
return task;
|
|
3940
|
+
}
|
|
3941
|
+
update(id, fields) {
|
|
3942
|
+
const current = this.get(id);
|
|
3943
|
+
if (current === void 0) throw new Error("\u4EFB\u52A1\u4E0D\u5B58\u5728");
|
|
3944
|
+
const next = {
|
|
3945
|
+
title: fields.title === void 0 ? current.title : String(fields.title).trim(),
|
|
3946
|
+
notes: fields.notes === void 0 ? current.notes : String(fields.notes ?? ""),
|
|
3947
|
+
due: fields.due === void 0 ? current.due : String(fields.due ?? ""),
|
|
3948
|
+
status: fields.status === void 0 ? current.status : statusOf(fields.status),
|
|
3949
|
+
priority: fields.priority === void 0 ? current.priority : Number(fields.priority),
|
|
3950
|
+
owner: fields.owner === void 0 ? current.owner : String(fields.owner ?? ""),
|
|
3951
|
+
projectId: fields.projectId === void 0 ? current.projectId : fields.projectId,
|
|
3952
|
+
messageKey: fields.messageKey === void 0 ? current.messageKey : String(fields.messageKey),
|
|
3953
|
+
messageSubject: fields.messageSubject === void 0 ? current.messageSubject : String(fields.messageSubject)
|
|
3954
|
+
};
|
|
3955
|
+
if (next.title === "") throw new Error("\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3956
|
+
const stamp3 = now();
|
|
3957
|
+
const completedAt = next.status === "COMPLETED" ? current.completedAt ?? stamp3 : null;
|
|
3958
|
+
this.open().prepare(
|
|
3959
|
+
`UPDATE tasks SET title = ?, notes = ?, due = ?, status = ?, priority = ?, owner = ?, project_id = ?,
|
|
3960
|
+
message_key = ?, message_subject = ?, completed_at = ?, dirty = 1, updated_at = ? WHERE id = ?`
|
|
3961
|
+
).run(
|
|
3962
|
+
next.title,
|
|
3963
|
+
next.notes,
|
|
3964
|
+
next.due,
|
|
3965
|
+
next.status,
|
|
3966
|
+
Math.max(0, Math.min(9, Number(next.priority) || 0)),
|
|
3967
|
+
next.owner,
|
|
3968
|
+
next.projectId,
|
|
3969
|
+
next.messageKey,
|
|
3970
|
+
next.messageSubject,
|
|
3971
|
+
completedAt,
|
|
3972
|
+
stamp3,
|
|
3973
|
+
id
|
|
3974
|
+
);
|
|
3975
|
+
const task = this.get(id);
|
|
3976
|
+
if (task === void 0) throw new Error("\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25");
|
|
3977
|
+
return task;
|
|
3978
|
+
}
|
|
3979
|
+
remove(id) {
|
|
3980
|
+
const current = this.get(id);
|
|
3981
|
+
if (current === void 0) return false;
|
|
3982
|
+
if (current.href !== null && current.href !== "") {
|
|
3983
|
+
this.open().prepare("UPDATE tasks SET deleted = 1, dirty = 1, updated_at = ? WHERE id = ?").run(now(), id);
|
|
3984
|
+
} else {
|
|
3985
|
+
this.open().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
3986
|
+
}
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
/** Apply a VTODO fetched from CalDAV; local pending edits win over remote. */
|
|
3990
|
+
applyRemote(input) {
|
|
3991
|
+
const db = this.open();
|
|
3992
|
+
const existing = db.prepare("SELECT * FROM tasks WHERE account_id = ? AND href = ?").get(input.accountId, input.href) ?? db.prepare("SELECT * FROM tasks WHERE account_id = ? AND uid = ?").get(input.accountId, input.todo.uid);
|
|
3993
|
+
const status = statusOf(input.todo.status);
|
|
3994
|
+
const completedAt = status === "COMPLETED" ? input.todo.completedAt ?? now() : null;
|
|
3995
|
+
if (existing !== void 0) {
|
|
3996
|
+
if (Number(existing.dirty) === 1) return rowToTask(existing);
|
|
3997
|
+
db.prepare(
|
|
3998
|
+
`UPDATE tasks SET calendar_href = ?, href = ?, etag = ?, title = ?, notes = ?, due = ?, status = ?, priority = ?,
|
|
3999
|
+
completed_at = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?`
|
|
4000
|
+
).run(
|
|
4001
|
+
input.calendarHref,
|
|
4002
|
+
input.href,
|
|
4003
|
+
input.etag,
|
|
4004
|
+
String(input.todo.title || existing.title),
|
|
4005
|
+
String(input.todo.notes ?? ""),
|
|
4006
|
+
String(input.todo.due ?? ""),
|
|
4007
|
+
status,
|
|
4008
|
+
Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),
|
|
4009
|
+
completedAt,
|
|
4010
|
+
now(),
|
|
4011
|
+
String(existing.id)
|
|
4012
|
+
);
|
|
4013
|
+
return this.get(String(existing.id));
|
|
4014
|
+
}
|
|
4015
|
+
const id = `task_${randomUUID4()}`;
|
|
4016
|
+
const stamp3 = now();
|
|
4017
|
+
db.prepare(
|
|
4018
|
+
`INSERT INTO tasks (id, account_id, calendar_href, href, etag, uid, title, notes, due, status, priority,
|
|
4019
|
+
completed_at, dirty, deleted, created_at, updated_at)
|
|
4020
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`
|
|
4021
|
+
).run(
|
|
4022
|
+
id,
|
|
4023
|
+
input.accountId,
|
|
4024
|
+
input.calendarHref,
|
|
4025
|
+
input.href,
|
|
4026
|
+
input.etag,
|
|
4027
|
+
input.todo.uid || `${randomUUID4()}@dsh-mailharbor`,
|
|
4028
|
+
String(input.todo.title || "(\u65E0\u6807\u9898)"),
|
|
4029
|
+
String(input.todo.notes ?? ""),
|
|
4030
|
+
String(input.todo.due ?? ""),
|
|
4031
|
+
status,
|
|
4032
|
+
Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),
|
|
4033
|
+
completedAt,
|
|
4034
|
+
stamp3,
|
|
4035
|
+
stamp3
|
|
4036
|
+
);
|
|
4037
|
+
return this.get(id);
|
|
4038
|
+
}
|
|
4039
|
+
pending(accountId) {
|
|
4040
|
+
return this.open().prepare("SELECT * FROM tasks WHERE account_id = ? AND dirty = 1").all(accountId).map((row) => rowToTask(row)).filter((task) => task !== void 0);
|
|
4041
|
+
}
|
|
4042
|
+
markPushed(id, result) {
|
|
4043
|
+
this.open().prepare(
|
|
4044
|
+
"UPDATE tasks SET href = ?, etag = ?, uid = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?"
|
|
4045
|
+
).run(result.href, result.etag, result.uid, now(), id);
|
|
4046
|
+
return this.get(id);
|
|
4047
|
+
}
|
|
4048
|
+
purge(id) {
|
|
4049
|
+
this.open().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
4050
|
+
}
|
|
4051
|
+
/** Remove local copies whose remote VTODO disappeared (not dirty, not tombstoned). */
|
|
4052
|
+
pruneMissingRemote(accountId, calendarHref, hrefs) {
|
|
4053
|
+
const db = this.open();
|
|
4054
|
+
const present = new Set(hrefs);
|
|
4055
|
+
const rows = db.prepare(
|
|
4056
|
+
`SELECT id, href FROM tasks
|
|
4057
|
+
WHERE account_id = ? AND dirty = 0 AND deleted = 0 AND href IS NOT NULL
|
|
4058
|
+
AND (calendar_href = ? OR calendar_href IS NULL OR calendar_href = '')`
|
|
4059
|
+
).all(accountId, calendarHref);
|
|
4060
|
+
let removed = 0;
|
|
4061
|
+
for (const row of rows) {
|
|
4062
|
+
if (present.has(String(row.href))) continue;
|
|
4063
|
+
db.prepare("DELETE FROM tasks WHERE id = ?").run(String(row.id));
|
|
4064
|
+
removed += 1;
|
|
4065
|
+
}
|
|
4066
|
+
return removed;
|
|
4067
|
+
}
|
|
4068
|
+
calendarState(accountId) {
|
|
4069
|
+
const row = this.open().prepare("SELECT * FROM calendar_state WHERE account_id = ?").get(accountId);
|
|
4070
|
+
if (row === void 0) return null;
|
|
4071
|
+
return {
|
|
4072
|
+
calendarHref: String(row.calendar_href ?? ""),
|
|
4073
|
+
lastSync: String(row.last_sync ?? ""),
|
|
4074
|
+
error: String(row.error ?? "")
|
|
4075
|
+
};
|
|
4076
|
+
}
|
|
4077
|
+
setCalendarState(accountId, state) {
|
|
4078
|
+
this.open().prepare(
|
|
4079
|
+
`INSERT INTO calendar_state (account_id, calendar_href, display_name, ctag, last_sync, error)
|
|
4080
|
+
VALUES (?, ?, NULL, NULL, ?, ?)
|
|
4081
|
+
ON CONFLICT(account_id) DO UPDATE SET calendar_href = excluded.calendar_href,
|
|
4082
|
+
last_sync = excluded.last_sync, error = excluded.error`
|
|
4083
|
+
).run(accountId, state.calendarHref ?? null, state.lastSync ?? null, state.error ?? null);
|
|
4084
|
+
}
|
|
4085
|
+
/** Pull remote tasks, push local edits, and record the sync result. */
|
|
4086
|
+
async sync(accountId, client) {
|
|
4087
|
+
try {
|
|
4088
|
+
const result = await client.sync(this, accountId);
|
|
4089
|
+
this.setCalendarState(accountId, {
|
|
4090
|
+
calendarHref: result.calendarHref,
|
|
4091
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4092
|
+
error: null
|
|
4093
|
+
});
|
|
4094
|
+
return result;
|
|
4095
|
+
} catch (error) {
|
|
4096
|
+
this.setCalendarState(accountId, {
|
|
4097
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4098
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4099
|
+
});
|
|
4100
|
+
throw error;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
|
|
2886
4105
|
// src/host/drafts.ts
|
|
2887
|
-
import { randomUUID as
|
|
2888
|
-
import { mkdirSync as
|
|
2889
|
-
import { dirname as
|
|
4106
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
4107
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4108
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
2890
4109
|
function text(value) {
|
|
2891
4110
|
return value === void 0 || value === null ? "" : String(value);
|
|
2892
4111
|
}
|
|
@@ -2897,7 +4116,7 @@ function isDraft(value) {
|
|
|
2897
4116
|
return value !== null && typeof value === "object" && typeof value.id === "string";
|
|
2898
4117
|
}
|
|
2899
4118
|
function draftPath(root) {
|
|
2900
|
-
return
|
|
4119
|
+
return join6(root, "drafts.json");
|
|
2901
4120
|
}
|
|
2902
4121
|
var DraftStore = class {
|
|
2903
4122
|
file;
|
|
@@ -2910,18 +4129,18 @@ var DraftStore = class {
|
|
|
2910
4129
|
}
|
|
2911
4130
|
load() {
|
|
2912
4131
|
try {
|
|
2913
|
-
const parsed = JSON.parse(
|
|
4132
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
2914
4133
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2915
4134
|
} catch {
|
|
2916
4135
|
this.items = [];
|
|
2917
4136
|
}
|
|
2918
4137
|
}
|
|
2919
4138
|
flush() {
|
|
2920
|
-
|
|
4139
|
+
mkdirSync7(dirname4(this.file), { recursive: true });
|
|
2921
4140
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2922
|
-
|
|
4141
|
+
writeFileSync6(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2923
4142
|
`, { encoding: "utf8", mode: 384 });
|
|
2924
|
-
|
|
4143
|
+
renameSync5(temp, this.file);
|
|
2925
4144
|
}
|
|
2926
4145
|
list() {
|
|
2927
4146
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2931,7 +4150,7 @@ var DraftStore = class {
|
|
|
2931
4150
|
return found === void 0 ? void 0 : { ...found };
|
|
2932
4151
|
}
|
|
2933
4152
|
save(input) {
|
|
2934
|
-
const
|
|
4153
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2935
4154
|
const existing = input.id === void 0 ? void 0 : this.items.find((draft2) => draft2.id === input.id);
|
|
2936
4155
|
const fields = {
|
|
2937
4156
|
to: text(input.to),
|
|
@@ -2948,15 +4167,15 @@ var DraftStore = class {
|
|
|
2948
4167
|
references: text(input.references)
|
|
2949
4168
|
};
|
|
2950
4169
|
if (existing !== void 0) {
|
|
2951
|
-
Object.assign(existing, fields, { updatedAt:
|
|
4170
|
+
Object.assign(existing, fields, { updatedAt: now2 });
|
|
2952
4171
|
this.flush();
|
|
2953
4172
|
return { ...existing };
|
|
2954
4173
|
}
|
|
2955
4174
|
const draft = {
|
|
2956
|
-
id:
|
|
4175
|
+
id: randomUUID5(),
|
|
2957
4176
|
accountId: this.accountId,
|
|
2958
4177
|
...fields,
|
|
2959
|
-
updatedAt:
|
|
4178
|
+
updatedAt: now2
|
|
2960
4179
|
};
|
|
2961
4180
|
this.items.push(draft);
|
|
2962
4181
|
this.flush();
|
|
@@ -2978,8 +4197,8 @@ var DraftStore = class {
|
|
|
2978
4197
|
init_imap();
|
|
2979
4198
|
|
|
2980
4199
|
// src/host/read-state.ts
|
|
2981
|
-
import { mkdirSync as
|
|
2982
|
-
import { dirname as
|
|
4200
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4201
|
+
import { dirname as dirname5 } from "node:path";
|
|
2983
4202
|
var ReadStateStore = class {
|
|
2984
4203
|
file;
|
|
2985
4204
|
keys = null;
|
|
@@ -2989,7 +4208,7 @@ var ReadStateStore = class {
|
|
|
2989
4208
|
load() {
|
|
2990
4209
|
if (this.keys !== null) return this.keys;
|
|
2991
4210
|
try {
|
|
2992
|
-
const parsed = JSON.parse(
|
|
4211
|
+
const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
|
|
2993
4212
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
2994
4213
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
2995
4214
|
} catch {
|
|
@@ -2998,14 +4217,14 @@ var ReadStateStore = class {
|
|
|
2998
4217
|
return this.keys;
|
|
2999
4218
|
}
|
|
3000
4219
|
flush() {
|
|
3001
|
-
|
|
4220
|
+
mkdirSync8(dirname5(this.file), { recursive: true });
|
|
3002
4221
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
3003
|
-
|
|
4222
|
+
writeFileSync7(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
3004
4223
|
`, {
|
|
3005
4224
|
encoding: "utf8",
|
|
3006
4225
|
mode: 384
|
|
3007
4226
|
});
|
|
3008
|
-
|
|
4227
|
+
renameSync6(temp, this.file);
|
|
3009
4228
|
}
|
|
3010
4229
|
isUnread(key) {
|
|
3011
4230
|
return key !== "" && this.load().has(key);
|
|
@@ -3064,12 +4283,30 @@ var MailHarborRuntime = class {
|
|
|
3064
4283
|
aiStores = /* @__PURE__ */ new Map();
|
|
3065
4284
|
backups = /* @__PURE__ */ new Map();
|
|
3066
4285
|
llmProvider;
|
|
4286
|
+
workspaceRegistry;
|
|
4287
|
+
sessions;
|
|
3067
4288
|
aiService = null;
|
|
4289
|
+
projectStore = null;
|
|
4290
|
+
taskStore = null;
|
|
3068
4291
|
disposed = false;
|
|
3069
4292
|
constructor(options = {}) {
|
|
3070
4293
|
this.config = loadConfig();
|
|
3071
4294
|
this.credentials = options.credentials ?? new CredentialManager();
|
|
3072
4295
|
this.llmProvider = options.llm ?? (() => void 0);
|
|
4296
|
+
this.workspaceRegistry = options.workspaceRegistry;
|
|
4297
|
+
this.sessions = options.sessions;
|
|
4298
|
+
}
|
|
4299
|
+
projects() {
|
|
4300
|
+
if (this.projectStore === null) {
|
|
4301
|
+
this.projectStore = new ProjectStore(join7(this.config.dataDir, "projects.json"));
|
|
4302
|
+
}
|
|
4303
|
+
return this.projectStore;
|
|
4304
|
+
}
|
|
4305
|
+
tasks() {
|
|
4306
|
+
if (this.taskStore === null) {
|
|
4307
|
+
this.taskStore = new TaskStore(join7(this.config.dataDir, "state", "state.db"));
|
|
4308
|
+
}
|
|
4309
|
+
return this.taskStore;
|
|
3073
4310
|
}
|
|
3074
4311
|
/** The Host model service, when the composition provides one. */
|
|
3075
4312
|
llm() {
|
|
@@ -3195,7 +4432,7 @@ var MailHarborRuntime = class {
|
|
|
3195
4432
|
return created;
|
|
3196
4433
|
}
|
|
3197
4434
|
aiThreads(account) {
|
|
3198
|
-
const file =
|
|
4435
|
+
const file = join7(this.paths(account).root, "ai-threads.json");
|
|
3199
4436
|
const existing = this.aiStores.get(account.id);
|
|
3200
4437
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3201
4438
|
const created = new AiThreadStore(file);
|
|
@@ -3203,7 +4440,7 @@ var MailHarborRuntime = class {
|
|
|
3203
4440
|
return created;
|
|
3204
4441
|
}
|
|
3205
4442
|
readState(account) {
|
|
3206
|
-
const file =
|
|
4443
|
+
const file = join7(this.paths(account).root, "read-state.json");
|
|
3207
4444
|
const existing = this.readStores.get(account.id);
|
|
3208
4445
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3209
4446
|
const created = new ReadStateStore(file);
|
|
@@ -3282,6 +4519,7 @@ var MailHarborRuntime = class {
|
|
|
3282
4519
|
this.providers.clear();
|
|
3283
4520
|
for (const db of this.databases.values()) db.value.close();
|
|
3284
4521
|
this.databases.clear();
|
|
4522
|
+
this.taskStore?.close();
|
|
3285
4523
|
}
|
|
3286
4524
|
};
|
|
3287
4525
|
|
|
@@ -3664,8 +4902,22 @@ ${page.items.map(
|
|
|
3664
4902
|
var name = "mailharbor";
|
|
3665
4903
|
var inject = ["webServer", "tools"];
|
|
3666
4904
|
var ROUTE = "/mail-plugin";
|
|
4905
|
+
function optionalService(ctx, name2) {
|
|
4906
|
+
try {
|
|
4907
|
+
const direct = ctx[name2];
|
|
4908
|
+
if (direct !== void 0 && direct !== null) return direct;
|
|
4909
|
+
} catch {
|
|
4910
|
+
}
|
|
4911
|
+
try {
|
|
4912
|
+
return ctx.get?.(name2) ?? void 0;
|
|
4913
|
+
} catch {
|
|
4914
|
+
return void 0;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
3667
4917
|
function apply(ctx) {
|
|
3668
4918
|
const runtime = new MailHarborRuntime({
|
|
4919
|
+
workspaceRegistry: optionalService(ctx, "workspaceRegistry"),
|
|
4920
|
+
sessions: optionalService(ctx, "sessions"),
|
|
3669
4921
|
llm: () => {
|
|
3670
4922
|
if (typeof ctx.get !== "function") return void 0;
|
|
3671
4923
|
try {
|