dsh-knowledge-library 0.2.0 → 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.
@@ -5,15 +5,18 @@ import { join } from 'node:path';
5
5
  import { Library } from './core/library.js';
6
6
  import { ImportQueue } from './core/jobs.js';
7
7
  import { API_PATH, createHandler, serveAsset } from './http.js';
8
- import { fetchPublicPage } from './core/fetch.js';
8
+ import { fetchPublicImage, fetchPublicPage } from './core/fetch.js';
9
+ import { ImageCapture } from './core/image-capture.js';
9
10
  import { createPreviewSettings } from './preview-settings.js';
10
11
  import { KnowledgeQueue } from './core/knowledge.js';
11
12
  import { previewKnowledgeModel } from './knowledge-model.js';
12
13
  const root = resolve(process.env.KNOWLEDGE_LIBRARY_ROOT || join(homedir(), 'Documents', 'DSH-Knowledge-Library'));
13
14
  const library = new Library(root);
14
- const knowledge = new KnowledgeQueue(library, previewKnowledgeModel());
15
+ const model = previewKnowledgeModel();
16
+ const knowledge = new KnowledgeQueue(library, model);
15
17
  const settings = createPreviewSettings(library, { allowProxyFakeIp: process.env.KNOWLEDGE_LIBRARY_ALLOW_FAKE_IP === '1' });
16
- const queue = new ImportQueue(library, (url, signal) => fetchPublicPage(url, signal, settings.fetchPolicy()));
18
+ const images = new ImageCapture(library, model, (url, signal, _policy, referer) => fetchPublicImage(url, signal, settings.fetchPolicy(), referer));
19
+ const queue = new ImportQueue(library, (url, signal) => fetchPublicPage(url, signal, settings.fetchPolicy()), images);
17
20
  const port = Number(process.env.PORT || 4317);
18
21
  let origin = `http://127.0.0.1:${port}`;
19
22
  const api = createHandler(library, queue, () => origin, settings, knowledge);
package/dist/types.d.ts CHANGED
@@ -41,6 +41,51 @@ export interface MaterialDetail extends Material {
41
41
  content: string;
42
42
  segments: Segment[];
43
43
  path: string;
44
+ images?: PageImageView[];
45
+ }
46
+ export interface PageImageCandidate {
47
+ url: string;
48
+ alt: string;
49
+ }
50
+ export interface PageImageRecord extends PageImageCandidate {
51
+ id: string;
52
+ status: 'pending' | 'downloading' | 'recognizing' | 'completed' | 'failed' | 'skipped';
53
+ attempts: number;
54
+ error?: string;
55
+ fileName?: string;
56
+ checksum?: string;
57
+ mediaType?: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
58
+ width?: number;
59
+ height?: number;
60
+ bytes?: number;
61
+ finalUrl?: string;
62
+ text?: string;
63
+ description?: string;
64
+ uncertainty?: string;
65
+ }
66
+ export interface PageImageManifest {
67
+ schemaVersion: 1;
68
+ model: string;
69
+ limit: number;
70
+ excluded: number;
71
+ records: PageImageRecord[];
72
+ }
73
+ export type PageImageView = Omit<PageImageRecord, 'text' | 'description' | 'fileName' | 'checksum'> & {
74
+ segmentId?: string;
75
+ originalAvailable: boolean;
76
+ model: string;
77
+ };
78
+ export interface ImageCaptureSettings {
79
+ enabled: boolean;
80
+ maxImages: number;
81
+ model?: string;
82
+ version: number;
83
+ }
84
+ export interface ImageCaptureStatus extends ImageCaptureSettings {
85
+ supported: boolean;
86
+ available: boolean;
87
+ currentModel?: string;
88
+ reason?: string;
44
89
  }
