@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/lib/client.js CHANGED
@@ -2,12 +2,14 @@
2
2
  // 仓库里没有 src/client.ts,pnpm run build 也不会重写它。改面板请直接改这里,
3
3
  // 改完用 node --check lib/client.js 自检(CI 里也会跑这一步)。
4
4
  //
5
- // 面板做两件事:把梦境日记(dreams.jsonl)画成人能看懂的样子 —— 概览、心境方块、
6
- // 教训榜、按时间倒序的梦卡片;再只读展示 M1 的技术经验(<journalDir>/knowledge/)。
7
- // 数据来自插件自己的同源只读路由 GET /_dsh/dsh-dream/journal 与
8
- // GET /_dsh/dsh-dream/knowledge(由 src/index.ts 挂载);前端不缓存业务数据,刷新即见。
5
+ // 面板做四件事:把梦境日记(dreams.jsonl)画成人能看懂的样子 —— 概览、心境方块、
6
+ // 教训榜、按时间倒序的梦卡片;展示与审阅技术经验(<journalDir>/knowledge/);
7
+ // 展示「待核实」(candidate/disputed/stale + 被扣下的原因)与「写入记录」
8
+ //(<journalDir>/bridge/ 的桥接应用记录)。
9
+ // 数据来自插件自己的同源只读路由 GET /_dsh/dsh-dream/journal、/_dsh/dsh-dream/knowledge
10
+ // 与 /_dsh/dsh-dream/bridge(由 src/index.ts 挂载);前端不缓存业务数据,刷新即见。
9
11
  // 经验区块只展示状态 / 条件 / 范围 / 适用与例外 / 证据摘要 / 最近核验;日志截断或坏行会明确提示,
10
- // 不展示权重、向量或原始 JSON。
12
+ // 不展示权重、向量或原始 JSON。经验行只提供「复制审阅 / 预览命令」,写操作仍在对话工具里执行。
11
13
  // 隐私纪律:面板只读,不写文件、不上传;「隐私模式」只做显示层模糊,DOM 仍是明文。
