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
@@ -0,0 +1,93 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { post } from './api.js';
4
+ import { Icon } from './icons.js';
5
+ const splitTags = (value) => value.split(/[,,]/).map(t => t.trim()).filter(Boolean);
6
+ export function GroupingEditor({ root, job, model, onRefresh, onNotify, onOpenSource }) {
7
+ const [plan, setPlan] = useState(job.plan);
8
+ const [version, setVersion] = useState(job.planVersion);
9
+ const [tagText, setTagText] = useState(Object.fromEntries(plan.groups.map(g => [g.id, g.tags.join(',')])));
10
+ const [apply, setApply] = useState(job.classification?.apply ?? plan.applyClassification ?? true);
11
+ const [busy, setBusy] = useState(false);
12
+ const [error, setError] = useState('');
13
+ const locked = !!job.classification;
14
+ const groups = plan.groups.filter(g => g.sourceIds.length);
15
+ function groupValue(id, change) { setPlan(p => ({ ...p, groups: p.groups.map(g => g.id === id ? { ...g, ...change } : g) })); }
16
+ function newGroup(sourceId) {
17
+ const source = job.sources.find(s => s.id === sourceId);
18
+ const analysis = job.analyses?.find(a => a.sourceId === sourceId);
19
+ const id = `group_${crypto.randomUUID()}`;
20
+ const item = { id, title: analysis?.theme || source?.title || '新分组', reason: source ? '独立整理这份资料' : '请填写资料之间的具体关联', sourceIds: sourceId ? [sourceId] : [], category: analysis?.category || '收件箱', tags: analysis?.tags || [] };
21
+ setTagText(values => ({ ...values, [id]: item.tags.join(',') }));
22
+ setPlan(p => ({ ...p, groups: [...p.groups.map(g => ({ ...g, sourceIds: g.sourceIds.filter(s => s !== sourceId) })), item], unassigned: p.unassigned.filter(s => s.sourceId !== sourceId) }));
23
+ }
24
+ function assign(sourceId, target) {
25
+ if (target === '__new__') {
26
+ newGroup(sourceId);
27
+ return;
28
+ }
29
+ const analysis = job.analyses?.find(a => a.sourceId === sourceId);
30
+ setPlan(p => ({ ...p,
31
+ groups: p.groups.map(g => ({ ...g, sourceIds: g.id === target ? [...g.sourceIds.filter(id => id !== sourceId), sourceId] : g.sourceIds.filter(id => id !== sourceId) })),
32
+ unassigned: [...p.unassigned.filter(s => s.sourceId !== sourceId), ...(target ? [] : [{ sourceId, reason: analysis?.uncertainty || '暂留待整理,后续再判断关联' }])],
33
+ }));
34
+ }
35
+ function removeGroup(id) {
36
+ setPlan(p => ({ ...p, groups: p.groups.filter(g => g.id !== id), unassigned: [...p.unassigned, ...(p.groups.find(g => g.id === id)?.sourceIds || []).map(sourceId => ({ sourceId, reason: '已移出分组,保留待整理' }))] }));
37
+ }
38
+ async function save(confirm, openSource) {
39
+ if (busy)
40
+ return;
41
+ setBusy(true);
42
+ setError('');
43
+ try {
44
+ let currentVersion = version;
45
+ if (!locked) {
46
+ const value = { ...plan, applyClassification: apply, groups: groups.map(g => ({ ...g, tags: splitTags(tagText[g.id] || '') })) };
47
+ const edited = await post(`/knowledge/jobs/${job.id}/plan`, { expectedRoot: root, version, plan: value }, 'PATCH');
48
+ currentVersion = edited.planVersion;
49
+ setVersion(currentVersion);
50
+ }
51
+ if (confirm) {
52
+ await post(`/knowledge/jobs/${job.id}/confirm-plan`, { expectedRoot: root, version: currentVersion, applyClassification: apply, expectedModel: model.model, confirmed: true });
53
+ onNotify(`${groups.length} 组资料已提交生成草稿;${plan.unassigned.length} 份保留待整理`);
54
+ onRefresh();
55
+ }
56
+ else if (openSource)
57
+ onOpenSource(openSource);
58
+ else {
59
+ onNotify('分组方案已保存,来源分类尚未修改');
60
+ onRefresh();
61
+ }
62
+ }
63
+ catch (e) {
64
+ setError(e.message);
65
+ if (confirm)
66
+ onRefresh();
67
+ }
68
+ finally {
69
+ setBusy(false);
70
+ }
71
+ }
72
+ async function rollback() {
73
+ setBusy(true);
74
+ setError('');
75
+ try {
76
+ await post(`/knowledge/jobs/${job.id}/rollback-plan`, { expectedRoot: root, confirmed: true });
77
+ onNotify('未完成的分类修改已回退');
78
+ onRefresh();
79
+ }
80
+ catch (e) {
81
+ setError(e.message);
82
+ onRefresh();
83
+ }
84
+ finally {
85
+ setBusy(false);
86
+ }
87
+ }
88
+ return _jsxs("div", { className: "kl-grouping-editor", children: [_jsxs("div", { className: "kl-grouping-summary", children: [_jsxs("b", { children: [groups.length, " \u4E2A\u5206\u7EC4"] }), _jsxs("span", { children: [plan.unassigned.length, " \u4EFD\u5F85\u6574\u7406"] })] }), _jsx("p", { className: "kl-knowledge-model", children: "\u5148\u6838\u5BF9\u9010\u7BC7\u5206\u6790\u4E0E\u5206\u7EC4\u7406\u7531\u3002\u65E0\u5173\u8054\u7684\u8D44\u6599\u53EF\u72EC\u7ACB\u6210\u7EC4\uFF0C\u4FE1\u606F\u4E0D\u8DB3\u7684\u8D44\u6599\u53EF\u4EE5\u7559\u5F85\u6574\u7406\u3002\u4FEE\u6539\u540E\u8BF7\u4FDD\u5B58\u65B9\u6848\uFF0C\u518D\u5173\u95ED\u7A97\u53E3\u3002" }), job.error && _jsx("p", { role: "alert", className: "kl-form-error", children: job.error }), locked && _jsxs("div", { className: "kl-grouping-recovery", role: "status", children: [_jsx("strong", { children: "\u4E0A\u6B21\u5F52\u7C7B\u5C1A\u672A\u5B8C\u6210" }), _jsxs("p", { children: [job.classification.steps.filter(s => s.done).length, " / ", job.classification.steps.length, " \u4EFD\u5DF2\u5B8C\u6210", job.classification.direction === 'rollback' ? '回退' : '归类', "\u3002\u65B9\u6848\u5DF2\u9501\u5B9A\uFF0C\u53EF\u7EE7\u7EED\u5904\u7406\u6216\u56DE\u9000\u5DF2\u66F4\u6539\u7684\u5206\u7C7B\u3002"] }), _jsx("button", { className: "kl-secondary", disabled: busy, onClick: () => void rollback(), children: job.classification.direction === 'rollback' ? '继续回退分类' : '回退已更改的分类' })] }), _jsx("div", { className: "kl-grouping-groups", children: plan.groups.map((group, index) => _jsxs("section", { className: "kl-grouping-group", "aria-label": `分组 ${index + 1}`, children: [_jsxs("div", { className: "kl-grouping-group-title", children: [_jsxs("span", { children: ["\u5206\u7EC4 ", index + 1, " \u00B7 ", group.sourceIds.length, " \u4EFD\u8D44\u6599"] }), _jsx("button", { className: "kl-icon-button kl-danger", "aria-label": `删除分组 ${index + 1}`, disabled: busy || locked, onClick: () => removeGroup(group.id), children: _jsx(Icon, { name: "trash", size: 15 }) })] }), _jsxs("label", { children: ["\u5206\u7EC4\u540D\u79F0", _jsx("input", { "aria-label": `分组名称 ${index + 1}`, maxLength: 200, value: group.title, disabled: busy || locked, onChange: e => groupValue(group.id, { title: e.target.value }) })] }), _jsx("p", { className: "kl-grouping-reason", children: group.reason }), _jsxs("details", { className: "kl-grouping-settings", children: [_jsxs("summary", { children: ["\u5206\u7C7B\u4E0E\u5206\u7EC4\u8BBE\u7F6E ", _jsx("span", { children: group.category })] }), _jsxs("label", { children: ["\u5206\u7EC4\u7406\u7531", _jsx("textarea", { "aria-label": `分组理由 ${index + 1}`, rows: 2, maxLength: 1200, value: group.reason, disabled: busy || locked, onChange: e => groupValue(group.id, { reason: e.target.value }) })] }), _jsxs("div", { className: "kl-knowledge-fields", children: [_jsxs("label", { children: ["\u6765\u6E90\u5206\u7C7B", _jsx("input", { "aria-label": `来源分类 ${index + 1}`, value: group.category, maxLength: 180, disabled: busy || locked, onChange: e => groupValue(group.id, { category: e.target.value }) })] }), _jsxs("label", { children: ["\u5206\u7EC4\u6807\u7B7E", _jsx("input", { "aria-label": `分组标签 ${index + 1}`, value: tagText[group.id] || '', disabled: busy || locked, onChange: e => setTagText(v => ({ ...v, [group.id]: e.target.value })) })] })] })] }), _jsx("ul", { children: group.sourceIds.map(id => _jsx("li", { children: job.sources.find(s => s.id === id)?.title }, id)) }), !group.sourceIds.length && _jsx("p", { className: "kl-muted", children: "\u6682\u65E0\u8D44\u6599\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u5FFD\u7565\u7A7A\u5206\u7EC4\u3002" })] }, group.id)) }), _jsxs("button", { className: "kl-secondary", disabled: busy || locked || plan.groups.length >= 10, onClick: () => newGroup(), children: [_jsx(Icon, { name: "plus", size: 14 }), "\u65B0\u589E\u5206\u7EC4"] }), _jsx("h3", { className: "kl-grouping-section-title", children: "\u9010\u7BC7\u5206\u6790\u4E0E\u6210\u5458\u8C03\u6574" }), job.sources.map(source => {
89
+ const analysis = job.analyses?.find(a => a.sourceId === source.id);
90
+ const unassigned = plan.unassigned.find(s => s.sourceId === source.id);
91
+ return _jsxs("article", { className: "kl-grouping-source", "aria-label": `${source.title} 的分析`, children: [_jsxs("div", { className: "kl-grouping-source-title", children: [_jsx("strong", { children: source.title }), _jsx("span", { className: "kl-knowledge-badge", children: analysis?.certainty === 'uncertain' ? '需核对' : '主题建议' })] }), _jsx("p", { children: analysis?.theme }), _jsxs("label", { children: ["\u6240\u5C5E\u5206\u7EC4", _jsxs("select", { "aria-label": `所属分组 ${source.title}`, value: plan.groups.find(g => g.sourceIds.includes(source.id))?.id || '', disabled: busy || locked, onChange: e => assign(source.id, e.target.value), children: [_jsx("option", { value: "", children: "\u4FDD\u7559\u5F85\u6574\u7406" }), plan.groups.map(g => _jsx("option", { value: g.id, children: g.title || '未命名分组' }, g.id)), _jsx("option", { value: "__new__", children: "\u5355\u72EC\u6210\u7EC4" })] })] }), unassigned && _jsxs("label", { children: ["\u5F85\u6574\u7406\u7406\u7531", _jsx("input", { "aria-label": `待整理理由 ${source.title}`, maxLength: 1200, value: unassigned.reason, disabled: busy || locked, onChange: e => setPlan(p => ({ ...p, unassigned: p.unassigned.map(s => s.sourceId === source.id ? { ...s, reason: e.target.value } : s) })) })] }), _jsxs("details", { children: [_jsx("summary", { children: "\u5206\u6790\u8BE6\u60C5\u4E0E\u6765\u6E90" }), _jsx("p", { children: analysis?.summary }), _jsxs("p", { children: ["\u7528\u9014\uFF1A", analysis?.usage] }), _jsxs("p", { children: ["\u6982\u5FF5\uFF1A", analysis?.concepts.join('、')] }), _jsxs("p", { children: ["\u5EFA\u8BAE\u5206\u7C7B\uFF1A", analysis?.category, " \u00B7 \u6807\u7B7E\uFF1A", analysis?.tags.join('、') || '无'] }), analysis?.uncertainty && _jsxs("p", { children: ["\u5F85\u6838\u5B9E\uFF1A", analysis.uncertainty] }), _jsxs("button", { className: "kl-secondary", disabled: busy || locked, onClick: () => void save(false, source.id), children: ["\u4FDD\u5B58\u65B9\u6848\u5E76\u67E5\u770B\u6765\u6E90", _jsx(Icon, { name: "arrow", size: 14 })] })] })] }, source.id);
92
+ }), _jsxs("label", { className: "kl-grouping-sync", children: [_jsx("input", { type: "checkbox", "aria-label": "\u540C\u6B65\u6765\u6E90\u5206\u7C7B\u548C\u6807\u7B7E", checked: apply, disabled: busy || locked, onChange: e => setApply(e.target.checked) }), _jsx("span", { children: "\u786E\u8BA4\u540E\u540C\u6B65\u6765\u6E90\u5206\u7C7B\u548C\u6807\u7B7E\u5230\u672C\u5730\u76EE\u5F55" })] }), _jsx("p", { className: "kl-knowledge-model", children: "\u540C\u6B65\u65F6\u4F1A\u79FB\u52A8\u5DF2\u5206\u7EC4\u8D44\u6599\u7684\u76EE\u5F55\u3001\u4FDD\u7559\u539F\u6807\u7B7E\u5E76\u6DFB\u52A0\u5206\u7EC4\u6807\u7B7E\uFF1B\u672A\u5206\u7EC4\u8D44\u6599\u4FDD\u6301\u5F85\u6574\u7406\u3002\u5173\u95ED\u540C\u6B65\u53EF\u53EA\u6309\u7EC4\u751F\u6210\u8349\u7A3F\u3002\u6BCF\u7EC4\u751F\u6210\u540E\u4ECD\u9700\u5355\u72EC\u786E\u8BA4\u4FDD\u5B58\u6B63\u5F0F\u7B14\u8BB0\u3002" }), _jsxs("p", { className: "kl-knowledge-model", children: ["\u751F\u6210\u8349\u7A3F\u4F7F\u7528\u6A21\u578B\uFF1A", model.model || '尚未配置', "\u3002", model.model !== job.model && `逐篇分析时使用的是 ${job.model || '未记录模型'}。`, "\u786E\u8BA4\u540E\u5C06\u7EE7\u7EED\u5411\u8BE5\u6A21\u578B\u53D1\u9001\u5404\u7EC4\u6B63\u6587\u53CA\u68C0\u7D22\u5230\u7684\u76F8\u5173\u77E5\u8BC6\u7B14\u8BB0\uFF0C\u7528\u4E8E\u751F\u6210\u8349\u7A3F\u548C\u5224\u65AD\u589E\u91CF\u8865\u5145\u3002\u672C\u6B21\u667A\u80FD\u6574\u7406\u53CA\u5176\u5B50\u4EFB\u52A1\u5408\u8BA1\u6700\u591A 80 \u6B21\u6A21\u578B\u8BF7\u6C42\uFF08\u5DF2\u4F7F\u7528 ", job.modelCalls || 0, " \u6B21\uFF09\uFF0C\u53EF\u80FD\u4EA7\u751F\u8C03\u7528\u8D39\u7528\u3002"] }), !!groups.length && !model.available && _jsx("p", { role: "alert", className: "kl-form-error", children: model.reason || '请先配置生成模型' }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [!locked && _jsx("button", { className: "kl-secondary", disabled: busy, onClick: () => void save(false), children: "\u4FDD\u5B58\u5206\u7EC4\u65B9\u6848" }), _jsx("button", { className: "kl-primary", disabled: busy || (!!groups.length && !model.available) || job.classification?.direction === 'rollback', onClick: () => void save(true), children: busy ? '正在处理…' : locked ? '继续归类并生成草稿' : groups.length ? `确认${apply ? '归类' : '分组'}并生成 ${groups.length} 份草稿` : '确认保留全部待整理' })] })] });
93
+ }
@@ -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
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import type { KnowledgeJob, KnowledgeOverview, KnowledgeState } from '../types.js';
3
+ export declare function KnowledgeBadge({ state }: {
4
+ state?: KnowledgeState;
5
+ }): React.JSX.Element;
6
+ export declare function StartKnowledgeDialog({ root, sourceIds, model, initialMode, onClose, onCreated }: {
7
+ root: string;
8
+ sourceIds: string[];
9
+ model: KnowledgeOverview;
10
+ initialMode?: 'smart' | 'compose';
11
+ onClose: () => void;
12
+ onCreated: (job: KnowledgeJob) => void;
13
+ }): React.JSX.Element;
14
+ interface JobsProps {
15
+ root: string;
16
+ jobId?: string;
17
+ overview: KnowledgeOverview;
18
+ onClose: () => void;
19
+ onRefresh: () => void;
20
+ onRegenerate: (ids: string[], mode?: 'smart' | 'compose') => void;
21
+ onOpenSource: (id: string, segmentId?: string) => void;
22
+ onSaved: (id: string) => void;
23
+ onNotify: (message: string) => void;
24
+ }
25
+ export declare function KnowledgeJobsDialog(props: JobsProps): React.JSX.Element;
26
+ export {};
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { api, post } from './api.js';
4
+ import { Dialog } from './Dialog.js';
5
+ import { Icon } from './icons.js';
6
+ import { GroupingEditor } from './Grouping.js';
7
+ const stateLabels = { pending: '待整理', running: '整理中', grouping: '待分组确认', draft: '待确认', completed: '已整理', failed: '整理失败' };
8
+ const jobLabels = { queued: '排队中', running: '整理中', grouping: '待分组确认', classifying: '归类中', grouped: '已分组', draft: '待确认', completed: '已保存', failed: '失败', cancelled: '已取消', discarded: '已舍弃' };
9
+ export function KnowledgeBadge({ state }) {
10
+ return _jsx("span", { className: `kl-knowledge-badge ${state?.status || 'pending'}`, children: stateLabels[state?.status || 'pending'] });
11
+ }
12
+ export function StartKnowledgeDialog({ root, sourceIds, model, initialMode, onClose, onCreated }) {
13
+ const [sources, setSources] = useState();
14
+ const [topic, setTopic] = useState('');
15
+ const [error, setError] = useState('');
16
+ const [busy, setBusy] = useState(false);
17
+ const [mode, setMode] = useState(initialMode || (sourceIds.length > 1 ? 'smart' : 'compose'));
18
+ const smart = mode === 'smart';
19
+ useEffect(() => {
20
+ const abort = new AbortController();
21
+ void Promise.all(sourceIds.map(id => api(`/materials/${id}`, { signal: abort.signal }))).then(setSources).catch(e => { if (!abort.signal.aborted)
22
+ setError(e.message); });
23
+ return () => abort.abort();
24
+ }, [sourceIds]);
25
+ const characters = sources?.reduce((sum, source) => sum + source.content.length, 0) || 0;
26
+ const invalidSource = sources?.some(s => s.deletedAt || s.kind === 'note' || s.status !== 'ready');
27
+ async function start(event) {
28
+ event.preventDefault();
29
+ setBusy(true);
30
+ setError('');
31
+ try {
32
+ onCreated(await post('/knowledge/jobs', { expectedRoot: root, sourceIds, topic, mode }));
33
+ }
34
+ catch (e) {
35
+ setError(e.message);
36
+ }
37
+ finally {
38
+ setBusy(false);
39
+ }
40
+ }
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 })] })] }), 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
+ }
44
+ export function KnowledgeJobsDialog(props) {
45
+ const { root, overview, onClose, onRefresh, onRegenerate, onOpenSource, onSaved, onNotify } = props;
46
+ const [selected, setSelected] = useState(props.jobId);
47
+ const [error, setError] = useState('');
48
+ const [busy, setBusy] = useState(false);
49
+ const job = overview.jobs.find(j => j.id === selected);
50
+ async function action(name) {
51
+ if (!job)
52
+ return;
53
+ setBusy(true);
54
+ setError('');
55
+ try {
56
+ await post(`/knowledge/jobs/${job.id}/${name}`, { expectedRoot: root });
57
+ onRefresh();
58
+ }
59
+ catch (e) {
60
+ setError(e.message);
61
+ }
62
+ finally {
63
+ setBusy(false);
64
+ }
65
+ }
66
+ return _jsx(Dialog, { title: job?.status === 'draft' ? '确认知识笔记' : job?.status === 'grouping' ? '确认智能分组' : '知识整理', description: "\u5206\u6790\u3001\u5206\u7EC4\u65B9\u6848\u4E0E\u8349\u7A3F\u4FDD\u5B58\u5728\u672C\u5730\u3002\u6B63\u5F0F\u77E5\u8BC6\u7B14\u8BB0\u4ECD\u9700\u9010\u4EFD\u786E\u8BA4\u4FDD\u5B58\u3002", onClose: () => { if (!busy)
67
+ onClose(); }, children: _jsxs("div", { className: "kl-knowledge-jobs", children: [!!selected && _jsx("button", { className: "kl-knowledge-back", onClick: () => { setSelected(undefined); setError(''); }, children: "\u2190 \u5168\u90E8\u6574\u7406\u4EFB\u52A1" }), !overview.supported && _jsx("p", { role: "alert", className: "kl-form-error", children: overview.reason }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), !selected ? _jsx(_Fragment, { children: !overview.jobs.length ? _jsxs("div", { className: "kl-knowledge-empty", children: [_jsx(Icon, { name: "note", size: 26 }), _jsx("p", { children: "\u8FD8\u6CA1\u6709\u6574\u7406\u4EFB\u52A1" }), _jsx("small", { children: "\u6253\u5F00\u4E00\u4EFD\u8D44\u6599\u70B9\u51FB\u201C\u751F\u6210\u77E5\u8BC6\u7B14\u8BB0\u201D\uFF0C\u6216\u5728\u5217\u8868\u4E2D\u9009\u62E9\u201C\u667A\u80FD\u6574\u7406\u201D\u3002" })] }) : overview.jobs.map(item => _jsxs("button", { className: "kl-knowledge-job", onClick: () => { setSelected(item.id); setError(''); }, children: [_jsxs("div", { children: [_jsx("strong", { children: item.topic || item.sources.map(s => s.title).join('、') }), _jsx("span", { className: `kl-knowledge-badge ${item.status}`, children: jobLabels[item.status] })] }), _jsxs("p", { children: [item.mode === 'smart' ? '智能分组 · ' : item.parentId ? '分组子任务 · ' : '', item.sourceIds.length, " \u4EFD\u6765\u6E90 \u00B7 ", new Date(item.updatedAt).toLocaleString('zh-CN')] }), item.error && _jsx("small", { children: item.error })] }, item.id)) }) : !job ? _jsx("p", { className: "kl-muted", children: "\u6B63\u5728\u8BFB\u53D6\u6574\u7406\u4EFB\u52A1\u2026" }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "kl-knowledge-job-heading", children: [_jsx("strong", { children: job.topic || job.sources.map(s => s.title).join('、') }), _jsx("span", { className: `kl-knowledge-badge ${job.status}`, children: jobLabels[job.status] })] }), job.status === 'grouping' && job.plan ? _jsx(GroupingEditor, { root: root, job: job, model: overview, onRefresh: onRefresh, onOpenSource: onOpenSource, onNotify: onNotify }, `${job.id}:${job.planVersion}`) : job.status === 'draft' && job.draft ? _jsx(DraftEditor, { root: root, job: job, onRefresh: onRefresh, onOpenSource: onOpenSource, onSaved: onSaved, onNotify: onNotify }, `${job.id}:${job.draftVersion}`) : _jsxs(_Fragment, { children: [_jsxs("p", { className: "kl-knowledge-progress", role: "status", children: [['queued', 'running'].includes(job.status) && _jsx("span", { className: "kl-spinner" }), job.progress] }), job.error && _jsx("p", { role: "alert", className: "kl-form-error", children: job.error }), job.model && _jsxs("p", { className: "kl-muted", children: ["\u6A21\u578B\uFF1A", job.model] }), job.status === 'completed' && job.noteId && (job.sourceIds.some(id => overview.states[id]?.noteIds.includes(job.noteId)) ? _jsx("button", { className: "kl-primary", onClick: () => onOpenSource(job.noteId), children: "\u6253\u5F00\u77E5\u8BC6\u7B14\u8BB0" }) : _jsx("p", { className: "kl-muted", children: "\u5173\u8054\u7B14\u8BB0\u5DF2\u79FB\u5165\u56DE\u6536\u7AD9\u6216\u6C38\u4E45\u5220\u9664\uFF0C\u53EF\u6062\u590D\u7B14\u8BB0\u6216\u91CD\u65B0\u6574\u7406\u3002" })), job.status === 'grouped' && _jsxs("div", { className: "kl-grouping-children", children: [job.children?.map(child => _jsxs("button", { className: "kl-knowledge-job", onClick: () => setSelected(child.jobId), children: [_jsxs("div", { children: [_jsx("strong", { children: job.plan?.groups.find(g => g.id === child.groupId)?.title || '分组草稿' }), _jsx("span", { className: "kl-knowledge-badge", children: jobLabels[overview.jobs.find(j => j.id === child.jobId)?.status || 'queued'] })] }), _jsx("p", { children: "\u6253\u5F00\u672C\u7EC4\u4EFB\u52A1\uFF0C\u5355\u72EC\u6838\u5BF9\u5E76\u4FDD\u5B58\u77E5\u8BC6\u7B14\u8BB0" })] }, child.jobId)), !!job.plan?.unassigned.length && _jsxs("div", { className: "kl-grouping-pending", children: [_jsx("strong", { children: "\u4FDD\u7559\u5F85\u6574\u7406" }), job.plan.unassigned.map(item => _jsxs("p", { children: [job.sources.find(s => s.id === item.sourceId)?.title, "\uFF1A", item.reason] }, item.sourceId))] })] })] }), _jsxs("div", { className: "kl-knowledge-actions", children: [['queued', 'running'].includes(job.status) && _jsx("button", { className: "kl-secondary", disabled: busy, onClick: () => void action('cancel'), children: "\u53D6\u6D88\u6574\u7406" }), ['draft', 'failed', 'grouping'].includes(job.status) && _jsxs("button", { className: "kl-secondary kl-danger", disabled: busy || !!job.classification, onClick: () => void action('discard'), children: ["\u820D\u5F03", job.status === 'draft' ? '草稿' : job.status === 'grouping' ? '方案' : '任务'] }), ['failed', 'cancelled', 'discarded', 'completed'].includes(job.status) && _jsx("button", { className: "kl-secondary", onClick: () => onRegenerate(job.sourceIds, job.mode), children: "\u91CD\u65B0\u6574\u7406" })] })] })] }) });
68
+ }
69
+ function DraftEditor({ root, job, onRefresh, onOpenSource, onSaved, onNotify }) {
70
+ const [draft, setDraft] = useState(job.draft);
71
+ const [integration, setIntegration] = useState(job.integration);
72
+ const [version, setVersion] = useState(job.draftVersion);
73
+ const [tags, setTags] = useState(draft.tags.join(','));
74
+ const [category, setCategory] = useState(draft.category || '知识笔记');
75
+ const [busy, setBusy] = useState(false);
76
+ const [error, setError] = useState('');
77
+ async function save(confirm, source) {
78
+ if (busy)
79
+ return;
80
+ setBusy(true);
81
+ setError('');
82
+ try {
83
+ const edited = await post(`/knowledge/jobs/${job.id}`, { expectedRoot: root, version, draft: { ...draft, category, tags: tags.split(/[,,]/).map(t => t.trim()).filter(Boolean) }, ...(integration ? { integration: { choice: integration.choice, content: integration.content } } : {}) }, 'PATCH');
84
+ setVersion(edited.draftVersion);
85
+ if (confirm) {
86
+ const result = await post(`/knowledge/jobs/${job.id}/confirm`, { expectedRoot: root, version: edited.draftVersion, category, confirmed: true, integrationChoice: integration?.choice });
87
+ onSaved(result.material.id);
88
+ }
89
+ else if (source)
90
+ onOpenSource(source.id, source.segment);
91
+ else {
92
+ onNotify('草稿已保存,正式知识笔记尚未修改');
93
+ onRefresh();
94
+ }
95
+ }
96
+ catch (e) {
97
+ setError(e.message);
98
+ }
99
+ finally {
100
+ setBusy(false);
101
+ }
102
+ }
103
+ const updating = integration?.choice === 'append' || integration?.choice === 'duplicate';
104
+ const confirmText = integration?.choice === 'append' ? '确认补充已有笔记' : integration?.choice === 'duplicate' ? '确认仅关联来源' : '确认保存笔记';
105
+ return _jsxs("form", { className: "kl-knowledge-draft", onSubmit: e => { e.preventDefault(); void save(true); }, children: [_jsx("p", { className: "kl-knowledge-model", children: "\u8FD9\u662F\u6A21\u578B\u751F\u6210\u7684\u8349\u7A3F\uFF0C\u8BF7\u6838\u5BF9\u7406\u89E3\u4E0E\u8BC1\u636E\u3002\u4FEE\u6539\u540E\u8BF7\u4FDD\u5B58\u8349\u7A3F\uFF0C\u518D\u5173\u95ED\u7A97\u53E3\u3002" }), integration && _jsxs("section", { className: "kl-note-integration", "aria-label": "\u5DF2\u6709\u77E5\u8BC6\u5BF9\u6BD4", children: [_jsxs("strong", { children: ["\u5EFA\u8BAE", integration.recommendation === 'append' ? '补充已有笔记' : integration.recommendation === 'duplicate' ? '仅关联已有笔记' : '新建笔记'] }), _jsx("p", { children: integration.reason }), _jsxs("p", { className: "kl-knowledge-model", children: ["\u68C0\u7D22\u4E86 ", integration.search.scanned, " \u4EFD\u6709\u6548\u7B14\u8BB0\uFF0C\u627E\u5230 ", integration.search.matched, " \u4EFD\u8BCD\u9762\u76F8\u5173\u7B14\u8BB0\uFF0C\u672C\u6B21\u5B8C\u6574\u5BF9\u6BD4 ", integration.search.compared, " \u4EFD\uFF08\u6700\u591A 5 \u4EFD\uFF09\u3002", integration.search.oversized > 0 && `${integration.search.oversized} 份超过对比长度上限。`, "\u672A\u5339\u914D\u4E0D\u4EE3\u8868\u6574\u5E93\u6CA1\u6709\u76F8\u5173\u77E5\u8BC6\u3002"] }), [['overlaps', '重复内容'], ['additions', '新增知识'], ['conflicts', '差异与待核实']].map(([key, title]) => !!integration[key].length && _jsxs("div", { className: "kl-integration-findings", children: [_jsx("span", { children: title }), _jsx("ul", { children: integration[key].map((item, index) => _jsx("li", { children: item }, index)) })] }, key)), integration.target && _jsxs(_Fragment, { children: [_jsxs("p", { className: "kl-integration-target", children: [integration.target.title, " \u00B7 ", integration.target.category, " \u00B7 \u7248\u672C ", integration.target.revision] }), _jsxs("fieldset", { className: "kl-knowledge-mode", children: [_jsx("legend", { children: "\u672C\u6B21\u5982\u4F55\u4FDD\u5B58" }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "integration-choice", "aria-label": integration.recommendation === 'duplicate' ? '仅关联已有笔记' : '补充已有笔记', checked: updating, onChange: () => setIntegration({ ...integration, choice: integration.recommendation }) }), integration.recommendation === 'duplicate' ? '仅关联来源,不重复正文' : '追加补充到已有笔记'] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "integration-choice", "aria-label": "\u53E6\u5B58\u4E3A\u65B0\u7B14\u8BB0", checked: !updating, onChange: () => setIntegration({ ...integration, choice: 'new' }) }), "\u53E6\u5B58\u4E3A\u65B0\u7B14\u8BB0"] })] }), _jsxs("details", { children: [_jsx("summary", { children: "\u67E5\u770B\u5DF2\u6709\u7B14\u8BB0\u539F\u6587" }), _jsx("pre", { className: "kl-integration-preview", children: integration.target.content }), _jsxs("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: () => void save(false, { id: integration.target.id }), children: ["\u4FDD\u5B58\u8349\u7A3F\u5E76\u6253\u5F00\u5DF2\u6709\u7B14\u8BB0", _jsx(Icon, { name: "arrow", size: 14 })] })] })] })] }), updating ? _jsxs(_Fragment, { children: [_jsx("p", { className: "kl-knowledge-model", children: "\u786E\u8BA4\u540E\u4FDD\u7559\u5DF2\u6709\u6807\u9898\u3001\u5206\u7C7B\u3001\u6B63\u6587\u4E0E\u65E7\u6BB5\u843D\u5F15\u7528\uFF0C\u5408\u5E76\u65B0\u6765\u6E90\u548C\u6807\u7B7E\u3002\u66F4\u65B0\u524D\u540E\u5FEB\u7167\u4FDD\u5B58\u5728\u672C\u5730\uFF1B\u5BA1\u6838\u671F\u95F4\u7B14\u8BB0\u53D8\u5316\u4F1A\u8981\u6C42\u91CD\u65B0\u6838\u5BF9\u3002" }), integration?.choice === 'append' ? _jsxs(_Fragment, { children: [_jsxs("label", { children: ["\u672C\u6B21\u65B0\u589E\u6B63\u6587", _jsx("textarea", { "aria-label": "\u672C\u6B21\u65B0\u589E\u6B63\u6587", required: true, rows: 12, maxLength: 50_000, value: integration.content, onChange: e => setIntegration({ ...integration, content: e.target.value }) })] }), _jsxs("details", { children: [_jsx("summary", { children: "\u66F4\u65B0\u540E\u6B63\u6587\u9884\u89C8" }), _jsxs("pre", { className: "kl-integration-preview", children: [integration.target.content, '\n\n## 知识补充 · ' + new Date().toISOString().slice(0, 10) + '\n\n' + integration.content] })] })] }) : _jsx("p", { className: "kl-muted", children: "\u672C\u6B21\u6CA1\u6709\u9700\u8981\u65B0\u589E\u7684\u6B63\u6587\u3002\u786E\u8BA4\u540E\u53EA\u628A\u8FD9\u4E9B\u6765\u6E90\u5173\u8054\u5230\u5DF2\u6709\u7B14\u8BB0\uFF0C\u4ECD\u4FDD\u7559\u5BF9\u6BD4\u7406\u7531\u4F9B\u6838\u67E5\u3002" })] }) : _jsxs(_Fragment, { children: [_jsxs("label", { children: ["\u7B14\u8BB0\u6807\u9898", _jsx("input", { "aria-label": "\u7B14\u8BB0\u6807\u9898", required: true, maxLength: 300, value: draft.title, onChange: e => setDraft({ ...draft, title: e.target.value }) })] }), _jsxs("label", { children: ["\u7B14\u8BB0\u6B63\u6587", _jsx("textarea", { "aria-label": "\u7B14\u8BB0\u6B63\u6587", required: true, rows: 14, maxLength: 50_000, value: draft.content, onChange: e => setDraft({ ...draft, content: e.target.value }) })] }), _jsxs("label", { children: ["\u7B14\u8BB0\u5206\u7C7B", _jsx("input", { "aria-label": "\u7B14\u8BB0\u5206\u7C7B", value: category, onChange: e => setCategory(e.target.value), maxLength: 180 })] })] }), _jsxs("label", { children: [updating ? '新增标签' : '标签', _jsx("input", { "aria-label": "\u7B14\u8BB0\u6807\u7B7E", value: tags, onChange: e => setTags(e.target.value), placeholder: "\u9017\u53F7\u5206\u9694" })] }), _jsxs("details", { className: "kl-knowledge-citations", children: [_jsxs("summary", { children: [draft.citations.length, " \u6761\u6765\u6E90\u5F15\u7528"] }), _jsx("p", { children: "\u67E5\u770B\u6765\u6E90\u524D\u4F1A\u5148\u4FDD\u5B58\u5F53\u524D\u8349\u7A3F\u3002" }), draft.citations.map((citation, index) => _jsxs("button", { type: "button", disabled: busy, onClick: () => void save(false, { id: citation.materialId, segment: citation.segmentId }), children: [_jsx(Icon, { name: "file", size: 14 }), _jsxs("span", { children: [job.sources.find(s => s.id === citation.materialId)?.title, " / ", citation.segmentId] }), _jsx(Icon, { name: "arrow", size: 14 })] }, index))] }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [_jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: () => void save(false), children: "\u4FDD\u5B58\u8349\u7A3F" }), _jsx("button", { className: "kl-primary", disabled: busy, children: busy ? '正在保存…' : confirmText })] })] });
106
+ }
@@ -0,0 +1,7 @@
1
+ export declare const API = "/knowledge-library/api";
2
+ export declare class ApiError extends Error {
3
+ readonly status: number;
4
+ constructor(message: string, status: number);
5
+ }
6
+ export declare function api<T = any>(path: string, options?: RequestInit): Promise<T>;
7
+ export declare const post: <T>(path: string, value: unknown, method?: string, signal?: AbortSignal) => Promise<T>;
package/dist/ui/api.js ADDED
@@ -0,0 +1,16 @@
1
+ export const API = '/knowledge-library/api';
2
+ export class ApiError extends Error {
3
+ status;
4
+ constructor(message, status) {
5
+ super(message);
6
+ this.status = status;
7
+ }
8
+ }
9
+ export async function api(path, options = {}) {
10
+ const response = await fetch(API + path, { ...options, headers: { 'Content-Type': 'application/json', 'X-Knowledge-Library': '1', ...options.headers } });
11
+ const data = await response.json();
12
+ if (!response.ok)
13
+ throw new ApiError(data.error || `请求失败:${response.status}`, response.status);
14
+ return data;
15
+ }
16
+ export const post = (path, value, method = 'POST', signal) => api(path, { method, body: JSON.stringify(value), signal });
package/dist/ui/icons.js CHANGED
@@ -19,6 +19,8 @@ export function Icon({ name, size = 20 }) {
19
19
  copy: _jsxs(_Fragment, { children: [_jsx("rect", { x: "8", y: "8", width: "12", height: "13", rx: "2" }), _jsx("path", { d: "M16 8V3H3v13h5" })] }),
20
20
  download: _jsx(_Fragment, { children: _jsx("path", { d: "M12 3v13m-5-5 5 5 5-5M3 17v4h18v-4" }) }),
21
21
  settings: _jsxs(_Fragment, { children: [_jsx("path", { d: "M4 7h16M4 17h16" }), _jsx("circle", { cx: "9", cy: "7", r: "3" }), _jsx("circle", { cx: "15", cy: "17", r: "3" })] }),
22
+ trash: _jsx(_Fragment, { children: _jsx("path", { d: "M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" }) }),
23
+ restore: _jsx(_Fragment, { children: _jsx("path", { d: "M3 10a9 9 0 1 1 2 9M3 4v6h6M12 7v5l3 2" }) }),
22
24
  };
23
25
  return _jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: paths[name] || paths.file });
24
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-knowledge-library",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "本地优先的 DeepSeek Harness 资料库:采集、分类、检索与可追溯知识笔记",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,10 @@
64
64
  "dev": "tsx src/standalone.ts",
65
65
  "start": "node dist/standalone.js",
66
66
  "test:ui": "python tests/run_ui.py",
67
+ "test:dsh:directory": "python tests/run_dsh_directory.py",
68
+ "test:knowledge:ui": "python tests/run_knowledge_ui.py",
69
+ "test:dsh:knowledge": "python tests/run_dsh_knowledge.py",
70
+ "test:images:ui": "python tests/run_images_ui.py",
67
71
  "pack:check": "node scripts/check-package.mjs",
68
72
  "verify": "npm run build && npm test && npm run pack:check",
69
73
  "prepack": "npm run build"