@stardustlc/dsh-dream 0.4.1 → 0.5.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 +20 -0
- package/README.en.md +30 -3
- package/README.md +33 -3
- package/lib/client.js +331 -11
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/journal.d.ts +15 -0
- package/lib/journal.js +32 -11
- package/lib/knowledge-store.d.ts +172 -0
- package/lib/knowledge-store.js +1453 -0
- package/lib/knowledge.d.ts +179 -0
- package/lib/knowledge.js +378 -0
- package/lib/retrieval.d.ts +98 -0
- package/lib/retrieval.js +515 -0
- package/lib/tools.js +522 -13
- package/lib/web.d.ts +46 -18
- package/lib/web.js +163 -39
- package/package.json +1 -1
- package/skills/dream-protocol/SKILL.md +103 -29
package/lib/client.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
// 仓库里没有 src/client.ts,pnpm run build 也不会重写它。改面板请直接改这里,
|
|
3
3
|
// 改完用 node --check lib/client.js 自检(CI 里也会跑这一步)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// GET /_dsh/dsh-dream/journal
|
|
5
|
+
// 面板做两件事:把梦境日记(dreams.jsonl)画成人能看懂的样子 —— 概览、心境方块、
|
|
6
|
+
// 教训榜、按时间倒序的梦卡片;再只读展示 M1 的技术经验(<journalDir>/knowledge/)。
|
|
7
|
+
// 数据来自插件自己的同源只读路由 GET /_dsh/dsh-dream/journal 与
|
|
8
|
+
// GET /_dsh/dsh-dream/knowledge(由 src/index.ts 挂载);前端不缓存业务数据,刷新即见。
|
|
9
|
+
// 经验区块只展示状态 / 条件 / 范围 / 适用与例外 / 证据摘要 / 最近核验;日志截断或坏行会明确提示,
|
|
10
|
+
// 不展示权重、向量或原始 JSON。
|
|
8
11
|
// 隐私纪律:面板只读,不写文件、不上传;「隐私模式」只做显示层模糊,DOM 仍是明文。
|
|
9
12
|
window.__ModuleLoader__.load({ id: "@stardustlc/dsh-dream", factory: (require) => {
|
|
10
13
|
var module = { exports: {} }; var exports = module.exports;
|
|
@@ -16,6 +19,9 @@ const h = React.createElement;
|
|
|
16
19
|
|
|
17
20
|
// 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
|
|
18
21
|
const ROUTE = "_dsh/dsh-dream/journal";
|
|
22
|
+
const KNOWLEDGE_ROUTE = "_dsh/dsh-dream/knowledge";
|
|
23
|
+
const KNOWLEDGE_LIMIT = 50;
|
|
24
|
+
const KNOWLEDGE_STATES = ["candidate", "usable", "disputed", "stale", "rejected"];
|
|
19
25
|
const DEFAULT_LIMIT = 50;
|
|
20
26
|
const MORE_LIMIT = 500;
|
|
21
27
|
const FETCH_TIMEOUT_MS = 15000;
|
|
@@ -35,9 +41,9 @@ function timestampOf(iso) {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** 路由 URL:按 document.baseURI 解析,兼容反代前缀;环境不齐时退回相对路径。 */
|
|
38
|
-
function
|
|
44
|
+
function routeUrlFor(route, query) {
|
|
39
45
|
const suffix = query === "" || query === undefined || query === null ? "" : "?" + query;
|
|
40
|
-
const relative =
|
|
46
|
+
const relative = route + suffix;
|
|
41
47
|
try {
|
|
42
48
|
let base = null;
|
|
43
49
|
if (typeof document !== "undefined" && document !== null && typeof document.baseURI === "string" && document.baseURI !== "") {
|
|
@@ -52,6 +58,11 @@ function routeUrl(query) {
|
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
/** 梦境日记路由的快捷方式(保持既有调用语义)。 */
|
|
62
|
+
function routeUrl(query) {
|
|
63
|
+
return routeUrlFor(ROUTE, query);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
function errorMessageOf(body, status) {
|
|
56
67
|
if (body !== null && typeof body === "object") {
|
|
57
68
|
if (body.error !== null && typeof body.error === "object" && typeof body.error.message === "string" && body.error.message !== "") return body.error.message;
|
|
@@ -87,6 +98,29 @@ async function fetchJournal(query, limit) {
|
|
|
87
98
|
};
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
/** GET /_dsh/dsh-dream/knowledge:只读契约体 { lessons, evidence, stats },无查询参数。 */
|
|
102
|
+
async function fetchKnowledge() {
|
|
103
|
+
const init = { credentials: "same-origin" };
|
|
104
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") init.signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
105
|
+
const res = await fetch(routeUrlFor(KNOWLEDGE_ROUTE, ""), init);
|
|
106
|
+
let body = null;
|
|
107
|
+
try { body = await res.json(); } catch (error) { body = null; }
|
|
108
|
+
if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
|
|
109
|
+
if (res.ok !== true || body.ok === false) {
|
|
110
|
+
const failure = new Error(errorMessageOf(body, res.status));
|
|
111
|
+
failure.status = res.status;
|
|
112
|
+
failure.code = body.error !== null && typeof body.error === "object" ? body.error.code : undefined;
|
|
113
|
+
throw failure;
|
|
114
|
+
}
|
|
115
|
+
// 契约体就是 { lessons, evidence, stats };万一被包进 { ok, value } 信封也认。
|
|
116
|
+
const data = body.value !== null && typeof body.value === "object" ? body.value : body;
|
|
117
|
+
return {
|
|
118
|
+
lessons: Array.isArray(data.lessons) ? data.lessons : [],
|
|
119
|
+
evidence: Array.isArray(data.evidence) ? data.evidence : [],
|
|
120
|
+
stats: data.stats !== null && typeof data.stats === "object" ? data.stats : null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
function normalizeDreams(value, limit) {
|
|
91
125
|
if (!Array.isArray(value) || limit <= 0) return [];
|
|
92
126
|
const out = [];
|
|
@@ -157,6 +191,150 @@ function normalizeStats(value, dreams) {
|
|
|
157
191
|
return { total: total, moods: moods, topLessons: topLessons };
|
|
158
192
|
}
|
|
159
193
|
|
|
194
|
+
/** 只认契约里的 5 个状态;未知状态不冒充候选。 */
|
|
195
|
+
function normalizeLessonState(value) {
|
|
196
|
+
return KNOWLEDGE_STATES.indexOf(value) !== -1 ? value : "unknown";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 状态 → 中文标签(面板文案集中在这里)。 */
|
|
200
|
+
function lessonStateLabel(state) {
|
|
201
|
+
switch (state) {
|
|
202
|
+
case "candidate": return "候选(未采纳)";
|
|
203
|
+
case "usable": return "可用";
|
|
204
|
+
case "disputed": return "有冲突";
|
|
205
|
+
case "stale": return "待复核";
|
|
206
|
+
case "rejected": return "已驳回";
|
|
207
|
+
default: return "未知状态";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 状态 → 视觉类名:candidate 与 disputed 必须不同色,其余状态各自可辨。 */
|
|
212
|
+
function lessonStateClass(state) {
|
|
213
|
+
switch (state) {
|
|
214
|
+
case "candidate": return "dshd-kstate-candidate";
|
|
215
|
+
case "usable": return "dshd-kstate-usable";
|
|
216
|
+
case "disputed": return "dshd-kstate-disputed";
|
|
217
|
+
case "stale": return "dshd-kstate-stale";
|
|
218
|
+
case "rejected": return "dshd-kstate-rejected";
|
|
219
|
+
default: return "dshd-kstate-unknown";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 只取字符串字段并去首尾空白;坏形状降级为 "",不让面板崩。 */
|
|
224
|
+
function trimmedText(value) {
|
|
225
|
+
return typeof value === "string" ? value.trim() : "";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** applicability 数组 → 紧凑适用串(package + versions / platform);坏形状跳过,空数组给 ""。 */
|
|
229
|
+
function applicabilityTextOf(value) {
|
|
230
|
+
if (!Array.isArray(value)) return "";
|
|
231
|
+
const parts = [];
|
|
232
|
+
for (const raw of value) {
|
|
233
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
234
|
+
const pkg = trimmedText(raw.package);
|
|
235
|
+
const versions = trimmedText(raw.versions);
|
|
236
|
+
const platform = trimmedText(raw.platform);
|
|
237
|
+
const main = pkg !== "" ? (versions !== "" ? pkg + " " + versions : pkg) : versions;
|
|
238
|
+
const bits = [];
|
|
239
|
+
if (main !== "") bits.push(main);
|
|
240
|
+
if (platform !== "") bits.push("平台 " + platform);
|
|
241
|
+
if (bits.length > 0) parts.push(bits.join(" · "));
|
|
242
|
+
}
|
|
243
|
+
if (parts.length === 0) return "";
|
|
244
|
+
if (parts.length > 4) return parts.slice(0, 4).join(";") + ";等 " + String(parts.length) + " 条适用范围";
|
|
245
|
+
return parts.join(";");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 经验视图模型:只投影展示所需字段(title / when / applicability / exceptions /
|
|
250
|
+
* state / scopeLabel / evidenceSummary / lastValidatedAt),不碰权重、向量、原始 JSON。
|
|
251
|
+
* limit 只截断渲染行数,byState 仍按全量统计;truncated / badLines 原样带给提示行。
|
|
252
|
+
*/
|
|
253
|
+
function normalizeKnowledge(value, now, limit) {
|
|
254
|
+
const source = value !== null && typeof value === "object" ? value : {};
|
|
255
|
+
const cap = typeof limit === "number" && limit > 0 ? limit : KNOWLEDGE_LIMIT;
|
|
256
|
+
const rawLessons = Array.isArray(source.lessons) ? source.lessons : [];
|
|
257
|
+
const statsSource = source.stats !== null && typeof source.stats === "object" ? source.stats : {};
|
|
258
|
+
const byState = {};
|
|
259
|
+
const rows = [];
|
|
260
|
+
for (const raw of rawLessons) {
|
|
261
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
262
|
+
const title = trimmedText(raw.title);
|
|
263
|
+
const action = trimmedText(raw.action);
|
|
264
|
+
if (title === "" && action === "") continue;
|
|
265
|
+
const state = normalizeLessonState(raw.state);
|
|
266
|
+
byState[state] = (byState[state] || 0) + 1;
|
|
267
|
+
if (rows.length >= cap) continue;
|
|
268
|
+
const lastValidatedAt = trimmedText(raw.lastValidatedAt);
|
|
269
|
+
rows.push({
|
|
270
|
+
id: trimmedText(raw.id),
|
|
271
|
+
title: title !== "" ? title : action,
|
|
272
|
+
when: trimmedText(raw.when),
|
|
273
|
+
applicability: applicabilityTextOf(raw.applicability),
|
|
274
|
+
exceptions: Array.isArray(raw.exceptions)
|
|
275
|
+
? raw.exceptions.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()).slice(0, 10)
|
|
276
|
+
: [],
|
|
277
|
+
state: state,
|
|
278
|
+
stateLabel: lessonStateLabel(state),
|
|
279
|
+
stateClass: lessonStateClass(state),
|
|
280
|
+
scopeLabel: trimmedText(raw.scopeLabel) || "范围未标注",
|
|
281
|
+
evidenceSummary: trimmedText(raw.evidenceSummary) || "暂无可展示的证据摘要",
|
|
282
|
+
lastValidatedAt: lastValidatedAt,
|
|
283
|
+
lastText: lastValidatedAt === "" ? "尚未核验" : relativeTime(lastValidatedAt, now),
|
|
284
|
+
lastTitle: lastValidatedAt === "" ? "没有 lastValidatedAt(从未核验)" : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
|
|
285
|
+
candidate: state === "candidate",
|
|
286
|
+
disputed: state === "disputed",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const total = typeof statsSource.lessons === "number" && statsSource.lessons >= 0 ? statsSource.lessons : rawLessons.length;
|
|
290
|
+
const evidenceTotal = typeof statsSource.evidence === "number" && statsSource.evidence >= 0 ? statsSource.evidence : 0;
|
|
291
|
+
const badLinesRaw = statsSource.badLines;
|
|
292
|
+
const badLines = typeof badLinesRaw === "number" && Number.isFinite(badLinesRaw) && badLinesRaw > 0 ? Math.floor(badLinesRaw) : 0;
|
|
293
|
+
return {
|
|
294
|
+
rows: rows,
|
|
295
|
+
total: total,
|
|
296
|
+
shown: rows.length,
|
|
297
|
+
stats: { lessons: total, evidence: evidenceTotal, byState: byState, truncated: statsSource.truncated === true, badLines: badLines },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 经验区块的统计文案:按状态计数,不包装成可靠度。 */
|
|
302
|
+
function knowledgeCountText(stats) {
|
|
303
|
+
const source = stats !== null && typeof stats === "object" ? stats : {};
|
|
304
|
+
const byState = source.byState !== null && typeof source.byState === "object" ? source.byState : {};
|
|
305
|
+
const parts = [];
|
|
306
|
+
for (const state of KNOWLEDGE_STATES) {
|
|
307
|
+
const count = typeof byState[state] === "number" && byState[state] > 0 ? byState[state] : 0;
|
|
308
|
+
if (count > 0) parts.push(lessonStateLabel(state) + " " + String(count));
|
|
309
|
+
}
|
|
310
|
+
if (parts.length === 0) {
|
|
311
|
+
const total = typeof source.lessons === "number" && source.lessons > 0 ? source.lessons : 0;
|
|
312
|
+
return total > 0 ? "共 " + String(total) + " 条" : "还没有经验";
|
|
313
|
+
}
|
|
314
|
+
return parts.join(" · ");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 截断 / 坏行提示:无问题时给 "";确保面板不静默展示不完整的经验集。 */
|
|
318
|
+
function knowledgeNoticeText(stats) {
|
|
319
|
+
const source = stats !== null && typeof stats === "object" ? stats : {};
|
|
320
|
+
const truncated = source.truncated === true;
|
|
321
|
+
const badLines = typeof source.badLines === "number" && Number.isFinite(source.badLines) && source.badLines > 0 ? Math.floor(source.badLines) : 0;
|
|
322
|
+
if (truncated !== true && badLines === 0) return "";
|
|
323
|
+
const parts = [];
|
|
324
|
+
if (truncated === true) parts.push("日志过大,仅回放最近一段事件");
|
|
325
|
+
if (badLines > 0) parts.push("有 " + String(badLines) + " 行损坏已跳过");
|
|
326
|
+
return parts.join(";") + ";events.jsonl 未改动。";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** 经验区块的独立空态:与「还没有梦境日记」分开,说明经验从哪来。 */
|
|
330
|
+
function knowledgeEmptyState() {
|
|
331
|
+
return {
|
|
332
|
+
title: "还没有经验记录",
|
|
333
|
+
body: "经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。",
|
|
334
|
+
hint: "旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
160
338
|
function spanOf(dreams) {
|
|
161
339
|
let min = null;
|
|
162
340
|
let max = null;
|
|
@@ -483,6 +661,78 @@ function LessonCard(props) {
|
|
|
483
661
|
]);
|
|
484
662
|
}
|
|
485
663
|
|
|
664
|
+
function KnowledgeRow(props) {
|
|
665
|
+
const row = props.row;
|
|
666
|
+
const privacy = props.privacy === true;
|
|
667
|
+
const [peek, setPeek] = useState(false);
|
|
668
|
+
const reveal = privacy === true ? function () { setPeek(peek !== true); } : undefined;
|
|
669
|
+
return h("div", {
|
|
670
|
+
className: "dshd-knowledge " + row.stateClass + (privacy === true && peek === true ? " dshd-peek" : ""),
|
|
671
|
+
onClick: reveal,
|
|
672
|
+
title: privacy === true ? "隐私模式:点击此条临时查看" : undefined,
|
|
673
|
+
}, [
|
|
674
|
+
h("div", { className: "dshd-khead", key: "h" }, [
|
|
675
|
+
h("span", { className: "dshd-kbadge", key: "b" }, row.stateLabel),
|
|
676
|
+
h("span", { className: "dshd-ktitle", key: "t", title: row.title }, row.title),
|
|
677
|
+
]),
|
|
678
|
+
h("div", { className: "dshd-kmeta", key: "m" }, [
|
|
679
|
+
h("span", { className: "dshd-kscope", key: "s", title: "适用范围" }, row.scopeLabel),
|
|
680
|
+
h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, "最近核验:" + row.lastText),
|
|
681
|
+
]),
|
|
682
|
+
h("div", { className: "dshd-kbody", key: "body" }, [
|
|
683
|
+
h("div", { key: "when" }, "何时用:" + (row.when !== "" ? row.when : "未标注适用条件")),
|
|
684
|
+
row.applicability !== "" ? h("div", { key: "applicability", title: row.applicability }, "适用:" + row.applicability) : null,
|
|
685
|
+
row.exceptions.length > 0 ? h("div", { key: "exceptions", title: row.exceptions.join(";") }, "例外:" + row.exceptions.join(";")) : null,
|
|
686
|
+
h("div", { key: "evidence" }, "证据:" + row.evidenceSummary),
|
|
687
|
+
]),
|
|
688
|
+
]);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function KnowledgeCard(props) {
|
|
692
|
+
const vm = props.vm;
|
|
693
|
+
const notice = knowledgeNoticeText(vm === null || vm === undefined ? null : vm.stats);
|
|
694
|
+
const noticeNode = notice === "" ? null : h("div", { className: "dshd-knotice", key: "notice", role: "status" }, notice);
|
|
695
|
+
const head = function (extra) {
|
|
696
|
+
return h("div", { className: "dshd-khead", key: "head" }, [
|
|
697
|
+
h("h4", { key: "t" }, "经验"),
|
|
698
|
+
h("span", { className: "dshd-spacer", key: "s" }),
|
|
699
|
+
extra,
|
|
700
|
+
]);
|
|
701
|
+
};
|
|
702
|
+
if (props.error !== null && props.error !== undefined) {
|
|
703
|
+
return h("section", { className: "dshd-card dshd-kcard" }, [
|
|
704
|
+
head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
|
|
705
|
+
h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
|
|
706
|
+
h("div", { className: "dshd-note", key: "n" }, "旧日记不受影响;修好后点「重试」。"),
|
|
707
|
+
]);
|
|
708
|
+
}
|
|
709
|
+
if (props.empty === true) {
|
|
710
|
+
const empty = knowledgeEmptyState();
|
|
711
|
+
return h("section", { className: "dshd-card dshd-kcard" }, [
|
|
712
|
+
h("h4", { key: "t" }, "经验"),
|
|
713
|
+
noticeNode,
|
|
714
|
+
h("div", { className: "dshd-kempty", key: "e" }, [
|
|
715
|
+
h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
|
|
716
|
+
h("p", { className: "dshd-kempty-body", key: "b" }, empty.body),
|
|
717
|
+
h("p", { className: "dshd-note", key: "h" }, empty.hint),
|
|
718
|
+
]),
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
return h("section", { className: "dshd-card dshd-kcard" }, [
|
|
722
|
+
head(props.loading === true
|
|
723
|
+
? h("span", { className: "dshd-hint", key: "loading" }, "加载中…")
|
|
724
|
+
: h("span", { className: "dshd-hint", key: "count" }, knowledgeCountText(vm.stats))),
|
|
725
|
+
noticeNode,
|
|
726
|
+
h("div", { className: "dshd-krows", key: "rows" }, vm.rows.map(function (row, index) {
|
|
727
|
+
return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, privacy: props.privacy === true });
|
|
728
|
+
})),
|
|
729
|
+
vm.shown < vm.total
|
|
730
|
+
? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条经验。")
|
|
731
|
+
: null,
|
|
732
|
+
h("div", { className: "dshd-note", key: "note" }, "只读展示:候选可被检索到,但默认不注入任务建议、且排在可用经验之后;未经审阅不得当作已证实结论。采纳、驳回或标记冲突请用 dream_review。"),
|
|
733
|
+
]);
|
|
734
|
+
}
|
|
735
|
+
|
|
486
736
|
function DreamCard(props) {
|
|
487
737
|
const dream = props.dream;
|
|
488
738
|
const privacy = props.privacy === true;
|
|
@@ -561,6 +811,9 @@ function DreamPanel() {
|
|
|
561
811
|
const [filters, setFilters] = useState({ moods: [], withLessons: false });
|
|
562
812
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
563
813
|
const [limit, setLimit] = useState(DEFAULT_LIMIT);
|
|
814
|
+
const [knowledge, setKnowledge] = useState(null);
|
|
815
|
+
const [knowledgeLoading, setKnowledgeLoading] = useState(true);
|
|
816
|
+
const [knowledgeError, setKnowledgeError] = useState(null);
|
|
564
817
|
|
|
565
818
|
// 搜索防抖:连续打字只发一次请求;回车立即生效。
|
|
566
819
|
useEffect(function () {
|
|
@@ -587,7 +840,26 @@ function DreamPanel() {
|
|
|
587
840
|
return function () { alive = false; };
|
|
588
841
|
}, [query, reloadToken]);
|
|
589
842
|
|
|
843
|
+
// 经验区块独立取数:知识路由挂了但日记读不出来时,经验仍能显示(反之亦然)。
|
|
844
|
+
useEffect(function () {
|
|
845
|
+
let alive = true;
|
|
846
|
+
setKnowledgeLoading(true);
|
|
847
|
+
setKnowledgeError(null);
|
|
848
|
+
fetchKnowledge().then(function (payload) {
|
|
849
|
+
if (alive !== true) return;
|
|
850
|
+
setKnowledge(normalizeKnowledge(payload, new Date()));
|
|
851
|
+
setKnowledgeLoading(false);
|
|
852
|
+
}).catch(function (failure) {
|
|
853
|
+
if (alive !== true) return;
|
|
854
|
+
setKnowledgeError(failure instanceof Error ? failure : new Error(String(failure)));
|
|
855
|
+
setKnowledgeLoading(false);
|
|
856
|
+
setKnowledge(null);
|
|
857
|
+
});
|
|
858
|
+
return function () { alive = false; };
|
|
859
|
+
}, [reloadToken]);
|
|
860
|
+
|
|
590
861
|
const vm = viewModelOf(dreams, stats, now);
|
|
862
|
+
const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : knowledge;
|
|
591
863
|
const pristine = vm.count === 0 && query === "";
|
|
592
864
|
const filterCount = filterCountOf(filters);
|
|
593
865
|
const visibleDreams = applyFilters(dreams, filters);
|
|
@@ -612,6 +884,16 @@ function DreamPanel() {
|
|
|
612
884
|
};
|
|
613
885
|
const clearFilters = function () { setFilters({ moods: [], withLessons: false }); };
|
|
614
886
|
const showMore = function () { if (limit < MORE_LIMIT) setLimit(MORE_LIMIT); };
|
|
887
|
+
const showKnowledge = function () { if (knowledgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
|
|
888
|
+
const knowledgeCard = h(KnowledgeCard, {
|
|
889
|
+
key: "knowledge",
|
|
890
|
+
vm: knowledgeVm,
|
|
891
|
+
loading: knowledgeLoading === true,
|
|
892
|
+
error: knowledgeError,
|
|
893
|
+
empty: knowledgeError === null && knowledgeLoading !== true && knowledgeVm.rows.length === 0,
|
|
894
|
+
privacy: privacy === true,
|
|
895
|
+
onRetry: showKnowledge,
|
|
896
|
+
});
|
|
615
897
|
|
|
616
898
|
// 筛选 popover:点面板外任意处收起;纯 node 冒烟没有事件系统就跳过。
|
|
617
899
|
useEffect(function () {
|
|
@@ -705,13 +987,16 @@ function DreamPanel() {
|
|
|
705
987
|
]);
|
|
706
988
|
|
|
707
989
|
const body = function () {
|
|
708
|
-
if (loading === true && vm.count === 0) return h(Skeleton, { key: "skeleton" });
|
|
990
|
+
if (loading === true && vm.count === 0) return h("div", { key: "boot" }, [h(Skeleton, { key: "skeleton" }), knowledgeCard]);
|
|
709
991
|
if (vm.count === 0) {
|
|
710
|
-
if (error !== null) return h("div", {
|
|
711
|
-
h("
|
|
712
|
-
|
|
992
|
+
if (error !== null) return h("div", { key: "failed-wrap" }, [
|
|
993
|
+
h("div", { className: "dshd-empty", key: "failed" }, [
|
|
994
|
+
h("h3", { key: "t" }, "暂时读不到梦境日记"),
|
|
995
|
+
h("p", { key: "p" }, "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。"),
|
|
996
|
+
]),
|
|
997
|
+
knowledgeCard,
|
|
713
998
|
]);
|
|
714
|
-
return h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch });
|
|
999
|
+
return h("div", { key: "empty-wrap" }, [h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch }), knowledgeCard]);
|
|
715
1000
|
}
|
|
716
1001
|
return h("div", { key: "content" }, [
|
|
717
1002
|
vm.showTiles
|
|
@@ -726,6 +1011,7 @@ function DreamPanel() {
|
|
|
726
1011
|
h(MoodCard, { key: "mood", vm: vm }),
|
|
727
1012
|
h(LessonCard, { key: "lessons", vm: vm }),
|
|
728
1013
|
]),
|
|
1014
|
+
knowledgeCard,
|
|
729
1015
|
h("div", { className: "dshd-listhead", key: "listhead" }, [
|
|
730
1016
|
h("h4", { key: "t" }, query === "" && filterCount === 0 ? "梦境时间线" : (query === "" ? "筛选结果" : "搜索结果")),
|
|
731
1017
|
h("span", { className: "dshd-hint", key: "n" }, listHintOf(query, vm.count, filterCount, visibleDreams.length)),
|
|
@@ -857,7 +1143,29 @@ const CSS = [
|
|
|
857
1143
|
".dshd-chip{font-family:ui-monospace,Consolas,monospace;font-size:11px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.05));border-radius:999px;padding:2px 9px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
|
|
858
1144
|
".dshd-skel{height:58px;border-radius:12px;margin-bottom:8px;background:linear-gradient(90deg,var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)) 25%,color-mix(in srgb,var(--dsw-alias-label-primary,#111) 10%,transparent) 37%,var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)) 63%);background-size:400% 100%;animation:dshd-shimmer 1.4s ease infinite}",
|
|
859
1145
|
"@keyframes dshd-shimmer{0%{background-position:100% 0}100%{background-position:0 0}}",
|
|
860
|
-
".dshd-footer{margin-top:12px;display:flex;align-items:center;gap:8px;justify-content:space-between;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;flex-wrap:wrap}"
|
|
1146
|
+
".dshd-footer{margin-top:12px;display:flex;align-items:center;gap:8px;justify-content:space-between;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;flex-wrap:wrap}",
|
|
1147
|
+
".dshd-kcard{margin:2px 0 14px}",
|
|
1148
|
+
".dshd-khead{display:flex;align-items:baseline;gap:8px;margin-bottom:8px}",
|
|
1149
|
+
".dshd-khead h4{margin:0;font-size:13px;font-weight:600}",
|
|
1150
|
+
".dshd-krows{display:flex;flex-direction:column;gap:7px}",
|
|
1151
|
+
".dshd-knowledge{border-left:3px solid var(--dsw-alias-label-tertiary,#6b7280);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:9px;padding:8px 10px;min-width:0}",
|
|
1152
|
+
".dshd-knowledge.dshd-kstate-candidate{border-left-color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
|
|
1153
|
+
".dshd-knowledge.dshd-kstate-disputed{border-left-color:var(--dsw-alias-state-error-primary,#dc2626);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 6%,var(--dsw-alias-bg-layer-1,#fff))}",
|
|
1154
|
+
".dshd-knowledge.dshd-kstate-usable{border-left-color:var(--dsw-alias-state-success-primary,#16a34a)}",
|
|
1155
|
+
".dshd-knowledge.dshd-kstate-stale,.dshd-knowledge.dshd-kstate-rejected,.dshd-knowledge.dshd-kstate-unknown{border-left-color:var(--dsw-alias-label-tertiary,#6b7280);opacity:.86}",
|
|
1156
|
+
".dshd-kbadge{flex:0 0 auto;font-size:11px;padding:1px 7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 8%,transparent);white-space:nowrap}",
|
|
1157
|
+
".dshd-kstate-candidate .dshd-kbadge{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 22%,transparent)}",
|
|
1158
|
+
".dshd-kstate-disputed .dshd-kbadge{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 20%,transparent);color:var(--dsw-alias-state-error-primary,#dc2626)}",
|
|
1159
|
+
".dshd-kstate-usable .dshd-kbadge{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 18%,transparent)}",
|
|
1160
|
+
".dshd-ktitle{font-size:12px;font-weight:600;word-break:break-word}",
|
|
1161
|
+
".dshd-kmeta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:2px}",
|
|
1162
|
+
".dshd-kbody{margin-top:5px;display:flex;flex-direction:column;gap:3px;font-size:12px;word-break:break-word}",
|
|
1163
|
+
".dshd-kempty{padding:14px 10px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280)}",
|
|
1164
|
+
".dshd-kempty-title{color:var(--dsw-alias-label-primary,#111);font-weight:600;font-size:13px;margin-bottom:4px}",
|
|
1165
|
+
".dshd-kempty-body{margin:0 auto 5px;max-width:460px;font-size:12px}",
|
|
1166
|
+
".dshd-knotice{font-size:11px;color:var(--dsw-alias-label-primary,#111);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 14%,transparent);border-radius:8px;padding:5px 8px;margin-bottom:7px}",
|
|
1167
|
+
".dshd-privacy .dshd-kbody{filter:blur(4px)}",
|
|
1168
|
+
".dshd-privacy .dshd-knowledge.dshd-peek .dshd-kbody{filter:none}"
|
|
861
1169
|
].join("");
|
|
862
1170
|
/* @ui-css-end */
|
|
863
1171
|
|
|
@@ -897,6 +1205,18 @@ exports.inject = inject;
|
|
|
897
1205
|
// 纯函数内部件:验证脚本可直接取来测,不必另抄一份算法。
|
|
898
1206
|
exports.__internals = {
|
|
899
1207
|
routeUrl: routeUrl,
|
|
1208
|
+
routeUrlFor: routeUrlFor,
|
|
1209
|
+
KNOWLEDGE_ROUTE: KNOWLEDGE_ROUTE,
|
|
1210
|
+
KNOWLEDGE_LIMIT: KNOWLEDGE_LIMIT,
|
|
1211
|
+
fetchKnowledge: fetchKnowledge,
|
|
1212
|
+
normalizeKnowledge: normalizeKnowledge,
|
|
1213
|
+
normalizeLessonState: normalizeLessonState,
|
|
1214
|
+
applicabilityTextOf: applicabilityTextOf,
|
|
1215
|
+
knowledgeNoticeText: knowledgeNoticeText,
|
|
1216
|
+
lessonStateLabel: lessonStateLabel,
|
|
1217
|
+
lessonStateClass: lessonStateClass,
|
|
1218
|
+
knowledgeCountText: knowledgeCountText,
|
|
1219
|
+
knowledgeEmptyState: knowledgeEmptyState,
|
|
900
1220
|
fetchJournal: fetchJournal,
|
|
901
1221
|
normalizeDreams: normalizeDreams,
|
|
902
1222
|
normalizeStats: normalizeStats,
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/journal.d.ts
CHANGED
|
@@ -9,6 +9,19 @@ export interface DreamEntry {
|
|
|
9
9
|
export declare function journalFile(journalDir: string): string;
|
|
10
10
|
/** 追加一条梦;返回落盘后的条目。 */
|
|
11
11
|
export declare function saveDream(journalDir: string, reflection: string, lessons: string[], mood: string): DreamEntry;
|
|
12
|
+
/** 读取结果:梦境 + 被跳过的行数(坏 JSON 或不是梦境记录)。 */
|
|
13
|
+
export interface DreamReadResult {
|
|
14
|
+
dreams: DreamEntry[];
|
|
15
|
+
skippedLines: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 倒序读取梦境并**报告**跳过了多少行。
|
|
19
|
+
*
|
|
20
|
+
* 两个健壮性要求(评审实测发现):
|
|
21
|
+
* - 文件开头的 UTF-8 BOM 必须剥离,否则第一行解析失败会让整份日记读空;
|
|
22
|
+
* - 坏行跳过要可计数,不能静默——调用方(dream_health / 只读路由)可以透出。
|
|
23
|
+
*/
|
|
24
|
+
export declare function readDreamsDetailed(journalDir: string, limit: number): DreamReadResult;
|
|
12
25
|
/** 倒序读取梦境(新梦在前);损坏行跳过。 */
|
|
13
26
|
export declare function readDreams(journalDir: string, limit: number): DreamEntry[];
|
|
14
27
|
/** 教训榜的一行:次数与最近一次出现时间(lastAt 缺失时是空串,绝不为 null)。 */
|
|
@@ -22,6 +35,8 @@ export interface DreamStats {
|
|
|
22
35
|
total: number;
|
|
23
36
|
moods: Record<string, number>;
|
|
24
37
|
topLessons: LessonStat[];
|
|
38
|
+
/** 解析时跳过的行数(坏 JSON 或不是梦境记录);0 表示全部可读。 */
|
|
39
|
+
skippedLines: number;
|
|
25
40
|
}
|
|
26
41
|
/**
|
|
27
42
|
* 统计梦境(基于全量日记)。
|
package/lib/journal.js
CHANGED
|
@@ -26,18 +26,27 @@ export function saveDream(journalDir, reflection, lessons, mood) {
|
|
|
26
26
|
}
|
|
27
27
|
/** 需要读全量日记时的上限:检索与统计共用,避免两处上限漂移。 */
|
|
28
28
|
const FULL_SCAN_LIMIT = 100000;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 倒序读取梦境并**报告**跳过了多少行。
|
|
31
|
+
*
|
|
32
|
+
* 两个健壮性要求(评审实测发现):
|
|
33
|
+
* - 文件开头的 UTF-8 BOM 必须剥离,否则第一行解析失败会让整份日记读空;
|
|
34
|
+
* - 坏行跳过要可计数,不能静默——调用方(dream_health / 只读路由)可以透出。
|
|
35
|
+
*/
|
|
36
|
+
export function readDreamsDetailed(journalDir, limit) {
|
|
31
37
|
const file = journalFile(journalDir);
|
|
32
38
|
if (!existsSync(file))
|
|
33
|
-
return [];
|
|
34
|
-
|
|
35
|
-
const
|
|
39
|
+
return { dreams: [], skippedLines: 0 };
|
|
40
|
+
// 开头 BOM 不参与解析(Windows 记事本等工具会写入)。
|
|
41
|
+
const text = readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
|
|
42
|
+
const lines = text.split('\n').filter((line) => line.trim() !== '');
|
|
43
|
+
const dreams = [];
|
|
44
|
+
let skippedLines = 0;
|
|
36
45
|
for (const line of lines) {
|
|
37
46
|
try {
|
|
38
47
|
const rec = JSON.parse(line);
|
|
39
48
|
if (typeof rec.reflection === 'string') {
|
|
40
|
-
|
|
49
|
+
dreams.push({
|
|
41
50
|
id: String(rec.id ?? ''),
|
|
42
51
|
at: String(rec.at ?? ''),
|
|
43
52
|
reflection: rec.reflection,
|
|
@@ -45,10 +54,18 @@ export function readDreams(journalDir, limit) {
|
|
|
45
54
|
mood: typeof rec.mood === 'string' ? rec.mood : '',
|
|
46
55
|
});
|
|
47
56
|
}
|
|
57
|
+
else
|
|
58
|
+
skippedLines += 1;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
skippedLines += 1;
|
|
48
62
|
}
|
|
49
|
-
catch { /* 损坏行跳过 */ }
|
|
50
63
|
}
|
|
51
|
-
return
|
|
64
|
+
return { dreams: dreams.reverse().slice(0, limit), skippedLines };
|
|
65
|
+
}
|
|
66
|
+
/** 倒序读取梦境(新梦在前);损坏行跳过。 */
|
|
67
|
+
export function readDreams(journalDir, limit) {
|
|
68
|
+
return readDreamsDetailed(journalDir, limit).dreams;
|
|
52
69
|
}
|
|
53
70
|
/**
|
|
54
71
|
* 统计梦境(基于全量日记)。
|
|
@@ -58,8 +75,10 @@ export function readDreams(journalDir, limit) {
|
|
|
58
75
|
* 大小写,不再二次 flatMap(否则同一条教训的三个数值会来自不同的扫描)。
|
|
59
76
|
*/
|
|
60
77
|
export function dreamStats(journalDir) {
|
|
61
|
-
const dreams =
|
|
62
|
-
|
|
78
|
+
const { dreams, skippedLines } = readDreamsDetailed(journalDir, FULL_SCAN_LIMIT);
|
|
79
|
+
// 无原型对象:心境是用户可控文本,'__proto__' / 'constructor' 这类键
|
|
80
|
+
// 在普通对象上会被原型吞掉或把计数变成字符串(评审实测)。
|
|
81
|
+
const moods = Object.create(null);
|
|
63
82
|
const lessons = new Map();
|
|
64
83
|
for (const dream of dreams) {
|
|
65
84
|
const mood = dream.mood !== '' ? dream.mood : '平静';
|
|
@@ -78,7 +97,9 @@ export function dreamStats(journalDir) {
|
|
|
78
97
|
const topLessons = [...lessons.values()]
|
|
79
98
|
.sort((a, b) => b.count - a.count || b.lastAt.localeCompare(a.lastAt))
|
|
80
99
|
.slice(0, 10);
|
|
81
|
-
|
|
100
|
+
// 对外给普通对象副本:内部用无原型对象防键污染,但消费方(deepStrictEqual / 原型判断)
|
|
101
|
+
// 期望的是普通对象。展开赋值走 CreateDataProperty,不会触发 __proto__ setter。
|
|
102
|
+
return { total: dreams.length, moods: { ...moods }, topLessons, skippedLines };
|
|
82
103
|
}
|
|
83
104
|
/** 关键词检索梦境(不区分大小写,命中 reflection/lessons;扫描全量,上限见 FULL_SCAN_LIMIT)。 */
|
|
84
105
|
export function searchDreams(journalDir, query, limit) {
|