12
14
  window.__ModuleLoader__.load({ id: "@stardustlc/dsh-dream", factory: (require) => {
13
15
  var module = { exports: {} }; var exports = module.exports;
@@ -19,9 +21,13 @@ const h = React.createElement;
19
21
 
20
22
  // 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
21
23
  const ROUTE = "_dsh/dsh-dream/journal";
24
+ const ACTION_ROUTE = "api/dsh-dream/actions";
22
25
  const KNOWLEDGE_ROUTE = "_dsh/dsh-dream/knowledge";
26
+ const BRIDGE_ROUTE = "_dsh/dsh-dream/bridge";
23
27
  const KNOWLEDGE_LIMIT = 50;
28
+ const BRIDGE_LIMIT = 50;
24
29
  const KNOWLEDGE_STATES = ["candidate", "usable", "disputed", "stale", "rejected"];
30
+ const HELD_STATES = ["candidate", "disputed", "stale"];
25
31
  const DEFAULT_LIMIT = 50;
26
32
  const MORE_LIMIT = 500;
27
33
  const FETCH_TIMEOUT_MS = 15000;
@@ -121,6 +127,58 @@ async function fetchKnowledge() {
121
127
  };
122
128
  }
123
129
 
130
+ /** GET /_dsh/dsh-dream/bridge:只读契约体 { records, stats },无查询参数。 */
131
+ async function fetchBridgeRecords() {
132
+ const init = { credentials: "same-origin" };
133
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") init.signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
134
+ const res = await fetch(routeUrlFor(BRIDGE_ROUTE, ""), init);
135
+ let body = null;
136
+ try { body = await res.json(); } catch (error) { body = null; }
137
+ if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
138
+ if (res.ok !== true || body.ok === false) {
139
+ const failure = new Error(errorMessageOf(body, res.status));
140
+ failure.status = res.status;
141
+ failure.code = body.error !== null && typeof body.error === "object" ? body.error.code : undefined;
142
+ throw failure;
143
+ }
144
+ const data = body.value !== null && typeof body.value === "object" ? body.value : body;
145
+ return {
146
+ records: Array.isArray(data.records) ? data.records : [],
147
+ stats: data.stats !== null && typeof data.stats === "object" ? data.stats : null,
148
+ };
149
+ }
150
+
151
+ async function postAction(body) {
152
+ const init = { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", "x-dsh-dream-action": "1" }, body: JSON.stringify(body) };
153
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") init.signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
154
+ const res = await fetch(routeUrlFor(ACTION_ROUTE, ""), init);
155
+ let value = null;
156
+ try { value = await res.json(); } catch (error) { /* Preserve HTTP status for useful recovery hints. */ }
157
+ if (res.ok !== true || value === null || value.ok === false) {
158
+ const hint = res.status === 401 ? "登录状态已失效,请重新打开 DSH 页面后重试。"
159
+ : res.status === 404 ? "当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。"
160
+ : errorMessageOf(value, res.status);
161
+ const failure = new Error(hint);
162
+ failure.code = value && value.error ? value.error.code : undefined;
163
+ failure.status = res.status;
164
+ throw failure;
165
+ }
166
+ return value;
167
+ }
168
+
169
+ /** 复制命令到剪贴板;没有 clipboard / 被拒绝时返回 false,调用方回退为可选中文本。 */
170
+ async function copyText(text) {
171
+ const value = String(text);
172
+ if (value === "") return false;
173
+ try {
174
+ if (typeof navigator !== "undefined" && navigator !== null && navigator.clipboard !== undefined && navigator.clipboard !== null && typeof navigator.clipboard.writeText === "function") {
175
+ await navigator.clipboard.writeText(value);
176
+ return true;
177
+ }
178
+ } catch (error) { /* 权限被拒 / 非安全上下文:回退展示可选中文本 */ }
179
+ return false;
180
+ }
181
+
124
182
  function normalizeDreams(value, limit) {
125
183
  if (!Array.isArray(value) || limit <= 0) return [];
126
184
  const out = [];
@@ -247,13 +305,20 @@ function applicabilityTextOf(value) {
247
305
 
248
306
  /**
249
307
  * 经验视图模型:只投影展示所需字段(title / when / applicability / exceptions /
250
- * state / scopeLabel / evidenceSummary / lastValidatedAt),不碰权重、向量、原始 JSON。
308
+ * state / scopeLabel / evidenceSummary / lastValidatedAt),并派生 revision / 扣留原因 /
309
+ * 可桥接标记与复制命令所需的稳定字段;不碰权重、向量、原始 JSON。
251
310
  * limit 只截断渲染行数,byState 仍按全量统计;truncated / badLines 原样带给提示行。
252
311
  */
253
312
  function normalizeKnowledge(value, now, limit) {
254
313
  const source = value !== null && typeof value === "object" ? value : {};
255
314
  const cap = typeof limit === "number" && limit > 0 ? limit : KNOWLEDGE_LIMIT;
256
315
  const rawLessons = Array.isArray(source.lessons) ? source.lessons : [];
316
+ const lessonTitles = new Map();
317
+ for (const raw of rawLessons) {
318
+ if (raw !== null && typeof raw === "object" && trimmedText(raw.id) !== "") {
319
+ lessonTitles.set(trimmedText(raw.id), trimmedText(raw.title) || trimmedText(raw.action));
320
+ }
321
+ }
257
322
  const statsSource = source.stats !== null && typeof source.stats === "object" ? source.stats : {};
258
323
  const byState = {};
259
324
  const rows = [];
@@ -269,6 +334,7 @@ function normalizeKnowledge(value, now, limit) {
269
334
  rows.push({
270
335
  id: trimmedText(raw.id),
271
336
  title: title !== "" ? title : action,
337
+ action: action,
272
338
  when: trimmedText(raw.when),
273
339
  applicability: applicabilityTextOf(raw.applicability),
274
340
  exceptions: Array.isArray(raw.exceptions)
@@ -279,9 +345,21 @@ function normalizeKnowledge(value, now, limit) {
279
345
  stateClass: lessonStateClass(state),
280
346
  scopeLabel: trimmedText(raw.scopeLabel) || "范围未标注",
281
347
  evidenceSummary: trimmedText(raw.evidenceSummary) || "暂无可展示的证据摘要",
348
+ evidence: Array.isArray(source.evidence) && Array.isArray(raw.evidenceIds)
349
+ ? source.evidence.filter(function (item) { return item && raw.evidenceIds.indexOf(item.id) !== -1; }).slice(0, 20) : [],
350
+ feedback: raw.feedback && typeof raw.feedback === "object" ? raw.feedback : { useful: 0, notApplicable: 0, recent: [] },
282
351
  lastValidatedAt: lastValidatedAt,
283
352
  lastText: lastValidatedAt === "" ? "尚未核验" : relativeTime(lastValidatedAt, now),
284
353
  lastTitle: lastValidatedAt === "" ? "没有 lastValidatedAt(从未核验)" : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
354
+ revision: typeof raw.revision === "number" && Number.isFinite(raw.revision) && raw.revision >= 0 ? Math.floor(raw.revision) : 0,
355
+ independentSupportCount: typeof raw.independentSupportCount === "number" && raw.independentSupportCount > 0 ? Math.floor(raw.independentSupportCount) : 0,
356
+ projectId: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.projectId) : "",
357
+ workspaceRoot: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.workspaceRoot) : "",
358
+ conflictIds: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }) : [],
359
+ conflicts: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }).map(function (id) { return { id: id, title: lessonTitles.get(id) || "未加载的经验(" + id + ")" }; }) : [],
360
+ holdReason: trimmedText(raw.holdReason) !== "" ? trimmedText(raw.holdReason) : holdReasonOf(raw),
361
+ holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? trimmedText(raw.holdReasonLabel) : skippedReasonLabel(holdReasonOf(raw)),
362
+ bridgeable: bridgeableOf(raw.state) && trimmedText(raw.id) !== "",
285
363
  candidate: state === "candidate",
286
364
  disputed: state === "disputed",
287
365
  });
@@ -335,6 +413,180 @@ function knowledgeEmptyState() {
335
413
  };
336
414
  }
337
415
 
416
+ /** 检索层 skipped reason 的中文映射(面板扣留原因文案;与 retrieval.ts 的 reason 常量同名)。 */
417
+ function skippedReasonLabel(reason) {
418
+ switch (trimmedText(reason)) {
419
+ case "state:rejected": return "已驳回:不参与默认检索与注入";
420
+ case "state:stale": return "待复核:条件或依赖可能已过期,默认不注入";
421
+ case "state:disputed": return "冲突未解决:默认不注入,先 resolve-conflict";
422
+ case "candidate-hold": return "候选:可被检索到,但默认不作为任务建议注入";
423
+ case "no-evidence": return "证据不足:没有独立证据,默认不注入";
424
+ case "version-mismatch": return "版本不匹配:当前依赖版本不满足适用范围";
425
+ case "limit": return "超出条数预算,本次未取回";
426
+ case "budget-stop": return "超出字符预算:按排名整条装入,遇到第一条放不下即停";
427
+ case "budget": return "超出字符预算,本次未取回";
428
+ default: return "";
429
+ }
430
+ }
431
+
432
+ /** 浏览态的确定性扣留原因;version-mismatch 需要 query / 包名上下文,不在此列。 */
433
+ function holdReasonOf(lesson) {
434
+ const source = lesson !== null && typeof lesson === "object" ? lesson : {};
435
+ const state = normalizeLessonState(source.state);
436
+ if (state === "disputed") return "state:disputed";
437
+ if (state === "stale") return "state:stale";
438
+ if (state === "rejected") return "state:rejected";
439
+ if (state === "candidate") {
440
+ const support = typeof source.independentSupportCount === "number" && source.independentSupportCount > 0 ? source.independentSupportCount : 0;
441
+ return support === 0 ? "no-evidence" : "candidate-hold";
442
+ }
443
+ return "";
444
+ }
445
+
446
+ /** 面板上的「可桥接」只按契约状态判断:usable;scope / 用户采纳等资格由 dream_bridge 执行时校验。 */
447
+ function bridgeableOf(state) {
448
+ return normalizeLessonState(state) === "usable";
449
+ }
450
+
451
+ /** 复制命令里的默认 action:按状态给最常用的一步,用户可在对话里再改。 */
452
+ function reviewActionFor(state) {
453
+ switch (normalizeLessonState(state)) {
454
+ case "candidate": return "accept";
455
+ case "disputed": return "resolve-conflict";
456
+ case "stale": return "accept";
457
+ case "usable": return "mark-stale";
458
+ case "rejected": return "reopen";
459
+ default: return "";
460
+ }
461
+ }
462
+
463
+ /** 派生 dream_review 复制命令;缺 id 时给 ""(面板不展示不可执行命令)。 */
464
+ function reviewCommandOf(lesson, options) {
465
+ const source = lesson !== null && typeof lesson === "object" ? lesson : {};
466
+ const id = trimmedText(source.id);
467
+ if (id === "" || !Number.isInteger(source.revision) || source.revision < 1) return "";
468
+ const opts = options || {};
469
+ const action = opts.action || reviewActionFor(source.state);
470
+ if (action === "") return "";
471
+ const args = { action: action, lessonId: id, expectedRevision: source.revision };
472
+ if (action === "resolve-conflict") {
473
+ if (!["prefer", "drop", "merge"].includes(opts.resolution) || trimmedText(opts.note) === "" || !Array.isArray(opts.affectedIds) || opts.affectedIds.length === 0) return "";
474
+ args.resolution = opts.resolution;
475
+ args.note = opts.note.trim();
476
+ args.affectedIds = opts.affectedIds;
477
+ }
478
+ return "dream_review " + JSON.stringify(args);
479
+ }
480
+
481
+ /** 派生 dream_bridge preview 复制命令(单条经验);projectId 来自 payload,scopeLabel 只展示。 */
482
+ function bridgePreviewCommandOf(lessonId, projectId, workspaceRoot) {
483
+ const id = trimmedText(lessonId);
484
+ if (id === "") return "";
485
+ const project = trimmedText(projectId);
486
+ const args = { mode: "preview", lessonIds: [id] };
487
+ if (project !== "") args.projectId = project;
488
+ const root = trimmedText(workspaceRoot);
489
+ if (root !== "") args.path = root.replace(/[\\/]+$/, "") + "/AGENTS.md";
490
+ return "dream_bridge " + JSON.stringify(args);
491
+ }
492
+
493
+ function bridgeRollbackCommandOf(row) {
494
+ if (!row || row.rollbackable !== true || trimmedText(row.target) === "") return "";
495
+ const backupId = trimmedText(row.id) || trimmedText(row.backupId);
496
+ if (backupId === "") return "";
497
+ return "dream_bridge " + JSON.stringify({ mode: "rollback", backupId: backupId, path: row.target });
498
+ }
499
+
500
+ /** 哈希只展示前 12 位;空值给 ""。 */
501
+ function shortHash(value) {
502
+ const text = trimmedText(value);
503
+ if (text === "") return "";
504
+ return text.length > 12 ? text.slice(0, 12) + "…" : text;
505
+ }
506
+
507
+ function bridgeActionLabel(action) {
508
+ switch (trimmedText(action)) {
509
+ case "create": return "新建";
510
+ case "replace": return "替换管理块";
511
+ case "append": return "追加管理块";
512
+ case "unchanged": return "未改动";
513
+ case "rollback": return "回滚";
514
+ default: return trimmedText(action) || "未知动作";
515
+ }
516
+ }
517
+
518
+ /** 写入记录视图模型:只投影展示字段,不展示原始 JSON / 管理块正文。 */
519
+ function normalizeBridgeRecords(value, now, limit) {
520
+ const source = value !== null && typeof value === "object" ? value : {};
521
+ const cap = typeof limit === "number" && limit > 0 ? limit : BRIDGE_LIMIT;
522
+ const rawRecords = Array.isArray(source.records) ? source.records : [];
523
+ const statsSource = source.stats !== null && typeof source.stats === "object" ? source.stats : {};
524
+ const rows = [];
525
+ let rollbackableCount = 0;
526
+ for (const raw of rawRecords) {
527
+ if (raw === null || typeof raw !== "object") continue;
528
+ const id = trimmedText(raw.backupId);
529
+ const target = trimmedText(raw.target);
530
+ if (id === "" && target === "") continue;
531
+ const rollbackable = raw.rollbackable === true;
532
+ if (rollbackable) rollbackableCount += 1;
533
+ if (rows.length >= cap) continue;
534
+ const at = trimmedText(raw.at);
535
+ const lessonRefs = [];
536
+ if (Array.isArray(raw.lessons)) {
537
+ for (const item of raw.lessons) {
538
+ if (item !== null && typeof item === "object") {
539
+ const lessonId = trimmedText(item.lessonId);
540
+ if (lessonId === "") continue;
541
+ const revision = typeof item.revision === "number" && Number.isFinite(item.revision) && item.revision >= 0 ? Math.floor(item.revision) : 0;
542
+ lessonRefs.push(lessonId + "@" + String(revision));
543
+ } else if (typeof item === "string" && item.trim() !== "") {
544
+ lessonRefs.push(item.trim());
545
+ }
546
+ }
547
+ }
548
+ const rollbackReason = trimmedText(raw.rollbackReason);
549
+ rows.push({
550
+ id: id,
551
+ at: at,
552
+ atText: at === "" ? "时间未知" : relativeTime(at, now),
553
+ atTitle: at === "" ? "没有记录时间" : formatDate(at) + (formatTime(at) === "" ? "" : " " + formatTime(at)),
554
+ target: target || "目标未记录",
555
+ action: trimmedText(raw.action),
556
+ actionLabel: bridgeActionLabel(raw.action),
557
+ lessons: lessonRefs,
558
+ lessonsText: lessonRefs.length === 0 ? "没有经验引用" : lessonRefs.join("、"),
559
+ beforeShort: shortHash(raw.beforeSha256),
560
+ afterShort: shortHash(raw.afterSha256),
561
+ rollbackable: rollbackable,
562
+ rollbackText: rollbackable ? "可回滚" : ("不可回滚" + (rollbackReason !== "" ? ":" + rollbackReason : "")),
563
+ rollbackReason: rollbackReason,
564
+ });
565
+ }
566
+ const total = typeof statsSource.total === "number" && statsSource.total >= 0 ? statsSource.total : rawRecords.length;
567
+ const statsRollback = typeof statsSource.rollbackable === "number" && statsSource.rollbackable >= 0 ? statsSource.rollbackable : rollbackableCount;
568
+ return {
569
+ rows: rows,
570
+ total: total,
571
+ shown: rows.length,
572
+ rollbackable: statsRollback,
573
+ };
574
+ }
575
+
576
+ /** 「写入记录」的独立空态。 */
577
+ function bridgeEmptyState() {
578
+ return {
579
+ title: "还没有写入记录",
580
+ body: "写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。",
581
+ hint: "在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。",
582
+ };
583
+ }
584
+
585
+ /** 「待核实」的独立空态。 */
586
+ function reviewQueueEmptyState() {
587
+ return "没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。";
588
+ }
589
+
338
590
  function spanOf(dreams) {
339
591
  let min = null;
340
592
  let max = null;
@@ -605,6 +857,7 @@ function Btn(props) {
605
857
  onClick: props.onClick,
606
858
  disabled: props.disabled === true,
607
859
  title: props.title,
860
+ "aria-expanded": props["aria-expanded"],
608
861
  "aria-pressed": props.pressed === undefined ? undefined : props.pressed,
609
862
  }, props.children);
610
863
  }
