dsh-knowledge-library 0.1.1 → 0.3.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +133 -14
  3. package/dist/client.js +1654 -304
  4. package/dist/core/extract.d.ts +5 -0
  5. package/dist/core/extract.js +78 -8
  6. package/dist/core/fetch.d.ts +5 -0
  7. package/dist/core/fetch.js +102 -25
  8. package/dist/core/grouping.d.ts +6 -0
  9. package/dist/core/grouping.js +71 -0
  10. package/dist/core/image-capture.d.ts +33 -0
  11. package/dist/core/image-capture.js +196 -0
  12. package/dist/core/incremental.d.ts +14 -0
  13. package/dist/core/incremental.js +52 -0
  14. package/dist/core/jobs.d.ts +21 -1
  15. package/dist/core/jobs.js +101 -12
  16. package/dist/core/knowledge.d.ts +37 -0
  17. package/dist/core/knowledge.js +619 -0
  18. package/dist/core/library.d.ts +36 -2
  19. package/dist/core/library.js +363 -17
  20. package/dist/core/page-access.d.ts +6 -0
  21. package/dist/core/page-access.js +38 -0
  22. package/dist/core/page-images.d.ts +15 -0
  23. package/dist/core/page-images.js +97 -0
  24. package/dist/core/purge.d.ts +20 -0
  25. package/dist/core/purge.js +106 -0
  26. package/dist/directory-settings.d.ts +18 -0
  27. package/dist/directory-settings.js +92 -0
  28. package/dist/http.d.ts +3 -1
  29. package/dist/http.js +152 -4
  30. package/dist/index.js +22 -7
  31. package/dist/knowledge-model.d.ts +30 -0
  32. package/dist/knowledge-model.js +194 -0
  33. package/dist/preview-settings.d.ts +6 -0
  34. package/dist/preview-settings.js +42 -0
  35. package/dist/public/app.css +145 -2
  36. package/dist/public/app.js +10 -6
  37. package/dist/standalone.js +12 -4
  38. package/dist/tools.d.ts +2 -1
  39. package/dist/tools.js +10 -1
  40. package/dist/types.d.ts +199 -1
  41. package/dist/ui/App.js +248 -47
  42. package/dist/ui/Dialog.d.ts +7 -0
  43. package/dist/ui/Dialog.js +32 -0
  44. package/dist/ui/Grouping.d.ts +12 -0
  45. package/dist/ui/Grouping.js +93 -0
  46. package/dist/ui/ImageCapture.d.ts +18 -0
  47. package/dist/ui/ImageCapture.js +76 -0
  48. package/dist/ui/Knowledge.d.ts +26 -0
  49. package/dist/ui/Knowledge.js +106 -0
  50. package/dist/ui/api.d.ts +7 -0
  51. package/dist/ui/api.js +16 -0
  52. package/dist/ui/icons.js +2 -0
  53. package/package.json +5 -1
package/dist/ui/App.js CHANGED
@@ -1,21 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { Icon } from './icons.js';
4
- const API = '/knowledge-library/api';
4
+ import { API, ApiError, api, post } from './api.js';
5
+ import { Dialog } from './Dialog.js';
6
+ import { KnowledgeBadge, KnowledgeJobsDialog, StartKnowledgeDialog } from './Knowledge.js';
7
+ import { ImageSettings, ImageCapturePolicy, PageImages, ImageRetryDialog, imagesPending } from './ImageCapture.js';
5
8
  const kinds = {
6
9
  web: { label: '网页', icon: 'web' }, text: { label: '文本', icon: 'file' }, file: { label: '文件', icon: 'file' },
7
10
  video: { label: '音视频', icon: 'video' }, transcript: { label: '字幕', icon: 'video' }, note: { label: '笔记', icon: 'note' },
8
11
  };
9
12
  const time = (v) => `${String(Math.floor(v / 3600)).padStart(2, '0')}:${String(Math.floor(v / 60) % 60).padStart(2, '0')}:${String(Math.floor(v) % 60).padStart(2, '0')}`;
10
13
  const date = (value) => new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit' }).format(new Date(value));
11
- async function api(path, options = {}) {
12
- const response = await fetch(API + path, { ...options, headers: { 'Content-Type': 'application/json', 'X-Knowledge-Library': '1', ...options.headers } });
13
- const data = await response.json();
14
- if (!response.ok)
15
- throw new Error(data.error || `请求失败:${response.status}`);
16
- return data;
17
- }
18
- const post = (path, value, method = 'POST') => api(path, { method, body: JSON.stringify(value) });
19
14
  function splitTags(value) { return value.split(/[,,]/).map(v => v.trim()).filter(Boolean); }
