@stardustlc/dsh-dream 0.8.1 → 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 +9 -0
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/docs/USAGE.en.md +8 -2
- package/docs/USAGE.md +9 -2
- package/docs/VALIDATION.md +1 -1
- 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 +142 -75
- 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/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
|
}
|