@stardustlc/dsh-dream 0.3.4 → 0.4.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 +59 -0
- package/README.en.md +17 -3
- package/README.md +17 -3
- package/lib/client.js +931 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5 -0
- package/lib/journal.d.ts +16 -7
- package/lib/journal.js +23 -13
- package/lib/web.d.ts +54 -0
- package/lib/web.js +137 -0
- package/package.json +10 -2
- package/skills/dream-protocol/SKILL.md +4 -0
package/lib/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface DreamPluginContext {
|
|
|
20
20
|
}): () => void;
|
|
21
21
|
};
|
|
22
22
|
on?(event: string, listener: () => void): () => void;
|
|
23
|
+
/** cordis 的依赖注入(宿主必给;headless 测试夹具可能没有)。 */
|
|
24
|
+
inject?(services: string[], callback: (scopedCtx: unknown) => void): void;
|
|
23
25
|
}
|
|
24
26
|
/** 随包技能目录。 */
|
|
25
27
|
export declare function bundledSkillsDir(): string;
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { resolveConfig } from './config.js';
|
|
10
10
|
import { buildDreamTools } from './tools.js';
|
|
11
|
+
import { installDreamWeb } from './web.js';
|
|
11
12
|
/** cordis 服务注入:要用 ctx.tools 与 ctx.skills。 */
|
|
12
13
|
export const name = 'dream';
|
|
13
14
|
export const inject = ['tools', 'skills'];
|
|
@@ -53,6 +54,10 @@ export function apply(ctx, config) {
|
|
|
53
54
|
console.warn('[dsh-dream] 做梦协议技能加载失败:' + (error instanceof Error ? error.message : String(error)));
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
// 面板同源路由:宿主有 webServer 才由 web.ts 用 ctx.inject 挂上;headless 夹具没有 inject 就跳过。
|
|
58
|
+
if (typeof ctx.inject === 'function') {
|
|
59
|
+
installDreamWeb(ctx, { journalDir: cfg.journalDir });
|
|
60
|
+
}
|
|
56
61
|
if (typeof ctx.on === 'function') {
|
|
57
62
|
ctx.on('dispose', () => {
|
|
58
63
|
for (const dispose of disposers)
|
package/lib/journal.d.ts
CHANGED
|
@@ -11,16 +11,25 @@ export declare function journalFile(journalDir: string): string;
|
|
|
11
11
|
export declare function saveDream(journalDir: string, reflection: string, lessons: string[], mood: string): DreamEntry;
|
|
12
12
|
/** 倒序读取梦境(新梦在前);损坏行跳过。 */
|
|
13
13
|
export declare function readDreams(journalDir: string, limit: number): DreamEntry[];
|
|
14
|
-
/**
|
|
14
|
+
/** 教训榜的一行:次数与最近一次出现时间(lastAt 缺失时是空串,绝不为 null)。 */
|
|
15
|
+
export interface LessonStat {
|
|
16
|
+
lesson: string;
|
|
17
|
+
count: number;
|
|
18
|
+
lastAt: string;
|
|
19
|
+
}
|
|
20
|
+
/** 梦境统计:总数、心境分布、教训榜。 */
|
|
15
21
|
export interface DreamStats {
|
|
16
22
|
total: number;
|
|
17
23
|
moods: Record<string, number>;
|
|
18
|
-
topLessons:
|
|
19
|
-
lesson: string;
|
|
20
|
-
count: number;
|
|
21
|
-
}>;
|
|
24
|
+
topLessons: LessonStat[];
|
|
22
25
|
}
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* 统计梦境(基于全量日记)。
|
|
28
|
+
*
|
|
29
|
+
* count 与 lastAt 来自同一遍倒序扫描:readDreams 已经新梦在前,所以某个
|
|
30
|
+
* 教训第一次出现的那条梦,就是它最近一次出现;顺手在那次循环里记下原始
|
|
31
|
+
* 大小写,不再二次 flatMap(否则同一条教训的三个数值会来自不同的扫描)。
|
|
32
|
+
*/
|
|
24
33
|
export declare function dreamStats(journalDir: string): DreamStats;
|
|
25
|
-
/** 关键词检索梦境(不区分大小写,命中 reflection/lessons)。 */
|
|
34
|
+
/** 关键词检索梦境(不区分大小写,命中 reflection/lessons;扫描全量,上限见 FULL_SCAN_LIMIT)。 */
|
|
26
35
|
export declare function searchDreams(journalDir: string, query: string, limit: number): DreamEntry[];
|
package/lib/journal.js
CHANGED
|
@@ -24,6 +24,8 @@ export function saveDream(journalDir, reflection, lessons, mood) {
|
|
|
24
24
|
appendFileSync(journalFile(journalDir), JSON.stringify(entry) + '\n', 'utf8');
|
|
25
25
|
return entry;
|
|
26
26
|
}
|
|
27
|
+
/** 需要读全量日记时的上限:检索与统计共用,避免两处上限漂移。 */
|
|
28
|
+
const FULL_SCAN_LIMIT = 100000;
|
|
27
29
|
/** 倒序读取梦境(新梦在前);损坏行跳过。 */
|
|
28
30
|
export function readDreams(journalDir, limit) {
|
|
29
31
|
const file = journalFile(journalDir);
|
|
@@ -48,32 +50,40 @@ export function readDreams(journalDir, limit) {
|
|
|
48
50
|
}
|
|
49
51
|
return out.reverse().slice(0, limit);
|
|
50
52
|
}
|
|
51
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* 统计梦境(基于全量日记)。
|
|
55
|
+
*
|
|
56
|
+
* count 与 lastAt 来自同一遍倒序扫描:readDreams 已经新梦在前,所以某个
|
|
57
|
+
* 教训第一次出现的那条梦,就是它最近一次出现;顺手在那次循环里记下原始
|
|
58
|
+
* 大小写,不再二次 flatMap(否则同一条教训的三个数值会来自不同的扫描)。
|
|
59
|
+
*/
|
|
52
60
|
export function dreamStats(journalDir) {
|
|
53
|
-
const dreams = readDreams(journalDir,
|
|
61
|
+
const dreams = readDreams(journalDir, FULL_SCAN_LIMIT);
|
|
54
62
|
const moods = {};
|
|
55
|
-
const
|
|
63
|
+
const lessons = new Map();
|
|
56
64
|
for (const dream of dreams) {
|
|
57
65
|
const mood = dream.mood !== '' ? dream.mood : '平静';
|
|
58
66
|
moods[mood] = (moods[mood] ?? 0) + 1;
|
|
59
67
|
for (const lesson of dream.lessons) {
|
|
60
68
|
const key = lesson.toLowerCase();
|
|
61
|
-
|
|
69
|
+
const known = lessons.get(key);
|
|
70
|
+
if (known === undefined) {
|
|
71
|
+
lessons.set(key, { lesson, count: 1, lastAt: dream.at });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
known.count += 1;
|
|
75
|
+
}
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
|
-
const topLessons = [...
|
|
65
|
-
.sort((a, b) => b
|
|
66
|
-
.slice(0, 10)
|
|
67
|
-
.map(([key, count]) => {
|
|
68
|
-
const original = dreams.flatMap((d) => d.lessons).find((l) => l.toLowerCase() === key) ?? key;
|
|
69
|
-
return { lesson: original, count };
|
|
70
|
-
});
|
|
78
|
+
const topLessons = [...lessons.values()]
|
|
79
|
+
.sort((a, b) => b.count - a.count || b.lastAt.localeCompare(a.lastAt))
|
|
80
|
+
.slice(0, 10);
|
|
71
81
|
return { total: dreams.length, moods, topLessons };
|
|
72
82
|
}
|
|
73
|
-
/** 关键词检索梦境(不区分大小写,命中 reflection/lessons)。 */
|
|
83
|
+
/** 关键词检索梦境(不区分大小写,命中 reflection/lessons;扫描全量,上限见 FULL_SCAN_LIMIT)。 */
|
|
74
84
|
export function searchDreams(journalDir, query, limit) {
|
|
75
85
|
const needle = query.toLowerCase();
|
|
76
|
-
return readDreams(journalDir,
|
|
86
|
+
return readDreams(journalDir, FULL_SCAN_LIMIT).filter((entry) => {
|
|
77
87
|
const haystack = (entry.reflection + ' ' + entry.lessons.join(' ')).toLowerCase();
|
|
78
88
|
return haystack.includes(needle);
|
|
79
89
|
}).slice(0, limit);
|
package/lib/web.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-dream 的网页端后端:给设置页里的梦境面板提供只读数据。
|
|
3
|
+
*
|
|
4
|
+
* 安全模型沿用 dsh-calendar / dsh-email 的设置路由:这个路由能读出用户的
|
|
5
|
+
* 全部梦境日记(都是隐私文本),而宿主 webserver 有可能绑定在 0.0.0.0。
|
|
6
|
+
* 三道门缺一不可 ——
|
|
7
|
+
* 1. remoteAddress 若是明确的非回环地址则拒绝(局域网不可达);
|
|
8
|
+
* 2. Host 头必须是 localhost 名(挡 DNS rebinding:恶意域名解析到 127.0.0.1);
|
|
9
|
+
* 3. 只接受 GET:本路由没有任何写路径,所以不需要 CSRF 那一套。
|
|
10
|
+
*
|
|
11
|
+
* 这一层不做自己的状态:每次请求都从 journal.ts 现读现算,磁盘上的 JSONL
|
|
12
|
+
* 就是唯一真相。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-dream/web
|
|
15
|
+
*/
|
|
16
|
+
import { type ResolvedDreamConfig } from './config.js';
|
|
17
|
+
import { type DreamEntry, type DreamStats } from './journal.js';
|
|
18
|
+
/** 面板与浏览器说话的同源路由。 */
|
|
19
|
+
export declare const DREAM_ROUTE = "/_dsh/dsh-dream/journal";
|
|
20
|
+
/** ?limit 的默认值与上下界。 */
|
|
21
|
+
export declare const DREAM_LIMIT_DEFAULT = 50;
|
|
22
|
+
export declare const DREAM_LIMIT_MIN = 1;
|
|
23
|
+
export declare const DREAM_LIMIT_MAX = 500;
|
|
24
|
+
/** Host 头裁决:undefined = 放行,否则给出拒绝原因。 */
|
|
25
|
+
export declare function hostVerdict(host: unknown): string | undefined;
|
|
26
|
+
/** 来源地址裁决:undefined = 放行(请求里缺失也放行),否则给出拒绝原因。 */
|
|
27
|
+
export declare function remoteVerdict(remote: unknown): string | undefined;
|
|
28
|
+
/** GET 返回的 JSON 形状(T2 面板按这份契约渲染)。 */
|
|
29
|
+
export interface DreamWebPayload {
|
|
30
|
+
/** 倒序的梦境(新梦在前),受 ?q 与 ?limit 影响。 */
|
|
31
|
+
dreams: DreamEntry[];
|
|
32
|
+
/** 全量日记的统计,不受 ?q 影响。 */
|
|
33
|
+
stats: DreamStats;
|
|
34
|
+
/** 本次生效的检索词(已去首尾空白;空串 = 不检索)。 */
|
|
35
|
+
query: string;
|
|
36
|
+
/** 本次生效的条数上限。 */
|
|
37
|
+
limit: number;
|
|
38
|
+
}
|
|
39
|
+
/** 安装参数;journalDir 缺省时从 config 解析。 */
|
|
40
|
+
export interface DreamWebOptions {
|
|
41
|
+
/** 梦境日记目录(优先)。 */
|
|
42
|
+
journalDir?: string;
|
|
43
|
+
/** 插件配置,用来兜底 journalDir(resolveConfig 的入参)。 */
|
|
44
|
+
config?: Record<string, unknown> | ResolvedDreamConfig | null;
|
|
45
|
+
}
|
|
46
|
+
/** ?limit 解析:缺省/非数字 → fallback;越界钳制到 [1, 500]。 */
|
|
47
|
+
export declare function limitFromQuery(raw: unknown, fallback?: number): number;
|
|
48
|
+
/** 组装路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
|
|
49
|
+
export declare function createDreamWebHandler(options?: DreamWebOptions): (req: any, res: any) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* 把梦境面板挂到宿主 webserver 上。
|
|
52
|
+
* 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由。
|
|
53
|
+
*/
|
|
54
|
+
export declare function installDreamWeb(ctx: any, options?: DreamWebOptions): void;
|
package/lib/web.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-dream 的网页端后端:给设置页里的梦境面板提供只读数据。
|
|
3
|
+
*
|
|
4
|
+
* 安全模型沿用 dsh-calendar / dsh-email 的设置路由:这个路由能读出用户的
|
|
5
|
+
* 全部梦境日记(都是隐私文本),而宿主 webserver 有可能绑定在 0.0.0.0。
|
|
6
|
+
* 三道门缺一不可 ——
|
|
7
|
+
* 1. remoteAddress 若是明确的非回环地址则拒绝(局域网不可达);
|
|
8
|
+
* 2. Host 头必须是 localhost 名(挡 DNS rebinding:恶意域名解析到 127.0.0.1);
|
|
9
|
+
* 3. 只接受 GET:本路由没有任何写路径,所以不需要 CSRF 那一套。
|
|
10
|
+
*
|
|
11
|
+
* 这一层不做自己的状态:每次请求都从 journal.ts 现读现算,磁盘上的 JSONL
|
|
12
|
+
* 就是唯一真相。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-dream/web
|
|
15
|
+
*/
|
|
16
|
+
import { resolveConfig } from './config.js';
|
|
17
|
+
import { dreamStats, readDreams, searchDreams } from './journal.js';
|
|
18
|
+
/** 面板与浏览器说话的同源路由。 */
|
|
19
|
+
export const DREAM_ROUTE = '/_dsh/dsh-dream/journal';
|
|
20
|
+
/** ?limit 的默认值与上下界。 */
|
|
21
|
+
export const DREAM_LIMIT_DEFAULT = 50;
|
|
22
|
+
export const DREAM_LIMIT_MIN = 1;
|
|
23
|
+
export const DREAM_LIMIT_MAX = 500;
|
|
24
|
+
/** 回环主机名白名单;带端口会被拆掉,IPv6 字面量保留方括号。 */
|
|
25
|
+
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
26
|
+
/** 回环来源地址白名单。 */
|
|
27
|
+
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
28
|
+
/** Host 头裁决:undefined = 放行,否则给出拒绝原因。 */
|
|
29
|
+
export function hostVerdict(host) {
|
|
30
|
+
if (typeof host !== 'string' || host.trim() === '')
|
|
31
|
+
return undefined;
|
|
32
|
+
const text = host.trim().toLowerCase();
|
|
33
|
+
const name = text.startsWith('[') ? text.slice(0, text.indexOf(']') + 1) : text.split(':')[0];
|
|
34
|
+
return LOCAL_HOSTNAMES.has(text) || LOCAL_HOSTNAMES.has(name) ? undefined : `host "${host}" is not a localhost name`;
|
|
35
|
+
}
|
|
36
|
+
/** 来源地址裁决:undefined = 放行(请求里缺失也放行),否则给出拒绝原因。 */
|
|
37
|
+
export function remoteVerdict(remote) {
|
|
38
|
+
if (typeof remote !== 'string' || remote.trim() === '')
|
|
39
|
+
return undefined;
|
|
40
|
+
const text = remote.trim().toLowerCase();
|
|
41
|
+
return LOOPBACK_ADDRESSES.has(text) ? undefined : `remote address "${remote}" is not a loopback address`;
|
|
42
|
+
}
|
|
43
|
+
/** ?limit 解析:缺省/非数字 → fallback;越界钳制到 [1, 500]。 */
|
|
44
|
+
export function limitFromQuery(raw, fallback = DREAM_LIMIT_DEFAULT) {
|
|
45
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
46
|
+
return fallback;
|
|
47
|
+
const value = Number(raw);
|
|
48
|
+
if (!Number.isFinite(value))
|
|
49
|
+
return fallback;
|
|
50
|
+
return Math.min(DREAM_LIMIT_MAX, Math.max(DREAM_LIMIT_MIN, Math.trunc(value)));
|
|
51
|
+
}
|
|
52
|
+
/** 解析后的 journalDir:显式路径优先,否则回退到 resolveConfig(config)。 */
|
|
53
|
+
function journalDirOf(options) {
|
|
54
|
+
if (typeof options.journalDir === 'string' && options.journalDir.trim() !== '')
|
|
55
|
+
return options.journalDir;
|
|
56
|
+
return resolveConfig((options.config ?? null)).journalDir;
|
|
57
|
+
}
|
|
58
|
+
/** 解析请求 URL;非法或缺失时退回路由本身(等价于没有查询参数)。 */
|
|
59
|
+
function requestUrl(raw) {
|
|
60
|
+
const text = typeof raw === 'string' && raw !== '' ? raw : DREAM_ROUTE;
|
|
61
|
+
try {
|
|
62
|
+
return new URL(text, 'http://localhost');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return new URL(DREAM_ROUTE, 'http://localhost');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 统一回 JSON;403/405/500 都用同一套 { ok:false, error:{ code, message } }。 */
|
|
69
|
+
function responseJson(res, status, payload, headers = {}) {
|
|
70
|
+
const text = JSON.stringify(payload);
|
|
71
|
+
if (typeof res?.writeHead === 'function') {
|
|
72
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers });
|
|
73
|
+
}
|
|
74
|
+
if (typeof res?.end === 'function')
|
|
75
|
+
res.end(text);
|
|
76
|
+
}
|
|
77
|
+
function forbidden(res, message) {
|
|
78
|
+
responseJson(res, 403, { ok: false, error: { code: 'forbidden', message } });
|
|
79
|
+
}
|
|
80
|
+
/** 组装路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
|
|
81
|
+
export function createDreamWebHandler(options = {}) {
|
|
82
|
+
const journalDir = journalDirOf(options);
|
|
83
|
+
return async (req, res) => {
|
|
84
|
+
const remote = remoteVerdict(req?.socket?.remoteAddress);
|
|
85
|
+
if (remote !== undefined) {
|
|
86
|
+
forbidden(res, remote);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const headers = (req?.headers ?? {});
|
|
90
|
+
const host = hostVerdict(headers.host);
|
|
91
|
+
if (host !== undefined) {
|
|
92
|
+
forbidden(res, host);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// 缺失 method 的请求在宿主内部调用里出现过,按只读的 GET 处理。
|
|
96
|
+
if (String(req?.method ?? 'GET').toUpperCase() !== 'GET') {
|
|
97
|
+
responseJson(res, 405, {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: { code: 'method-not-allowed', message: 'dsh-dream journal route is read-only; use GET' },
|
|
100
|
+
}, { allow: 'GET' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const url = requestUrl(req?.url);
|
|
104
|
+
const query = (url.searchParams.get('q') ?? '').trim();
|
|
105
|
+
const limit = limitFromQuery(url.searchParams.get('limit'));
|
|
106
|
+
try {
|
|
107
|
+
const dreams = query === '' ? readDreams(journalDir, limit) : searchDreams(journalDir, query, limit);
|
|
108
|
+
const stats = dreamStats(journalDir);
|
|
109
|
+
responseJson(res, 200, { dreams, stats, query, limit });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
responseJson(res, 500, {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: { code: 'internal', message: error instanceof Error ? error.message : String(error) },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 把梦境面板挂到宿主 webserver 上。
|
|
121
|
+
* 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由。
|
|
122
|
+
*/
|
|
123
|
+
export function installDreamWeb(ctx, options = {}) {
|
|
124
|
+
if (ctx === null || ctx === undefined || typeof ctx.inject !== 'function') {
|
|
125
|
+
throw new Error('dsh-dream: installDreamWeb 需要带 ctx.inject 的宿主上下文(宿主应提供 webServer 服务)');
|
|
126
|
+
}
|
|
127
|
+
const handler = createDreamWebHandler(options);
|
|
128
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
129
|
+
webCtx.effect(() => {
|
|
130
|
+
return webCtx.webServer.register({
|
|
131
|
+
kind: 'exact',
|
|
132
|
+
path: DREAM_ROUTE,
|
|
133
|
+
handler: (req, res) => handler(req, res),
|
|
134
|
+
}, 'dsh-dream: dream journal route');
|
|
135
|
+
}, 'dsh-dream: web routes');
|
|
136
|
+
});
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardustlc/dsh-dream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DSH 做梦插件:会话间隙回放近期对话、反思写入梦境日记,并把高频教训幂等桥接进 AGENTS.md;默认隐私脱敏,零运行时依赖。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
13
14
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
"skills",
|
|
19
20
|
"cordis.patch.yml",
|
|
20
21
|
"README.md",
|
|
21
|
-
"README.en.md"
|
|
22
|
+
"README.en.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
22
24
|
],
|
|
23
25
|
"scripts": {
|
|
24
26
|
"build": "tsc -p tsconfig.json",
|
|
@@ -29,6 +31,12 @@
|
|
|
29
31
|
"dsh": {
|
|
30
32
|
"bundle": {
|
|
31
33
|
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
38
|
+
],
|
|
39
|
+
"platform": "web"
|
|
32
40
|
}
|
|
33
41
|
},
|
|
34
42
|
"keywords": [
|
|
@@ -25,6 +25,8 @@ description: 做梦协议:会话开场或长任务收尾时,回放近期会
|
|
|
25
25
|
## 醒来之后
|
|
26
26
|
用户问起过往经验时,先 `dream_recall` 搜梦境日记再回答。
|
|
27
27
|
|
|
28
|
+
用户想看梦的时候,告诉他:DSH 的「设置 → 梦境日记」有只读面板(心境方块、教训榜、时间线、关键词搜索)。
|
|
29
|
+
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
# Dream Protocol (English)
|
|
@@ -48,3 +50,5 @@ You are an agent that dreams. Dreams are not hallucinations — they are memory
|
|
|
48
50
|
|
|
49
51
|
## After waking
|
|
50
52
|
When the user asks about past experience, search the journal with `dream_recall` first, then answer.
|
|
53
|
+
|
|
54
|
+
When the user wants to browse their dreams, point them at the read-only panel under DSH Settings → Dream journal (mood strip, lessons ledger, timeline, keyword search).
|