@@ -654,10 +907,143 @@ function LessonCard(props) {
654
907
  h("span", { className: "dshd-lesson-meta", key: "m" }, [
655
908
  h("span", { className: "dshd-times", key: "c" }, "×", h("b", { key: "n" }, String(row.count))),
656
909
  row.lastText === "" ? null : h("span", { className: "dshd-last", key: "l", title: row.lastTitle }, row.lastText),
657
- row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, "桥接候选") : null,
910
+ row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, "常见教训") : null,
658
911
  ]),
659
912
  ]))),
660
- h("div", { className: "dshd-note", key: "note" }, "次数 = 日记里出现过的次数,不代表重要度;出现 ≥3 次标为「桥接候选」。是否写入 AGENTS.md 由 dream_bridge 决定。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。"),
913
+ h("div", { className: "dshd-note", key: "note" }, "次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。"),
914
+ ]);
915
+ }
916
+
917
+ /** 复制命令按钮:成功给「已复制」,剪贴板不可用时回退为可选中文本。 */
918
+ function CopyCommand(props) {
919
+ const command = typeof props.command === "string" ? props.command : "";
920
+ const [copied, setCopied] = useState(false);
921
+ const [failed, setFailed] = useState(false);
922
+ if (command === "") return null;
923
+ const onCopy = function () {
924
+ copyText(command).then(function (ok) {
925
+ setCopied(ok === true);
926
+ setFailed(ok !== true);
927
+ });
928
+ };
929
+ const label = props.label === undefined ? "复制命令" : props.label;
930
+ return h("span", { className: "dshd-copywrap" }, [
931
+ h(Btn, { key: "b", onClick: onCopy, disabled: props.disabled === true, title: props.title === undefined ? command : props.title }, copied === true ? "已复制" : label),
932
+ failed === true ? h("code", { key: "cmd", className: "dshd-cmd", tabIndex: 0 }, command) : null,
933
+ failed === true ? h("span", { key: "hint", className: "dshd-note" }, "剪贴板不可用:请手动选中上面的命令复制。") : null,
934
+ ]);
935
+ }
936
+
937
+ /** 面板保持只读:先让人看清决策,再生成可直接粘贴进对话的完整命令。 */
938
+ function ReviewControls(props) {
939
+ const row = props.row;
940
+ const [open, setOpen] = useState(false);
941
+ const [action, setAction] = useState(reviewActionFor(row.state));
942
+ const [resolution, setResolution] = useState("prefer");
943
+ const [note, setNote] = useState("");
944
+ const [feedbackNote, setFeedbackNote] = useState("");
945
+ const [feedbackRetry, setFeedbackRetry] = useState(null);
946
+ const [affected, setAffected] = useState(function () { return row.conflictIds[0] || ""; });
947
+ const [pending, setPending] = useState(false);
948
+ const [error, setError] = useState("");
949
+ const [preview, setPreview] = useState(null);
950
+ const commands = row.state === "rejected" ? [["reopen", "重新送审"]]
951
+ : row.state === "disputed" ? [["resolve-conflict", "处理冲突"], ["reject", "驳回这条"]]
952
+ : row.state === "usable" ? [["mark-stale", "标记待复核"], ["reject", "驳回"]]
953
+ : [["accept", "采纳"], ["reject", "驳回"], ["mark-stale", "标记待复核"]];
954
+ const effectiveAction = commands.some(function (item) { return item[0] === action; }) ? action : commands[0][0];
955
+ const command = reviewCommandOf(row, { action: effectiveAction, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] });
956
+ const submit = async function (body, message, isPreview) {
957
+ if (pending) return;
958
+ setPending(true); setError("");
959
+ try {
960
+ const result = await postAction(body);
961
+ if (isPreview) setPreview(result);
962
+ else { setPreview(null); if (props.onChanged) props.onChanged(result.unchanged === true ? "管理块内容已一致,无需写入。" : message); }
963
+ } catch (failure) {
964
+ setError(failure.name === "TimeoutError" ? "请求超时,请先刷新确认结果后再重试。" : failure.message || String(failure));
965
+ if (body.operation === "apply") setPreview(null);
966
+ } finally { setPending(false); }
967
+ };
968
+ const review = function (chosen) {
969
+ return submit({ operation: "review", action: chosen, lessonId: row.id, expectedRevision: row.revision, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] }, "已更新「" + row.title + "」的审阅状态。");
970
+ };
971
+ const feedback = function (vote) {
972
+ const signature = row.id + "@" + row.revision + ":" + vote + ":" + feedbackNote;
973
+ const requestId = feedbackRetry && feedbackRetry.signature === signature ? feedbackRetry.id
974
+ : typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : "feedback_" + Date.now() + "_" + Math.random().toString(36).slice(2);
975
+ setFeedbackRetry({ signature: signature, id: requestId });
976
+ return submit({ operation: "feedback", lessonId: row.id, expectedRevision: row.revision, vote: vote, requestId: requestId, ...(feedbackNote.trim() === "" ? {} : { note: feedbackNote.trim() }) }, "已记录使用反馈;「" + row.title + "」的审阅状态保持原样。");
977
+ };
978
+ return h("div", { className: "dshd-review", role: "group", "aria-label": row.title + "的操作", onClick: function (event) { event.stopPropagation(); } }, [
979
+ h("div", { className: "dshd-kcmd", key: "buttons" }, [
980
+ ...commands.filter(function (item) { return item[0] !== "resolve-conflict"; }).map(function (item) {
981
+ return h(Btn, { key: item[0], disabled: pending || row.revision < 1, onClick: function () { review(item[0]); } }, item[1]);
982
+ }),
983
+ h(Btn, { key: "review", disabled: pending, onClick: function () { setOpen(!open); }, "aria-expanded": open }, open ? "收起审阅" : row.state === "disputed" ? "处理冲突" : "审阅详情"),
984
+ row.bridgeable === true && row.workspaceRoot !== "" ? h(Btn, { key: "preview", disabled: pending, onClick: function () { submit({ operation: "preview", lessonId: row.id, expectedRevision: row.revision }, "", true); } }, "预览规则变更") : null,
985
+ row.bridgeable === true && row.workspaceRoot === "" ? h(CopyCommand, { key: "preview-command", command: bridgePreviewCommandOf(row.id, row.projectId, row.workspaceRoot), label: "复制项目预览命令" }) : null,
986
+ ]),
987
+ pending ? h("div", { key: "pending", role: "status", className: "dshd-note" }, "正在处理…") : null,
988
+ error !== "" ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, [error, h(Btn, { key: "refresh", disabled: pending, onClick: function () { if (props.onChanged) props.onChanged("已刷新经验列表。"); } }, "刷新列表")]) : null,
989
+ open ? h("div", { className: "dshd-reviewform", key: "form", onKeyDown: function (event) {
990
+ if (event.key === "Escape" && !pending) { event.preventDefault(); event.stopPropagation(); setOpen(false); }
991
+ } }, [
992
+ h("label", { key: "action" }, ["审阅动作", h("select", { "aria-label": "审阅动作", disabled: pending, value: effectiveAction, onChange: function (event) { setAction(event.target.value); } }, commands.map(function (item) { return h("option", { value: item[0], key: item[0] }, item[1]); }))]),
993
+ effectiveAction === "resolve-conflict" ? h("div", { key: "conflict" }, [
994
+ h("label", { key: "against" }, ["冲突经验", h("select", { "aria-label": "冲突经验", disabled: pending, value: affected, onChange: function (event) { setAffected(event.target.value); } }, row.conflicts.map(function (item) { return h("option", { key: item.id, value: item.id, title: item.title }, item.title); }))]),
995
+ h("label", { key: "resolution" }, ["处理方式", h("select", { "aria-label": "处理方式", disabled: pending, value: resolution, onChange: function (event) { setResolution(event.target.value); } }, [
996
+ h("option", { key: "prefer", value: "prefer" }, "采用这条,驳回对方"),
997
+ h("option", { key: "drop", value: "drop" }, "放弃这条,保留对方"),
998
+ h("option", { key: "merge", value: "merge" }, "合并到这条,对方留作历史"),
999
+ ])]),
1000
+ h("label", { key: "note" }, ["处理依据", h("textarea", { "aria-label": "处理依据", disabled: pending, rows: 2, maxLength: 500, value: note, placeholder: "说明哪个来源支持这次决定…", onChange: function (event) { setNote(event.target.value); } })]),
1001
+ ]) : null,
1002
+ h(Btn, { key: "confirm", disabled: pending || command === "", onClick: function () { review(effectiveAction); }, variant: "primary" }, "确认审阅"),
1003
+ h("details", { key: "fallback" }, [h("summary", { key: "summary" }, "在对话里处理"), command === "" ? h("div", { key: "hint", className: "dshd-note" }, "选择冲突经验并填写处理依据后即可处理。") : h(CopyCommand, { key: "copy", command: command, label: "复制审阅命令" })]),
1004
+ ]) : null,
1005
+ preview ? h("section", { key: "preview", className: "dshd-reviewform", "aria-label": "规则变更预览" }, [
1006
+ h("strong", { key: "heading" }, "规则变更预览"),
1007
+ h("div", { key: "target", className: "dshd-note" }, "目标:" + preview.resolvedPath),
1008
+ h("pre", { key: "diff", className: "dshd-diff", tabIndex: 0 }, preview.diff || "管理块内容已经一致,无需更改。"),
1009
+ h("div", { key: "help", className: "dshd-note" }, "将按此预览重建 Dream 管理块,请核对新增和删除内容。块外内容保留,写入前创建回滚备份。"),
1010
+ h(Btn, { key: "apply", disabled: pending, variant: "primary", onClick: function () { submit({ operation: "apply", token: preview.token }, "规则已应用,可在「写入记录」中回滚。"); } }, "应用此变更"),
1011
+ h(Btn, { key: "cancel", disabled: pending, onClick: function () { setPreview(null); } }, "取消预览"),
1012
+ ]) : null,
1013
+ h("details", { key: "feedback", className: "dshd-feedback" }, [
1014
+ h("summary", { key: "summary" }, "使用反馈 · 有用 " + (row.feedback.useful || 0) + " · 不适用 " + (row.feedback.notApplicable || 0)),
1015
+ h("label", { key: "note" }, ["场景备注(可选)", h("textarea", { "aria-label": "反馈场景备注", rows: 2, disabled: pending, maxLength: 500, value: feedbackNote, placeholder: "这次在哪个场景使用…", onChange: function (event) { setFeedbackNote(event.target.value); } })]),
1016
+ h(Btn, { key: "useful", disabled: pending, onClick: function () { feedback("useful"); } }, "这次有用"),
1017
+ h(Btn, { key: "not", disabled: pending, onClick: function () { feedback("not-applicable"); } }, "这次不适用"),
1018
+ h("div", { key: "help", className: "dshd-note" }, "记录使用情况,供之后复核;采纳与驳回由审阅决定。"),
1019
+ ...(Array.isArray(row.feedback.recent) ? row.feedback.recent.slice(-3).reverse().map(function (item, index) {
1020
+ return h("div", { key: "recent-" + index, className: "dshd-note" }, formatDate(item.at) + " · " + (item.vote === "useful" ? "有用" : "不适用") + (item.note ? " · " + item.note : ""));
1021
+ }) : []),
1022
+ ]),
1023
+ ]);
1024
+ }
1025
+
1026
+ function RollbackControl(props) {
1027
+ const [confirm, setConfirm] = useState(false);
1028
+ const [pending, setPending] = useState(false);
1029
+ const [error, setError] = useState("");
1030
+ const rollback = async function () {
1031
+ if (pending) return;
1032
+ setPending(true); setError("");
1033
+ try {
1034
+ await postAction({ operation: "rollback", backupId: props.row.id });
1035
+ setConfirm(false);
1036
+ if (props.onChanged) props.onChanged("已恢复上一次的 Dream 管理块,块外内容保留。");
1037
+ } catch (failure) { setError(failure.message || String(failure)); }
1038
+ finally { setPending(false); }
1039
+ };
1040
+ return h("div", { className: "dshd-reviewform" }, [
1041
+ h(Btn, { key: "start", disabled: pending, onClick: function () { setConfirm(!confirm); } }, confirm ? "取消回滚" : "回滚这次变更"),
1042
+ confirm ? h("div", { key: "confirm" }, [
1043
+ h("p", { key: "target", className: "dshd-note" }, "将恢复「" + props.row.target + "」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。"),
1044
+ h(Btn, { key: "apply", variant: "primary", disabled: pending, onClick: rollback }, pending ? "正在回滚…" : "确认回滚管理块"),
1045
+ ]) : null,
1046
+ error ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, error) : null,
661
1047
  ]);
