dsh-knowhub 0.8.3 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/lib/client/index.js +328 -56
- package/lib/host/kb/route.js +14 -3
- package/lib/host/kb/store.js +15 -4
- package/lib/host/route.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,11 +116,18 @@ cmd /c mklink /J "$env:USERPROFILE\.dsh\profiles\knowhub\node_modules\dsh-knowhu
|
|
|
116
116
|
| Repo Wiki | 按知识库分组的 Wiki 文档树 + 阅读器:正文、来源清单、版本历史、编辑、标记过时、父级 / 子级知识(最多 5 层)、是否有帮助反馈、导出 Markdown;状态过滤与真实分页 |
|
|
117
117
|
| 知识卡片 | 决策 / 约定 / 问题 / 待办四类知识,类型与状态过滤,列表 + 阅读器 |
|
|
118
118
|
| 记忆 | 记忆 / 偏好两类知识,类型与状态过滤,列表 + 阅读器 |
|
|
119
|
-
| 知识库 |
|
|
119
|
+
| 知识库 | 知识库列表(固定列出全部:用户级 + 各工作区,不做工作区筛选)+ 详情:新建与编辑配置、初始化与增量任务(进度 / 日志 / 取消 / 重试)、项目索引、模型调用记录、知识列表分页、导出全部、导入 Markdown、删除 |
|
|
120
120
|
| 待审核 | 候选 / 冲突 / 草稿 / 过时四类队列,真实计数、筛选、排序、批量审批;点「查看详情与差异」进入文档对比页;版本历史与审核日志 |
|
|
121
121
|
| 检索 | 关键词 + 标签 + 知识库 / 类型 / 状态 / 排序;显示来源、作用域、审核状态与检索模式;结果分页加载 |
|
|
122
122
|
| 设置 | 默认策略、提炼开关与阈值、审核人与自动批准、知识注入开关与预算、备份保留、排除目录、存储位置迁移、备份预览与恢复、诊断信息 |
|
|
123
123
|
|
|
124
|
+
**交互反馈**
|
|
125
|
+
|
|
126
|
+
- 操作结果以右下角浮层提示呈现:成功、失败、提示分色显示,失败提示停留更久,均可手动关闭,不占用页面布局。
|
|
127
|
+
- 标记过时、恢复历史版本、迁移存储、从备份恢复等破坏性操作先弹确认对话框,展示操作说明与影响范围;Esc 取消、Enter 确认。
|
|
128
|
+
- 弹窗统一使用 DSH 视觉规范:半透明遮罩 + 背景模糊、层级阴影、进出场动画;打开时锁定页面滚动,Esc 或点击遮罩关闭。
|
|
129
|
+
- 表单内的字段校验仍在输入框旁就地提示;接口错误既进入浮层提示,也在受影响的区域给出定位。
|
|
130
|
+
|
|
124
131
|
## 配置项
|
|
125
132
|
|
|
126
133
|
设置保存在知识库存储的配置库中,面板「设置」页可修改。
|
package/lib/client/index.js
CHANGED
|
@@ -364,6 +364,44 @@ Object.assign(styles, {
|
|
|
364
364
|
seg: { height: '24px', padding: '0 10px', borderRadius: '6px', border: '0', background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', cursor: 'pointer' },
|
|
365
365
|
segActive: { height: '24px', padding: '0 10px', borderRadius: '6px', border: '0', background: 'var(--dsw-alias-interactive-bg-hover)', color: 'var(--dsw-alias-label-primary)', fontSize: '12px', cursor: 'pointer' },
|
|
366
366
|
});
|
|
367
|
+
// ---- 弹窗与浮层反馈(0.8.5:对齐 DSH 遮罩/阴影 token 与交互习惯)----
|
|
368
|
+
Object.assign(styles, {
|
|
369
|
+
modalBackdrop: {
|
|
370
|
+
position: 'fixed', inset: 0, zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
371
|
+
padding: '24px', background: 'var(--dsw-alias-bg-mask-2, rgba(0,0,0,0.45))',
|
|
372
|
+
backdropFilter: 'blur(var(--dsw-mask-blur, 2px))', WebkitBackdropFilter: 'blur(var(--dsw-mask-blur, 2px))',
|
|
373
|
+
animation: 'kh-backdrop-in 120ms ease-out',
|
|
374
|
+
},
|
|
375
|
+
modalCard: {
|
|
376
|
+
width: 'min(640px, calc(100vw - 48px))', maxHeight: '84vh', overflow: 'hidden',
|
|
377
|
+
display: 'flex', flexDirection: 'column', minHeight: 0,
|
|
378
|
+
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
379
|
+
borderRadius: '14px', boxShadow: 'var(--dsw-shadow-lv3, 0 24px 60px rgba(0,0,0,0.35))',
|
|
380
|
+
animation: 'kh-modal-in 140ms cubic-bezier(0.2, 0.9, 0.3, 1)',
|
|
381
|
+
},
|
|
382
|
+
confirmBody: {
|
|
383
|
+
padding: '2px 0 8px', fontSize: '13px', lineHeight: '21px',
|
|
384
|
+
color: 'var(--dsw-alias-label-secondary)', whiteSpace: 'pre-wrap',
|
|
385
|
+
},
|
|
386
|
+
toastHost: {
|
|
387
|
+
position: 'fixed', right: '20px', bottom: '20px', zIndex: 80, display: 'flex', flexDirection: 'column',
|
|
388
|
+
alignItems: 'flex-end', gap: '8px', maxWidth: 'min(420px, calc(100vw - 40px))', pointerEvents: 'none',
|
|
389
|
+
},
|
|
390
|
+
toast: {
|
|
391
|
+
pointerEvents: 'auto', display: 'flex', alignItems: 'flex-start', gap: '8px', padding: '9px 12px',
|
|
392
|
+
borderRadius: '10px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-1)',
|
|
393
|
+
boxShadow: 'var(--dsw-shadow-lv2, 0 8px 24px rgba(0,0,0,0.18))', color: 'var(--dsw-alias-label-primary)',
|
|
394
|
+
fontSize: '13px', lineHeight: '20px', animation: 'kh-toast-in 160ms ease-out',
|
|
395
|
+
},
|
|
396
|
+
toastError: { borderColor: 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent)' },
|
|
397
|
+
toastSuccess: { borderColor: 'color-mix(in srgb, var(--dsw-alias-state-business-primary) 42%, transparent)' },
|
|
398
|
+
toastIcon: { flexShrink: 0, width: '18px', textAlign: 'center', fontSize: '12px', fontWeight: 600, lineHeight: '20px' },
|
|
399
|
+
toastText: { flex: 1, minWidth: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' },
|
|
400
|
+
toastClose: {
|
|
401
|
+
flexShrink: 0, width: '20px', height: '20px', padding: 0, borderRadius: '6px', border: 0,
|
|
402
|
+
background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', fontSize: '11px',
|
|
403
|
+
},
|
|
404
|
+
});
|
|
367
405
|
const STYLE_ELEMENT_ID = 'knowhub-theme-styles';
|
|
368
406
|
const THEME_STYLESHEET = [
|
|
369
407
|
'.knowhub-page :focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}',
|
|
@@ -373,7 +411,14 @@ const THEME_STYLESHEET = [
|
|
|
373
411
|
'.knowhub-page .kh-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)!important}',
|
|
374
412
|
'.knowhub-page .kh-scroll{scrollbar-gutter:stable;--dsh-scrollbar-width:9px;--dsh-scrollbar-thumb-border:2px}',
|
|
375
413
|
'.knowhub-page input[type=checkbox],.knowhub-page input[type=radio]{accent-color:var(--dsw-alias-state-business-primary)}',
|
|
376
|
-
'
|
|
414
|
+
'.knowhub-page button{transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease,opacity 120ms ease}',
|
|
415
|
+
'.knowhub-page button:not(:disabled):active{transform:translateY(0.5px)}',
|
|
416
|
+
'.knowhub-page button:disabled{cursor:not-allowed}',
|
|
417
|
+
'.knowhub-page [role=alert]{scroll-margin-top:12px}',
|
|
418
|
+
'@keyframes kh-backdrop-in{from{opacity:0}to{opacity:1}}',
|
|
419
|
+
'@keyframes kh-modal-in{from{opacity:0;transform:translateY(6px) scale(0.985)}to{opacity:1;transform:none}}',
|
|
420
|
+
'@keyframes kh-toast-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}',
|
|
421
|
+
'@media (prefers-reduced-motion: reduce){.knowhub-page *,.knowhub-page *::before,.knowhub-page *::after{transition:none!important;animation-duration:1ms!important}}',
|
|
377
422
|
].join('\n');
|
|
378
423
|
/** 注入一次主题样式表;卸载时移除。 */
|
|
379
424
|
function injectStyles() {
|
|
@@ -402,7 +447,10 @@ async function request(path, options = {}) {
|
|
|
402
447
|
throw new Error('HTTP ' + response.status + ':响应不是合法 JSON');
|
|
403
448
|
}
|
|
404
449
|
if (!payload || payload.ok !== true) {
|
|
405
|
-
|
|
450
|
+
const failure = new Error(payload?.error?.message ?? ('HTTP ' + response.status));
|
|
451
|
+
failure.status = response.status;
|
|
452
|
+
failure.code = payload?.error?.code;
|
|
453
|
+
throw failure;
|
|
406
454
|
}
|
|
407
455
|
return payload.value;
|
|
408
456
|
}
|
|
@@ -424,6 +472,170 @@ function message(error) {
|
|
|
424
472
|
function Badge(props) {
|
|
425
473
|
return h('span', { style: props.style ?? styles.badge }, props.text);
|
|
426
474
|
}
|
|
475
|
+
const toastState = { items: [], listeners: new Set(), seq: 0 };
|
|
476
|
+
const TOAST_LIMIT = 4;
|
|
477
|
+
const TOAST_TIMEOUT = { success: 4000, info: 5000, error: 8000 };
|
|
478
|
+
function emitToasts() {
|
|
479
|
+
for (const listener of [...toastState.listeners]) {
|
|
480
|
+
try {
|
|
481
|
+
listener();
|
|
482
|
+
}
|
|
483
|
+
catch { /* 订阅方异常不影响其它 */ }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function removeToast(id) {
|
|
487
|
+
if (!toastState.items.some((item) => item.id === id))
|
|
488
|
+
return;
|
|
489
|
+
toastState.items = toastState.items.filter((item) => item.id !== id);
|
|
490
|
+
emitToasts();
|
|
491
|
+
}
|
|
492
|
+
/** 推入一条浮层提示(右下角、自动消失、可手动关闭)。 */
|
|
493
|
+
function pushToast(text, kind = 'success') {
|
|
494
|
+
const content = String(text ?? '').trim();
|
|
495
|
+
if (content === '')
|
|
496
|
+
return 0;
|
|
497
|
+
toastState.seq += 1;
|
|
498
|
+
const item = { id: toastState.seq, text: content, kind };
|
|
499
|
+
// 同文案不堆叠:先移除旧条目,只保留最新一条并重新计时
|
|
500
|
+
const rest = toastState.items.filter((entry) => entry.text !== content);
|
|
501
|
+
toastState.items = rest.concat([item]).slice(-TOAST_LIMIT);
|
|
502
|
+
emitToasts();
|
|
503
|
+
if (typeof setTimeout === 'function') {
|
|
504
|
+
const timer = setTimeout(() => removeToast(item.id), TOAST_TIMEOUT[kind] ?? 4000);
|
|
505
|
+
// 不拖住宿主进程退出(Node 下 unref,浏览器里 timer 没有这个方法)
|
|
506
|
+
if (typeof timer?.unref === 'function')
|
|
507
|
+
timer.unref();
|
|
508
|
+
}
|
|
509
|
+
return item.id;
|
|
510
|
+
}
|
|
511
|
+
const TOAST_ICON = { success: '✓', error: '!', info: 'i' };
|
|
512
|
+
function ToastHost() {
|
|
513
|
+
const [items, setItems] = R.useState(toastState.items);
|
|
514
|
+
R.useEffect(() => {
|
|
515
|
+
const listener = () => setItems(toastState.items.slice());
|
|
516
|
+
toastState.listeners.add(listener);
|
|
517
|
+
listener();
|
|
518
|
+
return () => { toastState.listeners.delete(listener); };
|
|
519
|
+
}, []);
|
|
520
|
+
if (items.length === 0)
|
|
521
|
+
return null;
|
|
522
|
+
return h('div', { style: styles.toastHost }, items.map((item) => h('div', {
|
|
523
|
+
key: item.id,
|
|
524
|
+
role: item.kind === 'error' ? 'alert' : 'status',
|
|
525
|
+
'aria-live': item.kind === 'error' ? 'assertive' : 'polite',
|
|
526
|
+
style: { ...styles.toast, ...(item.kind === 'error' ? styles.toastError : item.kind === 'success' ? styles.toastSuccess : {}) },
|
|
527
|
+
}, h('span', {
|
|
528
|
+
style: {
|
|
529
|
+
...styles.toastIcon,
|
|
530
|
+
color: item.kind === 'error' ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
531
|
+
},
|
|
532
|
+
}, TOAST_ICON[item.kind]), h('span', { style: styles.toastText }, item.text), h('button', {
|
|
533
|
+
type: 'button', style: styles.toastClose, title: '关闭提示', 'aria-label': '关闭提示',
|
|
534
|
+
onClick: () => removeToast(item.id),
|
|
535
|
+
}, '✕'))));
|
|
536
|
+
}
|
|
537
|
+
/** 组件内的临时 notice 文本统一转成 toast:不占布局、自动消失。 */
|
|
538
|
+
function useNoticeToast(notice, clear) {
|
|
539
|
+
R.useEffect(() => {
|
|
540
|
+
const text = String(notice ?? '').trim();
|
|
541
|
+
if (text === '')
|
|
542
|
+
return;
|
|
543
|
+
pushToast(text, 'success');
|
|
544
|
+
clear();
|
|
545
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
546
|
+
}, [notice]);
|
|
547
|
+
}
|
|
548
|
+
const confirmState = { current: null, listeners: new Set() };
|
|
549
|
+
function emitConfirm() {
|
|
550
|
+
for (const listener of [...confirmState.listeners]) {
|
|
551
|
+
try {
|
|
552
|
+
listener();
|
|
553
|
+
}
|
|
554
|
+
catch { /* 忽略 */ }
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Promise 化的确认框:await askConfirm({ text }) 返回 true / false。 */
|
|
558
|
+
function askConfirm(options) {
|
|
559
|
+
return new Promise((resolve) => {
|
|
560
|
+
if (confirmState.current !== null) {
|
|
561
|
+
const previous = confirmState.current;
|
|
562
|
+
confirmState.current = null;
|
|
563
|
+
try {
|
|
564
|
+
previous.resolve(false);
|
|
565
|
+
}
|
|
566
|
+
catch { /* 忽略 */ }
|
|
567
|
+
}
|
|
568
|
+
confirmState.current = { ...options, resolve };
|
|
569
|
+
emitConfirm();
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function settleConfirm(ok) {
|
|
573
|
+
const current = confirmState.current;
|
|
574
|
+
if (current === null)
|
|
575
|
+
return;
|
|
576
|
+
confirmState.current = null;
|
|
577
|
+
emitConfirm();
|
|
578
|
+
try {
|
|
579
|
+
current.resolve(ok);
|
|
580
|
+
}
|
|
581
|
+
catch { /* 忽略 */ }
|
|
582
|
+
}
|
|
583
|
+
/** DSH 风格弹窗外壳:统一遮罩模糊、圆角阴影、进出场动画、Esc 关闭与页面滚动锁。 */
|
|
584
|
+
function Modal(props) {
|
|
585
|
+
const { onClose, maxWidth, children, closeOnMask = true } = props;
|
|
586
|
+
R.useEffect(() => {
|
|
587
|
+
const doc = globalThis.document;
|
|
588
|
+
if (!doc || !doc.body)
|
|
589
|
+
return undefined;
|
|
590
|
+
const previous = doc.body.style.overflow;
|
|
591
|
+
doc.body.style.overflow = 'hidden';
|
|
592
|
+
return () => { doc.body.style.overflow = previous; };
|
|
593
|
+
}, []);
|
|
594
|
+
R.useEffect(() => {
|
|
595
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function')
|
|
596
|
+
return undefined;
|
|
597
|
+
const onKey = (event) => { if (event.key === 'Escape')
|
|
598
|
+
onClose(); };
|
|
599
|
+
window.addEventListener('keydown', onKey);
|
|
600
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
601
|
+
}, [onClose]);
|
|
602
|
+
return h('div', {
|
|
603
|
+
style: styles.modalBackdrop,
|
|
604
|
+
onMouseDown: (event) => { if (closeOnMask && event.target === event.currentTarget)
|
|
605
|
+
onClose(); },
|
|
606
|
+
}, h('div', {
|
|
607
|
+
style: maxWidth ? { ...styles.modalCard, maxWidth } : styles.modalCard,
|
|
608
|
+
role: 'dialog', 'aria-modal': 'true',
|
|
609
|
+
}, children));
|
|
610
|
+
}
|
|
611
|
+
function ConfirmHost() {
|
|
612
|
+
const [pending, setPending] = R.useState(confirmState.current);
|
|
613
|
+
R.useEffect(() => {
|
|
614
|
+
const listener = () => setPending(confirmState.current);
|
|
615
|
+
confirmState.listeners.add(listener);
|
|
616
|
+
listener();
|
|
617
|
+
return () => { confirmState.listeners.delete(listener); };
|
|
618
|
+
}, []);
|
|
619
|
+
R.useEffect(() => {
|
|
620
|
+
if (pending === null || typeof window === 'undefined' || typeof window.addEventListener !== 'function')
|
|
621
|
+
return undefined;
|
|
622
|
+
const onKey = (event) => {
|
|
623
|
+
if (event.key !== 'Enter')
|
|
624
|
+
return;
|
|
625
|
+
if (typeof event.preventDefault === 'function')
|
|
626
|
+
event.preventDefault();
|
|
627
|
+
settleConfirm(true);
|
|
628
|
+
};
|
|
629
|
+
window.addEventListener('keydown', onKey);
|
|
630
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
631
|
+
}, [pending]);
|
|
632
|
+
if (pending === null)
|
|
633
|
+
return null;
|
|
634
|
+
const confirmStyle = pending.danger
|
|
635
|
+
? { ...styles.primary, border: '1px solid var(--dsw-alias-state-error-primary)', background: 'var(--dsw-alias-state-error-primary)' }
|
|
636
|
+
: styles.primary;
|
|
637
|
+
return h(Modal, { onClose: () => settleConfirm(false), maxWidth: '440px' }, h('div', { style: styles.dialog }, h('div', { style: styles.dialogHead }, h('div', { style: { minWidth: 0 } }, h('div', { style: styles.dialogTitle }, pending.title ?? '确认操作'))), h('div', { style: styles.dialogBody }, h('div', { style: styles.confirmBody }, pending.text)), h('div', { style: styles.dialogFoot }, h('div', { style: styles.dialogFootHint }, 'Esc 取消 · Enter 确认'), h('div', { style: { display: 'flex', gap: '8px' } }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => settleConfirm(false) }, pending.cancelText ?? '取消'), h('button', { type: 'button', style: confirmStyle, className: pending.danger ? '' : 'kh-primary', onClick: () => settleConfirm(true) }, pending.confirmText ?? '确认')))));
|
|
638
|
+
}
|
|
427
639
|
// ---------- 知识库 ----------
|
|
428
640
|
function CreateKbForm(props) {
|
|
429
641
|
const { models, workspaces, currentWorkspaceId, onCancel, onCreated } = props;
|
|
@@ -712,12 +924,14 @@ function KbView(props) {
|
|
|
712
924
|
// 增量状态读取失败不影响主流程
|
|
713
925
|
}
|
|
714
926
|
}, []);
|
|
927
|
+
// 列表范围:始终展示全部知识库(用户级 + 所有工作区),不按工作区筛选。
|
|
928
|
+
// 知识库允许建在任意工作区;若按 cwd 反查的「当前工作区」过滤,会出现建完看不见、
|
|
929
|
+
// 同名却报已存在的错觉,所以这里固定请求 all=1。
|
|
715
930
|
const load = R.useCallback(async () => {
|
|
716
931
|
setLoading(true);
|
|
717
932
|
setError(null);
|
|
718
933
|
try {
|
|
719
|
-
const
|
|
720
|
-
const value = await request('/kb' + query);
|
|
934
|
+
const value = await request('/kb?all=1');
|
|
721
935
|
setList(value.knowledgeBases ?? []);
|
|
722
936
|
}
|
|
723
937
|
catch (failure) {
|
|
@@ -726,7 +940,7 @@ function KbView(props) {
|
|
|
726
940
|
finally {
|
|
727
941
|
setLoading(false);
|
|
728
942
|
}
|
|
729
|
-
}, [
|
|
943
|
+
}, []);
|
|
730
944
|
R.useEffect(() => { void load(); }, [load]);
|
|
731
945
|
R.useEffect(() => { void load(); }, [props.refreshKey]);
|
|
732
946
|
// 打开详情后每 1.5s 轮询任务;succeeded 时刷新文档与索引摘要(真实数据,无本地伪造)。
|
|
@@ -938,30 +1152,33 @@ function KbView(props) {
|
|
|
938
1152
|
}), h('button', {
|
|
939
1153
|
type: 'button', style: styles.primary, className: 'kh-primary',
|
|
940
1154
|
onClick: () => { setShowForm(!showForm); setDetail(null); },
|
|
941
|
-
}, showForm ? '收起' : '+ 新建')), error !== null ? h('div', { style: { ...styles.error, margin: '0 10px 10px' } }, error) : null, h('div', { style: styles.listScroll, className: 'kh-scroll' }, list === null ? (loading ? h('div', { style: styles.listEmpty }, '正在加载知识库列表…') : null)
|
|
942
|
-
: filteredList.length === 0 ? h('div', { style: styles.listEmpty }, list.length === 0 ? '
|
|
1155
|
+
}, showForm ? '收起' : '+ 新建')), h('div', { style: { padding: '0 12px 8px' } }, h('div', { style: styles.hint }, '已列出全部知识库(用户级 + 各工作区)' + (list !== null ? ',共 ' + list.length + ' 个' : '') + '。')), error !== null ? h('div', { style: { ...styles.error, margin: '0 10px 10px' } }, error) : null, h('div', { style: styles.listScroll, className: 'kh-scroll' }, list === null ? (loading ? h('div', { style: styles.listEmpty }, '正在加载知识库列表…') : null)
|
|
1156
|
+
: filteredList.length === 0 ? h('div', { style: styles.listEmpty }, list.length === 0 ? '还没有任何知识库。点击「+ 新建」创建。' : '没有匹配的知识库名称。')
|
|
943
1157
|
: filteredList.map((kb) => h('button', {
|
|
944
1158
|
key: kb.id, type: 'button',
|
|
945
1159
|
style: detail !== null && detail.knowledgeBase.id === kb.id ? styles.treeRowActive : styles.treeRow,
|
|
946
1160
|
onClick: () => void openDetail(kb.id),
|
|
947
|
-
}, h('span', { style: styles.treeTitle }, h('span', { style: styles.treeName }, kb.name), h('span', { style: styles.treeMeta }, (kb.scope === 'user' ? '用户级' : '工作区级') + ' · ' + (STATUS_LABEL[kb.status] ?? kb.status) + ' · ' + (kb.documentCount ?? 0) + ' 条')))))), h('div', { style: styles.readerCol }, h('div', { style: styles.readerHead }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h2', { style: styles.readerTitle }, detail !== null ? detail.knowledgeBase.name : '知识库'), detail !== null
|
|
1161
|
+
}, h('span', { style: styles.treeTitle }, h('span', { style: styles.treeName }, kb.name), h('span', { style: styles.treeMeta }, (kb.scope === 'user' ? '用户级' : '工作区级 · ' + (kb.workspaceTitle ?? kb.workspaceId ?? '未知工作区')) + ' · ' + (STATUS_LABEL[kb.status] ?? kb.status) + ' · ' + (kb.documentCount ?? 0) + ' 条')))))), h('div', { style: styles.readerCol }, h('div', { style: styles.readerHead }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h2', { style: styles.readerTitle }, detail !== null ? detail.knowledgeBase.name : '知识库'), detail !== null
|
|
948
1162
|
? h('div', { style: styles.readerMeta }, h(Badge, { text: detail.knowledgeBase.scope === 'user' ? '用户级' : '工作区级', style: styles.badgeBlue }), h(Badge, { text: STATUS_LABEL[detail.knowledgeBase.status] ?? detail.knowledgeBase.status, style: styles.badgeGreen }), h('span', null, '模型:' + detail.knowledgeBase.modelId + '(' + detail.knowledgeBase.modelProvider + ')'), h('span', null, '策略:' + detail.knowledgeBase.modelStrategy), h('span', null, detail.documentCount + ' 条知识'), detail.knowledgeBase.workspacePath ? h('span', null, '工作区:' + detail.knowledgeBase.workspacePath) : null)
|
|
949
|
-
: h('div', { style: styles.readerMeta }, '知识库列表与详情')), h('div', { style: styles.headerActions }, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: exportAll }, '导出全部') : null, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setImportOpen(true) }, '导入 Markdown') : null, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setEditOpen(true) }, '编辑配置') : null, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading, onClick: () => void load() }, loading ? '刷新中…' : '刷新'))), h('div', { style: styles.readerScroll, className: 'kh-scroll' }, h('div', { style: styles.readerInner }, detailSections()))), showForm ? h(
|
|
950
|
-
setShowForm(false); } }, h('div', { style: styles.modalCard }, h(CreateKbForm, {
|
|
1163
|
+
: h('div', { style: styles.readerMeta }, '知识库列表与详情')), h('div', { style: styles.headerActions }, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: exportAll }, '导出全部') : null, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setImportOpen(true) }, '导入 Markdown') : null, detail !== null ? h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setEditOpen(true) }, '编辑配置') : null, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading, onClick: () => void load() }, loading ? '刷新中…' : '刷新'))), h('div', { style: styles.readerScroll, className: 'kh-scroll' }, h('div', { style: styles.readerInner }, detailSections()))), showForm ? h(Modal, { onClose: () => setShowForm(false), maxWidth: '680px' }, h(CreateKbForm, {
|
|
951
1164
|
models, workspaces, currentWorkspaceId: currentWorkspace?.workspaceId ?? '',
|
|
952
1165
|
onCancel: () => setShowForm(false),
|
|
953
|
-
onCreated: async (kb) => {
|
|
954
|
-
|
|
955
|
-
|
|
1166
|
+
onCreated: async (kb) => {
|
|
1167
|
+
setShowForm(false);
|
|
1168
|
+
notify('知识库「' + kb.name + '」已创建。下一步:在详情里点「开始初始化」。');
|
|
1169
|
+
// 列表固定展示全部知识库,新建后直接刷新并打开详情即可。
|
|
1170
|
+
await load();
|
|
1171
|
+
await openDetail(kb.id);
|
|
1172
|
+
},
|
|
1173
|
+
})) : null, importOpen && detail !== null ? h(Modal, { onClose: () => setImportOpen(false), maxWidth: '760px' }, h(ImportMarkdownForm, {
|
|
956
1174
|
kbId: detail.knowledgeBase.id, kbName: detail.knowledgeBase.name,
|
|
957
1175
|
onCancel: () => setImportOpen(false),
|
|
958
1176
|
onDone: async () => { await openDetail(detail.knowledgeBase.id); await load(); },
|
|
959
|
-
}))
|
|
960
|
-
setEditOpen(false); } }, h('div', { style: styles.modalCard }, h(EditKbForm, {
|
|
1177
|
+
})) : null, editOpen && detail !== null ? h(Modal, { onClose: () => setEditOpen(false), maxWidth: '680px' }, h(EditKbForm, {
|
|
961
1178
|
kb: detail.knowledgeBase, models,
|
|
962
1179
|
onCancel: () => setEditOpen(false),
|
|
963
1180
|
onSaved: async (kb) => { setEditOpen(false); notify('知识库「' + kb.name + '」配置已更新'); await openDetail(kb.id); await load(); },
|
|
964
|
-
}))
|
|
1181
|
+
})) : null);
|
|
965
1182
|
}
|
|
966
1183
|
// ---------- 检索 ----------
|
|
967
1184
|
function SearchView(props) {
|
|
@@ -1492,7 +1709,7 @@ function DocumentDiffPage(props) {
|
|
|
1492
1709
|
? h('span', null, base.version === null || base.version === undefined ? '没有历史版本可对比,当前正文全部显示为新增' : '左为上一版内容,右为当前内容')
|
|
1493
1710
|
: h('span', null, doc ? '左为当前正式内容,右为候选内容' : '该候选是新增知识,下方内容全部为新增'), diff.truncated ? h('span', null, '内容过长,差异已截断展示') : null), h('div', { style: { ...styles.row, alignItems: 'center', marginTop: '10px' } }, h('span', { style: { ...styles.diffStat, ...styles.diffStatAdd } }, '新增 ' + String(stats.added) + ' 行'), h('span', { style: { ...styles.diffStat, ...styles.diffStatDel } }, '删除 ' + String(stats.removed) + ' 行'), h('span', { style: styles.diffStat }, '未变 ' + String(stats.unchanged) + ' 行'), h('div', { style: styles.segGroup }, h('button', { type: 'button', style: viewMode === 'unified' ? styles.segActive : styles.seg, onClick: () => { setViewMode('unified'); setActiveChange(0); } }, '统一视图'), h('button', { type: 'button', style: viewMode === 'split' ? styles.segActive : styles.seg, onClick: () => { setViewMode('split'); setActiveChange(0); } }, '并排视图')), h('div', { style: styles.segGroup }, viewMode === 'split' ? null : h('button', { type: 'button', style: wrap ? styles.segActive : styles.seg, onClick: () => setWrap(!wrap) }, '自动换行'), h('button', { type: 'button', style: expandAll ? styles.segActive : styles.seg, onClick: () => { setExpandAll(!expandAll); setFolded({}); } }, '展开全部上下文'))), h('div', { style: { ...styles.row, alignItems: 'center', marginTop: '6px' } }, changeCount > 0
|
|
1494
1711
|
? h('span', { style: styles.diffStat }, '第 ' + String(Math.min(activeChange, changeCount - 1) + 1) + ' / ' + String(changeCount) + ' 处改动')
|
|
1495
|
-
: h('span', { style: styles.hint }, '两侧内容一致,没有改动可跳转'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: changeCount === 0 || activeChange <= 0, onClick: () => jumpToChange(activeChange - 1) }, '↑ 上一处改动'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: changeCount === 0 || activeChange >= changeCount - 1, onClick: () => jumpToChange(activeChange + 1) }, '↓ 下一处改动'), h('span', { style: styles.hint }, '改动块左侧有蓝色标线;未变内容超过 7 行会自动折叠。')), diffPane, busy !== null ? h('div', { style: styles.hint }, busy + '…') : null, error !== null ? h('div', { style: styles.error
|
|
1712
|
+
: h('span', { style: styles.hint }, '两侧内容一致,没有改动可跳转'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: changeCount === 0 || activeChange <= 0, onClick: () => jumpToChange(activeChange - 1) }, '↑ 上一处改动'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: changeCount === 0 || activeChange >= changeCount - 1, onClick: () => jumpToChange(activeChange + 1) }, '↓ 下一处改动'), h('span', { style: styles.hint }, '改动块左侧有蓝色标线;未变内容超过 7 行会自动折叠。')), diffPane, busy !== null ? h('div', { style: styles.hint }, busy + '…') : null, error !== null ? h('div', { style: styles.error, role: 'alert' }, error) : null, actions);
|
|
1496
1713
|
}
|
|
1497
1714
|
function ReviewView(props) {
|
|
1498
1715
|
const { knowledgeBases } = props;
|
|
@@ -1501,6 +1718,7 @@ function ReviewView(props) {
|
|
|
1501
1718
|
const [loading, setLoading] = R.useState(false);
|
|
1502
1719
|
const [error, setError] = R.useState(null);
|
|
1503
1720
|
const [notice, setNotice] = R.useState('');
|
|
1721
|
+
useNoticeToast(notice, () => setNotice(''));
|
|
1504
1722
|
const [busy, setBusy] = R.useState(null);
|
|
1505
1723
|
const [diffPage, setDiffPage] = R.useState(null);
|
|
1506
1724
|
const [proposalEdit, setProposalEdit] = R.useState(null);
|
|
@@ -1760,7 +1978,7 @@ function ReviewView(props) {
|
|
|
1760
1978
|
const proposalCard = (item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, checkBox('proposal:' + item.id), item.title, h(Badge, { text: PROPOSAL_KIND_LABEL[item.kind] ?? item.kind, style: styles.badgeAmber }), item.docKind ? h(Badge, { text: KIND_LABEL[item.docKind] ?? item.docKind }) : null, h(Badge, { text: '候选 ' + String(item.id).slice(-6) })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '来源:' + (item.source ?? '—')), h('span', null, '提交人:' + (item.createdBy ?? '—')), h('span', null, '创建于 ' + new Date(item.createdAt).toLocaleString('zh-CN'))), item.reason ? h('div', { style: styles.snippet }, '说明:' + item.reason) : null, h('div', { style: styles.snippet }, String(item.body ?? '').slice(0, 260) + (String(item.body ?? '').length > 260 ? '…' : '')), h('div', { style: styles.field, marginTop: '8px' }, noteInput(item.id)), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDiff('proposal', item.id) }, '查看详情与差异'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('批准候选', () => request('/review/proposals/' + item.id + '/approve', { method: 'POST', body: { note: noteOf(item.id) || null } })) }, '批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => setProposalEdit({ id: item.id, body: String(item.body ?? ''), title: item.title }) }, '编辑后批准'), h('button', { type: 'button', style: styles.danger, className: 'kh-danger', disabled: busy !== null, onClick: () => void run('拒绝候选', () => request('/review/proposals/' + item.id + '/reject', { method: 'POST', body: { note: noteOf(item.id) || null } })) }, '拒绝')), proposalEdit !== null && proposalEdit.id === item.id ? h('div', { style: styles.field, marginTop: '10px' }, h('label', { style: styles.label }, '修改标题'), h('input', { style: styles.input, value: proposalEdit.title, onChange: (e) => setProposalEdit({ ...proposalEdit, title: e.target.value }) }), h('label', { style: { ...styles.label, marginTop: '8px' } }, '修改正文'), h('textarea', { style: { ...styles.textarea, minHeight: '140px' }, value: proposalEdit.body, onChange: (e) => setProposalEdit({ ...proposalEdit, body: e.target.value }) }), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('编辑后批准', () => request('/review/proposals/' + item.id + '/approve', { method: 'POST', body: { note: noteOf(item.id) || '编辑后批准', title: proposalEdit.title, body: proposalEdit.body } }), () => setProposalEdit(null)) }, '保存并批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setProposalEdit(null) }, '取消编辑'))) : null);
|
|
1761
1979
|
return h('div', null, h('div', { style: { ...styles.card, display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } }, h('select', { style: { ...styles.input, width: '200px' }, value: kbFilter, onChange: (e) => { setKbFilter(e.target.value); setPage(1); } }, h('option', { value: '' }, '全部知识库'), (knowledgeBases ?? []).map((kb) => h('option', { key: kb.id, value: kb.id }, kb.name))), h('select', { style: { ...styles.input, width: '150px' }, value: kindFilter, onChange: (e) => { setKindFilter(e.target.value); setPage(1); } }, h('option', { value: '' }, '全部候选类型'), h('option', { value: 'create' }, '新增知识'), h('option', { value: 'update' }, '更新知识'), h('option', { value: 'restore' }, '恢复知识'), h('option', { value: 'stale' }, '标记过时')), h('input', { style: { ...styles.input, width: '200px' }, value: qInput, placeholder: '关键词(标题/正文)', onChange: (e) => setQInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter')
|
|
1762
1980
|
applyFilters(); } }), h('input', { style: { ...styles.input, width: '160px' }, value: sourceInput, placeholder: '来源,如 pipeline', onChange: (e) => setSourceInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter')
|
|
1763
|
-
applyFilters(); } }), h('select', { style: { ...styles.input, width: '150px' }, value: sort, onChange: (e) => { setSort(e.target.value); setPage(1); } }, h('option', { value: 'newest' }, '最新提交优先'), h('option', { value: 'oldest' }, '最早提交优先'), h('option', { value: 'updated' }, '最近更新优先')), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: loading, onClick: applyFilters }, '筛选'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: resetFilters }, '清空'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading, onClick: () => void load() }, loading ? '刷新中…' : '刷新'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openLog() }, '审核日志')), h('div', { style: { ...styles.row, alignItems: 'center', marginTop: '8px' } }, h('span', { style: styles.badgeAmber }, '待审核合计 ' + pendingTotal + ' 项'), h('span', { style: styles.hint }, filterHint + ' · 第 ' + page + ' / ' + maxPages + ' 页(每页最多 ' + LIMIT + ' 条)'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page <= 1, onClick: () => setPage(page - 1) }, '上一页'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page >= maxPages, onClick: () => setPage(page + 1) }, '下一页')), selected.length > 0 ? h('div', { style: { ...styles.card, marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } }, h('span', null, '已选 ' + selected.length + ' 条'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void bulkRun('approve') }, '批量批准'), h('button', { type: 'button', style: styles.danger, className: 'kh-danger', disabled: busy !== null, onClick: () => void bulkRun('reject') }, '批量拒绝'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: clearSelection }, '清空选择'), h('span', { style: styles.hint }, '单次最多 ' + LIMIT + ' 条,执行前会二次确认')) : null, busy !== null ? h('div', { style: styles.hint }, busy + '…') : null, error !== null ? h('div', { style: styles.error }, error) : null, notice !== '' ? h('div', { style: styles.success }, notice) : null, queue === null ? h('div', { style: styles.empty }, loading ? '正在读取待审核数据…' : '暂无数据') : h('div', null, sectionHead('候选知识', counts.proposals, 'proposal', queue.proposals ?? []), (queue.proposals ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的候选。Agent 或面板提交的知识会先进入这里。') : (queue.proposals ?? []).map(proposalCard), sectionHead('版本冲突', counts.conflicts), (queue.conflicts ?? []).length === 0 ? h('div', { style: styles.empty }, '没有待处理冲突。') : (queue.conflicts ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, '冲突:' + (item.proposedTitle ?? item.docId), h(Badge, { text: '待处理', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '来源:' + (item.source ?? '—')), h('span', null, '基础版本:第 ' + item.baseVersion + ' 版 / 当前:第 ' + item.currentVersion + ' 版'), h('span', null, '检测于 ' + new Date(item.detectedAt).toLocaleString('zh-CN'))), h('div', { style: styles.snippet }, '提议内容:'), h('pre', { style: styles.pre }, String(item.proposedBody ?? '').slice(0, 1600)), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openDiff('conflict', item.id) }, '查看差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void run('保留当前', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'keep_current' } })) }, '保留当前知识'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('采用新提议', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'use_proposed' } })) }, '采用新提议(追加合并版本)'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => setConflictEdit({ id: item.id, title: String(item.proposedTitle ?? ''), body: String(item.proposedBody ?? '') }) }, '人工合并…')), conflictEdit !== null && conflictEdit.id === item.id ? h('div', { style: styles.field, marginTop: '10px' }, h('label', { style: styles.label }, '合并后标题'), h('input', { style: styles.input, value: conflictEdit.title, onChange: (e) => setConflictEdit({ ...conflictEdit, title: e.target.value }) }), h('label', { style: { ...styles.label, marginTop: '8px' } }, '合并后正文(预填新提议,可自由编辑)'), h('textarea', { style: { ...styles.textarea, minHeight: '180px' }, value: conflictEdit.body, onChange: (e) => setConflictEdit({ ...conflictEdit, body: e.target.value }) }), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('保存人工合并', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'merge', title: conflictEdit.title, body: conflictEdit.body } }), () => setConflictEdit(null)) }, '保存合并结果'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setConflictEdit(null) }, '取消合并'))) : null)), sectionHead('待审核草稿', counts.drafts, 'doc', queue.drafts ?? []), (queue.drafts ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的待审核草稿。') : (queue.drafts ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, checkBox('doc:' + item.id), item.title, h(Badge, { text: KIND_LABEL[item.kind] ?? item.kind }), h(Badge, { text: VERIFY_LABEL[item.verifyState] ?? item.verifyState }), h(Badge, { text: '第 ' + item.currentVersion + ' 版' })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '更新于 ' + new Date(item.updatedAt).toLocaleString('zh-CN')), h('span', null, item.path)), h('div', { style: { ...styles.row, marginTop: '8px' } }, h('div', { style: styles.field, flex: '1 1 260px' }, noteInput(item.id))), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDiff('document', item.id) }, '查看详情与差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openVersions(item.kbId, item.id) }, '版本历史'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('批准草稿', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'approve', note: noteOf(item.id) || null } })) }, '批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDraftEdit(item.kbId, item.id) }, '编辑后批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void run('标记过时', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'mark_stale', note: noteOf(item.id) || null } })) }, '标记过时'), h('button', { type: 'button', style: styles.danger, className: 'kh-danger', disabled: busy !== null, onClick: () => void run('拒绝草稿', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'reject', note: noteOf(item.id) || null } })) }, '拒绝(归档)')))), sectionHead('过时知识', counts.stale), (queue.staleDocuments ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的过时知识。') : (queue.staleDocuments ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, item.title, h(Badge, { text: '过时', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '更新于 ' + new Date(item.updatedAt).toLocaleString('zh-CN'))), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDiff('document', item.id) }, '查看详情与差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openVersions(item.kbId, item.id) }, '版本历史'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('恢复为已批准', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'approve', note: '确认内容仍有效' } })) }, '确认仍有效(恢复已批准)')))), maxPages > 1 ? h('div', { style: { ...styles.row, marginTop: '10px' } }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page <= 1, onClick: () => setPage(page - 1) }, '上一页'), h('span', { style: styles.hint }, '第 ' + page + ' / ' + maxPages + ' 页'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page >= maxPages, onClick: () => setPage(page + 1) }, '下一页')) : null), draftEdit !== null ? h('div', { style: styles.divider }, h('div', { style: styles.sectionTitle }, '编辑后批准草稿(将在第 ' + Number(draftEdit.version ?? 0) + ' 版基础上生成审核版本)'), h('label', { style: styles.label }, '标题'), h('input', { style: styles.input, value: draftEdit.title, onChange: (e) => setDraftEdit({ ...draftEdit, title: e.target.value }) }), h('label', { style: { ...styles.label, marginTop: '8px' } }, '正文(Markdown)'), h('textarea', { style: { ...styles.textarea, minHeight: '220px' }, value: draftEdit.body, onChange: (e) => setDraftEdit({ ...draftEdit, body: e.target.value }) }), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('编辑并批准草稿', () => request('/kb/' + draftEdit.kbId + '/docs/' + draftEdit.docId + '/review', { method: 'POST', body: { action: 'approve', title: draftEdit.title, body: draftEdit.body, note: '审核时编辑后批准' } }), () => setDraftEdit(null)) }, '保存并批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setDraftEdit(null) }, '取消'))) : null, docPanel !== null ? h('div', { style: styles.divider }, h('div', { style: styles.sectionTitle }, '版本历史(共 ' + docPanel.versions.length + ' 版,当前第 ' + docPanel.currentVersion + ' 版)'), docPanel.versions.map((item) => h('div', { key: item.version, style: styles.card }, h('div', { style: styles.cardTitle }, '第 ' + item.version + ' 版', h(Badge, { text: CHANGE_KIND_LABEL[item.changeKind] ?? item.changeKind }), item.version === docPanel.currentVersion ? h(Badge, { text: '当前', style: styles.badgeGreen }) : null), h('div', { style: styles.meta }, h('span', null, '来源:' + (item.source ?? '—')), h('span', null, '操作人:' + (item.actor ?? '—')), h('span', null, '时间:' + new Date(item.createdAt).toLocaleString('zh-CN'))), item.reason ? h('div', { style: styles.snippet }, '说明:' + item.reason) : null, h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: async () => {
|
|
1981
|
+
applyFilters(); } }), h('select', { style: { ...styles.input, width: '150px' }, value: sort, onChange: (e) => { setSort(e.target.value); setPage(1); } }, h('option', { value: 'newest' }, '最新提交优先'), h('option', { value: 'oldest' }, '最早提交优先'), h('option', { value: 'updated' }, '最近更新优先')), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: loading, onClick: applyFilters }, '筛选'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: resetFilters }, '清空'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading, onClick: () => void load() }, loading ? '刷新中…' : '刷新'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openLog() }, '审核日志')), h('div', { style: { ...styles.row, alignItems: 'center', marginTop: '8px' } }, h('span', { style: styles.badgeAmber }, '待审核合计 ' + pendingTotal + ' 项'), h('span', { style: styles.hint }, filterHint + ' · 第 ' + page + ' / ' + maxPages + ' 页(每页最多 ' + LIMIT + ' 条)'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page <= 1, onClick: () => setPage(page - 1) }, '上一页'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page >= maxPages, onClick: () => setPage(page + 1) }, '下一页')), selected.length > 0 ? h('div', { style: { ...styles.card, marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } }, h('span', null, '已选 ' + selected.length + ' 条'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void bulkRun('approve') }, '批量批准'), h('button', { type: 'button', style: styles.danger, className: 'kh-danger', disabled: busy !== null, onClick: () => void bulkRun('reject') }, '批量拒绝'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: clearSelection }, '清空选择'), h('span', { style: styles.hint }, '单次最多 ' + LIMIT + ' 条,执行前会二次确认')) : null, busy !== null ? h('div', { style: styles.hint }, busy + '…') : null, error !== null ? h('div', { style: styles.error, role: 'alert' }, error) : null, queue === null ? h('div', { style: styles.empty }, loading ? '正在读取待审核数据…' : '暂无数据') : h('div', null, sectionHead('候选知识', counts.proposals, 'proposal', queue.proposals ?? []), (queue.proposals ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的候选。Agent 或面板提交的知识会先进入这里。') : (queue.proposals ?? []).map(proposalCard), sectionHead('版本冲突', counts.conflicts), (queue.conflicts ?? []).length === 0 ? h('div', { style: styles.empty }, '没有待处理冲突。') : (queue.conflicts ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, '冲突:' + (item.proposedTitle ?? item.docId), h(Badge, { text: '待处理', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '来源:' + (item.source ?? '—')), h('span', null, '基础版本:第 ' + item.baseVersion + ' 版 / 当前:第 ' + item.currentVersion + ' 版'), h('span', null, '检测于 ' + new Date(item.detectedAt).toLocaleString('zh-CN'))), h('div', { style: styles.snippet }, '提议内容:'), h('pre', { style: styles.pre }, String(item.proposedBody ?? '').slice(0, 1600)), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openDiff('conflict', item.id) }, '查看差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void run('保留当前', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'keep_current' } })) }, '保留当前知识'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('采用新提议', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'use_proposed' } })) }, '采用新提议(追加合并版本)'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => setConflictEdit({ id: item.id, title: String(item.proposedTitle ?? ''), body: String(item.proposedBody ?? '') }) }, '人工合并…')), conflictEdit !== null && conflictEdit.id === item.id ? h('div', { style: styles.field, marginTop: '10px' }, h('label', { style: styles.label }, '合并后标题'), h('input', { style: styles.input, value: conflictEdit.title, onChange: (e) => setConflictEdit({ ...conflictEdit, title: e.target.value }) }), h('label', { style: { ...styles.label, marginTop: '8px' } }, '合并后正文(预填新提议,可自由编辑)'), h('textarea', { style: { ...styles.textarea, minHeight: '180px' }, value: conflictEdit.body, onChange: (e) => setConflictEdit({ ...conflictEdit, body: e.target.value }) }), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('保存人工合并', () => request('/review/conflicts/' + item.id + '/resolve', { method: 'POST', body: { mode: 'merge', title: conflictEdit.title, body: conflictEdit.body } }), () => setConflictEdit(null)) }, '保存合并结果'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setConflictEdit(null) }, '取消合并'))) : null)), sectionHead('待审核草稿', counts.drafts, 'doc', queue.drafts ?? []), (queue.drafts ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的待审核草稿。') : (queue.drafts ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, checkBox('doc:' + item.id), item.title, h(Badge, { text: KIND_LABEL[item.kind] ?? item.kind }), h(Badge, { text: VERIFY_LABEL[item.verifyState] ?? item.verifyState }), h(Badge, { text: '第 ' + item.currentVersion + ' 版' })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '更新于 ' + new Date(item.updatedAt).toLocaleString('zh-CN')), h('span', null, item.path)), h('div', { style: { ...styles.row, marginTop: '8px' } }, h('div', { style: styles.field, flex: '1 1 260px' }, noteInput(item.id))), h('div', { style: styles.row }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDiff('document', item.id) }, '查看详情与差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openVersions(item.kbId, item.id) }, '版本历史'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('批准草稿', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'approve', note: noteOf(item.id) || null } })) }, '批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDraftEdit(item.kbId, item.id) }, '编辑后批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void run('标记过时', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'mark_stale', note: noteOf(item.id) || null } })) }, '标记过时'), h('button', { type: 'button', style: styles.danger, className: 'kh-danger', disabled: busy !== null, onClick: () => void run('拒绝草稿', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'reject', note: noteOf(item.id) || null } })) }, '拒绝(归档)')))), sectionHead('过时知识', counts.stale), (queue.staleDocuments ?? []).length === 0 ? h('div', { style: styles.empty }, '没有符合条件的过时知识。') : (queue.staleDocuments ?? []).map((item) => h('div', { key: item.id, style: styles.card }, h('div', { style: styles.cardTitle }, item.title, h(Badge, { text: '过时', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识库:' + (item.kbName ?? item.kbId)), h('span', null, '更新于 ' + new Date(item.updatedAt).toLocaleString('zh-CN'))), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== null, onClick: () => void openDiff('document', item.id) }, '查看详情与差异'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => void openVersions(item.kbId, item.id) }, '版本历史'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('恢复为已批准', () => request('/kb/' + item.kbId + '/docs/' + item.id + '/review', { method: 'POST', body: { action: 'approve', note: '确认内容仍有效' } })) }, '确认仍有效(恢复已批准)')))), maxPages > 1 ? h('div', { style: { ...styles.row, marginTop: '10px' } }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page <= 1, onClick: () => setPage(page - 1) }, '上一页'), h('span', { style: styles.hint }, '第 ' + page + ' / ' + maxPages + ' 页'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: loading || page >= maxPages, onClick: () => setPage(page + 1) }, '下一页')) : null), draftEdit !== null ? h('div', { style: styles.divider }, h('div', { style: styles.sectionTitle }, '编辑后批准草稿(将在第 ' + Number(draftEdit.version ?? 0) + ' 版基础上生成审核版本)'), h('label', { style: styles.label }, '标题'), h('input', { style: styles.input, value: draftEdit.title, onChange: (e) => setDraftEdit({ ...draftEdit, title: e.target.value }) }), h('label', { style: { ...styles.label, marginTop: '8px' } }, '正文(Markdown)'), h('textarea', { style: { ...styles.textarea, minHeight: '220px' }, value: draftEdit.body, onChange: (e) => setDraftEdit({ ...draftEdit, body: e.target.value }) }), h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== null, onClick: () => void run('编辑并批准草稿', () => request('/kb/' + draftEdit.kbId + '/docs/' + draftEdit.docId + '/review', { method: 'POST', body: { action: 'approve', title: draftEdit.title, body: draftEdit.body, note: '审核时编辑后批准' } }), () => setDraftEdit(null)) }, '保存并批准'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setDraftEdit(null) }, '取消'))) : null, docPanel !== null ? h('div', { style: styles.divider }, h('div', { style: styles.sectionTitle }, '版本历史(共 ' + docPanel.versions.length + ' 版,当前第 ' + docPanel.currentVersion + ' 版)'), docPanel.versions.map((item) => h('div', { key: item.version, style: styles.card }, h('div', { style: styles.cardTitle }, '第 ' + item.version + ' 版', h(Badge, { text: CHANGE_KIND_LABEL[item.changeKind] ?? item.changeKind }), item.version === docPanel.currentVersion ? h(Badge, { text: '当前', style: styles.badgeGreen }) : null), h('div', { style: styles.meta }, h('span', null, '来源:' + (item.source ?? '—')), h('span', null, '操作人:' + (item.actor ?? '—')), h('span', null, '时间:' + new Date(item.createdAt).toLocaleString('zh-CN'))), item.reason ? h('div', { style: styles.snippet }, '说明:' + item.reason) : null, h('div', { style: styles.row, marginTop: '8px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: async () => {
|
|
1764
1982
|
setError(null);
|
|
1765
1983
|
try {
|
|
1766
1984
|
const value = await request('/kb/' + encodeURIComponent(docPanel.kbId) + '/docs/' + encodeURIComponent(docPanel.docId) + '/versions/' + item.version);
|
|
@@ -1797,7 +2015,7 @@ function EmptyState(props) {
|
|
|
1797
2015
|
return h('div', { style: styles.empty }, props.text ?? '暂无数据');
|
|
1798
2016
|
}
|
|
1799
2017
|
function ErrorBox(props) {
|
|
1800
|
-
return h('div', { style: styles.error }, props.text);
|
|
2018
|
+
return h('div', { style: styles.error, role: 'alert' }, h('span', { style: { marginRight: '6px', fontWeight: 600 } }, '⚠'), h('span', null, String(props.text ?? '')));
|
|
1801
2019
|
}
|
|
1802
2020
|
/** M5 总览:知识库/知识/审核/任务/最近活动一屏可见,点击可直接跳到对应页签。 */
|
|
1803
2021
|
function OverviewView(props) {
|
|
@@ -1995,6 +2213,7 @@ function DocsExplorer(props) {
|
|
|
1995
2213
|
const [loading, setLoading] = R.useState(false);
|
|
1996
2214
|
const [error, setError] = R.useState(null);
|
|
1997
2215
|
const [notice, setNotice] = R.useState('');
|
|
2216
|
+
useNoticeToast(notice, () => setNotice(''));
|
|
1998
2217
|
const [selectedId, setSelectedId] = R.useState('');
|
|
1999
2218
|
const [detail, setDetail] = R.useState(null);
|
|
2000
2219
|
const [detailError, setDetailError] = R.useState(null);
|
|
@@ -2096,7 +2315,12 @@ function DocsExplorer(props) {
|
|
|
2096
2315
|
const markStale = async () => {
|
|
2097
2316
|
if (detail === null)
|
|
2098
2317
|
return;
|
|
2099
|
-
|
|
2318
|
+
const ok = await askConfirm({
|
|
2319
|
+
title: '标记为过时',
|
|
2320
|
+
text: '确认把这条知识标记为过时?标记后仍保留正文与历史版本,可在审核里恢复。',
|
|
2321
|
+
confirmText: '确认标记过时',
|
|
2322
|
+
});
|
|
2323
|
+
if (!ok)
|
|
2100
2324
|
return;
|
|
2101
2325
|
setBusy(true);
|
|
2102
2326
|
setDetailError(null);
|
|
@@ -2156,7 +2380,12 @@ function DocsExplorer(props) {
|
|
|
2156
2380
|
const restoreVersion = async (version) => {
|
|
2157
2381
|
if (docBasePath() === '')
|
|
2158
2382
|
return;
|
|
2159
|
-
|
|
2383
|
+
const ok = await askConfirm({
|
|
2384
|
+
title: '恢复历史版本',
|
|
2385
|
+
text: '确认恢复为第 ' + version + ' 版内容?恢复会以新版本方式记录,不会静默覆盖当前内容。',
|
|
2386
|
+
confirmText: '确认恢复此版本',
|
|
2387
|
+
});
|
|
2388
|
+
if (!ok)
|
|
2160
2389
|
return;
|
|
2161
2390
|
setDetailError(null);
|
|
2162
2391
|
setVersionBusy('恢复第 ' + version + ' 版…');
|
|
@@ -2302,7 +2531,7 @@ function DocsExplorer(props) {
|
|
|
2302
2531
|
: h('div', { style: styles.readerMeta }, '从左侧列表选择一条知识,右侧显示正文、来源与版本历史。')), detail !== null
|
|
2303
2532
|
? h('div', { style: styles.headerActions }, detail !== null && detail.document ? h('div', { style: styles.segGroup }, h('button', { type: 'button', style: mdView ? styles.segActive : styles.seg, onClick: () => setMdView(true) }, '渲染'), h('button', { type: 'button', style: mdView ? styles.seg : styles.segActive, onClick: () => setMdView(false) }, '源码')) : null, h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setEditing(!editing) }, editing ? '取消编辑' : '编辑'), h('button', { type: 'button', style: { ...styles.primary, ...(busy || !editing ? styles.buttonDisabled : {}) }, className: 'kh-primary', disabled: busy || !editing, onClick: () => void saveEdit() }, busy ? '保存中…' : '保存修改'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => { if (detail !== null && detail.document)
|
|
2304
2533
|
downloadFile('/kb/' + encodeURIComponent(detail.document.kbId) + '/docs/' + encodeURIComponent(detail.document.id) + '/export'); } }, '导出 Markdown'), h('button', { type: 'button', style: { ...styles.danger, ...(busy ? styles.buttonDisabled : {}) }, className: 'kh-danger', disabled: busy, onClick: () => void markStale() }, '标记过时'))
|
|
2305
|
-
: null), h('div', { style: styles.readerScroll, className: 'kh-scroll' }, h('div', { style: styles.readerInner },
|
|
2534
|
+
: null), h('div', { style: styles.readerScroll, className: 'kh-scroll' }, h('div', { style: styles.readerInner }, detailError !== null ? ErrorBox({ text: detailError }) : null, detail === null
|
|
2306
2535
|
? (selectedId === '' ? EmptyState({ text: '从左侧选择一条知识查看正文。' }) : null)
|
|
2307
2536
|
: h('div', null, editing
|
|
2308
2537
|
? h('div', null, h('div', { style: styles.field }, h('label', { style: styles.label }, '标题'), h('input', { style: styles.input, value: draftTitle, onChange: (event) => setDraftTitle(event.target.value) })), h('div', { style: styles.field }, h('label', { style: styles.label }, '正文(Markdown)'), h('textarea', { style: { ...styles.textarea, minHeight: '260px' }, value: draftBody, onChange: (event) => setDraftBody(event.target.value) })))
|
|
@@ -2353,7 +2582,6 @@ function SettingsView() {
|
|
|
2353
2582
|
const [stats, setStats] = R.useState(null);
|
|
2354
2583
|
const [runs, setRuns] = R.useState([]);
|
|
2355
2584
|
const [error, setError] = R.useState(null);
|
|
2356
|
-
const [notice, setNotice] = R.useState('');
|
|
2357
2585
|
const [busy, setBusy] = R.useState('');
|
|
2358
2586
|
const [target, setTarget] = R.useState('');
|
|
2359
2587
|
const [backupNote, setBackupNote] = R.useState('');
|
|
@@ -2390,68 +2618,113 @@ function SettingsView() {
|
|
|
2390
2618
|
};
|
|
2391
2619
|
setBusy('save');
|
|
2392
2620
|
setError(null);
|
|
2393
|
-
setNotice('');
|
|
2394
2621
|
request('/settings', { method: 'PATCH', body }).then((value) => {
|
|
2395
2622
|
setSnapshot(value);
|
|
2396
2623
|
setForm({ ...value.settings, excludeDirsText: (value.settings?.excludeDirs ?? []).join('\n') });
|
|
2397
|
-
|
|
2624
|
+
pushToast('设置已保存', 'success');
|
|
2398
2625
|
}).catch((failure) => setError(message(failure))).then(() => setBusy(''));
|
|
2399
2626
|
};
|
|
2400
|
-
const migrate = (dryRun) => {
|
|
2627
|
+
const migrate = async (dryRun) => {
|
|
2401
2628
|
if (target.trim() === '') {
|
|
2402
2629
|
setError('请先填写目标目录绝对路径');
|
|
2403
2630
|
return;
|
|
2404
2631
|
}
|
|
2405
|
-
if (!dryRun
|
|
2406
|
-
|
|
2632
|
+
if (!dryRun) {
|
|
2633
|
+
const ok = await askConfirm({
|
|
2634
|
+
title: '迁移知识中心数据',
|
|
2635
|
+
text: '确认把知识中心数据迁移到:' + target + '?\n迁移前会自动创建备份,校验失败时不会切换位置。',
|
|
2636
|
+
confirmText: '开始迁移',
|
|
2637
|
+
});
|
|
2638
|
+
if (!ok)
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2407
2641
|
setBusy(dryRun ? 'dry' : 'migrate');
|
|
2408
2642
|
setError(null);
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2643
|
+
try {
|
|
2644
|
+
const value = await request('/settings/storage/migrate', { method: 'POST', body: { target: target.trim(), dryRun } });
|
|
2645
|
+
pushToast(dryRun
|
|
2646
|
+
? ('预检通过:将复制 ' + (value.willCopy?.files ?? 0) + ' 个文件,共 ' + formatBytes(value.willCopy?.bytes ?? 0))
|
|
2647
|
+
: ('已迁移到 ' + value.to + ';请重启 DSH 后生效(原数据仍在 ' + value.from + ')'), dryRun ? 'info' : 'success');
|
|
2648
|
+
}
|
|
2649
|
+
catch (failure) {
|
|
2650
|
+
pushToast(message(failure), 'error');
|
|
2651
|
+
}
|
|
2652
|
+
finally {
|
|
2653
|
+
setBusy('');
|
|
2654
|
+
}
|
|
2413
2655
|
};
|
|
2414
|
-
const createBackup = () => {
|
|
2656
|
+
const createBackup = async () => {
|
|
2415
2657
|
setBusy('backup');
|
|
2416
2658
|
setError(null);
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2659
|
+
try {
|
|
2660
|
+
const value = await request('/backups', { method: 'POST', body: { note: backupNote.trim() } });
|
|
2661
|
+
pushToast('备份完成:' + value.kbCount + ' 个知识库 / ' + value.docCount + ' 条知识 / ' + formatBytes(value.bytes), 'success');
|
|
2420
2662
|
setBackupNote('');
|
|
2421
2663
|
loadBackups();
|
|
2422
|
-
}
|
|
2664
|
+
}
|
|
2665
|
+
catch (failure) {
|
|
2666
|
+
pushToast(message(failure), 'error');
|
|
2667
|
+
}
|
|
2668
|
+
finally {
|
|
2669
|
+
setBusy('');
|
|
2670
|
+
}
|
|
2423
2671
|
};
|
|
2424
|
-
const openPreview = (item) => {
|
|
2672
|
+
const openPreview = async (item) => {
|
|
2425
2673
|
if (busy !== '')
|
|
2426
2674
|
return;
|
|
2427
2675
|
setBusy('preview:' + item.id);
|
|
2428
2676
|
setError(null);
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2677
|
+
try {
|
|
2678
|
+
setPreview(await request('/backups/' + encodeURIComponent(item.id) + '/preview'));
|
|
2679
|
+
}
|
|
2680
|
+
catch (failure) {
|
|
2681
|
+
pushToast(message(failure), 'error');
|
|
2682
|
+
}
|
|
2683
|
+
finally {
|
|
2684
|
+
setBusy('');
|
|
2685
|
+
}
|
|
2432
2686
|
};
|
|
2433
|
-
const restore = (item) => {
|
|
2434
|
-
|
|
2687
|
+
const restore = async (item) => {
|
|
2688
|
+
const ok = await askConfirm({
|
|
2689
|
+
title: '从备份恢复',
|
|
2690
|
+
text: '确认从备份 ' + item.file + ' 恢复?\n恢复采用合并语义:缺失的库与知识会重建,正文不同的知识只生成待审核候选,不会覆盖现有内容。',
|
|
2691
|
+
confirmText: '开始恢复',
|
|
2692
|
+
});
|
|
2693
|
+
if (!ok)
|
|
2435
2694
|
return;
|
|
2436
2695
|
setBusy(item.id);
|
|
2437
2696
|
setError(null);
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2697
|
+
try {
|
|
2698
|
+
const value = await request('/backups/' + encodeURIComponent(item.id) + '/restore', { method: 'POST', body: {} });
|
|
2699
|
+
pushToast('恢复完成:重建知识库 ' + value.createdKbs + ' 个 / 知识 ' + value.createdDocs + ' 条 / 新候选 ' + value.proposals + ' 条 / 跳过 ' + value.skipped + ' 条', 'success');
|
|
2441
2700
|
loadBackups();
|
|
2442
|
-
}
|
|
2701
|
+
}
|
|
2702
|
+
catch (failure) {
|
|
2703
|
+
pushToast(message(failure), 'error');
|
|
2704
|
+
}
|
|
2705
|
+
finally {
|
|
2706
|
+
setBusy('');
|
|
2707
|
+
}
|
|
2443
2708
|
};
|
|
2709
|
+
// 备份预览是独立浮层:补 Esc 关闭(与其它弹窗一致)。
|
|
2710
|
+
R.useEffect(() => {
|
|
2711
|
+
if (preview === null || typeof window === 'undefined' || typeof window.addEventListener !== 'function')
|
|
2712
|
+
return undefined;
|
|
2713
|
+
const onKey = (event) => { if (event.key === 'Escape')
|
|
2714
|
+
setPreview(null); };
|
|
2715
|
+
window.addEventListener('keydown', onKey);
|
|
2716
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
2717
|
+
}, [preview]);
|
|
2444
2718
|
if (snapshot === null || form === null) {
|
|
2445
2719
|
return error !== null ? h('div', null, ErrorBox({ text: error }), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: loadSettings }, '重试')) : Loading({ text: '正在读取设置…' });
|
|
2446
2720
|
}
|
|
2447
2721
|
const storage = snapshot.storage ?? {};
|
|
2448
2722
|
const checkbox = (key, label, hintText) => settingsCheckbox(form, patch, key, label, hintText);
|
|
2449
|
-
return h('div', null, error !== null ? ErrorBox({ text: error }) : null,
|
|
2450
|
-
setPreview(null); } }, h('div', { style: { ...styles.modalCard, maxWidth: '760px' } }, h('div', { style: styles.dialog }, h('div', { style: styles.dialogHead }, h('div', { style: { minWidth: 0 } }, h('div', { style: styles.dialogTitle }, '备份预览:' + preview.file), h('div', { style: styles.dialogSub }, '创建于 ' + formatTime(preview.createdAt) + ' · 格式 ' + preview.format + ' v' + preview.formatVersion + ' · schema v' + preview.schemaVersion + (preview.note ? ' · ' + preview.note : ''))), h('button', { type: 'button', style: styles.dialogClose, className: 'kh-hover', title: '关闭', onClick: () => setPreview(null) }, '✕')), h('div', { style: styles.dialogBody }, h('div', { style: styles.meta, marginBottom: '8px' }, h('span', null, '知识库 ' + preview.kbCount + ' 个'), h('span', null, '知识 ' + preview.docCount + ' 条'), h('span', null, '版本 ' + preview.versionCount + ' 条'), h('span', null, formatBytes(preview.bytes))), (preview.kbs ?? []).length === 0 ? EmptyState({ text: '这个备份里没有知识库数据。' }) : (preview.kbs ?? []).map((kb) => h('div', { key: kb.id, style: { ...styles.card, background: 'var(--dsw-alias-bg-layer-1)', marginBottom: '8px' } }, h('div', { style: styles.cardTitle }, kb.name, h(Badge, { text: kb.scope === 'user' ? '用户级' : '工作区级' }), kb.exists ? h(Badge, { text: '当前已存在', style: styles.badgeGreen }) : h(Badge, { text: '当前不存在', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识 ' + kb.documentCount + ' 条'), h('span', null, '版本 ' + kb.versionCount + ' 条'), h('span', null, '当前库同名知识 ' + kb.overlapCount + ' 条'), (kb.statusBreakdown ?? []).map((item) => h('span', { key: 's-' + item.status }, (DOC_STATUS_LABEL[item.status] ?? item.status) + ' ' + item.count)), (kb.kindBreakdown ?? []).map((item) => h('span', { key: 'k-' + item.kind }, (KIND_LABEL[item.kind] ?? item.kind) + ' ' + item.count))), (kb.sampleTitles ?? []).length > 0 ? h('div', { style: styles.hint }, '包含:' + (kb.sampleTitles ?? []).join('、')) : null, (kb.overlapTitles ?? []).length > 0 ? h('div', { style: styles.hint }, '恢复时只生成待审核候选(不覆盖现有正文):' + (kb.overlapTitles ?? []).slice(0, 10).join('、')) : null)), h('div', { style: styles.hint }, preview.note2 ?? '')), h('div', { style: styles.dialogFoot }, h('div', { style: styles.hint }, '预览只读,不会写入任何数据。'), h('div', { style: { flex: 1 } }), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setPreview(null) }, '关闭'), preview.kbs && preview.kbs.length > 0 ? h('button', {
|
|
2723
|
+
return h('div', null, error !== null ? ErrorBox({ text: error }) : null, h('div', { style: styles.card }, h('div', { style: styles.cardTitle }, '知识库默认设置'), h('div', { style: styles.field }, h('label', { style: styles.label }, '默认模型使用策略(新建知识库时的默认值)'), h('select', { style: styles.input, value: form.defaultStrategy ?? 'balanced', onChange: (event) => patch({ defaultStrategy: event.target.value }) }, STRATEGIES.map((item) => h('option', { key: item.value, value: item.value }, item.label + '(' + item.desc + ')')))), h('div', { style: styles.row }, h('div', { style: { ...styles.field, flex: '1 1 160px' } }, h('label', { style: styles.label }, '自动提炼最短字符数(80–4000)'), h('input', { type: 'number', style: styles.input, value: form.distillMinChars ?? 200, onChange: (event) => patch({ distillMinChars: event.target.value }) })), h('div', { style: { ...styles.field, flex: '1 1 160px' } }, h('label', { style: styles.label }, '备份保留份数(1–50)'), h('input', { type: 'number', style: styles.input, value: form.backupKeep ?? 10, onChange: (event) => patch({ backupKeep: event.target.value }) }))), checkbox('autoDistill', '开启自动提炼(默认关闭)', '会话 turn 结束时自动提炼长期知识,结果进入待审核队列,不会直接生效。'), checkbox('autoBackup', '恢复/迁移前自动备份(推荐开启)', '执行备份恢复或存储迁移前自动创建一份安全备份。'), h('div', { style: styles.row }, h('div', { style: { ...styles.field, flex: '1 1 160px' } }, h('label', { style: styles.label }, '审核人名称(1–40 字,默认系统用户名)'), h('input', { style: styles.input, value: form.reviewerName ?? '', maxLength: 40, onChange: (event) => patch({ reviewerName: event.target.value }) })), h('div', { style: { ...styles.field, flex: '1 1 160px' } }, h('label', { style: styles.label }, '自动批准最大差异行数(1–200)'), h('input', { type: 'number', style: styles.input, value: form.autoApproveMaxDiffLines ?? 10, onChange: (event) => patch({ autoApproveMaxDiffLines: event.target.value }) }))), checkbox('autoApprove', '开启自动批准(默认关闭,仅低风险 Wiki 更新)', '只对「已批准 Wiki 的更新候选」生效:基线版本未变、差异行数不超过阈值时自动落库,并写入审核日志标记 auto_approve;其他类型仍必须人工审核。'), h('div', { style: styles.field }, h('label', { style: styles.label }, '额外排除目录(每行一个或逗号分隔,仅目录名)'), h('textarea', { style: styles.textarea, value: form.excludeDirsText ?? '', onChange: (event) => patch({ excludeDirsText: event.target.value }), placeholder: '例如:vendor\nthird_party' }), h('div', { style: styles.hint }, '默认已排除 .git / node_modules / dist / build / 缓存 / 密钥文件等;这里用于补充项目自定义目录。')), h('button', { type: 'button', style: busy === 'save' ? { ...styles.primary, ...styles.buttonDisabled } : styles.primary, disabled: busy !== '', onClick: save }, busy === 'save' ? '保存中…' : '保存设置')), h(SettingsInjectionCard, { form, patch }), h('div', { style: styles.card }, h('div', { style: styles.cardTitle }, '存储位置'), h('pre', { style: styles.pre }, '当前数据目录:' + storage.storageRoot + '\n来源:' + (storage.source === 'config' ? '插件配置项' : storage.source === 'pointer' ? '本地位置指针文件' : '默认位置(DSH_HOME)') + (storage.pointerFile ? '\n指针文件:' + storage.pointerFile + (storage.pointerStorageRoot ? '(指向 ' + storage.pointerStorageRoot + ')' : '') : '')), h('div', { style: styles.field }, h('label', { style: styles.label }, '迁移到新目录(绝对路径)'), h('div', { style: styles.row }, h('input', { style: { ...styles.input, flex: '1 1 320px' }, value: target, placeholder: '例如 D:\\KnowHubData', onChange: (event) => setTarget(event.target.value) }), h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== '', onClick: () => migrate(true) }, busy === 'dry' ? '预检中…' : '预检'), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== '', onClick: () => migrate(false) }, busy === 'migrate' ? '迁移中…' : '执行迁移')), h('div', { style: styles.hint }, '迁移会先自动备份、拷贝数据库与知识文件并校验计数,成功后写入位置指针;DSH 重启后生效,失败不会切换位置。'))), h('div', { style: styles.card }, h('div', { style: styles.cardTitle }, '备份与恢复', stats !== null ? h('span', { style: styles.badge }, '共 ' + stats.count + ' 份 / ' + formatBytes(stats.totalBytes)) : null), h('div', { style: styles.row, marginBottom: '8px' }, h('input', { style: { ...styles.input, flex: '1 1 260px' }, value: backupNote, placeholder: '备份备注(可选)', onChange: (event) => setBackupNote(event.target.value) }), h('button', { type: 'button', style: styles.primary, className: 'kh-primary', disabled: busy !== '', onClick: createBackup }, busy === 'backup' ? '备份中…' : '立即备份'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: loadBackups }, '刷新列表')), (backups ?? []).length === 0 ? EmptyState({ text: '还没有备份。建议在升级或大改前手动创建一份。' }) : (backups ?? []).map((item) => h('div', { key: item.id, style: { borderTop: '1px solid var(--dsw-alias-border-l1)', padding: '8px 0' } }, h('div', { style: styles.cardTitle }, item.file, item.exists === false ? h('span', { style: styles.badgeAmber }, '文件缺失') : h('span', { style: styles.badgeGreen }, '可恢复')), h('div', { style: styles.meta }, h('span', null, item.kbCount + ' 个库 / ' + item.docCount + ' 条知识'), h('span', null, formatBytes(item.bytes)), h('span', null, formatTime(item.createdAt)), item.note ? h('span', null, item.note) : null), h('div', { style: styles.row, marginTop: '6px' }, h('button', { type: 'button', style: styles.button, className: 'kh-hover', disabled: busy !== '' || item.exists === false, onClick: () => openPreview(item) }, busy === 'preview:' + item.id ? '读取中…' : '预览内容'), h('button', { type: 'button', style: styles.button, disabled: busy !== '' || item.exists === false, onClick: () => restore(item) }, '从该备份恢复(合并)'))))), h('div', { style: styles.card }, h('div', { style: styles.cardTitle }, '自动提炼记录'), h('div', { style: styles.meta, marginBottom: '6px' }, h('span', null, '自动提炼:' + (form.autoDistill ? '已开启' : '未开启')), h('span', null, '最短字符数:' + form.distillMinChars)), runs.length === 0 ? EmptyState({ text: '还没有提炼记录。开启自动提炼后,会话结束时会自动生成待审核候选。' }) : h('pre', { style: styles.pre }, runs.map((run) => formatTime(run.createdAt) + ' ' + run.status + ' 新建 ' + run.createdCount + ' / 跳过 ' + run.skippedCount + ' ' + (run.modelProvider ?? '') + '/' + (run.modelId ?? '') + (run.note ? ' ' + run.note : '')).join('\n'))), preview !== null ? h(Modal, { onClose: () => setPreview(null), maxWidth: '800px' }, h('div', { style: styles.dialog }, h('div', { style: styles.dialogHead }, h('div', { style: { minWidth: 0 } }, h('div', { style: styles.dialogTitle }, '备份预览:' + preview.file), h('div', { style: styles.dialogSub }, '创建于 ' + formatTime(preview.createdAt) + ' · 格式 ' + preview.format + ' v' + preview.formatVersion + ' · schema v' + preview.schemaVersion + (preview.note ? ' · ' + preview.note : ''))), h('button', { type: 'button', style: styles.dialogClose, className: 'kh-hover', title: '关闭', onClick: () => setPreview(null) }, '✕')), h('div', { style: styles.dialogBody }, h('div', { style: styles.meta, marginBottom: '8px' }, h('span', null, '知识库 ' + preview.kbCount + ' 个'), h('span', null, '知识 ' + preview.docCount + ' 条'), h('span', null, '版本 ' + preview.versionCount + ' 条'), h('span', null, formatBytes(preview.bytes))), (preview.kbs ?? []).length === 0 ? EmptyState({ text: '这个备份里没有知识库数据。' }) : (preview.kbs ?? []).map((kb) => h('div', { key: kb.id, style: { ...styles.card, background: 'var(--dsw-alias-bg-layer-1)', marginBottom: '8px' } }, h('div', { style: styles.cardTitle }, kb.name, h(Badge, { text: kb.scope === 'user' ? '用户级' : '工作区级' }), kb.exists ? h(Badge, { text: '当前已存在', style: styles.badgeGreen }) : h(Badge, { text: '当前不存在', style: styles.badgeAmber })), h('div', { style: styles.meta }, h('span', null, '知识 ' + kb.documentCount + ' 条'), h('span', null, '版本 ' + kb.versionCount + ' 条'), h('span', null, '当前库同名知识 ' + kb.overlapCount + ' 条'), (kb.statusBreakdown ?? []).map((item) => h('span', { key: 's-' + item.status }, (DOC_STATUS_LABEL[item.status] ?? item.status) + ' ' + item.count)), (kb.kindBreakdown ?? []).map((item) => h('span', { key: 'k-' + item.kind }, (KIND_LABEL[item.kind] ?? item.kind) + ' ' + item.count))), (kb.sampleTitles ?? []).length > 0 ? h('div', { style: styles.hint }, '包含:' + (kb.sampleTitles ?? []).join('、')) : null, (kb.overlapTitles ?? []).length > 0 ? h('div', { style: styles.hint }, '恢复时只生成待审核候选(不覆盖现有正文):' + (kb.overlapTitles ?? []).slice(0, 10).join('、')) : null)), h('div', { style: styles.hint }, preview.note2 ?? '')), h('div', { style: styles.dialogFoot }, h('div', { style: styles.hint }, '预览只读,不会写入任何数据。'), h('div', { style: { flex: 1 } }), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => setPreview(null) }, '关闭'), preview.kbs && preview.kbs.length > 0 ? h('button', {
|
|
2451
2724
|
type: 'button', style: styles.button, className: 'kh-hover',
|
|
2452
2725
|
onClick: () => { const item = (backups ?? []).find((entry) => entry.id === preview.backupId); setPreview(null); if (item)
|
|
2453
2726
|
restore(item); },
|
|
2454
|
-
}, '按合并语义恢复…') : null)))
|
|
2727
|
+
}, '按合并语义恢复…') : null))) : null, h('div', { style: styles.sectionTitle }, '环境与接口诊断'), h(DiagnosticsView, null));
|
|
2455
2728
|
}
|
|
2456
2729
|
/**
|
|
2457
2730
|
* 知枢知识中心主面板。
|
|
@@ -2478,7 +2751,6 @@ function Page() {
|
|
|
2478
2751
|
const [kbList, setKbList] = R.useState(null);
|
|
2479
2752
|
const [bootError, setBootError] = R.useState(null);
|
|
2480
2753
|
const [booting, setBooting] = R.useState(false);
|
|
2481
|
-
const [notice, setNotice] = R.useState('');
|
|
2482
2754
|
const [reloadKey, setReloadKey] = R.useState(0);
|
|
2483
2755
|
const [reviewCount, setReviewCount] = R.useState(0);
|
|
2484
2756
|
R.useEffect(() => {
|
|
@@ -2517,7 +2789,7 @@ function Page() {
|
|
|
2517
2789
|
})();
|
|
2518
2790
|
return () => { alive = false; };
|
|
2519
2791
|
}, [reloadKey]);
|
|
2520
|
-
const notify = (text) =>
|
|
2792
|
+
const notify = (text) => { pushToast(text, 'success'); };
|
|
2521
2793
|
const refreshReviewCount = async () => {
|
|
2522
2794
|
try {
|
|
2523
2795
|
const value = await request('/review?limit=1');
|
|
@@ -2527,7 +2799,7 @@ function Page() {
|
|
|
2527
2799
|
catch { /* 徽标失败不打断界面 */ }
|
|
2528
2800
|
};
|
|
2529
2801
|
const goto = (next) => { setView(next); void refreshReviewCount(); };
|
|
2530
|
-
const renderOverlay = () => h('div', { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } }, h('div', { style: styles.backBar }, h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () =>
|
|
2802
|
+
const renderOverlay = () => h('div', { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } }, h('div', { style: styles.backBar }, h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => setView('') }, '← 返回知识中心'), h('h2', { style: styles.headerTitle }, OVERLAY_TITLES[view] ?? view)), h('div', { style: styles.overlayScroll, className: 'kh-scroll' }, h('div', { style: styles.overlayInner }, view === 'review' ? h(ReviewView, { knowledgeBases: kbList ?? [] }) : null, view === 'search' ? h('div', null, h(SearchView, { knowledgeBases: kbList ?? [] }), h('div', { style: styles.hint }, '说明:检索作用域由宿主根据当前工作目录推导,前端无法越过作用域读取其他项目私有知识。')) : null, view === 'settings' ? h(SettingsView, null) : null)));
|
|
2531
2803
|
const renderTab = () => {
|
|
2532
2804
|
if (tab === 'kb') {
|
|
2533
2805
|
return h('div', { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } }, h('div', { style: styles.subBar }, booting ? h('span', null, '正在读取 DSH 环境…') : null, currentWorkspace && currentWorkspace.workspaceId
|
|
@@ -2542,8 +2814,8 @@ function Page() {
|
|
|
2542
2814
|
return h('div', { style: styles.shell, className: 'knowhub-page' }, h('div', { style: styles.headerBar }, h('h1', { style: styles.headerTitle }, '知识中心'), h('div', { style: styles.headerTabs }, MAIN_TABS.map((item) => h('button', {
|
|
2543
2815
|
key: item.key, type: 'button',
|
|
2544
2816
|
style: view === '' && tab === item.key ? styles.headerTabActive : styles.headerTab,
|
|
2545
|
-
onClick: () => { setTab(item.key); setView('');
|
|
2546
|
-
}, item.label))), h('div', { style: styles.headerActions }, booting ? h('span', { style: styles.status }, '读取环境中…') : null, h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('review') }, '待审核', reviewCount > 0 ? h('span', { style: styles.countBadge }, String(reviewCount)) : null), h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('search') }, '检索'), h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('settings') }, '设置'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => { setReloadKey(reloadKey + 1); void refreshReviewCount(); } }, '刷新'))), bootError !== null ? h('div', { style: { ...styles.error, margin: '10px 24px 0' }
|
|
2817
|
+
onClick: () => { setTab(item.key); setView(''); },
|
|
2818
|
+
}, item.label))), h('div', { style: styles.headerActions }, booting ? h('span', { style: styles.status }, '读取环境中…') : null, h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('review') }, '待审核', reviewCount > 0 ? h('span', { style: styles.countBadge }, String(reviewCount)) : null), h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('search') }, '检索'), h('button', { type: 'button', style: styles.linkButton, className: 'kh-hover', onClick: () => goto('settings') }, '设置'), h('button', { type: 'button', style: styles.button, className: 'kh-hover', onClick: () => { setReloadKey(reloadKey + 1); void refreshReviewCount(); } }, '刷新'))), bootError !== null ? h('div', { style: { ...styles.error, margin: '10px 24px 0' }, role: 'alert' }, bootError) : null, view !== '' ? renderOverlay() : renderTab(), h(ToastHost, null), h(ConfirmHost, null));
|
|
2547
2819
|
}
|
|
2548
2820
|
/** sidebar.panellist 行图标:自绘 SVG(不依赖 @deepseek-ai/dsh-client-ui-primitives)。 */
|
|
2549
2821
|
function KnowHubIcon(props) {
|
|
@@ -2555,7 +2827,7 @@ function KnowHubIcon(props) {
|
|
|
2555
2827
|
}, h('path', { d: 'M4 5.5A2.5 2.5 0 0 1 6.5 3H19v15.5H6.5A2.5 2.5 0 0 0 4 21V5.5Z' }), h('path', { d: 'M4 18.5A2.5 2.5 0 0 1 6.5 16H19' }), h('path', { d: 'M8.5 7.5h7' }), h('path', { d: 'M8.5 11h4.5' }));
|
|
2556
2828
|
}
|
|
2557
2829
|
/** 仅供构建期渲染冒烟测试使用(宿主不使用)。 */
|
|
2558
|
-
exports.__smoke = { Page, DocsExplorer, KbView, CreateKbForm, EditKbForm, ImportMarkdownForm, ReviewView, SearchView, SettingsView, SettingsInjectionCard, DocumentDiffPage, diffUnifiedRows, diffSplitRows, renderMarkdown };
|
|
2830
|
+
exports.__smoke = { Page, DocsExplorer, KbView, CreateKbForm, EditKbForm, ImportMarkdownForm, ReviewView, SearchView, SettingsView, SettingsInjectionCard, DocumentDiffPage, diffUnifiedRows, diffSplitRows, renderMarkdown, Modal, ToastHost, ConfirmHost, pushToast, askConfirm, settleConfirm, useNoticeToast, toastSnapshot: () => toastState.items.map((item) => item.text) };
|
|
2559
2831
|
exports.inject = ['slots'];
|
|
2560
2832
|
function apply(ctx) {
|
|
2561
2833
|
const slots = (() => { try {
|
package/lib/host/kb/route.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { errorMessage, readBody, writeError, writeOk, writeText } from '../http.js';
|
|
9
9
|
import { all, openDatabase } from '../db/db.js';
|
|
10
10
|
import { resolveStorageRoot } from '../paths.js';
|
|
11
|
-
import { createKnowledgeBase, deleteKnowledgeBase, getKnowledgeBase, KbError, listKnowledgeBases, updateKnowledgeBase, } from './store.js';
|
|
11
|
+
import { createKnowledgeBase, deleteKnowledgeBase, getKnowledgeBase, KbError, listAllKnowledgeBases, listKnowledgeBases, updateKnowledgeBase, } from './store.js';
|
|
12
12
|
import { countDocumentList, countDocuments, createDocument, DocError, getDocument, listDocuments, resolveParentId, searchDocuments } from '../search.js';
|
|
13
13
|
import { addFeedback, FeedbackError, feedbackSummary, feedbackSummaries } from '../feedback.js';
|
|
14
14
|
import { exportKbMarkdown, importKbMarkdown, renderDocMarkdown, TransferError } from '../transfer.js';
|
|
@@ -69,6 +69,7 @@ export async function handleKbRequest(request) {
|
|
|
69
69
|
}
|
|
70
70
|
if (method === 'GET' && path === '/kb') {
|
|
71
71
|
const requested = str(url.searchParams.get('workspaceId'));
|
|
72
|
+
const wantAll = ['1', 'true', 'yes'].includes(str(url.searchParams.get('all')).toLowerCase());
|
|
72
73
|
let workspaceId;
|
|
73
74
|
if (requested !== '') {
|
|
74
75
|
const listed = await listWorkspaces(ctx);
|
|
@@ -83,11 +84,21 @@ export async function handleKbRequest(request) {
|
|
|
83
84
|
}
|
|
84
85
|
const listed = await listWorkspaces(ctx);
|
|
85
86
|
const workspaces = listed.workspaces.map((workspace) => ({ id: workspace.id, title: workspace.title, path: workspace.path }));
|
|
86
|
-
|
|
87
|
+
// 管理面全量视图:知识库可能建在任意工作区,只按「当前工作区」过滤会导致刚建好的库看不见
|
|
88
|
+
// (同名却报已存在)。all=1 返回所有未删除知识库,并带 workspaceTitle 便于辨认。
|
|
89
|
+
const source = wantAll ? listAllKnowledgeBases(db()) : listKnowledgeBases(db(), scopeKeysFor(workspaceId));
|
|
90
|
+
const rows = source.map((row) => ({
|
|
87
91
|
...withWorkspaceInfo(row, workspaces),
|
|
88
92
|
documentCount: countDocuments(db(), row.id),
|
|
89
93
|
}));
|
|
90
|
-
writeOk(res, {
|
|
94
|
+
writeOk(res, {
|
|
95
|
+
all: wantAll,
|
|
96
|
+
workspaceId,
|
|
97
|
+
currentWorkspaceId: workspaceId,
|
|
98
|
+
scopeKeys: scopeKeysFor(workspaceId),
|
|
99
|
+
count: rows.length,
|
|
100
|
+
knowledgeBases: rows,
|
|
101
|
+
});
|
|
91
102
|
return true;
|
|
92
103
|
}
|
|
93
104
|
if (method === 'POST' && path === '/kb') {
|
package/lib/host/kb/store.js
CHANGED
|
@@ -96,6 +96,16 @@ function findByName(db, scopeKey, name) {
|
|
|
96
96
|
const raw = get(db, SELECT + ' AND scope_key = ? AND name = ?', [scopeKey, name]);
|
|
97
97
|
return raw ? toRow(raw) : null;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* 同名冲突提示必须说清「已有的那条在哪个作用范围」:
|
|
101
|
+
* 面板默认只列一个工作区,用户很容易以为知识库没建成,实际上它建在别的工作区里。
|
|
102
|
+
*/
|
|
103
|
+
function duplicateMessage(existing, workspaceTitle) {
|
|
104
|
+
const where = existing.scope === 'user'
|
|
105
|
+
? '用户级(所有工作区共享)'
|
|
106
|
+
: '工作区级 · ' + (workspaceTitle ?? existing.workspaceId ?? '未知工作区');
|
|
107
|
+
return '同一作用范围内已存在同名知识库:' + existing.name + '(' + where + ',id ' + existing.id + ')。可换个名称,或在「知识库」页把列表切到该工作区查看。';
|
|
108
|
+
}
|
|
99
109
|
export async function createKnowledgeBase(db, input, options) {
|
|
100
110
|
const errors = validateKbInput(input);
|
|
101
111
|
if (errors.length > 0)
|
|
@@ -104,9 +114,9 @@ export async function createKnowledgeBase(db, input, options) {
|
|
|
104
114
|
const scope = str(input.scope);
|
|
105
115
|
const workspaceId = scope === 'workspace' ? str(input.workspaceId) : null;
|
|
106
116
|
const scopeKey = scopeKeyOf(scope, workspaceId);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const existing = findByName(db, scopeKey, name);
|
|
118
|
+
if (existing)
|
|
119
|
+
throw new KbError('KB_NAME_DUPLICATED', 409, duplicateMessage(existing, options.workspaceTitle ?? null));
|
|
110
120
|
const timestamp = Date.now();
|
|
111
121
|
const row = {
|
|
112
122
|
id: newId('kb'),
|
|
@@ -135,7 +145,8 @@ export async function createKnowledgeBase(db, input, options) {
|
|
|
135
145
|
catch (error) {
|
|
136
146
|
const message = error instanceof Error ? error.message : String(error);
|
|
137
147
|
if (/UNIQUE constraint failed/i.test(message)) {
|
|
138
|
-
|
|
148
|
+
const dup = findByName(db, scopeKey, name);
|
|
149
|
+
throw new KbError('KB_NAME_DUPLICATED', 409, dup ? duplicateMessage(dup, options.workspaceTitle ?? null) : '同一作用范围内已存在同名知识库:' + name);
|
|
139
150
|
}
|
|
140
151
|
throw error;
|
|
141
152
|
}
|
package/lib/host/route.js
CHANGED
|
@@ -49,6 +49,7 @@ export function createRouter(ctx, config) {
|
|
|
49
49
|
'POST /knowhub/api/storage-probe {value}',
|
|
50
50
|
'GET /knowhub/api/current-workspace',
|
|
51
51
|
'GET|POST /knowhub/api/kb',
|
|
52
|
+
'GET /knowhub/api/kb?workspaceId=&all=1',
|
|
52
53
|
'GET|PATCH|DELETE /knowhub/api/kb/:id',
|
|
53
54
|
'GET|POST /knowhub/api/kb/:id/docs',
|
|
54
55
|
'GET /knowhub/api/search?q=&tag=&kbId=&kind=&status=&sort=',
|