20
15
  export function App() {
21
16
  const [stats, setStats] = useState();
@@ -27,9 +22,19 @@ export function App() {
27
22
  const [detail, setDetail] = useState();
28
23
  const [anchor, setAnchor] = useState();
29
24
  const [editor, setEditor] = useState();
25
+ const [deletion, setDeletion] = useState();
26
+ const [restoring, setRestoring] = useState();
27
+ const [emptyRoot, setEmptyRoot] = useState();
30
28
  const [jobs, setJobs] = useState([]);
29
+ const [knowledge, setKnowledge] = useState();
30
+ const [batchMode, setBatchMode] = useState(false);
31
+ const [checkedIds, setCheckedIds] = useState([]);
32
+ const [generation, setGeneration] = useState();
33
+ const [knowledgeOpen, setKnowledgeOpen] = useState();
31
34
  const [showJobs, setShowJobs] = useState(false);
35
+ const [imageRetry, setImageRetry] = useState();
32
36
  const [showInfo, setShowInfo] = useState(false);
37
+ const [settingsFromJobs, setSettingsFromJobs] = useState(false);
33
38
  const [showNavigation, setShowNavigation] = useState(false);
34
39
  const [error, setError] = useState('');
35
40
  const [toast, setToast] = useState('');
@@ -41,12 +46,18 @@ export function App() {
41
46
  const detailRef = useRef(null);
42
47
  const refresh = useCallback(() => setRevision(v => v + 1), []);
43
48
  const notify = (message) => setToast(message);
44
- const chooseFilter = (next) => { setFilter(next); setLimit(40); setSelected(undefined); setAnchor(undefined); setDetail(undefined); setShowNavigation(false); };
49
+ const chooseFilter = (next) => { setFilter(next); setLimit(40); setSelected(undefined); setAnchor(undefined); setDetail(undefined); setShowNavigation(false); setBatchMode(false); setCheckedIds([]); };
45
50
  const refreshMeta = useCallback(async () => {
46
- const [s, j] = await Promise.all([api('/stats'), api('/jobs')]);
51
+ const [s, j, k] = await Promise.all([api('/stats'), api('/jobs'), api('/knowledge').catch(e => {
52
+ if (e instanceof ApiError && e.status === 404)
53
+ return { supported: false, available: false, reason: '请重启资料库服务以启用知识整理。', jobs: [], states: {} };
54
+ throw e;
55
+ })]);
47
56
  setStats(s);
48
57
  setJobs(j);
58
+ setKnowledge(k);
49
59
  }, []);
60
+ useEffect(() => { setCheckedIds([]); }, [query]);
50
61
  useEffect(() => { void refreshMeta().catch(e => setError(e.message)); }, [revision, refreshMeta]);
51
62
  useEffect(() => {
52
63
  const controller = new AbortController();
@@ -87,12 +98,14 @@ export function App() {
87
98
  element?.scrollIntoView({ block: 'center' });
88
99
  }, [detail, anchor]);
89
100
  const pending = jobs.filter(j => ['queued', 'running'].includes(j.status)).length;
101
+ const knowledgePending = knowledge?.jobs.filter(j => ['queued', 'running'].includes(j.status)).length || 0;
102
+ const draftCount = knowledge?.jobs.filter(j => ['draft', 'grouping'].includes(j.status)).length || 0;
90
103
  useEffect(() => {
91
- if (!pending)
104
+ if (!pending && !knowledgePending)
92
105
  return;
93
106
  const interval = setInterval(refresh, 2500);
94
107
  return () => clearInterval(interval);
95
- }, [pending, refresh]);
108
+ }, [pending, knowledgePending, refresh]);
96
109
  useEffect(() => {
97
110
  if (!toast)
98
111
  return;
@@ -118,8 +131,50 @@ export function App() {
118
131
  setError('剪贴板不可用,请从资料详情手动复制资料 ID');
119
132
  }
120
133
  }
121
- const currentTitle = filter.category || filter.collection || (filter.tag ? `标签 · ${filter.tag}` : filter.kind === 'note' ? '知识笔记' : '全部资料');
122
- return _jsxs("div", { className: "kl-app", children: [_jsxs("main", { className: "kl-workspace", children: [_jsxs("header", { className: "kl-header", children: [_jsx("h1", { children: currentTitle }), _jsxs("div", { className: "kl-header-actions", children: [_jsxs("button", { className: "kl-secondary", onClick: () => setEditor({ mode: 'note' }), children: [_jsx(Icon, { name: "note", size: 16 }), "\u5199\u7B14\u8BB0"] }), _jsxs("button", { className: "kl-primary", onClick: () => setEditor({ mode: 'capture' }), children: [_jsx(Icon, { name: "plus", size: 16 }), "\u6536\u96C6\u8D44\u6599"] })] })] }), _jsxs("div", { className: "kl-navigation", children: [_jsxs("nav", { "aria-label": "\u8D44\u6599\u5BFC\u822A", children: [_jsx("button", { "aria-pressed": !Object.values(filter).some(Boolean), onClick: () => chooseFilter({}), children: "\u5168\u90E8\u8D44\u6599" }), _jsx("button", { "aria-pressed": filter.category === '收件箱', onClick: () => chooseFilter({ category: '收件箱' }), children: "\u6536\u4EF6\u7BB1" }), _jsx("button", { "aria-pressed": filter.kind === 'note', onClick: () => chooseFilter({ kind: 'note' }), children: "\u77E5\u8BC6\u7B14\u8BB0" })] }), _jsxs("button", { className: "kl-directory-toggle", "aria-expanded": showNavigation, "aria-controls": "kl-directory", onClick: () => setShowNavigation(v => !v), children: [_jsx(Icon, { name: "folder", size: 16 }), "\u5206\u7C7B\u4E0E\u4E13\u9898"] })] }), _jsxs("div", { className: "kl-toolbar", children: [_jsxs("label", { className: "kl-search", children: [_jsx(Icon, { name: "search", size: 19 }), _jsx("input", { ref: searchRef, "aria-label": "\u641C\u7D22\u8D44\u6599", placeholder: "\u641C\u7D22\u6B63\u6587\u3001\u6807\u9898\u3001\u6807\u7B7E\u2026", value: query, onChange: e => { setQuery(e.target.value); setLimit(40); } }), _jsx("kbd", { children: "Ctrl K" })] }), _jsxs("button", { "aria-label": pending ? `采集任务,${pending} 项处理中` : '采集任务', className: `kl-job-button ${pending ? 'has-jobs' : ''}`, onClick: () => setShowJobs(true), children: [_jsx(Icon, { name: "clock", size: 17 }), _jsx("span", { children: pending ? `${pending} 项处理中` : '采集任务' }), jobs.some(j => j.status === 'failed') && _jsx("i", {})] }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u8D44\u6599\u5E93\u8BBE\u7F6E", title: "\u8D44\u6599\u5E93\u8BBE\u7F6E", onClick: () => setShowInfo(true), children: _jsx(Icon, { name: "settings", size: 18 }) })] }), !!error && _jsxs("div", { className: "kl-error", role: "alert", children: [_jsx("span", { children: error }), _jsx("button", { "aria-label": "\u5173\u95ED\u9519\u8BEF", onClick: () => setError(''), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-filterbar", children: [_jsxs("span", { children: [_jsx("b", { children: total }), " \u4EFD\u8D44\u6599", query && ` · “${query}”`] }), _jsxs("div", { children: [_jsxs("select", { "aria-label": "\u6309\u7C7B\u578B\u7B5B\u9009", value: filter.kind || '', onChange: e => { setFilter({ ...filter, kind: e.target.value || undefined }); setLimit(40); }, children: [_jsx("option", { value: "", children: "\u6240\u6709\u7C7B\u578B" }), Object.entries(kinds).map(([value, kind]) => _jsx("option", { value: value, children: kind.label }, value))] }), _jsx("span", { className: "kl-sort-label", children: "\u6700\u8FD1\u66F4\u65B0 \u2193" })] })] }), _jsxs("div", { className: `kl-body ${detail || detailLoading ? 'with-detail' : ''}`, children: [showNavigation && _jsxs("aside", { className: "kl-nav", id: "kl-directory", "aria-label": "\u5206\u7C7B\u4E0E\u4E13\u9898", children: [_jsxs("div", { className: "kl-directory-title", children: [_jsx("span", { children: "\u5206\u7C7B\u4E0E\u4E13\u9898" }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u5173\u95ED\u5206\u7C7B\u5BFC\u822A", onClick: () => setShowNavigation(false), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u5206\u7C7B" }), stats?.categories.filter(c => c.name !== '收件箱').map(c => _jsxs("button", { "aria-pressed": filter.category === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ category: c.name }), children: [_jsx(Icon, { name: "folder", size: 16 }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.categories.some(c => c.name !== '收件箱') && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u5206\u7C7B" })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u4E13\u9898" }), stats?.collections.map(c => _jsxs("button", { "aria-pressed": filter.collection === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ collection: c.name }), children: [_jsx("span", { className: "kl-collection-dot" }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.collections.length && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u4E13\u9898" })] }), !!stats?.tags.length && _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u6807\u7B7E" }), _jsx("div", { className: "kl-tag-cloud", children: stats.tags.slice(0, 15).map(tag => _jsxs("button", { "aria-pressed": filter.tag === tag.name, onClick: () => chooseFilter({ tag: tag.name }), children: ["#", tag.name] }, tag.name)) })] }), _jsxs("div", { className: "kl-nav-bottom", children: [_jsx("span", { className: "kl-live-dot" }), "\u4FDD\u5B58\u5728\u8FD9\u53F0\u7535\u8111"] })] }), _jsxs("section", { className: "kl-list", "aria-label": "\u8D44\u6599\u5217\u8868", "aria-busy": loading, children: [loading && !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u6B63\u5728\u8BFB\u53D6\u8D44\u6599\u5E93\u2026"] }) : !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx(Icon, { name: "inbox", size: 28 }), _jsx("h2", { children: query || Object.values(filter).some(Boolean) ? '没有找到匹配的资料' : '还没有资料,收集的内容会显示在这里' }), query && _jsx("p", { children: "\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\uFF0C\u6216\u5207\u6362\u5206\u7C7B\u3002" }), _jsx("button", { className: "kl-secondary", onClick: () => query || Object.values(filter).some(Boolean) ? (setQuery(''), chooseFilter({})) : setEditor({ mode: 'capture' }), children: query || Object.values(filter).some(Boolean) ? '查看全部资料' : '收集第一份资料' })] }) : items.map(item => _jsxs("button", { className: `kl-item ${selected === item.id ? 'selected' : ''}`, onClick: () => { setAnchor(undefined); setSelected(item.id); }, children: [_jsxs("div", { className: "kl-item-top", children: [_jsxs("span", { className: `kl-type kl-type-${item.kind}`, children: [_jsx(Icon, { name: kinds[item.kind].icon, size: 15 }), kinds[item.kind].label] }), _jsx("time", { children: date(item.createdAt) })] }), _jsx("h2", { children: item.title }), _jsx("p", { children: item.excerpt || item.extraction }), _jsxs("div", { className: "kl-item-bottom", children: [_jsxs("span", { className: "kl-category", children: [_jsx(Icon, { name: "folder", size: 13 }), item.category] }), item.tags.slice(0, 2).map(tag => _jsx("span", { className: "kl-chip", children: tag }, tag)), item.status === 'stored' && _jsx("span", { className: "kl-stored", children: "\u4EC5\u5F52\u6863" }), _jsx(Icon, { name: "arrow", size: 15 })] })] }, item.id)), items.length < total && _jsx("button", { className: "kl-load-more", onClick: () => setLimit(v => Math.min(100, v + 40)), disabled: limit >= 100, children: limit >= 100 ? '请缩小搜索范围(最多显示 100 条)' : '加载更多' })] }), selected && _jsxs("section", { className: "kl-detail", "aria-label": "\u8D44\u6599\u8BE6\u60C5", ref: detailRef, children: [_jsxs("div", { className: "kl-detail-toolbar", children: [_jsxs("button", { className: "kl-back", onClick: () => { setSelected(undefined); setDetail(undefined); }, children: [_jsx(Icon, { name: "close", size: 18 }), _jsx("span", { children: "\u5173\u95ED\u9605\u8BFB" })] }), detail && _jsxs("div", { children: [_jsx("button", { title: "\u590D\u5236\u8D44\u6599\u5F15\u7528", "aria-label": "\u590D\u5236\u8D44\u6599\u5F15\u7528", onClick: () => void copy(`library://${detail.id}`), children: _jsx(Icon, { name: "copy", size: 17 }) }), _jsx("a", { title: "\u5BFC\u51FA Markdown", "aria-label": "\u5BFC\u51FA Markdown", href: `${API}/materials/${detail.id}/export`, children: _jsx(Icon, { name: "download", size: 17 }) }), _jsx("button", { onClick: () => setEditor({ mode: 'edit', source: detail }), children: "\u7F16\u8F91\u4FE1\u606F" })] })] }), detailLoading ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u8BFB\u53D6\u4E2D\u2026"] }) : detail && _jsxs("article", { children: [_jsxs("div", { className: "kl-detail-kicker", children: [_jsx("span", { children: kinds[detail.kind].label }), _jsx("span", { children: detail.category })] }), _jsx("h2", { children: detail.title }), _jsxs("div", { className: "kl-source", children: [detail.sourceUrl ? _jsxs("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: [new URL(detail.sourceUrl).hostname, _jsx(Icon, { name: "arrow", size: 12 })] }) : _jsx("span", { children: detail.originalName || '本地收集' }), _jsx("span", { children: new Date(detail.createdAt).toLocaleDateString('zh-CN') }), detail.author && _jsx("span", { children: detail.author })] }), !!detail.tags.length && _jsx("div", { className: "kl-detail-tags", children: detail.tags.map(t => _jsxs("span", { className: "kl-chip", children: ["#", t] }, t)) }), detail.sourceNote && _jsx("aside", { className: "kl-source-note", children: detail.sourceNote }), detail.status === 'stored' && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "file" }), _jsxs("div", { children: [_jsx("strong", { children: "\u539F\u6587\u4EF6\u5DF2\u5B89\u5168\u5F52\u6863" }), _jsx("p", { children: detail.extraction })] })] }), _jsx("div", { className: "kl-content", children: detail.segments.map(segment => _jsxs("div", { className: `kl-segment ${anchor === segment.id ? 'kl-cited' : ''}`, id: segment.id, "data-segment-id": segment.id, children: [_jsxs("div", { className: "kl-segment-line", children: [_jsx("span", { children: segment.startSeconds !== undefined ? time(segment.startSeconds) : segment.id }), _jsx("button", { title: "\u6458\u5F55\u4E3A\u7B14\u8BB0", "aria-label": `摘录 ${segment.id} 为笔记`, onClick: () => setEditor({ mode: 'note', source: detail, segment }), children: _jsx(Icon, { name: "note", size: 14 }) }), _jsx("button", { title: "\u590D\u5236\u6BB5\u843D\u5F15\u7528", "aria-label": `复制 ${segment.id} 引用`, onClick: () => void copy(`library://${detail.id}/${segment.id}`), children: _jsx(Icon, { name: "copy", size: 14 }) })] }), /^#{1,6}\s/.test(segment.text) ? _jsx("h3", { children: segment.text.replace(/^#{1,6}\s+/, '') }) : _jsx("p", { children: segment.text })] }, segment.id)) }), !!detail.citations.length && _jsxs("div", { className: "kl-references", children: [_jsx("h3", { children: "\u6765\u6E90\u5F15\u7528" }), detail.citations.map((c, i) => _jsxs("button", { "aria-label": `查看引用 ${i + 1}`, onClick: () => { setAnchor(c.segmentId); setSelected(c.materialId); }, children: [_jsx(Icon, { name: "file", size: 15 }), _jsxs("span", { children: [c.materialId.slice(0, 15), "\u2026", c.segmentId ? ` / ${c.segmentId}` : ''] }), _jsx(Icon, { name: "arrow", size: 14 })] }, i))] }), _jsxs("footer", { className: "kl-detail-footer", children: [_jsxs("div", { children: [_jsx("span", { className: "kl-live-dot" }), detail.extraction] }), detail.originalFile && _jsxs("a", { href: `${API}/materials/${detail.id}/original`, children: ["\u4E0B\u8F7D\u539F\u59CB\u6587\u4EF6", _jsx(Icon, { name: "download", size: 14 })] }), _jsx("code", { children: detail.id })] })] })] })] })] }), editor && _jsx(EditorDialog, { editor: editor, categories: stats?.categories.map(c => c.name) || [], onClose: () => setEditor(undefined), onSaved: result => {
134
+ function askDelete(item) {
135
+ if (stats)
136
+ setDeletion({ id: item.id, title: item.title, root: stats.root });
137
+ }
138
+ function afterRemoval(id, message) {
139
+ if (selected === id) {
140
+ setSelected(undefined);
141
+ setDetail(undefined);
142
+ setAnchor(undefined);
143
+ }
144
+ refresh();
145
+ notify(message);
146
+ searchRef.current?.focus();
147
+ }
148
+ async function restoreItem(id) {
149
+ if (!stats || restoring)
150
+ return;
151
+ setRestoring(id);
152
+ try {
153
+ await post(`/materials/${id}/restore`, { expectedRoot: stats.root });
154
+ afterRemoval(id, '资料已恢复到原分类');
155
+ }
156
+ catch (e) {
157
+ setError(e.message);
158
+ }
159
+ finally {
160
+ setRestoring(undefined);
161
+ }
162
+ }
163
+ function openKnowledgeSource(id, segmentId) {
164
+ setKnowledgeOpen(undefined);
165
+ chooseFilter({});
166
+ setQuery('');
167
+ setSelected(id);
168
+ setAnchor(segmentId);
169
+ refresh();
170
+ }
171
+ function generateFor(ids, mode) {
172
+ if (stats)
173
+ setGeneration({ root: stats.root, sourceIds: ids, mode });
174
+ }
175
+ const currentTitle = filter.trashed ? '回收站' : filter.category || filter.collection || (filter.tag ? `标签 · ${filter.tag}` : filter.kind === 'note' ? '知识笔记' : '全部资料');
176
+ return _jsxs("div", { className: "kl-app", children: [_jsxs("main", { className: "kl-workspace", children: [_jsxs("header", { className: "kl-header", children: [_jsx("h1", { children: currentTitle }), _jsxs("div", { className: "kl-header-actions", children: [_jsxs("button", { className: "kl-secondary", onClick: () => setEditor({ mode: 'note' }), children: [_jsx(Icon, { name: "note", size: 16 }), "\u5199\u7B14\u8BB0"] }), _jsxs("button", { className: "kl-primary", onClick: () => setEditor({ mode: 'capture' }), children: [_jsx(Icon, { name: "plus", size: 16 }), "\u6536\u96C6\u8D44\u6599"] })] })] }), _jsxs("div", { className: "kl-navigation", children: [_jsxs("nav", { "aria-label": "\u8D44\u6599\u5BFC\u822A", children: [_jsx("button", { "aria-pressed": !Object.values(filter).some(Boolean), onClick: () => chooseFilter({}), children: "\u5168\u90E8\u8D44\u6599" }), _jsx("button", { "aria-pressed": filter.category === '收件箱', onClick: () => chooseFilter({ category: '收件箱' }), children: "\u6536\u4EF6\u7BB1" }), _jsx("button", { "aria-pressed": filter.kind === 'note', onClick: () => chooseFilter({ kind: 'note' }), children: "\u77E5\u8BC6\u7B14\u8BB0" }), _jsx("button", { "aria-pressed": filter.trashed === '1', onClick: () => chooseFilter({ trashed: '1' }), children: "\u56DE\u6536\u7AD9" })] }), _jsxs("button", { className: "kl-directory-toggle", "aria-expanded": showNavigation, "aria-controls": "kl-directory", onClick: () => setShowNavigation(v => !v), children: [_jsx(Icon, { name: "folder", size: 16 }), "\u5206\u7C7B\u4E0E\u4E13\u9898"] })] }), _jsxs("div", { className: "kl-toolbar", children: [_jsxs("label", { className: "kl-search", children: [_jsx(Icon, { name: "search", size: 19 }), _jsx("input", { ref: searchRef, "aria-label": "\u641C\u7D22\u8D44\u6599", placeholder: "\u641C\u7D22\u6B63\u6587\u3001\u6807\u9898\u3001\u6807\u7B7E\u2026", value: query, onChange: e => { setQuery(e.target.value); setLimit(40); } }), _jsx("kbd", { children: "Ctrl K" })] }), _jsxs("button", { "aria-label": pending ? `采集任务,${pending} 项处理中` : '采集任务', className: `kl-job-button ${pending ? 'has-jobs' : ''}`, onClick: () => setShowJobs(true), children: [_jsx(Icon, { name: "clock", size: 17 }), _jsx("span", { children: pending ? `${pending} 项处理中` : '采集任务' }), jobs.some(j => j.status === 'failed') && _jsx("i", {})] }), _jsxs("button", { "aria-label": "\u77E5\u8BC6\u6574\u7406", title: "\u77E5\u8BC6\u6574\u7406", className: `kl-job-button ${knowledgePending || draftCount ? 'has-jobs' : ''}`, onClick: () => { refresh(); if (stats)
177
+ setKnowledgeOpen({ root: stats.root }); }, children: [_jsx(Icon, { name: "note", size: 17 }), _jsx("span", { children: draftCount ? `${draftCount} 份待确认` : '知识整理' }), !!knowledgePending && _jsx("span", { className: "kl-spinner" })] }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u8D44\u6599\u5E93\u8BBE\u7F6E", title: "\u8D44\u6599\u5E93\u8BBE\u7F6E", onClick: () => { setSettingsFromJobs(false); setShowInfo(true); }, children: _jsx(Icon, { name: "settings", size: 18 }) })] }), !!error && _jsxs("div", { className: "kl-error", role: "alert", children: [_jsx("span", { children: error }), _jsx("button", { "aria-label": "\u5173\u95ED\u9519\u8BEF", onClick: () => setError(''), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-filterbar", children: [_jsxs("span", { children: [_jsx("b", { children: total }), " \u4EFD\u8D44\u6599", query && ` · “${query}”`] }), _jsxs("div", { children: [_jsxs("select", { "aria-label": "\u6309\u7C7B\u578B\u7B5B\u9009", value: filter.kind || '', onChange: e => { setFilter({ ...filter, kind: e.target.value || undefined }); setLimit(40); }, children: [_jsx("option", { value: "", children: "\u6240\u6709\u7C7B\u578B" }), Object.entries(kinds).map(([value, kind]) => _jsx("option", { value: value, children: kind.label }, value))] }), _jsx("span", { className: "kl-sort-label", children: "\u6700\u8FD1\u66F4\u65B0 \u2193" })] })] }), filter.trashed && _jsxs("div", { className: "kl-trash-toolbar", children: [_jsx("p", { children: stats?.canEmptyTrash ? (stats.purgePending ? `${stats.purgePending} 份资料的文件清理未完成,可再次清空以重试。` : '清空将永久删除整个回收站,不受当前搜索和类型筛选限制。') : '请重启资料库服务以启用清空回收站。' }), _jsxs("button", { className: "kl-secondary kl-danger", disabled: !stats?.canEmptyTrash || !(stats.trashed || stats.purgePending) || !!restoring, onClick: () => stats && setEmptyRoot(stats.root), children: [_jsx(Icon, { name: "trash", size: 15 }), "\u6E05\u7A7A\u56DE\u6536\u7AD9"] })] }), !filter.trashed && filter.kind !== 'note' && _jsxs("div", { className: "kl-knowledge-toolbar", children: [_jsx("p", { children: batchMode ? `已选择 ${checkedIds.length} / 10 份资料;切换搜索或分类会清除选择。` : knowledge?.smartSupported ? '零散资料先智能分组,再按组形成知识笔记。' : '请重启资料库服务以启用智能分组。' }), _jsxs("div", { children: [batchMode && _jsx("button", { className: "kl-primary", disabled: !checkedIds.length || !knowledge?.smartSupported, onClick: () => generateFor(checkedIds, 'smart'), children: "\u667A\u80FD\u5206\u6790\u5206\u7EC4" }), _jsx("button", { className: "kl-secondary", disabled: !knowledge?.smartSupported, onClick: () => { setBatchMode(!batchMode); setCheckedIds([]); }, children: batchMode ? '取消选择' : '智能整理' })] })] }), _jsxs("div", { className: `kl-body ${detail || detailLoading ? 'with-detail' : ''}`, children: [showNavigation && _jsxs("aside", { className: "kl-nav", id: "kl-directory", "aria-label": "\u5206\u7C7B\u4E0E\u4E13\u9898", children: [_jsxs("div", { className: "kl-directory-title", children: [_jsx("span", { children: "\u5206\u7C7B\u4E0E\u4E13\u9898" }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u5173\u95ED\u5206\u7C7B\u5BFC\u822A", onClick: () => setShowNavigation(false), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u5206\u7C7B" }), stats?.categories.filter(c => c.name !== '收件箱').map(c => _jsxs("button", { "aria-pressed": filter.category === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ category: c.name }), children: [_jsx(Icon, { name: "folder", size: 16 }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.categories.some(c => c.name !== '收件箱') && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u5206\u7C7B" })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u4E13\u9898" }), stats?.collections.map(c => _jsxs("button", { "aria-pressed": filter.collection === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ collection: c.name }), children: [_jsx("span", { className: "kl-collection-dot" }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.collections.length && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u4E13\u9898" })] }), !!stats?.tags.length && _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u6807\u7B7E" }), _jsx("div", { className: "kl-tag-cloud", children: stats.tags.slice(0, 15).map(tag => _jsxs("button", { "aria-pressed": filter.tag === tag.name, onClick: () => chooseFilter({ tag: tag.name }), children: ["#", tag.name] }, tag.name)) })] }), _jsxs("div", { className: "kl-nav-bottom", children: [_jsx("span", { className: "kl-live-dot" }), "\u4FDD\u5B58\u5728\u8FD9\u53F0\u7535\u8111"] })] }), _jsxs("section", { className: "kl-list", "aria-label": "\u8D44\u6599\u5217\u8868", "aria-busy": loading, children: [loading && !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u6B63\u5728\u8BFB\u53D6\u8D44\u6599\u5E93\u2026"] }) : !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx(Icon, { name: filter.trashed ? 'trash' : 'inbox', size: 28 }), _jsx("h2", { children: filter.trashed ? '回收站中没有匹配的资料' : query || Object.values(filter).some(Boolean) ? '没有找到匹配的资料' : '还没有资料,收集的内容会显示在这里' }), query && _jsx("p", { children: "\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\uFF0C\u6216\u5207\u6362\u5206\u7C7B\u3002" }), _jsx("button", { className: "kl-secondary", onClick: () => query || Object.values(filter).some(Boolean) ? (setQuery(''), chooseFilter({})) : setEditor({ mode: 'capture' }), children: query || Object.values(filter).some(Boolean) ? '查看全部资料' : '收集第一份资料' })] }) : items.map(item => _jsxs("div", { className: `kl-item-row ${batchMode ? 'kl-selectable' : ''}`, children: [batchMode && _jsx("input", { type: "checkbox", className: "kl-material-checkbox", "aria-label": `选择 ${item.title}`, checked: checkedIds.includes(item.id), disabled: item.kind === 'note' || item.status !== 'ready' || (!!checkedIds.length && checkedIds.length >= 10 && !checkedIds.includes(item.id)), onChange: e => setCheckedIds(ids => e.target.checked ? [...ids, item.id] : ids.filter(id => id !== item.id)) }), _jsxs("button", { className: `kl-item ${selected === item.id ? 'selected' : ''}`, onClick: () => { setAnchor(undefined); setSelected(item.id); }, children: [_jsxs("div", { className: "kl-item-top", children: [_jsxs("span", { className: `kl-type kl-type-${item.kind}`, children: [_jsx(Icon, { name: kinds[item.kind].icon, size: 15 }), kinds[item.kind].label] }), _jsx("time", { children: date(item.createdAt) })] }), _jsx("h2", { children: item.title }), _jsx("p", { children: item.excerpt || item.extraction }), _jsxs("div", { className: "kl-item-bottom", children: [_jsxs("span", { className: "kl-category", children: [_jsx(Icon, { name: "folder", size: 13 }), item.category] }), item.tags.slice(0, 2).map(tag => _jsx("span", { className: "kl-chip", children: tag }, tag)), item.status === 'stored' && _jsx("span", { className: "kl-stored", children: "\u4EC5\u5F52\u6863" }), !item.deletedAt && item.kind !== 'note' && item.status === 'ready' && knowledge?.supported && _jsx(KnowledgeBadge, { state: knowledge.states[item.id] }), _jsx(Icon, { name: "arrow", size: 15 })] })] }), _jsx("button", { className: `kl-icon-button kl-row-action ${item.deletedAt ? '' : 'kl-danger'}`, title: item.deletedAt ? '恢复资料' : '删除资料', "aria-label": `${item.deletedAt ? '恢复' : '删除'} ${item.title}`, disabled: !stats || !!restoring, onClick: () => item.deletedAt ? void restoreItem(item.id) : askDelete(item), children: _jsx(Icon, { name: item.deletedAt ? 'restore' : 'trash', size: 16 }) })] }, item.id)), items.length < total && _jsx("button", { className: "kl-load-more", onClick: () => setLimit(v => Math.min(100, v + 40)), disabled: limit >= 100, children: limit >= 100 ? '请缩小搜索范围(最多显示 100 条)' : '加载更多' })] }), selected && _jsxs("section", { className: "kl-detail", "aria-label": "\u8D44\u6599\u8BE6\u60C5", ref: detailRef, children: [_jsxs("div", { className: "kl-detail-toolbar", children: [_jsxs("button", { className: "kl-back", onClick: () => { setSelected(undefined); setDetail(undefined); }, children: [_jsx(Icon, { name: "close", size: 18 }), _jsx("span", { children: "\u5173\u95ED\u9605\u8BFB" })] }), detail && _jsxs("div", { children: [_jsx("button", { title: "\u590D\u5236\u8D44\u6599\u5F15\u7528", "aria-label": "\u590D\u5236\u8D44\u6599\u5F15\u7528", onClick: () => void copy(`library://${detail.id}`), children: _jsx(Icon, { name: "copy", size: 17 }) }), _jsx("a", { title: "\u5BFC\u51FA Markdown", "aria-label": "\u5BFC\u51FA Markdown", href: `${API}/materials/${detail.id}/export`, children: _jsx(Icon, { name: "download", size: 17 }) }), detail.deletedAt ? _jsxs("button", { disabled: !stats || !!restoring, onClick: () => void restoreItem(detail.id), children: [_jsx(Icon, { name: "restore", size: 16 }), "\u6062\u590D\u8D44\u6599"] }) : _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setEditor({ mode: 'edit', source: detail }), children: "\u7F16\u8F91\u4FE1\u606F" }), _jsx("button", { className: "kl-danger", title: "\u5220\u9664\u8D44\u6599", "aria-label": "\u5220\u9664\u8D44\u6599", disabled: !stats || !!restoring, onClick: () => askDelete(detail), children: _jsx(Icon, { name: "trash", size: 17 }) })] })] })] }), detailLoading ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u8BFB\u53D6\u4E2D\u2026"] }) : detail && _jsxs("article", { children: [_jsxs("div", { className: "kl-detail-kicker", children: [_jsx("span", { children: kinds[detail.kind].label }), _jsx("span", { children: detail.category })] }), _jsx("h2", { children: detail.title }), _jsxs("div", { className: "kl-source", children: [detail.sourceUrl ? _jsxs("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: [new URL(detail.sourceUrl).hostname, _jsx(Icon, { name: "arrow", size: 12 })] }) : _jsx("span", { children: detail.originalName || '本地收集' }), _jsx("span", { children: new Date(detail.createdAt).toLocaleDateString('zh-CN') }), detail.author && _jsx("span", { children: detail.author })] }), !!detail.tags.length && _jsx("div", { className: "kl-detail-tags", children: detail.tags.map(t => _jsxs("span", { className: "kl-chip", children: ["#", t] }, t)) }), detail.sourceNote && _jsx("aside", { className: "kl-source-note", children: detail.sourceNote }), detail.deletedAt && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "trash" }), _jsxs("div", { children: [_jsx("strong", { children: "\u8FD9\u4EFD\u8D44\u6599\u5728\u56DE\u6536\u7AD9" }), _jsx("p", { children: "\u539F\u6587\u4E0E\u6765\u6E90\u5F15\u7528\u4ECD\u53EF\u67E5\u770B\uFF0C\u6062\u590D\u540E\u56DE\u5230\u539F\u5206\u7C7B\u3002" })] })] }), detail.status === 'stored' && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "file" }), _jsxs("div", { children: [_jsx("strong", { children: "\u539F\u6587\u4EF6\u5DF2\u5B89\u5168\u5F52\u6863" }), _jsx("p", { children: detail.extraction })] })] }), !detail.deletedAt && detail.kind !== 'note' && detail.status === 'ready' && _jsxs("div", { className: "kl-knowledge-source", children: [_jsxs("div", { children: [_jsx(KnowledgeBadge, { state: knowledge?.states[detail.id] }), _jsx("span", { children: "\u4ECE\u8FD9\u4EFD\u8D44\u6599\u5F62\u6210\u7406\u89E3\u3001\u65B9\u6CD5\u4E0E\u5F85\u9A8C\u8BC1\u7684\u95EE\u9898\u3002" })] }), _jsxs("div", { className: "kl-knowledge-actions", children: [['running', 'draft', 'grouping'].includes(knowledge?.states[detail.id]?.status || '') ? _jsx("button", { className: "kl-secondary", onClick: () => stats && setKnowledgeOpen({ root: stats.root, jobId: knowledge?.states[detail.id]?.jobId }), children: knowledge?.states[detail.id]?.status === 'grouping' ? '查看分组方案' : '查看整理任务' }) : _jsx("button", { className: "kl-secondary", disabled: !knowledge?.supported || imagesPending(detail), onClick: () => generateFor([detail.id]), children: "\u751F\u6210\u77E5\u8BC6\u7B14\u8BB0" }), knowledge?.states[detail.id]?.noteIds.map(id => _jsxs("button", { className: "kl-secondary", onClick: () => openKnowledgeSource(id), children: ["\u67E5\u770B\u77E5\u8BC6\u7B14\u8BB0", _jsx(Icon, { name: "arrow", size: 14 })] }, id))] }), !knowledge?.supported && _jsx("p", { className: "kl-muted", children: "\u8BF7\u91CD\u542F\u8D44\u6599\u5E93\u670D\u52A1\u4EE5\u542F\u7528\u77E5\u8BC6\u6574\u7406\u3002" })] }), _jsx(PageImages, { detail: detail, onRetry: () => stats && setImageRetry({ id: detail.id, root: stats.root }), onJump: setAnchor }), imagesPending(detail) && _jsx("p", { className: "kl-image-policy", children: "\u6B63\u6587\u5DF2\u4FDD\u5B58\uFF0C\u56FE\u7247\u4ECD\u5728\u5904\u7406\u4E2D\u3002\u56FE\u7247\u5904\u7406\u7ED3\u675F\u6216\u505C\u6B62\u540E\u53EF\u751F\u6210\u77E5\u8BC6\u7B14\u8BB0\u3002" }), _jsx("div", { className: "kl-content", children: detail.segments.map(segment => _jsxs("div", { className: `kl-segment ${anchor === segment.id ? 'kl-cited' : ''}`, id: segment.id, "data-segment-id": segment.id, children: [_jsxs("div", { className: "kl-segment-line", children: [_jsx("span", { children: segment.startSeconds !== undefined ? time(segment.startSeconds) : segment.id }), _jsx("button", { title: "\u6458\u5F55\u4E3A\u7B14\u8BB0", "aria-label": `摘录 ${segment.id} 为笔记`, onClick: () => setEditor({ mode: 'note', source: detail, segment }), children: _jsx(Icon, { name: "note", size: 14 }) }), _jsx("button", { title: "\u590D\u5236\u6BB5\u843D\u5F15\u7528", "aria-label": `复制 ${segment.id} 引用`, onClick: () => void copy(`library://${detail.id}/${segment.id}`), children: _jsx(Icon, { name: "copy", size: 14 }) })] }), /^#{1,6}\s/.test(segment.text) ? _jsx("h3", { children: segment.text.replace(/^#{1,6}\s+/, '') }) : _jsx("p", { children: segment.text })] }, segment.id)) }), !!detail.citations.length && _jsxs("div", { className: "kl-references", children: [_jsx("h3", { children: "\u6765\u6E90\u5F15\u7528" }), detail.citations.map((c, i) => _jsxs("button", { disabled: detail.missingSources?.includes(c.materialId), "aria-label": `查看引用 ${i + 1}`, onClick: () => { setAnchor(c.segmentId); setSelected(c.materialId); }, children: [_jsx(Icon, { name: "file", size: 15 }), _jsx("span", { children: detail.missingSources?.includes(c.materialId) ? '来源已永久删除' : `${c.materialId.slice(0, 15)}…${c.segmentId ? ` / ${c.segmentId}` : ''}` }), _jsx(Icon, { name: "arrow", size: 14 })] }, i))] }), _jsxs("footer", { className: "kl-detail-footer", children: [_jsxs("div", { children: [_jsx("span", { className: "kl-live-dot" }), detail.extraction] }), detail.originalFile && _jsxs("a", { href: `${API}/materials/${detail.id}/original`, children: ["\u4E0B\u8F7D\u539F\u59CB\u6587\u4EF6", _jsx(Icon, { name: "download", size: 14 })] }), _jsx("code", { children: detail.id })] })] })] })] })] }), generation && knowledge && _jsx(StartKnowledgeDialog, { root: generation.root, sourceIds: generation.sourceIds, model: knowledge, initialMode: generation.mode, onClose: () => setGeneration(undefined), onCreated: job => { setGeneration(undefined); setCheckedIds([]); setBatchMode(false); setKnowledgeOpen({ root: generation.root, jobId: job.id }); refresh(); } }), knowledgeOpen && knowledge && _jsx(KnowledgeJobsDialog, { root: knowledgeOpen.root, jobId: knowledgeOpen.jobId, overview: knowledge, onClose: () => setKnowledgeOpen(undefined), onRefresh: refresh, onRegenerate: (ids, mode) => { setKnowledgeOpen(undefined); generateFor(ids, mode); }, onOpenSource: openKnowledgeSource, onSaved: id => { openKnowledgeSource(id); notify('知识笔记已保存,原始资料已保留'); }, onNotify: notify }), editor && _jsx(EditorDialog, { editor: editor, categories: stats?.categories.map(c => c.name) || [], onClose: () => setEditor(undefined), onSaved: result => {
123
178
  setEditor(undefined);
124
179
  refresh();
125
180
  if (result.material) {
@@ -136,42 +191,186 @@ export function App() {
136
191
  setShowJobs(true);
137
192
  notify('网页已加入采集队列');
138
193
  }
139
- } }), showJobs && _jsx(Dialog, { title: "\u91C7\u96C6\u4EFB\u52A1", description: "\u7F51\u9875\u5728\u540E\u53F0\u63D0\u53D6\u6B63\u6587\u3002\u5173\u95ED\u5E94\u7528\u540E\uFF0C\u4E0B\u6B21\u542F\u52A8\u4F1A\u6062\u590D\u672A\u5B8C\u6210\u4EFB\u52A1\u3002", onClose: () => setShowJobs(false), children: _jsx("div", { className: "kl-jobs", children: !jobs.length ? _jsx("p", { className: "kl-muted", children: "\u8FD8\u6CA1\u6709\u91C7\u96C6\u4EFB\u52A1\u3002\u6DFB\u52A0\u4E00\u4E2A\u7F51\u9875\u94FE\u63A5\u5F00\u59CB\u3002" }) : jobs.map(job => _jsxs("div", { className: "kl-job", children: [_jsxs("div", { children: [_jsx("span", { className: `kl-job-status ${job.status}`, children: ({ queued: '排队中', running: '提取中', completed: '已完成', failed: '失败' })[job.status] }), _jsx("time", { children: date(job.createdAt) })] }), _jsx("strong", { children: job.url }), job.error && _jsx("p", { role: job.status === 'failed' ? 'alert' : undefined, children: job.error }), _jsxs("div", { children: [job.materialId && _jsxs("button", { onClick: () => { setSelected(job.materialId); setShowJobs(false); refresh(); }, children: ["\u67E5\u770B\u8D44\u6599 ", _jsx(Icon, { name: "arrow", size: 14 })] }), job.status === 'failed' && _jsx("button", { onClick: () => void post(`/jobs/${job.id}/retry`, {}).then(refresh).catch(e => setError(e.message)), children: "\u91CD\u65B0\u91C7\u96C6" })] })] }, job.id)) }) }), showInfo && _jsx(Dialog, { title: "\u4F60\u7684\u8D44\u6599\uFF0C\u7531\u4F60\u4FDD\u7BA1", description: "\u5185\u5BB9\u4EE5\u666E\u901A\u6587\u4EF6\u4FDD\u5B58\uFF0C\u7D22\u5F15\u53EF\u4EE5\u91CD\u5EFA\u3002", onClose: () => setShowInfo(false), children: _jsxs("div", { className: "kl-info", children: [_jsx("label", { children: "\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsx("code", { children: stats?.root }), _jsx("p", { children: "\u6BCF\u4EFD\u8D44\u6599\u4FDD\u7559\u539F\u59CB\u6587\u4EF6\u3001\u6B63\u6587\u3001\u5143\u6570\u636E\u548C\u5F15\u7528\u6BB5\u843D\u3002\u5206\u7C7B\u5BF9\u5E94\u672C\u5730\u6587\u4EF6\u5939\uFF0C\u63D2\u4EF6\u5378\u8F7D\u4E0D\u4F1A\u5220\u9664\u8FD9\u4E9B\u6587\u4EF6\u3002" }), _jsx("p", { children: "\u672C\u7248\u672C\u53EF\u63D0\u53D6\u6587\u7AE0\u6B63\u6587\u53CA\u5B57\u5E55\uFF1B\u97F3\u89C6\u9891\u3001PDF \u7B49\u683C\u5F0F\u53EF\u4EE5\u5F52\u6863\uFF0C\u81EA\u52A8\u8F6C\u5199\u548C OCR \u5C1A\u672A\u63A5\u5165\u3002" }), stats?.warnings.map(w => _jsx("p", { role: "alert", children: w }, w)), _jsx("button", { className: "kl-secondary", onClick: () => void post('/rebuild', {}).then(() => { refresh(); notify('索引已重建'); }).catch(e => setError(e.message)), children: "\u4ECE\u8D44\u6599\u6587\u4EF6\u91CD\u5EFA\u7D22\u5F15" })] }) }), toast && _jsxs("div", { className: "kl-toast", role: "status", children: [_jsx(Icon, { name: "check", size: 17 }), toast] })] });
194
+ } }), deletion && _jsx(DeleteDialog, { item: deletion, onClose: () => setDeletion(undefined), onDeleted: () => { setDeletion(undefined); afterRemoval(deletion.id, '资料已移入回收站,可随时恢复'); } }), emptyRoot && _jsx(EmptyTrashDialog, { root: emptyRoot, onClose: () => setEmptyRoot(undefined), onEmptied: result => {
195
+ setEmptyRoot(undefined);
196
+ setSelected(undefined);
197
+ setDetail(undefined);
198
+ setAnchor(undefined);
199
+ setError('');
200
+ refresh();
201
+ if (result.errors.length || result.pending)
202
+ setError(`已永久删除 ${result.deleted} 份资料;${result.pending} 份文件待清理。${result.errors.join(';')}`);
203
+ else
204
+ notify(`回收站已清空,永久删除 ${result.deleted} 份资料`);
205
+ searchRef.current?.focus();
206
+ } }), imageRetry && _jsx(ImageRetryDialog, { ...imageRetry, onClose: () => setImageRetry(undefined), onStarted: () => { setImageRetry(undefined); refresh(); notify('失败图片已加入重试队列'); } }), showJobs && _jsx(Dialog, { title: "\u91C7\u96C6\u4EFB\u52A1", description: "\u7F51\u9875\u5728\u540E\u53F0\u63D0\u53D6\u6B63\u6587\u548C\u5DF2\u542F\u7528\u7684\u56FE\u7247\u3002\u56FE\u7247\u8BC6\u522B\u4E2D\u65AD\u540E\u9700\u4E3B\u52A8\u91CD\u8BD5\uFF0C\u4E0D\u4F1A\u5728\u91CD\u542F\u65F6\u81EA\u52A8\u91CD\u590D\u6A21\u578B\u8BF7\u6C42\u3002", onClose: () => setShowJobs(false), children: _jsx("div", { className: "kl-jobs", children: !jobs.length ? _jsx("p", { className: "kl-muted", children: "\u8FD8\u6CA1\u6709\u91C7\u96C6\u4EFB\u52A1\u3002\u6DFB\u52A0\u4E00\u4E2A\u7F51\u9875\u94FE\u63A5\u5F00\u59CB\u3002" }) : jobs.map(job => _jsxs("div", { className: "kl-job", children: [_jsxs("div", { children: [_jsx("span", { className: `kl-job-status ${job.status}`, children: job.status === 'failed' && job.errorCode === 'PAGE_REQUIRES_BROWSER' ? '需浏览器验证' : ({ queued: '排队中', running: '提取中', completed: '已完成', failed: '失败' })[job.status] }), _jsx("time", { children: date(job.createdAt) })] }), _jsx("strong", { children: job.url }), job.progress && _jsx("p", { children: job.progress }), job.imageSummary && _jsxs("p", { children: ["\u56FE\u7247\uFF1A", job.imageSummary.completed, " \u5F20\u5DF2\u8BC6\u522B \u00B7 ", job.imageSummary.failed, " \u5F20\u672A\u5B8C\u6210 \u00B7 ", job.imageSummary.skipped, " \u5F20\u8DF3\u8FC7"] }), job.error && _jsx("p", { role: job.status === 'failed' ? 'alert' : undefined, children: job.error }), _jsxs("div", { className: "kl-job-actions", children: [job.materialId && _jsxs("button", { onClick: () => { setSelected(job.materialId); setShowJobs(false); refresh(); }, children: ["\u67E5\u770B\u8D44\u6599 ", _jsx(Icon, { name: "arrow", size: 14 })] }), job.materialId && job.imageCapture && ['queued', 'running'].includes(job.status) && _jsx("button", { onClick: () => void post(`/jobs/${job.id}/images/cancel`, { expectedRoot: stats?.root }).then(refresh).catch(e => setError(e.message)), children: "\u505C\u6B62\u56FE\u7247\u5904\u7406" }), job.materialId && job.status === 'completed' && !!job.imageSummary?.failed && _jsx("button", { onClick: () => { if (stats) {
207
+ setShowJobs(false);
208
+ setImageRetry({ id: job.materialId, root: stats.root });
209
+ } }, children: "\u91CD\u8BD5\u5931\u8D25\u56FE\u7247" }), job.status === 'failed' && _jsxs(_Fragment, { children: [/^https?:\/\//i.test(job.url) && _jsxs("a", { href: job.url, target: "_blank", rel: "noopener noreferrer", children: ["\u6253\u5F00\u539F\u7F51\u9875", _jsx(Icon, { name: "arrow", size: 14 })] }), _jsx("button", { onClick: () => { setShowJobs(false); setEditor({ mode: 'capture', initialType: 'text', sourceUrl: job.url }); }, children: "\u7C98\u8D34\u6B63\u6587" }), /fake-ip/i.test(job.error || '') && _jsx("button", { onClick: () => { setShowJobs(false); setSettingsFromJobs(true); setShowInfo(true); }, children: "\u4EE3\u7406\u517C\u5BB9\u8BBE\u7F6E" }), _jsx("button", { onClick: () => void post(`/jobs/${job.id}/retry`, {}).then(refresh).catch(e => setError(e.message)), children: "\u91CD\u65B0\u91C7\u96C6" })] })] })] }, job.id)) }) }), showInfo && _jsx(SettingsDialog, { root: stats?.root || '', warnings: stats?.warnings || [], onClose: () => { setShowInfo(false); if (settingsFromJobs)
210
+ setShowJobs(true); setSettingsFromJobs(false); }, onChanged: () => {
211
+ setSelected(undefined);
212
+ setDetail(undefined);
213
+ setAnchor(undefined);
214
+ setItems([]);
215
+ setQuery('');
216
+ chooseFilter({});
217
+ refresh();
218
+ notify('资料库目录已切换');
219
+ }, onRebuilt: () => { refresh(); notify('索引已重建'); }, onProxySaved: enabled => { refresh(); notify(enabled ? '代理兼容已开启,可重新采集失败任务' : '代理兼容已关闭'); if (settingsFromJobs) {
220
+ setShowInfo(false);
221
+ setShowJobs(true);
222
+ setSettingsFromJobs(false);
223
+ } } }), toast && _jsxs("div", { className: "kl-toast", role: "status", children: [_jsx(Icon, { name: "check", size: 17 }), toast] })] });
140
224
  }
141
- function Dialog({ title, description, onClose, children }) {
142
- const ref = useRef(null);
143
- const closeRef = useRef(onClose);
144
- closeRef.current = onClose;
145
- useEffect(() => {
146
- const previous = document.activeElement;
147
- const timer = setTimeout(() => ref.current?.querySelector('input,textarea,button')?.focus(), 0);
148
- const key = (event) => {
149
- if (event.key === 'Escape')
150
- closeRef.current();
151
- if (event.key === 'Tab') {
152
- const elements = Array.from(ref.current?.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select,a[href]') || []);
153
- const first = elements[0], last = elements[elements.length - 1];
154
- if (event.shiftKey && document.activeElement === first) {
155
- event.preventDefault();
156
- last?.focus();
157
- }
158
- if (!event.shiftKey && document.activeElement === last) {
159
- event.preventDefault();
160
- first?.focus();
161
- }
162
- }
163
- };
164
- document.addEventListener('keydown', key);
165
- return () => { clearTimeout(timer); document.removeEventListener('keydown', key); previous?.focus(); };
166
- }, []);
167
- return _jsx("div", { className: "kl-modal-backdrop", onMouseDown: event => { if (event.target === event.currentTarget)
168
- onClose(); }, children: _jsxs("div", { className: "kl-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, ref: ref, children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), _jsx("button", { "aria-label": "\u5173\u95ED\u5F39\u7A97", onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }), children] }) });
225
+ function DeleteDialog({ item, onClose, onDeleted }) {
226
+ const [busy, setBusy] = useState(false);
227
+ const [error, setError] = useState('');
228
+ async function remove() {
229
+ setBusy(true);
230
+ setError('');
231
+ try {
232
+ await post(`/materials/${item.id}`, { expectedRoot: item.root }, 'DELETE');
233
+ onDeleted();
234
+ }
235
+ catch (e) {
236
+ setError(e.message);
237
+ }
238
+ finally {
239
+ setBusy(false);
240
+ }
241
+ }
242
+ return _jsx(Dialog, { title: "\u5220\u9664\u8D44\u6599", description: "\u8D44\u6599\u5C06\u79FB\u5165\u56DE\u6536\u7AD9\uFF0C\u539F\u6587\u4EF6\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u6062\u590D\u3002", onClose: () => { if (!busy)
243
+ onClose(); }, children: _jsxs("div", { className: "kl-delete-dialog", children: [_jsx("p", { className: "kl-delete-title", children: item.title }), _jsx("p", { className: "kl-muted", children: "\u5DF2\u6709\u7B14\u8BB0\u4ECD\u80FD\u67E5\u770B\u8FD9\u4EFD\u8D44\u6599\u7684\u6765\u6E90\u5F15\u7528\u3002" }), error && _jsx("p", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsx("button", { className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-secondary kl-danger", disabled: busy, onClick: () => void remove(), children: busy ? '正在删除…' : '移入回收站' })] })] }) });
244
+ }
245
+ function EmptyTrashDialog({ root, onClose, onEmptied }) {
246
+ const [snapshot, setSnapshot] = useState();
247
+ const [busy, setBusy] = useState(false);
248
+ const [error, setError] = useState('');
249
+ useEffect(() => { const abort = new AbortController(); void api('/trash', { signal: abort.signal }).then(setSnapshot).catch(e => { if (!abort.signal.aborted)
250
+ setError(e.message); }); return () => abort.abort(); }, []);
251
+ async function empty() {
252
+ if (!snapshot || busy)
253
+ return;
254
+ setBusy(true);
255
+ setError('');
256
+ try {
257
+ onEmptied(await post('/trash/empty', { expectedRoot: root, confirmed: true, token: snapshot.token }));
258
+ }
259
+ catch (e) {
260
+ setError(e.message);
261
+ setSnapshot(undefined);
262
+ }
263
+ finally {
264
+ setBusy(false);
265
+ }
266
+ }
267
+ return _jsx(Dialog, { title: "\u6E05\u7A7A\u56DE\u6536\u7AD9", description: "\u6C38\u4E45\u5220\u9664\u540E\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u786E\u8BA4\u6574\u4E2A\u56DE\u6536\u7AD9\u5185\u7684\u8D44\u6599\u90FD\u4E0D\u518D\u9700\u8981\u3002", onClose: () => { if (!busy)
268
+ onClose(); }, children: _jsxs("div", { className: "kl-delete-dialog", children: [_jsx("p", { className: "kl-delete-title", children: snapshot ? `将永久删除回收站中的全部 ${snapshot.count} 份资料。` : error ? '请关闭窗口后重新确认。' : '正在核对回收站…' }), !!snapshot?.pending && _jsxs("p", { children: ["\u53E6\u6709 ", snapshot.pending, " \u4EFD\u5DF2\u786E\u8BA4\u5220\u9664\u7684\u6587\u4EF6\u5C06\u7EE7\u7EED\u6E05\u7406\u3002"] }), _jsx("p", { className: "kl-muted", children: "\u6B63\u6587\u3001\u539F\u59CB\u9644\u4EF6\u548C\u7D22\u5F15\u5C06\u88AB\u79FB\u9664\u3002\u76F8\u5173\u7B14\u8BB0\u7684\u6B63\u6587\u4F1A\u4FDD\u7559\uFF0C\u5F15\u7528\u8FD9\u4E9B\u8D44\u6599\u7684\u4F4D\u7F6E\u5C06\u6807\u8BB0\u4E3A\u201C\u6765\u6E90\u5DF2\u6C38\u4E45\u5220\u9664\u201D\u3002" }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [_jsx("button", { className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-secondary kl-danger", disabled: busy || !snapshot || !(snapshot.count || snapshot.pending), onClick: () => void empty(), children: busy ? '正在永久删除…' : '永久清空' })] })] }) });
269
+ }
270
+ function SettingsDialog({ root, warnings, onClose, onChanged, onRebuilt, onProxySaved }) {
271
+ const [settings, setSettings] = useState();
272
+ const [proxyMode, setProxyMode] = useState(false);
273
+ const [target, setTarget] = useState(root);
274
+ const [listing, setListing] = useState();
275
+ const [browsePath, setBrowsePath] = useState('');
276
+ const [browsing, setBrowsing] = useState(false);
277
+ const [busy, setBusy] = useState(false);
278
+ const [saving, setSaving] = useState(false);
279
+ const [error, setError] = useState('');
280
+ const request = useRef();
281
+ useEffect(() => { const abort = new AbortController(); void api('/settings', { signal: abort.signal }).then(data => { setSettings(data); setProxyMode(data.allowProxyFakeIp === true); }).catch(e => { if (!abort.signal.aborted)
282
+ setError(e.message); }); return () => { abort.abort(); request.current?.abort(); }; }, []);
283
+ useEffect(() => { setTarget(root); }, [root]);
284
+ async function browse(path) {
285
+ request.current?.abort();
286
+ const abort = new AbortController();
287
+ request.current = abort;
288
+ setBusy(true);
289
+ setError('');
290
+ setListing(undefined);
291
+ try {
292
+ const result = await post('/directory/list', { path }, 'POST', abort.signal);
293
+ setListing(result);
294
+ setBrowsePath(result.path);
295
+ }
296
+ catch (e) {
297
+ if (!abort.signal.aborted)
298
+ setError(e.message);
299
+ }
300
+ finally {
301
+ if (!abort.signal.aborted)
302
+ setBusy(false);
303
+ }
304
+ }
305
+ async function pick() {
306
+ if (settings?.picker === 'browse') {
307
+ setBrowsing(true);
308
+ setListing(undefined);
309
+ setBrowsePath(target);
310
+ await browse(target || undefined);
311
+ return;
312
+ }
313
+ setBusy(true);
314
+ setError('');
315
+ const abort = new AbortController();
316
+ request.current = abort;
317
+ try {
318
+ const result = await post('/directory/pick', {}, 'POST', abort.signal);
319
+ if (result.path)
320
+ setTarget(result.path);
321
+ }
322
+ catch (e) {
323
+ if (!abort.signal.aborted)
324
+ setError(e.message);
325
+ }
326
+ finally {
327
+ if (!abort.signal.aborted)
328
+ setBusy(false);
329
+ }
330
+ }
331
+ async function change() {
332
+ setSaving(true);
333
+ setError('');
334
+ try {
335
+ const result = await post('/settings/root', { root: target.trim(), expectedRoot: root });
336
+ setTarget(result.root);
337
+ onChanged();
338
+ }
339
+ catch (e) {
340
+ setError(e.message);
341
+ }
342
+ finally {
343
+ setSaving(false);
344
+ }
345
+ }
346
+ async function saveProxyMode() {
347
+ if (!settings)
348
+ return;
349
+ setSaving(true);
350
+ setError('');
351
+ try {
352
+ const result = await post('/settings/network', { allowProxyFakeIp: proxyMode, expected: settings.allowProxyFakeIp, expectedRoot: root });
353
+ setSettings({ ...settings, allowProxyFakeIp: result.allowProxyFakeIp });
354
+ setProxyMode(result.allowProxyFakeIp);
355
+ onProxySaved(result.allowProxyFakeIp);
356
+ }
357
+ catch (e) {
358
+ setError(e.message);
359
+ }
360
+ finally {
361
+ setSaving(false);
362
+ }
363
+ }
364
+ const changed = target.trim() && target.trim() !== root;
365
+ return _jsx(Dialog, { title: "\u4F60\u7684\u8D44\u6599\uFF0C\u7531\u4F60\u4FDD\u7BA1", description: "\u5185\u5BB9\u4EE5\u666E\u901A\u6587\u4EF6\u4FDD\u5B58\uFF0C\u7D22\u5F15\u53EF\u4EE5\u91CD\u5EFA\u3002", onClose: () => { if (!saving)
366
+ onClose(); }, children: _jsxs("div", { className: "kl-info", children: [browsing ? _jsxs("div", { className: "kl-folder-browser", "aria-label": "\u9009\u62E9\u8D44\u6599\u5E93\u76EE\u5F55", children: [_jsxs("div", { className: "kl-folder-heading", children: [_jsx("strong", { children: "\u9009\u62E9\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsx("button", { className: "kl-secondary", disabled: busy, onClick: () => void browse(), children: "\u4E3B\u76EE\u5F55" })] }), _jsxs("form", { className: "kl-folder-path", onSubmit: e => { e.preventDefault(); void browse(browsePath); }, children: [_jsx("input", { "aria-label": "\u6D4F\u89C8\u76EE\u5F55\u8DEF\u5F84", value: browsePath, onChange: e => setBrowsePath(e.target.value) }), _jsx("button", { className: "kl-secondary", disabled: busy || !browsePath.trim(), children: "\u524D\u5F80" })] }), _jsx("nav", { className: "kl-folder-crumbs", "aria-label": "\u76EE\u5F55\u5C42\u7EA7", children: listing?.crumbs.map(c => _jsxs("button", { disabled: busy, onClick: () => void browse(c.path), children: [c.name, _jsx("span", { children: "/" })] }, c.path)) }), _jsx("div", { className: "kl-folder-entries", "aria-busy": busy, children: busy ? _jsx("p", { className: "kl-muted", children: "\u6B63\u5728\u8BFB\u53D6\u76EE\u5F55\u2026" }) : listing && !listing.entries.length ? _jsx("p", { className: "kl-muted", children: "\u6B64\u76EE\u5F55\u6CA1\u6709\u5B50\u6587\u4EF6\u5939" }) : listing?.entries.map(entry => _jsxs("button", { onClick: () => void browse(entry.path), children: [_jsx(Icon, { name: "folder", size: 17 }), _jsx("span", { children: entry.name }), _jsx(Icon, { name: "arrow", size: 15 })] }, entry.path)) }), listing?.truncated && _jsx("p", { className: "kl-muted", children: "\u5B50\u76EE\u5F55\u8F83\u591A\uFF0C\u4EC5\u663E\u793A\u90E8\u5206\u3002\u53EF\u586B\u5199\u5B8C\u6574\u8DEF\u5F84\u524D\u5F80\u3002" }), _jsxs("div", { className: "kl-folder-actions", children: [_jsx("button", { className: "kl-secondary", onClick: () => { request.current?.abort(); setBusy(false); setBrowsing(false); setError(''); }, children: "\u53D6\u6D88\u9009\u62E9" }), _jsx("button", { className: "kl-primary", disabled: busy || !listing, onClick: () => { setTarget(listing.path); setBrowsing(false); setError(''); }, children: "\u9009\u62E9\u6B64\u76EE\u5F55" })] })] }) : _jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "kl-library-root", children: "\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsxs("div", { className: "kl-root-field", children: [_jsx("input", { id: "kl-library-root", value: target, title: target, spellCheck: false, readOnly: !settings?.canChange, disabled: saving || busy, onChange: e => setTarget(e.target.value) }), _jsxs("button", { className: "kl-secondary", disabled: !settings?.canChange || !settings.picker || busy || saving, onClick: () => void pick(), children: [_jsx(Icon, { name: "folder", size: 16 }), busy ? '选择中…' : '选择目录'] })] }), settings && !settings.canChange && _jsx("p", { className: "kl-muted", children: "\u8BF7\u5728 Harness \u4E2D\u9009\u62E9\u548C\u5207\u6362\u76EE\u5F55\uFF1B\u72EC\u7ACB\u9884\u89C8\u53EF\u901A\u8FC7\u542F\u52A8\u914D\u7F6E\u6307\u5B9A\u76EE\u5F55\u3002" }), !!changed && _jsxs("div", { className: "kl-root-confirm", children: [_jsx("p", { children: "\u5207\u6362\u540E\u5C06\u8BFB\u53D6\u65B0\u76EE\u5F55\uFF0C\u5E76\u5C06\u540E\u7EED\u8D44\u6599\u4FDD\u5B58\u5230\u90A3\u91CC\u3002\u539F\u76EE\u5F55\u7684\u8D44\u6599\u4F1A\u4FDD\u7559\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u642C\u8FC1\u3002" }), _jsxs("div", { children: [_jsx("button", { className: "kl-secondary", disabled: saving || busy, onClick: () => { setTarget(root); setError(''); }, children: "\u53D6\u6D88\u4FEE\u6539" }), _jsx("button", { className: "kl-primary", disabled: saving || busy, onClick: () => void change(), children: saving ? '正在切换…' : '切换资料库' })] })] }), _jsxs("section", { className: "kl-network-settings", "aria-label": "\u7F51\u9875\u91C7\u96C6\u8BBE\u7F6E", children: [_jsx("h3", { children: "\u7F51\u9875\u91C7\u96C6" }), _jsxs("label", { className: "kl-proxy-option", children: [_jsx("input", { type: "checkbox", "aria-describedby": "kl-proxy-help", checked: proxyMode, disabled: !settings?.canChangeNetwork || saving || busy, onChange: e => { setProxyMode(e.target.checked); setError(''); } }), _jsx("span", { children: "Clash/Mihomo \u4EE3\u7406\u517C\u5BB9\uFF08fake-IP\uFF09" })] }), _jsx("p", { id: "kl-proxy-help", children: "\u672C\u673A\u4F7F\u7528 Clash/Mihomo TUN \u7684 fake-IP DNS \u65F6\u5F00\u542F\uFF0C\u5141\u8BB8\u901A\u8FC7\u4EE3\u7406\u91C7\u96C6\u89E3\u6790\u5230 198.18.* \u6216 198.19.* \u7684\u7F51\u7AD9\u3002\u672C\u673A\u53CA\u5176\u4ED6\u5185\u7F51\u5730\u5740\u4ECD\u4E0D\u5141\u8BB8\u8BBF\u95EE\u3002" }), settings?.canChangeNetwork && !settings.canChange && _jsx("p", { className: "kl-muted", children: "\u6B64\u8BBE\u7F6E\u7528\u4E8E\u5F53\u524D\u8D44\u6599\u5E93\u7684\u72EC\u7ACB\u9884\u89C8\uFF0C\u4FDD\u5B58\u540E\u5BF9\u540E\u7EED\u91C7\u96C6\u751F\u6548\u3002" }), settings && !settings.canChangeNetwork && _jsx("p", { className: "kl-muted", children: "\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u4FDD\u5B58\u91C7\u96C6\u8BBE\u7F6E\u3002" }), settings?.canChangeNetwork && proxyMode !== settings.allowProxyFakeIp && _jsx("button", { className: "kl-secondary", disabled: saving || busy, onClick: () => void saveProxyMode(), children: saving ? '正在保存…' : '保存采集设置' })] }), _jsx(ImageSettings, { root: root }, root), _jsx("p", { children: "\u6BCF\u4EFD\u8D44\u6599\u4FDD\u7559\u539F\u59CB\u6587\u4EF6\u3001\u6B63\u6587\u3001\u5143\u6570\u636E\u548C\u5F15\u7528\u6BB5\u843D\u3002\u5206\u7C7B\u5BF9\u5E94\u672C\u5730\u6587\u4EF6\u5939\uFF0C\u63D2\u4EF6\u5378\u8F7D\u4E0D\u4F1A\u5220\u9664\u8FD9\u4E9B\u6587\u4EF6\u3002" }), _jsx("p", { children: "\u652F\u6301\u7F51\u9875\u6B63\u6587\u56FE\u7247\u8BC6\u522B\uFF1B\u97F3\u89C6\u9891\u3001PDF \u7B49\u683C\u5F0F\u53EF\u4EE5\u5F52\u6863\uFF0C\u81EA\u52A8\u8F6C\u5199\u548C\u6587\u4EF6 OCR \u5C1A\u672A\u63A5\u5165\u3002" }), warnings.map(w => _jsx("p", { role: "alert", children: w }, w)), _jsx("button", { className: "kl-secondary", disabled: busy || saving, onClick: () => { setSaving(true); setError(''); void post('/rebuild', {}).then(onRebuilt).catch(e => setError(e.message)).finally(() => setSaving(false)); }, children: "\u4ECE\u8D44\u6599\u6587\u4EF6\u91CD\u5EFA\u7D22\u5F15" })] }), error && _jsx("p", { className: "kl-settings-error", role: "alert", children: error })] }) });
169
367
  }
170
368
  function EditorDialog({ editor, categories, onClose, onSaved }) {
171
369
  const editing = editor.mode === 'edit';
172
370
  const note = editor.mode === 'note';
173
371
  const source = editor.source;
174
- const [type, setType] = useState('url');
372
+ const [type, setType] = useState(editor.initialType || 'url');
373
+ const [sourceUrl, setSourceUrl] = useState(editor.sourceUrl || '');
175
374
  const [title, setTitle] = useState(editing ? source?.title || '' : note && source ? `${source.title} · 摘录` : '');
176
375
  const [content, setContent] = useState(editor.segment ? `> ${editor.segment.text}\n\n` : '');
177
376
  const [category, setCategory] = useState(editing ? source?.category || '收件箱' : note ? '知识笔记' : '收件箱');
@@ -188,6 +387,8 @@ function EditorDialog({ editor, categories, onClose, onSaved }) {
188
387
  setBusy(true);
189
388
  try {
190
389
  const data = { title, category, tags: splitTags(tags), collection, sourceNote };
390
+ if (!editing && !note && type === 'text' && sourceUrl.trim())
391
+ data.sourceUrl = sourceUrl.trim();
191
392
  let result;
192
393
  if (editing)
193
394
  result = await post(`/materials/${source.id}`, data, 'PATCH');
@@ -219,5 +420,5 @@ function EditorDialog({ editor, categories, onClose, onSaved }) {
219
420
  }
220
421
  const changeType = (value) => { setType(value); setContent(''); setError(''); };
221
422
  return _jsx(Dialog, { title: editing ? '整理资料' : note ? '写一份知识笔记' : '收集一份资料', description: editing ? '分类会同步到本地文件夹,原文和引用保持不变。' : note ? '写下理解,也留下它的来源。' : '先收集,再慢慢整理。所有内容保存在你的电脑。', onClose: () => { if (!busy)
222
- onClose(); }, children: _jsxs("form", { onSubmit: submit, className: "kl-editor", children: [!editing && !note && _jsx("div", { className: "kl-tabs", role: "tablist", "aria-label": "\u91C7\u96C6\u65B9\u5F0F", children: ['url', 'text', 'file'].map(value => _jsxs("button", { type: "button", role: "tab", "aria-selected": type === value, className: type === value ? 'active' : '', onClick: () => changeType(value), children: [_jsx(Icon, { name: value === 'url' ? 'web' : value === 'file' ? 'upload' : 'file', size: 17 }), value === 'url' ? '网页链接' : value === 'file' ? '本地文件' : '文本摘录'] }, value)) }), !editing && !note && type === 'url' && _jsxs("label", { children: ["\u7F51\u9875\u5730\u5740", _jsx("input", { type: "url", required: true, placeholder: "https://\u2026", value: content, onChange: e => setContent(e.target.value) }), _jsx("small", { children: "\u652F\u6301\u53EF\u76F4\u63A5\u8BBF\u95EE\u7684\u6587\u7AE0\u7F51\u9875\u3002\u767B\u5F55\u9875\u6216\u52A8\u6001\u9875\u9762\u53EF\u590D\u5236\u6B63\u6587\u5BFC\u5165\u3002" })] }), !editing && !note && type === 'file' && _jsxs("label", { className: "kl-upload", children: [_jsx(Icon, { name: "upload", size: 27 }), _jsx("strong", { children: file?.name || '选择或拖入文件' }), _jsx("span", { children: "\u6587\u672C\u3001HTML\u3001SRT / VTT \u5B57\u5E55\u53CA\u5176\u4ED6\u6587\u4EF6 \u00B7 \u6700\u5927 64 MB" }), _jsx("input", { "aria-label": "\u9009\u62E9\u672C\u5730\u6587\u4EF6", type: "file", onChange: e => setFile(e.target.files?.[0]) }), _jsx("small", { children: "\u89C6\u9891\u548C PDF \u53EF\u5F52\u6863\uFF0C\u5F53\u524D\u7248\u672C\u4E0D\u4F1A\u81EA\u52A8\u8F6C\u5199\u6216 OCR\u3002" })] }), _jsxs("label", { children: ["\u6807\u9898", !editing && !note && _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { required: editing || note, value: title, onChange: e => setTitle(e.target.value), placeholder: type === 'url' && !note ? '自动提取网页标题,也可以自己填写' : '给这份内容起个名字', maxLength: 300 })] }), !editing && (note || type === 'text') && _jsxs("label", { children: [note ? '笔记内容' : '正文', _jsx("textarea", { required: true, rows: 7, value: content, onChange: e => setContent(e.target.value), placeholder: note ? '记录你的理解、结论与尚待验证的问题…' : '粘贴想保留的文字,支持 Markdown…' })] }), !!citations.length && _jsxs("div", { className: "kl-citation-preview", children: [_jsx(Icon, { name: "file", size: 16 }), _jsxs("span", { children: ["\u5F15\u7528\uFF1A", source.title, editor.segment ? ` / ${editor.segment.id}` : ''] })] }), _jsxs("div", { className: "kl-form-grid", children: [_jsxs("label", { children: ["\u5206\u7C7B", _jsx("input", { list: "kl-categories", value: category, onChange: e => setCategory(e.target.value), placeholder: "\u4F8B\u5982 \u6280\u672F/AI" }), _jsx("datalist", { id: "kl-categories", children: categories.map(c => _jsx("option", { value: c }, c)) })] }), _jsxs("label", { children: ["\u4E13\u9898", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: collection, onChange: e => setCollection(e.target.value), placeholder: "\u56F4\u7ED5\u4E00\u4E2A\u95EE\u9898\u7EC4\u7EC7\u8D44\u6599" })] })] }), _jsxs("label", { children: ["\u6807\u7B7E", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: tags, onChange: e => setTags(e.target.value), placeholder: "\u7528\u9017\u53F7\u5206\u9694\uFF0C\u4F8B\u5982\uFF1A\u5F85\u8BFB\uFF0CRAG\uFF0C\u5B9E\u8DF5" })] }), _jsxs("label", { children: ["\u6765\u6E90\u5907\u6CE8", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: sourceNote, onChange: e => setSourceNote(e.target.value), placeholder: "\u4E3A\u4EC0\u4E48\u503C\u5F97\u4FDD\u5B58\uFF0C\u6216\u8005\u5B83\u6765\u81EA\u54EA\u91CC" })] }), error && _jsx("div", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsxs("span", { children: [_jsx("span", { className: "kl-live-dot" }), "\u4EC5\u4FDD\u5B58\u5728\u672C\u5730"] }), _jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsxs("button", { className: "kl-primary", type: "submit", disabled: busy, children: [busy ? '保存中…' : editing ? '保存修改' : note ? '保存笔记' : type === 'url' ? '开始采集' : '保存资料', !busy && _jsx(Icon, { name: "arrow", size: 16 })] })] })] }) });
423
+ onClose(); }, children: _jsxs("form", { onSubmit: submit, className: "kl-editor", children: [!editing && !note && _jsx("div", { className: "kl-tabs", role: "tablist", "aria-label": "\u91C7\u96C6\u65B9\u5F0F", children: ['url', 'text', 'file'].map(value => _jsxs("button", { type: "button", role: "tab", "aria-selected": type === value, className: type === value ? 'active' : '', onClick: () => changeType(value), children: [_jsx(Icon, { name: value === 'url' ? 'web' : value === 'file' ? 'upload' : 'file', size: 17 }), value === 'url' ? '网页链接' : value === 'file' ? '本地文件' : '文本摘录'] }, value)) }), !editing && !note && type === 'url' && _jsxs("label", { children: ["\u7F51\u9875\u5730\u5740", _jsx("input", { type: "url", required: true, placeholder: "https://\u2026", value: content, onChange: e => setContent(e.target.value) }), _jsx("small", { children: "\u652F\u6301\u53EF\u76F4\u63A5\u8BBF\u95EE\u7684\u6587\u7AE0\u7F51\u9875\u3002\u767B\u5F55\u9875\u6216\u52A8\u6001\u9875\u9762\u53EF\u590D\u5236\u6B63\u6587\u5BFC\u5165\u3002" })] }), !editing && !note && type === 'url' && _jsx(ImageCapturePolicy, {}), !editing && !note && type === 'file' && _jsxs("label", { className: "kl-upload", children: [_jsx(Icon, { name: "upload", size: 27 }), _jsx("strong", { children: file?.name || '选择或拖入文件' }), _jsx("span", { children: "\u6587\u672C\u3001HTML\u3001SRT / VTT \u5B57\u5E55\u53CA\u5176\u4ED6\u6587\u4EF6 \u00B7 \u6700\u5927 64 MB" }), _jsx("input", { "aria-label": "\u9009\u62E9\u672C\u5730\u6587\u4EF6", type: "file", onChange: e => setFile(e.target.files?.[0]) }), _jsx("small", { children: "\u89C6\u9891\u548C PDF \u53EF\u5F52\u6863\uFF0C\u5F53\u524D\u7248\u672C\u4E0D\u4F1A\u81EA\u52A8\u8F6C\u5199\u6216 OCR\u3002" })] }), _jsxs("label", { children: ["\u6807\u9898", !editing && !note && _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { required: editing || note, value: title, onChange: e => setTitle(e.target.value), placeholder: type === 'url' && !note ? '自动提取网页标题,也可以自己填写' : '给这份内容起个名字', maxLength: 300 })] }), !editing && (note || type === 'text') && _jsxs("label", { children: [note ? '笔记内容' : '正文', _jsx("textarea", { required: true, rows: 7, value: content, onChange: e => setContent(e.target.value), placeholder: note ? '记录你的理解、结论与尚待验证的问题…' : '粘贴想保留的文字,支持 Markdown…' })] }), !editing && !note && type === 'text' && _jsxs("label", { children: ["\u6765\u6E90\u7F51\u9875", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { type: "url", "aria-label": "\u6765\u6E90\u7F51\u9875", placeholder: "https://\u2026", value: sourceUrl, onChange: e => setSourceUrl(e.target.value) }), _jsx("small", { children: "\u4FDD\u7559\u539F\u7F51\u9875\u5730\u5740\uFF0C\u65B9\u4FBF\u56DE\u6EAF\u6765\u6E90\u3002" })] }), !!citations.length && _jsxs("div", { className: "kl-citation-preview", children: [_jsx(Icon, { name: "file", size: 16 }), _jsxs("span", { children: ["\u5F15\u7528\uFF1A", source.title, editor.segment ? ` / ${editor.segment.id}` : ''] })] }), _jsxs("div", { className: "kl-form-grid", children: [_jsxs("label", { children: ["\u5206\u7C7B", _jsx("input", { list: "kl-categories", value: category, onChange: e => setCategory(e.target.value), placeholder: "\u4F8B\u5982 \u6280\u672F/AI" }), _jsx("datalist", { id: "kl-categories", children: categories.map(c => _jsx("option", { value: c }, c)) })] }), _jsxs("label", { children: ["\u4E13\u9898", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: collection, onChange: e => setCollection(e.target.value), placeholder: "\u56F4\u7ED5\u4E00\u4E2A\u95EE\u9898\u7EC4\u7EC7\u8D44\u6599" })] })] }), _jsxs("label", { children: ["\u6807\u7B7E", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: tags, onChange: e => setTags(e.target.value), placeholder: "\u7528\u9017\u53F7\u5206\u9694\uFF0C\u4F8B\u5982\uFF1A\u5F85\u8BFB\uFF0CRAG\uFF0C\u5B9E\u8DF5" })] }), _jsxs("label", { children: ["\u6765\u6E90\u5907\u6CE8", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: sourceNote, onChange: e => setSourceNote(e.target.value), placeholder: "\u4E3A\u4EC0\u4E48\u503C\u5F97\u4FDD\u5B58\uFF0C\u6216\u8005\u5B83\u6765\u81EA\u54EA\u91CC" })] }), error && _jsx("div", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsxs("span", { children: [_jsx("span", { className: "kl-live-dot" }), !editing && !note && type === 'url' ? '采集结果保存在本地' : '仅保存在本地'] }), _jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsxs("button", { className: "kl-primary", type: "submit", disabled: busy, children: [busy ? '保存中…' : editing ? '保存修改' : note ? '保存笔记' : type === 'url' ? '开始采集' : '保存资料', !busy && _jsx(Icon, { name: "arrow", size: 16 })] })] })] }) });
223
424
  }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export declare function Dialog({ title, description, onClose, children }: {
3
+ title: string;
4
+ description: string;
5
+ onClose: () => void;
6
+ children: React.ReactNode;
7
+ }): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Icon } from './icons.js';
4
+ export function Dialog({ title, description, onClose, children }) {
5
+ const ref = useRef(null);
6
+ const closeRef = useRef(onClose);
7
+ closeRef.current = onClose;
8
+ useEffect(() => {
9
+ const previous = document.activeElement;
10
+ const timer = setTimeout(() => ref.current?.querySelector('input,textarea,button')?.focus(), 0);
11
+ const key = (event) => {
12
+ if (event.key === 'Escape')
13
+ closeRef.current();
14
+ if (event.key === 'Tab') {
15
+ const elements = Array.from(ref.current?.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select,a[href]') || []);
16
+ const first = elements[0], last = elements[elements.length - 1];
17
+ if (event.shiftKey && document.activeElement === first) {
18
+ event.preventDefault();
19
+ last?.focus();
20
+ }
21
+ if (!event.shiftKey && document.activeElement === last) {
22
+ event.preventDefault();
23
+ first?.focus();
24
+ }
25
+ }
26
+ };
27
+ document.addEventListener('keydown', key);
28
+ return () => { clearTimeout(timer); document.removeEventListener('keydown', key); previous?.focus(); };
29
+ }, []);
30
+ return _jsx("div", { className: "kl-modal-backdrop", onMouseDown: event => { if (event.target === event.currentTarget)
31
+ onClose(); }, children: _jsxs("div", { className: "kl-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, ref: ref, children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), _jsx("button", { "aria-label": "\u5173\u95ED\u5F39\u7A97", onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }), children] }) });
32
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { KnowledgeJob, KnowledgeOverview } from '../types.js';
3
+ interface Props {
4
+ root: string;
5
+ job: KnowledgeJob;
6
+ model: Pick<KnowledgeOverview, 'available' | 'model' | 'reason'>;
7
+ onRefresh: () => void;
8
+ onNotify: (message: string) => void;
9
+ onOpenSource: (id: string, segmentId?: string) => void;
10
+ }
11
+ export declare function GroupingEditor({ root, job, model, onRefresh, onNotify, onOpenSource }: Props): React.JSX.Element;
12
+ export {};