662
1048
  }
663
1049
 
@@ -676,25 +1062,44 @@ function KnowledgeRow(props) {
676
1062
  h("span", { className: "dshd-ktitle", key: "t", title: row.title }, row.title),
677
1063
  ]),
678
1064
  h("div", { className: "dshd-kmeta", key: "m" }, [
679
- h("span", { className: "dshd-kscope", key: "s", title: "适用范围" }, row.scopeLabel),
680
1065
  h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, "最近核验:" + row.lastText),
681
1066
  ]),
682
1067
  h("div", { className: "dshd-kbody", key: "body" }, [
683
1068
  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),
1069
+ h("div", { key: "action", className: "dshd-action" }, "怎么做:" + (row.action || "未标注具体动作")),
1070
+ h("details", { key: "details", onClick: function (event) { event.stopPropagation(); } }, [
1071
+ h("summary", { key: "summary" }, "适用范围与证据"),
1072
+ h("div", { key: "scope" }, "范围:" + row.scopeLabel),
1073
+ row.applicability !== "" ? h("div", { key: "applicability" }, "适用:" + row.applicability) : null,
1074
+ row.exceptions.length > 0 ? h("div", { key: "exceptions" }, "例外:" + row.exceptions.join(";")) : null,
1075
+ h("div", { key: "evidence" }, "证据:" + row.evidenceSummary),
1076
+ row.evidence.length === 0 ? h("div", { key: "sources-empty", className: "dshd-note" }, "没有关联来源记录,采纳前请核对依据。") : h("div", { key: "sources", className: "dshd-sources" }, row.evidence.map(function (item) {
1077
+ return h("details", { key: item.id }, [
1078
+ h("summary", { key: "summary" }, "来源记录 · " + (item.verification === "read" ? "已读到来源" : "待核验") + " · " + (item.kind === "session" ? "会话" : item.kind === "user-correction" ? "用户纠正" : "本地资料")),
1079
+ h("div", { key: "summary-text" }, item.summary || "未提供摘要"),
1080
+ item.sessionId ? h("div", { key: "session", className: "dshd-note" }, "会话:" + item.sessionId + (Number.isInteger(item.recordSeq) ? " · 记录 " + item.recordSeq : "")) : null,
1081
+ h("div", { key: "date", className: "dshd-note" }, "记录时间:" + formatDate(item.observedAt)),
1082
+ item.verificationReason ? h("div", { key: "reason", className: "dshd-note" }, "核验说明:" + item.verificationReason) : null,
1083
+ item.sourceHash ? h("code", { key: "hash", className: "dshd-cmd", title: item.sourceHash }, "来源校验值:" + item.sourceHash) : null,
1084
+ ]);
1085
+ })),
1086
+ ]),
1087
+ props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, "为什么被扣下:" + row.holdReasonLabel) : null,
687
1088
  ]),