45
90
  export interface TrashSnapshot {
46
91
  count: number;
@@ -91,6 +136,17 @@ export interface ImportJob {
91
136
  materialId?: string;
92
137
  error?: string;
93
138
  errorCode?: 'PAGE_REQUIRES_BROWSER';
139
+ progress?: string;
140
+ imageCapture?: {
141
+ model: string;
142
+ maxImages: number;
143
+ };
144
+ imageSummary?: {
145
+ total: number;
146
+ completed: number;
147
+ failed: number;
148
+ skipped: number;
149
+ };
94
150
  }
95
151
  export interface KnowledgeDraft {
96
152
  title: string;
package/dist/ui/App.js CHANGED
@@ -4,6 +4,7 @@ import { Icon } from './icons.js';
4
4
  import { API, ApiError, api, post } from './api.js';
5
5
  import { Dialog } from './Dialog.js';
6
6
  import { KnowledgeBadge, KnowledgeJobsDialog, StartKnowledgeDialog } from './Knowledge.js';
7
+ import { ImageSettings, ImageCapturePolicy, PageImages, ImageRetryDialog, imagesPending } from './ImageCapture.js';
7
8
  const kinds = {
8
9
  web: { label: '网页', icon: 'web' }, text: { label: '文本', icon: 'file' }, file: { label: '文件', icon: 'file' },
9
10
  video: { label: '音视频', icon: 'video' }, transcript: { label: '字幕', icon: 'video' }, note: { label: '笔记', icon: 'note' },
@@ -31,6 +32,7 @@ export function App() {
31
32
  const [generation, setGeneration] = useState();
32
33
  const [knowledgeOpen, setKnowledgeOpen] = useState();
33
34
  const [showJobs, setShowJobs] = useState(false);
35
+ const [imageRetry, setImageRetry] = useState();
34
36
  const [showInfo, setShowInfo] = useState(false);
35
37
  const [settingsFromJobs, setSettingsFromJobs] = useState(false);
36
38
  const [showNavigation, setShowNavigation] = useState(false);
@@ -172,7 +174,7 @@ export function App() {
172
174
  }
173
175
  const currentTitle = filter.trashed ? '回收站' : filter.category || filter.collection || (filter.tag ? `标签 · ${filter.tag}` : filter.kind === 'note' ? '知识笔记' : '全部资料');
174
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)
175
- 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, 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("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 => {
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 => {
176
178
  setEditor(undefined);
177
179
  refresh();
178
180
  if (result.material) {
@@ -201,7 +203,10 @@ export function App() {
201
203
  else
202
204
  notify(`回收站已清空,永久删除 ${result.deleted} 份资料`);
203
205
  searchRef.current?.focus();
204
- } }), 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: 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.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.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)
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)
205
210
  setShowJobs(true); setSettingsFromJobs(false); }, onChanged: () => {
206
211
  setSelected(undefined);
207
212
  setDetail(undefined);
@@ -358,7 +363,7 @@ function SettingsDialog({ root, warnings, onClose, onChanged, onRebuilt, onProxy
358
363
  }
359
364
  const changed = target.trim() && target.trim() !== root;
360
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)
361
- 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("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" }), 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 })] }) });
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 })] }) });
362
367
  }
