@stardustlc/dsh-dream 0.6.0 → 0.7.1
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 +41 -15
- package/README.en.md +23 -105
- package/README.md +22 -152
- package/docs/CHANGELOG.en.md +43 -0
- package/docs/USAGE.en.md +84 -0
- package/docs/USAGE.md +133 -0
- package/docs/VALIDATION.md +29 -0
- package/docs/validation/0.5.1.md +68 -0
- package/docs/validation/0.5.2.md +39 -0
- package/docs/validation/0.6.0.md +73 -0
- package/docs/validation/0.7.0.md +11 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +634 -212
- package/lib/index.js +3 -1
- package/lib/knowledge-store.d.ts +2 -0
- package/lib/knowledge-store.js +40 -0
- package/lib/knowledge.d.ts +11 -0
- package/lib/knowledge.js +25 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +209 -205
- package/lib/web-actions.d.ts +11 -0
- package/lib/web-actions.js +175 -0
- package/lib/web.d.ts +2 -0
- package/lib/web.js +1 -1
- package/package.json +4 -3
- package/skills/dream-protocol/SKILL.md +6 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Mutations are mounted exclusively on Connection's authenticated Fetch carrier. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join } from 'node:path';
|
|
5
|
+
import { resolveConfig } from './config.js';
|
|
6
|
+
import { KnowledgeError } from './knowledge.js';
|
|
7
|
+
import { KnowledgeStore } from './knowledge-store.js';
|
|
8
|
+
import { maskSecrets } from './mask.js';
|
|
9
|
+
import { buildDreamTools, executeDreamReview } from './tools.js';
|
|
10
|
+
import { rollbackBridge } from './bridge.js';
|
|
11
|
+
import { maskDeep } from './web.js';
|
|
12
|
+
export const DREAM_ACTION_ROUTE = '/api/dsh-dream/actions';
|
|
13
|
+
const BODY_LIMIT = 64 * 1024;
|
|
14
|
+
const PREVIEW_LIFETIME = 10 * 60 * 1000;
|
|
15
|
+
const MAX_PREVIEWS = 100;
|
|
16
|
+
function json(status, value) {
|
|
17
|
+
return new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } });
|
|
18
|
+
}
|
|
19
|
+
function fail(status, code, message) {
|
|
20
|
+
return json(status, { ok: false, error: { code, message: maskSecrets(message) } });
|
|
21
|
+
}
|
|
22
|
+
function text(body, key) {
|
|
23
|
+
const value = body[key];
|
|
24
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > 2000)
|
|
25
|
+
throw new KnowledgeError('invalid', key + ' 必须是非空文本,最多 2000 字符');
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
function checkRevision(store, body) {
|
|
29
|
+
const id = text(body, 'lessonId');
|
|
30
|
+
const lesson = store.getLesson(id);
|
|
31
|
+
if (lesson === undefined)
|
|
32
|
+
throw new KnowledgeError('invalid', '经验不存在,请刷新列表');
|
|
33
|
+
if (typeof body.expectedRevision !== 'number' || !Number.isInteger(body.expectedRevision) || body.expectedRevision < 1)
|
|
34
|
+
throw new KnowledgeError('invalid', '缺少有效的经验版本');
|
|
35
|
+
if (lesson.revision !== body.expectedRevision)
|
|
36
|
+
throw new KnowledgeError('revision', '经验已更新,请刷新后重新操作');
|
|
37
|
+
return lesson;
|
|
38
|
+
}
|
|
39
|
+
/** Connection has authenticated the operator before this handler is called. */
|
|
40
|
+
export function createDreamActionHandler(options = {}) {
|
|
41
|
+
const cfg = { ...resolveConfig(options.config), ...(options.journalDir === undefined ? {} : { journalDir: options.journalDir }) };
|
|
42
|
+
const store = new KnowledgeStore(join(cfg.journalDir, 'knowledge'), { maskSecrets: cfg.maskSecrets });
|
|
43
|
+
const bridge = buildDreamTools(cfg).find(tool => tool.name === 'dream_bridge');
|
|
44
|
+
const previews = new Map();
|
|
45
|
+
return async (request) => {
|
|
46
|
+
if (request.method !== 'POST')
|
|
47
|
+
return fail(405, 'method', '请使用 POST');
|
|
48
|
+
if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
|
|
49
|
+
return fail(415, 'content-type', '需要 application/json');
|
|
50
|
+
// A web form or cross-origin simple request cannot supply this header.
|
|
51
|
+
if (request.headers.get('x-dsh-dream-action') !== '1')
|
|
52
|
+
return fail(403, 'forbidden', '请从 Dream 面板操作');
|
|
53
|
+
const site = request.headers.get('sec-fetch-site');
|
|
54
|
+
if (site !== null && site !== 'same-origin' && site !== 'none')
|
|
55
|
+
return fail(403, 'forbidden', '拒绝跨站操作');
|
|
56
|
+
const origin = request.headers.get('origin');
|
|
57
|
+
if (origin !== null && /^https?:/i.test(origin)) {
|
|
58
|
+
try {
|
|
59
|
+
// The official node:http bridge intentionally uses http://dsh.internal
|
|
60
|
+
// as its Fetch URL base; the preserved Host is the original authority.
|
|
61
|
+
const host = request.headers.get('host');
|
|
62
|
+
const authority = host === null ? new URL(request.url).host : new URL('http://' + host).host;
|
|
63
|
+
if (new URL(origin).host !== authority)
|
|
64
|
+
return fail(403, 'forbidden', '拒绝跨源操作');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return fail(403, 'forbidden', '无效来源');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const reader = request.body?.getReader();
|
|
72
|
+
if (reader === undefined)
|
|
73
|
+
return fail(400, 'invalid', '缺少请求内容');
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let size = 0;
|
|
76
|
+
try {
|
|
77
|
+
for (;;) {
|
|
78
|
+
const part = await reader.read();
|
|
79
|
+
if (part.done)
|
|
80
|
+
break;
|
|
81
|
+
size += part.value.byteLength;
|
|
82
|
+
if (size > BODY_LIMIT) {
|
|
83
|
+
await reader.cancel();
|
|
84
|
+
return fail(413, 'too-large', '请求内容超过 64 KB');
|
|
85
|
+
}
|
|
86
|
+
chunks.push(part.value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
reader.releaseLock();
|
|
91
|
+
}
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return fail(400, 'invalid', '请求不是有效 JSON');
|
|
98
|
+
}
|
|
99
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
100
|
+
return fail(400, 'invalid', '请求必须是对象');
|
|
101
|
+
const body = raw;
|
|
102
|
+
if (request.signal.aborted)
|
|
103
|
+
return fail(409, 'cancelled', '操作已取消');
|
|
104
|
+
const operation = text(body, 'operation');
|
|
105
|
+
if (operation === 'review') {
|
|
106
|
+
if (!['accept', 'reject', 'reopen', 'mark-stale', 'resolve-conflict'].includes(String(body.action)))
|
|
107
|
+
throw new KnowledgeError('invalid', '不支持此页面审阅动作');
|
|
108
|
+
checkRevision(store, body);
|
|
109
|
+
const result = await executeDreamReview(cfg, body, undefined, 'human');
|
|
110
|
+
return json(200, maskDeep(result));
|
|
111
|
+
}
|
|
112
|
+
if (operation === 'feedback') {
|
|
113
|
+
// The store checks revision inside its lock and keeps retry keys idempotent.
|
|
114
|
+
const id = text(body, 'lessonId');
|
|
115
|
+
const key = text(body, 'requestId');
|
|
116
|
+
if (!/^[a-zA-Z0-9_-]{8,100}$/.test(key))
|
|
117
|
+
throw new KnowledgeError('invalid', '反馈请求标识无效');
|
|
118
|
+
const note = body.note === undefined ? undefined : text(body, 'note');
|
|
119
|
+
const lesson = store.recordFeedback(id, body.vote, note, body.expectedRevision, 'web-feedback:' + key);
|
|
120
|
+
return json(200, maskDeep({ ok: true, lesson, message: '已记录使用反馈,审阅状态保持原样' }));
|
|
121
|
+
}
|
|
122
|
+
for (const [key, value] of previews)
|
|
123
|
+
if (value.expires < Date.now())
|
|
124
|
+
previews.delete(key);
|
|
125
|
+
if (operation === 'preview') {
|
|
126
|
+
const lesson = checkRevision(store, body);
|
|
127
|
+
const root = lesson.scope.workspaceRoot;
|
|
128
|
+
if (root === undefined || !isAbsolute(root))
|
|
129
|
+
throw new KnowledgeError('invalid', '这条经验没有项目目录,请在对应项目的对话中预览规则');
|
|
130
|
+
const canonicalRoot = realpathSync(root);
|
|
131
|
+
const result = await bridge.execute({ mode: 'preview', lessonIds: [lesson.id], path: 'AGENTS.md', projectId: lesson.scope.projectId }, { cwd: canonicalRoot });
|
|
132
|
+
if (result.ok !== true)
|
|
133
|
+
return json(409, maskDeep(result));
|
|
134
|
+
if (result.lessons.length === 0)
|
|
135
|
+
return fail(409, 'ineligible', '当前经验不满足写入资格,请先查看适用范围与审阅状态');
|
|
136
|
+
while (previews.size >= MAX_PREVIEWS)
|
|
137
|
+
previews.delete(previews.keys().next().value);
|
|
138
|
+
const token = randomUUID();
|
|
139
|
+
previews.set(token, { expires: Date.now() + PREVIEW_LIFETIME, root: canonicalRoot, lessonId: lesson.id, revision: lesson.revision, projectId: lesson.scope.projectId, sha: result.expected.sha256, previewId: result.previewId });
|
|
140
|
+
return json(200, { ...maskDeep(result), token });
|
|
141
|
+
}
|
|
142
|
+
if (operation === 'apply') {
|
|
143
|
+
const token = text(body, 'token');
|
|
144
|
+
const preview = previews.get(token);
|
|
145
|
+
if (preview === undefined)
|
|
146
|
+
return fail(409, 'preview-expired', '预览已过期,请重新预览');
|
|
147
|
+
const lesson = checkRevision(store, { lessonId: preview.lessonId, expectedRevision: preview.revision });
|
|
148
|
+
if (realpathSync(lesson.scope.workspaceRoot) !== preview.root)
|
|
149
|
+
return fail(409, 'revision', '项目目录已变化,请重新预览');
|
|
150
|
+
const result = await bridge.execute({ mode: 'apply', lessonIds: [preview.lessonId], path: 'AGENTS.md', projectId: preview.projectId, expectedSha256: preview.sha, previewId: preview.previewId }, { cwd: preview.root });
|
|
151
|
+
previews.delete(token);
|
|
152
|
+
return json(result.ok === true ? 200 : 409, maskDeep(result));
|
|
153
|
+
}
|
|
154
|
+
if (operation === 'rollback') {
|
|
155
|
+
const result = rollbackBridge({ journalDir: cfg.journalDir, projectRoot: '', backupId: text(body, 'backupId') });
|
|
156
|
+
return json(result.ok === true ? 200 : 409, maskDeep(result));
|
|
157
|
+
}
|
|
158
|
+
throw new KnowledgeError('invalid', '不支持此操作');
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof KnowledgeError)
|
|
162
|
+
return fail(error.code === 'revision' || error.code === 'duplicate' ? 409 : error.code === 'io' ? 500 : 400, error.code, error.message);
|
|
163
|
+
return fail(500, 'io', '操作失败:' + (error instanceof Error ? error.message : String(error)));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** No unauthenticated webServer fallback: old hosts keep their read-only panel. */
|
|
168
|
+
export function installDreamActions(ctx, options = {}) {
|
|
169
|
+
const fetch = createDreamActionHandler(options);
|
|
170
|
+
ctx.inject(['connection'], (host) => {
|
|
171
|
+
if (typeof host.connection?.fetch?.register !== 'function')
|
|
172
|
+
return;
|
|
173
|
+
host.connection.fetch.register({ path: DREAM_ACTION_ROUTE, methods: ['POST'], requestBody: 'buffered', fetch });
|
|
174
|
+
});
|
|
175
|
+
}
|
package/lib/web.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export declare function limitFromQuery(raw: unknown, fallback?: number): number;
|
|
|
106
106
|
export declare function knowledgeDirOf(options: DreamWebOptions, journalDir?: string): string;
|
|
107
107
|
/** 解析后的 bridgeDir:显式路径优先,否则 = <journalDir>/bridge。 */
|
|
108
108
|
export declare function bridgeDirOf(options: DreamWebOptions, journalDir?: string): string;
|
|
109
|
+
/** 递归脱敏;结构化绝对路径按目录分段,避免 Unix 分隔符把正常路径连成长令牌。 */
|
|
110
|
+
export declare function maskDeep<T>(value: T, key?: string): T;
|
|
109
111
|
/** 知识目录不存在(或尚未建立)时的空载荷:不写盘、不报 404。 */
|
|
110
112
|
export declare function emptyKnowledgePayload(): KnowledgeWebPayload;
|
|
111
113
|
/**
|
package/lib/web.js
CHANGED
|
@@ -139,7 +139,7 @@ const HASH_KEY_RE = /^(?:sourceHash|beforeSha256|afterSha256|afterBlockSha256|af
|
|
|
139
139
|
const REFERENCE_KEYS = new Set(['id', 'lessonId', 'lessonIds', 'evidenceIds', 'conflictIds', 'supersedes', 'targetId', 'affectedIds', 'backupId', 'previewId']);
|
|
140
140
|
const PATH_KEYS = new Set(['target', 'path', 'resolvedPath', 'workspaceRoot']);
|
|
141
141
|
/** 递归脱敏;结构化绝对路径按目录分段,避免 Unix 分隔符把正常路径连成长令牌。 */
|
|
142
|
-
function maskDeep(value, key) {
|
|
142
|
+
export function maskDeep(value, key) {
|
|
143
143
|
if (typeof value === 'string') {
|
|
144
144
|
if (key !== undefined && HASH_KEY_RE.test(key) && /^[0-9a-f]{64}$/i.test(value))
|
|
145
145
|
return value;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardustlc/dsh-dream",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "回顾会话、保存反思,并把经过核验和审阅的经验用于后续任务。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"README.md",
|
|
22
22
|
"README.en.md",
|
|
23
|
-
"CHANGELOG.md"
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"docs"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsc -p tsconfig.json",
|
|
@@ -9,6 +9,8 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
|
|
|
9
9
|
|
|
10
10
|
但「梦」和「经验」是两条流水线:**第一人称感悟**是主观的,进梦境日记;**技术经验**必须可执行、有条件、有证据,先进候选,再经审阅才可能被任务检索取用。
|
|
11
11
|
|
|
12
|
+
插件依靠你调用工具完成记录和取回,不会在后台监听所有会话或每轮自动学习。用户无需手工填写经验;遵循下述任务前取回、任务后提交候选的流程。日记写入、采纳与规则应用是不同操作,不能把候选当成已生效规则。
|
|
13
|
+
|
|
12
14
|
## 两条流水线(不要混)
|
|
13
15
|
|
|
14
16
|
| | 第一人称感悟(梦) | 技术经验(lesson) |
|
|
@@ -65,7 +67,7 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
|
|
|
65
67
|
- 想查看/审阅候选、处理冲突 → `dream_review`;
|
|
66
68
|
- attach-evidence 只补充核验后的证据,不自动采纳;resolve-conflict 必须给 resolution、affectedIds、note,整组原子更新;reopen 将驳回经验重新送审。
|
|
67
69
|
|
|
68
|
-
-
|
|
70
|
+
- 面板操作:「设置 → 梦境日记」可浏览时间线、查看来源、采纳或驳回候选、标记待复核、处理冲突与记录使用反馈。规则变更单独预览后应用,也可确认回滚;真实页面操作记录为 human,工具操作记录为 model。界面跟随宿主语言,日记和经验原文保留。
|
|
69
71
|
|
|
70
72
|
---
|
|
71
73
|
|
|
@@ -75,6 +77,8 @@ You are an agent that dreams. Dreams are not hallucinations — they are memory
|
|
|
75
77
|
|
|
76
78
|
Dreams and **lessons** are two separate pipelines: a **first-person reflection** is subjective and goes to the dream journal; a **technical lesson** must be actionable, conditional and evidence-backed — it starts as a candidate and is only retrieved for tasks after review.
|
|
77
79
|
|
|
80
|
+
The plugin records and retrieves information when you call its tools; it does not monitor every session or learn after every turn in the background. Users do not need to type lessons manually. Follow the retrieval and candidate-submission steps below. Saving a journal, accepting a lesson and applying a rule are separate operations; a candidate is not an active rule.
|
|
81
|
+
|
|
78
82
|
## Two pipelines (do not mix)
|
|
79
83
|
|
|
80
84
|
| | First-person reflection (dream) | Technical lesson |
|
|
@@ -130,4 +134,4 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
|
|
|
130
134
|
- Retrieving a few reusable lessons → `dream_context` (scoped, budgeted, read-only);
|
|
131
135
|
- Browsing or reviewing candidates and conflicts → `dream_review`;
|
|
132
136
|
- attach-evidence adds verified sources without promoting state. resolve-conflict requires resolution, affectedIds and note and updates the group atomically. reopen sends a rejected lesson back for review.
|
|
133
|
-
-
|
|
137
|
+
- **Settings → Dream journal** lets users browse the timeline and sources, accept/reject candidates, request re-validation, resolve conflicts and record usage feedback. Rule changes require a separate preview and apply action and can be rolled back with confirmation. Page actions record human; tool actions record model. UI language follows the host; journal and lesson content remains original.
|