@chance722/dsh-inbox 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/README.zh.md +182 -0
- package/cordis.patch.yml +11 -0
- package/lib/cli.js +259 -0
- package/lib/client.js +3823 -0
- package/lib/index.js +3718 -0
- package/lib/types/cli.d.ts +49 -0
- package/lib/types/client/card.d.ts +52 -0
- package/lib/types/client/dock.d.ts +52 -0
- package/lib/types/client/heading.d.ts +54 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/manual.d.ts +22 -0
- package/lib/types/client/scheme.d.ts +47 -0
- package/lib/types/host/capture.d.ts +117 -0
- package/lib/types/host/classify/model.d.ts +112 -0
- package/lib/types/host/classify/redact.d.ts +18 -0
- package/lib/types/host/classify/rules.d.ts +58 -0
- package/lib/types/host/command.d.ts +18 -0
- package/lib/types/host/crypto/secret-box.d.ts +60 -0
- package/lib/types/host/index.d.ts +21 -0
- package/lib/types/host/link-title.d.ts +64 -0
- package/lib/types/host/remote/auto-push.d.ts +37 -0
- package/lib/types/host/remote/merge.d.ts +80 -0
- package/lib/types/host/remote/pull.d.ts +60 -0
- package/lib/types/host/remote/push.d.ts +101 -0
- package/lib/types/host/remote/remove.d.ts +65 -0
- package/lib/types/host/remote/writer.d.ts +43 -0
- package/lib/types/host/rpc.d.ts +36 -0
- package/lib/types/host/s3/client.d.ts +216 -0
- package/lib/types/host/s3/probe.d.ts +28 -0
- package/lib/types/host/tools.d.ts +56 -0
- package/lib/types/host/ui/config.d.ts +54 -0
- package/lib/types/host/vault/lease.d.ts +54 -0
- package/lib/types/host/vault/query.d.ts +38 -0
- package/lib/types/host/vault/spec.d.ts +193 -0
- package/lib/types/host/vault/vault.d.ts +269 -0
- package/lib/types/host/webdav/client.d.ts +102 -0
- package/lib/types/host/webdav/config.d.ts +129 -0
- package/lib/types/host/webdav/probe.d.ts +22 -0
- package/lib/types/host/webdav/run.d.ts +34 -0
- package/lib/types/shared/constants.d.ts +23 -0
- package/lib/types/shared/panel-wire.d.ts +474 -0
- package/lib/types/shared/vocabulary.d.ts +39 -0
- package/package.json +101 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,3718 @@
|
|
|
1
|
+
// src/host/index.ts
|
|
2
|
+
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
|
|
4
|
+
// src/shared/constants.ts
|
|
5
|
+
var PACKAGE_NAME = "@chance722/dsh-inbox";
|
|
6
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
7
|
+
var DEFAULT_USER_AGENT = "dsh-inbox";
|
|
8
|
+
|
|
9
|
+
// src/shared/vocabulary.ts
|
|
10
|
+
var KINDS = ["text", "link", "image", "file"];
|
|
11
|
+
var CATEGORIES = [
|
|
12
|
+
"idea",
|
|
13
|
+
"article",
|
|
14
|
+
"media",
|
|
15
|
+
"image",
|
|
16
|
+
"document",
|
|
17
|
+
"secret",
|
|
18
|
+
"other"
|
|
19
|
+
];
|
|
20
|
+
var SOURCES = ["panel", "chat", "webdav", "import"];
|
|
21
|
+
var CATEGORY_SOURCES = ["rule", "model", "user"];
|
|
22
|
+
var CATEGORY_LABELS = {
|
|
23
|
+
idea: "\u7075\u611F/\u5F85\u529E",
|
|
24
|
+
article: "\u6587\u7AE0",
|
|
25
|
+
media: "\u89C6\u9891/\u97F3\u9891",
|
|
26
|
+
image: "\u56FE\u7247",
|
|
27
|
+
document: "\u8BC1\u4EF6",
|
|
28
|
+
secret: "\u5BC6\u94A5/\u8D26\u5BC6",
|
|
29
|
+
other: "\u5176\u5B83"
|
|
30
|
+
};
|
|
31
|
+
var KIND_LABELS = {
|
|
32
|
+
text: "\u6587\u672C",
|
|
33
|
+
link: "\u94FE\u63A5",
|
|
34
|
+
image: "\u56FE\u7247",
|
|
35
|
+
file: "\u6587\u4EF6"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/host/classify/redact.ts
|
|
39
|
+
var ASSIGNMENT = /\b((?:secret|access)[-_]?(?:id|key)|api[-_]?key|apikey|password|passwd|pwd|token|bearer)\b(\s*[:=]\s*)("[^"]{4,}"|'[^']{4,}'|\S{4,})/gi;
|
|
40
|
+
var TOKENS = [
|
|
41
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
42
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
43
|
+
/\bghp_[A-Za-z0-9]{20,}\b/g,
|
|
44
|
+
/\bmongodb(\+srv)?:\/\/[^\s:@]+:[^\s:@]+@/gi
|
|
45
|
+
];
|
|
46
|
+
var PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
47
|
+
function mask(value) {
|
|
48
|
+
const length = Math.max(4, Math.min(24, value.length));
|
|
49
|
+
return "\u2022".repeat(length);
|
|
50
|
+
}
|
|
51
|
+
function redact(text) {
|
|
52
|
+
let out = text.replace(PRIVATE_KEY, "\xAB\u79C1\u94A5\u5DF2\u8131\u654F\xBB");
|
|
53
|
+
out = out.replace(
|
|
54
|
+
ASSIGNMENT,
|
|
55
|
+
(_match, label, separator, value) => `${label}${separator}${mask(value)}`
|
|
56
|
+
);
|
|
57
|
+
for (const pattern of TOKENS) {
|
|
58
|
+
out = out.replace(pattern, (match) => mask(match));
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/host/classify/model.ts
|
|
64
|
+
var PROVIDER = "deepseek-official";
|
|
65
|
+
var MODEL = "deepseek-flash";
|
|
66
|
+
var DEFAULT_BUDGET = {
|
|
67
|
+
maxCallsPerDay: 200,
|
|
68
|
+
maxTokensPerDay: 1e5,
|
|
69
|
+
maxTokensPerCall: 512
|
|
70
|
+
};
|
|
71
|
+
function today(now = /* @__PURE__ */ new Date()) {
|
|
72
|
+
const month = `${now.getMonth() + 1}`.padStart(2, "0");
|
|
73
|
+
const day = `${now.getDate()}`.padStart(2, "0");
|
|
74
|
+
return `${String(now.getFullYear())}-${month}-${day}`;
|
|
75
|
+
}
|
|
76
|
+
function freshSpend(now = /* @__PURE__ */ new Date()) {
|
|
77
|
+
return { day: today(now), calls: 0, tokens: 0 };
|
|
78
|
+
}
|
|
79
|
+
function rollSpend(spend, now = /* @__PURE__ */ new Date()) {
|
|
80
|
+
if (spend === void 0 || spend.day !== today(now)) return freshSpend(now);
|
|
81
|
+
return spend;
|
|
82
|
+
}
|
|
83
|
+
function withinBudget(spend, budget = DEFAULT_BUDGET) {
|
|
84
|
+
return spend.calls < budget.maxCallsPerDay && spend.tokens < budget.maxTokensPerDay;
|
|
85
|
+
}
|
|
86
|
+
function spendOf(spend, tokens) {
|
|
87
|
+
return { day: spend.day, calls: spend.calls + 1, tokens: spend.tokens + Math.max(0, tokens) };
|
|
88
|
+
}
|
|
89
|
+
function shouldAskModel(verdict, item, minimumChars = 8) {
|
|
90
|
+
if (item.kind === "image") return item.attachmentIds.length > 0;
|
|
91
|
+
if (verdict.confidence !== "unsure") return false;
|
|
92
|
+
if (item.kind === "link") {
|
|
93
|
+
return verdict.platform !== void 0;
|
|
94
|
+
}
|
|
95
|
+
if (item.kind !== "text") return false;
|
|
96
|
+
const subject = item.text ?? item.url ?? "";
|
|
97
|
+
return subject.trim().length >= minimumChars;
|
|
98
|
+
}
|
|
99
|
+
function parseCategory(answer) {
|
|
100
|
+
const normalized = answer.toLowerCase();
|
|
101
|
+
return CATEGORIES.find((category) => normalized.includes(category));
|
|
102
|
+
}
|
|
103
|
+
async function classifyWithModel(ctx, vault, item, verdict, budget = DEFAULT_BUDGET) {
|
|
104
|
+
if (!shouldAskModel(verdict, item)) {
|
|
105
|
+
return { kind: "skipped", reason: "\u89C4\u5219\u5DF2\u7ECF\u5B9A\u4E86\uFF0C\u6216\u8005\u5185\u5BB9\u592A\u77ED" };
|
|
106
|
+
}
|
|
107
|
+
const llm = ctx.get("llm");
|
|
108
|
+
if (llm === void 0) {
|
|
109
|
+
const reason = "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u6A21\u578B\u670D\u52A1";
|
|
110
|
+
await vault.setModelSpend(rollSpend(vault.global.model), `skipped: ${reason}`);
|
|
111
|
+
return { kind: "skipped", reason };
|
|
112
|
+
}
|
|
113
|
+
const spend = rollSpend(vault.global.model);
|
|
114
|
+
if (!withinBudget(spend, budget)) {
|
|
115
|
+
const reason = "\u4ECA\u5929\u7684\u6A21\u578B\u9884\u7B97\u7528\u5B8C\u4E86";
|
|
116
|
+
await vault.setModelSpend(spend, `skipped: ${reason}`);
|
|
117
|
+
return { kind: "skipped", reason };
|
|
118
|
+
}
|
|
119
|
+
const subject = redact((item.text ?? item.url ?? "").slice(0, 2e3));
|
|
120
|
+
const image = item.kind === "image" ? firstImage(vault, item) : void 0;
|
|
121
|
+
if (item.kind === "image" && image === void 0) {
|
|
122
|
+
const reason = "\u8FD9\u6761\u56FE\u7247\u8BB0\u5F55\u6CA1\u6709\u53EF\u7528\u7684\u9644\u4EF6";
|
|
123
|
+
await vault.setModelSpend(rollSpend(vault.global.model), `skipped: ${reason}`);
|
|
124
|
+
return { kind: "skipped", reason };
|
|
125
|
+
}
|
|
126
|
+
let answer = "";
|
|
127
|
+
let reasoning = "";
|
|
128
|
+
let tokens = 0;
|
|
129
|
+
try {
|
|
130
|
+
const stream = llm.stream({
|
|
131
|
+
provider: PROVIDER,
|
|
132
|
+
model: MODEL,
|
|
133
|
+
maxTokens: budget.maxTokensPerCall,
|
|
134
|
+
temperature: 0,
|
|
135
|
+
system: `\u4F60\u5728\u7ED9\u4E00\u4E2A\u4E2A\u4EBA\u6536\u85CF\u5939\u5206\u7C7B\u3002\u53EA\u56DE\u4E00\u4E2A\u8BCD\uFF0C\u4ECE\u8FD9\u4E9B\u91CC\u9009\uFF1A${CATEGORIES.join(" / ")}\u3002\u4E0D\u8981\u89E3\u91CA\uFF0C\u4E0D\u8981\u6807\u70B9\u3002`,
|
|
136
|
+
messages: [
|
|
137
|
+
{
|
|
138
|
+
role: "user",
|
|
139
|
+
content: [
|
|
140
|
+
...image === void 0 ? [] : [image],
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
text: image === void 0 ? `\u8FD9\u6BB5\u4E1C\u897F\u5C5E\u4E8E\u54EA\u4E00\u7C7B\uFF1F
|
|
144
|
+
|
|
145
|
+
${subject}` : "\u8FD9\u5F20\u56FE\u5C5E\u4E8E\u54EA\u4E00\u7C7B\uFF1F\uFF08\u624B\u673A\u62CD\u7684\u7167\u7247\u5F80\u5F80\u662F\u8BC1\u4EF6\u7167\uFF0C\u6240\u4EE5\u4E0D\u8981\u53EA\u770B\u5F62\u72B6\uFF0C\u770B\u56FE\u91CC\u6709\u4EC0\u4E48\uFF09"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
for await (const chunk of stream) {
|
|
152
|
+
if (chunk.type === "text-delta") answer += chunk.text;
|
|
153
|
+
else if (chunk.type === "reasoning-delta") reasoning += chunk.text;
|
|
154
|
+
else if (chunk.type === "usage") {
|
|
155
|
+
tokens = (chunk.usage?.inputTokens ?? 0) + (chunk.usage?.outputTokens ?? 0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
160
|
+
await vault.setModelSpend(spendOf(spend, tokens), `failed: ${reason}`);
|
|
161
|
+
return { kind: "failed", reason };
|
|
162
|
+
}
|
|
163
|
+
const after = spendOf(spend, tokens);
|
|
164
|
+
const category = parseCategory(answer) ?? parseCategory(reasoning);
|
|
165
|
+
if (category === void 0) {
|
|
166
|
+
const said = answer.trim() || reasoning.trim();
|
|
167
|
+
const reason = `\u6A21\u578B\u6CA1\u7ED9\u51FA\u53EF\u7528\u7C7B\u76EE\uFF0C\u5B83\u8BF4\u7684\u662F\uFF1A${said.slice(0, 60) || "\uFF08\u7A7A\uFF09"}`;
|
|
168
|
+
await vault.setModelSpend(after, `failed: ${reason}`);
|
|
169
|
+
return { kind: "failed", reason };
|
|
170
|
+
}
|
|
171
|
+
const current = vault.get(item.id);
|
|
172
|
+
if (current === void 0 || current.categorySource === "user") {
|
|
173
|
+
const reason = "\u8BB0\u5F55\u5DF2\u7ECF\u4E0D\u5728\uFF0C\u6216\u8005\u7528\u6237\u81EA\u5DF1\u5B9A\u4E86\u7C7B\u76EE";
|
|
174
|
+
await vault.setModelSpend(after, `skipped: ${reason}`);
|
|
175
|
+
return { kind: "skipped", reason };
|
|
176
|
+
}
|
|
177
|
+
await vault.patch(item.id, { category, categorySource: "model" });
|
|
178
|
+
await vault.setModelSpend(after, `applied: ${category} (${String(tokens)} tokens)`);
|
|
179
|
+
return { kind: "applied", category, tokens };
|
|
180
|
+
}
|
|
181
|
+
function firstImage(vault, item) {
|
|
182
|
+
for (const attachmentId of item.attachmentIds) {
|
|
183
|
+
const record = vault.getAttachment(attachmentId);
|
|
184
|
+
if (record === void 0 || !record.mime.startsWith("image/")) continue;
|
|
185
|
+
return {
|
|
186
|
+
type: "image",
|
|
187
|
+
attachment: {
|
|
188
|
+
attachmentId: record.storeId,
|
|
189
|
+
mediaType: record.mime,
|
|
190
|
+
bytes: record.bytes,
|
|
191
|
+
width: record.width ?? 0,
|
|
192
|
+
height: record.height ?? 0,
|
|
193
|
+
...record.filename === void 0 ? {} : { name: record.filename }
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/host/link-title.ts
|
|
201
|
+
var MAX_LINK_TITLE_CHARS = 200;
|
|
202
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
203
|
+
var ENTITIES = {
|
|
204
|
+
amp: "&",
|
|
205
|
+
lt: "<",
|
|
206
|
+
gt: ">",
|
|
207
|
+
quot: '"',
|
|
208
|
+
apos: "'",
|
|
209
|
+
nbsp: " "
|
|
210
|
+
};
|
|
211
|
+
function decodeEntities(text) {
|
|
212
|
+
return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, body) => {
|
|
213
|
+
if (body.startsWith("#")) {
|
|
214
|
+
const hex = body[1] === "x" || body[1] === "X";
|
|
215
|
+
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
216
|
+
return Number.isInteger(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
217
|
+
}
|
|
218
|
+
return ENTITIES[body.toLowerCase()] ?? whole;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function metaContent(html, keys) {
|
|
222
|
+
for (const key of keys) {
|
|
223
|
+
const tag = new RegExp(`<meta[^>]+(?:property|name)=["']${key}["'][^>]*>`, "i").exec(html)?.[0];
|
|
224
|
+
if (tag === void 0) continue;
|
|
225
|
+
const content = /content=["']([^"']*)["']/i.exec(tag)?.[1];
|
|
226
|
+
if (content !== void 0 && content.trim().length > 0) return content;
|
|
227
|
+
}
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
function titleFromHtml(html) {
|
|
231
|
+
const candidates = [
|
|
232
|
+
/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1],
|
|
233
|
+
metaContent(html, ["og:title", "twitter:title"])
|
|
234
|
+
];
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
if (candidate === void 0) continue;
|
|
237
|
+
const flat = decodeEntities(candidate).replace(/\s+/g, " ").trim();
|
|
238
|
+
if (flat.length === 0) continue;
|
|
239
|
+
return flat.length > MAX_LINK_TITLE_CHARS ? flat.slice(0, MAX_LINK_TITLE_CHARS) : flat;
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
async function fetchLinkTitle(vault, id, web, log = () => {
|
|
244
|
+
}) {
|
|
245
|
+
const filed = vault.get(id);
|
|
246
|
+
if (filed === void 0 || filed.kind !== "link" || filed.url === void 0) return void 0;
|
|
247
|
+
if (filed.title !== void 0 || filed.linkTitle !== void 0) return void 0;
|
|
248
|
+
const host = (() => {
|
|
249
|
+
try {
|
|
250
|
+
return new URL(filed.url).host;
|
|
251
|
+
} catch {
|
|
252
|
+
return "\uFF08\u65E0\u6CD5\u89E3\u6790\u7684\u5730\u5740\uFF09";
|
|
253
|
+
}
|
|
254
|
+
})();
|
|
255
|
+
let result;
|
|
256
|
+
try {
|
|
257
|
+
result = await web.fetch({ url: filed.url }, AbortSignal.timeout(FETCH_TIMEOUT_MS));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
260
|
+
await miss(vault, id, `network:${reason}`);
|
|
261
|
+
log(`${host}\uFF1A\u8BF7\u6C42\u5931\u8D25\uFF08${reason}\uFF09`);
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
if (result.statusCode >= 400) {
|
|
265
|
+
await miss(vault, id, `http:${String(result.statusCode)}`);
|
|
266
|
+
log(`${host}\uFF1AHTTP ${String(result.statusCode)}`);
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
if (result.body.kind !== "html") {
|
|
270
|
+
await miss(vault, id, `not-html:${result.body.kind}`);
|
|
271
|
+
log(`${host}\uFF1A\u4E0D\u662F HTML\uFF08${result.body.kind}\uFF09`);
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
const title = titleFromHtml(result.body.content);
|
|
275
|
+
if (title === void 0) {
|
|
276
|
+
await miss(vault, id, "no-title");
|
|
277
|
+
log(`${host}\uFF1A\u9875\u9762\u91CC\u6CA1\u6709 <title>\uFF08${String(result.body.content.length)} \u5B57\u7B26\uFF09`);
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
const current = vault.get(id);
|
|
281
|
+
if (current === void 0 || current.title !== void 0 || current.linkTitle !== void 0) {
|
|
282
|
+
log(`${host}\uFF1A\u6293\u5230\u4E86\u6807\u9898\uFF0C\u4F46\u8BB0\u5F55\u5DF2\u7ECF\u6709\u540D\u5B57\u4E86`);
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
await vault.patch(id, { linkTitle: title, linkTitleError: "" });
|
|
286
|
+
log(`${host}\uFF1A\u6807\u9898\u300C${title}\u300D`);
|
|
287
|
+
return title;
|
|
288
|
+
}
|
|
289
|
+
async function miss(vault, id, reason) {
|
|
290
|
+
const current = vault.get(id);
|
|
291
|
+
if (current !== void 0 && current.title === void 0 && current.linkTitle === void 0) {
|
|
292
|
+
await vault.patch(id, { linkTitleError: reason });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/host/classify/rules.ts
|
|
297
|
+
var PLATFORMS = [
|
|
298
|
+
["bilibili.com", "bilibili"],
|
|
299
|
+
["b23.tv", "bilibili"],
|
|
300
|
+
["mp.weixin.qq.com", "wechat"],
|
|
301
|
+
["weixin.qq.com", "wechat"],
|
|
302
|
+
["zhihu.com", "zhihu"],
|
|
303
|
+
["xiaohongshu.com", "xiaohongshu"],
|
|
304
|
+
["xhslink.com", "xiaohongshu"],
|
|
305
|
+
["maimai.cn", "maimai"],
|
|
306
|
+
["github.com", "github"],
|
|
307
|
+
["youtube.com", "youtube"],
|
|
308
|
+
["youtu.be", "youtube"],
|
|
309
|
+
["x.com", "twitter"],
|
|
310
|
+
["twitter.com", "twitter"]
|
|
311
|
+
];
|
|
312
|
+
var SECRET_PATTERNS = [
|
|
313
|
+
[/\b(secret|access)[-_]?(id|key)\b\s*[:=]/i, "\u51FA\u73B0 secretId / secretKey \u8D4B\u503C"],
|
|
314
|
+
[/\b(api[-_]?key|apikey)\b\s*[:=]/i, "\u51FA\u73B0 api key \u8D4B\u503C"],
|
|
315
|
+
[/\b(password|passwd|pwd)\b\s*[:=]\s*\S/i, "\u51FA\u73B0\u5BC6\u7801\u8D4B\u503C"],
|
|
316
|
+
[/\b(token|bearer)\b\s*[:=]\s*\S/i, "\u51FA\u73B0 token \u8D4B\u503C"],
|
|
317
|
+
[/\bsk-[A-Za-z0-9_-]{16,}\b/, "\u51FA\u73B0 sk- \u5F62\u5F0F\u7684\u5BC6\u94A5"],
|
|
318
|
+
[/\bAKIA[0-9A-Z]{16}\b/, "\u51FA\u73B0 AWS access key id"],
|
|
319
|
+
[/\bghp_[A-Za-z0-9]{20,}\b/, "\u51FA\u73B0 GitHub token"],
|
|
320
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "\u51FA\u73B0\u79C1\u94A5\u5757"],
|
|
321
|
+
[/\bmongodb(\+srv)?:\/\/[^\s]+:[^\s]+@/i, "\u51FA\u73B0\u5E26\u53E3\u4EE4\u7684\u8FDE\u63A5\u4E32"]
|
|
322
|
+
];
|
|
323
|
+
function findSecret(text) {
|
|
324
|
+
for (const [pattern, reason] of SECRET_PATTERNS) {
|
|
325
|
+
if (pattern.test(text)) return reason;
|
|
326
|
+
}
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
function hostOf(url) {
|
|
330
|
+
try {
|
|
331
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
332
|
+
} catch {
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function platformOf(url) {
|
|
337
|
+
const host = hostOf(url);
|
|
338
|
+
if (host === void 0) return void 0;
|
|
339
|
+
for (const [suffix, platform] of PLATFORMS) {
|
|
340
|
+
if (host === suffix || host.endsWith(`.${suffix}`)) return platform;
|
|
341
|
+
}
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
function isMedia(host, path) {
|
|
345
|
+
if (/bilibili\.com$|b23\.tv$|youtube\.com$|youtu\.be$/.test(host)) {
|
|
346
|
+
return !path.startsWith("/read/");
|
|
347
|
+
}
|
|
348
|
+
return /\/video\/|\/watch\b|\/audio\/|\/podcast/.test(path);
|
|
349
|
+
}
|
|
350
|
+
function isArticle(host, path) {
|
|
351
|
+
if (/mp\.weixin\.qq\.com$|weixin\.qq\.com$/.test(host)) return true;
|
|
352
|
+
if (/zhihu\.com$/.test(host)) return true;
|
|
353
|
+
if (/xiaohongshu\.com$|xhslink\.com$/.test(host)) return true;
|
|
354
|
+
return /\/article\/|\/post\/|\/blog\/|\/read\//.test(path);
|
|
355
|
+
}
|
|
356
|
+
function classifyLink(url) {
|
|
357
|
+
const platform = platformOf(url);
|
|
358
|
+
const parsed = (() => {
|
|
359
|
+
try {
|
|
360
|
+
return new URL(url);
|
|
361
|
+
} catch {
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
})();
|
|
365
|
+
if (parsed === void 0) {
|
|
366
|
+
return { category: "other", confidence: "unsure", reason: "\u94FE\u63A5\u89E3\u6790\u4E0D\u4E86" };
|
|
367
|
+
}
|
|
368
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
369
|
+
const path = parsed.pathname;
|
|
370
|
+
const base = platform === void 0 ? {} : { platform };
|
|
371
|
+
if (isMedia(host, path)) {
|
|
372
|
+
return { ...base, category: "media", confidence: "decided", reason: "\u94FE\u63A5\u6307\u5411\u89C6\u9891/\u97F3\u9891\u9875" };
|
|
373
|
+
}
|
|
374
|
+
if (isArticle(host, path)) {
|
|
375
|
+
return { ...base, category: "article", confidence: "decided", reason: "\u94FE\u63A5\u6307\u5411\u6587\u7AE0\u9875" };
|
|
376
|
+
}
|
|
377
|
+
if (platform !== void 0) {
|
|
378
|
+
return { ...base, category: "other", confidence: "unsure", reason: `\u8BA4\u5F97\u51FA\u5E73\u53F0\u662F ${platform}\uFF0C\u4F46\u8BF4\u4E0D\u51C6\u662F\u6587\u7AE0\u8FD8\u662F\u89C6\u9891` };
|
|
379
|
+
}
|
|
380
|
+
return { category: "other", confidence: "unsure", reason: "\u4E0D\u8BA4\u8BC6\u7684\u7AD9\u70B9\uFF0C\u5148\u653E\u5176\u5B83" };
|
|
381
|
+
}
|
|
382
|
+
function classifyText(text) {
|
|
383
|
+
const reason = findSecret(text);
|
|
384
|
+
if (reason !== void 0) {
|
|
385
|
+
return { category: "secret", confidence: "decided", reason };
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
category: "other",
|
|
389
|
+
confidence: "unsure",
|
|
390
|
+
reason: "\u6CA1\u547D\u4E2D\u89C4\u5219\uFF0C\u6682\u65F6\u653E\u5176\u5B83"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
var DOCUMENT_RATIOS = [
|
|
394
|
+
[85.6 / 54, "\u8EAB\u4EFD\u8BC1/\u94F6\u884C\u5361\uFF0885.6\xD754\uFF09"],
|
|
395
|
+
[1.4142, "A4 \u7AD6\u7248\uFF081:\u221A2\uFF09"],
|
|
396
|
+
[1.42, "\u62A4\u7167\u6570\u636E\u9875"]
|
|
397
|
+
];
|
|
398
|
+
function classifyImage(dimensions) {
|
|
399
|
+
const { width, height } = dimensions;
|
|
400
|
+
if (width === void 0 || height === void 0 || width <= 0 || height <= 0) {
|
|
401
|
+
return { category: "image", confidence: "unsure", reason: "\u6CA1\u6709\u5C3A\u5BF8\u4FE1\u606F\uFF0C\u53EA\u80FD\u5148\u5F53\u56FE\u7247" };
|
|
402
|
+
}
|
|
403
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
404
|
+
for (const [target, label] of DOCUMENT_RATIOS) {
|
|
405
|
+
if (Math.abs(ratio - target) <= 0.03) {
|
|
406
|
+
return {
|
|
407
|
+
category: "image",
|
|
408
|
+
confidence: "unsure",
|
|
409
|
+
reason: `\u957F\u5BBD\u6BD4 ${ratio.toFixed(2)} \u63A5\u8FD1${label}\uFF0C\u5148\u6807\u7591\u4F3C\uFF0C\u7B49\u4F60\u786E\u8BA4`,
|
|
410
|
+
tags: ["\u7591\u4F3C\u8BC1\u4EF6"]
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { category: "image", confidence: "decided", reason: "\u5E38\u89C4\u7167\u7247\u6BD4\u4F8B" };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/host/capture.ts
|
|
418
|
+
var TRACKING_PARAMS = [
|
|
419
|
+
"spm_id_from",
|
|
420
|
+
"vd_source",
|
|
421
|
+
"share_source",
|
|
422
|
+
"share_medium",
|
|
423
|
+
"from",
|
|
424
|
+
"from_source",
|
|
425
|
+
"share_token"
|
|
426
|
+
];
|
|
427
|
+
function parse(raw) {
|
|
428
|
+
try {
|
|
429
|
+
return new URL(raw);
|
|
430
|
+
} catch {
|
|
431
|
+
return void 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function sniff(raw) {
|
|
435
|
+
const trimmed = raw.trim();
|
|
436
|
+
if (trimmed.length === 0 || /\s/.test(trimmed) || !/^https?:\/\//i.test(trimmed)) {
|
|
437
|
+
return { kind: "text" };
|
|
438
|
+
}
|
|
439
|
+
if (parse(trimmed) === void 0) return { kind: "text" };
|
|
440
|
+
const platform = platformOf(trimmed);
|
|
441
|
+
return { kind: "link", url: trimmed, ...platform === void 0 ? {} : { platform } };
|
|
442
|
+
}
|
|
443
|
+
function normalizeLink(raw) {
|
|
444
|
+
const url = parse(raw);
|
|
445
|
+
if (url === void 0) return raw.trim();
|
|
446
|
+
url.hash = "";
|
|
447
|
+
url.hostname = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
448
|
+
for (const param of [...url.searchParams.keys()]) {
|
|
449
|
+
if (param.startsWith("utm_") || TRACKING_PARAMS.includes(param)) url.searchParams.delete(param);
|
|
450
|
+
}
|
|
451
|
+
const search = url.searchParams.toString();
|
|
452
|
+
const path = url.pathname.replace(/\/$/, "");
|
|
453
|
+
return `${url.protocol}//${url.host}${path}${search.length === 0 ? "" : `?${search}`}`;
|
|
454
|
+
}
|
|
455
|
+
function mergedNote(current, incoming) {
|
|
456
|
+
if (incoming === void 0 || incoming.length === 0) return current.note;
|
|
457
|
+
return current.note ?? incoming;
|
|
458
|
+
}
|
|
459
|
+
async function absorb(vault, existing, incoming) {
|
|
460
|
+
const patched = await vault.patch(existing.id, {
|
|
461
|
+
note: mergedNote(existing, incoming.note),
|
|
462
|
+
...existing.title === void 0 && incoming.title !== void 0 ? { title: incoming.title } : {}
|
|
463
|
+
});
|
|
464
|
+
const restored = existing.deletedAt !== void 0;
|
|
465
|
+
const item = restored ? await vault.restore(existing.id) : patched;
|
|
466
|
+
return {
|
|
467
|
+
item,
|
|
468
|
+
merged: true,
|
|
469
|
+
restored,
|
|
470
|
+
verdict: {
|
|
471
|
+
category: item.category,
|
|
472
|
+
confidence: "decided",
|
|
473
|
+
reason: "\u5408\u5E76\u5230\u5DF2\u6709\u8BB0\u5F55\uFF0C\u4E0D\u91CD\u590D\u5206\u7C7B"
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function captureText(vault, raw, source, note) {
|
|
478
|
+
const sniffed = sniff(raw);
|
|
479
|
+
const verdict = sniffed.kind === "link" ? classifyLink(raw.trim()) : classifyText(raw);
|
|
480
|
+
const platform = verdict.platform ?? sniffed.platform;
|
|
481
|
+
const sealed = verdict.category === "secret" ? vault.sealSecret(raw) : void 0;
|
|
482
|
+
const candidate = {
|
|
483
|
+
kind: sniffed.kind,
|
|
484
|
+
category: verdict.category,
|
|
485
|
+
source,
|
|
486
|
+
tags: verdict.tags ?? [],
|
|
487
|
+
...note === void 0 || note.length === 0 ? {} : { note },
|
|
488
|
+
...sniffed.kind === "link" ? {
|
|
489
|
+
url: sniffed.url,
|
|
490
|
+
...platform === void 0 ? {} : { platform }
|
|
491
|
+
} : sealed === void 0 ? { text: raw } : sealed
|
|
492
|
+
};
|
|
493
|
+
const existing = vault.list({ includeDeleted: true, kinds: [sniffed.kind] }).find(
|
|
494
|
+
(item) => sniffed.kind === "link" ? item.url !== void 0 && normalizeLink(item.url) === normalizeLink(raw) : sealed === void 0 ? item.text === raw : item.secretDigest === sealed.secretDigest
|
|
495
|
+
);
|
|
496
|
+
if (existing !== void 0) return absorb(vault, existing, { note });
|
|
497
|
+
return { item: await vault.create(candidate), merged: false, restored: false, verdict };
|
|
498
|
+
}
|
|
499
|
+
async function captureImage(vault, attachment, source, note) {
|
|
500
|
+
const known = vault.findAttachmentByStoreId(attachment.id);
|
|
501
|
+
const existing = known === void 0 ? void 0 : vault.list({ includeDeleted: true, kinds: ["image", "file"] }).find((item2) => item2.attachmentIds.includes(known.id));
|
|
502
|
+
if (existing !== void 0) return absorb(vault, existing, { note });
|
|
503
|
+
const record = await vault.addAttachment({
|
|
504
|
+
storeId: attachment.id,
|
|
505
|
+
mime: attachment.mime,
|
|
506
|
+
bytes: attachment.bytes,
|
|
507
|
+
filename: attachment.filename,
|
|
508
|
+
width: attachment.width,
|
|
509
|
+
height: attachment.height,
|
|
510
|
+
sha256: attachment.sha256
|
|
511
|
+
});
|
|
512
|
+
const verdict = classifyImage(attachment);
|
|
513
|
+
const item = await vault.create({
|
|
514
|
+
kind: attachment.mime.startsWith("image/") ? "image" : "file",
|
|
515
|
+
category: verdict.category,
|
|
516
|
+
source,
|
|
517
|
+
tags: verdict.tags ?? [],
|
|
518
|
+
...note === void 0 || note.length === 0 ? {} : { note },
|
|
519
|
+
attachmentIds: [record.id]
|
|
520
|
+
});
|
|
521
|
+
return { item, merged: false, restored: false, verdict };
|
|
522
|
+
}
|
|
523
|
+
async function capture(vault, payload, source, options = {}) {
|
|
524
|
+
let stored = 0;
|
|
525
|
+
let merged = 0;
|
|
526
|
+
let restored = 0;
|
|
527
|
+
const tally = (outcome) => {
|
|
528
|
+
if (outcome.merged) merged += 1;
|
|
529
|
+
else stored += 1;
|
|
530
|
+
if (outcome.restored) restored += 1;
|
|
531
|
+
if (options.ctx !== void 0) {
|
|
532
|
+
const ctx = options.ctx;
|
|
533
|
+
if (!outcome.merged) {
|
|
534
|
+
try {
|
|
535
|
+
void classifyWithModel(ctx, vault, outcome.item, outcome.verdict).catch(() => void 0);
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const web = ctx.get("web");
|
|
540
|
+
if (web !== void 0 && outcome.item.kind === "link") {
|
|
541
|
+
try {
|
|
542
|
+
const logLine = (message) => {
|
|
543
|
+
try {
|
|
544
|
+
ctx.logger?.info("inbox: %s", message);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
void fetchLinkTitle(vault, outcome.item.id, web, logLine).catch(() => void 0);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
for (const attachment of payload.attachments ?? []) {
|
|
555
|
+
tally(await captureImage(vault, attachment, source));
|
|
556
|
+
}
|
|
557
|
+
const text = payload.text?.trim() ?? "";
|
|
558
|
+
if (text.length > 0) tally(await captureText(vault, text, source));
|
|
559
|
+
return { stored, merged, restored };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/host/s3/client.ts
|
|
563
|
+
import { createHash, createHmac } from "node:crypto";
|
|
564
|
+
function userAgentOf(config) {
|
|
565
|
+
const configured = config.userAgent?.trim() ?? "";
|
|
566
|
+
return configured.length === 0 ? DEFAULT_USER_AGENT : configured;
|
|
567
|
+
}
|
|
568
|
+
async function refused(response, what) {
|
|
569
|
+
let body = "";
|
|
570
|
+
try {
|
|
571
|
+
body = (await response.text()).trim().slice(0, 300);
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
const challenge = response.headers?.get("www-authenticate") ?? "";
|
|
575
|
+
const hints = [body, challenge.length === 0 ? "" : `WWW-Authenticate: ${challenge}`].filter(
|
|
576
|
+
(part) => part.length > 0
|
|
577
|
+
);
|
|
578
|
+
const identity = response.status === 401 || response.status === 403 ? "\n\uFF08\u8FD9\u7C7B\u7F51\u5173\u5E38\u6309\u5BA2\u6237\u7AEF\u6807\u8BC6\u8BA4\u4EBA\uFF1A\u8FD9\u628A AccessKey \u7ED1\u5B9A\u7684\u5E94\u7528\u540D\uFF0C\u8981\u586B\u8FDB\u8BBE\u7F6E\u7684\u300C\u5BA2\u6237\u7AEF\u6807\u8BC6\u300D\uFF09" : "";
|
|
579
|
+
return new Error(
|
|
580
|
+
`${what}\u5931\u8D25\uFF1AHTTP ${String(response.status)}${hints.length === 0 ? "" : ` \u2014 ${hints.join(" ")}`}${identity}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function amzDate(now) {
|
|
584
|
+
return `${now.toISOString().replace(/[-:]/g, "").split(".")[0] ?? ""}Z`;
|
|
585
|
+
}
|
|
586
|
+
function sha256Hex(value) {
|
|
587
|
+
return createHash("sha256").update(value).digest("hex");
|
|
588
|
+
}
|
|
589
|
+
function hmac(key, value) {
|
|
590
|
+
return createHmac("sha256", key).update(value).digest();
|
|
591
|
+
}
|
|
592
|
+
function encodePart(value, encodeSlash) {
|
|
593
|
+
const encoded = encodeURIComponent(value).replace(
|
|
594
|
+
/[!'()*]/g,
|
|
595
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
|
|
596
|
+
);
|
|
597
|
+
return encodeSlash ? encoded : encoded.replace(/%2F/g, "/");
|
|
598
|
+
}
|
|
599
|
+
function rfc1123(now) {
|
|
600
|
+
return now.toUTCString();
|
|
601
|
+
}
|
|
602
|
+
function signRequestV2(config, deps, method, key, query = {}) {
|
|
603
|
+
const date = rfc1123(deps.now ?? /* @__PURE__ */ new Date());
|
|
604
|
+
const base = config.endpoint.replace(/\/$/, "");
|
|
605
|
+
const canonicalUri = `/${encodePart(config.bucket, false)}${key.length === 0 ? "" : `/${encodePart(key, false)}`}`;
|
|
606
|
+
const queryString = Object.entries(query).map(([name2, value]) => `${encodePart(name2, true)}=${encodePart(value, true)}`).sort().join("&");
|
|
607
|
+
const subResource = ["acl", "location", "versioning"].find(
|
|
608
|
+
(name2) => query[name2] !== void 0
|
|
609
|
+
);
|
|
610
|
+
const canonicalResource = `${canonicalUri}${subResource === void 0 ? "" : `?${subResource}`}`;
|
|
611
|
+
const stringToSign = [method, "", "", date, canonicalResource].join("\n");
|
|
612
|
+
const signature = createHmac("sha1", deps.accessKeySecret).update(stringToSign).digest("base64");
|
|
613
|
+
return {
|
|
614
|
+
url: `${base}${canonicalUri}${queryString.length === 0 ? "" : `?${queryString}`}`,
|
|
615
|
+
headers: {
|
|
616
|
+
date,
|
|
617
|
+
authorization: `AWS ${deps.accessKeyId}:${signature}`,
|
|
618
|
+
"user-agent": userAgentOf(config)
|
|
619
|
+
},
|
|
620
|
+
canonicalRequest: stringToSign,
|
|
621
|
+
signature,
|
|
622
|
+
stringToSign
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function signRequest(config, deps, method, key, query = {}, body) {
|
|
626
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
627
|
+
const stamp = amzDate(now);
|
|
628
|
+
const day = stamp.slice(0, 8);
|
|
629
|
+
const region = config.region ?? "us-east-1";
|
|
630
|
+
const base = config.endpoint.replace(/\/$/, "");
|
|
631
|
+
const canonicalUri = `/${encodePart(config.bucket, false)}${key.length === 0 ? "" : `/${encodePart(key, false)}`}`;
|
|
632
|
+
const canonicalQuery = Object.entries(query).map(([name2, value]) => `${encodePart(name2, true)}=${encodePart(value, true)}`).sort().join("&");
|
|
633
|
+
const payloadHash = sha256Hex(body ?? "");
|
|
634
|
+
const host = new URL(base).host;
|
|
635
|
+
const signedHeaders = "host;x-amz-content-sha256;x-amz-date";
|
|
636
|
+
const canonicalHeaders = `host:${host}
|
|
637
|
+
x-amz-content-sha256:${payloadHash}
|
|
638
|
+
x-amz-date:${stamp}
|
|
639
|
+
`;
|
|
640
|
+
const canonicalRequest = [
|
|
641
|
+
method,
|
|
642
|
+
canonicalUri,
|
|
643
|
+
canonicalQuery,
|
|
644
|
+
canonicalHeaders,
|
|
645
|
+
signedHeaders,
|
|
646
|
+
payloadHash
|
|
647
|
+
].join("\n");
|
|
648
|
+
const scope = `${day}/${region}/s3/aws4_request`;
|
|
649
|
+
const stringToSign = [
|
|
650
|
+
"AWS4-HMAC-SHA256",
|
|
651
|
+
stamp,
|
|
652
|
+
scope,
|
|
653
|
+
sha256Hex(canonicalRequest)
|
|
654
|
+
].join("\n");
|
|
655
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${deps.accessKeySecret}`, day), region), "s3"), "aws4_request");
|
|
656
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
657
|
+
return {
|
|
658
|
+
url: `${base}${canonicalUri}${canonicalQuery.length === 0 ? "" : `?${canonicalQuery}`}`,
|
|
659
|
+
headers: {
|
|
660
|
+
host,
|
|
661
|
+
"x-amz-date": stamp,
|
|
662
|
+
"x-amz-content-sha256": payloadHash,
|
|
663
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${deps.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
664
|
+
"user-agent": userAgentOf(config)
|
|
665
|
+
},
|
|
666
|
+
canonicalRequest,
|
|
667
|
+
signature
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function signRequestV4Minimal(config, deps, method, key, query = {}) {
|
|
671
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
672
|
+
const stamp = amzDate(now);
|
|
673
|
+
const day = stamp.slice(0, 8);
|
|
674
|
+
const region = config.region ?? "us-east-1";
|
|
675
|
+
const base = config.endpoint.replace(/\/$/, "");
|
|
676
|
+
const canonicalUri = `/${encodePart(config.bucket, false)}${key.length === 0 ? "" : `/${encodePart(key, false)}`}`;
|
|
677
|
+
const canonicalQuery = Object.entries(query).map(([name2, value]) => `${encodePart(name2, true)}=${encodePart(value, true)}`).sort().join("&");
|
|
678
|
+
const host = new URL(base).host;
|
|
679
|
+
const signedHeaders = "host;x-amz-date";
|
|
680
|
+
const canonicalHeaders = `host:${host}
|
|
681
|
+
x-amz-date:${stamp}
|
|
682
|
+
`;
|
|
683
|
+
const payloadHash = sha256Hex("");
|
|
684
|
+
const canonicalRequest = [
|
|
685
|
+
method,
|
|
686
|
+
canonicalUri,
|
|
687
|
+
canonicalQuery,
|
|
688
|
+
canonicalHeaders,
|
|
689
|
+
signedHeaders,
|
|
690
|
+
payloadHash
|
|
691
|
+
].join("\n");
|
|
692
|
+
const scope = `${day}/${region}/s3/aws4_request`;
|
|
693
|
+
const stringToSign = ["AWS4-HMAC-SHA256", stamp, scope, sha256Hex(canonicalRequest)].join("\n");
|
|
694
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${deps.accessKeySecret}`, day), region), "s3"), "aws4_request");
|
|
695
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
696
|
+
return {
|
|
697
|
+
url: `${base}${canonicalUri}${canonicalQuery.length === 0 ? "" : `?${canonicalQuery}`}`,
|
|
698
|
+
headers: {
|
|
699
|
+
host,
|
|
700
|
+
"x-amz-date": stamp,
|
|
701
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${deps.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
702
|
+
"user-agent": userAgentOf(config)
|
|
703
|
+
},
|
|
704
|
+
canonicalRequest,
|
|
705
|
+
signature
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function signRequestUnsignedPayload(config, deps, method, key, query = {}) {
|
|
709
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
710
|
+
const stamp = amzDate(now);
|
|
711
|
+
const day = stamp.slice(0, 8);
|
|
712
|
+
const region = config.region ?? "us-east-1";
|
|
713
|
+
const base = config.endpoint.replace(/\/$/, "");
|
|
714
|
+
const canonicalUri = `/${encodePart(config.bucket, false)}${key.length === 0 ? "" : `/${encodePart(key, false)}`}`;
|
|
715
|
+
const canonicalQuery = Object.entries(query).map(([name2, value]) => `${encodePart(name2, true)}=${encodePart(value, true)}`).sort().join("&");
|
|
716
|
+
const payloadHash = "UNSIGNED-PAYLOAD";
|
|
717
|
+
const host = new URL(base).host;
|
|
718
|
+
const signedHeaders = "host;x-amz-content-sha256;x-amz-date";
|
|
719
|
+
const canonicalHeaders = `host:${host}
|
|
720
|
+
x-amz-content-sha256:${payloadHash}
|
|
721
|
+
x-amz-date:${stamp}
|
|
722
|
+
`;
|
|
723
|
+
const canonicalRequest = [
|
|
724
|
+
method,
|
|
725
|
+
canonicalUri,
|
|
726
|
+
canonicalQuery,
|
|
727
|
+
canonicalHeaders,
|
|
728
|
+
signedHeaders,
|
|
729
|
+
payloadHash
|
|
730
|
+
].join("\n");
|
|
731
|
+
const scope = `${day}/${region}/s3/aws4_request`;
|
|
732
|
+
const stringToSign = ["AWS4-HMAC-SHA256", stamp, scope, sha256Hex(canonicalRequest)].join("\n");
|
|
733
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${deps.accessKeySecret}`, day), region), "s3"), "aws4_request");
|
|
734
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
735
|
+
return {
|
|
736
|
+
url: `${base}${canonicalUri}${canonicalQuery.length === 0 ? "" : `?${canonicalQuery}`}`,
|
|
737
|
+
headers: {
|
|
738
|
+
host,
|
|
739
|
+
"x-amz-date": stamp,
|
|
740
|
+
"x-amz-content-sha256": payloadHash,
|
|
741
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${deps.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
742
|
+
"user-agent": userAgentOf(config)
|
|
743
|
+
},
|
|
744
|
+
canonicalRequest,
|
|
745
|
+
signature
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
var KEY_BLOCK = /<Key>([\s\S]*?)<\/Key>/i;
|
|
749
|
+
var LAST_MODIFIED = /<LastModified>([\s\S]*?)<\/LastModified>/i;
|
|
750
|
+
var SIZE = /<Size>([\s\S]*?)<\/Size>/i;
|
|
751
|
+
var CONTENTS = /<Contents>[\s\S]*?<\/Contents>/gi;
|
|
752
|
+
function decode(value) {
|
|
753
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").trim();
|
|
754
|
+
}
|
|
755
|
+
function parseListing(xml) {
|
|
756
|
+
const objects = [];
|
|
757
|
+
for (const block of xml.match(CONTENTS) ?? []) {
|
|
758
|
+
const key = KEY_BLOCK.exec(block)?.[1];
|
|
759
|
+
if (key === void 0) continue;
|
|
760
|
+
const lastModified = LAST_MODIFIED.exec(block)?.[1];
|
|
761
|
+
const size = SIZE.exec(block)?.[1];
|
|
762
|
+
const parsedSize = size === void 0 ? void 0 : Number.parseInt(size, 10);
|
|
763
|
+
objects.push({
|
|
764
|
+
key: decode(key),
|
|
765
|
+
...lastModified === void 0 ? {} : { lastModified: decode(lastModified) },
|
|
766
|
+
...parsedSize === void 0 || Number.isNaN(parsedSize) ? {} : { bytes: parsedSize }
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
return objects;
|
|
770
|
+
}
|
|
771
|
+
async function listPrefix(config, prefix, deps) {
|
|
772
|
+
const sign = signer(config);
|
|
773
|
+
const v2 = sign(config, deps, "GET", "", { "list-type": "2", prefix });
|
|
774
|
+
const first = await deps.fetch(v2.url, { method: "GET", headers: v2.headers });
|
|
775
|
+
if (first.ok) return parseListing(await first.text());
|
|
776
|
+
const refusal2 = await refused(first, "\u5217\u5BF9\u8C61");
|
|
777
|
+
if (first.status < 500) {
|
|
778
|
+
if (config.signatureVersion?.toLowerCase() !== "v2" && first.status === 401) {
|
|
779
|
+
const minimal = signRequestV4Minimal(config, deps, "GET", "", { "list-type": "2", prefix });
|
|
780
|
+
const retry = await deps.fetch(minimal.url, { method: "GET", headers: minimal.headers });
|
|
781
|
+
if (retry.ok) return parseListing(await retry.text());
|
|
782
|
+
throw new Error(
|
|
783
|
+
`${(await refused(retry, "\u5217\u5BF9\u8C61\uFF08\u7CBE\u7B80 v4\uFF09")).message}
|
|
784
|
+
\u5B8C\u6574 v4\uFF1A${refusal2.message}`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
throw refusal2;
|
|
788
|
+
}
|
|
789
|
+
const v1 = sign(config, deps, "GET", "", { prefix });
|
|
790
|
+
const second = await deps.fetch(v1.url, { method: "GET", headers: v1.headers });
|
|
791
|
+
if (!second.ok) {
|
|
792
|
+
throw new Error(`${await refused(second, "\u5217\u5BF9\u8C61\uFF08V1 \u56DE\u9000\uFF09").then((e) => e.message)}
|
|
793
|
+
\u9996\u6B21\u5C1D\u8BD5\uFF1A${refusal2.message}`);
|
|
794
|
+
}
|
|
795
|
+
return parseListing(await second.text());
|
|
796
|
+
}
|
|
797
|
+
async function readObject(config, key, deps) {
|
|
798
|
+
const signed = signer(config)(config, deps, "GET", key);
|
|
799
|
+
const response = await deps.fetch(signed.url, { method: "GET", headers: signed.headers });
|
|
800
|
+
if (!response.ok) throw await refused(response, "\u53D6\u5BF9\u8C61");
|
|
801
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
802
|
+
const declared = response.headers?.get("content-type") ?? "application/octet-stream";
|
|
803
|
+
return { bytes, contentType: declared.split(";")[0] ?? "application/octet-stream" };
|
|
804
|
+
}
|
|
805
|
+
async function putObject(config, key, body, deps, contentType = "application/octet-stream") {
|
|
806
|
+
const signed = signer(config)(config, deps, "PUT", key, {}, body);
|
|
807
|
+
const response = await deps.fetch(signed.url, {
|
|
808
|
+
method: "PUT",
|
|
809
|
+
headers: {
|
|
810
|
+
...signed.headers,
|
|
811
|
+
"content-type": contentType,
|
|
812
|
+
"content-length": String(body.byteLength)
|
|
813
|
+
},
|
|
814
|
+
body
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) throw await refused(response, "\u5199\u5BF9\u8C61");
|
|
817
|
+
}
|
|
818
|
+
async function deleteObject(config, key, deps) {
|
|
819
|
+
const signed = signer(config)(config, deps, "DELETE", key);
|
|
820
|
+
const response = await deps.fetch(signed.url, { method: "DELETE", headers: signed.headers });
|
|
821
|
+
if (!response.ok && response.status !== 404) throw await refused(response, "\u5220\u5BF9\u8C61");
|
|
822
|
+
}
|
|
823
|
+
function signer(config) {
|
|
824
|
+
return config.signatureVersion?.toLowerCase() === "v2" ? signRequestV2 : signRequest;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/host/webdav/client.ts
|
|
828
|
+
function authHeaders(auth) {
|
|
829
|
+
if (auth === void 0) return {};
|
|
830
|
+
const token = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
|
|
831
|
+
return { authorization: `Basic ${token}` };
|
|
832
|
+
}
|
|
833
|
+
function userAgentHeaders(deps) {
|
|
834
|
+
const configured = deps.userAgent?.trim() ?? "";
|
|
835
|
+
return { "user-agent": configured.length === 0 ? DEFAULT_USER_AGENT : configured };
|
|
836
|
+
}
|
|
837
|
+
function joinUrl(base, part) {
|
|
838
|
+
const left = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
839
|
+
const right = part.startsWith("/") ? part.slice(1) : part;
|
|
840
|
+
return `${left}/${right}`;
|
|
841
|
+
}
|
|
842
|
+
async function writeFile(baseUrl, path, bytes, deps, contentType = "application/octet-stream") {
|
|
843
|
+
const response = await deps.fetch(joinUrl(baseUrl, path), {
|
|
844
|
+
method: "PUT",
|
|
845
|
+
headers: {
|
|
846
|
+
...authHeaders(deps.auth),
|
|
847
|
+
...userAgentHeaders(deps),
|
|
848
|
+
"content-type": contentType,
|
|
849
|
+
"content-length": String(bytes.byteLength)
|
|
850
|
+
},
|
|
851
|
+
body: new TextDecoder().decode(bytes)
|
|
852
|
+
});
|
|
853
|
+
if (response.status >= 400) {
|
|
854
|
+
const detail = (await response.text().catch(() => "")).trim().slice(0, 200);
|
|
855
|
+
throw new Error(`\u5199\u8FDC\u7AEF\u5931\u8D25\uFF1AHTTP ${String(response.status)}${detail.length === 0 ? "" : ` \xB7 ${detail}`}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function deleteFile(baseUrl, path, deps) {
|
|
859
|
+
const response = await deps.fetch(joinUrl(baseUrl, path), {
|
|
860
|
+
method: "DELETE",
|
|
861
|
+
headers: { ...authHeaders(deps.auth), ...userAgentHeaders(deps) }
|
|
862
|
+
});
|
|
863
|
+
if (response.status === 404) return;
|
|
864
|
+
if (response.status >= 400) {
|
|
865
|
+
const detail = (await response.text().catch(() => "")).trim().slice(0, 200);
|
|
866
|
+
throw new Error(`\u5220\u8FDC\u7AEF\u5931\u8D25\uFF1AHTTP ${String(response.status)}${detail.length === 0 ? "" : ` \xB7 ${detail}`}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
var RESPONSE_BLOCK = /<[a-z0-9]*:?response\b[\s\S]*?<\/[a-z0-9]*:?response>/gi;
|
|
870
|
+
var HREF = /<[a-z0-9]*:?href[^>]*>([\s\S]*?)<\/[a-z0-9]*:?href>/i;
|
|
871
|
+
var LAST_MODIFIED2 = /<[a-z0-9]*:?getlastmodified[^>]*>([\s\S]*?)<\/[a-z0-9]*:?getlastmodified>/i;
|
|
872
|
+
var CONTENT_TYPE = /<[a-z0-9]*:?getcontenttype[^>]*>([\s\S]*?)<\/[a-z0-9]*:?getcontenttype>/i;
|
|
873
|
+
var IS_COLLECTION = /<[a-z0-9]*:?collection\s*\/?>/i;
|
|
874
|
+
function decode2(value) {
|
|
875
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').trim();
|
|
876
|
+
}
|
|
877
|
+
function parseListing2(xml, directory) {
|
|
878
|
+
const files = [];
|
|
879
|
+
const folder = directory.replace(/\/+$/, "");
|
|
880
|
+
for (const block of xml.match(RESPONSE_BLOCK) ?? []) {
|
|
881
|
+
if (IS_COLLECTION.test(block)) continue;
|
|
882
|
+
const href = HREF.exec(block)?.[1];
|
|
883
|
+
if (href === void 0) continue;
|
|
884
|
+
const path = decode2(href);
|
|
885
|
+
if (folder.length > 0 && path.replace(/\/+$/, "").endsWith(folder)) continue;
|
|
886
|
+
const lastModified = LAST_MODIFIED2.exec(block)?.[1];
|
|
887
|
+
const contentType = CONTENT_TYPE.exec(block)?.[1];
|
|
888
|
+
files.push({
|
|
889
|
+
path,
|
|
890
|
+
...lastModified === void 0 ? {} : { lastModified: decode2(lastModified) },
|
|
891
|
+
...contentType === void 0 ? {} : { contentType: decode2(contentType).split(";")[0] ?? "" }
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return files;
|
|
895
|
+
}
|
|
896
|
+
async function refusal(what, response) {
|
|
897
|
+
let body = "";
|
|
898
|
+
try {
|
|
899
|
+
body = (await response.text()).trim().slice(0, 200);
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
const identity = response.status === 401 || response.status === 403 ? "\n\uFF08\u8FD9\u7C7B\u7F51\u5173\u5E38\u6309\u5BA2\u6237\u7AEF\u6807\u8BC6\u8BA4\u4EBA\uFF1A\u8FD9\u4E2A\u8D26\u53F7\u7ED1\u5B9A\u7684\u5E94\u7528\u540D\u8981\u586B\u8FDB\u8BBE\u7F6E\u7684\u300C\u5BA2\u6237\u7AEF\u6807\u8BC6\u300D\uFF09" : "";
|
|
903
|
+
return new Error(
|
|
904
|
+
`${what}\u5931\u8D25\uFF1AHTTP ${String(response.status)}${body.length === 0 ? "" : ` \u2014 ${body.replace(/\s+/g, " ")}`}${identity}`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
async function listFolder(baseUrl, directory, deps) {
|
|
908
|
+
const response = await deps.fetch(joinUrl(baseUrl, directory), {
|
|
909
|
+
method: "PROPFIND",
|
|
910
|
+
headers: {
|
|
911
|
+
depth: "1",
|
|
912
|
+
"content-type": "application/xml",
|
|
913
|
+
...authHeaders(deps.auth),
|
|
914
|
+
...userAgentHeaders(deps)
|
|
915
|
+
},
|
|
916
|
+
body: '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:getlastmodified/><d:getcontenttype/><d:resourcetype/></d:prop></d:propfind>'
|
|
917
|
+
});
|
|
918
|
+
if (!response.ok) {
|
|
919
|
+
throw await refusal("\u5217\u76EE\u5F55", response);
|
|
920
|
+
}
|
|
921
|
+
return parseListing2(await response.text(), directory);
|
|
922
|
+
}
|
|
923
|
+
async function readFile(path, deps, absoluteUrl) {
|
|
924
|
+
const response = await deps.fetch(absoluteUrl ?? path, {
|
|
925
|
+
method: "GET",
|
|
926
|
+
headers: { ...authHeaders(deps.auth), ...userAgentHeaders(deps) }
|
|
927
|
+
});
|
|
928
|
+
if (!response.ok) {
|
|
929
|
+
throw await refusal("\u53D6\u6587\u4EF6", response);
|
|
930
|
+
}
|
|
931
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
932
|
+
const declared = response.headers?.get("content-type") ?? "application/octet-stream";
|
|
933
|
+
return { bytes, contentType: declared.split(";")[0] ?? "application/octet-stream" };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/host/webdav/config.ts
|
|
937
|
+
import z from "@deepseek-ai/schemastery";
|
|
938
|
+
var SETTINGS_NAMESPACE = "dsh-inbox-webdav";
|
|
939
|
+
var PASSWORD_KEY = "DSH_INBOX_WEBDAV_PASSWORD";
|
|
940
|
+
var S3_SECRET_KEY = "DSH_INBOX_S3_SECRET";
|
|
941
|
+
var DEFAULT_SETTINGS = {
|
|
942
|
+
protocol: "webdav",
|
|
943
|
+
baseUrl: "",
|
|
944
|
+
directory: "/inbox",
|
|
945
|
+
username: "",
|
|
946
|
+
endpoint: "",
|
|
947
|
+
bucket: "",
|
|
948
|
+
region: "us-east-1",
|
|
949
|
+
signatureVersion: "v4",
|
|
950
|
+
accessKeyId: "",
|
|
951
|
+
userAgent: "",
|
|
952
|
+
webdavUserAgent: ""
|
|
953
|
+
};
|
|
954
|
+
var WebdavSettingsSchema = z.object({
|
|
955
|
+
protocol: z.union(["webdav", "s3"]).default("webdav"),
|
|
956
|
+
baseUrl: z.string().default(""),
|
|
957
|
+
directory: z.string().default("/inbox"),
|
|
958
|
+
username: z.string().default(""),
|
|
959
|
+
endpoint: z.string().default(""),
|
|
960
|
+
bucket: z.string().default(""),
|
|
961
|
+
region: z.string().default("us-east-1"),
|
|
962
|
+
signatureVersion: z.string().default("v4"),
|
|
963
|
+
accessKeyId: z.string().default(""),
|
|
964
|
+
userAgent: z.string().default(""),
|
|
965
|
+
webdavUserAgent: z.string().default("")
|
|
966
|
+
});
|
|
967
|
+
function readSettings(ctx) {
|
|
968
|
+
const settings = ctx.get("settings");
|
|
969
|
+
if (settings === void 0) return DEFAULT_SETTINGS;
|
|
970
|
+
return settings.get(SETTINGS_NAMESPACE) ?? DEFAULT_SETTINGS;
|
|
971
|
+
}
|
|
972
|
+
function installWebdavSettings(ctx, base = DEFAULT_SETTINGS) {
|
|
973
|
+
const settings = ctx.get("settings");
|
|
974
|
+
if (settings === void 0) return;
|
|
975
|
+
try {
|
|
976
|
+
settings.register(SETTINGS_NAMESPACE, WebdavSettingsSchema, { base });
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function readPassword(ctx) {
|
|
981
|
+
const credentials = ctx.get("credentials");
|
|
982
|
+
if (credentials === void 0) return void 0;
|
|
983
|
+
try {
|
|
984
|
+
return (await credentials.resolve(PASSWORD_KEY))?.value;
|
|
985
|
+
} catch {
|
|
986
|
+
return void 0;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function readS3Secret(ctx) {
|
|
990
|
+
const credentials = ctx.get("credentials");
|
|
991
|
+
if (credentials === void 0) return void 0;
|
|
992
|
+
try {
|
|
993
|
+
return (await credentials.resolve(S3_SECRET_KEY))?.value;
|
|
994
|
+
} catch {
|
|
995
|
+
return void 0;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function describeWebdav(ctx) {
|
|
999
|
+
const credentials = ctx.get("credentials");
|
|
1000
|
+
const password = await readPassword(ctx);
|
|
1001
|
+
const secret = await readS3Secret(ctx);
|
|
1002
|
+
return {
|
|
1003
|
+
settings: readSettings(ctx),
|
|
1004
|
+
passwordSet: password !== void 0,
|
|
1005
|
+
secretSet: secret !== void 0,
|
|
1006
|
+
settingsAvailable: ctx.get("settings") !== void 0,
|
|
1007
|
+
credentialsAvailable: credentials !== void 0
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function activeUserAgent(settings) {
|
|
1011
|
+
return settings.protocol === "s3" ? settings.userAgent : settings.webdavUserAgent;
|
|
1012
|
+
}
|
|
1013
|
+
function normalizeUrl(value) {
|
|
1014
|
+
const trimmed = value.trim();
|
|
1015
|
+
if (trimmed.length === 0) return "";
|
|
1016
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
1017
|
+
return `https://${trimmed}`;
|
|
1018
|
+
}
|
|
1019
|
+
async function saveWebdav(ctx, vault, base, patch) {
|
|
1020
|
+
const settings = ctx.get("settings");
|
|
1021
|
+
const credentials = ctx.get("credentials");
|
|
1022
|
+
const config = {};
|
|
1023
|
+
if (patch.protocol === "webdav" || patch.protocol === "s3") config.protocol = patch.protocol;
|
|
1024
|
+
if (patch.baseUrl !== void 0) config.baseUrl = normalizeUrl(patch.baseUrl);
|
|
1025
|
+
if (patch.directory !== void 0) {
|
|
1026
|
+
const directory = patch.directory.trim();
|
|
1027
|
+
config.directory = directory.startsWith("/") ? directory : `/${directory}`;
|
|
1028
|
+
}
|
|
1029
|
+
if (patch.username !== void 0) config.username = patch.username.trim();
|
|
1030
|
+
if (patch.endpoint !== void 0) config.endpoint = normalizeUrl(patch.endpoint);
|
|
1031
|
+
if (patch.bucket !== void 0) config.bucket = patch.bucket.trim();
|
|
1032
|
+
if (patch.region !== void 0) config.region = patch.region.trim() || "us-east-1";
|
|
1033
|
+
if (patch.signatureVersion !== void 0) {
|
|
1034
|
+
const version = patch.signatureVersion.trim().toLowerCase();
|
|
1035
|
+
if (version !== "v4" && version !== "v2") {
|
|
1036
|
+
return { ok: false, reason: `\u7B7E\u540D\u7248\u672C\u53EA\u652F\u6301 v4 \u6216 v2\uFF0C\u6536\u5230\u7684\u662F ${version}` };
|
|
1037
|
+
}
|
|
1038
|
+
config.signatureVersion = version;
|
|
1039
|
+
}
|
|
1040
|
+
if (patch.accessKeyId !== void 0) config.accessKeyId = patch.accessKeyId.trim();
|
|
1041
|
+
if (patch.userAgent !== void 0) {
|
|
1042
|
+
const protocol = patch.protocol === "webdav" || patch.protocol === "s3" ? patch.protocol : readSettings(ctx).protocol;
|
|
1043
|
+
if (protocol === "webdav") config.webdavUserAgent = patch.userAgent.trim();
|
|
1044
|
+
else config.userAgent = patch.userAgent.trim();
|
|
1045
|
+
}
|
|
1046
|
+
if (Object.keys(config).length > 0) {
|
|
1047
|
+
if (settings === void 0) {
|
|
1048
|
+
return { ok: false, reason: "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u8BBE\u7F6E\u670D\u52A1\uFF0C\u6539\u4E0D\u4E86\u5730\u5740" };
|
|
1049
|
+
}
|
|
1050
|
+
installWebdavSettings(ctx, base);
|
|
1051
|
+
settings.update(SETTINGS_NAMESPACE, config);
|
|
1052
|
+
}
|
|
1053
|
+
if (patch.password !== void 0) {
|
|
1054
|
+
if (credentials === void 0) {
|
|
1055
|
+
return { ok: false, reason: "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u51ED\u8BC1\u670D\u52A1\uFF0C\u5BC6\u7801\u4E0D\u77E5\u9053\u5B58\u54EA\u624D\u5B89\u5168" };
|
|
1056
|
+
}
|
|
1057
|
+
if (patch.password.length === 0) await credentials.unset(PASSWORD_KEY);
|
|
1058
|
+
else await credentials.set(PASSWORD_KEY, patch.password);
|
|
1059
|
+
}
|
|
1060
|
+
if (patch.accessKeySecret !== void 0) {
|
|
1061
|
+
if (credentials === void 0) {
|
|
1062
|
+
return { ok: false, reason: "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u51ED\u8BC1\u670D\u52A1\uFF0C\u5BC6\u94A5\u4E0D\u77E5\u9053\u5B58\u54EA\u624D\u5B89\u5168" };
|
|
1063
|
+
}
|
|
1064
|
+
if (patch.accessKeySecret.length === 0) await credentials.unset(S3_SECRET_KEY);
|
|
1065
|
+
else await credentials.set(S3_SECRET_KEY, patch.accessKeySecret);
|
|
1066
|
+
}
|
|
1067
|
+
void vault;
|
|
1068
|
+
return { ok: true };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/host/remote/pull.ts
|
|
1072
|
+
import { admitEncodedFile, admitEncodedImages } from "@deepseek-ai/dsh-attachment";
|
|
1073
|
+
|
|
1074
|
+
// src/shared/panel-wire.ts
|
|
1075
|
+
var INBOX_API_PREFIX = "/api/inbox";
|
|
1076
|
+
var INBOX_ENDPOINT_CAPTURE = "capture";
|
|
1077
|
+
var INBOX_ENDPOINT_LIST = "list";
|
|
1078
|
+
var INBOX_ENDPOINT_DETAIL = "detail";
|
|
1079
|
+
var INBOX_ENDPOINT_UPDATE = "update";
|
|
1080
|
+
var INBOX_ENDPOINT_DELETE = "delete";
|
|
1081
|
+
var INBOX_ENDPOINT_RESTORE = "restore";
|
|
1082
|
+
var INBOX_ENDPOINT_PURGE = "purge";
|
|
1083
|
+
var INBOX_ENDPOINT_ATTACHMENT = "attachment";
|
|
1084
|
+
var INBOX_ENDPOINT_WEBDAV = "webdav";
|
|
1085
|
+
var INBOX_ENDPOINT_PULL = "pull";
|
|
1086
|
+
var INBOX_ENDPOINT_PROBE = "probe";
|
|
1087
|
+
var INBOX_ENDPOINT_UI = "ui";
|
|
1088
|
+
var INBOX_ENDPOINT_TAGS = "tags";
|
|
1089
|
+
var INBOX_ENDPOINT_SECRET = "secret";
|
|
1090
|
+
var INBOX_ENDPOINT_PUSH = "push";
|
|
1091
|
+
var INBOX_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
1092
|
+
var LIST_LIMIT = 50;
|
|
1093
|
+
var UI_LIST_MODES = ["grid", "compact"];
|
|
1094
|
+
var PREVIEW_CHARS = 140;
|
|
1095
|
+
var MAX_ATTACHMENTS_PER_SUBMISSION = 20;
|
|
1096
|
+
var MAX_NOTE_CHARS = 2e3;
|
|
1097
|
+
var MAX_TAGS = 20;
|
|
1098
|
+
var MAX_TAG_CHARS = 40;
|
|
1099
|
+
var MAX_FILTER_CHARS = 200;
|
|
1100
|
+
var MAX_TITLE_CHARS = 300;
|
|
1101
|
+
|
|
1102
|
+
// src/host/remote/pull.ts
|
|
1103
|
+
var DEFAULT_DIRECTORY = "/inbox";
|
|
1104
|
+
var TEXT_TYPES = ["text/", "application/json"];
|
|
1105
|
+
var TEXT_SUFFIXES = [".txt", ".md", ".url", ".json"];
|
|
1106
|
+
function looksTextual(name2, contentType) {
|
|
1107
|
+
const lower = name2.toLowerCase();
|
|
1108
|
+
if (TEXT_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return true;
|
|
1109
|
+
return contentType !== void 0 && TEXT_TYPES.some((prefix) => contentType.startsWith(prefix));
|
|
1110
|
+
}
|
|
1111
|
+
function isSyncObject(path) {
|
|
1112
|
+
return path.split("/").some((part) => part === "sync");
|
|
1113
|
+
}
|
|
1114
|
+
function laterOf(current, candidate) {
|
|
1115
|
+
if (candidate === void 0) return current;
|
|
1116
|
+
if (current === void 0) return candidate;
|
|
1117
|
+
return Date.parse(candidate) > Date.parse(current) ? candidate : current;
|
|
1118
|
+
}
|
|
1119
|
+
function nameOf(path) {
|
|
1120
|
+
const parts = path.split("/").filter((part) => part.length > 0);
|
|
1121
|
+
return decodeURIComponent(parts[parts.length - 1] ?? path);
|
|
1122
|
+
}
|
|
1123
|
+
function isNewer(entry, lastPullAt) {
|
|
1124
|
+
if (lastPullAt === void 0) return true;
|
|
1125
|
+
if (entry.lastModified === void 0) return true;
|
|
1126
|
+
const seen = Date.parse(lastPullAt);
|
|
1127
|
+
const remote = Date.parse(entry.lastModified);
|
|
1128
|
+
if (Number.isNaN(seen) || Number.isNaN(remote)) return true;
|
|
1129
|
+
return remote > seen;
|
|
1130
|
+
}
|
|
1131
|
+
async function ingestFrom(vault, source, attachments) {
|
|
1132
|
+
const lastPullAt = vault.global.sync.lastPullAt;
|
|
1133
|
+
let entries;
|
|
1134
|
+
try {
|
|
1135
|
+
entries = await source.list();
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1138
|
+
return { status: "failed", reason, pulled: 0, failed: 0, skipped: 0, listed: 0 };
|
|
1139
|
+
}
|
|
1140
|
+
const listed = entries.length;
|
|
1141
|
+
let pulled = 0;
|
|
1142
|
+
let failed2 = 0;
|
|
1143
|
+
let skipped = 0;
|
|
1144
|
+
let newestSuccess;
|
|
1145
|
+
const failures = [];
|
|
1146
|
+
for (const entry of entries) {
|
|
1147
|
+
if (isSyncObject(entry.path)) {
|
|
1148
|
+
skipped += 1;
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (!isNewer(entry, lastPullAt)) {
|
|
1152
|
+
skipped += 1;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const name2 = nameOf(entry.path);
|
|
1156
|
+
try {
|
|
1157
|
+
const finished = entry.lastModified;
|
|
1158
|
+
const fetched = await source.read(entry);
|
|
1159
|
+
if (looksTextual(name2, entry.contentType ?? fetched.contentType)) {
|
|
1160
|
+
await captureText(vault, new TextDecoder().decode(fetched.bytes), "webdav");
|
|
1161
|
+
pulled += 1;
|
|
1162
|
+
newestSuccess = laterOf(newestSuccess, finished);
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const data = Buffer.from(fetched.bytes).toString("base64");
|
|
1166
|
+
const mediaType = entry.contentType ?? fetched.contentType;
|
|
1167
|
+
if (INBOX_IMAGE_TYPES.includes(mediaType)) {
|
|
1168
|
+
const [ref2] = await admitEncodedImages(attachments, [
|
|
1169
|
+
{ mediaType, data, name: name2 }
|
|
1170
|
+
]);
|
|
1171
|
+
if (ref2 !== void 0) {
|
|
1172
|
+
await captureImage(
|
|
1173
|
+
vault,
|
|
1174
|
+
{
|
|
1175
|
+
id: ref2.attachmentId,
|
|
1176
|
+
mime: ref2.mediaType,
|
|
1177
|
+
bytes: ref2.bytes,
|
|
1178
|
+
width: ref2.width,
|
|
1179
|
+
height: ref2.height,
|
|
1180
|
+
filename: name2
|
|
1181
|
+
},
|
|
1182
|
+
"webdav"
|
|
1183
|
+
);
|
|
1184
|
+
pulled += 1;
|
|
1185
|
+
newestSuccess = laterOf(newestSuccess, finished);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const ref = await admitEncodedFile(attachments, { data, name: name2 });
|
|
1190
|
+
await captureImage(
|
|
1191
|
+
vault,
|
|
1192
|
+
{ id: ref.attachmentId, mime: mediaType, bytes: ref.bytes, filename: name2 },
|
|
1193
|
+
"webdav"
|
|
1194
|
+
);
|
|
1195
|
+
pulled += 1;
|
|
1196
|
+
newestSuccess = laterOf(newestSuccess, finished);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
failed2 += 1;
|
|
1199
|
+
failures.push(`${name2}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1203
|
+
const cursor = failed2 > 0 && newestSuccess === void 0 ? lastPullAt : newestSuccess ?? now;
|
|
1204
|
+
await vault.setSync({ ...vault.global.sync, lastPullAt: cursor });
|
|
1205
|
+
return {
|
|
1206
|
+
status: "ok",
|
|
1207
|
+
pulled,
|
|
1208
|
+
failed: failed2,
|
|
1209
|
+
skipped,
|
|
1210
|
+
listed,
|
|
1211
|
+
lastPullAt: cursor,
|
|
1212
|
+
...failures.length === 0 ? {} : { reason: failures.slice(0, 3).join("\uFF1B") }
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async function pullRemote(vault, config, deps) {
|
|
1216
|
+
const baseUrl = config.baseUrl.trim();
|
|
1217
|
+
if (baseUrl.length === 0) {
|
|
1218
|
+
return {
|
|
1219
|
+
status: "unconfigured",
|
|
1220
|
+
reason: "\u8FD8\u6CA1\u914D\u7F6E WebDAV \u5730\u5740",
|
|
1221
|
+
pulled: 0,
|
|
1222
|
+
failed: 0,
|
|
1223
|
+
skipped: 0,
|
|
1224
|
+
listed: 0
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
const directory = config.directory ?? DEFAULT_DIRECTORY;
|
|
1228
|
+
return ingestFrom(
|
|
1229
|
+
vault,
|
|
1230
|
+
{
|
|
1231
|
+
list: async () => (await listFolder(baseUrl, directory, deps)).map((file) => ({
|
|
1232
|
+
path: file.path,
|
|
1233
|
+
...file.lastModified === void 0 ? {} : { lastModified: file.lastModified },
|
|
1234
|
+
...file.contentType === void 0 ? {} : { contentType: file.contentType }
|
|
1235
|
+
})),
|
|
1236
|
+
read: async (entry) => readFile(entry.path, deps, entry.path.startsWith("http") ? entry.path : void 0)
|
|
1237
|
+
},
|
|
1238
|
+
deps.attachments
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
async function pullS3(vault, config, prefix, deps) {
|
|
1242
|
+
if (config.endpoint.trim().length === 0 || config.bucket.trim().length === 0) {
|
|
1243
|
+
return {
|
|
1244
|
+
status: "unconfigured",
|
|
1245
|
+
reason: "\u8FD8\u6CA1\u914D\u7F6E S3 \u7684 endpoint \u6216 bucket",
|
|
1246
|
+
pulled: 0,
|
|
1247
|
+
failed: 0,
|
|
1248
|
+
skipped: 0,
|
|
1249
|
+
listed: 0
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
return ingestFrom(
|
|
1253
|
+
vault,
|
|
1254
|
+
{
|
|
1255
|
+
list: async () => (await listPrefix(config, prefix, deps)).map((object) => ({
|
|
1256
|
+
path: object.key,
|
|
1257
|
+
...object.lastModified === void 0 ? {} : { lastModified: object.lastModified }
|
|
1258
|
+
})),
|
|
1259
|
+
read: async (entry) => readObject(config, entry.path, deps)
|
|
1260
|
+
},
|
|
1261
|
+
deps.attachments
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/host/remote/merge.ts
|
|
1266
|
+
import { admitEncodedFile as admitEncodedFile2, admitEncodedImages as admitEncodedImages2 } from "@deepseek-ai/dsh-attachment";
|
|
1267
|
+
function nameOf2(path) {
|
|
1268
|
+
const parts = path.split("/").filter((part) => part.length > 0);
|
|
1269
|
+
return parts[parts.length - 1] ?? path;
|
|
1270
|
+
}
|
|
1271
|
+
function idOf(path) {
|
|
1272
|
+
const name2 = decodeURIComponent(nameOf2(path));
|
|
1273
|
+
const match = /^([0-9a-fA-F-]{36})\.([A-Za-z0-9]{1,8})$/.exec(name2);
|
|
1274
|
+
return match?.[1];
|
|
1275
|
+
}
|
|
1276
|
+
function itemOf(bytes) {
|
|
1277
|
+
try {
|
|
1278
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
1279
|
+
if (parsed.format !== "dsh-inbox-item/1") return void 0;
|
|
1280
|
+
const record = parsed.record;
|
|
1281
|
+
if (record === void 0 || typeof record.id !== "string") return void 0;
|
|
1282
|
+
if (typeof record.updatedAt !== "string") return void 0;
|
|
1283
|
+
return record;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function attachmentOf(bytes) {
|
|
1289
|
+
try {
|
|
1290
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
1291
|
+
if (parsed.format !== "dsh-inbox-attachment/1") return void 0;
|
|
1292
|
+
const row = parsed.attachment;
|
|
1293
|
+
if (row === void 0 || typeof row.id !== "string" || typeof row.mime !== "string") {
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
return row;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return void 0;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function wins(remote, local) {
|
|
1302
|
+
if (local === void 0) return true;
|
|
1303
|
+
const incoming = Date.parse(remote.updatedAt);
|
|
1304
|
+
const current = Date.parse(local.updatedAt);
|
|
1305
|
+
if (Number.isNaN(incoming) || Number.isNaN(current)) return false;
|
|
1306
|
+
return incoming > current;
|
|
1307
|
+
}
|
|
1308
|
+
async function mergeOnce(vault, tree, prefix, admit) {
|
|
1309
|
+
const failures = [];
|
|
1310
|
+
let merged = 0;
|
|
1311
|
+
let kept = 0;
|
|
1312
|
+
let attachments = 0;
|
|
1313
|
+
let objects;
|
|
1314
|
+
try {
|
|
1315
|
+
objects = await tree.list(`${prefix}/items/`);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
return { merged: 0, kept: 0, attachments: 0, failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`] };
|
|
1318
|
+
}
|
|
1319
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
1320
|
+
const bytesAt = /* @__PURE__ */ new Map();
|
|
1321
|
+
try {
|
|
1322
|
+
for (const object of await tree.list(`${prefix}/attachments/`)) {
|
|
1323
|
+
const id = idOf(object.path);
|
|
1324
|
+
if (id === void 0 || nameOf2(object.path).endsWith(".meta.json")) continue;
|
|
1325
|
+
bytesAt.set(id, object.path);
|
|
1326
|
+
}
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
failures.push(`\u5217\u8FDC\u7AEF\u9644\u4EF6\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`);
|
|
1329
|
+
}
|
|
1330
|
+
for (const object of objects) {
|
|
1331
|
+
const id = idOf(object.path);
|
|
1332
|
+
const name2 = decodeURIComponent(nameOf2(object.path));
|
|
1333
|
+
if (id === void 0 || !name2.endsWith(".json")) continue;
|
|
1334
|
+
try {
|
|
1335
|
+
const remote = itemOf(await tree.read(object.path));
|
|
1336
|
+
if (remote === void 0) {
|
|
1337
|
+
failures.push(`${name2}\uFF1A\u4E0D\u662F\u672C\u63D2\u4EF6\u7684\u8BB0\u5F55\u683C\u5F0F`);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const local = vault.get(remote.id);
|
|
1341
|
+
if (!wins(remote, local)) {
|
|
1342
|
+
kept += 1;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
await vault.import(remote);
|
|
1346
|
+
merged += 1;
|
|
1347
|
+
for (const attachmentId of remote.attachmentIds) {
|
|
1348
|
+
if (vault.getAttachment(attachmentId) === void 0) {
|
|
1349
|
+
wanted.set(attachmentId, remote.id);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
failures.push(`${name2}\uFF1A${reasonOf(error)}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
for (const [attachmentId, owner] of wanted) {
|
|
1357
|
+
try {
|
|
1358
|
+
const meta = attachmentOf(await tree.read(`${prefix}/attachments/${attachmentId}.meta.json`));
|
|
1359
|
+
if (meta === void 0) {
|
|
1360
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF08\u8BB0\u5F55 ${owner}\uFF09\uFF1A\u8FDC\u7AEF\u6CA1\u6709\u5B83\u7684\u5143\u6570\u636E`);
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
const sibling = bytesAt.get(attachmentId);
|
|
1364
|
+
if (sibling === void 0) {
|
|
1365
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF08\u8BB0\u5F55 ${owner}\uFF09\uFF1A\u8FDC\u7AEF\u6CA1\u6709\u5B83\u7684\u5B57\u8282`);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const bytes = await tree.read(sibling);
|
|
1369
|
+
const name2 = meta.filename ?? `${attachmentId}.${extensionOfName(sibling)}`;
|
|
1370
|
+
const stored = meta.mime.startsWith("image/") ? await admit.image(bytes, meta.mime, name2) : await admit.file(bytes, name2);
|
|
1371
|
+
if (stored === void 0) {
|
|
1372
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A\u672C\u673A\u9644\u4EF6\u4ED3\u5E93\u62D2\u7EDD\u4E86\u5B83`);
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
await vault.importAttachment({ ...meta, storeId: stored.storeId });
|
|
1376
|
+
attachments += 1;
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf(error)}`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return { merged, kept, attachments, failures };
|
|
1382
|
+
}
|
|
1383
|
+
function extensionOfName(path) {
|
|
1384
|
+
return /\.([A-Za-z0-9]{1,8})$/.exec(nameOf2(path))?.[1]?.toLowerCase() ?? "bin";
|
|
1385
|
+
}
|
|
1386
|
+
function reasonOf(error) {
|
|
1387
|
+
return error instanceof Error ? error.message : String(error);
|
|
1388
|
+
}
|
|
1389
|
+
async function mergeRemote(ctx, vault, attachments) {
|
|
1390
|
+
const settings = readSettings(ctx);
|
|
1391
|
+
const prefix = syncRoot(settings);
|
|
1392
|
+
try {
|
|
1393
|
+
const tree = settings.protocol === "s3" ? await s3Tree(ctx, settings) : await webdavTree(ctx, settings);
|
|
1394
|
+
if (tree === void 0) return { merged: 0, kept: 0, attachments: 0, failures: [] };
|
|
1395
|
+
return await mergeOnce(vault, tree, prefix, admitWith(attachments));
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
return { merged: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function admitWith(store) {
|
|
1401
|
+
return {
|
|
1402
|
+
image: async (bytes, mime, name2) => {
|
|
1403
|
+
if (!INBOX_IMAGE_TYPES.includes(mime)) return void 0;
|
|
1404
|
+
const data = Buffer.from(bytes).toString("base64");
|
|
1405
|
+
const [ref] = await admitEncodedImages2(store, [
|
|
1406
|
+
{ mediaType: mime, data, name: name2 }
|
|
1407
|
+
]);
|
|
1408
|
+
return ref === void 0 ? void 0 : { storeId: ref.attachmentId };
|
|
1409
|
+
},
|
|
1410
|
+
file: async (bytes, name2) => {
|
|
1411
|
+
const ref = await admitEncodedFile2(store, {
|
|
1412
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
1413
|
+
name: name2
|
|
1414
|
+
});
|
|
1415
|
+
return { storeId: ref.attachmentId };
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
async function s3Tree(ctx, settings) {
|
|
1420
|
+
const secret = await readS3Secret(ctx);
|
|
1421
|
+
if (settings.endpoint.trim().length === 0 || settings.bucket.trim().length === 0) return void 0;
|
|
1422
|
+
if (secret === void 0) return void 0;
|
|
1423
|
+
const config = {
|
|
1424
|
+
endpoint: settings.endpoint,
|
|
1425
|
+
bucket: settings.bucket,
|
|
1426
|
+
region: settings.region,
|
|
1427
|
+
signatureVersion: settings.signatureVersion,
|
|
1428
|
+
userAgent: activeUserAgent(settings)
|
|
1429
|
+
};
|
|
1430
|
+
const deps = {
|
|
1431
|
+
fetch: s3Fetch,
|
|
1432
|
+
accessKeyId: settings.accessKeyId,
|
|
1433
|
+
accessKeySecret: secret
|
|
1434
|
+
};
|
|
1435
|
+
return {
|
|
1436
|
+
list: async (path) => (await listPrefix(config, path, deps)).map((entry) => ({
|
|
1437
|
+
path: entry.key,
|
|
1438
|
+
...entry.lastModified === void 0 ? {} : { lastModified: entry.lastModified }
|
|
1439
|
+
})),
|
|
1440
|
+
read: async (path) => (await readObject(config, path, deps)).bytes
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
async function webdavTree(ctx, settings) {
|
|
1444
|
+
if (settings.baseUrl.trim().length === 0) return void 0;
|
|
1445
|
+
const password = await readPassword(ctx);
|
|
1446
|
+
const deps = {
|
|
1447
|
+
fetch: webdavFetch,
|
|
1448
|
+
...settings.username.length === 0 || password === void 0 ? {} : { auth: { username: settings.username, password } },
|
|
1449
|
+
userAgent: activeUserAgent(settings)
|
|
1450
|
+
};
|
|
1451
|
+
const baseUrl = settings.baseUrl;
|
|
1452
|
+
return {
|
|
1453
|
+
list: async (path) => (await listFolder(baseUrl, path, deps)).map((file) => ({
|
|
1454
|
+
path: file.path,
|
|
1455
|
+
...file.lastModified === void 0 ? {} : { lastModified: file.lastModified }
|
|
1456
|
+
})),
|
|
1457
|
+
read: async (path) => (await readFile(path, deps, path.startsWith("http") ? path : void 0)).bytes
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/host/webdav/run.ts
|
|
1462
|
+
var webdavFetch = async (url, init) => {
|
|
1463
|
+
const response = await fetch(url, {
|
|
1464
|
+
method: init.method,
|
|
1465
|
+
headers: init.headers,
|
|
1466
|
+
...init.body === void 0 ? {} : { body: init.body }
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
ok: response.ok,
|
|
1470
|
+
status: response.status,
|
|
1471
|
+
text: () => response.text(),
|
|
1472
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
1473
|
+
headers: { get: (name2) => response.headers.get(name2) }
|
|
1474
|
+
};
|
|
1475
|
+
};
|
|
1476
|
+
var s3Fetch = async (url, init) => {
|
|
1477
|
+
const body = init.body === void 0 ? void 0 : init.body;
|
|
1478
|
+
const response = await fetch(url, {
|
|
1479
|
+
method: init.method,
|
|
1480
|
+
headers: init.headers,
|
|
1481
|
+
...body === void 0 ? {} : { body }
|
|
1482
|
+
});
|
|
1483
|
+
return {
|
|
1484
|
+
ok: response.ok,
|
|
1485
|
+
status: response.status,
|
|
1486
|
+
text: () => response.text(),
|
|
1487
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
1488
|
+
headers: { get: (name2) => response.headers.get(name2) }
|
|
1489
|
+
};
|
|
1490
|
+
};
|
|
1491
|
+
async function runPull(ctx, vault, attachments) {
|
|
1492
|
+
return withMerge(ctx, vault, attachments, await pullDropFolder(ctx, vault, attachments));
|
|
1493
|
+
}
|
|
1494
|
+
async function withMerge(ctx, vault, attachments, pulled) {
|
|
1495
|
+
if (pulled.status !== "ok" || attachments === void 0) return pulled;
|
|
1496
|
+
const outcome = await mergeRemote(ctx, vault, attachments);
|
|
1497
|
+
const troubles = [...pulled.failed > 0 && pulled.reason !== void 0 ? [pulled.reason] : [], ...outcome.failures];
|
|
1498
|
+
return {
|
|
1499
|
+
...pulled,
|
|
1500
|
+
merged: outcome.merged,
|
|
1501
|
+
attachments: outcome.attachments,
|
|
1502
|
+
failed: pulled.failed + outcome.failures.length,
|
|
1503
|
+
...troubles.length === 0 ? {} : { reason: troubles.slice(0, 3).join("\uFF1B") }
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function pullDropFolder(ctx, vault, attachments) {
|
|
1507
|
+
const settings = readSettings(ctx);
|
|
1508
|
+
if (attachments === void 0) {
|
|
1509
|
+
return {
|
|
1510
|
+
status: "failed",
|
|
1511
|
+
reason: "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u9644\u4EF6\u4ED3\u5E93\uFF0C\u62C9\u4E0B\u6765\u7684\u6587\u4EF6\u6CA1\u5904\u653E",
|
|
1512
|
+
pulled: 0,
|
|
1513
|
+
failed: 0,
|
|
1514
|
+
skipped: 0,
|
|
1515
|
+
listed: 0
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if (settings.protocol === "s3") {
|
|
1519
|
+
const secret = await readS3Secret(ctx);
|
|
1520
|
+
if (settings.endpoint.trim().length === 0 || settings.bucket.trim().length === 0) {
|
|
1521
|
+
return {
|
|
1522
|
+
status: "unconfigured",
|
|
1523
|
+
reason: "\u8FD8\u6CA1\u914D\u7F6E S3 \u7684 endpoint \u6216 bucket",
|
|
1524
|
+
pulled: 0,
|
|
1525
|
+
failed: 0,
|
|
1526
|
+
skipped: 0,
|
|
1527
|
+
listed: 0
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
if (secret === void 0) {
|
|
1531
|
+
return {
|
|
1532
|
+
status: "unconfigured",
|
|
1533
|
+
reason: "\u8FD8\u6CA1\u5B58 S3 \u7684 AccessKey Secret",
|
|
1534
|
+
pulled: 0,
|
|
1535
|
+
failed: 0,
|
|
1536
|
+
skipped: 0,
|
|
1537
|
+
listed: 0
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
return pullS3(
|
|
1541
|
+
vault,
|
|
1542
|
+
{
|
|
1543
|
+
endpoint: settings.endpoint,
|
|
1544
|
+
bucket: settings.bucket,
|
|
1545
|
+
region: settings.region,
|
|
1546
|
+
signatureVersion: settings.signatureVersion,
|
|
1547
|
+
userAgent: activeUserAgent(settings)
|
|
1548
|
+
},
|
|
1549
|
+
settings.directory.replace(/^\//, ""),
|
|
1550
|
+
{
|
|
1551
|
+
fetch: s3Fetch,
|
|
1552
|
+
accessKeyId: settings.accessKeyId,
|
|
1553
|
+
accessKeySecret: secret,
|
|
1554
|
+
attachments
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (settings.baseUrl.trim().length === 0) {
|
|
1559
|
+
return { status: "unconfigured", reason: "\u8FD8\u6CA1\u914D\u7F6E WebDAV \u5730\u5740", pulled: 0, failed: 0, skipped: 0, listed: 0 };
|
|
1560
|
+
}
|
|
1561
|
+
const password = await readPassword(ctx);
|
|
1562
|
+
const deps = {
|
|
1563
|
+
fetch: webdavFetch,
|
|
1564
|
+
attachments,
|
|
1565
|
+
userAgent: activeUserAgent(settings),
|
|
1566
|
+
...settings.username.length === 0 || password === void 0 ? {} : { auth: { username: settings.username, password } }
|
|
1567
|
+
};
|
|
1568
|
+
return pullRemote(vault, settings, deps);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/host/remote/writer.ts
|
|
1572
|
+
function syncRoot(settings) {
|
|
1573
|
+
const prefix = settings.directory.replace(/^\/+|\/+$/g, "");
|
|
1574
|
+
return prefix.length === 0 ? "sync" : `${prefix}/sync`;
|
|
1575
|
+
}
|
|
1576
|
+
async function remoteWriter(ctx) {
|
|
1577
|
+
const settings = readSettings(ctx);
|
|
1578
|
+
try {
|
|
1579
|
+
if (settings.protocol === "s3") {
|
|
1580
|
+
if (settings.endpoint.trim().length === 0 || settings.bucket.trim().length === 0) {
|
|
1581
|
+
return { status: "unconfigured", reason: "\u8FD8\u6CA1\u914D\u7F6E S3 \u7684 endpoint \u6216 bucket" };
|
|
1582
|
+
}
|
|
1583
|
+
const secret = await readS3Secret(ctx);
|
|
1584
|
+
if (secret === void 0) return { status: "unconfigured", reason: "\u8FD8\u6CA1\u5B58 AccessKey Secret" };
|
|
1585
|
+
const config = {
|
|
1586
|
+
endpoint: settings.endpoint,
|
|
1587
|
+
bucket: settings.bucket,
|
|
1588
|
+
region: settings.region,
|
|
1589
|
+
signatureVersion: settings.signatureVersion,
|
|
1590
|
+
userAgent: activeUserAgent(settings)
|
|
1591
|
+
};
|
|
1592
|
+
const deps2 = {
|
|
1593
|
+
fetch: s3Fetch,
|
|
1594
|
+
accessKeyId: settings.accessKeyId,
|
|
1595
|
+
accessKeySecret: secret
|
|
1596
|
+
};
|
|
1597
|
+
return {
|
|
1598
|
+
status: "ok",
|
|
1599
|
+
root: syncRoot(settings),
|
|
1600
|
+
writer: {
|
|
1601
|
+
write: (path, bytes, contentType) => putObject(config, path, bytes, deps2, contentType),
|
|
1602
|
+
remove: (path) => deleteObject(config, path, deps2)
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
if (settings.baseUrl.trim().length === 0) {
|
|
1607
|
+
return { status: "unconfigured", reason: "\u8FD8\u6CA1\u914D\u7F6E\u8FDC\u7AEF\u5730\u5740" };
|
|
1608
|
+
}
|
|
1609
|
+
const password = await readPassword(ctx);
|
|
1610
|
+
const deps = {
|
|
1611
|
+
fetch: webdavFetch,
|
|
1612
|
+
...settings.username.length === 0 || password === void 0 ? {} : { auth: { username: settings.username, password } },
|
|
1613
|
+
userAgent: activeUserAgent(settings)
|
|
1614
|
+
};
|
|
1615
|
+
const base = settings.baseUrl;
|
|
1616
|
+
return {
|
|
1617
|
+
status: "ok",
|
|
1618
|
+
root: syncRoot(settings),
|
|
1619
|
+
writer: {
|
|
1620
|
+
write: (path, bytes, contentType) => writeFile(base, path, bytes, deps, contentType),
|
|
1621
|
+
remove: (path) => deleteFile(base, path, deps)
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
return { status: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/host/remote/push.ts
|
|
1630
|
+
function imageRef(record) {
|
|
1631
|
+
return {
|
|
1632
|
+
attachmentId: record.storeId,
|
|
1633
|
+
mediaType: record.mime,
|
|
1634
|
+
bytes: record.bytes,
|
|
1635
|
+
width: record.width ?? 0,
|
|
1636
|
+
height: record.height ?? 0,
|
|
1637
|
+
...record.filename === void 0 ? {} : { name: record.filename }
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function fileRef(record) {
|
|
1641
|
+
return {
|
|
1642
|
+
attachmentId: record.storeId,
|
|
1643
|
+
name: record.filename ?? "file",
|
|
1644
|
+
bytes: record.bytes
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
async function bytesOf(attachments, record) {
|
|
1648
|
+
try {
|
|
1649
|
+
if (record.mime.startsWith("image/")) {
|
|
1650
|
+
return (await attachments.readImage(imageRef(record))).data;
|
|
1651
|
+
}
|
|
1652
|
+
const chunks = [];
|
|
1653
|
+
for await (const chunk of attachments.readFileStream(fileRef(record))) chunks.push(chunk);
|
|
1654
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
1655
|
+
const joined = new Uint8Array(total);
|
|
1656
|
+
let at = 0;
|
|
1657
|
+
for (const chunk of chunks) {
|
|
1658
|
+
joined.set(chunk, at);
|
|
1659
|
+
at += chunk.byteLength;
|
|
1660
|
+
}
|
|
1661
|
+
return joined;
|
|
1662
|
+
} catch {
|
|
1663
|
+
return void 0;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function packItem(item) {
|
|
1667
|
+
return new TextEncoder().encode(
|
|
1668
|
+
JSON.stringify({ format: "dsh-inbox-item/1", record: item }, void 0, 0)
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
var EXTENSIONS = {
|
|
1672
|
+
"image/jpeg": "jpg",
|
|
1673
|
+
"image/png": "png",
|
|
1674
|
+
"image/webp": "webp",
|
|
1675
|
+
"image/gif": "gif",
|
|
1676
|
+
"video/mp4": "mp4",
|
|
1677
|
+
"video/quicktime": "mov",
|
|
1678
|
+
"audio/mpeg": "mp3",
|
|
1679
|
+
"audio/mp4": "m4a",
|
|
1680
|
+
"application/pdf": "pdf",
|
|
1681
|
+
"text/plain": "txt",
|
|
1682
|
+
"application/json": "json"
|
|
1683
|
+
};
|
|
1684
|
+
function extensionOf(filename, mime) {
|
|
1685
|
+
const fromName = /\.([A-Za-z0-9]{1,8})$/.exec(filename ?? "")?.[1];
|
|
1686
|
+
if (fromName !== void 0) return fromName.toLowerCase();
|
|
1687
|
+
return EXTENSIONS[mime.toLowerCase()] ?? "bin";
|
|
1688
|
+
}
|
|
1689
|
+
function attachmentObjectName(attachmentId, record) {
|
|
1690
|
+
return `${attachmentId}.${extensionOf(record.filename, record.mime)}`;
|
|
1691
|
+
}
|
|
1692
|
+
function packAttachment(record) {
|
|
1693
|
+
return new TextEncoder().encode(
|
|
1694
|
+
JSON.stringify(
|
|
1695
|
+
{
|
|
1696
|
+
format: "dsh-inbox-attachment/1",
|
|
1697
|
+
attachment: {
|
|
1698
|
+
id: record.id,
|
|
1699
|
+
mime: record.mime,
|
|
1700
|
+
bytes: record.bytes,
|
|
1701
|
+
// Part of the row, and a domain that reads without it refuses to open
|
|
1702
|
+
// at all — the merge learned that the hard way.
|
|
1703
|
+
createdAt: record.createdAt,
|
|
1704
|
+
...record.filename === void 0 ? {} : { filename: record.filename },
|
|
1705
|
+
...record.width === void 0 ? {} : { width: record.width },
|
|
1706
|
+
...record.height === void 0 ? {} : { height: record.height },
|
|
1707
|
+
...record.sha256 === void 0 ? {} : { sha256: record.sha256 }
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
void 0,
|
|
1711
|
+
0
|
|
1712
|
+
)
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
function readableTime(iso) {
|
|
1716
|
+
const at = new Date(iso);
|
|
1717
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
1718
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1719
|
+
return `${String(at.getFullYear())}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
1720
|
+
}
|
|
1721
|
+
function renderItemText(item, attachmentNames) {
|
|
1722
|
+
const heading = item.title ?? item.linkTitle ?? item.url ?? (item.category === "secret" ? "\u5BC6\u94A5 / \u8D26\u5BC6" : "\uFF08\u65E0\u6807\u9898\uFF09");
|
|
1723
|
+
const lines = [heading, "=".repeat(Math.min(40, Math.max(6, heading.length))), ""];
|
|
1724
|
+
lines.push(`\u7C7B\u76EE\uFF1A${CATEGORY_LABELS[item.category]}\u3000\u7C7B\u578B\uFF1A${KIND_LABELS[item.kind]}\u3000\u6765\u6E90\uFF1A${item.source}`);
|
|
1725
|
+
lines.push(`\u5B58\u5165\uFF1A${readableTime(item.createdAt)}${item.updatedAt === item.createdAt ? "" : `\u3000\u66F4\u65B0\uFF1A${readableTime(item.updatedAt)}`}`);
|
|
1726
|
+
if (item.platform !== void 0) lines.push(`\u5E73\u53F0\uFF1A${item.platform}`);
|
|
1727
|
+
if (item.tags.length > 0) lines.push(`\u6807\u7B7E\uFF1A${item.tags.map((tag) => `#${tag}`).join(" ")}`);
|
|
1728
|
+
if (item.watchLater === true) lines.push("\u5F85\u770B\uFF1A\u662F");
|
|
1729
|
+
if (item.deletedAt !== void 0) lines.push(`\u56DE\u6536\u7AD9\uFF1A\u662F\uFF08${readableTime(item.deletedAt)} \u5220\u9664\uFF09`);
|
|
1730
|
+
if (item.url !== void 0) lines.push(`\u94FE\u63A5\uFF1A${item.url}`);
|
|
1731
|
+
if (item.linkTitle !== void 0 && item.linkTitle !== heading) lines.push(`\u9875\u9762\u6807\u9898\uFF1A${item.linkTitle}`);
|
|
1732
|
+
if (attachmentNames.length > 0) {
|
|
1733
|
+
lines.push("\u9644\u4EF6\uFF1A");
|
|
1734
|
+
for (const name2 of attachmentNames) lines.push(` - ${name2}`);
|
|
1735
|
+
}
|
|
1736
|
+
lines.push("");
|
|
1737
|
+
if (item.note !== void 0 && item.note.length > 0) {
|
|
1738
|
+
lines.push("\u5907\u6CE8", "----", item.note, "");
|
|
1739
|
+
}
|
|
1740
|
+
if (item.category === "secret") {
|
|
1741
|
+
lines.push("\u6B63\u6587", "----", "\uFF08\u52A0\u5BC6\u3002\u9700\u8981\u4E3B\u5BC6\u7801\u5728\u672C\u673A\u89E3\u9501\u540E\u624D\u80FD\u8BFB\u5230\u6B63\u6587\uFF1B\u5BC6\u94A5\u4E0E\u5BC6\u7801\u6C38\u4E0D\u968F\u540C\u6B65\u4E0A\u4F20\u3002\uFF09");
|
|
1742
|
+
} else if (item.text !== void 0 && item.text.length > 0) {
|
|
1743
|
+
lines.push("\u6B63\u6587", "----", item.text);
|
|
1744
|
+
}
|
|
1745
|
+
return `${lines.join("\n").trimEnd()}
|
|
1746
|
+
`;
|
|
1747
|
+
}
|
|
1748
|
+
async function pushRemote(ctx, vault, attachments, options = {}) {
|
|
1749
|
+
if (attachments === void 0) return failed("\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u9644\u4EF6\u4ED3\u5E93\uFF0C\u9644\u4EF6\u6CA1\u6CD5\u4E0A\u4F20");
|
|
1750
|
+
const connection = await remoteWriter(ctx);
|
|
1751
|
+
if (connection.status === "unconfigured") return unconfigured(connection.reason);
|
|
1752
|
+
if (connection.status === "failed") return failed(connection.reason);
|
|
1753
|
+
return pushOnce(
|
|
1754
|
+
vault,
|
|
1755
|
+
attachments,
|
|
1756
|
+
connection.writer.write,
|
|
1757
|
+
connection.root,
|
|
1758
|
+
options,
|
|
1759
|
+
connection.writer.remove
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
async function pushOnce(vault, attachments, writer, basePath, options = {}, remover) {
|
|
1763
|
+
const lastPushAt = options.all === true ? void 0 : vault.global.sync.lastPushAt;
|
|
1764
|
+
const items = vault.list({ includeDeleted: true });
|
|
1765
|
+
let pushed = 0;
|
|
1766
|
+
let attachmentCount = 0;
|
|
1767
|
+
let skipped = 0;
|
|
1768
|
+
const failures = [];
|
|
1769
|
+
const seenAttachments = /* @__PURE__ */ new Set();
|
|
1770
|
+
for (const item of items) {
|
|
1771
|
+
if (lastPushAt !== void 0 && item.updatedAt <= lastPushAt) {
|
|
1772
|
+
skipped += 1;
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
await writer(`${basePath}/items/${item.id}.json`, packItem(item), "application/json");
|
|
1777
|
+
pushed += 1;
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
failures.push(`\u8BB0\u5F55 ${item.id}\uFF1A${reasonOf2(error)}`);
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const attachmentNames = [];
|
|
1783
|
+
for (const attachmentId of item.attachmentIds) {
|
|
1784
|
+
if (seenAttachments.has(attachmentId)) continue;
|
|
1785
|
+
seenAttachments.add(attachmentId);
|
|
1786
|
+
const record = vault.getAttachment(attachmentId);
|
|
1787
|
+
if (record === void 0) continue;
|
|
1788
|
+
const objectName = attachmentObjectName(attachmentId, record);
|
|
1789
|
+
attachmentNames.push(`attachments/${objectName}\uFF08${record.mime}\uFF0C${String(record.bytes)} \u5B57\u8282${record.filename === void 0 ? "" : `\uFF0C\u539F\u540D ${record.filename}`}\uFF09`);
|
|
1790
|
+
const bytes = await bytesOf(attachments, record);
|
|
1791
|
+
if (bytes === void 0) {
|
|
1792
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A\u672C\u673A\u62FF\u4E0D\u5230\u5B57\u8282`);
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
try {
|
|
1796
|
+
await writer(`${basePath}/attachments/${objectName}`, bytes, record.mime);
|
|
1797
|
+
await writer(
|
|
1798
|
+
`${basePath}/attachments/${attachmentId}.meta.json`,
|
|
1799
|
+
packAttachment(record),
|
|
1800
|
+
"application/json"
|
|
1801
|
+
);
|
|
1802
|
+
try {
|
|
1803
|
+
await remover?.(`${basePath}/attachments/${attachmentId}`);
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
attachmentCount += 1;
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf2(error)}`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
await writer(
|
|
1813
|
+
`${basePath}/items/${item.id}.txt`,
|
|
1814
|
+
new TextEncoder().encode(renderItemText(item, attachmentNames)),
|
|
1815
|
+
"text/plain; charset=utf-8"
|
|
1816
|
+
);
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
failures.push(`\u8BB0\u5F55 ${item.id}\uFF08\u6587\u672C\u89C6\u56FE\uFF09\uFF1A${reasonOf2(error)}`);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1822
|
+
if (pushed > 0 || attachmentCount > 0) await vault.setSync({ ...vault.global.sync, lastPushAt: now });
|
|
1823
|
+
return {
|
|
1824
|
+
status: failures.length === 0 ? "ok" : pushed + attachmentCount === 0 ? "failed" : "partial",
|
|
1825
|
+
pushed,
|
|
1826
|
+
attachments: attachmentCount,
|
|
1827
|
+
skipped,
|
|
1828
|
+
listed: items.length,
|
|
1829
|
+
lastPushAt: now,
|
|
1830
|
+
...failures.length === 0 ? {} : { reason: failures.slice(0, 3).join("\uFF1B") }
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function failed(reason) {
|
|
1834
|
+
return {
|
|
1835
|
+
status: "failed",
|
|
1836
|
+
reason,
|
|
1837
|
+
pushed: 0,
|
|
1838
|
+
attachments: 0,
|
|
1839
|
+
skipped: 0,
|
|
1840
|
+
listed: 0
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function unconfigured(reason) {
|
|
1844
|
+
return { ...failed(reason), status: "unconfigured" };
|
|
1845
|
+
}
|
|
1846
|
+
function reasonOf2(error) {
|
|
1847
|
+
return error instanceof Error ? error.message : String(error);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/host/remote/auto-push.ts
|
|
1851
|
+
var AUTO_PUSH_DELAY_MS = 5e3;
|
|
1852
|
+
var timer;
|
|
1853
|
+
function scheduleAutoPush(run) {
|
|
1854
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1855
|
+
timer = setTimeout(() => {
|
|
1856
|
+
timer = void 0;
|
|
1857
|
+
void run().catch(() => void 0);
|
|
1858
|
+
}, AUTO_PUSH_DELAY_MS);
|
|
1859
|
+
timer.unref?.();
|
|
1860
|
+
}
|
|
1861
|
+
function makeAutoPush(ctx, vault, attachments) {
|
|
1862
|
+
return async () => {
|
|
1863
|
+
const open3 = vault();
|
|
1864
|
+
const store = attachments();
|
|
1865
|
+
if (open3 === void 0 || store === void 0) return;
|
|
1866
|
+
await pushRemote(ctx, open3, store);
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/host/command.ts
|
|
1871
|
+
function describe(summary) {
|
|
1872
|
+
const parts = [];
|
|
1873
|
+
if (summary.stored > 0) parts.push(`\u5DF2\u5B58\u5165 ${summary.stored} \u6761`);
|
|
1874
|
+
const repeats = summary.merged - summary.restored;
|
|
1875
|
+
if (repeats > 0) parts.push(`\u5408\u5E76 ${repeats} \u6761\u91CD\u590D\u9879`);
|
|
1876
|
+
if (summary.restored > 0) parts.push(`\u4ECE\u56DE\u6536\u7AD9\u53D6\u56DE ${summary.restored} \u6761`);
|
|
1877
|
+
return parts.join("\uFF0C");
|
|
1878
|
+
}
|
|
1879
|
+
function toCaptured(block) {
|
|
1880
|
+
if (block.type === "image") {
|
|
1881
|
+
const { attachmentId: attachmentId2, mediaType, bytes: bytes2, width, height, name: name3 } = block.attachment;
|
|
1882
|
+
return {
|
|
1883
|
+
id: attachmentId2,
|
|
1884
|
+
mime: mediaType,
|
|
1885
|
+
bytes: bytes2,
|
|
1886
|
+
width,
|
|
1887
|
+
height,
|
|
1888
|
+
...name3 === void 0 ? {} : { filename: name3 }
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
const { attachmentId, name: name2, bytes } = block.attachment;
|
|
1892
|
+
return { id: attachmentId, mime: "application/octet-stream", bytes, filename: name2 };
|
|
1893
|
+
}
|
|
1894
|
+
function registerInboxCommand(ctx, vault) {
|
|
1895
|
+
let autoPush;
|
|
1896
|
+
ctx.inject(["attachments"], (scoped) => {
|
|
1897
|
+
autoPush = makeAutoPush(scoped, vault, () => scoped.attachments);
|
|
1898
|
+
});
|
|
1899
|
+
ctx.commands.register({
|
|
1900
|
+
name: "inbox",
|
|
1901
|
+
description: "\u6536\u8FDB\u4ED3\u5E93\uFF1A\u628A\u8FD9\u6BB5\u6587\u5B57\u3001\u94FE\u63A5\u6216\u9644\u4EF6\u5B58\u8FDB dsh-inbox\uFF0C\u4E0D\u53D1\u7ED9\u6A21\u578B",
|
|
1902
|
+
input: { hint: "<\u6587\u5B57 / \u94FE\u63A5\uFF1B\u56FE\u7247\u53EF\u76F4\u63A5\u62D6\u8FDB\u8F93\u5165\u6846>", attachments: true },
|
|
1903
|
+
recordInput: false,
|
|
1904
|
+
async handler(invocation) {
|
|
1905
|
+
const open3 = vault();
|
|
1906
|
+
if (open3 === void 0) {
|
|
1907
|
+
return { kind: "error", text: "dsh-inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5" };
|
|
1908
|
+
}
|
|
1909
|
+
const summary = await capture(
|
|
1910
|
+
open3,
|
|
1911
|
+
{ text: invocation.rawInput, attachments: invocation.attachments.map(toCaptured) },
|
|
1912
|
+
"chat",
|
|
1913
|
+
{ ctx }
|
|
1914
|
+
);
|
|
1915
|
+
if (summary.stored === 0 && summary.merged === 0) {
|
|
1916
|
+
return {
|
|
1917
|
+
kind: "error",
|
|
1918
|
+
text: "\u6CA1\u4E1C\u897F\u53EF\u5B58\uFF1A/inbox \u540E\u9762\u8DDF\u6587\u5B57\u6216\u94FE\u63A5\uFF0C\u6216\u8005\u628A\u56FE\u7247\u62D6\u8FDB\u8F93\u5165\u6846"
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
if (autoPush !== void 0 && summary.stored > 0) scheduleAutoPush(autoPush);
|
|
1922
|
+
return { kind: "success", text: describe(summary) };
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/host/rpc.ts
|
|
1928
|
+
import { open as open2, readFile as readFile2 } from "node:fs/promises";
|
|
1929
|
+
import {
|
|
1930
|
+
admitEncodedFile as admitEncodedFile3,
|
|
1931
|
+
admitEncodedImages as admitEncodedImages3,
|
|
1932
|
+
isAttachmentError
|
|
1933
|
+
} from "@deepseek-ai/dsh-attachment";
|
|
1934
|
+
import { z as z4 } from "zod";
|
|
1935
|
+
|
|
1936
|
+
// src/host/remote/remove.ts
|
|
1937
|
+
async function removeRemoteRecords(ctx, removed) {
|
|
1938
|
+
if (removed.itemIds.length === 0 && removed.attachments.length === 0) {
|
|
1939
|
+
return { removed: 0, failures: [] };
|
|
1940
|
+
}
|
|
1941
|
+
const connection = await remoteWriter(ctx);
|
|
1942
|
+
if (connection.status !== "ok") {
|
|
1943
|
+
return { removed: 0, failures: [], skipped: true };
|
|
1944
|
+
}
|
|
1945
|
+
return removeWith(connection.writer, connection.root, removed);
|
|
1946
|
+
}
|
|
1947
|
+
async function removeWith(writer, root, removed) {
|
|
1948
|
+
const failures = [];
|
|
1949
|
+
let removedCount = 0;
|
|
1950
|
+
const drop = async (path) => {
|
|
1951
|
+
try {
|
|
1952
|
+
await writer.remove(path);
|
|
1953
|
+
removedCount += 1;
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
failures.push(`${path}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
for (const id of removed.itemIds) {
|
|
1959
|
+
await drop(`${root}/items/${id}.json`);
|
|
1960
|
+
await drop(`${root}/items/${id}.txt`);
|
|
1961
|
+
}
|
|
1962
|
+
for (const record of removed.attachments) {
|
|
1963
|
+
await drop(`${root}/attachments/${record.id}.${extensionOf(record.filename, record.mime)}`);
|
|
1964
|
+
await drop(`${root}/attachments/${record.id}.meta.json`);
|
|
1965
|
+
await drop(`${root}/attachments/${record.id}`);
|
|
1966
|
+
}
|
|
1967
|
+
return { removed: removedCount, failures };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/host/s3/probe.ts
|
|
1971
|
+
var EXCERPT = 400;
|
|
1972
|
+
async function probe(config, deps, label, key, query, signOverride) {
|
|
1973
|
+
const signed = (signOverride ?? signer(config))(config, deps, "GET", key, query);
|
|
1974
|
+
try {
|
|
1975
|
+
const response = await deps.fetch(signed.url, { method: "GET", headers: signed.headers });
|
|
1976
|
+
let detail = "";
|
|
1977
|
+
try {
|
|
1978
|
+
detail = (await response.text()).trim().slice(0, EXCERPT).replace(/\s+/g, " ");
|
|
1979
|
+
} catch {
|
|
1980
|
+
detail = "(\u8BFB\u4E0D\u5230\u54CD\u5E94\u4F53)";
|
|
1981
|
+
}
|
|
1982
|
+
return { label, url: signed.url, status: response.status, detail };
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
return {
|
|
1985
|
+
label,
|
|
1986
|
+
url: signed.url,
|
|
1987
|
+
status: 0,
|
|
1988
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
async function probeS3(config, deps, prefix) {
|
|
1993
|
+
return [
|
|
1994
|
+
// Version and region are what a v4 signature covers, and a gateway that
|
|
1995
|
+
// checks either will refuse a request whose scope disagrees with it. Trying
|
|
1996
|
+
// a few regions at once answers "which one does it want" in one click.
|
|
1997
|
+
await probe({ ...config, signatureVersion: "v4", region: "us-east-1" }, deps, "v4 \xB7 us-east-1 \xB7 \u65E0\u53C2\u6570", "", {}),
|
|
1998
|
+
await probe(
|
|
1999
|
+
{ ...config, signatureVersion: "v4", region: "us-east-1" },
|
|
2000
|
+
deps,
|
|
2001
|
+
"v4 \u7CBE\u7B80\uFF08\u53EA\u7B7E host + date\uFF09",
|
|
2002
|
+
"",
|
|
2003
|
+
{},
|
|
2004
|
+
signRequestV4Minimal
|
|
2005
|
+
),
|
|
2006
|
+
await probe(
|
|
2007
|
+
{ ...config, signatureVersion: "v4", region: "us-east-1" },
|
|
2008
|
+
deps,
|
|
2009
|
+
"v4 \xB7 UNSIGNED-PAYLOAD\uFF08SDK \u7684\u5E38\u89C4\u5F62\u6001\uFF09",
|
|
2010
|
+
"",
|
|
2011
|
+
{},
|
|
2012
|
+
signRequestUnsignedPayload
|
|
2013
|
+
),
|
|
2014
|
+
await probe({ ...config, signatureVersion: "v4", region: "cn-north-1" }, deps, "v4 \xB7 cn-north-1 \xB7 \u65E0\u53C2\u6570", "", {}),
|
|
2015
|
+
await probe({ ...config, signatureVersion: "v4", region: "cn-northwest-1" }, deps, "v4 \xB7 cn-northwest-1 \xB7 \u65E0\u53C2\u6570", "", {}),
|
|
2016
|
+
await probe({ ...config, signatureVersion: "v4", region: "us-east-1" }, deps, "v4 \xB7 us-east-1 \xB7 \u5E26 prefix", "", { prefix }),
|
|
2017
|
+
await probe({ ...config, signatureVersion: "v2" }, deps, "v2 \xB7 \u65E0\u53C2\u6570", "", {}),
|
|
2018
|
+
await probe({ ...config, signatureVersion: "v2" }, deps, "v2 \xB7 \u5E26 prefix", "", { prefix })
|
|
2019
|
+
];
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/host/webdav/probe.ts
|
|
2023
|
+
var EXCERPT2 = 120;
|
|
2024
|
+
async function probeWebdav(deps, baseUrl, directory) {
|
|
2025
|
+
const url = joinUrl(baseUrl.replace(/\/+$/, ""), directory.replace(/^\/+/, ""));
|
|
2026
|
+
const headers = {
|
|
2027
|
+
...authHeaders(deps.auth),
|
|
2028
|
+
...userAgentHeaders(deps),
|
|
2029
|
+
depth: "0"
|
|
2030
|
+
};
|
|
2031
|
+
try {
|
|
2032
|
+
const response = await deps.fetch(url, { method: "PROPFIND", headers });
|
|
2033
|
+
let detail = "";
|
|
2034
|
+
try {
|
|
2035
|
+
detail = (await response.text()).trim().slice(0, EXCERPT2).replace(/\s+/g, " ");
|
|
2036
|
+
} catch {
|
|
2037
|
+
detail = "(\u8BFB\u4E0D\u5230\u54CD\u5E94\u4F53)";
|
|
2038
|
+
}
|
|
2039
|
+
return [{ label: "PROPFIND \u76EE\u5F55\uFF08\u53EA\u8BFB\uFF09", url, status: response.status, detail }];
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
return [
|
|
2042
|
+
{
|
|
2043
|
+
label: "PROPFIND \u76EE\u5F55\uFF08\u53EA\u8BFB\uFF09",
|
|
2044
|
+
url,
|
|
2045
|
+
status: 0,
|
|
2046
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2047
|
+
}
|
|
2048
|
+
];
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// src/host/ui/config.ts
|
|
2053
|
+
import z2 from "@deepseek-ai/schemastery";
|
|
2054
|
+
var UI_SETTINGS_NAMESPACE = "dsh-inbox-ui";
|
|
2055
|
+
var DEFAULT_UI_PREFS = { listMode: "grid" };
|
|
2056
|
+
var UiPrefsSchema = z2.object({
|
|
2057
|
+
listMode: z2.union(["grid", "compact"]).default("grid")
|
|
2058
|
+
});
|
|
2059
|
+
function installUiSettings(ctx) {
|
|
2060
|
+
const settings = ctx.get("settings");
|
|
2061
|
+
if (settings === void 0) return;
|
|
2062
|
+
try {
|
|
2063
|
+
settings.register(UI_SETTINGS_NAMESPACE, UiPrefsSchema, { base: DEFAULT_UI_PREFS });
|
|
2064
|
+
} catch {
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function readUiPrefs(ctx) {
|
|
2068
|
+
const settings = ctx.get("settings");
|
|
2069
|
+
if (settings === void 0) return { ...DEFAULT_UI_PREFS, settingsAvailable: false };
|
|
2070
|
+
const stored = settings.get(UI_SETTINGS_NAMESPACE)?.listMode;
|
|
2071
|
+
const listMode = UI_LIST_MODES.includes(stored) ? stored : DEFAULT_UI_PREFS.listMode;
|
|
2072
|
+
return { listMode, settingsAvailable: true };
|
|
2073
|
+
}
|
|
2074
|
+
function saveUiPrefs(ctx, patch) {
|
|
2075
|
+
if (patch.listMode === void 0) return { ok: true };
|
|
2076
|
+
if (!UI_LIST_MODES.includes(patch.listMode)) {
|
|
2077
|
+
return {
|
|
2078
|
+
ok: false,
|
|
2079
|
+
reason: `\u5217\u8868\u6A21\u5F0F\u53EA\u652F\u6301 ${UI_LIST_MODES.join(" / ")}\uFF0C\u6536\u5230\u7684\u662F ${patch.listMode}`
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const settings = ctx.get("settings");
|
|
2083
|
+
if (settings === void 0) return { ok: false, reason: "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u8BBE\u7F6E\u670D\u52A1\uFF0C\u8BB0\u4E0D\u4F4F\u5217\u8868\u6A21\u5F0F" };
|
|
2084
|
+
installUiSettings(ctx);
|
|
2085
|
+
settings.update(UI_SETTINGS_NAMESPACE, { listMode: patch.listMode });
|
|
2086
|
+
return { ok: true };
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/host/vault/vault.ts
|
|
2090
|
+
import { createHmac as createHmac2, randomUUID } from "node:crypto";
|
|
2091
|
+
|
|
2092
|
+
// src/host/vault/query.ts
|
|
2093
|
+
function haystack(item) {
|
|
2094
|
+
return [item.title, item.linkTitle, item.text, item.url, item.note, ...item.tags].filter((part) => typeof part === "string" && part.length > 0).join("\n").toLowerCase();
|
|
2095
|
+
}
|
|
2096
|
+
function matches(item, query, needle) {
|
|
2097
|
+
if (item.deletedAt !== void 0 && query.includeDeleted !== true) return false;
|
|
2098
|
+
if (query.categories !== void 0 && !query.categories.includes(item.category)) return false;
|
|
2099
|
+
if (query.kinds !== void 0 && !query.kinds.includes(item.kind)) return false;
|
|
2100
|
+
if (query.watchLater !== void 0 && item.watchLater === true !== query.watchLater) return false;
|
|
2101
|
+
if (query.tags !== void 0) {
|
|
2102
|
+
for (const tag of query.tags) {
|
|
2103
|
+
if (!item.tags.includes(tag)) return false;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (needle.length > 0 && !haystack(item).includes(needle)) return false;
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
function selectItems(items, query = {}) {
|
|
2110
|
+
const needle = query.text?.trim().toLowerCase() ?? "";
|
|
2111
|
+
const matched = items.filter((item) => matches(item, query, needle));
|
|
2112
|
+
matched.sort((left, right) => {
|
|
2113
|
+
if (left.createdAt === right.createdAt) return left.id < right.id ? 1 : -1;
|
|
2114
|
+
return left.createdAt < right.createdAt ? 1 : -1;
|
|
2115
|
+
});
|
|
2116
|
+
const offset = query.offset ?? 0;
|
|
2117
|
+
if (query.limit === void 0) return offset === 0 ? matched : matched.slice(offset);
|
|
2118
|
+
return matched.slice(offset, offset + query.limit);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/host/crypto/secret-box.ts
|
|
2122
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
2123
|
+
var DEFAULT_KDF = { n: 32768, r: 8, p: 1 };
|
|
2124
|
+
var KEY_BYTES = 32;
|
|
2125
|
+
var IV_BYTES = 12;
|
|
2126
|
+
var TAG_BYTES = 16;
|
|
2127
|
+
var PREFIX = "v1";
|
|
2128
|
+
var MAX_MEM = 96 * 1024 * 1024;
|
|
2129
|
+
function newSalt() {
|
|
2130
|
+
return randomBytes(16);
|
|
2131
|
+
}
|
|
2132
|
+
function deriveKey(password, salt, kdf = DEFAULT_KDF) {
|
|
2133
|
+
return scryptSync(password.normalize("NFKC"), salt, KEY_BYTES, {
|
|
2134
|
+
N: kdf.n,
|
|
2135
|
+
r: kdf.r,
|
|
2136
|
+
p: kdf.p,
|
|
2137
|
+
maxmem: MAX_MEM
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
function seal(key, plaintext) {
|
|
2141
|
+
const iv = randomBytes(IV_BYTES);
|
|
2142
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
2143
|
+
const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2144
|
+
return [
|
|
2145
|
+
PREFIX,
|
|
2146
|
+
iv.toString("base64"),
|
|
2147
|
+
cipher.getAuthTag().toString("base64"),
|
|
2148
|
+
body.toString("base64")
|
|
2149
|
+
].join(":");
|
|
2150
|
+
}
|
|
2151
|
+
function open(key, envelope) {
|
|
2152
|
+
const parts = envelope.split(":");
|
|
2153
|
+
if (parts.length !== 4 || parts[0] !== PREFIX) return void 0;
|
|
2154
|
+
try {
|
|
2155
|
+
const iv = Buffer.from(parts[1] ?? "", "base64");
|
|
2156
|
+
const tag = Buffer.from(parts[2] ?? "", "base64");
|
|
2157
|
+
const body = Buffer.from(parts[3] ?? "", "base64");
|
|
2158
|
+
if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) return void 0;
|
|
2159
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
2160
|
+
decipher.setAuthTag(tag);
|
|
2161
|
+
return Buffer.concat([decipher.update(body), decipher.final()]).toString("utf8");
|
|
2162
|
+
} catch {
|
|
2163
|
+
return void 0;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/host/vault/spec.ts
|
|
2168
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
2169
|
+
import { z as z3 } from "zod";
|
|
2170
|
+
var timestamp = z3.string().min(1);
|
|
2171
|
+
var itemSchema = z3.object({
|
|
2172
|
+
id: z3.string().min(1),
|
|
2173
|
+
kind: z3.enum(KINDS),
|
|
2174
|
+
category: z3.enum(CATEGORIES),
|
|
2175
|
+
/**
|
|
2176
|
+
* Who decided the category — `user` outranks `model`, which outranks `rule`.
|
|
2177
|
+
* Absent on records written before domain version 2.
|
|
2178
|
+
*/
|
|
2179
|
+
categorySource: z3.enum(CATEGORY_SOURCES).optional(),
|
|
2180
|
+
/**
|
|
2181
|
+
* "I want to come back to this" — the only progress flag, and the user's to
|
|
2182
|
+
* set. Absent means not flagged, which is where every new record starts.
|
|
2183
|
+
*
|
|
2184
|
+
* Domain version 3 replaced the old read/unread pair with this: read/unread
|
|
2185
|
+
* claimed knowledge the software does not have (nothing here knows whether
|
|
2186
|
+
* you *consumed* a link), and it made every capture start as "unread", so the
|
|
2187
|
+
* badge counted your own typing back at you.
|
|
2188
|
+
*/
|
|
2189
|
+
watchLater: z3.boolean().optional(),
|
|
2190
|
+
source: z3.enum(SOURCES),
|
|
2191
|
+
createdAt: timestamp,
|
|
2192
|
+
updatedAt: timestamp,
|
|
2193
|
+
/** Link title, or a short label the user gave a text note. */
|
|
2194
|
+
title: z3.string().optional(),
|
|
2195
|
+
/**
|
|
2196
|
+
* The headline fetched from the link's own page (`<title>`), never the user's
|
|
2197
|
+
* word and never derived from record content.
|
|
2198
|
+
*
|
|
2199
|
+
* Kept apart from `title` on purpose: `title` is what the person typed, and
|
|
2200
|
+
* the credentials rule ("only the user names a record", `AGENTS.md` 3) has to
|
|
2201
|
+
* stay true even with an automatic writer in the picture.
|
|
2202
|
+
*/
|
|
2203
|
+
linkTitle: z3.string().optional(),
|
|
2204
|
+
/**
|
|
2205
|
+
* Why the last headline fetch did not produce a `linkTitle`.
|
|
2206
|
+
*
|
|
2207
|
+
* A short code, not a sentence: the panel turns it into a hint ("那个站点要求
|
|
2208
|
+
* 验证"), and the point of storing it at all is that a *silent* miss is
|
|
2209
|
+
* indistinguishable from a broken feature. Cleared when a headline does arrive.
|
|
2210
|
+
*/
|
|
2211
|
+
linkTitleError: z3.string().optional(),
|
|
2212
|
+
/** The pasted text itself, for `text` and `secret` records. */
|
|
2213
|
+
text: z3.string().optional(),
|
|
2214
|
+
/**
|
|
2215
|
+
* A credential's text, sealed with the master password (`crypto/secret-box.ts`).
|
|
2216
|
+
*
|
|
2217
|
+
* Its own field for one reason: `text` is what the panel shows and what the
|
|
2218
|
+
* search reads, and a credential's body must reach neither in the clear. A
|
|
2219
|
+
* sealed record has `secret` and **no** `text` — the migration moves them.
|
|
2220
|
+
*/
|
|
2221
|
+
secret: z3.string().optional(),
|
|
2222
|
+
/**
|
|
2223
|
+
* A keyed digest of a credential's plaintext, so a re-paste can be recognised
|
|
2224
|
+
* as the same credential without keeping the plaintext around to compare.
|
|
2225
|
+
*
|
|
2226
|
+
* Useless to an attacker without the master key, which is the point: it
|
|
2227
|
+
* survives on disk (and, later, in the sync package) while the secret does not.
|
|
2228
|
+
*/
|
|
2229
|
+
secretDigest: z3.string().optional(),
|
|
2230
|
+
url: z3.string().optional(),
|
|
2231
|
+
/** Recognised host platform: bilibili, wechat, zhihu, xiaohongshu, … */
|
|
2232
|
+
platform: z3.string().optional(),
|
|
2233
|
+
/** Description the user appended. When present it is the authoritative label. */
|
|
2234
|
+
note: z3.string().optional(),
|
|
2235
|
+
/** Soft delete marker; a deleted item keeps its bytes and can be restored. */
|
|
2236
|
+
deletedAt: z3.string().optional(),
|
|
2237
|
+
tags: z3.array(z3.string()),
|
|
2238
|
+
attachmentIds: z3.array(z3.string())
|
|
2239
|
+
});
|
|
2240
|
+
var attachmentSchema = z3.object({
|
|
2241
|
+
/**
|
|
2242
|
+
* Our own record key. It has to be path-safe (`[a-zA-Z0-9_-]+`) because the
|
|
2243
|
+
* per-record backend names a document after it — which is exactly why the
|
|
2244
|
+
* store's id below cannot serve as the key.
|
|
2245
|
+
*/
|
|
2246
|
+
id: z3.string().min(1),
|
|
2247
|
+
/**
|
|
2248
|
+
* The owning store's identifier: dsh's attachment id for anything the user
|
|
2249
|
+
* pasted. Opaque, and observed in the wild as `sha256:<hex>` — the colon is
|
|
2250
|
+
* not path-safe, so it lives here and never in the key.
|
|
2251
|
+
*/
|
|
2252
|
+
storeId: z3.string().min(1),
|
|
2253
|
+
mime: z3.string().min(1),
|
|
2254
|
+
bytes: z3.number().int().nonnegative(),
|
|
2255
|
+
createdAt: timestamp,
|
|
2256
|
+
filename: z3.string().optional(),
|
|
2257
|
+
width: z3.number().int().positive().optional(),
|
|
2258
|
+
height: z3.number().int().positive().optional(),
|
|
2259
|
+
/** Present when the medium identifies the bytes by digest. */
|
|
2260
|
+
sha256: z3.string().optional()
|
|
2261
|
+
});
|
|
2262
|
+
var vaultGlobalSchema = z3.object({
|
|
2263
|
+
sync: z3.object({
|
|
2264
|
+
lastPullAt: z3.string().optional(),
|
|
2265
|
+
/** When this machine last wrote its own records up; the push cursor. */
|
|
2266
|
+
lastPushAt: z3.string().optional(),
|
|
2267
|
+
cursor: z3.string().optional()
|
|
2268
|
+
}),
|
|
2269
|
+
/**
|
|
2270
|
+
* How to recognise a master password, and nothing more.
|
|
2271
|
+
*
|
|
2272
|
+
* The salt and work factors are not secret; the `verifier` is a sealed
|
|
2273
|
+
* constant, so a wrong password fails the same way a tampered envelope does.
|
|
2274
|
+
* The password itself is never written anywhere, and neither is the derived
|
|
2275
|
+
* key — that lives in memory for as long as the process does.
|
|
2276
|
+
*/
|
|
2277
|
+
master: z3.object({
|
|
2278
|
+
version: z3.number().int().positive(),
|
|
2279
|
+
salt: z3.string(),
|
|
2280
|
+
kdf: z3.object({ n: z3.number().int().positive(), r: z3.number().int().positive(), p: z3.number().int().positive() }),
|
|
2281
|
+
verifier: z3.string()
|
|
2282
|
+
}).optional(),
|
|
2283
|
+
/** Today's model-fallback spend, so a restart cannot reset the meter. */
|
|
2284
|
+
model: z3.object({
|
|
2285
|
+
day: z3.string(),
|
|
2286
|
+
calls: z3.number().int().nonnegative(),
|
|
2287
|
+
tokens: z3.number().int().nonnegative(),
|
|
2288
|
+
/** Why the last attempt ended the way it did; the only place to look when
|
|
2289
|
+
* a category did not change and the log is long gone. */
|
|
2290
|
+
last: z3.string().optional()
|
|
2291
|
+
}).optional()
|
|
2292
|
+
});
|
|
2293
|
+
var vaultSpec = defineDomain({
|
|
2294
|
+
name: "dsh_inbox",
|
|
2295
|
+
/**
|
|
2296
|
+
* Version 2 added the optional `categorySource`; version 3 swaps
|
|
2297
|
+
* `status` for `watchLater` and drops the `待看` tag. Both older shapes still
|
|
2298
|
+
* validate — the removed `status` key is simply ignored, and `watchLater` is
|
|
2299
|
+
* optional — and `Vault.open` rewrites them once so the flag is real.
|
|
2300
|
+
*
|
|
2301
|
+
* Version 4 adds the optional `linkTitle` (the fetched page headline). It is
|
|
2302
|
+
* a pure addition, so every older record still validates unchanged.
|
|
2303
|
+
* Version 4 adds the optional `linkTitle` (the fetched page headline), version
|
|
2304
|
+
* 5 the optional `linkTitleError` that explains a miss. Both are pure
|
|
2305
|
+
* additions, so every older record still validates unchanged.
|
|
2306
|
+
*
|
|
2307
|
+
* Version 6 adds the optional `secret` / `secretDigest`: a credential's text
|
|
2308
|
+
* moves out of `text` and into a sealed envelope. Also a pure addition — a
|
|
2309
|
+
* version-5 record with plaintext simply gets migrated on the next unlock.
|
|
2310
|
+
*
|
|
2311
|
+
* Version 7 adds `sync.lastPushAt`: the cursor that keeps a push to "what
|
|
2312
|
+
* changed since last time" instead of re-uploading the vault on every pass.
|
|
2313
|
+
* A `global` field, so no record shape changes at all.
|
|
2314
|
+
*/
|
|
2315
|
+
version: 7,
|
|
2316
|
+
compatibleVersions: [1, 2, 3, 4, 5, 6],
|
|
2317
|
+
layout: "per-record",
|
|
2318
|
+
global: {
|
|
2319
|
+
schema: vaultGlobalSchema,
|
|
2320
|
+
initial: { sync: {} }
|
|
2321
|
+
},
|
|
2322
|
+
tables: {
|
|
2323
|
+
items: domainTable(itemSchema),
|
|
2324
|
+
attachments: domainTable(attachmentSchema)
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
// src/host/vault/vault.ts
|
|
2329
|
+
var VERIFIER_PLAINTEXT = "dsh-inbox master password check";
|
|
2330
|
+
var VaultLockedError = class extends Error {
|
|
2331
|
+
constructor() {
|
|
2332
|
+
super("\u4ED3\u5E93\u9501\u7740\uFF08\u6216\u8FD8\u6CA1\u8BBE\u4E3B\u5BC6\u7801\uFF09\uFF1A\u8D26\u5BC6\u7C7B\u5185\u5BB9\u8981\u5148\u5728\u300C\u8BBE\u7F6E \u2192 \u8D26\u5BC6\u52A0\u5BC6\u300D\u91CC\u89E3\u9501\u624D\u80FD\u5B58");
|
|
2333
|
+
this.name = "VaultLockedError";
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
var RETIRED_TAG = "\u5F85\u770B";
|
|
2337
|
+
var Vault = class _Vault {
|
|
2338
|
+
constructor(ctx, domain, unit) {
|
|
2339
|
+
this.ctx = ctx;
|
|
2340
|
+
this.domain = domain;
|
|
2341
|
+
this.unit = unit;
|
|
2342
|
+
}
|
|
2343
|
+
closed = false;
|
|
2344
|
+
/**
|
|
2345
|
+
* The derived key, **memory only**.
|
|
2346
|
+
*
|
|
2347
|
+
* Nothing on disk can be used to read a credential: the master password is
|
|
2348
|
+
* never stored, the key is never stored, and a restart therefore locks the
|
|
2349
|
+
* vault again. That is the trade the red line asks for ("主密码永不上传"),
|
|
2350
|
+
* and it is why the panel has an explicit unlock.
|
|
2351
|
+
*/
|
|
2352
|
+
key;
|
|
2353
|
+
/**
|
|
2354
|
+
* Open the vault's domain over whatever backend the composition routed it to.
|
|
2355
|
+
*
|
|
2356
|
+
* @param ctx - host context carrying the storage domain facility.
|
|
2357
|
+
* @param unit - unit location, recorded for diagnostics only.
|
|
2358
|
+
* @returns the open vault.
|
|
2359
|
+
*/
|
|
2360
|
+
static async open(ctx, unit = "") {
|
|
2361
|
+
const domain = await ctx.storageDomain.open(vaultSpec);
|
|
2362
|
+
const vault = new _Vault(ctx, domain, unit);
|
|
2363
|
+
await vault.migrateOnce();
|
|
2364
|
+
return vault;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Bring records written by version 1/2 into the version-3 shape: strip the
|
|
2368
|
+
* `待看` tag, because the flag now says the same thing.
|
|
2369
|
+
*
|
|
2370
|
+
* The old read/unread pair is **not** converted, on purpose. 待看 is the
|
|
2371
|
+
* user's own mark, and back-filling it would have flagged twelve records on
|
|
2372
|
+
* their behalf at first launch.
|
|
2373
|
+
*
|
|
2374
|
+
* (A note for whoever reads this next: you cannot even see the old `status`
|
|
2375
|
+
* from here. The domain validates on read and zod drops unknown keys, so by
|
|
2376
|
+
* the time a record reaches this loop the field is gone — measured, after the
|
|
2377
|
+
* first version of this migration silently converted nothing and the rail
|
|
2378
|
+
* read 「待看 0」.)
|
|
2379
|
+
*/
|
|
2380
|
+
async migrateOnce() {
|
|
2381
|
+
for (const [key, stored] of this.items.entries()) {
|
|
2382
|
+
if (!stored.tags.includes(RETIRED_TAG)) continue;
|
|
2383
|
+
await this.items.put(key, {
|
|
2384
|
+
...stored,
|
|
2385
|
+
tags: stored.tags.filter((tag) => tag !== RETIRED_TAG)
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
get items() {
|
|
2390
|
+
return this.domain.table("items");
|
|
2391
|
+
}
|
|
2392
|
+
get attachments() {
|
|
2393
|
+
return this.domain.table("attachments");
|
|
2394
|
+
}
|
|
2395
|
+
/** Where the key state stands; what the panel shows and the tools consult. */
|
|
2396
|
+
get lockState() {
|
|
2397
|
+
return { configured: this.global.master !== void 0, unlocked: this.key !== void 0 };
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Set (or replace) the master password, and seal everything that needs it.
|
|
2401
|
+
*
|
|
2402
|
+
* Replacing it is allowed on purpose: a user who wrote the password down
|
|
2403
|
+
* badly, or wants a stronger one, must be able to fix that. Records sealed
|
|
2404
|
+
* with the *old* password are re-sealed with the new key, which is why the
|
|
2405
|
+
* old password has to be supplied again in the panel.
|
|
2406
|
+
*
|
|
2407
|
+
* @param password - the new master password, never stored anywhere.
|
|
2408
|
+
* @returns how many records were sealed in the process.
|
|
2409
|
+
*/
|
|
2410
|
+
async setMasterPassword(password) {
|
|
2411
|
+
const sealedAlready = [...this.items.entries()].filter(([, item]) => item.secret !== void 0);
|
|
2412
|
+
if (sealedAlready.length > 0 && this.key === void 0) {
|
|
2413
|
+
throw new Error(
|
|
2414
|
+
`\u4ED3\u5E93\u91CC\u5DF2\u6709 ${String(sealedAlready.length)} \u6761\u5BC6\u6587\uFF0C\u4F46\u73B0\u5728\u662F\u9501\u5B9A\u72B6\u6001\uFF1A\u5148\u7528\u73B0\u6709\u4E3B\u5BC6\u7801\u89E3\u9501\uFF0C\u518D\u66F4\u6362\u4E3B\u5BC6\u7801`
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
const previous = this.key;
|
|
2418
|
+
const salt = newSalt();
|
|
2419
|
+
const kdf = DEFAULT_KDF;
|
|
2420
|
+
const key = deriveKey(password, salt, kdf);
|
|
2421
|
+
await this.setGlobal({
|
|
2422
|
+
...this.global,
|
|
2423
|
+
master: {
|
|
2424
|
+
version: 1,
|
|
2425
|
+
salt: salt.toString("base64"),
|
|
2426
|
+
kdf,
|
|
2427
|
+
verifier: seal(key, VERIFIER_PLAINTEXT)
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
this.key = key;
|
|
2431
|
+
return await this.resealSecrets(previous) + await this.sealLegacySecrets();
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Derive the key from the stored salt and check it against the verifier.
|
|
2435
|
+
*
|
|
2436
|
+
* @param password - what the user typed.
|
|
2437
|
+
* @returns true when the vault is now unlocked.
|
|
2438
|
+
*/
|
|
2439
|
+
async unlock(password) {
|
|
2440
|
+
const master = this.global.master;
|
|
2441
|
+
if (master === void 0) return false;
|
|
2442
|
+
const key = deriveKey(password, Buffer.from(master.salt, "base64"), master.kdf);
|
|
2443
|
+
if (open(key, master.verifier) !== VERIFIER_PLAINTEXT) return false;
|
|
2444
|
+
this.key = key;
|
|
2445
|
+
await this.sealLegacySecrets();
|
|
2446
|
+
return true;
|
|
2447
|
+
}
|
|
2448
|
+
/** Drop the key. Credentials stay on disk, unreadable until the next unlock. */
|
|
2449
|
+
lock() {
|
|
2450
|
+
this.key = void 0;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Seal one credential body for storage.
|
|
2454
|
+
*
|
|
2455
|
+
* @param plaintext - the credential as the user pasted it.
|
|
2456
|
+
* @returns the envelope to store, and the keyed digest used for de-duplication.
|
|
2457
|
+
*/
|
|
2458
|
+
sealSecret(plaintext) {
|
|
2459
|
+
const key = this.key;
|
|
2460
|
+
if (key === void 0) throw new VaultLockedError();
|
|
2461
|
+
return { secret: seal(key, plaintext), secretDigest: this.digestOf(plaintext) };
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* The plaintext behind a credential, when it can be read.
|
|
2465
|
+
*
|
|
2466
|
+
* @param item - the record.
|
|
2467
|
+
* @returns the plaintext, or undefined while locked.
|
|
2468
|
+
*/
|
|
2469
|
+
secretText(item) {
|
|
2470
|
+
if (item.secret === void 0) return void 0;
|
|
2471
|
+
return this.key === void 0 ? void 0 : open(this.key, item.secret);
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* The keyed digest a re-paste is matched against (see `spec.ts`).
|
|
2475
|
+
*
|
|
2476
|
+
* @param plaintext - the credential as pasted.
|
|
2477
|
+
* @returns a hex digest, stable for one vault and useless without its key.
|
|
2478
|
+
*/
|
|
2479
|
+
digestOf(plaintext) {
|
|
2480
|
+
const key = this.key;
|
|
2481
|
+
if (key === void 0) throw new VaultLockedError();
|
|
2482
|
+
return createHmac2("sha256", key).update(plaintext).digest("hex");
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Move credentials that are still sitting in `text` into the sealed field.
|
|
2486
|
+
*
|
|
2487
|
+
* Version-5 vaults kept a credential's body in plain text — that is what this
|
|
2488
|
+
* whole change is about — so the first unlock rewrites them. A record that is
|
|
2489
|
+
* not a `secret` is left alone, even if it looks like one: the category is the
|
|
2490
|
+
* user's own statement about what a record is.
|
|
2491
|
+
*
|
|
2492
|
+
* @returns how many records were sealed.
|
|
2493
|
+
*/
|
|
2494
|
+
async sealLegacySecrets() {
|
|
2495
|
+
let sealed = 0;
|
|
2496
|
+
for (const [id, item] of this.items.entries()) {
|
|
2497
|
+
if (item.category !== "secret" || item.text === void 0) continue;
|
|
2498
|
+
const { secret, secretDigest } = this.sealSecret(item.text);
|
|
2499
|
+
const { text: _dropped, ...rest } = item;
|
|
2500
|
+
await this.items.put(id, { ...rest, secret, secretDigest });
|
|
2501
|
+
sealed += 1;
|
|
2502
|
+
}
|
|
2503
|
+
return sealed;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Re-seal everything that was sealed with the previous key.
|
|
2507
|
+
*
|
|
2508
|
+
* @param previous - the key in use until a moment ago, if there was one.
|
|
2509
|
+
* @returns how many records were re-sealed.
|
|
2510
|
+
*/
|
|
2511
|
+
async resealSecrets(previous) {
|
|
2512
|
+
if (previous === void 0) return 0;
|
|
2513
|
+
let resealed = 0;
|
|
2514
|
+
for (const [id, item] of this.items.entries()) {
|
|
2515
|
+
if (item.secret === void 0) continue;
|
|
2516
|
+
const plaintext = open(previous, item.secret);
|
|
2517
|
+
if (plaintext === void 0) continue;
|
|
2518
|
+
const { secret, secretDigest } = this.sealSecret(plaintext);
|
|
2519
|
+
await this.items.put(id, { ...item, secret, secretDigest });
|
|
2520
|
+
resealed += 1;
|
|
2521
|
+
}
|
|
2522
|
+
return resealed;
|
|
2523
|
+
}
|
|
2524
|
+
/** Total records held, including soft-deleted ones. */
|
|
2525
|
+
get size() {
|
|
2526
|
+
return this.items.size;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* File a new item.
|
|
2530
|
+
*
|
|
2531
|
+
* @param input - the caller-owned fields; id and timestamps are assigned here.
|
|
2532
|
+
* @returns the stored record.
|
|
2533
|
+
*/
|
|
2534
|
+
async create(input) {
|
|
2535
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2536
|
+
const item = {
|
|
2537
|
+
id: randomUUID(),
|
|
2538
|
+
kind: input.kind,
|
|
2539
|
+
category: input.category,
|
|
2540
|
+
categorySource: input.categorySource ?? "rule",
|
|
2541
|
+
source: input.source,
|
|
2542
|
+
createdAt: now,
|
|
2543
|
+
updatedAt: now,
|
|
2544
|
+
tags: [...input.tags ?? []],
|
|
2545
|
+
attachmentIds: [...input.attachmentIds ?? []],
|
|
2546
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
2547
|
+
...input.linkTitle === void 0 ? {} : { linkTitle: input.linkTitle },
|
|
2548
|
+
...input.linkTitleError === void 0 ? {} : { linkTitleError: input.linkTitleError },
|
|
2549
|
+
...input.text === void 0 ? {} : { text: input.text },
|
|
2550
|
+
...input.secret === void 0 ? {} : { secret: input.secret },
|
|
2551
|
+
...input.secretDigest === void 0 ? {} : { secretDigest: input.secretDigest },
|
|
2552
|
+
...input.url === void 0 ? {} : { url: input.url },
|
|
2553
|
+
...input.platform === void 0 ? {} : { platform: input.platform },
|
|
2554
|
+
...input.note === void 0 ? {} : { note: input.note }
|
|
2555
|
+
};
|
|
2556
|
+
await this.items.put(item.id, item);
|
|
2557
|
+
return item;
|
|
2558
|
+
}
|
|
2559
|
+
/** Read one live or soft-deleted record. */
|
|
2560
|
+
get(id) {
|
|
2561
|
+
return this.items.get(id);
|
|
2562
|
+
}
|
|
2563
|
+
/** Records sitting in the recycle bin, newest first. */
|
|
2564
|
+
getBin() {
|
|
2565
|
+
return this.list({ includeDeleted: true }).filter((item) => item.deletedAt !== void 0);
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* List records matching a query, newest first.
|
|
2569
|
+
*
|
|
2570
|
+
* @param query - filters and paging.
|
|
2571
|
+
* @returns matching records.
|
|
2572
|
+
*/
|
|
2573
|
+
list(query = {}) {
|
|
2574
|
+
return selectItems([...this.items.entries()].map(([, item]) => item), query);
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Replace editable fields. The atomic read-modify-write keeps concurrent
|
|
2578
|
+
* edits from interleaving on the domain's write chain.
|
|
2579
|
+
*
|
|
2580
|
+
* @param id - record key.
|
|
2581
|
+
* @param patch - fields to replace.
|
|
2582
|
+
* @returns the stored record.
|
|
2583
|
+
*/
|
|
2584
|
+
async patch(id, patch) {
|
|
2585
|
+
return this.items.update(id, (current) => {
|
|
2586
|
+
const next = { ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2587
|
+
if (patch.category !== void 0) next.category = patch.category;
|
|
2588
|
+
if (patch.categorySource !== void 0) next.categorySource = patch.categorySource;
|
|
2589
|
+
if (patch.watchLater !== void 0) {
|
|
2590
|
+
if (patch.watchLater) next.watchLater = true;
|
|
2591
|
+
else delete next.watchLater;
|
|
2592
|
+
}
|
|
2593
|
+
if (patch.title !== void 0) {
|
|
2594
|
+
if (patch.title.trim().length === 0) delete next.title;
|
|
2595
|
+
else next.title = patch.title;
|
|
2596
|
+
}
|
|
2597
|
+
if (patch.linkTitle !== void 0) next.linkTitle = patch.linkTitle;
|
|
2598
|
+
if (patch.linkTitleError !== void 0) {
|
|
2599
|
+
if (patch.linkTitleError.length === 0) delete next.linkTitleError;
|
|
2600
|
+
else next.linkTitleError = patch.linkTitleError;
|
|
2601
|
+
}
|
|
2602
|
+
if (patch.note !== void 0) next.note = patch.note;
|
|
2603
|
+
if (patch.platform !== void 0) next.platform = patch.platform;
|
|
2604
|
+
if (patch.tags !== void 0) next.tags = [...patch.tags];
|
|
2605
|
+
if (patch.attachmentIds !== void 0) next.attachmentIds = [...patch.attachmentIds];
|
|
2606
|
+
return next;
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
/** Flag or unflag a record for later. */
|
|
2610
|
+
async setWatchLater(id, on = true) {
|
|
2611
|
+
return this.patch(id, { watchLater: on });
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Strip one tag from every record that carries it.
|
|
2615
|
+
*
|
|
2616
|
+
* A tag is not an entity here — it is a word on a record — so "delete this
|
|
2617
|
+
* tag" can only mean "take this word off everything". Returns how many
|
|
2618
|
+
* records changed, so the panel can say so instead of guessing.
|
|
2619
|
+
*
|
|
2620
|
+
* @param tag - the exact tag to remove.
|
|
2621
|
+
* @returns the number of records that carried it.
|
|
2622
|
+
*/
|
|
2623
|
+
async removeTag(tag) {
|
|
2624
|
+
const touched = [...this.items.entries()].filter(([, item]) => item.tags.includes(tag));
|
|
2625
|
+
for (const [key, item] of touched) {
|
|
2626
|
+
await this.items.put(key, { ...item, tags: item.tags.filter((value) => value !== tag) });
|
|
2627
|
+
}
|
|
2628
|
+
return touched.length;
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Soft delete: the record stops appearing in lists but keeps its bytes, so the
|
|
2632
|
+
* recycle bin (and restoring) needs no second table.
|
|
2633
|
+
*/
|
|
2634
|
+
async softDelete(id) {
|
|
2635
|
+
return this.items.update(id, (current) => ({
|
|
2636
|
+
...current,
|
|
2637
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2638
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2639
|
+
}));
|
|
2640
|
+
}
|
|
2641
|
+
/** Undo a soft delete. */
|
|
2642
|
+
async restore(id) {
|
|
2643
|
+
return this.items.update(id, (current) => {
|
|
2644
|
+
const { deletedAt: _dropped, ...rest } = current;
|
|
2645
|
+
return { ...rest, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Delete one record for good, together with its attachment rows.
|
|
2650
|
+
*
|
|
2651
|
+
* The bytes behind an attachment live in dsh's own store, which never deletes
|
|
2652
|
+
* automatically — emptying the recycle bin drops our references, not their
|
|
2653
|
+
* objects. A row another record still references is left alone.
|
|
2654
|
+
*
|
|
2655
|
+
* @param id - record key.
|
|
2656
|
+
* @returns whether the record existed.
|
|
2657
|
+
*/
|
|
2658
|
+
async remove(id) {
|
|
2659
|
+
const item = this.items.get(id);
|
|
2660
|
+
if (item === void 0) return false;
|
|
2661
|
+
const stillReferenced = /* @__PURE__ */ new Set();
|
|
2662
|
+
for (const [, other] of this.items.entries()) {
|
|
2663
|
+
if (other.id === id) continue;
|
|
2664
|
+
for (const attachmentId of other.attachmentIds) stillReferenced.add(attachmentId);
|
|
2665
|
+
}
|
|
2666
|
+
for (const attachmentId of item.attachmentIds) {
|
|
2667
|
+
if (!stillReferenced.has(attachmentId)) await this.attachments.delete(attachmentId);
|
|
2668
|
+
}
|
|
2669
|
+
return this.items.delete(id);
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Record an attachment's metadata. The bytes stay in the store named by
|
|
2673
|
+
* `storeId`; this row is our own index over them, keyed by a generated id
|
|
2674
|
+
* because store ids are not path-safe.
|
|
2675
|
+
*/
|
|
2676
|
+
async addAttachment(input) {
|
|
2677
|
+
const record = {
|
|
2678
|
+
...input,
|
|
2679
|
+
id: randomUUID(),
|
|
2680
|
+
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2681
|
+
};
|
|
2682
|
+
await this.attachments.put(record.id, record);
|
|
2683
|
+
return record;
|
|
2684
|
+
}
|
|
2685
|
+
/** Read one attachment record. */
|
|
2686
|
+
getAttachment(id) {
|
|
2687
|
+
return this.attachments.get(id);
|
|
2688
|
+
}
|
|
2689
|
+
/** Find our index row for a store-side attachment id, if we have one. */
|
|
2690
|
+
findAttachmentByStoreId(storeId) {
|
|
2691
|
+
for (const [, record] of this.attachments.entries()) {
|
|
2692
|
+
if (record.storeId === storeId) return record;
|
|
2693
|
+
}
|
|
2694
|
+
return void 0;
|
|
2695
|
+
}
|
|
2696
|
+
/** Current sync state; M6 writes it. */
|
|
2697
|
+
get global() {
|
|
2698
|
+
return this.domain.global.get();
|
|
2699
|
+
}
|
|
2700
|
+
/** Replace the global slot. */
|
|
2701
|
+
async setGlobal(value) {
|
|
2702
|
+
await this.domain.global.set(vaultGlobalSchema.parse(value));
|
|
2703
|
+
}
|
|
2704
|
+
/** Record today's model-fallback spend. */
|
|
2705
|
+
async setModelSpend(spend, last) {
|
|
2706
|
+
await this.setGlobal({ ...this.global, model: { ...spend, ...last === void 0 ? {} : { last } } });
|
|
2707
|
+
}
|
|
2708
|
+
/** Record where the last WebDAV pull got to. */
|
|
2709
|
+
async setSync(sync) {
|
|
2710
|
+
await this.setGlobal({ ...this.global, sync });
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Write a record exactly as another device had it.
|
|
2714
|
+
*
|
|
2715
|
+
* The merge's only write, and deliberately not `create`: an imported record
|
|
2716
|
+
* keeps the id other devices already know, the `createdAt` it was made with and
|
|
2717
|
+
* the `updatedAt` the conflict was settled on. Re-stamping any of them would
|
|
2718
|
+
* make the next pull decide the other way and bounce the record back and forth.
|
|
2719
|
+
*
|
|
2720
|
+
* @param item - the record, already validated by the merge's own parse.
|
|
2721
|
+
* @returns the stored record.
|
|
2722
|
+
*/
|
|
2723
|
+
async import(item) {
|
|
2724
|
+
const parsed = itemSchema.parse(item);
|
|
2725
|
+
await this.items.put(parsed.id, parsed);
|
|
2726
|
+
return parsed;
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* The same for one attachment's row.
|
|
2730
|
+
*
|
|
2731
|
+
* `storeId` is the *local* store's id for the bytes the merge just admitted;
|
|
2732
|
+
* everything else — our row id in particular — travels with the object so the
|
|
2733
|
+
* record's `attachmentIds` still point at something.
|
|
2734
|
+
*
|
|
2735
|
+
* @param record - the row, with a local `storeId`.
|
|
2736
|
+
* @returns the stored row.
|
|
2737
|
+
*/
|
|
2738
|
+
async importAttachment(record) {
|
|
2739
|
+
const parsed = attachmentSchema.parse(
|
|
2740
|
+
Object.assign({ createdAt: (/* @__PURE__ */ new Date()).toISOString() }, record)
|
|
2741
|
+
);
|
|
2742
|
+
await this.attachments.put(parsed.id, parsed);
|
|
2743
|
+
return parsed;
|
|
2744
|
+
}
|
|
2745
|
+
/** Release the domain handle. Idempotent. */
|
|
2746
|
+
async close() {
|
|
2747
|
+
if (this.closed) return;
|
|
2748
|
+
this.closed = true;
|
|
2749
|
+
await this.domain.close();
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
|
|
2753
|
+
// src/host/rpc.ts
|
|
2754
|
+
var MAX_TEXT_CHARS = 2e5;
|
|
2755
|
+
var imageSchema = z4.object({
|
|
2756
|
+
mediaType: z4.enum(INBOX_IMAGE_TYPES),
|
|
2757
|
+
data: z4.string().min(1),
|
|
2758
|
+
name: z4.string().optional()
|
|
2759
|
+
});
|
|
2760
|
+
var fileSchema = z4.object({
|
|
2761
|
+
data: z4.string(),
|
|
2762
|
+
name: z4.string().optional(),
|
|
2763
|
+
// The browser's declaration, kept only for playable media (see
|
|
2764
|
+
// `fileAttachment`); a schema that promised more would be a promise the host
|
|
2765
|
+
// does not keep.
|
|
2766
|
+
mediaType: z4.string().optional()
|
|
2767
|
+
});
|
|
2768
|
+
var captureRequestSchema = z4.object({
|
|
2769
|
+
text: z4.string().max(MAX_TEXT_CHARS).optional(),
|
|
2770
|
+
images: z4.array(imageSchema).max(MAX_ATTACHMENTS_PER_SUBMISSION).optional(),
|
|
2771
|
+
files: z4.array(fileSchema).max(MAX_ATTACHMENTS_PER_SUBMISSION).optional()
|
|
2772
|
+
});
|
|
2773
|
+
var tagSchema = z4.string().min(1).max(MAX_TAG_CHARS);
|
|
2774
|
+
var listRequestSchema = z4.object({
|
|
2775
|
+
scope: z4.enum(["live", "bin"]).optional(),
|
|
2776
|
+
categories: z4.array(z4.enum(CATEGORIES)).max(CATEGORIES.length).optional(),
|
|
2777
|
+
watchLater: z4.boolean().optional(),
|
|
2778
|
+
kinds: z4.array(z4.enum(KINDS)).max(KINDS.length).optional(),
|
|
2779
|
+
tags: z4.array(tagSchema).max(MAX_TAGS).optional(),
|
|
2780
|
+
text: z4.string().max(MAX_FILTER_CHARS).optional(),
|
|
2781
|
+
limit: z4.number().int().positive().max(LIST_LIMIT).optional(),
|
|
2782
|
+
offset: z4.number().int().nonnegative().optional()
|
|
2783
|
+
});
|
|
2784
|
+
var idRequestSchema = z4.object({ id: z4.string().min(1) });
|
|
2785
|
+
var updateRequestSchema = idRequestSchema.extend({
|
|
2786
|
+
category: z4.enum(CATEGORIES).optional(),
|
|
2787
|
+
watchLater: z4.boolean().optional(),
|
|
2788
|
+
note: z4.string().max(MAX_NOTE_CHARS).optional(),
|
|
2789
|
+
title: z4.string().max(MAX_TITLE_CHARS).optional(),
|
|
2790
|
+
tags: z4.array(tagSchema).max(MAX_TAGS).optional()
|
|
2791
|
+
});
|
|
2792
|
+
function failure(code, message, details = {}) {
|
|
2793
|
+
return { ok: false, error: { code, message, details } };
|
|
2794
|
+
}
|
|
2795
|
+
function reasonOf3(error) {
|
|
2796
|
+
return error instanceof Error ? error.message : String(error);
|
|
2797
|
+
}
|
|
2798
|
+
function digestOf(attachmentId) {
|
|
2799
|
+
const match = /^sha256:([a-f0-9]{64})$/.exec(attachmentId);
|
|
2800
|
+
return match?.[1];
|
|
2801
|
+
}
|
|
2802
|
+
function imageAttachment(ref) {
|
|
2803
|
+
const sha256 = digestOf(ref.attachmentId);
|
|
2804
|
+
return {
|
|
2805
|
+
id: ref.attachmentId,
|
|
2806
|
+
mime: ref.mediaType,
|
|
2807
|
+
bytes: ref.bytes,
|
|
2808
|
+
width: ref.width,
|
|
2809
|
+
height: ref.height,
|
|
2810
|
+
...sha256 === void 0 ? {} : { sha256 },
|
|
2811
|
+
...ref.name === void 0 ? {} : { filename: ref.name }
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
function fileAttachment(ref, declared) {
|
|
2815
|
+
const sha256 = digestOf(ref.attachmentId);
|
|
2816
|
+
const playable = declared !== void 0 && (declared.startsWith("video/") || declared.startsWith("audio/"));
|
|
2817
|
+
return {
|
|
2818
|
+
id: ref.attachmentId,
|
|
2819
|
+
mime: playable ? declared : "application/octet-stream",
|
|
2820
|
+
bytes: ref.bytes,
|
|
2821
|
+
filename: ref.name,
|
|
2822
|
+
...sha256 === void 0 ? {} : { sha256 }
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
function collapse(text) {
|
|
2826
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2827
|
+
}
|
|
2828
|
+
function renderableMime(mime) {
|
|
2829
|
+
return INBOX_IMAGE_TYPES.includes(mime) || mime.startsWith("video/") || mime.startsWith("audio/");
|
|
2830
|
+
}
|
|
2831
|
+
function toSummary(item, attachmentOf2) {
|
|
2832
|
+
const preview = item.text === void 0 ? void 0 : collapse(item.text).slice(0, PREVIEW_CHARS);
|
|
2833
|
+
const previewRecord = item.attachmentIds.map((id) => attachmentOf2?.(id)).find((record) => record !== void 0 && renderableMime(record.mime));
|
|
2834
|
+
const namedRecord = previewRecord?.filename === void 0 ? item.attachmentIds.map((id) => attachmentOf2?.(id)).find((record) => record !== void 0 && record.filename !== void 0) : previewRecord;
|
|
2835
|
+
return {
|
|
2836
|
+
id: item.id,
|
|
2837
|
+
kind: item.kind,
|
|
2838
|
+
category: item.category,
|
|
2839
|
+
...item.categorySource === void 0 ? {} : { categorySource: item.categorySource },
|
|
2840
|
+
watchLater: item.watchLater === true,
|
|
2841
|
+
createdAt: item.createdAt,
|
|
2842
|
+
updatedAt: item.updatedAt,
|
|
2843
|
+
tags: [...item.tags],
|
|
2844
|
+
attachmentCount: item.attachmentIds.length,
|
|
2845
|
+
...previewRecord === void 0 ? {} : { previewId: previewRecord.id, previewMime: previewRecord.mime },
|
|
2846
|
+
...namedRecord?.filename === void 0 ? {} : { attachmentName: namedRecord.filename },
|
|
2847
|
+
...item.title === void 0 ? {} : { title: item.title },
|
|
2848
|
+
...item.linkTitle === void 0 ? {} : { linkTitle: item.linkTitle },
|
|
2849
|
+
...item.linkTitleError === void 0 ? {} : { linkTitleError: item.linkTitleError },
|
|
2850
|
+
// Belt and braces on top of the sealed field: a credential's row never
|
|
2851
|
+
// carries an excerpt of its body, whatever shape the record is in.
|
|
2852
|
+
...item.category === "secret" || preview === void 0 || preview.length === 0 ? {} : { preview },
|
|
2853
|
+
...item.url === void 0 ? {} : { url: item.url },
|
|
2854
|
+
...item.platform === void 0 ? {} : { platform: item.platform },
|
|
2855
|
+
...item.note === void 0 ? {} : { note: item.note },
|
|
2856
|
+
...item.deletedAt === void 0 ? {} : { deletedAt: item.deletedAt }
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
function toAttachmentSummary(record) {
|
|
2860
|
+
return {
|
|
2861
|
+
id: record.id,
|
|
2862
|
+
mime: record.mime,
|
|
2863
|
+
bytes: record.bytes,
|
|
2864
|
+
image: record.mime.startsWith("image/"),
|
|
2865
|
+
...record.filename === void 0 ? {} : { filename: record.filename },
|
|
2866
|
+
...record.width === void 0 ? {} : { width: record.width },
|
|
2867
|
+
...record.height === void 0 ? {} : { height: record.height }
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
function toDetail(vault, item) {
|
|
2871
|
+
const attachments = [];
|
|
2872
|
+
for (const attachmentId of item.attachmentIds) {
|
|
2873
|
+
const record = vault.getAttachment(attachmentId);
|
|
2874
|
+
if (record !== void 0) attachments.push(toAttachmentSummary(record));
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
...toSummary(item, (id) => vault.getAttachment(id)),
|
|
2878
|
+
/*
|
|
2879
|
+
A credential's body leaves the host only when it can be opened at all:
|
|
2880
|
+
while the vault is locked this record arrives with no `text`, and the pane
|
|
2881
|
+
says so instead of showing an empty box. Nothing in the *list* ever carries
|
|
2882
|
+
it — `toSummary` refuses the excerpt too.
|
|
2883
|
+
*/
|
|
2884
|
+
...item.category === "secret" ? (() => {
|
|
2885
|
+
const plaintext = vault.secretText(item);
|
|
2886
|
+
return plaintext === void 0 ? {} : { text: plaintext };
|
|
2887
|
+
})() : item.text === void 0 ? {} : { text: item.text },
|
|
2888
|
+
attachments
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
function byCountThenName(left, right) {
|
|
2892
|
+
if (left.count !== right.count) return right.count - left.count;
|
|
2893
|
+
return left.value.localeCompare(right.value);
|
|
2894
|
+
}
|
|
2895
|
+
function facetsOf(live) {
|
|
2896
|
+
const categories = /* @__PURE__ */ new Map();
|
|
2897
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2898
|
+
for (const item of live) {
|
|
2899
|
+
categories.set(item.category, (categories.get(item.category) ?? 0) + 1);
|
|
2900
|
+
for (const tag of item.tags) tags.set(tag, (tags.get(tag) ?? 0) + 1);
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
categories: [...categories].map(([value, count]) => ({ value, count })).sort(byCountThenName),
|
|
2904
|
+
tags: [...tags].map(([value, count]) => ({ value, count })).sort(byCountThenName)
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
async function handleCapture(vault, attachments, payload, ctx) {
|
|
2908
|
+
if (vault === void 0) {
|
|
2909
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
2910
|
+
}
|
|
2911
|
+
const parsed = captureRequestSchema.safeParse(payload);
|
|
2912
|
+
if (!parsed.success) {
|
|
2913
|
+
return failure("inbox/bad-request", "\u63D0\u4EA4\u7684\u5185\u5BB9\u4E0D\u7B26\u5408\u9884\u671F\u5F62\u72B6", {
|
|
2914
|
+
issues: parsed.error.issues.map((issue) => issue.message)
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
const { text, images = [], files = [] } = parsed.data;
|
|
2918
|
+
let stored;
|
|
2919
|
+
try {
|
|
2920
|
+
stored = (await admitEncodedImages3(attachments, images)).map(imageAttachment);
|
|
2921
|
+
for (const file of files) {
|
|
2922
|
+
stored.push(fileAttachment(await admitEncodedFile3(attachments, file), file.mediaType));
|
|
2923
|
+
}
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
if (isAttachmentError(error)) {
|
|
2926
|
+
return failure("inbox/attachment-refused", error.message, { code: error.code });
|
|
2927
|
+
}
|
|
2928
|
+
return failure("inbox/attachment-failed", reasonOf3(error));
|
|
2929
|
+
}
|
|
2930
|
+
try {
|
|
2931
|
+
const summary = await capture(vault, { text, attachments: stored }, "panel", {
|
|
2932
|
+
ctx
|
|
2933
|
+
});
|
|
2934
|
+
return { ok: true, value: summary };
|
|
2935
|
+
} catch (error) {
|
|
2936
|
+
if (error instanceof VaultLockedError) return failure("inbox/locked", error.message);
|
|
2937
|
+
return failure("inbox/capture-failed", reasonOf3(error));
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
function handleList(vault, payload) {
|
|
2941
|
+
if (vault === void 0) {
|
|
2942
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
2943
|
+
}
|
|
2944
|
+
const parsed = listRequestSchema.safeParse(payload);
|
|
2945
|
+
if (!parsed.success) {
|
|
2946
|
+
return failure("inbox/bad-request", "\u7B5B\u9009\u6761\u4EF6\u4E0D\u7B26\u5408\u9884\u671F\u5F62\u72B6", {
|
|
2947
|
+
issues: parsed.error.issues.map((issue) => issue.message)
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
const { scope = "live", limit = LIST_LIMIT, offset = 0, ...filters } = parsed.data;
|
|
2951
|
+
const all = vault.list({ includeDeleted: true });
|
|
2952
|
+
const live = all.filter((item) => item.deletedAt === void 0);
|
|
2953
|
+
const bin = all.filter((item) => item.deletedAt !== void 0);
|
|
2954
|
+
const matched = vault.list({
|
|
2955
|
+
includeDeleted: true,
|
|
2956
|
+
...filters
|
|
2957
|
+
}).filter((item) => scope === "bin" ? item.deletedAt !== void 0 : item.deletedAt === void 0);
|
|
2958
|
+
const value = {
|
|
2959
|
+
entries: matched.slice(offset, offset + limit).map((item) => toSummary(item, (id) => vault.getAttachment(id))),
|
|
2960
|
+
matched: matched.length,
|
|
2961
|
+
total: live.length,
|
|
2962
|
+
watchLater: live.filter((item) => item.watchLater === true).length,
|
|
2963
|
+
deleted: bin.length,
|
|
2964
|
+
...facetsOf(live)
|
|
2965
|
+
};
|
|
2966
|
+
return { ok: true, value };
|
|
2967
|
+
}
|
|
2968
|
+
function handleDetail(vault, payload) {
|
|
2969
|
+
if (vault === void 0) {
|
|
2970
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
2971
|
+
}
|
|
2972
|
+
const parsed = idRequestSchema.safeParse(payload);
|
|
2973
|
+
if (!parsed.success) return failure("inbox/bad-request", "\u7F3A\u5C11\u8BB0\u5F55 id");
|
|
2974
|
+
const item = vault.get(parsed.data.id);
|
|
2975
|
+
if (item === void 0) return failure("inbox/not-found", "\u8FD9\u6761\u8BB0\u5F55\u4E0D\u5728\u4E86");
|
|
2976
|
+
const value = { entry: toDetail(vault, item) };
|
|
2977
|
+
return { ok: true, value };
|
|
2978
|
+
}
|
|
2979
|
+
async function handleUpdate(vault, payload) {
|
|
2980
|
+
if (vault === void 0) {
|
|
2981
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
2982
|
+
}
|
|
2983
|
+
const parsed = updateRequestSchema.safeParse(payload);
|
|
2984
|
+
if (!parsed.success) {
|
|
2985
|
+
return failure("inbox/bad-request", "\u8981\u6539\u7684\u5185\u5BB9\u4E0D\u7B26\u5408\u9884\u671F\u5F62\u72B6", {
|
|
2986
|
+
issues: parsed.error.issues.map((issue) => issue.message)
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
const { id, ...patch } = parsed.data;
|
|
2990
|
+
const item = vault.get(id);
|
|
2991
|
+
if (item === void 0) return failure("inbox/not-found", "\u8FD9\u6761\u8BB0\u5F55\u4E0D\u5728\u4E86");
|
|
2992
|
+
try {
|
|
2993
|
+
const updated = await vault.patch(id, {
|
|
2994
|
+
...patch,
|
|
2995
|
+
...patch.category === void 0 ? {} : { categorySource: "user" }
|
|
2996
|
+
});
|
|
2997
|
+
const value = { entry: toSummary(updated) };
|
|
2998
|
+
return { ok: true, value };
|
|
2999
|
+
} catch (error) {
|
|
3000
|
+
return failure("inbox/update-failed", reasonOf3(error));
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
async function handleDelete(vault, payload) {
|
|
3004
|
+
if (vault === void 0) {
|
|
3005
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3006
|
+
}
|
|
3007
|
+
const parsed = idRequestSchema.safeParse(payload);
|
|
3008
|
+
if (!parsed.success) return failure("inbox/bad-request", "\u7F3A\u5C11\u8BB0\u5F55 id");
|
|
3009
|
+
if (vault.get(parsed.data.id) === void 0) {
|
|
3010
|
+
return failure("inbox/not-found", "\u8FD9\u6761\u8BB0\u5F55\u4E0D\u5728\u4E86");
|
|
3011
|
+
}
|
|
3012
|
+
try {
|
|
3013
|
+
const value = { entry: toSummary(await vault.softDelete(parsed.data.id)) };
|
|
3014
|
+
return { ok: true, value };
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
return failure("inbox/delete-failed", reasonOf3(error));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
async function handleRestore(vault, payload) {
|
|
3020
|
+
if (vault === void 0) {
|
|
3021
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3022
|
+
}
|
|
3023
|
+
const parsed = idRequestSchema.safeParse(payload);
|
|
3024
|
+
if (!parsed.success) return failure("inbox/bad-request", "\u7F3A\u5C11\u8BB0\u5F55 id");
|
|
3025
|
+
if (vault.get(parsed.data.id) === void 0) {
|
|
3026
|
+
return failure("inbox/not-found", "\u8FD9\u6761\u8BB0\u5F55\u4E0D\u5728\u4E86");
|
|
3027
|
+
}
|
|
3028
|
+
try {
|
|
3029
|
+
const value = { entry: toSummary(await vault.restore(parsed.data.id)) };
|
|
3030
|
+
return { ok: true, value };
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
return failure("inbox/restore-failed", reasonOf3(error));
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
async function handlePurge(ctx, vault) {
|
|
3036
|
+
if (vault === void 0) {
|
|
3037
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3038
|
+
}
|
|
3039
|
+
const bin = vault.getBin();
|
|
3040
|
+
const doomedAttachments = /* @__PURE__ */ new Map();
|
|
3041
|
+
for (const item of bin) {
|
|
3042
|
+
for (const attachmentId of item.attachmentIds) {
|
|
3043
|
+
const record = vault.getAttachment(attachmentId);
|
|
3044
|
+
if (record !== void 0) doomedAttachments.set(attachmentId, record);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
let removed = 0;
|
|
3048
|
+
for (const item of bin) {
|
|
3049
|
+
if (await vault.remove(item.id)) removed += 1;
|
|
3050
|
+
}
|
|
3051
|
+
const stillReferenced = new Set(
|
|
3052
|
+
vault.list({ includeDeleted: true }).flatMap((item) => [...item.attachmentIds])
|
|
3053
|
+
);
|
|
3054
|
+
const orphans = [...doomedAttachments.values()].filter((record) => !stillReferenced.has(record.id));
|
|
3055
|
+
const remote = await removeRemoteRecords(ctx, {
|
|
3056
|
+
itemIds: bin.map((item) => item.id),
|
|
3057
|
+
attachments: orphans
|
|
3058
|
+
});
|
|
3059
|
+
const value = {
|
|
3060
|
+
removed,
|
|
3061
|
+
remoteRemoved: remote.removed,
|
|
3062
|
+
...remote.skipped === true ? { remoteSkipped: true } : {},
|
|
3063
|
+
...remote.failures.length === 0 ? {} : { reason: remote.failures.slice(0, 3).join("\uFF1B") }
|
|
3064
|
+
};
|
|
3065
|
+
return { ok: true, value };
|
|
3066
|
+
}
|
|
3067
|
+
async function handleWebdav(ctx, vault, payload) {
|
|
3068
|
+
const request = payload ?? {};
|
|
3069
|
+
const action = request.action === "save" ? "save" : "read";
|
|
3070
|
+
if (action === "save") {
|
|
3071
|
+
const patch = {};
|
|
3072
|
+
if (typeof request.protocol === "string") patch.protocol = request.protocol;
|
|
3073
|
+
if (typeof request.baseUrl === "string") patch.baseUrl = request.baseUrl;
|
|
3074
|
+
if (typeof request.directory === "string") patch.directory = request.directory;
|
|
3075
|
+
if (typeof request.username === "string") patch.username = request.username;
|
|
3076
|
+
if (typeof request.password === "string") patch.password = request.password;
|
|
3077
|
+
if (typeof request.endpoint === "string") patch.endpoint = request.endpoint;
|
|
3078
|
+
if (typeof request.bucket === "string") patch.bucket = request.bucket;
|
|
3079
|
+
if (typeof request.region === "string") patch.region = request.region;
|
|
3080
|
+
if (typeof request.signatureVersion === "string") {
|
|
3081
|
+
patch.signatureVersion = request.signatureVersion;
|
|
3082
|
+
}
|
|
3083
|
+
if (typeof request.accessKeyId === "string") patch.accessKeyId = request.accessKeyId;
|
|
3084
|
+
if (typeof request.accessKeySecret === "string") {
|
|
3085
|
+
patch.accessKeySecret = request.accessKeySecret;
|
|
3086
|
+
}
|
|
3087
|
+
if (typeof request.userAgent === "string") patch.userAgent = request.userAgent;
|
|
3088
|
+
const saved = await saveWebdav(ctx, vault, readSettings(ctx), patch);
|
|
3089
|
+
if (!saved.ok) return failure("inbox/webdav-unsaved", saved.reason ?? "\u5B58\u4E0D\u8FDB\u53BB");
|
|
3090
|
+
}
|
|
3091
|
+
const status = await describeWebdav(ctx);
|
|
3092
|
+
return { ok: true, value: status };
|
|
3093
|
+
}
|
|
3094
|
+
async function handlePull(ctx, vault, attachments) {
|
|
3095
|
+
if (vault === void 0) {
|
|
3096
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3097
|
+
}
|
|
3098
|
+
const settings = readSettings(ctx);
|
|
3099
|
+
const result = await runPull(ctx, vault, attachments);
|
|
3100
|
+
return { ok: true, value: result };
|
|
3101
|
+
}
|
|
3102
|
+
async function handlePush(ctx, vault, attachments, payload) {
|
|
3103
|
+
if (vault === void 0) {
|
|
3104
|
+
return failure("inbox/vault-closed", "inbox \u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3105
|
+
}
|
|
3106
|
+
const all = payload?.all === true;
|
|
3107
|
+
const result = await pushRemote(ctx, vault, attachments, { all });
|
|
3108
|
+
return { ok: true, value: result };
|
|
3109
|
+
}
|
|
3110
|
+
async function handleProbe(ctx) {
|
|
3111
|
+
const settings = readSettings(ctx);
|
|
3112
|
+
if (settings.protocol === "s3") {
|
|
3113
|
+
if (settings.endpoint.trim().length === 0 || settings.bucket.trim().length === 0) {
|
|
3114
|
+
return failure("inbox/unconfigured", "\u8FD8\u6CA1\u914D\u7F6E S3 \u7684 endpoint \u6216 bucket\uFF0C\u5148\u586B\u4E0A\u518D\u81EA\u68C0");
|
|
3115
|
+
}
|
|
3116
|
+
const secret = await readS3Secret(ctx);
|
|
3117
|
+
if (secret === void 0) return failure("inbox/no-secret", "\u8FD8\u6CA1\u5B58 AccessKey Secret");
|
|
3118
|
+
const rows2 = await probeS3(
|
|
3119
|
+
{
|
|
3120
|
+
endpoint: settings.endpoint,
|
|
3121
|
+
bucket: settings.bucket,
|
|
3122
|
+
region: settings.region,
|
|
3123
|
+
signatureVersion: settings.signatureVersion,
|
|
3124
|
+
userAgent: activeUserAgent(settings)
|
|
3125
|
+
},
|
|
3126
|
+
{ fetch: s3Fetch, accessKeyId: settings.accessKeyId, accessKeySecret: secret },
|
|
3127
|
+
settings.directory.replace(/^\//, "")
|
|
3128
|
+
);
|
|
3129
|
+
return { ok: true, value: rows2 };
|
|
3130
|
+
}
|
|
3131
|
+
if (settings.baseUrl.trim().length === 0) {
|
|
3132
|
+
return failure("inbox/unconfigured", "\u8FD8\u6CA1\u914D\u7F6E\u8FDC\u7AEF\u5730\u5740\uFF0C\u5148\u586B\u4E0A\u518D\u81EA\u68C0");
|
|
3133
|
+
}
|
|
3134
|
+
const password = await readPassword(ctx);
|
|
3135
|
+
const rows = await probeWebdav(
|
|
3136
|
+
{
|
|
3137
|
+
fetch: webdavFetch,
|
|
3138
|
+
...settings.username.length === 0 || password === void 0 ? {} : { auth: { username: settings.username, password } },
|
|
3139
|
+
userAgent: activeUserAgent(settings)
|
|
3140
|
+
},
|
|
3141
|
+
settings.baseUrl,
|
|
3142
|
+
settings.directory
|
|
3143
|
+
);
|
|
3144
|
+
return { ok: true, value: rows };
|
|
3145
|
+
}
|
|
3146
|
+
async function handleSecret(vault, payload) {
|
|
3147
|
+
if (vault === void 0) return failure("inbox/vault-closed", "\u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3148
|
+
const parsed = z4.object({
|
|
3149
|
+
action: z4.enum(["status", "set", "unlock", "lock"]),
|
|
3150
|
+
password: z4.string().min(1).max(1e3).optional()
|
|
3151
|
+
}).safeParse(payload);
|
|
3152
|
+
if (!parsed.success) return failure("inbox/bad-secret-request", "\u8FD9\u4E2A\u8BF7\u6C42\u4E0D\u7B26\u5408\u9884\u671F\u5F62\u72B6");
|
|
3153
|
+
const request = parsed.data;
|
|
3154
|
+
try {
|
|
3155
|
+
switch (request.action) {
|
|
3156
|
+
case "status":
|
|
3157
|
+
return { ok: true, value: { ...vault.lockState } };
|
|
3158
|
+
case "set": {
|
|
3159
|
+
if (request.password === void 0) {
|
|
3160
|
+
return failure("inbox/no-password", "\u8981\u8BBE\u4E3B\u5BC6\u7801\uFF0C\u603B\u5F97\u7ED9\u4E00\u4E2A");
|
|
3161
|
+
}
|
|
3162
|
+
const sealed = await vault.setMasterPassword(request.password);
|
|
3163
|
+
return {
|
|
3164
|
+
ok: true,
|
|
3165
|
+
value: { ...vault.lockState, sealed }
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
case "unlock": {
|
|
3169
|
+
if (request.password === void 0) {
|
|
3170
|
+
return failure("inbox/no-password", "\u8981\u89E3\u9501\uFF0C\u603B\u5F97\u7ED9\u5BC6\u7801");
|
|
3171
|
+
}
|
|
3172
|
+
if (!await vault.unlock(request.password)) {
|
|
3173
|
+
return failure("inbox/wrong-password", "\u5BC6\u7801\u4E0D\u5BF9\u2014\u2014\u89E3\u4E0D\u5F00\u5DF2\u7ECF\u843D\u76D8\u7684\u90A3\u4E9B\u5BC6\u6587");
|
|
3174
|
+
}
|
|
3175
|
+
return { ok: true, value: { ...vault.lockState } };
|
|
3176
|
+
}
|
|
3177
|
+
case "lock":
|
|
3178
|
+
vault.lock();
|
|
3179
|
+
return { ok: true, value: { ...vault.lockState } };
|
|
3180
|
+
}
|
|
3181
|
+
} catch (error) {
|
|
3182
|
+
return failure("inbox/secret-failed", reasonOf3(error));
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
async function handleTags(vault, payload) {
|
|
3186
|
+
if (vault === void 0) return failure("inbox/vault-closed", "\u4ED3\u5E93\u8FD8\u6CA1\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u7A0D\u540E\u518D\u8BD5");
|
|
3187
|
+
const parsed = z4.object({ action: z4.enum(["remove"]).default("remove"), tag: z4.string().min(1).max(MAX_TAG_CHARS) }).safeParse(payload);
|
|
3188
|
+
if (!parsed.success) return failure("inbox/bad-tag-request", parsed.error.message);
|
|
3189
|
+
const removed = await vault.removeTag(parsed.data.tag);
|
|
3190
|
+
return { ok: true, value: { removed } };
|
|
3191
|
+
}
|
|
3192
|
+
async function handleUi(ctx, payload) {
|
|
3193
|
+
const parsed = z4.object({
|
|
3194
|
+
action: z4.enum(["read", "save"]).default("read"),
|
|
3195
|
+
listMode: z4.enum(["grid", "compact"]).optional()
|
|
3196
|
+
}).safeParse(payload);
|
|
3197
|
+
if (!parsed.success) return failure("inbox/bad-ui-request", parsed.error.message);
|
|
3198
|
+
if (parsed.data.action === "save" && parsed.data.listMode !== void 0) {
|
|
3199
|
+
const saved = saveUiPrefs(ctx, { listMode: parsed.data.listMode });
|
|
3200
|
+
if (!saved.ok) return failure("inbox/ui-unsaved", saved.reason ?? "\u5B58\u4E0D\u8FDB\u53BB");
|
|
3201
|
+
}
|
|
3202
|
+
return { ok: true, value: readUiPrefs(ctx) };
|
|
3203
|
+
}
|
|
3204
|
+
function imageRef2(record) {
|
|
3205
|
+
return {
|
|
3206
|
+
attachmentId: record.storeId,
|
|
3207
|
+
mediaType: record.mime,
|
|
3208
|
+
bytes: record.bytes,
|
|
3209
|
+
width: record.width ?? 0,
|
|
3210
|
+
height: record.height ?? 0,
|
|
3211
|
+
...record.filename === void 0 ? {} : { name: record.filename }
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
function fileRef2(record) {
|
|
3215
|
+
return {
|
|
3216
|
+
attachmentId: record.storeId,
|
|
3217
|
+
name: record.filename ?? "file",
|
|
3218
|
+
bytes: record.bytes
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
async function readSlice(path, start, length) {
|
|
3222
|
+
const handle = await open2(path, "r");
|
|
3223
|
+
try {
|
|
3224
|
+
const buffer = Buffer.alloc(length);
|
|
3225
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
3226
|
+
return buffer.subarray(0, bytesRead);
|
|
3227
|
+
} finally {
|
|
3228
|
+
await handle.close();
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
function parseByteRange(header, total) {
|
|
3232
|
+
const match = header === null ? null : /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
3233
|
+
if (match === null || total === 0) return void 0;
|
|
3234
|
+
const rawStart = match[1] ?? "";
|
|
3235
|
+
const rawEnd = match[2] ?? "";
|
|
3236
|
+
if (rawStart === "" && rawEnd === "") return void 0;
|
|
3237
|
+
const start = rawStart === "" ? Math.max(0, total - Number(rawEnd)) : Number(rawStart);
|
|
3238
|
+
const end = rawStart === "" || rawEnd === "" ? total - 1 : Math.min(Number(rawEnd), total - 1);
|
|
3239
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start > end || start >= total) {
|
|
3240
|
+
return void 0;
|
|
3241
|
+
}
|
|
3242
|
+
return { start, end };
|
|
3243
|
+
}
|
|
3244
|
+
async function handleAttachment(vault, attachments, request) {
|
|
3245
|
+
const id = new URL(request.url).searchParams.get("id") ?? "";
|
|
3246
|
+
const record = vault?.getAttachment(id);
|
|
3247
|
+
if (record === void 0) {
|
|
3248
|
+
return Response.json(failure("inbox/attachment-missing", "\u9644\u4EF6\u4E0D\u5B58\u5728"), { status: 404 });
|
|
3249
|
+
}
|
|
3250
|
+
if (!renderableMime(record.mime)) {
|
|
3251
|
+
return Response.json(
|
|
3252
|
+
failure("inbox/attachment-not-renderable", "\u53EA\u6709\u56FE\u7247\u3001\u89C6\u9891\u548C\u97F3\u9891\u80FD\u5728\u9762\u677F\u91CC\u9884\u89C8"),
|
|
3253
|
+
{ status: 415 }
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
const image = INBOX_IMAGE_TYPES.includes(record.mime);
|
|
3257
|
+
const path = image ? attachments.imageHostPath(imageRef2(record)) : attachments.fileHostPath(fileRef2(record));
|
|
3258
|
+
if (path === void 0) {
|
|
3259
|
+
return Response.json(
|
|
3260
|
+
failure("inbox/attachment-remote", "\u8FD9\u4E2A\u9644\u4EF6\u4E0D\u5728\u672C\u673A\uFF0C\u9762\u677F\u6CA1\u6CD5\u76F4\u63A5\u8BFB\u5B83"),
|
|
3261
|
+
{ status: 404 }
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
try {
|
|
3265
|
+
const headers = {
|
|
3266
|
+
"content-type": record.mime,
|
|
3267
|
+
"cache-control": "private, max-age=86400",
|
|
3268
|
+
"accept-ranges": "bytes",
|
|
3269
|
+
"x-content-type-options": "nosniff"
|
|
3270
|
+
};
|
|
3271
|
+
const range = parseByteRange(request.headers.get("range"), record.bytes);
|
|
3272
|
+
if (range === void 0) {
|
|
3273
|
+
return new Response(await readFile2(path), { headers });
|
|
3274
|
+
}
|
|
3275
|
+
const length = range.end - range.start + 1;
|
|
3276
|
+
return new Response(new Uint8Array(await readSlice(path, range.start, length)), {
|
|
3277
|
+
status: 206,
|
|
3278
|
+
headers: {
|
|
3279
|
+
...headers,
|
|
3280
|
+
"content-length": String(length),
|
|
3281
|
+
"content-range": `bytes ${String(range.start)}-${String(range.end)}/${String(record.bytes)}`
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
} catch (error) {
|
|
3285
|
+
return Response.json(failure("inbox/attachment-unreadable", reasonOf3(error)), { status: 500 });
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function endpoint(path, run) {
|
|
3289
|
+
return {
|
|
3290
|
+
path,
|
|
3291
|
+
methods: ["POST"],
|
|
3292
|
+
requestBody: "buffered",
|
|
3293
|
+
async fetch(request) {
|
|
3294
|
+
let payload;
|
|
3295
|
+
try {
|
|
3296
|
+
payload = await request.json();
|
|
3297
|
+
} catch {
|
|
3298
|
+
return Response.json(failure("inbox/bad-json", "\u8BF7\u6C42\u4F53\u4E0D\u662F JSON"), { status: 400 });
|
|
3299
|
+
}
|
|
3300
|
+
try {
|
|
3301
|
+
return Response.json(await run(payload));
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
return Response.json(failure("inbox/handler-threw", reasonOf3(error)));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
function registerInboxRpc(ctx, vault) {
|
|
3309
|
+
ctx.inject(["connection", "attachments"], (scoped) => {
|
|
3310
|
+
const attachments = scoped.attachments;
|
|
3311
|
+
let queue = Promise.resolve();
|
|
3312
|
+
const serialise = (task) => {
|
|
3313
|
+
const next = queue.then(task, task);
|
|
3314
|
+
queue = next.then(
|
|
3315
|
+
() => void 0,
|
|
3316
|
+
() => void 0
|
|
3317
|
+
);
|
|
3318
|
+
return next;
|
|
3319
|
+
};
|
|
3320
|
+
const autoPush = makeAutoPush(scoped, vault, () => attachments);
|
|
3321
|
+
const afterChange = () => scheduleAutoPush(autoPush);
|
|
3322
|
+
const routes = [
|
|
3323
|
+
endpoint(
|
|
3324
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_CAPTURE}`,
|
|
3325
|
+
(payload) => serialise(async () => {
|
|
3326
|
+
const answer = await handleCapture(vault(), attachments, payload, scoped);
|
|
3327
|
+
if (answer.ok) afterChange();
|
|
3328
|
+
return answer;
|
|
3329
|
+
})
|
|
3330
|
+
),
|
|
3331
|
+
endpoint(
|
|
3332
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_LIST}`,
|
|
3333
|
+
(payload) => Promise.resolve(handleList(vault(), payload))
|
|
3334
|
+
),
|
|
3335
|
+
endpoint(
|
|
3336
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_DETAIL}`,
|
|
3337
|
+
(payload) => Promise.resolve(handleDetail(vault(), payload))
|
|
3338
|
+
),
|
|
3339
|
+
endpoint(
|
|
3340
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_UPDATE}`,
|
|
3341
|
+
(payload) => serialise(async () => {
|
|
3342
|
+
const answer = await handleUpdate(vault(), payload);
|
|
3343
|
+
if (answer.ok) afterChange();
|
|
3344
|
+
return answer;
|
|
3345
|
+
})
|
|
3346
|
+
),
|
|
3347
|
+
endpoint(
|
|
3348
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_DELETE}`,
|
|
3349
|
+
(payload) => serialise(async () => {
|
|
3350
|
+
const answer = await handleDelete(vault(), payload);
|
|
3351
|
+
if (answer.ok) afterChange();
|
|
3352
|
+
return answer;
|
|
3353
|
+
})
|
|
3354
|
+
),
|
|
3355
|
+
endpoint(
|
|
3356
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_RESTORE}`,
|
|
3357
|
+
(payload) => serialise(async () => {
|
|
3358
|
+
const answer = await handleRestore(vault(), payload);
|
|
3359
|
+
if (answer.ok) afterChange();
|
|
3360
|
+
return answer;
|
|
3361
|
+
})
|
|
3362
|
+
),
|
|
3363
|
+
endpoint(
|
|
3364
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_PURGE}`,
|
|
3365
|
+
() => serialise(() => handlePurge(scoped, vault()))
|
|
3366
|
+
),
|
|
3367
|
+
endpoint(
|
|
3368
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_WEBDAV}`,
|
|
3369
|
+
(payload) => serialise(() => handleWebdav(scoped, vault(), payload))
|
|
3370
|
+
),
|
|
3371
|
+
endpoint(
|
|
3372
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_PULL}`,
|
|
3373
|
+
() => serialise(() => handlePull(scoped, vault(), attachments))
|
|
3374
|
+
),
|
|
3375
|
+
endpoint(
|
|
3376
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_PUSH}`,
|
|
3377
|
+
(payload) => serialise(() => handlePush(scoped, vault(), attachments, payload))
|
|
3378
|
+
),
|
|
3379
|
+
endpoint(
|
|
3380
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_PROBE}`,
|
|
3381
|
+
() => Promise.resolve(handleProbe(scoped))
|
|
3382
|
+
),
|
|
3383
|
+
endpoint(
|
|
3384
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_UI}`,
|
|
3385
|
+
(payload) => serialise(() => handleUi(scoped, payload))
|
|
3386
|
+
),
|
|
3387
|
+
endpoint(
|
|
3388
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_TAGS}`,
|
|
3389
|
+
(payload) => serialise(() => handleTags(vault(), payload))
|
|
3390
|
+
),
|
|
3391
|
+
endpoint(
|
|
3392
|
+
`${INBOX_API_PREFIX}/${INBOX_ENDPOINT_SECRET}`,
|
|
3393
|
+
(payload) => serialise(() => handleSecret(vault(), payload))
|
|
3394
|
+
),
|
|
3395
|
+
{
|
|
3396
|
+
path: `${INBOX_API_PREFIX}/${INBOX_ENDPOINT_ATTACHMENT}`,
|
|
3397
|
+
methods: ["GET"],
|
|
3398
|
+
requestBody: "buffered",
|
|
3399
|
+
fetch: (request) => handleAttachment(vault(), attachments, request)
|
|
3400
|
+
}
|
|
3401
|
+
];
|
|
3402
|
+
for (const route of routes) {
|
|
3403
|
+
scoped.effect(
|
|
3404
|
+
() => scoped.connection.fetch.register(route),
|
|
3405
|
+
`dsh-inbox: ${route.path}`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
// src/host/tools.ts
|
|
3412
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3413
|
+
var SEARCH_PAGE = 10;
|
|
3414
|
+
var TEXT_BUDGET = 1e3;
|
|
3415
|
+
function attachmentMarker(attachmentId) {
|
|
3416
|
+
return `[attachment:${attachmentId}]`;
|
|
3417
|
+
}
|
|
3418
|
+
function pictureIn(vault, item) {
|
|
3419
|
+
for (const id of item.attachmentIds) {
|
|
3420
|
+
const record = vault.getAttachment(id);
|
|
3421
|
+
if (record !== void 0 && INBOX_IMAGE_TYPES.includes(record.mime)) {
|
|
3422
|
+
return id;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return void 0;
|
|
3426
|
+
}
|
|
3427
|
+
function imageRefOf(record) {
|
|
3428
|
+
return {
|
|
3429
|
+
attachmentId: record.storeId,
|
|
3430
|
+
mediaType: record.mime,
|
|
3431
|
+
bytes: record.bytes,
|
|
3432
|
+
width: record.width ?? 0,
|
|
3433
|
+
height: record.height ?? 0,
|
|
3434
|
+
...record.filename === void 0 ? {} : { name: record.filename }
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function when(item) {
|
|
3438
|
+
return new Date(item.createdAt).toLocaleString();
|
|
3439
|
+
}
|
|
3440
|
+
function labelOf(item) {
|
|
3441
|
+
return `[${KIND_LABELS[item.kind]} \xB7 ${CATEGORY_LABELS[item.category]}${item.platform === void 0 ? "" : ` \xB7 ${item.platform}`}]`;
|
|
3442
|
+
}
|
|
3443
|
+
function headline(item) {
|
|
3444
|
+
if (item.title !== void 0) return item.title;
|
|
3445
|
+
if (item.linkTitle !== void 0) return item.linkTitle;
|
|
3446
|
+
if (item.url !== void 0) return item.url;
|
|
3447
|
+
if (item.note !== void 0 && item.note.length > 0) return item.note;
|
|
3448
|
+
if (item.category !== "secret" && item.text !== void 0) {
|
|
3449
|
+
const flat = item.text.replace(/\s+/g, " ").trim();
|
|
3450
|
+
return flat.length > PREVIEW_CHARS ? `${flat.slice(0, PREVIEW_CHARS)}\u2026` : flat;
|
|
3451
|
+
}
|
|
3452
|
+
if (item.category === "secret") return "\uFF08\u5BC6\u94A5\u7C7B\u8BB0\u5F55\uFF0C\u660E\u6587\u4E0D\u5916\u4F20\uFF09";
|
|
3453
|
+
return item.attachmentIds.length > 0 ? `\uFF08${String(item.attachmentIds.length)} \u4E2A\u9644\u4EF6\uFF09` : "\uFF08\u65E0\u6807\u9898\uFF09";
|
|
3454
|
+
}
|
|
3455
|
+
function formatSearch(entries, matched, pictureOf) {
|
|
3456
|
+
if (matched === 0) {
|
|
3457
|
+
return "\u4ED3\u5E93\u91CC\u6CA1\u6709\u5339\u914D\u7684\u8BB0\u5F55\u3002\u53EF\u4EE5\u6362\u4E2A\u8BF4\u6CD5\uFF0C\u6216\u8005\u8BA9\u7528\u6237\u5728 dsh-inbox \u9762\u677F\u91CC\u7FFB\u4E00\u7FFB\u3002";
|
|
3458
|
+
}
|
|
3459
|
+
const lines = entries.map((item, index) => {
|
|
3460
|
+
const picture = pictureOf?.(item);
|
|
3461
|
+
const parts = [
|
|
3462
|
+
`${String(index + 1)}. ${labelOf(item)} ${headline(item)}` + (picture === void 0 ? "" : ` ${attachmentMarker(picture)}`)
|
|
3463
|
+
];
|
|
3464
|
+
const named = item.title ?? item.linkTitle;
|
|
3465
|
+
if (item.note !== void 0 && item.note.length > 0 && named !== void 0) {
|
|
3466
|
+
parts.push(` \u5907\u6CE8\uFF1A${item.note}`);
|
|
3467
|
+
}
|
|
3468
|
+
if (item.url !== void 0 && named !== void 0) parts.push(` ${item.url}`);
|
|
3469
|
+
if (item.tags.length > 0) parts.push(` \u6807\u7B7E\uFF1A${item.tags.map((tag) => `#${tag}`).join(" ")}`);
|
|
3470
|
+
parts.push(` \u5B58\u5165\uFF1A${when(item)} \xB7 id: ${item.id}`);
|
|
3471
|
+
return parts.join("\n");
|
|
3472
|
+
});
|
|
3473
|
+
const header = matched > entries.length ? `\u5339\u914D ${String(matched)} \u6761\uFF0C\u5148\u770B\u6700\u8FD1 ${String(entries.length)} \u6761\uFF1A` : `\u5339\u914D ${String(matched)} \u6761\uFF1A`;
|
|
3474
|
+
const tail = matched > entries.length ? `
|
|
3475
|
+
\u8FD8\u6709 ${String(matched - entries.length)} \u6761\u6CA1\u5217\u51FA\u6765\u2014\u2014\u9700\u8981\u7684\u8BDD\u518D\u7F29\u5C0F\u6761\u4EF6\uFF0C\u6216\u8005\u8BA9\u7528\u6237\u5230\u9762\u677F\u91CC\u770B\u5168\u90E8\u3002` : "";
|
|
3476
|
+
return `${header}
|
|
3477
|
+
|
|
3478
|
+
${lines.join("\n\n")}${tail}`;
|
|
3479
|
+
}
|
|
3480
|
+
function describeAttachment(attachment) {
|
|
3481
|
+
const size = attachment.bytes < 1024 ? `${String(attachment.bytes)} B` : `${(attachment.bytes / 1024).toFixed(1)} KB`;
|
|
3482
|
+
const dimensions = attachment.width === void 0 || attachment.height === void 0 ? "" : ` \xB7 ${String(attachment.width)}\xD7${String(attachment.height)}`;
|
|
3483
|
+
return `${attachment.filename ?? attachment.mime} \xB7 ${attachment.mime}${dimensions} \xB7 ${size}`;
|
|
3484
|
+
}
|
|
3485
|
+
function formatDetail(vault, item) {
|
|
3486
|
+
const header = `\u3010${labelOf(item).slice(1, -1)}\u3011${headline(item)}
|
|
3487
|
+
\u5B58\u5165\uFF1A${when(item)} \xB7 id: ${item.id}`;
|
|
3488
|
+
if (item.category === "secret") {
|
|
3489
|
+
return `${header}
|
|
3490
|
+
|
|
3491
|
+
\u8FD9\u6761\u8BB0\u5F55\u88AB\u5F52\u7C7B\u4E3A\u5BC6\u94A5/\u8D26\u5BC6\uFF1A**\u660E\u6587\u4E0D\u4F1A\u901A\u8FC7\u5BF9\u8BDD\u8F93\u51FA**\u3002\u8BF7\u8BA9\u7528\u6237\u5230 dsh-inbox \u9762\u677F\u91CC\u67E5\u770B\u4E0E\u590D\u5236\u2014\u2014\u8FD9\u4E5F\u662F\u8FD9\u6761\u8BB0\u5F55\u80FD\u88AB\u5B89\u5168\u68C0\u7D22\u7684\u539F\u56E0\u3002`;
|
|
3492
|
+
}
|
|
3493
|
+
const blocks = [header];
|
|
3494
|
+
if (item.url !== void 0) blocks.push(item.url);
|
|
3495
|
+
if (item.note !== void 0 && item.note.length > 0) blocks.push(`\u5907\u6CE8\uFF08\u7528\u6237\u5199\u7684\uFF09\uFF1A${item.note}`);
|
|
3496
|
+
if (item.tags.length > 0) blocks.push(`\u6807\u7B7E\uFF1A${item.tags.map((tag) => `#${tag}`).join(" ")}`);
|
|
3497
|
+
if (item.text !== void 0 && item.text.length > 0) {
|
|
3498
|
+
if (item.text.length <= TEXT_BUDGET) {
|
|
3499
|
+
blocks.push(item.text);
|
|
3500
|
+
} else {
|
|
3501
|
+
blocks.push(
|
|
3502
|
+
`${item.text.slice(0, TEXT_BUDGET)}
|
|
3503
|
+
|
|
3504
|
+
\uFF08\u5171 ${String(item.text.length)} \u5B57\uFF0C\u8FD9\u91CC\u662F\u524D ${String(TEXT_BUDGET)} \u5B57\uFF1B\u5269\u4F59\u90E8\u5206\u5728\u9762\u677F\u91CC\u770B\uFF09`
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
const attachments = [];
|
|
3509
|
+
for (const attachmentId of item.attachmentIds) {
|
|
3510
|
+
const record = vault.getAttachment(attachmentId);
|
|
3511
|
+
if (record !== void 0) attachments.push(record);
|
|
3512
|
+
}
|
|
3513
|
+
if (attachments.length > 0) {
|
|
3514
|
+
const lines = attachments.map((attachment) => `- ${describeAttachment(attachment)}`);
|
|
3515
|
+
if (item.category === "document") {
|
|
3516
|
+
lines.push(
|
|
3517
|
+
"\uFF08\u8BC1\u4EF6\u7C7B\uFF1A\u56FE\u7247\u7684\u5B57\u8282\u4E0D\u4F1A\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u4E5F\u4E0D\u4F1A\u53D1\u7ED9\u6A21\u578B\uFF1B\u5361\u7247\u91CC\u7684\u7F29\u7565\u56FE\u662F\u5728\u672C\u673A\u6E32\u67D3\u7684\u3002\uFF09"
|
|
3518
|
+
);
|
|
3519
|
+
}
|
|
3520
|
+
for (const attachment of attachments) lines.push(attachmentMarker(attachment.id));
|
|
3521
|
+
blocks.push(`\u9644\u4EF6\uFF1A
|
|
3522
|
+
${lines.join("\n")}`);
|
|
3523
|
+
}
|
|
3524
|
+
return blocks.join("\n\n");
|
|
3525
|
+
}
|
|
3526
|
+
var CATEGORY_VALUES = ["idea", "article", "media", "image", "document", "secret", "other"];
|
|
3527
|
+
var KIND_VALUES = ["text", "link", "image", "file"];
|
|
3528
|
+
function narrow(values, raw) {
|
|
3529
|
+
if (raw === void 0) return void 0;
|
|
3530
|
+
const match = values.find((value) => value === raw.trim().toLowerCase());
|
|
3531
|
+
return match;
|
|
3532
|
+
}
|
|
3533
|
+
function registerInboxTools(ctx, vault) {
|
|
3534
|
+
ctx.tools.register(
|
|
3535
|
+
defineTool({
|
|
3536
|
+
name: "inbox_search",
|
|
3537
|
+
description: "Search the user's personal dsh-inbox \u2014 their \u6536\u4EF6\u7BB1, which they also call \u4ED3\u5E93 / \u4E2A\u4EBA\u4ED3\u5E93 / inbox. It is the local store where they paste links, text, images and credentials. Use it whenever they ask what they saved, ask for something from \u6536\u4EF6\u7BB1 / \u4ED3\u5E93 / inbox, want a link they stored earlier, or want the ones they flagged \u5F85\u770B. Returns at most ten matches with their ids, newest first; records classified as secrets are listed but their text is never returned.",
|
|
3538
|
+
parameters: {
|
|
3539
|
+
text: { type: "string", description: "Words to look for in title, text, url, note or tags." },
|
|
3540
|
+
category: {
|
|
3541
|
+
type: "string",
|
|
3542
|
+
description: `One of: ${CATEGORY_VALUES.join(", ")}.`
|
|
3543
|
+
},
|
|
3544
|
+
watchLater: { type: "boolean", description: "true for only the records the user flagged \u5F85\u770B." },
|
|
3545
|
+
kind: { type: "string", description: "text, link, image or file." },
|
|
3546
|
+
tag: { type: "string", description: "Only records carrying this exact tag." }
|
|
3547
|
+
},
|
|
3548
|
+
output: {
|
|
3549
|
+
schema: { type: "string" },
|
|
3550
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
3551
|
+
},
|
|
3552
|
+
async execute(args) {
|
|
3553
|
+
const open3 = vault();
|
|
3554
|
+
if (open3 === void 0) return "\u4ED3\u5E93\u6CA1\u6709\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u6682\u65F6\u67E5\u4E0D\u4E86\u3002";
|
|
3555
|
+
const category = narrow(CATEGORY_VALUES, args.category);
|
|
3556
|
+
const kind = narrow(KIND_VALUES, args.kind);
|
|
3557
|
+
const matched = open3.list({
|
|
3558
|
+
...args.text === void 0 ? {} : { text: args.text.slice(0, MAX_FILTER_CHARS) },
|
|
3559
|
+
...category === void 0 ? {} : { categories: [category] },
|
|
3560
|
+
...args.watchLater === void 0 ? {} : { watchLater: args.watchLater },
|
|
3561
|
+
...kind === void 0 ? {} : { kinds: [kind] },
|
|
3562
|
+
...args.tag === void 0 ? {} : { tags: [args.tag] }
|
|
3563
|
+
});
|
|
3564
|
+
return formatSearch(
|
|
3565
|
+
matched.slice(0, SEARCH_PAGE),
|
|
3566
|
+
matched.length,
|
|
3567
|
+
(item) => pictureIn(open3, item)
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
})
|
|
3571
|
+
);
|
|
3572
|
+
ctx.tools.register(
|
|
3573
|
+
defineTool({
|
|
3574
|
+
name: "inbox_get",
|
|
3575
|
+
description: "Open one record from the user's dsh-inbox (their \u6536\u4EF6\u7BB1 / \u4ED3\u5E93 / inbox) by the id a search returned: its text (truncated to 1000 characters), link, note, tags and attachment facts. Credentials are never returned in clear text. An image is described by an attachment marker the UI renders locally; set withImage only when the user asks you to look at the picture itself (that sends its bytes to you, once, for this call).",
|
|
3576
|
+
parameters: {
|
|
3577
|
+
id: { type: "string", required: true, description: "The record id from inbox_search." },
|
|
3578
|
+
withImage: {
|
|
3579
|
+
type: "boolean",
|
|
3580
|
+
description: "Send the record\u2019s image to you so you can look at it (costs tokens and puts the picture in this conversation). Use it only when the user asks you to see or verify the image; leave it out otherwise."
|
|
3581
|
+
}
|
|
3582
|
+
},
|
|
3583
|
+
output: {
|
|
3584
|
+
schema: { type: "string" },
|
|
3585
|
+
render: (args, value) => {
|
|
3586
|
+
const parts = [{ type: "text", text: value }];
|
|
3587
|
+
if (args.withImage !== true) return parts;
|
|
3588
|
+
const open3 = vault();
|
|
3589
|
+
const item = open3?.get(args.id.trim());
|
|
3590
|
+
if (open3 === void 0 || item === void 0) return parts;
|
|
3591
|
+
const id = pictureIn(open3, item);
|
|
3592
|
+
const record = id === void 0 ? void 0 : open3.getAttachment(id);
|
|
3593
|
+
if (record !== void 0) parts.push({ type: "image", attachment: imageRefOf(record) });
|
|
3594
|
+
return parts;
|
|
3595
|
+
}
|
|
3596
|
+
},
|
|
3597
|
+
async execute(args) {
|
|
3598
|
+
const open3 = vault();
|
|
3599
|
+
if (open3 === void 0) return "\u4ED3\u5E93\u6CA1\u6709\u6253\u5F00\uFF08\u6216\u6253\u5F00\u5931\u8D25\uFF09\uFF0C\u6682\u65F6\u67E5\u4E0D\u4E86\u3002";
|
|
3600
|
+
const item = open3.get(args.id.trim());
|
|
3601
|
+
if (item === void 0) return `\u6CA1\u627E\u5230 id \u4E3A ${args.id} \u7684\u8BB0\u5F55\uFF1B\u5B83\u53EF\u80FD\u5DF2\u7ECF\u88AB\u5220\u6389\u4E86\u3002`;
|
|
3602
|
+
return formatDetail(open3, item);
|
|
3603
|
+
}
|
|
3604
|
+
})
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/host/vault/lease.ts
|
|
3609
|
+
var ENTRY = "dsh_inbox";
|
|
3610
|
+
function registryOf() {
|
|
3611
|
+
const scope = globalThis;
|
|
3612
|
+
return scope.__dshInboxVaults ??= /* @__PURE__ */ new Map();
|
|
3613
|
+
}
|
|
3614
|
+
function leaseVault(ctx, onOpened) {
|
|
3615
|
+
const store = registryOf();
|
|
3616
|
+
const live = store.get(ENTRY);
|
|
3617
|
+
if (live !== void 0) {
|
|
3618
|
+
live.owners += 1;
|
|
3619
|
+
return leaseOf(live, store);
|
|
3620
|
+
}
|
|
3621
|
+
const entry = { owners: 1, opening: Promise.resolve() };
|
|
3622
|
+
store.set(ENTRY, entry);
|
|
3623
|
+
entry.opening = Vault.open(ctx).then(
|
|
3624
|
+
(vault) => {
|
|
3625
|
+
entry.vault = vault;
|
|
3626
|
+
if (entry.owners === 0) {
|
|
3627
|
+
store.delete(ENTRY);
|
|
3628
|
+
void vault.close();
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
onOpened?.(vault);
|
|
3632
|
+
},
|
|
3633
|
+
(error) => {
|
|
3634
|
+
if (store.get(ENTRY) === entry) store.delete(ENTRY);
|
|
3635
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
3636
|
+
}
|
|
3637
|
+
);
|
|
3638
|
+
return leaseOf(entry, store);
|
|
3639
|
+
}
|
|
3640
|
+
function leaseOf(entry, store) {
|
|
3641
|
+
return {
|
|
3642
|
+
current: () => entry.vault,
|
|
3643
|
+
failure: () => entry.error,
|
|
3644
|
+
release: async () => {
|
|
3645
|
+
entry.owners -= 1;
|
|
3646
|
+
if (entry.owners > 0) return;
|
|
3647
|
+
await entry.opening;
|
|
3648
|
+
const vault = entry.vault;
|
|
3649
|
+
entry.vault = void 0;
|
|
3650
|
+
if (store.get(ENTRY) === entry) store.delete(ENTRY);
|
|
3651
|
+
await vault?.close();
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// src/host/index.ts
|
|
3657
|
+
var name = "dsh-inbox";
|
|
3658
|
+
var inject = ["tools", "commands", "storageDomain"];
|
|
3659
|
+
function apply(ctx) {
|
|
3660
|
+
ctx.inject(["settings"], (withSettings) => {
|
|
3661
|
+
installWebdavSettings(withSettings);
|
|
3662
|
+
installUiSettings(withSettings);
|
|
3663
|
+
});
|
|
3664
|
+
const lease = leaseVault(ctx, (opened) => {
|
|
3665
|
+
ctx.inject(["attachments"], (scoped) => {
|
|
3666
|
+
void runPull(scoped, opened, scoped.attachments).catch(() => void 0);
|
|
3667
|
+
});
|
|
3668
|
+
});
|
|
3669
|
+
ctx.effect(() => () => lease.release(), "dsh-inbox: vault");
|
|
3670
|
+
const openVault = () => lease.current();
|
|
3671
|
+
registerInboxCommand(ctx, openVault);
|
|
3672
|
+
registerInboxRpc(ctx, openVault);
|
|
3673
|
+
registerInboxTools(ctx, openVault);
|
|
3674
|
+
ctx.tools.register(
|
|
3675
|
+
defineTool2({
|
|
3676
|
+
name: "inbox_status",
|
|
3677
|
+
description: "Report whether the dsh-inbox (the user's \u6536\u4EF6\u7BB1 / \u4ED3\u5E93 / inbox) is loaded, how many records it holds and whether it opened cleanly. Takes no arguments.",
|
|
3678
|
+
parameters: {},
|
|
3679
|
+
output: {
|
|
3680
|
+
schema: {
|
|
3681
|
+
type: "object",
|
|
3682
|
+
additionalProperties: false,
|
|
3683
|
+
properties: {
|
|
3684
|
+
ok: { type: "boolean", required: true },
|
|
3685
|
+
package: { type: "string", required: true },
|
|
3686
|
+
version: { type: "string", required: true },
|
|
3687
|
+
vaultOpen: { type: "boolean", required: true },
|
|
3688
|
+
items: { type: "number", required: true },
|
|
3689
|
+
error: { type: "string" }
|
|
3690
|
+
}
|
|
3691
|
+
},
|
|
3692
|
+
render: (_args, value) => [
|
|
3693
|
+
{
|
|
3694
|
+
type: "text",
|
|
3695
|
+
text: value.vaultOpen ? `dsh-inbox v${value.version}: vault open, ${value.items} record(s).` : `dsh-inbox v${value.version}: vault NOT open${value.error ? ` \u2014 ${value.error}` : ""}.`
|
|
3696
|
+
}
|
|
3697
|
+
]
|
|
3698
|
+
},
|
|
3699
|
+
async execute() {
|
|
3700
|
+
const open3 = openVault();
|
|
3701
|
+
const openError = lease.failure();
|
|
3702
|
+
return {
|
|
3703
|
+
ok: true,
|
|
3704
|
+
package: PACKAGE_NAME,
|
|
3705
|
+
version: VERSION,
|
|
3706
|
+
vaultOpen: open3 !== void 0,
|
|
3707
|
+
items: open3?.size ?? 0,
|
|
3708
|
+
...openError === void 0 ? {} : { error: openError }
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
})
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3714
|
+
export {
|
|
3715
|
+
apply,
|
|
3716
|
+
inject,
|
|
3717
|
+
name
|
|
3718
|
+
};
|