@stardustlc/dsh-dream 0.5.1 → 0.7.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 +22 -0
- package/README.en.md +39 -14
- package/README.md +43 -18
- package/lib/bridge.d.ts +205 -2
- package/lib/bridge.js +1053 -6
- package/lib/client.js +634 -27
- package/lib/evidence.d.ts +15 -0
- package/lib/evidence.js +123 -0
- package/lib/index.js +3 -1
- package/lib/knowledge-store.d.ts +23 -5
- package/lib/knowledge-store.js +311 -41
- package/lib/knowledge.d.ts +40 -0
- package/lib/knowledge.js +54 -0
- package/lib/mask.d.ts +3 -1
- package/lib/mask.js +8 -2
- package/lib/retrieval.d.ts +12 -0
- package/lib/retrieval.js +68 -3
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +436 -128
- package/lib/web-actions.d.ts +11 -0
- package/lib/web-actions.js +175 -0
- package/lib/web.d.ts +50 -1
- package/lib/web.js +153 -13
- package/package.json +2 -2
- package/skills/dream-protocol/SKILL.md +9 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EvidenceInput } from './knowledge.js';
|
|
2
|
+
export interface EvidenceClaim extends EvidenceInput {
|
|
3
|
+
/** 摘要为转述时,另给原文片段以便核验;不持久化原文。 */
|
|
4
|
+
quote?: string;
|
|
5
|
+
artifactPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EvidenceContext {
|
|
8
|
+
sessionsRoot: string;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
workspaceRoot?: string;
|
|
11
|
+
/** 本地文件只在宿主执行上下文的工作区内读取。 */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 核验失败保留 claimed 和原因;永远不以模型提供的 hash、verification 或用户角色为准。 */
|
|
15
|
+
export declare function verifyEvidence(claim: EvidenceClaim, context: EvidenceContext): EvidenceInput;
|
package/lib/evidence.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** 工具参数只是来源声明;只有本模块读到匹配的可见记录后才产生 read。 */
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { decompressAll, listSessionFiles } from './sessions.js';
|
|
6
|
+
function record(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
function visibleText(value) {
|
|
10
|
+
if (typeof value === 'string')
|
|
11
|
+
return value;
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.map(visibleText).join('');
|
|
14
|
+
const item = record(value);
|
|
15
|
+
if (item.type !== undefined && item.type !== 'text')
|
|
16
|
+
return '';
|
|
17
|
+
return visibleText(item.text ?? item.content ?? '');
|
|
18
|
+
}
|
|
19
|
+
function normalized(text) { return text.normalize('NFC').replace(/\s+/g, ' ').trim(); }
|
|
20
|
+
function inside(root, file) {
|
|
21
|
+
const rel = relative(root, file);
|
|
22
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
23
|
+
}
|
|
24
|
+
function samePath(a, b) {
|
|
25
|
+
const canonical = (p) => { try {
|
|
26
|
+
return realpathSync(p);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return resolve(p);
|
|
30
|
+
} };
|
|
31
|
+
const left = canonical(a), right = canonical(b);
|
|
32
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
33
|
+
}
|
|
34
|
+
function readBounded(file, maxBytes) {
|
|
35
|
+
const stat = statSync(file);
|
|
36
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
37
|
+
throw new Error('source-unreadable');
|
|
38
|
+
const bytes = readFileSync(file);
|
|
39
|
+
if (bytes.length > maxBytes)
|
|
40
|
+
throw new Error('source-unreadable');
|
|
41
|
+
return bytes;
|
|
42
|
+
}
|
|
43
|
+
/** 核验失败保留 claimed 和原因;永远不以模型提供的 hash、verification 或用户角色为准。 */
|
|
44
|
+
export function verifyEvidence(claim, context) {
|
|
45
|
+
const base = {
|
|
46
|
+
kind: claim.kind, summary: claim.summary, verification: 'claimed',
|
|
47
|
+
...(claim.sessionId !== undefined ? { sessionId: claim.sessionId } : {}),
|
|
48
|
+
...(claim.recordSeq !== undefined ? { recordSeq: claim.recordSeq } : {}),
|
|
49
|
+
...(context.projectId !== undefined ? { projectId: context.projectId } : {}),
|
|
50
|
+
};
|
|
51
|
+
const held = (reason) => ({ ...base, verificationReason: reason });
|
|
52
|
+
if (claim.verification !== 'read')
|
|
53
|
+
return held('model-claimed');
|
|
54
|
+
const quote = normalized(claim.quote ?? claim.summary);
|
|
55
|
+
if (quote === '')
|
|
56
|
+
return held('quote-missing');
|
|
57
|
+
try {
|
|
58
|
+
let text;
|
|
59
|
+
if (claim.kind === 'local-artifact') {
|
|
60
|
+
if (!context.cwd || !claim.artifactPath)
|
|
61
|
+
return held('artifact-context-missing');
|
|
62
|
+
const root = realpathSync(context.cwd);
|
|
63
|
+
if (context.workspaceRoot && !samePath(root, context.workspaceRoot))
|
|
64
|
+
return held('workspace-mismatch');
|
|
65
|
+
const file = realpathSync(resolve(root, claim.artifactPath));
|
|
66
|
+
if (!inside(root, file))
|
|
67
|
+
return held('artifact-outside-workspace');
|
|
68
|
+
const bytes = readBounded(file, 1024 * 1024);
|
|
69
|
+
text = bytes.toString('utf8');
|
|
70
|
+
if (bytes.includes(0) || !Buffer.from(text, 'utf8').equals(bytes))
|
|
71
|
+
return held('artifact-not-text');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (!claim.sessionId || claim.recordSeq === undefined)
|
|
75
|
+
return held('session-reference-missing');
|
|
76
|
+
const root = realpathSync(context.sessionsRoot);
|
|
77
|
+
const candidates = listSessionFiles(root, 10000).filter(file => basename(dirname(file)) === claim.sessionId);
|
|
78
|
+
if (candidates.length !== 1)
|
|
79
|
+
return held('session-not-found-or-ambiguous');
|
|
80
|
+
const file = realpathSync(candidates[0]);
|
|
81
|
+
if (!inside(root, file))
|
|
82
|
+
return held('session-outside-root');
|
|
83
|
+
if (context.projectId && basename(dirname(dirname(file))) !== context.projectId)
|
|
84
|
+
return held('project-mismatch');
|
|
85
|
+
const bytes = readBounded(file, 64 * 1024 * 1024);
|
|
86
|
+
const content = file.endsWith('.zstd') ? decompressAll(bytes) : bytes.toString('utf8');
|
|
87
|
+
if (Buffer.byteLength(content) > 64 * 1024 * 1024)
|
|
88
|
+
return held('source-unreadable');
|
|
89
|
+
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
90
|
+
const header = record(JSON.parse(lines[0] ?? 'null'));
|
|
91
|
+
if (header.type !== 'session' || header.id !== claim.sessionId || (header.version !== undefined && ![0, 1, 2, 3, 4].includes(header.version)))
|
|
92
|
+
return held('session-header-mismatch');
|
|
93
|
+
if (context.workspaceRoot && (typeof header.cwd !== 'string' || !samePath(header.cwd, context.workspaceRoot)))
|
|
94
|
+
return held('workspace-mismatch');
|
|
95
|
+
const found = [];
|
|
96
|
+
for (const line of lines.slice(1)) {
|
|
97
|
+
try {
|
|
98
|
+
const item = record(JSON.parse(line));
|
|
99
|
+
if (item.seq !== undefined && String(item.seq) === String(claim.recordSeq))
|
|
100
|
+
found.push(item);
|
|
101
|
+
}
|
|
102
|
+
catch { /* 坏行不是证据 */ }
|
|
103
|
+
}
|
|
104
|
+
if (found.length !== 1)
|
|
105
|
+
return held('record-not-found-or-ambiguous');
|
|
106
|
+
const entry = found[0], data = record(entry.data);
|
|
107
|
+
if (claim.kind === 'user-correction' && (entry.type !== 'user/message' || (header.version >= 4 && record(data.source).kind !== 'user')))
|
|
108
|
+
return held('not-human-message');
|
|
109
|
+
if (entry.type !== 'user/message' && entry.type !== 'assistant/message' && entry.type !== 'tool/result')
|
|
110
|
+
return held('record-not-visible');
|
|
111
|
+
text = visibleText(data.content ?? data.text ?? data.message ?? entry.data);
|
|
112
|
+
}
|
|
113
|
+
if (!normalized(text).includes(quote))
|
|
114
|
+
return held('quote-mismatch');
|
|
115
|
+
const hash = createHash('sha256').update(text, 'utf8').digest('hex');
|
|
116
|
+
if (claim.sourceHash !== undefined && claim.sourceHash !== hash)
|
|
117
|
+
return held('source-hash-mismatch');
|
|
118
|
+
return { ...base, verification: 'read', sourceHash: hash, verificationReason: 'source-verified' };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return held('source-unreadable');
|
|
122
|
+
}
|
|
123
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { resolveConfig } from './config.js';
|
|
10
10
|
import { buildDreamTools } from './tools.js';
|
|
11
11
|
import { installDreamWeb } from './web.js';
|
|
12
|
+
import { installDreamActions } from './web-actions.js';
|
|
12
13
|
/** cordis 服务注入:要用 ctx.tools 与 ctx.skills。 */
|
|
13
14
|
export const name = 'dream';
|
|
14
15
|
export const inject = ['tools', 'skills'];
|
|
@@ -56,7 +57,8 @@ export function apply(ctx, config) {
|
|
|
56
57
|
}
|
|
57
58
|
// 面板同源路由:宿主有 webServer 才由 web.ts 用 ctx.inject 挂上;headless 夹具没有 inject 就跳过。
|
|
58
59
|
if (typeof ctx.inject === 'function') {
|
|
59
|
-
installDreamWeb(ctx, { journalDir: cfg.journalDir });
|
|
60
|
+
installDreamWeb(ctx, { journalDir: cfg.journalDir, config: cfg });
|
|
61
|
+
installDreamActions(ctx, { config: cfg });
|
|
60
62
|
}
|
|
61
63
|
if (typeof ctx.on === 'function') {
|
|
62
64
|
ctx.on('dispose', () => {
|
package/lib/knowledge-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Evidence, type EvidenceInput, type Lesson, type LessonInput, type LessonState, type ReviewDecision } from './knowledge.js';
|
|
1
|
+
import { type Evidence, type EvidenceInput, type Lesson, type LessonInput, type LessonReviewResolution, type LessonState, type ReviewDecision } from './knowledge.js';
|
|
2
2
|
/** events.jsonl 默认最多回放的行数(有界读取)。 */
|
|
3
3
|
export declare const MAX_REPLAY_EVENTS = 200000;
|
|
4
4
|
/** KnowledgeStore 构造选项(都可省略,保持 FREEZE 的 constructor(knowledgeDir) 可用)。 */
|
|
@@ -47,16 +47,31 @@ export declare class KnowledgeStore {
|
|
|
47
47
|
/**
|
|
48
48
|
* 创建候选经验。同 idempotencyKey + 同 requestHash 幂等;同指纹(文本 + 范围相同)确定性合并,
|
|
49
49
|
* 只把新证据并入已有经验,不新增经验。
|
|
50
|
-
*
|
|
50
|
+
* 经调用方核验的 read 来源可用于项目经验;来源读取不代表用户采纳,review 始终 unreviewed。
|
|
51
51
|
*/
|
|
52
|
-
createLesson(input: LessonInput, idempotencyKey: string
|
|
52
|
+
createLesson(input: LessonInput, idempotencyKey: string, options?: {
|
|
53
|
+
requireReview?: boolean;
|
|
54
|
+
}): {
|
|
53
55
|
lesson: Lesson;
|
|
54
56
|
created: boolean;
|
|
55
57
|
};
|
|
58
|
+
/** 追加证据与更新引用共用一把锁;先检查 revision 与全部输入,不改变审阅状态。 */
|
|
59
|
+
attachEvidence(id: string, inputs: EvidenceInput[], expectedRevision: number, idempotencyKey: string): Lesson;
|
|
56
60
|
/** 按 patch 更新经验;revision 不符抛 KnowledgeError('revision')(details 带 currentRevision)且不写。 */
|
|
57
61
|
updateLesson(id: string, patch: Partial<Lesson>, expectedRevision: number, idempotencyKey: string): Lesson;
|
|
62
|
+
/** 记录一次使用反馈,不改变审阅、状态、证据和最后核验时间。 */
|
|
63
|
+
recordFeedback(id: string, vote: 'useful' | 'not-applicable', note: string | undefined, expectedRevision: number, idempotencyKey: string): Lesson;
|
|
58
64
|
/** 审阅经验:accepted → usable,rejected → rejected,unreviewed → candidate。 */
|
|
59
|
-
reviewLesson(id: string, decision: ReviewDecision, expectedRevision: number, idempotencyKey: string, actor?: string): Lesson;
|
|
65
|
+
reviewLesson(id: string, decision: ReviewDecision, expectedRevision: number, idempotencyKey: string, actor?: string, note?: string, resolution?: LessonReviewResolution): Lesson;
|
|
66
|
+
/**
|
|
67
|
+
* 批量原子更新(M2-②):一个事件承载全部变更;先统一校验,
|
|
68
|
+
* 任一条不通过 → 零写入并抛 KnowledgeError(details.failedId)。
|
|
69
|
+
*/
|
|
70
|
+
updateLessonsBatch(updates: Array<{
|
|
71
|
+
id: string;
|
|
72
|
+
patch: Partial<Lesson>;
|
|
73
|
+
expectedRevision: number;
|
|
74
|
+
}>, idempotencyKey: string): Lesson[];
|
|
60
75
|
/** 状态迁移;不允许的迁移抛 KnowledgeError('invalid') 且不写。 */
|
|
61
76
|
applyTransition(id: string, to: LessonState, expectedRevision: number, idempotencyKey: string): Lesson;
|
|
62
77
|
/** 从事件完整重建派生索引与 checkpoint;不删除、不改写 events.jsonl / evidence.jsonl。 */
|
|
@@ -71,10 +86,11 @@ export declare class KnowledgeStore {
|
|
|
71
86
|
badLines: number;
|
|
72
87
|
orphanEvents: number;
|
|
73
88
|
unsupportedVersions: number;
|
|
89
|
+
skippedEvents: number;
|
|
74
90
|
firstReplayedEventId?: string;
|
|
75
91
|
replayMode: 'full' | 'snapshot+tail';
|
|
76
92
|
replayedEvents: number;
|
|
77
|
-
|
|
93
|
+
unreplayedEvents: number;
|
|
78
94
|
};
|
|
79
95
|
/** 只读诊断:锁、文件、计数与最近一次派生索引写失败;绝不获取锁、绝不写盘。 */
|
|
80
96
|
diagnose(): {
|
|
@@ -113,6 +129,7 @@ export declare class KnowledgeStore {
|
|
|
113
129
|
badLines: number;
|
|
114
130
|
orphanEvents: number;
|
|
115
131
|
unsupportedVersions: number;
|
|
132
|
+
skippedEvents: number;
|
|
116
133
|
truncated: boolean;
|
|
117
134
|
byState: Record<LessonState, number>;
|
|
118
135
|
};
|
|
@@ -135,6 +152,7 @@ export declare class KnowledgeStore {
|
|
|
135
152
|
* 在命中该幂等键的那一刻取快照;失败再退回当前实体(best effort)。
|
|
136
153
|
*/
|
|
137
154
|
private resolveIdempotentLesson;
|
|
155
|
+
private resolveIdempotentBatch;
|
|
138
156
|
private replayUntilIdempotencyKey;
|
|
139
157
|
/** 顺序读取 JSONL 的非空行;每 TOUCH_EVERY_LINES 行调用一次 touch(长回放锁心跳)。 */
|
|
140
158
|
private forEachLine;
|