1089
+ h(ReviewControls, { row: row, key: row.id + "@" + row.revision, onChanged: props.onChanged }),
688
1090
  ]);
689
1091
  }
690
1092
 
691
1093
  function KnowledgeCard(props) {
692
1094
  const vm = props.vm;
1095
+ const [stateFilter, setStateFilter] = useState("usable");
1096
+ const [pageSize, setPageSize] = useState(5);
1097
+ const reviewed = vm.rows.filter(function (row) { return row.state === stateFilter; });
693
1098
  const notice = knowledgeNoticeText(vm === null || vm === undefined ? null : vm.stats);
694
1099
  const noticeNode = notice === "" ? null : h("div", { className: "dshd-knotice", key: "notice", role: "status" }, notice);
695
1100
  const head = function (extra) {
696
1101
  return h("div", { className: "dshd-khead", key: "head" }, [
697
- h("h4", { key: "t" }, "经验"),
1102
+ h("h4", { key: "t" }, "已审阅经验"),
698
1103
  h("span", { className: "dshd-spacer", key: "s" }),
699
1104
  extra,
700
1105
  ]);
@@ -723,13 +1128,119 @@ function KnowledgeCard(props) {
723
1128
  ? h("span", { className: "dshd-hint", key: "loading" }, "加载中…")
724
1129
  : h("span", { className: "dshd-hint", key: "count" }, knowledgeCountText(vm.stats))),
725
1130
  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 });
1131
+ h("label", { className: "dshd-note", key: "filter" }, ["查看 ", h("select", { "aria-label": "经验状态", value: stateFilter, onChange: function (event) { setStateFilter(event.target.value); setPageSize(5); } }, [
1132
+ h("option", { key: "usable", value: "usable" }, "可用经验"), h("option", { key: "rejected", value: "rejected" }, "已驳回历史"),
1133
+ ])]),
1134
+ reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? "暂无可用经验;待核实的记录在下方。" : "没有已驳回记录。") : null,
1135
+ h("div", { className: "dshd-krows", key: "rows" }, reviewed.slice(0, pageSize).map(function (row, index) {
1136
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true });
728
1137
  })),
1138
+ reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多经验") : null,
729
1139
  vm.shown < vm.total
730
1140
  ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条经验。")
731
1141
  : null,
