weread-export 0.1.2
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 +50 -0
- package/README.zh.md +50 -0
- package/cordis.patch.yml +14 -0
- package/lib/client.js +796 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +2164 -0
- package/lib/types/api.d.ts +238 -0
- package/lib/types/cache.d.ts +57 -0
- package/lib/types/client/WereadSettingsPanel.d.ts +2 -0
- package/lib/types/client/api.d.ts +69 -0
- package/lib/types/client/index.d.ts +8 -0
- package/lib/types/export.d.ts +45 -0
- package/lib/types/flomo.d.ts +33 -0
- package/lib/types/index.d.ts +44 -0
- package/lib/types/llm.d.ts +31 -0
- package/lib/types/routes.d.ts +57 -0
- package/lib/types/store.d.ts +87 -0
- package/lib/types/tools.d.ts +63 -0
- package/package.json +89 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "weread-export",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/api.ts
|
|
10
|
+
/** Error carrying the route's JSON error message. */
|
|
11
|
+
var WereadApiError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "WereadApiError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Parse a JSON response or throw a WereadApiError. */
|
|
18
|
+
async function readJson(response) {
|
|
19
|
+
let body;
|
|
20
|
+
try {
|
|
21
|
+
body = await response.json();
|
|
22
|
+
} catch {
|
|
23
|
+
throw new WereadApiError(`HTTP ${response.status}: invalid JSON response`);
|
|
24
|
+
}
|
|
25
|
+
if (!response.ok) throw new WereadApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `HTTP ${response.status}`);
|
|
26
|
+
return body;
|
|
27
|
+
}
|
|
28
|
+
/** Plain fetch helper with an error wrapper. */
|
|
29
|
+
async function request(path, init) {
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(path, init);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new WereadApiError("网络请求失败: " + String(error instanceof Error ? error.message : error));
|
|
35
|
+
}
|
|
36
|
+
return readJson(response);
|
|
37
|
+
}
|
|
38
|
+
/** The weread panel API. */
|
|
39
|
+
var WereadApi = class {
|
|
40
|
+
async getConfig() {
|
|
41
|
+
return request("/api/weread-export/config");
|
|
42
|
+
}
|
|
43
|
+
async setConfig(patch) {
|
|
44
|
+
return request("/api/weread-export/config", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify(patch)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async getStatus() {
|
|
51
|
+
return request("/api/weread-export/status");
|
|
52
|
+
}
|
|
53
|
+
async test() {
|
|
54
|
+
return request("/api/weread-export/test", { method: "POST" });
|
|
55
|
+
}
|
|
56
|
+
async sync() {
|
|
57
|
+
return request("/api/weread-export/sync", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({})
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async books() {
|
|
64
|
+
return (await request("/api/weread-export/books")).books ?? [];
|
|
65
|
+
}
|
|
66
|
+
async exportFlomo(bookId, tag, limit = 20) {
|
|
67
|
+
return request("/api/weread-export/flomo", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
bookId,
|
|
72
|
+
tag,
|
|
73
|
+
limit
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Multi-target export: flomo / local / notion with optional prompt. */
|
|
78
|
+
async exportData(body) {
|
|
79
|
+
return request("/api/weread-export/export", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify(body)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/client/WereadSettingsPanel.tsx
|
|
88
|
+
/**
|
|
89
|
+
* WeRead settings panel — rendered inside the web settings page
|
|
90
|
+
* (settings.section entry). Connection setup (Skills API key), export
|
|
91
|
+
* preferences (destination flomo/local/Notion, per-export limit, local dir,
|
|
92
|
+
* Notion token + target page, LLM prompt processing with a user-editable
|
|
93
|
+
* template), manual sync, and a quick multi-target export. Plain React,
|
|
94
|
+
* inline styles only.
|
|
95
|
+
*/
|
|
96
|
+
/** Module-level API client (stateless; the component closes over it). */
|
|
97
|
+
const api = new WereadApi();
|
|
98
|
+
/** One shared style sheet (kept tiny and theme-agnostic). */
|
|
99
|
+
const s = {
|
|
100
|
+
card: {
|
|
101
|
+
display: "flex",
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
gap: "10px",
|
|
104
|
+
maxWidth: "680px",
|
|
105
|
+
padding: "14px 16px",
|
|
106
|
+
borderRadius: "10px",
|
|
107
|
+
border: "1px solid rgba(128,128,128,0.3)",
|
|
108
|
+
fontSize: "13px",
|
|
109
|
+
color: "inherit"
|
|
110
|
+
},
|
|
111
|
+
title: {
|
|
112
|
+
fontWeight: 600,
|
|
113
|
+
fontSize: "13px",
|
|
114
|
+
margin: 0
|
|
115
|
+
},
|
|
116
|
+
section: {
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
fontSize: "12px",
|
|
119
|
+
margin: "6px 0 0",
|
|
120
|
+
opacity: .9
|
|
121
|
+
},
|
|
122
|
+
status: {
|
|
123
|
+
fontSize: "12px",
|
|
124
|
+
opacity: .85,
|
|
125
|
+
whiteSpace: "pre-wrap"
|
|
126
|
+
},
|
|
127
|
+
statusWarn: {
|
|
128
|
+
fontSize: "12px",
|
|
129
|
+
opacity: .9,
|
|
130
|
+
color: "#c9763a"
|
|
131
|
+
},
|
|
132
|
+
row: {
|
|
133
|
+
display: "flex",
|
|
134
|
+
gap: "6px",
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
minWidth: 0
|
|
137
|
+
},
|
|
138
|
+
col: {
|
|
139
|
+
display: "flex",
|
|
140
|
+
flexDirection: "column",
|
|
141
|
+
gap: "4px",
|
|
142
|
+
minWidth: 0
|
|
143
|
+
},
|
|
144
|
+
label: {
|
|
145
|
+
fontSize: "12px",
|
|
146
|
+
opacity: .8,
|
|
147
|
+
whiteSpace: "nowrap"
|
|
148
|
+
},
|
|
149
|
+
input: {
|
|
150
|
+
flex: 1,
|
|
151
|
+
minWidth: 0,
|
|
152
|
+
boxSizing: "border-box",
|
|
153
|
+
padding: "5px 8px",
|
|
154
|
+
borderRadius: "6px",
|
|
155
|
+
border: "1px solid rgba(128,128,128,0.35)",
|
|
156
|
+
background: "rgba(128,128,128,0.08)",
|
|
157
|
+
color: "inherit",
|
|
158
|
+
fontSize: "12px"
|
|
159
|
+
},
|
|
160
|
+
textarea: {
|
|
161
|
+
width: "100%",
|
|
162
|
+
minHeight: "96px",
|
|
163
|
+
boxSizing: "border-box",
|
|
164
|
+
padding: "6px 8px",
|
|
165
|
+
borderRadius: "6px",
|
|
166
|
+
border: "1px solid rgba(128,128,128,0.35)",
|
|
167
|
+
background: "rgba(128,128,128,0.08)",
|
|
168
|
+
color: "inherit",
|
|
169
|
+
fontSize: "12px",
|
|
170
|
+
fontFamily: "inherit",
|
|
171
|
+
lineHeight: 1.5,
|
|
172
|
+
resize: "vertical"
|
|
173
|
+
},
|
|
174
|
+
select: {
|
|
175
|
+
flex: 1,
|
|
176
|
+
minWidth: 0,
|
|
177
|
+
maxWidth: "100%",
|
|
178
|
+
overflow: "hidden",
|
|
179
|
+
textOverflow: "ellipsis",
|
|
180
|
+
padding: "4px 6px",
|
|
181
|
+
borderRadius: "6px",
|
|
182
|
+
border: "1px solid rgba(128,128,128,0.35)",
|
|
183
|
+
background: "rgba(128,128,128,0.08)",
|
|
184
|
+
color: "inherit",
|
|
185
|
+
fontSize: "12px"
|
|
186
|
+
},
|
|
187
|
+
flex: { flex: 1 },
|
|
188
|
+
button: {
|
|
189
|
+
padding: "4px 10px",
|
|
190
|
+
borderRadius: "6px",
|
|
191
|
+
cursor: "pointer",
|
|
192
|
+
border: "1px solid rgba(128,128,128,0.4)",
|
|
193
|
+
background: "rgba(128,128,128,0.14)",
|
|
194
|
+
color: "inherit",
|
|
195
|
+
fontSize: "12px",
|
|
196
|
+
whiteSpace: "nowrap"
|
|
197
|
+
},
|
|
198
|
+
msg: {
|
|
199
|
+
fontSize: "12px",
|
|
200
|
+
whiteSpace: "pre-wrap",
|
|
201
|
+
wordBreak: "break-all",
|
|
202
|
+
opacity: .9
|
|
203
|
+
},
|
|
204
|
+
hint: {
|
|
205
|
+
fontSize: "11px",
|
|
206
|
+
opacity: .75,
|
|
207
|
+
lineHeight: 1.6
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/** Status line for the current config view. */
|
|
211
|
+
function statusText(view) {
|
|
212
|
+
if (view === null) return "加载中…";
|
|
213
|
+
if (!view.configured) return "未配置 — 打开 https://weread.qq.com/r/weread-skills,用微信读书账号登录后点击「创建 Key」并复制(wrk- 开头),粘贴到上方输入框。";
|
|
214
|
+
const destLabel = view.exportDest === "local" ? "本地文件" : view.exportDest === "notion" ? "Notion" : "flomo";
|
|
215
|
+
return "已配置 · Key " + view.apiKeyMasked + "\n默认导出目标 " + destLabel + " · 导出条数 " + (view.exportLimit === 0 ? "全部" : view.exportLimit + " 条") + " · 默认标签 #" + view.defaultFlomoTag + "\nprompt 处理 " + (view.usePrompt ? "开(" + (view.llmConfigured ? view.llmModel + " @ " + view.llmBaseUrl : "LLM 未配置") + ")" : "关") + " · Notion " + (view.notionConfigured ? "已配置" + (view.notionTargetPageId !== "" ? " + 目标页" : "(未填目标页)") : "未配置") + " · flomo " + (view.flomoConfigured ? "已配置" : "未配置") + " · 缓存书架 " + view.cachedShelfBooks + " 本 / 有笔记 " + view.cachedNoteBooks + " 本" + (view.lastSyncAt !== "" ? " · 最近同步 " + view.lastSyncAt : "");
|
|
216
|
+
}
|
|
217
|
+
/** The WeRead settings panel component. */
|
|
218
|
+
function WereadSettingsPanel() {
|
|
219
|
+
const [view, setView] = (0, react.useState)(null);
|
|
220
|
+
const [apiKey, setApiKey] = (0, react.useState)("");
|
|
221
|
+
const [defaultFlomoTag, setDefaultFlomoTag] = (0, react.useState)("");
|
|
222
|
+
const [limitChoice, setLimitChoice] = (0, react.useState)("20");
|
|
223
|
+
const [customLimit, setCustomLimit] = (0, react.useState)("");
|
|
224
|
+
const [exportDest, setExportDest] = (0, react.useState)("flomo");
|
|
225
|
+
const [localExportDir, setLocalExportDir] = (0, react.useState)("");
|
|
226
|
+
const [notionToken, setNotionToken] = (0, react.useState)("");
|
|
227
|
+
const [notionTargetPageId, setNotionTargetPageId] = (0, react.useState)("");
|
|
228
|
+
const [usePrompt, setUsePrompt] = (0, react.useState)(false);
|
|
229
|
+
const [llmBaseUrl, setLlmBaseUrl] = (0, react.useState)("");
|
|
230
|
+
const [llmApiKey, setLlmApiKey] = (0, react.useState)("");
|
|
231
|
+
const [llmModel, setLlmModel] = (0, react.useState)("");
|
|
232
|
+
const [exportPrompt, setExportPrompt] = (0, react.useState)("");
|
|
233
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
234
|
+
const [message, setMessage] = (0, react.useState)("");
|
|
235
|
+
const [books, setBooks] = (0, react.useState)([]);
|
|
236
|
+
const [bookId, setBookId] = (0, react.useState)("");
|
|
237
|
+
const [destNow, setDestNow] = (0, react.useState)("");
|
|
238
|
+
const [localDirNow, setLocalDirNow] = (0, react.useState)("");
|
|
239
|
+
const [flomoTagNow, setFlomoTagNow] = (0, react.useState)("");
|
|
240
|
+
const [usePromptNow, setUsePromptNow] = (0, react.useState)(null);
|
|
241
|
+
/** Map the persisted exportLimit to the choice selector. */
|
|
242
|
+
const limitFromView = (value) => {
|
|
243
|
+
if (value === 0) return "0";
|
|
244
|
+
if ([
|
|
245
|
+
20,
|
|
246
|
+
50,
|
|
247
|
+
100
|
|
248
|
+
].includes(value)) return String(value);
|
|
249
|
+
return "custom";
|
|
250
|
+
};
|
|
251
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
252
|
+
try {
|
|
253
|
+
const next = await api.getStatus();
|
|
254
|
+
setView(next);
|
|
255
|
+
setDefaultFlomoTag((prev) => prev === "" ? next.defaultFlomoTag : prev);
|
|
256
|
+
setFlomoTagNow((prev) => prev === "" ? next.defaultFlomoTag : prev);
|
|
257
|
+
setLimitChoice(limitFromView(next.exportLimit));
|
|
258
|
+
if (![
|
|
259
|
+
0,
|
|
260
|
+
20,
|
|
261
|
+
50,
|
|
262
|
+
100
|
|
263
|
+
].includes(next.exportLimit)) setCustomLimit(String(next.exportLimit));
|
|
264
|
+
setExportDest(next.exportDest);
|
|
265
|
+
setLocalExportDir(next.localExportDir);
|
|
266
|
+
setNotionTargetPageId(next.notionTargetPageId);
|
|
267
|
+
setUsePrompt(next.usePrompt);
|
|
268
|
+
setLlmBaseUrl(next.llmBaseUrl);
|
|
269
|
+
setLlmModel(next.llmModel);
|
|
270
|
+
if (destNow === "") setDestNow(next.exportDest);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
setMessage("状态读取失败: " + String(error instanceof Error ? error.message : error));
|
|
273
|
+
}
|
|
274
|
+
}, [destNow]);
|
|
275
|
+
(0, react.useEffect)(() => {
|
|
276
|
+
refresh();
|
|
277
|
+
}, [refresh]);
|
|
278
|
+
const reloadBooks = (0, react.useCallback)(async () => {
|
|
279
|
+
try {
|
|
280
|
+
const list = await api.books();
|
|
281
|
+
setBooks(list);
|
|
282
|
+
if (list.length > 0) setBookId((prev) => prev !== "" && list.some((b) => b.bookId === prev) ? prev : list[0]?.bookId ?? "");
|
|
283
|
+
} catch {
|
|
284
|
+
setBooks([]);
|
|
285
|
+
}
|
|
286
|
+
}, []);
|
|
287
|
+
(0, react.useEffect)(() => {
|
|
288
|
+
reloadBooks();
|
|
289
|
+
}, [reloadBooks]);
|
|
290
|
+
/** Resolve the chosen export limit to a number (0 = all). */
|
|
291
|
+
const resolveExportLimit = () => {
|
|
292
|
+
if (limitChoice === "custom") {
|
|
293
|
+
const parsed = Number(customLimit);
|
|
294
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
|
|
295
|
+
}
|
|
296
|
+
return Number(limitChoice) || 0;
|
|
297
|
+
};
|
|
298
|
+
const saveConfig = async () => {
|
|
299
|
+
setBusy(true);
|
|
300
|
+
setMessage("");
|
|
301
|
+
try {
|
|
302
|
+
const patch = {};
|
|
303
|
+
if (apiKey.trim() !== "") patch.apiKey = apiKey.trim();
|
|
304
|
+
if (defaultFlomoTag.trim() !== "") patch.defaultFlomoTag = defaultFlomoTag.trim();
|
|
305
|
+
patch.exportLimit = resolveExportLimit();
|
|
306
|
+
patch.exportDest = exportDest;
|
|
307
|
+
if (localExportDir.trim() !== "") patch.localExportDir = localExportDir.trim();
|
|
308
|
+
if (notionToken.trim() !== "") patch.notionToken = notionToken.trim();
|
|
309
|
+
if (notionTargetPageId.trim() !== "") patch.notionTargetPageId = notionTargetPageId.trim();
|
|
310
|
+
patch.usePrompt = usePrompt;
|
|
311
|
+
if (llmBaseUrl.trim() !== "") patch.llmBaseUrl = llmBaseUrl.trim();
|
|
312
|
+
if (llmApiKey.trim() !== "") patch.llmApiKey = llmApiKey.trim();
|
|
313
|
+
if (llmModel.trim() !== "") patch.llmModel = llmModel.trim();
|
|
314
|
+
if (exportPrompt.trim() !== "") patch.exportPrompt = exportPrompt;
|
|
315
|
+
const next = await api.setConfig(patch);
|
|
316
|
+
setView({
|
|
317
|
+
...next,
|
|
318
|
+
flomoConfigured: view?.flomoConfigured ?? false,
|
|
319
|
+
cachedShelfBooks: view?.cachedShelfBooks ?? 0,
|
|
320
|
+
cachedNoteBooks: view?.cachedNoteBooks ?? 0,
|
|
321
|
+
cacheUpdatedAt: view?.cacheUpdatedAt ?? ""
|
|
322
|
+
});
|
|
323
|
+
setMessage(next.configured ? "配置已保存。" : "配置未保存完整:缺少 API Key。");
|
|
324
|
+
setApiKey("");
|
|
325
|
+
setNotionToken("");
|
|
326
|
+
setLlmApiKey("");
|
|
327
|
+
} catch (error) {
|
|
328
|
+
setMessage("保存失败: " + String(error instanceof Error ? error.message : error));
|
|
329
|
+
} finally {
|
|
330
|
+
setBusy(false);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const clearConfig = async () => {
|
|
334
|
+
setBusy(true);
|
|
335
|
+
setMessage("");
|
|
336
|
+
try {
|
|
337
|
+
const next = await api.setConfig({ reset: true });
|
|
338
|
+
setView({
|
|
339
|
+
...next,
|
|
340
|
+
flomoConfigured: view?.flomoConfigured ?? false,
|
|
341
|
+
cachedShelfBooks: view?.cachedShelfBooks ?? 0,
|
|
342
|
+
cachedNoteBooks: view?.cachedNoteBooks ?? 0,
|
|
343
|
+
cacheUpdatedAt: view?.cacheUpdatedAt ?? ""
|
|
344
|
+
});
|
|
345
|
+
setMessage("已清除配置。");
|
|
346
|
+
} catch (error) {
|
|
347
|
+
setMessage("清除失败: " + String(error instanceof Error ? error.message : error));
|
|
348
|
+
} finally {
|
|
349
|
+
setBusy(false);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const runTest = async () => {
|
|
353
|
+
setBusy(true);
|
|
354
|
+
setMessage("测试中…");
|
|
355
|
+
try {
|
|
356
|
+
const result = await api.test();
|
|
357
|
+
setMessage(result.ok ? result.message : "测试失败: " + result.message);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
setMessage("测试失败: " + String(error instanceof Error ? error.message : error));
|
|
360
|
+
} finally {
|
|
361
|
+
setBusy(false);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const runSync = async () => {
|
|
365
|
+
setBusy(true);
|
|
366
|
+
setMessage("同步中…");
|
|
367
|
+
try {
|
|
368
|
+
const result = await api.sync();
|
|
369
|
+
setMessage(result.ok ? result.message : "同步失败: " + result.message);
|
|
370
|
+
await refresh();
|
|
371
|
+
await reloadBooks();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
setMessage("同步失败: " + String(error instanceof Error ? error.message : error));
|
|
374
|
+
} finally {
|
|
375
|
+
setBusy(false);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const runExportNow = async () => {
|
|
379
|
+
if (bookId === "") {
|
|
380
|
+
setMessage("请先同步以加载书籍列表。");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const dest = destNow || view?.exportDest || "flomo";
|
|
384
|
+
if (dest === "local" && localDirNow.trim() === "") {
|
|
385
|
+
setMessage("本地导出需要填写导出路径(每次必填)。");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
setBusy(true);
|
|
389
|
+
setMessage("导出中…");
|
|
390
|
+
try {
|
|
391
|
+
const body = {
|
|
392
|
+
bookId,
|
|
393
|
+
dest
|
|
394
|
+
};
|
|
395
|
+
if (dest === "local") body.localDir = localDirNow.trim();
|
|
396
|
+
if (dest === "flomo" && flomoTagNow.trim() !== "") body.tag = flomoTagNow.trim();
|
|
397
|
+
if (usePromptNow !== null) body.usePrompt = usePromptNow;
|
|
398
|
+
const result = await api.exportData(body);
|
|
399
|
+
setMessage(result.ok ? result.message : "导出失败: " + result.message);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
setMessage("导出失败: " + String(error instanceof Error ? error.message : error));
|
|
402
|
+
} finally {
|
|
403
|
+
setBusy(false);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
407
|
+
style: s.card,
|
|
408
|
+
children: [
|
|
409
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
410
|
+
style: s.title,
|
|
411
|
+
children: "微信读书"
|
|
412
|
+
}),
|
|
413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
414
|
+
style: view?.configured === false ? s.statusWarn : s.status,
|
|
415
|
+
children: statusText(view)
|
|
416
|
+
}),
|
|
417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
418
|
+
style: s.section,
|
|
419
|
+
children: "连接与导出偏好"
|
|
420
|
+
}),
|
|
421
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
422
|
+
style: s.row,
|
|
423
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
424
|
+
style: s.input,
|
|
425
|
+
placeholder: "Skills API Key(wrk- 开头)",
|
|
426
|
+
value: apiKey,
|
|
427
|
+
onChange: (e) => setApiKey(e.target.value)
|
|
428
|
+
})
|
|
429
|
+
}),
|
|
430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
431
|
+
style: s.row,
|
|
432
|
+
children: [
|
|
433
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
434
|
+
style: s.label,
|
|
435
|
+
children: "默认导出标签"
|
|
436
|
+
}),
|
|
437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
438
|
+
style: {
|
|
439
|
+
...s.input,
|
|
440
|
+
width: "130px"
|
|
441
|
+
},
|
|
442
|
+
placeholder: "如 读书笔记",
|
|
443
|
+
value: defaultFlomoTag,
|
|
444
|
+
onChange: (e) => setDefaultFlomoTag(e.target.value)
|
|
445
|
+
}),
|
|
446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
447
|
+
style: s.label,
|
|
448
|
+
children: "导出条数"
|
|
449
|
+
}),
|
|
450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
451
|
+
style: {
|
|
452
|
+
...s.select,
|
|
453
|
+
flex: 0,
|
|
454
|
+
minWidth: "104px"
|
|
455
|
+
},
|
|
456
|
+
value: limitChoice,
|
|
457
|
+
onChange: (e) => setLimitChoice(e.target.value),
|
|
458
|
+
children: [
|
|
459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
460
|
+
value: "0",
|
|
461
|
+
children: "全部导出"
|
|
462
|
+
}),
|
|
463
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
464
|
+
value: "20",
|
|
465
|
+
children: "20 条(默认)"
|
|
466
|
+
}),
|
|
467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
468
|
+
value: "50",
|
|
469
|
+
children: "50 条"
|
|
470
|
+
}),
|
|
471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
472
|
+
value: "100",
|
|
473
|
+
children: "100 条"
|
|
474
|
+
}),
|
|
475
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
476
|
+
value: "custom",
|
|
477
|
+
children: "自定义…"
|
|
478
|
+
})
|
|
479
|
+
]
|
|
480
|
+
}),
|
|
481
|
+
limitChoice === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
482
|
+
style: {
|
|
483
|
+
...s.input,
|
|
484
|
+
width: "64px"
|
|
485
|
+
},
|
|
486
|
+
placeholder: "条数",
|
|
487
|
+
value: customLimit,
|
|
488
|
+
onChange: (e) => setCustomLimit(e.target.value)
|
|
489
|
+
}),
|
|
490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: s.flex }),
|
|
491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
492
|
+
style: s.button,
|
|
493
|
+
onClick: () => void saveConfig(),
|
|
494
|
+
disabled: busy,
|
|
495
|
+
children: "保存配置"
|
|
496
|
+
}),
|
|
497
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
498
|
+
style: s.button,
|
|
499
|
+
onClick: () => void runTest(),
|
|
500
|
+
disabled: busy || !view?.configured,
|
|
501
|
+
children: "测试连接"
|
|
502
|
+
}),
|
|
503
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
504
|
+
style: s.button,
|
|
505
|
+
onClick: () => void clearConfig(),
|
|
506
|
+
disabled: busy,
|
|
507
|
+
children: "清除"
|
|
508
|
+
})
|
|
509
|
+
]
|
|
510
|
+
}),
|
|
511
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
512
|
+
style: s.row,
|
|
513
|
+
children: [
|
|
514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
515
|
+
style: s.label,
|
|
516
|
+
children: "默认目标"
|
|
517
|
+
}),
|
|
518
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
519
|
+
style: {
|
|
520
|
+
...s.select,
|
|
521
|
+
flex: 0,
|
|
522
|
+
minWidth: "110px"
|
|
523
|
+
},
|
|
524
|
+
value: exportDest,
|
|
525
|
+
onChange: (e) => setExportDest(e.target.value),
|
|
526
|
+
children: [
|
|
527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
528
|
+
value: "flomo",
|
|
529
|
+
children: "flomo"
|
|
530
|
+
}),
|
|
531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
532
|
+
value: "local",
|
|
533
|
+
children: "本地文件"
|
|
534
|
+
}),
|
|
535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
536
|
+
value: "notion",
|
|
537
|
+
children: "Notion"
|
|
538
|
+
})
|
|
539
|
+
]
|
|
540
|
+
}),
|
|
541
|
+
exportDest === "local" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
542
|
+
style: s.input,
|
|
543
|
+
placeholder: "本地导出目录(绝对路径,可留空导出时填)",
|
|
544
|
+
value: localExportDir,
|
|
545
|
+
onChange: (e) => setLocalExportDir(e.target.value)
|
|
546
|
+
})
|
|
547
|
+
]
|
|
548
|
+
}),
|
|
549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
550
|
+
style: s.section,
|
|
551
|
+
children: "Notion(本插件独立配置)"
|
|
552
|
+
}),
|
|
553
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
554
|
+
style: s.row,
|
|
555
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
556
|
+
style: s.input,
|
|
557
|
+
type: "password",
|
|
558
|
+
placeholder: "Notion Integration Token(notion.so/my-integrations 创建)",
|
|
559
|
+
value: notionToken,
|
|
560
|
+
onChange: (e) => setNotionToken(e.target.value)
|
|
561
|
+
})
|
|
562
|
+
}),
|
|
563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
564
|
+
style: s.row,
|
|
565
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
566
|
+
style: s.input,
|
|
567
|
+
placeholder: "目标父页面 URL 或 32 位 ID(页面需分享给该 Integration)",
|
|
568
|
+
value: notionTargetPageId,
|
|
569
|
+
onChange: (e) => setNotionTargetPageId(e.target.value)
|
|
570
|
+
})
|
|
571
|
+
}),
|
|
572
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
573
|
+
style: s.section,
|
|
574
|
+
children: "AI · prompt 处理(导出前用 LLM 按模板整理)"
|
|
575
|
+
}),
|
|
576
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
577
|
+
style: s.row,
|
|
578
|
+
children: [
|
|
579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
580
|
+
style: {
|
|
581
|
+
...s.label,
|
|
582
|
+
display: "flex",
|
|
583
|
+
alignItems: "center",
|
|
584
|
+
gap: "4px",
|
|
585
|
+
cursor: "pointer"
|
|
586
|
+
},
|
|
587
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
588
|
+
type: "checkbox",
|
|
589
|
+
checked: usePrompt,
|
|
590
|
+
onChange: (e) => setUsePrompt(e.target.checked)
|
|
591
|
+
}), "启用"]
|
|
592
|
+
}),
|
|
593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
594
|
+
style: s.input,
|
|
595
|
+
placeholder: "Base URL(默认 https://api.deepseek.com/v1)",
|
|
596
|
+
value: llmBaseUrl,
|
|
597
|
+
onChange: (e) => setLlmBaseUrl(e.target.value)
|
|
598
|
+
}),
|
|
599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
600
|
+
style: {
|
|
601
|
+
...s.input,
|
|
602
|
+
width: "130px"
|
|
603
|
+
},
|
|
604
|
+
placeholder: "模型",
|
|
605
|
+
value: llmModel,
|
|
606
|
+
onChange: (e) => setLlmModel(e.target.value)
|
|
607
|
+
}),
|
|
608
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
609
|
+
style: s.input,
|
|
610
|
+
type: "password",
|
|
611
|
+
placeholder: "LLM API Key(自定义)",
|
|
612
|
+
value: llmApiKey,
|
|
613
|
+
onChange: (e) => setLlmApiKey(e.target.value)
|
|
614
|
+
})
|
|
615
|
+
]
|
|
616
|
+
}),
|
|
617
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
618
|
+
style: s.col,
|
|
619
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
620
|
+
style: s.textarea,
|
|
621
|
+
placeholder: "导出 prompt 模板,可用 {title} {author} {highlights} {thoughts} 占位符",
|
|
622
|
+
value: exportPrompt,
|
|
623
|
+
onChange: (e) => setExportPrompt(e.target.value)
|
|
624
|
+
})
|
|
625
|
+
}),
|
|
626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
627
|
+
style: s.row,
|
|
628
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
629
|
+
style: s.button,
|
|
630
|
+
onClick: () => void runSync(),
|
|
631
|
+
disabled: busy || !view?.configured,
|
|
632
|
+
children: "同步书架/笔记"
|
|
633
|
+
})
|
|
634
|
+
}),
|
|
635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
636
|
+
style: s.section,
|
|
637
|
+
children: "快捷导出"
|
|
638
|
+
}),
|
|
639
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
640
|
+
style: s.row,
|
|
641
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
642
|
+
style: s.label,
|
|
643
|
+
children: "选择书籍"
|
|
644
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
645
|
+
style: s.select,
|
|
646
|
+
value: bookId,
|
|
647
|
+
onChange: (e) => setBookId(e.target.value),
|
|
648
|
+
disabled: books.length === 0,
|
|
649
|
+
children: books.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
650
|
+
value: "",
|
|
651
|
+
children: "(缓存无书籍,先同步)"
|
|
652
|
+
}) : books.map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
653
|
+
value: b.bookId,
|
|
654
|
+
children: [
|
|
655
|
+
"《",
|
|
656
|
+
b.title,
|
|
657
|
+
"》",
|
|
658
|
+
b.author !== "" ? " · " + b.author : ""
|
|
659
|
+
]
|
|
660
|
+
}, b.bookId))
|
|
661
|
+
})]
|
|
662
|
+
}),
|
|
663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
664
|
+
style: s.row,
|
|
665
|
+
children: [
|
|
666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
667
|
+
style: s.label,
|
|
668
|
+
children: "本次目标"
|
|
669
|
+
}),
|
|
670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
671
|
+
style: {
|
|
672
|
+
...s.select,
|
|
673
|
+
flex: 0,
|
|
674
|
+
minWidth: "110px"
|
|
675
|
+
},
|
|
676
|
+
value: destNow,
|
|
677
|
+
onChange: (e) => setDestNow(e.target.value),
|
|
678
|
+
children: [
|
|
679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
680
|
+
value: "flomo",
|
|
681
|
+
children: "flomo"
|
|
682
|
+
}),
|
|
683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
684
|
+
value: "local",
|
|
685
|
+
children: "本地文件"
|
|
686
|
+
}),
|
|
687
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
688
|
+
value: "notion",
|
|
689
|
+
children: "Notion"
|
|
690
|
+
})
|
|
691
|
+
]
|
|
692
|
+
}),
|
|
693
|
+
destNow === "local" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
694
|
+
style: s.input,
|
|
695
|
+
placeholder: "导出路径(必填)",
|
|
696
|
+
value: localDirNow,
|
|
697
|
+
onChange: (e) => setLocalDirNow(e.target.value)
|
|
698
|
+
}),
|
|
699
|
+
destNow === "flomo" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
700
|
+
style: {
|
|
701
|
+
...s.input,
|
|
702
|
+
width: "150px"
|
|
703
|
+
},
|
|
704
|
+
placeholder: "本次标签(#)",
|
|
705
|
+
value: flomoTagNow,
|
|
706
|
+
onChange: (e) => setFlomoTagNow(e.target.value)
|
|
707
|
+
})
|
|
708
|
+
]
|
|
709
|
+
}),
|
|
710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
711
|
+
style: s.row,
|
|
712
|
+
children: [
|
|
713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
714
|
+
style: {
|
|
715
|
+
...s.label,
|
|
716
|
+
display: "flex",
|
|
717
|
+
alignItems: "center",
|
|
718
|
+
gap: "4px",
|
|
719
|
+
cursor: "pointer"
|
|
720
|
+
},
|
|
721
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
722
|
+
type: "checkbox",
|
|
723
|
+
checked: usePromptNow === null ? view?.usePrompt ?? false : usePromptNow,
|
|
724
|
+
onChange: (e) => setUsePromptNow(e.target.checked)
|
|
725
|
+
}), "本次用 prompt 处理(默认跟随配置)"]
|
|
726
|
+
}),
|
|
727
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: s.flex }),
|
|
728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
729
|
+
style: s.button,
|
|
730
|
+
onClick: () => void runExportNow(),
|
|
731
|
+
disabled: busy || !view?.configured,
|
|
732
|
+
children: ["导出到 ", destNow === "local" ? "本地" : destNow === "notion" ? "Notion" : "flomo"]
|
|
733
|
+
})
|
|
734
|
+
]
|
|
735
|
+
}),
|
|
736
|
+
message !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
737
|
+
style: s.msg,
|
|
738
|
+
children: message
|
|
739
|
+
}),
|
|
740
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
741
|
+
style: s.hint,
|
|
742
|
+
children: "【API Key】打开 https://weread.qq.com/r/weread-skills → 微信读书账号登录 → 「创建 Key」→ 复制(wrk- 开头)。Key 绑定你的账号身份,请勿泄露。存于 ~/.dsh/weread-export.json(权限 0600)。"
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
745
|
+
style: s.hint,
|
|
746
|
+
children: "【导出目标】flomo = 发到浮墨(复用「Flomo」面板凭据,超长自动拆多条 MEMO);本地 = 导出 Markdown 文件到指定目录(每次导出需填路径);Notion = 用本插件自己的 Integration Token 在目标父页面下创建子页面写入(目标页需分享给该 Integration)。"
|
|
747
|
+
}),
|
|
748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
749
|
+
style: s.hint,
|
|
750
|
+
children: [
|
|
751
|
+
"【AI prompt】启用后导出前会调用 LLM(OpenAI 兼容,默认 DeepSeek)按模板整理划线内容再导出。模板支持 ",
|
|
752
|
+
"{title}",
|
|
753
|
+
" ",
|
|
754
|
+
"{author}",
|
|
755
|
+
" ",
|
|
756
|
+
"{highlights}",
|
|
757
|
+
" ",
|
|
758
|
+
"{thoughts}",
|
|
759
|
+
" 占位符,可随意编辑;本次导出也可临时开关。"
|
|
760
|
+
]
|
|
761
|
+
}),
|
|
762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
763
|
+
style: s.hint,
|
|
764
|
+
children: "【导出条数】「全部导出」= 全部划线都导出(flomo 超长自动拆多条);「20/50/100/自定义」= 最多导出的条数。 同步后可用 weread_shelf / weread_notes / weread_readdata / weread_search / weread_book / weread_export 等工具。"
|
|
765
|
+
})
|
|
766
|
+
]
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/client/index.ts
|
|
771
|
+
/** Required services. */
|
|
772
|
+
const inject = ["slots"];
|
|
773
|
+
/**
|
|
774
|
+
* Register the WeRead settings page.
|
|
775
|
+
* @param ctx - client root context.
|
|
776
|
+
*/
|
|
777
|
+
function apply(ctx) {
|
|
778
|
+
try {
|
|
779
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
780
|
+
name: "settings.section",
|
|
781
|
+
id: "weread",
|
|
782
|
+
order: 316,
|
|
783
|
+
label: () => "微信读书"
|
|
784
|
+
}, WereadSettingsPanel));
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.warn("[weread-export] settings panel registration failed:", error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
790
|
+
exports.apply = apply;
|
|
791
|
+
exports.inject = inject;
|
|
792
|
+
return module.exports;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
//# sourceMappingURL=client.js.map
|