363
368
  function EditorDialog({ editor, categories, onClose, onSaved }) {
364
369
  const editing = editor.mode === 'edit';
@@ -415,5 +420,5 @@ function EditorDialog({ editor, categories, onClose, onSaved }) {
415
420
  }
416
421
  const changeType = (value) => { setType(value); setContent(''); setError(''); };
417
422
  return _jsx(Dialog, { title: editing ? '整理资料' : note ? '写一份知识笔记' : '收集一份资料', description: editing ? '分类会同步到本地文件夹,原文和引用保持不变。' : note ? '写下理解,也留下它的来源。' : '先收集,再慢慢整理。所有内容保存在你的电脑。', onClose: () => { if (!busy)
418
- 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…' })] }), !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" }), "\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 })] })] })] }) });
419
424
  }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import type { MaterialDetail } from '../types.js';
3
+ export declare function ImageSettings({ root }: {
4
+ root: string;
5
+ }): React.JSX.Element;
6
+ export declare function ImageCapturePolicy(): React.JSX.Element;
7
+ export declare const imagesPending: (detail: MaterialDetail) => boolean | undefined;
8
+ export declare function PageImages({ detail, onRetry, onJump }: {
9
+ detail: MaterialDetail;
10
+ onRetry: () => void;
11
+ onJump: (id: string) => void;
12
+ }): React.JSX.Element | null;
13
+ export declare function ImageRetryDialog({ id, root, onClose, onStarted }: {
14
+ id: string;
15
+ root: string;
16
+ onClose: () => void;
17
+ onStarted: () => void;
18
+ }): React.JSX.Element;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { API, api, post } from './api.js';
4
+ import { Dialog } from './Dialog.js';
5
+ export function ImageSettings({ root }) {
6
+ const [status, setStatus] = useState();
7
+ const [enabled, setEnabled] = useState(false);
8
+ const [limit, setLimit] = useState(6);
9
+ const [confirmed, setConfirmed] = useState(false);
10
+ const [busy, setBusy] = useState(false);
11
+ const [error, setError] = useState('');
12
+ const [saved, setSaved] = useState(false);
13
+ useEffect(() => { const abort = new AbortController(); void api('/settings/images', { signal: abort.signal }).then(value => { setStatus(value); setEnabled(value.enabled); setLimit(value.maxImages); }).catch(e => { if (!abort.signal.aborted)
14
+ setError(e.message); }); return () => abort.abort(); }, [root]);
15
+ async function save() {
16
+ if (!status || busy)
17
+ return;
18
+ setBusy(true);
19
+ setError('');
20
+ setSaved(false);
21
+ try {
22
+ const value = await post('/settings/images', { settings: { enabled, maxImages: limit }, version: status.version, expectedRoot: root, expectedModel: status.currentModel, confirmed });
23
+ setStatus(value);
24
+ setEnabled(value.enabled);
25
+ setLimit(value.maxImages);
26
+ setConfirmed(false);
27
+ setSaved(true);
28
+ }
29
+ catch (e) {
30
+ setError(e.message);
31
+ }
32
+ finally {
33
+ setBusy(false);
34
+ }
35
+ }
36
+ const mismatch = status?.enabled && status.model !== status.currentModel;
37
+ return _jsxs("section", { className: "kl-image-settings", "aria-label": "\u7F51\u9875\u56FE\u7247\u8BC6\u522B\u8BBE\u7F6E", children: [_jsx("h3", { children: "\u6B63\u6587\u56FE\u7247\u8BC6\u522B" }), _jsxs("label", { className: "kl-proxy-option", children: [_jsx("input", { type: "checkbox", checked: enabled, disabled: !status?.supported || busy, onChange: e => { setEnabled(e.target.checked); setConfirmed(false); setSaved(false); } }), _jsx("span", { children: "\u91C7\u96C6\u7F51\u9875\u65F6\u81EA\u52A8\u8BC6\u522B\u6B63\u6587\u56FE\u7247" })] }), _jsx("p", { children: "\u5F00\u542F\u540E\uFF0C\u540E\u7EED\u7F51\u9875\u7684\u6B63\u6587\u56FE\u7247\u4F1A\u81EA\u52A8\u53D1\u9001\u7ED9\u5F53\u524D\u89C6\u89C9\u6A21\u578B\uFF0C\u53EF\u80FD\u4EA7\u751F\u6A21\u578B\u8D39\u7528\u3002\u539F\u56FE\u4E0E\u8BC6\u522B\u6587\u5B57\u4FDD\u5B58\u5728\u672C\u5730\uFF0C\u53EF\u641C\u7D22\u3001\u5F15\u7528\u548C\u6574\u7406\u4E3A\u77E5\u8BC6\u7B14\u8BB0\u3002" }), _jsxs("p", { className: "kl-image-model", children: ["\u5F53\u524D\u6A21\u578B\uFF1A", status?.currentModel || '未配置', status?.model && _jsxs(_Fragment, { children: [_jsx("br", {}), "\u5DF2\u7ED1\u5B9A\u6A21\u578B\uFF1A", status.model] })] }), status?.reason && _jsx("p", { className: "kl-muted", children: status.reason }), mismatch && _jsx("p", { role: "alert", children: "\u6A21\u578B\u5DF2\u6539\u53D8\uFF0C\u539F\u56FE\u7247\u4EFB\u52A1\u4E0D\u4F1A\u81EA\u52A8\u5207\u6362\u6A21\u578B\u3002\u8BF7\u5207\u56DE\u539F\u6A21\u578B\uFF0C\u6216\u786E\u8BA4\u65B0\u8BBE\u7F6E\u7528\u4E8E\u540E\u7EED\u7F51\u9875\u3002" }), _jsxs("label", { className: "kl-image-limit", children: ["\u6BCF\u9875\u6700\u591A\u8BC6\u522B\u56FE\u7247\u6570", _jsx("input", { "aria-label": "\u6BCF\u9875\u6700\u591A\u8BC6\u522B\u56FE\u7247\u6570", type: "number", min: "1", max: "10", value: limit, disabled: busy || !status?.supported, onChange: e => { setLimit(Number(e.target.value)); setConfirmed(false); setSaved(false); } })] }), enabled && _jsxs("label", { className: "kl-proxy-option", children: [_jsx("input", { type: "checkbox", checked: confirmed, disabled: busy || !status?.available, onChange: e => setConfirmed(e.target.checked) }), _jsxs("span", { children: ["\u786E\u8BA4\u4F7F\u7528 ", status?.currentModel || '当前模型', "\uFF0C\u6BCF\u9875\u6700\u591A ", limit, " \u5F20\uFF0C\u5141\u8BB8\u540E\u7EED\u91C7\u96C6\u81EA\u52A8\u53D1\u9001\u56FE\u7247\u5E76\u4EA7\u751F\u6A21\u578B\u8D39\u7528"] })] }), _jsx("button", { className: "kl-secondary", disabled: busy || !status?.supported || !Number.isInteger(limit) || limit < 1 || limit > 10 || (enabled && (!status?.available || !confirmed)), onClick: () => void save(), children: busy ? '正在保存…' : '保存图片识别设置' }), saved && _jsx("p", { role: "status", children: "\u5DF2\u4FDD\u5B58\uFF0C\u5BF9\u540E\u7EED\u7F51\u9875\u91C7\u96C6\u751F\u6548\uFF1B\u5DF2\u6709\u8D44\u6599\u53EF\u4FDD\u7559\u539F\u6837\u3002" }), error && _jsx("p", { className: "kl-form-error", role: "alert", children: error })] });
38
+ }
39
+ export function ImageCapturePolicy() {
40
+ const [status, setStatus] = useState();
41
+ useEffect(() => { const abort = new AbortController(); void api('/settings/images', { signal: abort.signal }).then(setStatus).catch(() => { }); return () => abort.abort(); }, []);
42
+ return _jsx("div", { className: "kl-image-policy", children: !status ? '正在读取图片采集设置…' : status.enabled ? _jsxs(_Fragment, { children: ["\u56FE\u7247\u8BC6\u522B\u5DF2\u5F00\u542F\uFF1A", status.model, "\uFF0C\u6BCF\u9875\u6700\u591A ", status.maxImages, " \u5F20\u3002\u6B63\u6587\u56FE\u7247\u5C06\u53D1\u9001\u7ED9\u6A21\u578B\uFF0C\u53EF\u80FD\u4EA7\u751F\u8D39\u7528\u3002", (!status.available || status.model !== status.currentModel) && ' 当前模型不可用或与绑定模型不同,图片将保留为失败任务供核对。'] }) : '图片识别未开启。可在资料库设置中启用,自动保存原图并提取图中文字与图示含义。' });
43
+ }
44
+ const imageStates = { pending: '等待处理', downloading: '下载中', recognizing: '识别中', completed: '已识别', failed: '未完成', skipped: '已跳过' };
45
+ export const imagesPending = (detail) => detail.images?.some(image => ['pending', 'downloading', 'recognizing'].includes(image.status));
46
+ export function PageImages({ detail, onRetry, onJump }) {
47
+ if (!detail.images?.length)
48
+ return null;
49
+ return _jsxs("section", { className: "kl-page-images", "aria-label": "\u6B63\u6587\u56FE\u7247", children: [_jsxs("div", { className: "kl-image-heading", children: [_jsxs("h3", { children: ["\u6B63\u6587\u56FE\u7247 \u00B7 ", detail.images.length] }), !detail.deletedAt && !imagesPending(detail) && detail.images.some(image => image.status === 'failed' && image.attempts < 3) && _jsx("button", { className: "kl-secondary", onClick: onRetry, children: "\u91CD\u8BD5\u5931\u8D25\u56FE\u7247" })] }), _jsx("p", { className: "kl-muted", children: "\u56FE\u7247\u6587\u5B57\u548C\u56FE\u793A\u89E3\u8BFB\u5DF2\u6807\u6CE8\u4E3A\u6A21\u578B\u8F93\u51FA\uFF0C\u8BF7\u7ED3\u5408\u539F\u56FE\u6838\u5BF9\u3002" }), _jsx("div", { className: "kl-image-grid", children: detail.images.map((image, index) => _jsxs("div", { className: "kl-image-record", children: [image.originalAvailable && _jsx("a", { href: `${API}/materials/${detail.id}/images/${image.id}`, target: "_blank", rel: "noreferrer", "aria-label": `查看原图 ${index + 1}`, children: _jsx("img", { loading: "lazy", src: `${API}/materials/${detail.id}/images/${image.id}`, alt: image.alt || `正文图片 ${index + 1}` }) }), _jsxs("strong", { children: ["\u56FE\u7247 ", index + 1, " \u00B7 ", imageStates[image.status]] }), _jsx("small", { children: image.model }), _jsx("a", { href: image.url, target: "_blank", rel: "noreferrer", children: "\u6765\u6E90\u56FE\u7247" }), image.error && _jsx("p", { children: image.error }), image.uncertainty && _jsxs("p", { children: ["\u5F85\u6838\u5B9E\uFF1A", image.uncertainty] }), image.segmentId && _jsx("button", { className: "kl-secondary", onClick: () => onJump(image.segmentId), children: "\u67E5\u770B\u8BC6\u522B\u6587\u5B57" })] }, image.id)) })] });
50
+ }
51
+ export function ImageRetryDialog({ id, root, onClose, onStarted }) {
52
+ const [detail, setDetail] = useState();
53
+ const [busy, setBusy] = useState(false);
54
+ const [error, setError] = useState('');
55
+ useEffect(() => { const abort = new AbortController(); void api(`/materials/${id}`, { signal: abort.signal }).then(setDetail).catch(e => { if (!abort.signal.aborted)
56
+ setError(e.message); }); return () => abort.abort(); }, [id]);
57
+ const eligible = detail?.images?.filter(image => image.status === 'failed' && image.attempts < 3) || [];
58
+ async function retry() {
59
+ if (!eligible.length || busy)
60
+ return;
61
+ setBusy(true);
62
+ setError('');
63
+ try {
64
+ await post(`/materials/${id}/images/retry`, { expectedRoot: root, expectedModel: eligible[0].model, imageIds: eligible.map(image => image.id), confirmed: true });
65
+ onStarted();
66
+ }
67
+ catch (e) {
68
+ setError(e.message);
69
+ }
70
+ finally {
71
+ setBusy(false);
72
+ }
73
+ }
74
+ return _jsx(Dialog, { title: "\u91CD\u8BD5\u5931\u8D25\u56FE\u7247", description: "\u5C06\u518D\u6B21\u53D1\u9001\u4E0B\u5217\u5931\u8D25\u56FE\u7247\u5230\u539F\u4EFB\u52A1\u6A21\u578B\uFF0C\u53EF\u80FD\u4EA7\u751F\u8D39\u7528\u3002\u5DF2\u6210\u529F\u56FE\u7247\u4E0D\u4F1A\u91CD\u590D\u8BC6\u522B\u3002", onClose: () => { if (!busy)
75
+ onClose(); }, children: _jsxs("div", { className: "kl-delete-dialog", children: [_jsx("p", { children: detail ? `${detail.title} · ${eligible.length} 张可重试图片` : '正在核对图片…' }), _jsxs("p", { children: ["\u6A21\u578B\uFF1A", eligible[0]?.model || '—', "\u3002\u6BCF\u5F20\u56FE\u7247\u6700\u591A\u53D1\u8D77 3 \u6B21\u6A21\u578B\u8BF7\u6C42\u3002"] }), _jsx("ul", { children: eligible.map(image => _jsxs("li", { children: [image.alt || image.url, "\uFF08\u5DF2\u8BF7\u6C42 ", image.attempts, " \u6B21\uFF09"] }, image.id)) }), 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-primary", disabled: busy || !eligible.length || !!detail && (!!detail.deletedAt || !!imagesPending(detail)), onClick: () => void retry(), children: busy ? '正在提交…' : `确认重试 ${eligible.length} 张图片` })] })] }) });
76
+ }
@@ -39,7 +39,7 @@ export function StartKnowledgeDialog({ root, sourceIds, model, initialMode, onCl
39
39
  }
40
40
  }
41
41
  return _jsx(Dialog, { title: smart ? '智能整理' : '生成知识笔记', description: smart ? '先逐篇分析、建议分组,再确认分类与生成各组知识草稿。' : '先提炼资料,再由你确认。原始资料与来源引用会保留。', onClose: () => { if (!busy)
42
- onClose(); }, children: _jsxs("form", { className: "kl-knowledge-form", onSubmit: event => void start(event), children: [_jsxs("div", { className: "kl-knowledge-summary", children: [_jsxs("b", { children: [sourceIds.length, " \u4EFD\u6765\u6E90\u8D44\u6599"] }), _jsxs("span", { children: [characters.toLocaleString(), " \u5B57"] })] }), _jsx("ul", { className: "kl-knowledge-source-list", children: sources?.map(s => _jsxs("li", { children: [_jsx(Icon, { name: "file", size: 14 }), _jsx("span", { children: s.title })] }, s.id)) }), _jsxs("fieldset", { className: "kl-knowledge-mode", children: [_jsx("legend", { children: "\u6574\u7406\u65B9\u5F0F" }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "knowledge-mode", value: "smart", checked: smart, disabled: !model.smartSupported || busy, onChange: () => setMode('smart') }), "\u667A\u80FD\u5206\u7EC4", _jsx("small", { children: "\u9002\u5408\u96F6\u6563\u8D44\u6599\uFF0C\u5148\u5224\u65AD\u5173\u8054" })] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "knowledge-mode", value: "compose", checked: !smart, disabled: busy, onChange: () => setMode('compose') }), "\u540C\u4E3B\u9898\u76F4\u63A5\u7EFC\u5408", _jsx("small", { children: "\u5DF2\u786E\u8BA4\u56F4\u7ED5\u540C\u4E00\u95EE\u9898\u65F6\u4F7F\u7528" })] })] }), _jsxs("label", { children: [smart ? '整理方向(可选)' : '整理主题', _jsx("input", { "aria-label": "\u6574\u7406\u4E3B\u9898", value: topic, onChange: e => setTopic(e.target.value), maxLength: 300, placeholder: smart ? '留空可按资料的实际主题自动分组' : '例如:Harness 如何提高 Agent 的可靠性' }), _jsx("small", { children: smart ? '方向用于辅助判断,不会要求把无关联资料合并。' : '留空时围绕资料本身整理;多篇资料会比较共识、差异和矛盾。' })] }), _jsx("p", { className: "kl-muted", children: smart ? '逐篇提取主题、概念与用途,给出分组理由和分类标签建议。无关联资料可独立成组,信息不足的资料保留待整理。确认方案前不移动资料。' : '将提炼核心观点、关键概念、可复用方法、适用条件与局限、待验证问题,并标注来源段落。' }), smart && !model.smartSupported && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u8BF7\u91CD\u542F\u8D44\u6599\u5E93\u670D\u52A1\u4EE5\u542F\u7528\u667A\u80FD\u5206\u7EC4\u3002" }), model.available ? _jsxs("p", { className: "kl-knowledge-model", children: ["\u4F7F\u7528\u6A21\u578B\uFF1A", model.model, "\u3002\u70B9\u51FB\u5F00\u59CB\u540E\u4F1A\u5411\u8BE5\u6A21\u578B\u53D1\u9001\u6240\u9009\u6B63\u6587\u53CA\u68C0\u7D22\u5230\u7684\u76F8\u5173\u77E5\u8BC6\u7B14\u8BB0\uFF0C\u53EF\u80FD\u4EA7\u751F\u8C03\u7528\u8D39\u7528\u3002", smart ? '逐篇分析和分组最多 40 次请求;确认后各组继续生成草稿,整次任务合计最多 80 次。' : '长文会分段提炼,含旧笔记对比最多 22 次模型请求。', "\u5355\u6B21\u6700\u591A 10 \u4EFD\u3001\u6B63\u6587\u603B\u91CF 10 \u4E07\u5B57\u3002"] }) : _jsxs("div", { children: [_jsx("p", { role: "alert", className: "kl-form-error", children: model.reason }), _jsxs("a", { className: "kl-knowledge-help", href: "https://github.com/Alan-dong-dong/dsh-knowledge-library#\u5C06\u8D44\u6599\u6574\u7406\u4E3A\u77E5\u8BC6\u7B14\u8BB0", target: "_blank", rel: "noreferrer", children: ["\u67E5\u770B\u6A21\u578B\u914D\u7F6E\u8BF4\u660E ", _jsx(Icon, { name: "arrow", size: 12 })] })] }), characters > 100_000 && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u6B63\u6587\u8D85\u8FC7 10 \u4E07\u5B57\uFF0C\u8BF7\u51CF\u5C11\u9009\u62E9\u540E\u5206\u6279\u6574\u7406\u3002" }), invalidSource && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u90E8\u5206\u8D44\u6599\u5DF2\u5220\u9664\u3001\u4EC5\u5F52\u6863\u6216\u5C5E\u4E8E\u77E5\u8BC6\u7B14\u8BB0\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u3002" }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [_jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-primary", disabled: busy || !sources || !model.available || (smart && !model.smartSupported) || characters > 100_000 || invalidSource, children: busy ? '正在提交…' : smart ? '开始分析分组' : '开始整理' })] })] }) });
42
+ onClose(); }, children: _jsxs("form", { className: "kl-knowledge-form", onSubmit: event => void start(event), children: [_jsxs("div", { className: "kl-knowledge-summary", children: [_jsxs("b", { children: [sourceIds.length, " \u4EFD\u6765\u6E90\u8D44\u6599"] }), _jsxs("span", { children: [characters.toLocaleString(), " \u5B57"] })] }), _jsx("ul", { className: "kl-knowledge-source-list", children: sources?.map(s => _jsxs("li", { children: [_jsx(Icon, { name: "file", size: 14 }), _jsx("span", { children: s.title })] }, s.id)) }), _jsxs("fieldset", { className: "kl-knowledge-mode", children: [_jsx("legend", { children: "\u6574\u7406\u65B9\u5F0F" }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "knowledge-mode", value: "smart", checked: smart, disabled: !model.smartSupported || busy, onChange: () => setMode('smart') }), "\u667A\u80FD\u5206\u7EC4", _jsx("small", { children: "\u9002\u5408\u96F6\u6563\u8D44\u6599\uFF0C\u5148\u5224\u65AD\u5173\u8054" })] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "knowledge-mode", value: "compose", checked: !smart, disabled: busy, onChange: () => setMode('compose') }), "\u540C\u4E3B\u9898\u76F4\u63A5\u7EFC\u5408", _jsx("small", { children: "\u5DF2\u786E\u8BA4\u56F4\u7ED5\u540C\u4E00\u95EE\u9898\u65F6\u4F7F\u7528" })] })] }), _jsxs("label", { children: [smart ? '整理方向(可选)' : '整理主题', _jsx("input", { "aria-label": "\u6574\u7406\u4E3B\u9898", value: topic, onChange: e => setTopic(e.target.value), maxLength: 300, placeholder: smart ? '留空可按资料的实际主题自动分组' : '例如:Harness 如何提高 Agent 的可靠性' }), _jsx("small", { children: smart ? '方向用于辅助判断,不会要求把无关联资料合并。' : '留空时围绕资料本身整理;多篇资料会比较共识、差异和矛盾。' })] }), _jsx("p", { className: "kl-muted", children: smart ? '逐篇提取主题、概念与用途,给出分组理由和分类标签建议。无关联资料可独立成组,信息不足的资料保留待整理。确认方案前不移动资料。' : '将提炼核心观点、关键概念、可复用方法、适用条件与局限、待验证问题,并标注来源段落。' }), smart && !model.smartSupported && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u8BF7\u91CD\u542F\u8D44\u6599\u5E93\u670D\u52A1\u4EE5\u542F\u7528\u667A\u80FD\u5206\u7EC4\u3002" }), model.available ? _jsxs("p", { className: "kl-knowledge-model", children: ["\u4F7F\u7528\u6A21\u578B\uFF1A", model.model, "\u3002\u70B9\u51FB\u5F00\u59CB\u540E\u4F1A\u5411\u8BE5\u6A21\u578B\u53D1\u9001\u6240\u9009\u6B63\u6587\u53CA\u68C0\u7D22\u5230\u7684\u76F8\u5173\u77E5\u8BC6\u7B14\u8BB0\uFF0C\u53EF\u80FD\u4EA7\u751F\u8C03\u7528\u8D39\u7528\u3002", smart ? '逐篇分析和分组最多 40 次请求;确认后各组继续生成草稿,整次任务合计最多 80 次。' : '长文会分段提炼,含旧笔记对比最多 22 次模型请求。', "\u5355\u6B21\u6700\u591A 10 \u4EFD\u3001\u6B63\u6587\u603B\u91CF 10 \u4E07\u5B57\u3002"] }) : _jsxs("div", { children: [_jsx("p", { role: "alert", className: "kl-form-error", children: model.reason }), _jsxs("a", { className: "kl-knowledge-help", href: "https://github.com/Alan-dong-dong/dsh-knowledge-library#\u5C06\u8D44\u6599\u6574\u7406\u4E3A\u77E5\u8BC6\u7B14\u8BB0", target: "_blank", rel: "noreferrer", children: ["\u67E5\u770B\u6A21\u578B\u914D\u7F6E\u8BF4\u660E ", _jsx(Icon, { name: "arrow", size: 12 })] })] }), model.available && _jsxs("details", { className: "kl-knowledge-model", children: [_jsx("summary", { children: "\u8F93\u51FA\u914D\u7F6E\u4E0E\u601D\u8003" }), _jsx("p", { children: "\u8F93\u51FA\u989D\u5EA6\u6CBF\u7528\u5F53\u524D\u6A21\u578B\u914D\u7F6E\uFF0C\u63D2\u4EF6\u4E0D\u53E6\u8BBE token \u4E0A\u9650\u3002Harness \u4F7F\u7528\u6A21\u578B\u9002\u914D\u5668\u7684\u9ED8\u8BA4\u8F93\u51FA\u914D\u7F6E\uFF0C\u72EC\u7ACB\u9884\u89C8\u4F7F\u7528\u6A21\u578B\u670D\u52A1\u9ED8\u8BA4\u914D\u7F6E\u3002\u4FDD\u7559\u5F53\u524D\u601D\u8003\u5F3A\u5EA6\uFF0C\u90E8\u5206\u6A21\u578B\u5C06\u601D\u8003\u4E0E\u6B63\u6587\u4E00\u8D77\u8BA1\u5165\u989D\u5EA6\uFF0C\u8D39\u7528\u6309\u6A21\u578B\u670D\u52A1\u89C4\u5219\u8BA1\u7B97\u3002\u63D0\u70BC/\u5206\u6790\u6BCF\u6B21\u6700\u591A\u7B49\u5F85 5 \u5206\u949F\uFF0C\u5206\u7EC4/\u7EFC\u5408/\u5BF9\u6BD4\u6700\u591A 10 \u5206\u949F\uFF0C\u53EF\u968F\u65F6\u53D6\u6D88\uFF1B\u622A\u65AD\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002" })] }), characters > 100_000 && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u6B63\u6587\u8D85\u8FC7 10 \u4E07\u5B57\uFF0C\u8BF7\u51CF\u5C11\u9009\u62E9\u540E\u5206\u6279\u6574\u7406\u3002" }), invalidSource && _jsx("p", { role: "alert", className: "kl-form-error", children: "\u90E8\u5206\u8D44\u6599\u5DF2\u5220\u9664\u3001\u4EC5\u5F52\u6863\u6216\u5C5E\u4E8E\u77E5\u8BC6\u7B14\u8BB0\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u3002" }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [_jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-primary", disabled: busy || !sources || !model.available || (smart && !model.smartSupported) || characters > 100_000 || invalidSource, children: busy ? '正在提交…' : smart ? '开始分析分组' : '开始整理' })] })] }) });
43
43
  }
44
44
  export function KnowledgeJobsDialog(props) {
45
45
  const { root, overview, onClose, onRefresh, onRegenerate, onOpenSource, onSaved, onNotify } = props;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-knowledge-library",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "本地优先的 DeepSeek Harness 资料库:采集、分类、检索与可追溯知识笔记",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,7 @@
67
67
  "test:dsh:directory": "python tests/run_dsh_directory.py",
68
68
  "test:knowledge:ui": "python tests/run_knowledge_ui.py",
69
69
  "test:dsh:knowledge": "python tests/run_dsh_knowledge.py",
70
+ "test:images:ui": "python tests/run_images_ui.py",
70
71
  "pack:check": "node scripts/check-package.mjs",
71
72
  "verify": "npm run build && npm test && npm run pack:check",
72
73
  "prepack": "npm run build"