732
- h("div", { className: "dshd-note", key: "note" }, "只读展示:候选可被检索到,但默认不注入任务建议、且排在可用经验之后;未经审阅不得当作已证实结论。采纳、驳回或标记冲突请用 dream_review。"),
1142
+ h("div", { className: "dshd-note", key: "note" }, "采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。"),
1143
+ ]);
1144
+ }
1145
+
1146
+ /** 「待核实」只读区块:candidate / disputed / stale + 被扣下的原因(复用 KnowledgeRow)。 */
1147
+ function ReviewQueueCard(props) {
1148
+ const vm = props.vm;
1149
+ const [pageSize, setPageSize] = useState(5);
1150
+ const rows = vm !== null && vm !== undefined && Array.isArray(vm.rows)
1151
+ ? vm.rows.filter(function (row) { return HELD_STATES.indexOf(row.state) !== -1; })
1152
+ : [];
1153
+ const head = function (extra) {
1154
+ return h("div", { className: "dshd-khead", key: "head" }, [
1155
+ h("h4", { key: "t" }, "待核实"),
1156
+ h("span", { className: "dshd-spacer", key: "s" }),
1157
+ extra,
1158
+ ]);
1159
+ };
1160
+ if (props.error !== null && props.error !== undefined) {
1161
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1162
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1163
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1164
+ ]);
1165
+ }
1166
+ if (props.loading === true && rows.length === 0) {
1167
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1168
+ head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1169
+ ]);
1170
+ }
1171
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1172
+ head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + " 条待核实")),
1173
+ rows.length === 0
1174
+ ? h("div", { className: "dshd-kempty", key: "empty" }, [
1175
+ h("div", { className: "dshd-kempty-title", key: "t" }, "没有待核实的经验"),
1176
+ h("p", { className: "dshd-kempty-body", key: "b" }, reviewQueueEmptyState()),
1177
+ ])
1178
+ : h("div", { className: "dshd-krows", key: "rows" }, rows.slice(0, pageSize).map(function (row, index) {
1179
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "held-" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true, showHoldReason: true });
1180
+ })),
1181
+ rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多待核实经验") : null,
1182
+ h("div", { className: "dshd-note", key: "note" }, "这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。"),
1183
+ ]);
1184
+ }
1185
+
1186
+ /** 「写入记录」只读区块:<journalDir>/bridge/ 的桥接应用记录。 */
1187
+ function BridgeLogCard(props) {
1188
+ const vm = props.vm;
1189
+ const head = function (extra) {
1190
+ return h("div", { className: "dshd-khead", key: "head" }, [
1191
+ h("h4", { key: "t" }, "写入记录"),
1192
+ h("span", { className: "dshd-spacer", key: "s" }),
1193
+ extra,
1194
+ ]);
1195
+ };
1196
+ if (props.error !== null && props.error !== undefined) {
1197
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1198
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1199
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取写入记录失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1200
+ ]);
1201
+ }
1202
+ if (props.loading === true) {
1203
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1204
+ head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1205
+ ]);
1206
+ }
1207
+ if (vm.rows.length === 0) {
1208
+ const empty = bridgeEmptyState();
1209
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1210
+ h("h4", { key: "t" }, "写入记录"),
1211
+ h("div", { className: "dshd-kempty", key: "empty" }, [
1212
+ h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
1213
+ h("p", { className: "dshd-kempty-body", key: "b" }, empty.body),
1214
+ h("p", { className: "dshd-note", key: "h" }, empty.hint),
1215
+ ]),
1216
+ ]);
1217
+ }
1218
+ return h("section", { className: "dshd-card dshd-kcard" }, [
1219
+ head(h("span", { className: "dshd-hint", key: "c" }, String(vm.total) + " 次写入 · " + String(vm.rollbackable) + " 次可回滚")),
1220
+ h("div", { className: "dshd-krows", key: "rows" }, vm.rows.map(function (row, index) {
1221
+ return h("div", { className: "dshd-bridge", key: row.id !== "" ? row.id : "bridge-" + String(index) }, [
1222
+ h("div", { className: "dshd-bridge-head", key: "h" }, [
1223
+ h("span", { className: "dshd-kbadge", key: "a" }, row.actionLabel),
1224
+ h("span", { className: "dshd-bridge-target", key: "t", title: row.target }, row.target),
1225
+ h("span", { className: "dshd-spacer", key: "s" }),
1226
+ h("span", { className: "dshd-klast", key: "at", title: row.atTitle }, row.atText),
1227
+ ]),
1228
+ h("div", { className: "dshd-bridge-meta", key: "m" }, [
1229
+ h("span", { key: "lessons", title: row.lessonsText }, "经验:" + row.lessonsText),
1230
+ row.beforeShort === "" ? null : h("span", { key: "before" }, "前哈希:" + row.beforeShort),
1231
+ row.afterShort === "" ? null : h("span", { key: "after" }, "后哈希:" + row.afterShort),
1232
+ h("span", { key: "rb", className: row.rollbackable === true ? "dshd-rollback-ok" : "dshd-rollback-no" }, row.rollbackText),
1233
+ ]),
1234
+ h("details", { key: "backup" }, [
1235
+ h("summary", { key: "summary" }, "备份标识与回滚命令"),
1236
+ h("code", { key: "id", className: "dshd-cmd" }, row.id),
1237
+ row.rollbackable === true ? h("code", { key: "command", className: "dshd-cmd", tabIndex: 0 }, bridgeRollbackCommandOf(row)) : null,
1238
+ ]),
1239
+ row.rollbackable === true ? h(RollbackControl, { key: "rollback", row: row, onChanged: props.onChanged }) : null,
1240
+ ]);
1241
+ })),
1242
+ vm.shown < vm.total ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条记录。") : null,
1243
+ h("div", { className: "dshd-note", key: "note" }, "回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。"),
733
1244
  ]);
734
1245
  }
735
1246
 
