@stardustlc/dsh-dream 0.8.0 → 0.9.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.
- package/CHANGELOG.md +14 -0
- package/README.en.md +8 -2
- package/README.md +8 -2
- package/docs/USAGE.en.md +8 -2
- package/docs/USAGE.md +9 -2
- package/docs/VALIDATION.md +1 -1
- package/docs/validation/0.8.1.md +18 -0
- package/docs/validation/0.9.0.md +11 -0
- package/lib/automatic-queue.d.ts +29 -0
- package/lib/automatic-queue.js +184 -0
- package/lib/automatic-state.d.ts +35 -0
- package/lib/automatic-state.js +14 -1
- package/lib/automatic.d.ts +11 -1
- package/lib/automatic.js +168 -82
- package/lib/bridge.js +3 -0
- package/lib/client.js +73 -15
- package/lib/config.d.ts +1 -0
- package/lib/config.js +1 -0
- package/lib/knowledge-store.d.ts +12 -1
- package/lib/knowledge-store.js +73 -1
- package/lib/memory.d.ts +36 -0
- package/lib/memory.js +35 -0
- package/lib/retrieval.d.ts +1 -0
- package/lib/retrieval.js +6 -0
- package/lib/web-actions.js +32 -0
- package/lib/web.d.ts +5 -0
- package/lib/web.js +11 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16,6 +16,14 @@ const h = React.createElement;
|
|
|
16
16
|
// Interface language follows DSH; user content and stored records keep their original text.
|
|
17
17
|
let localeService;
|
|
18
18
|
const EN = Object.freeze({
|
|
19
|
+
'已到复核时间': 'Review deadline reached',
|
|
20
|
+
'最近取回:': 'Last retrieved: ',
|
|
21
|
+
'取回仅表示提供参考,不等于重新核验。': 'Retrieval means reference exposure, not renewed verification.',
|
|
22
|
+
'偏好': 'Preference', '流程': 'Procedure', '避坑': 'Pitfall', '事实': 'Fact',
|
|
23
|
+
'项目与主题': 'Project and topic', '全部待核实': 'All awaiting review',
|
|
24
|
+
'批量采纳': 'Accept selected', '批量驳回': 'Reject selected',
|
|
25
|
+
'选中候选:': 'Select candidate: ', '已完成所选候选的审阅。': 'Selected candidates reviewed.',
|
|
26
|
+
'先展开核对来源、条件与例外;只批量处理同范围、同主题且无冲突的候选。': 'Expand sources, conditions and exceptions first. Batch review requires candidates with the same scope and topic and no conflicts.',
|
|
19
27
|
"自动整理的内容在「经验」页待审阅;日记可让助手复盘时单独生成。": "Automatically collected lessons await review in Lessons. Ask the assistant to reflect when you want a separate journal entry.",
|
|
20
28
|
"查看待审阅经验": "Review candidate lessons",
|
|
21
29
|
"请求失败(HTTP ": "Request failed (HTTP ",
|
|
@@ -631,12 +639,16 @@ function normalizeKnowledge(value, now, limit) {
|
|
|
631
639
|
const title = trimmedText(raw.title);
|
|
632
640
|
const action = trimmedText(raw.action);
|
|
633
641
|
if (title === "" && action === "") continue;
|
|
634
|
-
const state = normalizeLessonState(raw.state);
|
|
642
|
+
const state = raw.reviewDue === true && raw.state === 'usable' ? 'stale' : normalizeLessonState(raw.state);
|
|
635
643
|
byState[state] = (byState[state] || 0) + 1;
|
|
636
644
|
if (rows.length >= cap) continue;
|
|
637
645
|
const lastValidatedAt = trimmedText(raw.lastValidatedAt);
|
|
638
646
|
rows.push({
|
|
639
647
|
id: trimmedText(raw.id),
|
|
648
|
+
kind: trimmedText(raw.kind),
|
|
649
|
+
groupKey: JSON.stringify([raw.kind, raw.scope?.global === true, raw.scope?.projectId ?? null, raw.scope?.workspaceRoot ?? null]),
|
|
650
|
+
reviewDue: raw.reviewDue === true,
|
|
651
|
+
lastRetrievedAt: trimmedText(raw.lastRetrievedAt),
|
|
640
652
|
title: title !== "" ? title : action,
|
|
641
653
|
action: action,
|
|
642
654
|
when: trimmedText(raw.when),
|
|
@@ -661,9 +673,9 @@ function normalizeKnowledge(value, now, limit) {
|
|
|
661
673
|
workspaceRoot: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.workspaceRoot) : "",
|
|
662
674
|
conflictIds: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }) : [],
|
|
663
675
|
conflicts: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }).map(function (id) { return { id: id, title: lessonTitles.get(id) || t("未加载的经验(") + id + ")" }; }) : [],
|
|
664
|
-
holdReason: trimmedText(raw.holdReason) !== "" ? trimmedText(raw.holdReason) : holdReasonOf(raw),
|
|
665
|
-
holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? t(trimmedText(raw.holdReasonLabel)) : skippedReasonLabel(holdReasonOf(raw)),
|
|
666
|
-
bridgeable: bridgeableOf(raw.state) && trimmedText(raw.id) !== "",
|
|
676
|
+
holdReason: raw.reviewDue === true ? 'review-due' : trimmedText(raw.holdReason) !== "" ? trimmedText(raw.holdReason) : holdReasonOf(raw),
|
|
677
|
+
holdReasonLabel: raw.reviewDue === true ? t('已到复核时间') : trimmedText(raw.holdReasonLabel) !== "" ? t(trimmedText(raw.holdReasonLabel)) : skippedReasonLabel(holdReasonOf(raw)),
|
|
678
|
+
bridgeable: raw.reviewDue !== true && bridgeableOf(raw.state) && trimmedText(raw.id) !== "",
|
|
667
679
|
candidate: state === "candidate",
|
|
668
680
|
disputed: state === "disputed",
|
|
669
681
|
});
|
|
@@ -1367,6 +1379,7 @@ function KnowledgeRow(props) {
|
|
|
1367
1379
|
]),
|
|
1368
1380
|
h("div", { className: "dshd-kmeta", key: "m" }, [
|
|
1369
1381
|
h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, t("最近核验:") + row.lastText),
|
|
1382
|
+
row.lastRetrievedAt ? h('span', { className:'dshd-klast', key:'retrieved', title:t('取回仅表示提供参考,不等于重新核验。') }, t('最近取回:') + formatDate(row.lastRetrievedAt) + ' ' + formatTime(row.lastRetrievedAt)) : null,
|
|
1370
1383
|
]),
|
|
1371
1384
|
h("div", { className: "dshd-kbody", key: "body" }, [
|
|
1372
1385
|
h("div", { key: "when" }, t("何时用:") + (row.when !== "" ? row.when : t("未标注适用条件"))),
|
|
@@ -1451,9 +1464,30 @@ function KnowledgeCard(props) {
|
|
|
1451
1464
|
function ReviewQueueCard(props) {
|
|
1452
1465
|
const vm = props.vm;
|
|
1453
1466
|
const [pageSize, setPageSize] = useState(5);
|
|
1454
|
-
const
|
|
1467
|
+
const [group, setGroup] = useState('');
|
|
1468
|
+
const [selected, setSelected] = useState([]);
|
|
1469
|
+
const [pending, setPending] = useState(false);
|
|
1470
|
+
const [failure, setFailure] = useState('');
|
|
1471
|
+
const [retry, setRetry] = useState(null);
|
|
1472
|
+
const all = vm !== null && vm !== undefined && Array.isArray(vm.rows)
|
|
1455
1473
|
? vm.rows.filter(function (row) { return HELD_STATES.indexOf(row.state) !== -1; })
|
|
1456
1474
|
: [];
|
|
1475
|
+
const rows = group === '' ? all : all.filter(row => row.groupKey === group);
|
|
1476
|
+
const groups = [...new Map(all.map(row => [row.groupKey, row.scopeLabel + ' · ' + t(({preference:'偏好',procedure:'流程',pitfall:'避坑',fact:'事实'})[row.kind] || row.kind)]))];
|
|
1477
|
+
const picked = rows.filter(row => selected.includes(row.id) && row.candidate && !row.conflictIds.length);
|
|
1478
|
+
async function batch(action) {
|
|
1479
|
+
if (pending || !picked.length) return;
|
|
1480
|
+
const items = picked.map(row => ({ lessonId:row.id,expectedRevision:row.revision }));
|
|
1481
|
+
const signature = JSON.stringify({action,items});
|
|
1482
|
+
const id = retry?.signature === signature ? retry.id : crypto.randomUUID();
|
|
1483
|
+
setRetry({signature,id}); setPending(true); setFailure('');
|
|
1484
|
+
try {
|
|
1485
|
+
await postAction({operation:'review-batch',action,items,requestId:id});
|
|
1486
|
+
setSelected([]); setRetry(null);
|
|
1487
|
+
props.onChanged?.(t('已完成所选候选的审阅。'));
|
|
1488
|
+
} catch(error) { setFailure(displayError(error)); }
|
|
1489
|
+
finally { setPending(false); }
|
|
1490
|
+
}
|
|
1457
1491
|
const head = function (extra) {
|
|
1458
1492
|
return h("div", { className: "dshd-khead", key: "head" }, [
|
|
1459
1493
|
h("h4", { key: "t" }, t("待核实")),
|
|
@@ -1474,13 +1508,20 @@ function ReviewQueueCard(props) {
|
|
|
1474
1508
|
}
|
|
1475
1509
|
return h("section", { className: "dshd-card dshd-kcard" }, [
|
|
1476
1510
|
head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + t(" 条待核实"))),
|
|
1511
|
+
all.length ? h('label', {key:'groups',className:'dshd-note'}, [t('项目与主题'), ' ', h('select', {'aria-label':t('项目与主题'),value:group,disabled:pending,onChange:event=>{setGroup(event.target.value);setSelected([]);setPageSize(5);}}, [h('option',{key:'all',value:''},t('全部待核实')), ...groups.map(([key,label])=>h('option',{key,value:key},label))])]) : null,
|
|
1512
|
+
picked.length ? h('div',{key:'batch',className:'dshd-kcmd'}, [h(Btn,{key:'accept',disabled:pending || new Set(picked.map(row=>row.groupKey)).size>1,onClick:()=>batch('accept')},t('批量采纳') + ' (' + picked.length + ')'),h(Btn,{key:'reject',disabled:pending || new Set(picked.map(row=>row.groupKey)).size>1,onClick:()=>batch('reject')},t('批量驳回') + ' (' + picked.length + ')')]) : null,
|
|
1513
|
+
failure ? h('p',{key:'failure',role:'alert'},failure) : null,
|
|
1514
|
+
all.length ? h('p',{key:'batch-note',className:'dshd-note'},t('先展开核对来源、条件与例外;只批量处理同范围、同主题且无冲突的候选。')) : null,
|
|
1477
1515
|
rows.length === 0
|
|
1478
1516
|
? h("div", { className: "dshd-kempty", key: "empty" }, [
|
|
1479
1517
|
h("div", { className: "dshd-kempty-title", key: "t" }, t("没有待核实的经验")),
|
|
1480
1518
|
h("p", { className: "dshd-kempty-body", key: "b" }, reviewQueueEmptyState()),
|
|
1481
1519
|
])
|
|
1482
1520
|
: h("div", { className: "dshd-krows", key: "rows" }, rows.slice(0, pageSize).map(function (row, index) {
|
|
1483
|
-
return h(
|
|
1521
|
+
return h('div',{key:row.id || 'held-' + index},[
|
|
1522
|
+
row.candidate && !row.conflictIds.length ? h('label',{key:'select',className:'dshd-note'}, [h('input',{type:'checkbox',checked:selected.includes(row.id),disabled:pending || (!selected.includes(row.id) && selected.length >= 20),onChange:event=>setSelected(event.target.checked?[...selected,row.id]:selected.filter(id=>id!==row.id))}), ' ' + t('选中候选:') + row.title]) : null,
|
|
1523
|
+
h(KnowledgeRow, { key:'row', row: row, onChanged: props.onChanged, privacy: props.privacy === true, showHoldReason: true }),
|
|
1524
|
+
]);
|
|
1484
1525
|
})),
|
|
1485
1526
|
rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, t("显示更多待核实经验")) : null,
|
|
1486
1527
|
h("div", { className: "dshd-note", key: "note" }, t("这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。")),
|
|
@@ -1615,6 +1656,7 @@ function AutomaticMemoryCard(props) {
|
|
|
1615
1656
|
const [value, setValue] = useState(null);
|
|
1616
1657
|
const [failure, setFailure] = useState(null);
|
|
1617
1658
|
const [pending, setPending] = useState(false);
|
|
1659
|
+
const [chatId, setChatId] = useState('');
|
|
1618
1660
|
const a = function (zh, en) { return languageOf() === 'en' ? en : zh; };
|
|
1619
1661
|
useEffect(function () {
|
|
1620
1662
|
let alive = true, timer;
|
|
@@ -1631,12 +1673,13 @@ function AutomaticMemoryCard(props) {
|
|
|
1631
1673
|
refresh();
|
|
1632
1674
|
return function () { alive = false; clearTimeout(timer); controller.abort(); };
|
|
1633
1675
|
}, [props.reloadToken]);
|
|
1634
|
-
async function
|
|
1676
|
+
async function change(body) {
|
|
1635
1677
|
setPending(true); setFailure(null);
|
|
1636
|
-
try { const result = await postAction(
|
|
1678
|
+
try { const result = await postAction(body); setValue(result.automatic); }
|
|
1637
1679
|
catch (error) { setFailure(error); }
|
|
1638
1680
|
finally { setPending(false); }
|
|
1639
1681
|
}
|
|
1682
|
+
const toggle = (key,next) => change({operation:'automatic',[key]:next});
|
|
1640
1683
|
const reasons = {
|
|
1641
1684
|
saved: a('已保存新候选,等待审阅', 'New candidates saved; review required'),
|
|
1642
1685
|
merged: a('已合并重复经验', 'Duplicate lessons merged'),
|
|
@@ -1644,18 +1687,22 @@ function AutomaticMemoryCard(props) {
|
|
|
1644
1687
|
'ordinary-chat': a('普通闲聊,已跳过', 'Ordinary chat skipped'),
|
|
1645
1688
|
'no-source': a('缺少可见来源,已跳过', 'No visible sources; skipped'),
|
|
1646
1689
|
'daily-budget': a('今天的整理预算已用完', 'Daily consolidation budget reached'),
|
|
1647
|
-
cooldown: a('
|
|
1690
|
+
cooldown: a('冷却中,轮次保留等待整理', 'Cooling down; queued turns retained'),
|
|
1691
|
+
queued: a('已加入队列,安静后合并整理', 'Queued; consolidate after quiet time'),
|
|
1648
1692
|
disabled: a('自动整理已暂停', 'Automatic collection paused'),
|
|
1649
1693
|
unfinished: a('任务未正常完成,已跳过', 'Turn did not complete normally; skipped'),
|
|
1650
1694
|
'no-model': a('当前会话尚无可用模型', 'No model route for this session'),
|
|
1651
1695
|
recording: a('正在整理候选经验', 'Collecting candidate lessons'),
|
|
1652
1696
|
timeout: a('整理超时,本次已停止', 'Consolidation timed out and stopped'),
|
|
1653
|
-
cancelled: a('
|
|
1697
|
+
cancelled: a('已暂停辅助调用,未完成轮次保留', 'Auxiliary call stopped; unfinished turns retained'),
|
|
1654
1698
|
superseded: a('已有新任务,旧轮次不再整理', 'New work superseded this turn; skipped'),
|
|
1655
1699
|
'model-error': a('模型返回失败或无法核验,请检查 DSH 模型连接', 'Model output failed or could not be verified. Check the DSH model connection.'),
|
|
1656
1700
|
'input-limit': a('内容超过预算,已跳过', 'Input exceeds the budget; skipped'),
|
|
1657
1701
|
};
|
|
1658
1702
|
const last = value && value.last;
|
|
1703
|
+
const chats = value?.sessions || [];
|
|
1704
|
+
const chosenId = chats.some(row=>row.sessionId===chatId) ? chatId : (chats[0]?.sessionId || '');
|
|
1705
|
+
const chat = chats.find(row=>row.sessionId===chosenId);
|
|
1659
1706
|
const minutes = Math.max(0, Number(value && value.cooldownMinutes) || 0);
|
|
1660
1707
|
const seconds = Math.ceil(minutes * 60);
|
|
1661
1708
|
const interval = minutes < 1 ? String(seconds) + a(' 秒', seconds === 1 ? ' second' : ' seconds') : new Intl.NumberFormat(languageOf() === 'en' ? 'en-US' : 'zh-CN', { maximumFractionDigits: 2 }).format(minutes) + a(' 分钟', minutes === 1 ? ' minute' : ' minutes');
|
|
@@ -1664,19 +1711,30 @@ function AutomaticMemoryCard(props) {
|
|
|
1664
1711
|
h('p', { key: 'intro' }, a('任务结束后整理可复用经验,不用记住“做梦”口令。新经验先待审阅,不自动修改项目规则。', 'Collect reusable lessons after tasks, without a trigger phrase. New lessons await review; project rules are not changed automatically.')),
|
|
1665
1712
|
failure ? h('p', { key: 'error', role: 'alert' }, displayError(failure)) : null,
|
|
1666
1713
|
!value ? (failure ? null : h('p', { key: 'loading' }, a('正在读取状态…', 'Loading status…'))) : h('div', { key: 'controls' }, [
|
|
1667
|
-
h('p', { key: 'status', role: 'status' }, value.problem ? a('状态异常,请检查 DSH 日志。', 'Status error. Check the DSH logs.') : value.available ? (value.
|
|
1714
|
+
h('p', { key: 'status', role: 'status' }, value.problem ? a('状态异常,请检查 DSH 日志。', 'Status error. Check the DSH logs.') : value.available ? (value.running ? a('正在整理', 'Consolidating') : value.enabled === false ? a('全局默认已暂停;聊天独立设置仍生效', 'Global collection is paused; chat overrides still apply') : a('已就绪,等待符合条件的任务', 'Ready; waiting for a qualifying task')) : a('当前宿主服务尚未就绪;手动工具仍可使用。', 'Host services are not ready; manual tools remain available.')),
|
|
1668
1715
|
h('label', { key: 'collect', style: { display: 'block', marginBottom: '8px' } }, [
|
|
1669
1716
|
h('input', { key: 'input', type: 'checkbox', checked: value.enabled === true, disabled: pending, onChange: event => toggle('enabled', event.target.checked) }),
|
|
1670
|
-
' ' + a('
|
|
1717
|
+
' ' + a('默认自动整理候选经验', 'Collect candidate lessons by default'),
|
|
1671
1718
|
]),
|
|
1672
1719
|
h('label', { key: 'retrieve', style: { display: 'block' } }, [
|
|
1673
1720
|
h('input', { key: 'input', type: 'checkbox', checked: value.retrievalEnabled === true, disabled: pending, onChange: event => toggle('retrievalEnabled', event.target.checked) }),
|
|
1674
|
-
' ' + a('
|
|
1721
|
+
' ' + a('默认自动取回已采纳经验', 'Retrieve accepted lessons by default'),
|
|
1675
1722
|
]),
|
|
1676
1723
|
h('p', { key: 'budget' }, a('今天已调用模型 ', 'Model calls today: ') + String(value.callsToday ?? 0) + ' / ' + String(value.maxCallsPerDay ?? 0) + a(' 次;每次间隔至少 ', '; at least ') + interval + a('。', ' between calls.')),
|
|
1724
|
+
h('p',{key:'queue'},a('待整理 ', 'Queued turns: ') + (value.pendingTurns ?? 0) + a(' 轮;安静 ', '; quiet time: ') + (value.idleSeconds ?? 60) + a(' 秒后,同项目最多合并 ', ' seconds; up to ') + (value.maxBatchTurns ?? 4) + a(' 轮。', ' turns per project in a batch.')),
|
|
1725
|
+
h(Btn, { key: 'review', onClick: props.onReview }, a('待审阅 ', 'Awaiting review: ') + String(value.candidates ?? 0)),
|
|
1726
|
+
h('details',{key:'advanced',className:'dshd-note'},[
|
|
1727
|
+
h('summary',{key:'summary',style:{cursor:'pointer',marginTop:'10px'}},a('聊天设置与整理预算','Chat controls and consolidation limits')),
|
|
1728
|
+
h(Btn,{key:'flush',disabled:pending || !value.available || value.mainTaskRunning || !value.pendingTurns,onClick:()=>change({operation:'automatic-flush'})},a('现在整理待处理轮次', 'Consolidate queued turns now')),
|
|
1729
|
+
h('p',{key:'retention',className:'dshd-note'},a('手动整理仍遵守冷却和每日预算。待处理片段最多保留 24 小时、32 轮;过期 ', 'Manual consolidation respects cooldown and daily budget. Pending excerpts keep for up to 24 hours / 32 turns; expired: ') + (value.expiredTurns ?? 0) + a(' 轮,超出容量 ', '; overflow: ') + (value.overflowTurns ?? 0) + a(' 轮。', ' turns.')),
|
|
1730
|
+
chat ? h('div',{key:'chat',className:'dshd-reviewform'},[
|
|
1731
|
+
h('label',{key:'select'},[a('聊天记忆设置','Chat memory settings'),h('select',{'aria-label':a('聊天记忆设置','Chat memory settings'),value:chosenId,disabled:pending,onChange:event=>setChatId(event.target.value)},chats.map(row=>h('option',{key:row.sessionId,value:row.sessionId},(row.cwd || '') + ' · ' + row.sessionId.slice(0,8))))]),
|
|
1732
|
+
...[['use',a('使用记忆','Use memory')],['contribute',a('贡献记忆','Contribute memory')]].map(([key,label])=>h('label',{key},[label,h('select',{'aria-label':label,disabled:pending,value:chat[key]===null?'inherit':String(chat[key]),onChange:event=>change({operation:'automatic-chat',sessionId:chosenId,[key]:event.target.value==='inherit'?null:event.target.value==='true'})},[h('option',{key:'inherit',value:'inherit'},a('跟随全局','Follow global default')),h('option',{key:'true',value:'true'},a('仅此聊天开启','Enable for this chat')),h('option',{key:'false',value:'false'},a('仅此聊天关闭','Disable for this chat'))])])),
|
|
1733
|
+
h('p',{key:'note',className:'dshd-note'},a('两个选项独立保存;关闭贡献会移除该聊天尚未整理的片段,已有经验仍保留。','Both controls persist independently. Disabling contribution removes pending excerpts for this chat; existing lessons remain.')),
|
|
1734
|
+
]) : null,
|
|
1677
1735
|
h('p', { key: 'cost', className: 'dshd-note' }, a('沿用当前会话模型,会消耗模型额度;普通闲聊跳过。每次最多 ', 'Uses the current session model and its quota; ordinary chat is skipped. Up to ') + String(value.maxOutputTokens ?? 0) + a(' 个输出 token。', ' output tokens per call.')),
|
|
1736
|
+
]),
|
|
1678
1737
|
last ? h('p', { key: 'last' }, a('最近检查:', 'Last check: ') + formatDate(new Date(last.at).toISOString()) + ' ' + formatTime(new Date(last.at).toISOString()) + ' · ' + (reasons[last.reason] || a('已检查', 'Checked'))) : null,
|
|
1679
|
-
h(Btn, { key: 'review', onClick: props.onReview }, a('待审阅 ', 'Awaiting review: ') + String(value.candidates ?? 0)),
|
|
1680
1738
|
]),
|
|
1681
1739
|
]);
|
|
1682
1740
|
}
|
|
@@ -1970,11 +2028,11 @@ function DreamPanel() {
|
|
|
1970
2028
|
return h("div", { className: "dshd-root" + (privacy === true ? " dshd-privacy" : ""), key: "root" }, [
|
|
1971
2029
|
h("div", { className: "dshd-shell", key: "shell" }, [
|
|
1972
2030
|
errorBox,
|
|
1973
|
-
h(AutomaticMemoryCard, { key: 'automatic', reloadToken: reloadToken, onReview: function () { setActiveTab('knowledge'); reload(); } }),
|
|
1974
2031
|
h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": t("梦境面板视图") }, [["journal", t("日记")], ["knowledge", t("经验")], ["bridge", t("写入记录")]].map(function (item) {
|
|
1975
2032
|
return h("button", { key: item[0], type: "button", className: "dshd-chipbtn" + (activeTab === item[0] ? " on" : ""), "aria-pressed": activeTab === item[0], onClick: function () { setFilterOpen(false); setActiveTab(item[0]); } }, item[1]);
|
|
1976
2033
|
})),
|
|
1977
2034
|
toolbar,
|
|
2035
|
+
activeTab === 'journal' ? h(AutomaticMemoryCard, { key: 'automatic', reloadToken: reloadToken, onReview: function () { setActiveTab('knowledge'); reload(); } }) : null,
|
|
1978
2036
|
privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, t("隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。")) : null,
|
|
1979
2037
|
body(),
|
|
1980
2038
|
activeTab !== "journal" ? null : h("div", { className: "dshd-footer", key: "footer" }, [
|
package/lib/config.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ResolvedDreamConfig {
|
|
|
21
21
|
autoMaxInputChars: number;
|
|
22
22
|
autoMaxOutputTokens: number;
|
|
23
23
|
autoTimeoutMs: number;
|
|
24
|
+
autoIdleMs: number;
|
|
24
25
|
}
|
|
25
26
|
/** 解析并校验插件配置。 */
|
|
26
27
|
export declare function resolveConfig(raw?: Record<string, unknown> | null, env?: NodeJS.ProcessEnv): ResolvedDreamConfig;
|
package/lib/config.js
CHANGED
|
@@ -32,5 +32,6 @@ export function resolveConfig(raw, env = process.env) {
|
|
|
32
32
|
autoMaxInputChars: clamp(cfg.autoMaxInputChars, 12000, 1000, 20000),
|
|
33
33
|
autoMaxOutputTokens: clamp(cfg.autoMaxOutputTokens, 1200, 200, 2000),
|
|
34
34
|
autoTimeoutMs: clamp(cfg.autoTimeoutMs, 30000, 1000, 60000),
|
|
35
|
+
autoIdleMs: clamp(cfg.autoIdleMs, 60000, 0, 3600000),
|
|
35
36
|
};
|
|
36
37
|
}
|
package/lib/knowledge-store.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type Evidence, type EvidenceInput, type Lesson, type LessonInput, type LessonReviewResolution, type LessonState, type ReviewDecision } from './knowledge.js';
|
|
2
|
+
type MemorySnapshot = {
|
|
3
|
+
sourceKey: string;
|
|
4
|
+
lessons: Lesson[];
|
|
5
|
+
evidence: Evidence[];
|
|
6
|
+
badLines: number;
|
|
7
|
+
complete: boolean;
|
|
8
|
+
};
|
|
2
9
|
/** events.jsonl 默认最多回放的行数(有界读取)。 */
|
|
3
10
|
export declare const MAX_REPLAY_EVENTS = 200000;
|
|
4
11
|
/** KnowledgeStore 构造选项(都可省略,保持 FREEZE 的 constructor(knowledgeDir) 可用)。 */
|
|
@@ -37,6 +44,9 @@ export declare class KnowledgeStore {
|
|
|
37
44
|
listLessons(): Lesson[];
|
|
38
45
|
/** 全部证据,按 observedAt 升序(同刻按 id 稳定排序)。 */
|
|
39
46
|
listEvidence(): Evidence[];
|
|
47
|
+
/** Content-addressed, immutable snapshot. Hashes also detect same-size, same-mtime writes. */
|
|
48
|
+
readSnapshot(): MemorySnapshot;
|
|
49
|
+
private sourceFingerprint;
|
|
40
50
|
/** 按 id 取经验;不存在返回 undefined。 */
|
|
41
51
|
getLesson(id: string): Lesson | undefined;
|
|
42
52
|
/** 追加证据;同 sessionId+recordSeq 或同 sourceHash 视为重复,返回已有记录且不写。 */
|
|
@@ -71,7 +81,7 @@ export declare class KnowledgeStore {
|
|
|
71
81
|
id: string;
|
|
72
82
|
patch: Partial<Lesson>;
|
|
73
83
|
expectedRevision: number;
|
|
74
|
-
}>, idempotencyKey: string): Lesson[];
|
|
84
|
+
}>, idempotencyKey: string, candidateReview?: 'accepted' | 'rejected'): Lesson[];
|
|
75
85
|
/** 状态迁移;不允许的迁移抛 KnowledgeError('invalid') 且不写。 */
|
|
76
86
|
applyTransition(id: string, to: LessonState, expectedRevision: number, idempotencyKey: string): Lesson;
|
|
77
87
|
/** 从事件完整重建派生索引与 checkpoint;不删除、不改写 events.jsonl / evidence.jsonl。 */
|
|
@@ -188,3 +198,4 @@ export declare class KnowledgeStore {
|
|
|
188
198
|
private writeIndex;
|
|
189
199
|
private writeIndexStrict;
|
|
190
200
|
}
|
|
201
|
+
export {};
|
package/lib/knowledge-store.js
CHANGED
|
@@ -22,6 +22,16 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { StringDecoder } from 'node:string_decoder';
|
|
23
23
|
import { KnowledgeError, canTransition, evidenceNaturalKey, evidenceSessionKey, lessonFingerprint, mergeEvidenceSupport, validateEvidence, validateLesson, } from './knowledge.js';
|
|
24
24
|
import { maskSecrets } from './mask.js';
|
|
25
|
+
import { memoryDirectory } from './memory.js';
|
|
26
|
+
const snapshots = new Map();
|
|
27
|
+
function freezeTree(value) {
|
|
28
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
29
|
+
for (const child of Object.values(value))
|
|
30
|
+
freezeTree(child);
|
|
31
|
+
Object.freeze(value);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
25
35
|
/** events.jsonl 默认最多回放的行数(有界读取)。 */
|
|
26
36
|
export const MAX_REPLAY_EVENTS = 200000;
|
|
27
37
|
const PREFIX_HASH_BYTES = 64 * 1024;
|
|
@@ -336,6 +346,53 @@ export class KnowledgeStore {
|
|
|
336
346
|
.map((item) => clone(item))
|
|
337
347
|
.sort((a, b) => (a.observedAt === b.observedAt ? a.id.localeCompare(b.id) : a.observedAt.localeCompare(b.observedAt)));
|
|
338
348
|
}
|
|
349
|
+
/** Content-addressed, immutable snapshot. Hashes also detect same-size, same-mtime writes. */
|
|
350
|
+
readSnapshot() {
|
|
351
|
+
const key = this.sourceFingerprint();
|
|
352
|
+
const cacheKey = this.dir + ':' + this.maxReplayEvents;
|
|
353
|
+
const cached = snapshots.get(cacheKey);
|
|
354
|
+
if (cached?.sourceKey === key)
|
|
355
|
+
return cached;
|
|
356
|
+
snapshots.delete(cacheKey);
|
|
357
|
+
// Cold reads verify the authority, including edits in the middle of a
|
|
358
|
+
// legacy checkpoint's prefix/tail guards. Warm reads only hash the files.
|
|
359
|
+
const state = this.mergeEvidenceFile(this.loadEventsBounded(0));
|
|
360
|
+
if (this.sourceFingerprint() !== key)
|
|
361
|
+
throw new KnowledgeError('io', '记忆正在更新,请稍后重试');
|
|
362
|
+
const snapshot = freezeTree({ sourceKey: key, lessons: [...state.lessons.values()], evidence: [...state.evidence.values()], badLines: state.badLines,
|
|
363
|
+
complete: state.idempotencyComplete && state.orphanEvents === 0 && state.unsupportedVersions === 0 && state.skippedEvents === 0 });
|
|
364
|
+
if (snapshots.size >= 16)
|
|
365
|
+
snapshots.delete(snapshots.keys().next().value);
|
|
366
|
+
snapshots.set(cacheKey, snapshot);
|
|
367
|
+
return snapshot;
|
|
368
|
+
}
|
|
369
|
+
sourceFingerprint() {
|
|
370
|
+
const digest = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024);
|
|
371
|
+
for (const file of [this.eventsPath, this.evidencePath, this.indexPath]) {
|
|
372
|
+
digest.update(file);
|
|
373
|
+
let fd;
|
|
374
|
+
try {
|
|
375
|
+
fd = openSync(file, 'r');
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error.code === 'ENOENT') {
|
|
379
|
+
digest.update(':missing');
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
let count;
|
|
386
|
+
while ((count = readSync(fd, buffer, 0, buffer.length, null)) > 0)
|
|
387
|
+
digest.update(buffer.subarray(0, count));
|
|
388
|
+
digest.update(':end');
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
closeSync(fd);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return digest.digest('hex');
|
|
395
|
+
}
|
|
339
396
|
/** 按 id 取经验;不存在返回 undefined。 */
|
|
340
397
|
getLesson(id) {
|
|
341
398
|
const state = this.loadEvents();
|
|
@@ -693,7 +750,7 @@ export class KnowledgeStore {
|
|
|
693
750
|
* 批量原子更新(M2-②):一个事件承载全部变更;先统一校验,
|
|
694
751
|
* 任一条不通过 → 零写入并抛 KnowledgeError(details.failedId)。
|
|
695
752
|
*/
|
|
696
|
-
updateLessonsBatch(updates, idempotencyKey) {
|
|
753
|
+
updateLessonsBatch(updates, idempotencyKey, candidateReview) {
|
|
697
754
|
assertIdempotencyKey(idempotencyKey);
|
|
698
755
|
if (!Array.isArray(updates) || updates.length === 0)
|
|
699
756
|
throw new KnowledgeError('invalid', 'updates 必须是非空数组');
|
|
@@ -707,6 +764,7 @@ export class KnowledgeStore {
|
|
|
707
764
|
}
|
|
708
765
|
const requestHash = this.hashRequest({
|
|
709
766
|
operation: 'lesson.batch',
|
|
767
|
+
...(candidateReview === undefined ? {} : { candidateReview }),
|
|
710
768
|
updates: updates.map((update) => ({ id: update.id, expectedRevision: update.expectedRevision, patch: this.hashablePatch(update.patch) })),
|
|
711
769
|
});
|
|
712
770
|
return this.withLock((touch) => {
|
|
@@ -721,6 +779,7 @@ export class KnowledgeStore {
|
|
|
721
779
|
}
|
|
722
780
|
const now = new Date().toISOString();
|
|
723
781
|
const planned = [];
|
|
782
|
+
let reviewGroup;
|
|
724
783
|
for (const update of updates) {
|
|
725
784
|
const previous = state.lessons.get(update.id);
|
|
726
785
|
if (previous === undefined)
|
|
@@ -733,6 +792,18 @@ export class KnowledgeStore {
|
|
|
733
792
|
});
|
|
734
793
|
}
|
|
735
794
|
const clean = this.cleanPatch(update.patch);
|
|
795
|
+
if (candidateReview !== undefined) {
|
|
796
|
+
if (previous.state !== 'candidate' || previous.conflictIds.length)
|
|
797
|
+
throw new KnowledgeError('invalid', '批量审阅只接受没有冲突的候选,请逐条核对其他记录');
|
|
798
|
+
const group = JSON.stringify([previous.kind, previous.scope.global === true, previous.scope.projectId, previous.scope.workspaceRoot]);
|
|
799
|
+
reviewGroup ??= group;
|
|
800
|
+
if (group !== reviewGroup)
|
|
801
|
+
throw new KnowledgeError('invalid', '批量审阅需要相同项目、范围和主题');
|
|
802
|
+
if (candidateReview === 'accepted' && !previous.evidenceIds.some(id => state.evidence.get(id)?.verification === 'read'))
|
|
803
|
+
throw new KnowledgeError('invalid', '请先核对候选的实际来源');
|
|
804
|
+
clean.state = candidateReview === 'accepted' ? 'usable' : 'rejected';
|
|
805
|
+
clean.review = { decision: candidateReview, actor: 'human', at: now };
|
|
806
|
+
}
|
|
736
807
|
if (clean.state !== undefined && clean.state !== previous.state && !canTransition(previous.state, clean.state)) {
|
|
737
808
|
throw new KnowledgeError('invalid', '不允许的状态迁移:' + previous.state + ' → ' + clean.state, { failedId: update.id });
|
|
738
809
|
}
|
|
@@ -1678,6 +1749,7 @@ export class KnowledgeStore {
|
|
|
1678
1749
|
lessons,
|
|
1679
1750
|
stateCounts,
|
|
1680
1751
|
byState: stateCounts,
|
|
1752
|
+
memory: memoryDirectory([...state.lessons.values()], [...state.evidence.values()]),
|
|
1681
1753
|
};
|
|
1682
1754
|
const checkpoint = this.buildCheckpoint(state);
|
|
1683
1755
|
if (checkpoint !== undefined)
|
package/lib/memory.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Evidence, Lesson } from './knowledge.js';
|
|
2
|
+
/** A retrieval deadline is not a verification result; feedback never renews it. */
|
|
3
|
+
export declare function reviewDeadline(lesson: Lesson): number | undefined;
|
|
4
|
+
export declare function needsMemoryReview(lesson: Lesson, now?: number): boolean;
|
|
5
|
+
/** Derived layers contain pointers; authoritative lessons and evidence remain untouched. */
|
|
6
|
+
export declare function memoryDirectory(lessons: Lesson[], evidence: Evidence[], now?: number): {
|
|
7
|
+
summary: {
|
|
8
|
+
lessonId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
scope: {
|
|
11
|
+
projectId?: string;
|
|
12
|
+
workspaceRoot?: string;
|
|
13
|
+
global: boolean;
|
|
14
|
+
};
|
|
15
|
+
}[];
|
|
16
|
+
entries: {
|
|
17
|
+
lessonId: string;
|
|
18
|
+
revision: number;
|
|
19
|
+
title: string;
|
|
20
|
+
topic: import("./knowledge.js").LessonKind;
|
|
21
|
+
scope: {
|
|
22
|
+
projectId?: string;
|
|
23
|
+
workspaceRoot?: string;
|
|
24
|
+
global: boolean;
|
|
25
|
+
};
|
|
26
|
+
state: import("./knowledge.js").LessonState;
|
|
27
|
+
reviewDue: boolean;
|
|
28
|
+
reviewAfter: string | null;
|
|
29
|
+
sources: {
|
|
30
|
+
evidenceId: string;
|
|
31
|
+
sessionId: string | undefined;
|
|
32
|
+
recordSeq: string | number | undefined;
|
|
33
|
+
verification: "read" | "claimed";
|
|
34
|
+
}[];
|
|
35
|
+
}[];
|
|
36
|
+
};
|
package/lib/memory.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A retrieval deadline is not a verification result; feedback never renews it. */
|
|
2
|
+
export function reviewDeadline(lesson) {
|
|
3
|
+
const explicit = lesson.reviewAfter ? Date.parse(lesson.reviewAfter) : NaN;
|
|
4
|
+
const reviewed = Date.parse(lesson.review.at ?? lesson.lastValidatedAt ?? lesson.createdAt);
|
|
5
|
+
const renewed = lesson.review.at ? Date.parse(lesson.review.at) : NaN;
|
|
6
|
+
if (Number.isFinite(explicit) && (!Number.isFinite(renewed) || renewed <= explicit))
|
|
7
|
+
return explicit;
|
|
8
|
+
if (lesson.kind !== 'fact')
|
|
9
|
+
return undefined;
|
|
10
|
+
return Number.isFinite(reviewed) ? reviewed + 30 * 86400000 : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function needsMemoryReview(lesson, now = Date.now()) {
|
|
13
|
+
const deadline = reviewDeadline(lesson);
|
|
14
|
+
return lesson.state === 'usable' && deadline !== undefined && deadline <= now;
|
|
15
|
+
}
|
|
16
|
+
/** Derived layers contain pointers; authoritative lessons and evidence remain untouched. */
|
|
17
|
+
export function memoryDirectory(lessons, evidence, now = Date.now()) {
|
|
18
|
+
const byId = new Map(evidence.map(item => [item.id, item]));
|
|
19
|
+
const accepted = new Set(lessons.filter(item => item.review.decision === 'accepted').map(item => item.id));
|
|
20
|
+
const entries = lessons.filter(item => item.state !== 'rejected').map(item => ({
|
|
21
|
+
lessonId: item.id, revision: item.revision, title: item.title, topic: item.kind, scope: item.scope,
|
|
22
|
+
state: item.state, reviewDue: needsMemoryReview(item, now),
|
|
23
|
+
reviewAfter: reviewDeadline(item) === undefined ? null : new Date(reviewDeadline(item)).toISOString(),
|
|
24
|
+
sources: item.evidenceIds.slice(0, 3).flatMap(id => {
|
|
25
|
+
const source = byId.get(id);
|
|
26
|
+
return source ? [{ evidenceId: id, sessionId: source.sessionId, recordSeq: source.recordSeq, verification: source.verification }] : [];
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
let chars = 0;
|
|
30
|
+
const summary = entries.filter(item => item.state === 'usable' && !item.reviewDue
|
|
31
|
+
&& accepted.has(item.lessonId))
|
|
32
|
+
.slice(0, 12).filter(item => { chars += item.title.length; return chars <= 2400; })
|
|
33
|
+
.map(({ lessonId, title, scope }) => ({ lessonId, title, scope }));
|
|
34
|
+
return { summary, entries };
|
|
35
|
+
}
|
package/lib/retrieval.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface RetrievalQuery {
|
|
|
28
28
|
includeCandidates?: boolean;
|
|
29
29
|
/** 是否保留 independentSupportCount===0 且非 usable 的条目;默认 true。dream_context 传 false(R1)。 */
|
|
30
30
|
includeNoEvidence?: boolean;
|
|
31
|
+
now?: number;
|
|
31
32
|
}
|
|
32
33
|
/** 检索结果中的单条经验(when/exceptions 永不截断)。 */
|
|
33
34
|
export interface RetrievedLesson {
|
package/lib/retrieval.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeLessonText } from './knowledge.js';
|
|
2
|
+
import { needsMemoryReview } from './memory.js';
|
|
2
3
|
/** 默认条数。 */
|
|
3
4
|
export const DEFAULT_RETRIEVAL_LIMIT = 5;
|
|
4
5
|
/** 条数硬上限。 */
|
|
@@ -150,6 +151,7 @@ const SKIPPED_REASON_LABELS = {
|
|
|
150
151
|
'no-evidence': '无独立证据',
|
|
151
152
|
'version-mismatch': '版本条件不匹配',
|
|
152
153
|
'platform-mismatch': '平台条件不匹配',
|
|
154
|
+
'review-due': '已到复核时间',
|
|
153
155
|
limit: '超出条数上限',
|
|
154
156
|
'budget-stop': '预算放不下',
|
|
155
157
|
budget: '预算放不下(旧值)',
|
|
@@ -478,6 +480,10 @@ export function retrieveLessons(lessons, evidence, q) {
|
|
|
478
480
|
for (const lesson of lessons) {
|
|
479
481
|
if (!scopeMatches(lesson, q))
|
|
480
482
|
continue;
|
|
483
|
+
if (needsMemoryReview(lesson, q.now ?? Date.now())) {
|
|
484
|
+
skipped.push({ lessonId: lesson.id, reason: 'review-due' });
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
481
487
|
const excludedReason = EXCLUDED_STATE_REASONS[lesson.state];
|
|
482
488
|
if (excludedReason !== undefined) {
|
|
483
489
|
skipped.push({ lessonId: lesson.id, reason: excludedReason });
|
package/lib/web-actions.js
CHANGED
|
@@ -104,6 +104,36 @@ export function createDreamActionHandler(options = {}) {
|
|
|
104
104
|
if (request.signal.aborted)
|
|
105
105
|
return fail(409, 'cancelled', '操作已取消');
|
|
106
106
|
const operation = text(body, 'operation');
|
|
107
|
+
if (operation === 'automatic-chat' || operation === 'automatic-flush') {
|
|
108
|
+
const runtime = automaticRuntime(cfg);
|
|
109
|
+
if (!runtime)
|
|
110
|
+
return fail(503, 'unavailable', '自动功能尚未就绪');
|
|
111
|
+
if (operation === 'automatic-flush')
|
|
112
|
+
runtime.flush();
|
|
113
|
+
else {
|
|
114
|
+
if (['use', 'contribute'].some(key => body[key] !== undefined && body[key] !== null && typeof body[key] !== 'boolean')
|
|
115
|
+
|| (body.use === undefined && body.contribute === undefined))
|
|
116
|
+
throw new KnowledgeError('invalid', '聊天记忆开关需要布尔值或跟随全局');
|
|
117
|
+
runtime.controlSession(text(body, 'sessionId'), { use: body.use, contribute: body.contribute });
|
|
118
|
+
}
|
|
119
|
+
return json(200, { ok: true, automatic: runtime.status() });
|
|
120
|
+
}
|
|
121
|
+
if (operation === 'review-batch') {
|
|
122
|
+
if (!['accept', 'reject'].includes(String(body.action)) || !Array.isArray(body.items) || body.items.length < 1 || body.items.length > 20)
|
|
123
|
+
throw new KnowledgeError('invalid', '批量审阅每次选择 1 到 20 条候选');
|
|
124
|
+
const key = text(body, 'requestId');
|
|
125
|
+
if (!/^[a-zA-Z0-9_-]{8,100}$/.test(key))
|
|
126
|
+
throw new KnowledgeError('invalid', '审阅请求标识无效');
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const updates = body.items.map(item => {
|
|
129
|
+
if (!item || typeof item.lessonId !== 'string' || seen.has(item.lessonId) || !Number.isSafeInteger(item.expectedRevision) || item.expectedRevision < 1)
|
|
130
|
+
throw new KnowledgeError('invalid', '候选及版本无效或重复');
|
|
131
|
+
seen.add(item.lessonId);
|
|
132
|
+
return { id: item.lessonId, expectedRevision: item.expectedRevision, patch: {} };
|
|
133
|
+
});
|
|
134
|
+
const lessons = store.updateLessonsBatch(updates, 'web-review-batch:' + key, body.action === 'accept' ? 'accepted' : 'rejected');
|
|
135
|
+
return json(200, maskDeep({ ok: true, lessons, message: '已完成所选候选的审阅' }));
|
|
136
|
+
}
|
|
107
137
|
if (operation === 'automatic') {
|
|
108
138
|
if ((body.enabled !== undefined && typeof body.enabled !== 'boolean') || (body.retrievalEnabled !== undefined && typeof body.retrievalEnabled !== 'boolean') || (body.enabled === undefined && body.retrievalEnabled === undefined))
|
|
109
139
|
throw new KnowledgeError('invalid', '自动功能开关必须是布尔值');
|
|
@@ -169,6 +199,8 @@ export function createDreamActionHandler(options = {}) {
|
|
|
169
199
|
throw new KnowledgeError('invalid', '不支持此操作');
|
|
170
200
|
}
|
|
171
201
|
catch (error) {
|
|
202
|
+
if (error instanceof Error && error.message.startsWith('automatic-'))
|
|
203
|
+
return fail(409, 'invalid', error.message === 'automatic-main-task-active' ? '主任务进行中,请等待完成后整理' : '聊天状态已变化或正在写入,请刷新后重试');
|
|
172
204
|
if (error instanceof KnowledgeError)
|
|
173
205
|
return fail(error.code === 'revision' || error.code === 'duplicate' ? 409 : error.code === 'io' ? 500 : 400, error.code, error.message);
|
|
174
206
|
return fail(500, 'io', '操作失败:' + (error instanceof Error ? error.message : String(error)));
|
package/lib/web.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ResolvedDreamConfig } from './config.js';
|
|
2
2
|
import { type DreamEntry, type DreamStats } from './journal.js';
|
|
3
3
|
import type { Evidence, Lesson } from './knowledge.js';
|
|
4
|
+
import { memoryDirectory } from './memory.js';
|
|
4
5
|
/** 面板与浏览器说话的同源路由。 */
|
|
5
6
|
export declare const DREAM_ROUTE = "/_dsh/dsh-dream/journal";
|
|
6
7
|
/** M1 经验面板的只读路由。 */
|
|
@@ -43,7 +44,11 @@ export interface KnowledgeWebPayload {
|
|
|
43
44
|
lessons: Array<Lesson & {
|
|
44
45
|
scopeLabel: string;
|
|
45
46
|
evidenceSummary: string;
|
|
47
|
+
reviewDue?: boolean;
|
|
48
|
+
nextReviewAt?: string;
|
|
49
|
+
lastRetrievedAt?: string;
|
|
46
50
|
}>;
|
|
51
|
+
memory?: ReturnType<typeof memoryDirectory>;
|
|
47
52
|
/** 脱敏后的证据记录。 */
|
|
48
53
|
evidence: Evidence[];
|
|
49
54
|
/** 全量统计(不受条数影响)。 */
|
package/lib/web.js
CHANGED
|
@@ -22,6 +22,8 @@ import { dreamStats, readDreams, searchDreams } from './journal.js';
|
|
|
22
22
|
import { KnowledgeStore } from './knowledge-store.js';
|
|
23
23
|
import { isDreamReference, maskSecrets } from './mask.js';
|
|
24
24
|
import { buildEvidenceSummary, lessonScopeLabel } from './retrieval.js';
|
|
25
|
+
import { memoryDirectory, needsMemoryReview, reviewDeadline } from './memory.js';
|
|
26
|
+
import { readAutomaticState } from './automatic-state.js';
|
|
25
27
|
/** 面板与浏览器说话的同源路由。 */
|
|
26
28
|
export const DREAM_ROUTE = '/_dsh/dsh-dream/journal';
|
|
27
29
|
/** M1 经验面板的只读路由。 */
|
|
@@ -343,14 +345,23 @@ export function createKnowledgeWebHandler(options = {}) {
|
|
|
343
345
|
const lessons = reader.listLessons();
|
|
344
346
|
const evidence = reader.listEvidence();
|
|
345
347
|
const stats = normalizeKnowledgeStats(reader.stats());
|
|
348
|
+
let retrieved = {};
|
|
349
|
+
try {
|
|
350
|
+
retrieved = readAutomaticState({ ...resolveConfig(options.config), journalDir: journalDirOf(options) }).retrieved ?? {};
|
|
351
|
+
}
|
|
352
|
+
catch { /* read-only panel still reports knowledge */ }
|
|
346
353
|
const payload = {
|
|
347
354
|
lessons: lessons.map((lesson) => maskDeep({
|
|
348
355
|
...lesson,
|
|
349
356
|
scopeLabel: lessonScopeLabel(lesson.scope),
|
|
350
357
|
evidenceSummary: buildEvidenceSummary(lesson, evidence),
|
|
358
|
+
reviewDue: needsMemoryReview(lesson),
|
|
359
|
+
...(reviewDeadline(lesson) === undefined ? {} : { nextReviewAt: new Date(reviewDeadline(lesson)).toISOString() }),
|
|
360
|
+
...(retrieved[lesson.id] ? { lastRetrievedAt: new Date(retrieved[lesson.id].at).toISOString() } : {}),
|
|
351
361
|
})),
|
|
352
362
|
evidence: evidence.map((item) => maskDeep(item)),
|
|
353
363
|
stats,
|
|
364
|
+
memory: maskDeep(memoryDirectory(lessons, evidence)),
|
|
354
365
|
};
|
|
355
366
|
responseJson(res, 200, payload);
|
|
356
367
|
}
|