@@ -799,6 +1310,7 @@ function Skeleton() {
799
1310
  // ───────────────────────── 主面板 ─────────────────────────
800
1311
 
801
1312
  function DreamPanel() {
1313
+ const [activeTab, setActiveTab] = useState("journal");
802
1314
  const [dreams, setDreams] = useState([]);
803
1315
  const [stats, setStats] = useState(null);
804
1316
  const [loading, setLoading] = useState(true);
@@ -806,6 +1318,7 @@ function DreamPanel() {
806
1318
  const [queryInput, setQueryInput] = useState("");
807
1319
  const [query, setQuery] = useState("");
808
1320
  const [reloadToken, setReloadToken] = useState(0);
1321
+ const [actionNotice, setActionNotice] = useState("");
809
1322
  const [now, setNow] = useState(function () { return new Date(); });
810
1323
  const [privacy, setPrivacy] = useState(loadPrivacy);
811
1324
  const [filters, setFilters] = useState({ moods: [], withLessons: false });
@@ -814,6 +1327,9 @@ function DreamPanel() {
814
1327
  const [knowledge, setKnowledge] = useState(null);
815
1328
  const [knowledgeLoading, setKnowledgeLoading] = useState(true);
816
1329
  const [knowledgeError, setKnowledgeError] = useState(null);
1330
+ const [bridge, setBridge] = useState(null);
1331
+ const [bridgeLoading, setBridgeLoading] = useState(true);
1332
+ const [bridgeError, setBridgeError] = useState(null);
817
1333
 
818
1334
  // 搜索防抖:连续打字只发一次请求;回车立即生效。
819
1335
  useEffect(function () {
@@ -858,8 +1374,27 @@ function DreamPanel() {
858
1374
  return function () { alive = false; };
859
1375
  }, [reloadToken]);
860
1376
 
1377
+ // 写入记录独立取数:桥接目录不存在时返回空,不影响其余区块。
1378
+ useEffect(function () {
1379
+ let alive = true;
1380
+ setBridgeLoading(true);
1381
+ setBridgeError(null);
1382
+ fetchBridgeRecords().then(function (payload) {
1383
+ if (alive !== true) return;
1384
+ setBridge(normalizeBridgeRecords(payload, new Date()));
1385
+ setBridgeLoading(false);
1386
+ }).catch(function (failure) {
1387
+ if (alive !== true) return;
1388
+ setBridgeError(failure instanceof Error ? failure : new Error(String(failure)));
1389
+ setBridgeLoading(false);
1390
+ setBridge(null);
1391
+ });
1392
+ return function () { alive = false; };
1393
+ }, [reloadToken]);
1394
+
861
1395
  const vm = viewModelOf(dreams, stats, now);
862
1396
  const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : knowledge;
1397
+ const bridgeVm = bridge === null ? { rows: [], total: 0, shown: 0, rollbackable: 0 } : bridge;
863
1398
  const pristine = vm.count === 0 && query === "";
864
1399
  const filterCount = filterCountOf(filters);
865
1400
  const visibleDreams = applyFilters(dreams, filters);
@@ -884,6 +1419,7 @@ function DreamPanel() {
884
1419
  };
885
1420
  const clearFilters = function () { setFilters({ moods: [], withLessons: false }); };
886
1421
  const showMore = function () { if (limit < MORE_LIMIT) setLimit(MORE_LIMIT); };
1422
+ const onChanged = function (message) { setActionNotice(message); setReloadToken(function (value) { return value + 1; }); };
887
1423
  const showKnowledge = function () { if (knowledgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
888
1424
  const knowledgeCard = h(KnowledgeCard, {
889
1425
  key: "knowledge",
@@ -893,6 +1429,25 @@ function DreamPanel() {
893
1429
  empty: knowledgeError === null && knowledgeLoading !== true && knowledgeVm.rows.length === 0,
894
1430
  privacy: privacy === true,
895
1431
  onRetry: showKnowledge,
1432
+ onChanged: onChanged,
1433
+ });
1434
+ const reviewQueueCard = h(ReviewQueueCard, {
1435
+ key: "review-queue",
1436
+ vm: knowledgeVm,
1437
+ loading: knowledgeLoading === true,
1438
+ error: knowledgeError,
1439
+ privacy: privacy === true,
1440
+ onRetry: showKnowledge,
1441
+ onChanged: onChanged,
1442
+ });
1443
+ const showBridge = function () { if (bridgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
1444
+ const bridgeLogCard = h(BridgeLogCard, {
1445
+ key: "bridge-log",
1446
+ vm: bridgeVm,
1447
+ loading: bridgeLoading === true,
1448
+ error: bridgeError,
1449
+ onRetry: showBridge,
1450
+ onChanged: onChanged,
896
1451
  });
897
1452
 
898
1453
  // 筛选 popover:点面板外任意处收起;纯 node 冒烟没有事件系统就跳过。
@@ -904,8 +1459,18 @@ function DreamPanel() {
904
1459
  if (pop !== null && typeof pop.contains === "function" && pop.contains(event.target) === true) return;
905
1460
  setFilterOpen(false);
906
1461
  };
1462
+ const onKey = function (event) {
1463
+ if (event.key !== "Escape") return;
1464
+ event.preventDefault();
1465
+ event.stopPropagation();
1466
+ event.stopImmediatePropagation();
1467
+ setFilterOpen(false);
1468
+ const trigger = document.querySelector(".dshd-filter button");
1469
+ if (trigger && typeof trigger.focus === "function") trigger.focus();
1470
+ };
907
1471
  document.addEventListener("mousedown", onDown);
908
- return function () { document.removeEventListener("mousedown", onDown); };
1472
+ document.addEventListener("keydown", onKey, true);
1473
+ return function () { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey, true); };
909
1474
  }, [filterOpen]);
910
1475
 
911
1476
  const searchIcon = h("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", key: "i" }, [
@@ -914,7 +1479,7 @@ function DreamPanel() {
914
1479
  ]);
915
1480
 
916
1481
  const toolbar = h("div", { className: "dshd-toolbar", key: "toolbar" }, [
917
- pristine ? null : h("div", { className: "dshd-search", key: "search" }, [
1482
+ pristine || activeTab !== "journal" ? null : h("div", { className: "dshd-search", key: "search" }, [
918
1483
  searchIcon,
919
1484
  h("input", {
920
1485
  key: "search-input",
@@ -935,10 +1500,10 @@ function DreamPanel() {
935
1500
  }, "×"),
936
1501
  ]),
937
1502
  h("span", { className: "dshd-spacer", key: "spacer" }),
938
- pristine ? null : h("span", { className: "dshd-hint", key: "count" }, loading === true
1503
+ pristine || activeTab !== "journal" ? null : h("span", { className: "dshd-hint", key: "count" }, loading === true
939
1504
  ? "加载中…"
940
1505
  : countTextOf(query, vm.total, vm.count, filterCount, visibleDreams.length)),
941
- showFilterButton === true ? h("div", { className: "dshd-filter", key: "filter" }, [
1506
+ showFilterButton === true && activeTab === "journal" ? h("div", { className: "dshd-filter", key: "filter" }, [
942
1507
  h(Btn, {
943
1508
  key: "btn",
944
1509
  on: filterCount > 0,
@@ -987,16 +1552,17 @@ function DreamPanel() {
987
1552
  ]);
988
1553
 
989
1554
  const body = function () {
990
- if (loading === true && vm.count === 0) return h("div", { key: "boot" }, [h(Skeleton, { key: "skeleton" }), knowledgeCard]);
1555
+ if (activeTab === "knowledge") return h("div", { key: "knowledge-tab" }, [actionNotice ? h("div", { key: "notice", className: "dshd-knotice", role: "status" }, actionNotice) : null, knowledgeCard, reviewQueueCard]);
1556
+ if (activeTab === "bridge") return h("div", { key: "bridge-tab" }, [actionNotice ? h("div", { key: "notice", className: "dshd-knotice", role: "status" }, actionNotice) : null, bridgeLogCard]);
1557
+ if (loading === true && vm.count === 0) return h(Skeleton, { key: "skeleton" });
991
1558
  if (vm.count === 0) {
992
1559
  if (error !== null) return h("div", { key: "failed-wrap" }, [
993
1560
  h("div", { className: "dshd-empty", key: "failed" }, [
994
1561
  h("h3", { key: "t" }, "暂时读不到梦境日记"),
995
1562
  h("p", { key: "p" }, "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。"),
996
1563
  ]),
997
- knowledgeCard,
998
1564
  ]);
999
- return h("div", { key: "empty-wrap" }, [h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch }), knowledgeCard]);
1565
+ return h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch });
1000
1566
  }
1001
1567
  return h("div", { key: "content" }, [
1002
1568
  vm.showTiles
@@ -1011,7 +1577,6 @@ function DreamPanel() {
1011
1577
  h(MoodCard, { key: "mood", vm: vm }),
1012
1578
  h(LessonCard, { key: "lessons", vm: vm }),
1013
1579
  ]),
1014
- knowledgeCard,
1015
1580
  h("div", { className: "dshd-listhead", key: "listhead" }, [
1016
1581
  h("h4", { key: "t" }, query === "" && filterCount === 0 ? "梦境时间线" : (query === "" ? "筛选结果" : "搜索结果")),
1017
1582
  h("span", { className: "dshd-hint", key: "n" }, listHintOf(query, vm.count, filterCount, visibleDreams.length)),
@@ -1031,10 +1596,13 @@ function DreamPanel() {
1031
1596
  return h("div", { className: "dshd-root" + (privacy === true ? " dshd-privacy" : ""), key: "root" }, [
1032
1597
  h("div", { className: "dshd-shell", key: "shell" }, [
1033
1598
  errorBox,
1599
+ h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": "梦境面板视图" }, [["journal", "日记"], ["knowledge", "经验"], ["bridge", "写入记录"]].map(function (item) {
1600
+ return h("button", { key: item[0], type: "button", className: "dshd-chipbtn" + (activeTab === item[0] ? " on" : ""), "aria-pressed": activeTab === item[0], onClick: function () { setFilterOpen(false); setActiveTab(item[0]); } }, item[1]);
1601
+ })),
1034
1602
  toolbar,
1035
1603
  privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。") : null,
1036
1604
  body(),
1037
- h("div", { className: "dshd-footer", key: "footer" }, [
1605
+ activeTab !== "journal" ? null : h("div", { className: "dshd-footer", key: "footer" }, [
1038
1606
  h("span", { key: "l" }, footerTextOf(query, limit, vm.total, vm.count)),
1039
1607
  h("span", { className: "dshd-spacer", key: "sp" }),
1040
1608
  canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? "加载中…" : "显示更早的梦") : null,
@@ -1057,7 +1625,6 @@ const CSS = [
1057
1625
  ".dshd-root{color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:1.6}",
1058
1626
  ".dshd-shell{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.08));border-radius:14px;padding:14px 16px;container-type:inline-size}",
1059
1627
  ".dshd-sub{color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px;margin:4px 0 12px}",
1060
- "@container (max-width:620px){.dshd-two{grid-template-columns:1fr}.dshd-toolbar .dshd-hint{display:none}}",
1061
1628
  ".dshd-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px}",
1062
1629
  ".dshd-search{position:relative;display:flex;align-items:center;flex:1;min-width:180px}",
1063
1630
  ".dshd-search svg{position:absolute;left:9px;color:var(--dsw-alias-label-tertiary,#6b7280);pointer-events:none}",
@@ -1165,7 +1732,30 @@ const CSS = [
1165
1732
  ".dshd-kempty-body{margin:0 auto 5px;max-width:460px;font-size:12px}",
1166
1733
  ".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
1734
  ".dshd-privacy .dshd-kbody{filter:blur(4px)}",
1168
- ".dshd-privacy .dshd-knowledge.dshd-peek .dshd-kbody{filter:none}"
1735
+ ".dshd-privacy .dshd-knowledge.dshd-peek .dshd-kbody{filter:none}",
1736
+ ".dshd-kcmd{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}",
1737
+ ".dshd-tabs{display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap}",
1738
+ ".dshd-action{font-weight:500;white-space:pre-wrap}",
1739
+ ".dshd-kbody details summary,.dshd-bridge-row details summary{cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}",
1740
+ ".dshd-reviewform{margin-top:8px;padding:9px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px}",
1741
+ ".dshd-reviewform label{display:flex;flex-direction:column;gap:3px;margin-bottom:6px;font-size:12px}",
1742
+ ".dshd-reviewform .dshd-btn,.dshd-feedback .dshd-btn{margin:4px 5px 0 0}",
1743
+ ".dshd-diff{white-space:pre-wrap;overflow-wrap:anywhere;max-height:320px;overflow:auto;padding:10px;border-radius:6px;font:11px/1.65 ui-monospace,Consolas,monospace;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04));color:inherit}",
1744
+ ".dshd-feedback{margin-top:8px;font-size:12px}.dshd-feedback summary{cursor:pointer}.dshd-feedback label{display:flex;flex-direction:column;gap:4px;margin-top:8px}.dshd-feedback textarea{font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:6px;padding:6px;max-width:100%;box-sizing:border-box}",
1745
+ ".dshd-sources{padding:6px 0}.dshd-sources details{margin-top:6px}.dshd-reviewform{min-width:0}",
1746
+ ".dshd-privacy .dshd-feedback,.dshd-privacy .dshd-diff{filter:blur(4px)}.dshd-privacy .dshd-knowledge.dshd-peek .dshd-feedback,.dshd-privacy .dshd-knowledge.dshd-peek .dshd-diff{filter:none}",
1747
+ ".dshd-reviewform select,.dshd-reviewform textarea,.dshd-kcard select{background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:6px;max-width:100%;padding:4px;font:inherit;box-sizing:border-box}",
1748
+ ".dshd-copywrap{display:inline-flex;flex-direction:column;gap:3px;max-width:100%;align-items:flex-start}",
1749
+ ".dshd-cmd{display:block;font-family:ui-monospace,Consolas,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04));border-radius:6px;padding:4px 7px;user-select:text;cursor:text;color:inherit;max-width:100%}",
1750
+ ".dshd-holdreason{font-size:11px;color:var(--dsw-alias-state-warn-primary,#f59e0b);margin-top:4px}",
1751
+ ".dshd-bridge{background:var(--dsw-alias-bg-layer-1,#fff);border-radius:9px;padding:8px 10px;border-left:3px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));min-width:0}",
1752
+ ".dshd-bridge-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
1753
+ ".dshd-bridge-target{font-size:12px;font-weight:600;word-break:break-all;min-width:0}",
1754
+ ".dshd-bridge-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:3px}",
1755
+ ".dshd-rollback-ok{color:var(--dsw-alias-state-success-primary,#16a34a)}",
1756
+ ".dshd-rollback-no{color:var(--dsw-alias-label-tertiary,#6b7280)}",
1757
+ "@container (max-width:620px){.dshd-two{grid-template-columns:1fr}.dshd-toolbar .dshd-hint{display:none}}",
1758
+ "@media(max-width:640px){[role=dialog]:has(.dshd-root){display:flex;flex-direction:column}[role=dialog]:has(.dshd-root)>nav{display:flex;flex-direction:row;align-items:center;flex-shrink:0;gap:6px;width:auto;max-width:100%;height:auto;padding:10px;overflow-x:auto;border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12))}[role=dialog]:has(.dshd-root)>nav>*{flex-shrink:0}[role=dialog]:has(.dshd-root)>nav>div:has(>button){display:flex;flex-direction:row;gap:6px}[role=dialog]:has(.dshd-root)>nav button{width:auto;flex-shrink:0}[role=dialog]:has(.dshd-root)>div{min-width:0;min-height:0;flex:1}.dshd-reviewform,.dshd-feedback{max-width:100%}}"
1169
1759
  ].join("");
1170
1760
  /* @ui-css-end */
1171
1761
 
@@ -1209,6 +1799,23 @@ exports.__internals = {
1209
1799
  KNOWLEDGE_ROUTE: KNOWLEDGE_ROUTE,
1210
1800
  KNOWLEDGE_LIMIT: KNOWLEDGE_LIMIT,
1211
1801
  fetchKnowledge: fetchKnowledge,
1802
+ fetchBridgeRecords: fetchBridgeRecords,
1803
+ postAction: postAction,
1804
+ copyText: copyText,
1805
+ normalizeBridgeRecords: normalizeBridgeRecords,
1806
+ skippedReasonLabel: skippedReasonLabel,
1807
+ holdReasonOf: holdReasonOf,
1808
+ bridgeableOf: bridgeableOf,
1809
+ reviewActionFor: reviewActionFor,
1810
+ reviewCommandOf: reviewCommandOf,
1811
+ bridgePreviewCommandOf: bridgePreviewCommandOf,
1812
+ bridgeRollbackCommandOf: bridgeRollbackCommandOf,
1813
+ shortHash: shortHash,
1814
+ bridgeActionLabel: bridgeActionLabel,
1815
+ bridgeEmptyState: bridgeEmptyState,
1816
+ reviewQueueEmptyState: reviewQueueEmptyState,
1817
+ BRIDGE_ROUTE: BRIDGE_ROUTE,
1818
+ BRIDGE_LIMIT: BRIDGE_LIMIT,
1212
1819
  normalizeKnowledge: normalizeKnowledge,
1213
1820
  normalizeLessonState: normalizeLessonState,
1214
1821
  applicabilityTextOf: applicabilityTextOf,