@stardustlc/dsh-dream 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0(2026-10-02)
4
+
5
+ - 设置页可直接采纳、驳回、重新送审、标记待复核和处理冲突;页面操作记录为 human,模型工具继续记录为 model。保留 revision 校验,失败时保留填写内容。
6
+ - 展开关联来源可查看核验状态、摘要、记录时间、会话序号和来源校验值;读到来源不等同于证明结论正确。
7
+ - 项目经验可在面板查看 AGENTS.md 差异后应用,预览绑定经验版本与文件哈希,10 分钟失效。写入记录提供确认回滚,管理块外的人工内容保留。
8
+ - 新增独立使用反馈(有用 / 不适用及场景备注),不自动采纳、变更状态或提高证据数量;并发校验、重复请求幂等,明细保留最近 20 次,历史在事件流中。
9
+ - 写操作使用 DSH 0.2.0-rc.2 的认证 Connection 通道,并检查 JSON、来源和请求大小;旧宿主保留对话命令入口。修复真实宿主内部请求地址引起的正常操作拒绝。
10
+ - 窄窗口打开 Dream 时把宿主设置导航改为顶部横向导航,避免面板被压成单字列。详见 [验收范围](docs/validation/0.7.0.md)。
11
+
3
12
  ## 0.6.0(2026-09-30)
4
13
 
5
14
  - **经验来源核验**:工具读取实际会话记录或工作目录内的本地产物,核对项目、角色、记录序号、引用和哈希;伪造 read 或无法定位的来源降为 claimed,并给出原因。新经验先进入候选;读到用户纠正不再自动冒充用户采纳。
package/README.en.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  # dsh-dream
6
6
 
7
+ ## 0.7.0 update (2026-10-02)
8
+
9
+ - Direct review buttons, conflict resolution with a required reason, and expandable source records. UI decisions record `human`; model tools still record `model`. Revision conflicts preserve entered text.
10
+ - Project lessons can preview the exact AGENTS.md changes, apply the approved preview, and roll back the managed block while preserving manual content outside it. Previews expire after ten minutes and check both lesson revision and file hash. Lessons without a project directory keep the project-chat preview command.
11
+ - Separate useful / not-applicable feedback with optional context, idempotent retries, and the latest twenty details. Feedback does not automatically accept, reject, validate, or add evidence to a lesson.
12
+ - Mutations use Harness 0.2.0-rc.2's authenticated Connection Fetch carrier. Read-only endpoints and command fallbacks remain. A Dream-scoped narrow-window layout prevents the settings navigation from squeezing the content into single-character columns.
13
+ - See [0.7.0 validation](docs/validation/0.7.0.md) for browser, automatic-test and native Desktop boundaries.
14
+
7
15
  ## 0.6.0 update (2026-09-30)
8
16
 
9
17
  - **Source verification and review**: reads actual session records or bounded project artifacts. Unverifiable claims stay claimed with a reason. New lessons start as candidates, including user corrections.
@@ -59,18 +67,18 @@ Restart the web service afterwards. The dream journal lives at `~/.dsh/.dsh-drea
59
67
 
60
68
  ## Dream journal panel (0.4.0+)
61
69
 
62
- Once installed, DSH's **Settings →「梦境日记」(Dream journal)** grows a read-only panel (Web profile only; headless hosts keep the tools and the skill without a settings page).
70
+ Once installed, DSH's **Settings →「梦境日记」(Dream journal)** provides Journal, Lessons and Write log views. From 0.7.0, reviews and feedback run through the authenticated Connection carrier in Harness 0.2.0-rc.2. Headless hosts keep the tools and skill without a settings page.
63
71
 
64
72
  - **What it shows**: totals, a mood-square strip (never empty for a single dream, wraps when there are many) and the lessons board at the top; below that, a card timeline, newest first, with timestamp, mood, reflection body and lessons. Reflections are expanded by default and collapse to 4 lines with a show-more control beyond 4 lines or 220 characters.
65
73
  - **Lessons board**: ranked by journal occurrences and recency; 3 or more occurrences are labelled common lessons. Frequency does not establish correctness, importance or bridge eligibility. Technical lessons need their own sources, review and a write preview.
66
74
  - **Search**: filters by keyword across reflections and lessons, case-insensitively. Mood distribution and the lessons board are computed over the full journal; the timeline is paged by search and limit (latest 50 by default).
67
75
  - **Filtering and older dreams**: from 8 dreams on, a Filter menu appears (by mood, or only dreams with lessons; it applies to the loaded window and says so). When 50 dreams are loaded and more exist, the footer offers "show older dreams", which fetches up to the 500-dream route cap in one go.
68
- - **Read-only**: the panel only reads from the plugin's own GET /_dsh/dsh-dream/journal (loopback-only; non-GET → 405, non-local Host → 403). There is no write action; saving, listing and recalling dreams still happen in chat via dream_save / dream_journal / dream_recall, and the panel never modifies the journal file.
76
+ - **Journal reads**: GET /_dsh/dsh-dream/journal is loopback-only; non-GET → 405, non-local Host → 403. Saving, listing and recalling dreams still happen in chat via dream_save / dream_journal / dream_recall. Panel reviews update knowledge events; previews do not write files.
69
77
  - **Data file**: the panel renders the journal itself — ~/.dsh/.dsh-dream/dreams.jsonl by default ($DSH_HOME/.dsh-dream/dreams.jsonl; an explicit journalDir wins). The storage format is unchanged: one JSON object per line, safe to remove or read with dream_journal.
70
78
  - **Privacy**: read-only routes mask text again while preserving validated lesson, evidence and backup references for commands. The UI reads local same-origin routes and does not upload to third-party services. Privacy blur is a display effect; DOM text and local logs remain plaintext.
71
79
  - **Before the first dream**: the panel shows an empty state pointing you to ask the agent to「做个梦」; a single dream renders as one card, with neither the mood strip nor the lessons board left empty and no misleading charts.
72
- - **Lessons view**: usable lessons and the review queue appear separately; rejected history is optional. Cards show when/action directly, with expandable scope, versions, exceptions and evidence. Review forms generate complete commands to paste into DSH chat; refresh afterwards. The panel itself never writes files.
73
- - **Write log and rollback (M2)**: `dream_bridge apply` appends an application record to `<journalDir>/bridge/records.jsonl` (target, time, lessonId@revision, before/after hashes, original managed block); the panel's "Write log" shows it read-only, and only reports "rollbackable" while the current managed block still matches that apply.
80
+ - **Lessons view**: usable lessons and the review queue appear separately; rejected history is optional. Review buttons save directly with revision checks. Conflict resolution requires a reason. Source records can be expanded; useful / not-applicable feedback is recorded independently of review and validation. Command fallbacks remain available.
81
+ - **Write log and rollback**: `dream_bridge apply` appends an application record with the target, time, lesson revisions, hashes and original managed block. The panel provides a confirmed rollback action while the current block still matches. Text outside the managed block remains intact; externally changed blocks are not overwritten.
74
82
 
75
83
  ## Tools
76
84
 
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  # dsh-dream
4
4
 
5
+ ## 0.7.0 更新(2026-10-02)
6
+
7
+ - 设置页直接采纳、驳回、标记待复核和处理冲突;关联来源可展开查看。页面审阅记录为人工操作,模型调用仍记录为模型。
8
+ - 带项目目录的经验可预览实际规则差异后应用,并在写入记录确认回滚。旧预览不能覆盖已变化的经验或文件;块外手工内容保留。
9
+ - 独立记录“这次有用 / 这次不适用”和场景备注,不自动改变审阅状态。失败时保留输入,重复反馈请求不会重复计数。
10
+ - 窄窗口使用顶部横向设置导航;写操作通过官方 DSH 0.2.0-rc.2 认证通道。验收范围见 [0.7.0 记录](docs/validation/0.7.0.md)。
11
+
5
12
  ## 0.6.0 更新(2026-09-30)
6
13
 
7
14
  - **来源核验与独立审阅**:插件实际读取会话记录或项目内产物核对来源;无法核对时降为 claimed 并说明原因。新经验先进入候选,用户纠正原文也需要单独审阅。
@@ -66,18 +73,19 @@ dsh plugin --profile web remove @stardustlc/dsh-dream
66
73
 
67
74
  ## 梦境日记面板(0.4.0+)
68
75
 
69
- 装好后,DSH 的「设置 → 梦境日记」里会多出一节**只读**面板(Web profile 专用;headless 宿主没有设置页,工具与技能照常可用)。
76
+ 装好后,DSH 的「设置 → 梦境日记」提供日记、经验与写入记录。0.7.0 起可以直接审阅和反馈;写操作使用 DSH 0.2.0-rc.2 的认证 Connection 通道。headless 宿主没有设置页,工具与技能照常可用。
70
77
 
71
78
  - **看什么**:顶部是梦境总数、心境方块序列(单条时不空,多条时自动换行)与教训榜;下面是卡片式时间线,按时间倒序,逐条显示时间、心境、反思正文与教训列表。反思正文默认展开,超过 4 行或 220 字时折叠为 4 行并给出「展开全文」。
72
79
  - **教训榜**:按日记出现次数与最近时间排序,≥3 次标为“常见教训”;频率不代表正确性、重要度或桥接资格。技术经验必须另行提交来源、审阅,再预览写入规则。
73
80
  - **搜索**:按关键词过滤,命中反思与教训,不区分大小写。心境分布与教训榜基于**全量日记**,时间线按搜索与条数上限(默认最近 50 场)展示。
74
81
  - **筛选与更早的梦**:≥8 场时出现「筛选 ▾」(按心境、只看有教训的梦,作用于已加载窗口并在弹层注明);加载满 50 场且总梦数更多时,页脚提供「显示更早的梦」,点击后一次拉满到 500 场(路由上限)。
75
- - **只读**:面板只从插件自己的 GET /_dsh/dsh-dream/journal 读取数据(仅回环地址可达;非 GET → 405,非本机 Host → 403),没有任何写操作。记梦、翻梦、忆梦仍在对话里通过 dream_save / dream_journal / dream_recall 完成,面板不会修改你的日记文件。
82
+ - **日记读取**:GET /_dsh/dsh-dream/journal 仅回环地址可达;非 GET → 405,非本机 Host → 403。记梦、翻梦、忆梦仍通过 dream_save / dream_journal / dream_recall 完成。面板审阅只更新经验事件,预览本身不写文件。
76
83
  - **数据文件**:面板读的就是梦境日记本身,默认 ~/.dsh/.dsh-dream/dreams.jsonl($DSH_HOME/.dsh-dream/dreams.jsonl;配置了 journalDir 时以配置为准)。存储格式不变,仍是每行一条 JSON 的 JSONL,可直接删除或用 dream_journal 读取。
77
84
  - **隐私**:只读路由会再次脱敏正文;已校验的内部经验、证据与备份编号保留以供操作。界面只访问本机同源路由,不上传第三方服务。“隐私模式”仅模糊显示,DOM 仍有文本;日志本身是明文。
78
85
  - **还没有梦时**:面板显示空态,提示对 agent 说「做个梦」——它会回放最近的会话、反思之后把第一条梦写进这里;只有一场梦时按单条卡片展示,心境方块与教训榜都不留空、不画失真图表。
79
- - **经验视图**:可用经验与待核实队列分开,已驳回历史按需查看;直接显示“何时用 / 怎么做”,适用范围、版本、例外与证据可以展开。审阅表单生成完整命令,复制后粘贴到 DSH 对话执行,再刷新查看结果;不会直接写文件。
80
- - **写入记录与回滚(M2)**:`dream_bridge apply` 在 `<journalDir>/bridge/records.jsonl` 追加应用记录(目标、时间、lessonId@revision、前后哈希、原管理块),面板「写入记录」只读展示;当前管理块仍匹配该次 apply 时才显示「可回滚」。
86
+ - **经验视图**:可用经验与待核实队列分开,已驳回历史按需查看。来源记录可展开查看核验信息;采纳、驳回、重新送审和标记待复核可以直接点击。处理冲突须选择对方经验和填写依据。每次操作检查版本,冲突时保留输入并提供刷新;对话命令入口仍可使用。
87
+ - **使用反馈**:展开“使用反馈”记录这次有用或不适用,并可填写场景。反馈不会自动改变审阅状态、证据或最后核验时间;最近 20 条明细可恢复,事件流保留历史。
88
+ - **规则变更与回滚**:带项目目录的可用经验提供“预览规则变更”,展示实际目标与新增/删除差异后才可应用。此预览按所选经验重建 Dream 管理块,保留块外内容;经验版本或文件哈希变化时拒绝应用。没有项目目录的经验须在对应项目对话中使用预览命令。写入记录展示备份与“回滚这次变更”;当前管理块仍匹配该次 apply 时才允许回滚。
81
89
 
82
90
  ## 工具一览
83
91
 
@@ -115,7 +123,7 @@ dream_review { action: "accept", lessonId: "lesson-…", expectedRevision: 1 }
115
123
  - `apply`:必须带预览的 `expectedSha256`(新文件为 null),建议同时带 `previewId`,文件被并发修改时返回 `conflict` 且零写入;只替换 Dream 管理块(`<!-- dsh-dream:start/end -->`),保留块外文字、BOM 与换行风格,先持久化 pending 恢复记录再原子替换文件,最后记录 applied;
116
124
  - `rollback`:用 backupId 定位记录,只在当前管理块仍等于该次 apply 结果时回滚,保留块外后来追加的人工内容。
117
125
 
118
- 缺省不传 `mode` 保持 0.5.x 直写并返回弃用提示;建议新流程一律显式 `preview → apply`。只有 `usable`、scope 匹配当前项目(或显式 global 且 review.decision 为 accepted)的经验可桥接,频次不再准入;写入内容统一过 `mask.ts`。面板的「写入记录」只读展示这些记录与可否回滚。
126
+ 缺省不传 `mode` 保持 0.5.x 直写并返回弃用提示;建议新流程一律显式 `preview → apply`。只有 `usable`、scope 匹配当前项目(或显式 global 且 review.decision 为 accepted)的经验可桥接,频次不再准入;写入内容统一过 `mask.ts`。面板的「写入记录」展示备份与可否回滚,0.7.0 起提供确认回滚按钮。
119
127
 
120
128
  ## 做梦协议(随包技能)
121
129
 
package/lib/client.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // 改完用 node --check lib/client.js 自检(CI 里也会跑这一步)。
4
4
  //
5
5
  // 面板做四件事:把梦境日记(dreams.jsonl)画成人能看懂的样子 —— 概览、心境方块、
6
- // 教训榜、按时间倒序的梦卡片;只读展示技术经验(<journalDir>/knowledge/);
7
- // 只读展示「待核实」(candidate/disputed/stale + 被扣下的原因)与「写入记录」
6
+ // 教训榜、按时间倒序的梦卡片;展示与审阅技术经验(<journalDir>/knowledge/);
7
+ // 展示「待核实」(candidate/disputed/stale + 被扣下的原因)与「写入记录」
8
8
  //(<journalDir>/bridge/ 的桥接应用记录)。
9
9
  // 数据来自插件自己的同源只读路由 GET /_dsh/dsh-dream/journal、/_dsh/dsh-dream/knowledge
10
10
  // 与 /_dsh/dsh-dream/bridge(由 src/index.ts 挂载);前端不缓存业务数据,刷新即见。
@@ -21,6 +21,7 @@ const h = React.createElement;
21
21
 
22
22
  // 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
23
23
  const ROUTE = "_dsh/dsh-dream/journal";
24
+ const ACTION_ROUTE = "api/dsh-dream/actions";
24
25
  const KNOWLEDGE_ROUTE = "_dsh/dsh-dream/knowledge";
25
26
  const BRIDGE_ROUTE = "_dsh/dsh-dream/bridge";
26
27
  const KNOWLEDGE_LIMIT = 50;
@@ -147,6 +148,24 @@ async function fetchBridgeRecords() {
147
148
  };
148
149
  }
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
+
150
169
  /** 复制命令到剪贴板;没有 clipboard / 被拒绝时返回 false,调用方回退为可选中文本。 */
151
170
  async function copyText(text) {
152
171
  const value = String(text);
@@ -326,6 +345,9 @@ function normalizeKnowledge(value, now, limit) {
326
345
  stateClass: lessonStateClass(state),
327
346
  scopeLabel: trimmedText(raw.scopeLabel) || "范围未标注",
328
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: [] },
329
351
  lastValidatedAt: lastValidatedAt,
330
352
  lastText: lastValidatedAt === "" ? "尚未核验" : relativeTime(lastValidatedAt, now),
331
353
  lastTitle: lastValidatedAt === "" ? "没有 lastValidatedAt(从未核验)" : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
@@ -916,38 +938,112 @@ function CopyCommand(props) {
916
938
  function ReviewControls(props) {
917
939
  const row = props.row;
918
940
  const [open, setOpen] = useState(false);
919
- const [action, setAction] = useState(function () { return reviewActionFor(row.state); });
941
+ const [action, setAction] = useState(reviewActionFor(row.state));
920
942
  const [resolution, setResolution] = useState("prefer");
921
943
  const [note, setNote] = useState("");
944
+ const [feedbackNote, setFeedbackNote] = useState("");
945
+ const [feedbackRetry, setFeedbackRetry] = useState(null);
922
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);
923
950
  const commands = row.state === "rejected" ? [["reopen", "重新送审"]]
924
951
  : row.state === "disputed" ? [["resolve-conflict", "处理冲突"], ["reject", "驳回这条"]]
952
+ : row.state === "usable" ? [["mark-stale", "标记待复核"], ["reject", "驳回"]]
925
953
  : [["accept", "采纳"], ["reject", "驳回"], ["mark-stale", "标记待复核"]];
926
- const command = reviewCommandOf(row, { action: action, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] });
927
- return h("div", { className: "dshd-review", onClick: function (event) { event.stopPropagation(); } }, [
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(); } }, [
928
979
  h("div", { className: "dshd-kcmd", key: "buttons" }, [
929
- h(Btn, { key: "review", onClick: function () { setOpen(!open); }, "aria-expanded": open }, open ? "收起审阅" : "审阅"),
930
- row.bridgeable === true ? h(CopyCommand, { key: "preview", command: bridgePreviewCommandOf(row.id, row.projectId, row.workspaceRoot), label: "复制预览命令", title: "粘贴到对应项目对话,先查看写入差异" }) : null,
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,
931
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,
932
989
  open ? h("div", { className: "dshd-reviewform", key: "form", onKeyDown: function (event) {
933
- if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); setOpen(false); }
990
+ if (event.key === "Escape" && !pending) { event.preventDefault(); event.stopPropagation(); setOpen(false); }
934
991
  } }, [
935
- h("label", { key: "action" }, ["审阅动作", h("select", { "aria-label": "审阅动作", value: action, onChange: function (event) { setAction(event.target.value); } }, commands.map(function (item) { return h("option", { value: item[0], key: item[0] }, item[1]); }))]),
936
- action === "resolve-conflict" ? h("div", { key: "conflict" }, [
937
- h("label", { key: "against" }, ["冲突经验", h("select", { "aria-label": "冲突经验", 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); }))]),
938
- h("label", { key: "resolution" }, ["处理方式", h("select", { "aria-label": "处理方式", value: resolution, onChange: function (event) { setResolution(event.target.value); } }, [
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); } }, [
939
996
  h("option", { key: "prefer", value: "prefer" }, "采用这条,驳回对方"),
940
997
  h("option", { key: "drop", value: "drop" }, "放弃这条,保留对方"),
941
998
  h("option", { key: "merge", value: "merge" }, "合并到这条,对方留作历史"),
942
999
  ])]),
943
- h("label", { key: "note" }, ["处理依据", h("textarea", { "aria-label": "处理依据", rows: 2, value: note, placeholder: "说明哪个来源支持这次决定…", onChange: function (event) { setNote(event.target.value); } })]),
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); } })]),
944
1001
  ]) : null,
945
- command === "" ? h("div", { className: "dshd-note", key: "incomplete" }, row.revision < 1 ? "记录缺少版本,请刷新后重试。" : "选择冲突经验并填写处理依据后,才能生成命令。") : h("div", { key: "command" }, [
946
- h("code", { className: "dshd-cmd", tabIndex: 0, key: "text" }, command),
947
- h(CopyCommand, { key: "copy", command: command, label: "复制审阅命令" }),
948
- ]),
949
- h("div", { className: "dshd-note", key: "help" }, "复制后粘贴到 DSH 对话,请助手执行;完成后刷新。命令带版本校验,记录变化时会拒绝旧命令。"),
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 ? "正在回滚…" : "确认回滚管理块"),
950
1045
  ]) : null,
1046
+ error ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, error) : null,
951
1047
  ]);
952
1048
  }
953
1049
 
@@ -977,10 +1073,20 @@ function KnowledgeRow(props) {
977
1073
  row.applicability !== "" ? h("div", { key: "applicability" }, "适用:" + row.applicability) : null,
978
1074
  row.exceptions.length > 0 ? h("div", { key: "exceptions" }, "例外:" + row.exceptions.join(";")) : null,
979
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
+ })),
980
1086
  ]),
981
1087
  props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, "为什么被扣下:" + row.holdReasonLabel) : null,
982
1088
  ]),
983
- h(ReviewControls, { row: row, key: row.id + "@" + row.revision }),
1089
+ h(ReviewControls, { row: row, key: row.id + "@" + row.revision, onChanged: props.onChanged }),
984
1090
  ]);
985
1091
  }
986
1092
 
@@ -1027,7 +1133,7 @@ function KnowledgeCard(props) {
1027
1133
  ])]),
1028
1134
  reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? "暂无可用经验;待核实的记录在下方。" : "没有已驳回记录。") : null,
1029
1135
  h("div", { className: "dshd-krows", key: "rows" }, reviewed.slice(0, pageSize).map(function (row, index) {
1030
- return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, privacy: props.privacy === true });
1136
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true });
1031
1137
  })),
1032
1138
  reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多经验") : null,
1033
1139
  vm.shown < vm.total
@@ -1057,7 +1163,7 @@ function ReviewQueueCard(props) {
1057
1163
  h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1058
1164
  ]);
1059
1165
  }
1060
- if (props.loading === true) {
1166
+ if (props.loading === true && rows.length === 0) {
1061
1167
  return h("section", { className: "dshd-card dshd-kcard" }, [
1062
1168
  head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1063
1169
  ]);
@@ -1070,10 +1176,10 @@ function ReviewQueueCard(props) {
1070
1176
  h("p", { className: "dshd-kempty-body", key: "b" }, reviewQueueEmptyState()),
1071
1177
  ])
1072
1178
  : h("div", { className: "dshd-krows", key: "rows" }, rows.slice(0, pageSize).map(function (row, index) {
1073
- return h(KnowledgeRow, { key: row.id !== "" ? row.id : "held-" + String(index), row: row, privacy: props.privacy === true, showHoldReason: true });
1179
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "held-" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true, showHoldReason: true });
1074
1180
  })),
1075
1181
  rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多待核实经验") : null,
1076
- h("div", { className: "dshd-note", key: "note" }, "这些经验默认不会作为任务建议注入;扣留原因来自检索层的 skipped reason。复制审阅命令后到对话里执行,面板不会写文件。"),
1182
+ h("div", { className: "dshd-note", key: "note" }, "这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。"),
1077
1183
  ]);
1078
1184
  }
1079
1185
 
@@ -1130,11 +1236,11 @@ function BridgeLogCard(props) {
1130
1236
  h("code", { key: "id", className: "dshd-cmd" }, row.id),
1131
1237
  row.rollbackable === true ? h("code", { key: "command", className: "dshd-cmd", tabIndex: 0 }, bridgeRollbackCommandOf(row)) : null,
1132
1238
  ]),
1133
- row.rollbackable === true ? h(CopyCommand, { key: "rollback", label: "复制回滚命令", command: bridgeRollbackCommandOf(row), title: "粘贴到对应项目对话执行;只恢复管理块,保留块外人工内容" }) : null,
1239
+ row.rollbackable === true ? h(RollbackControl, { key: "rollback", row: row, onChanged: props.onChanged }) : null,
1134
1240
  ]);
1135
1241
  })),
1136
1242
  vm.shown < vm.total ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条记录。") : null,
1137
- h("div", { className: "dshd-note", key: "note" }, "只读展示:记录来自 dream_bridge 的 apply;回滚请按 dream_bridge 返回的备份标识在对话里执行,面板不会写文件。"),
1243
+ h("div", { className: "dshd-note", key: "note" }, "回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。"),
1138
1244
  ]);
1139
1245
  }
1140
1246
 
@@ -1212,6 +1318,7 @@ function DreamPanel() {
1212
1318
  const [queryInput, setQueryInput] = useState("");
1213
1319
  const [query, setQuery] = useState("");
1214
1320
  const [reloadToken, setReloadToken] = useState(0);
1321
+ const [actionNotice, setActionNotice] = useState("");
1215
1322
  const [now, setNow] = useState(function () { return new Date(); });
1216
1323
  const [privacy, setPrivacy] = useState(loadPrivacy);
1217
1324
  const [filters, setFilters] = useState({ moods: [], withLessons: false });
@@ -1312,6 +1419,7 @@ function DreamPanel() {
1312
1419
  };
1313
1420
  const clearFilters = function () { setFilters({ moods: [], withLessons: false }); };
1314
1421
  const showMore = function () { if (limit < MORE_LIMIT) setLimit(MORE_LIMIT); };
1422
+ const onChanged = function (message) { setActionNotice(message); setReloadToken(function (value) { return value + 1; }); };
1315
1423
  const showKnowledge = function () { if (knowledgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
1316
1424
  const knowledgeCard = h(KnowledgeCard, {
1317
1425
  key: "knowledge",
@@ -1321,6 +1429,7 @@ function DreamPanel() {
1321
1429
  empty: knowledgeError === null && knowledgeLoading !== true && knowledgeVm.rows.length === 0,
1322
1430
  privacy: privacy === true,
1323
1431
  onRetry: showKnowledge,
1432
+ onChanged: onChanged,
1324
1433
  });
1325
1434
  const reviewQueueCard = h(ReviewQueueCard, {
1326
1435
  key: "review-queue",
@@ -1329,6 +1438,7 @@ function DreamPanel() {
1329
1438
  error: knowledgeError,
1330
1439
  privacy: privacy === true,
1331
1440
  onRetry: showKnowledge,
1441
+ onChanged: onChanged,
1332
1442
  });
1333
1443
  const showBridge = function () { if (bridgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
1334
1444
  const bridgeLogCard = h(BridgeLogCard, {
@@ -1337,6 +1447,7 @@ function DreamPanel() {
1337
1447
  loading: bridgeLoading === true,
1338
1448
  error: bridgeError,
1339
1449
  onRetry: showBridge,
1450
+ onChanged: onChanged,
1340
1451
  });
1341
1452
 
1342
1453
  // 筛选 popover:点面板外任意处收起;纯 node 冒烟没有事件系统就跳过。
@@ -1441,8 +1552,8 @@ function DreamPanel() {
1441
1552
  ]);
1442
1553
 
1443
1554
  const body = function () {
1444
- if (activeTab === "knowledge") return h("div", { key: "knowledge-tab" }, [knowledgeCard, reviewQueueCard]);
1445
- if (activeTab === "bridge") return bridgeLogCard;
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]);
1446
1557
  if (loading === true && vm.count === 0) return h(Skeleton, { key: "skeleton" });
1447
1558
  if (vm.count === 0) {
1448
1559
  if (error !== null) return h("div", { key: "failed-wrap" }, [
@@ -1628,6 +1739,11 @@ const CSS = [
1628
1739
  ".dshd-kbody details summary,.dshd-bridge-row details summary{cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}",
1629
1740
  ".dshd-reviewform{margin-top:8px;padding:9px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px}",
1630
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}",
1631
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}",
1632
1748
  ".dshd-copywrap{display:inline-flex;flex-direction:column;gap:3px;max-width:100%;align-items:flex-start}",
1633
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%}",
@@ -1638,7 +1754,8 @@ const CSS = [
1638
1754
  ".dshd-bridge-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:3px}",
1639
1755
  ".dshd-rollback-ok{color:var(--dsw-alias-state-success-primary,#16a34a)}",
1640
1756
  ".dshd-rollback-no{color:var(--dsw-alias-label-tertiary,#6b7280)}",
1641
- "@container (max-width:620px){.dshd-two{grid-template-columns:1fr}.dshd-toolbar .dshd-hint{display:none}}"
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%}}"
1642
1759
  ].join("");
1643
1760
  /* @ui-css-end */
1644
1761
 
@@ -1683,6 +1800,7 @@ exports.__internals = {
1683
1800
  KNOWLEDGE_LIMIT: KNOWLEDGE_LIMIT,
1684
1801
  fetchKnowledge: fetchKnowledge,
1685
1802
  fetchBridgeRecords: fetchBridgeRecords,
1803
+ postAction: postAction,
1686
1804
  copyText: copyText,
1687
1805
  normalizeBridgeRecords: normalizeBridgeRecords,
1688
1806
  skippedReasonLabel: skippedReasonLabel,
package/lib/index.js CHANGED
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { resolveConfig } from './config.js';
10
10
  import { buildDreamTools } from './tools.js';
11
11
  import { installDreamWeb } from './web.js';
12
+ import { installDreamActions } from './web-actions.js';
12
13
  /** cordis 服务注入:要用 ctx.tools 与 ctx.skills。 */
13
14
  export const name = 'dream';
14
15
  export const inject = ['tools', 'skills'];
@@ -56,7 +57,8 @@ export function apply(ctx, config) {
56
57
  }
57
58
  // 面板同源路由:宿主有 webServer 才由 web.ts 用 ctx.inject 挂上;headless 夹具没有 inject 就跳过。
58
59
  if (typeof ctx.inject === 'function') {
59
- installDreamWeb(ctx, { journalDir: cfg.journalDir });
60
+ installDreamWeb(ctx, { journalDir: cfg.journalDir, config: cfg });
61
+ installDreamActions(ctx, { config: cfg });
60
62
  }
61
63
  if (typeof ctx.on === 'function') {
62
64
  ctx.on('dispose', () => {
@@ -59,6 +59,8 @@ export declare class KnowledgeStore {
59
59
  attachEvidence(id: string, inputs: EvidenceInput[], expectedRevision: number, idempotencyKey: string): Lesson;
60
60
  /** 按 patch 更新经验;revision 不符抛 KnowledgeError('revision')(details 带 currentRevision)且不写。 */
61
61
  updateLesson(id: string, patch: Partial<Lesson>, expectedRevision: number, idempotencyKey: string): Lesson;
62
+ /** 记录一次使用反馈,不改变审阅、状态、证据和最后核验时间。 */
63
+ recordFeedback(id: string, vote: 'useful' | 'not-applicable', note: string | undefined, expectedRevision: number, idempotencyKey: string): Lesson;
62
64
  /** 审阅经验:accepted → usable,rejected → rejected,unreviewed → candidate。 */
63
65
  reviewLesson(id: string, decision: ReviewDecision, expectedRevision: number, idempotencyKey: string, actor?: string, note?: string, resolution?: LessonReviewResolution): Lesson;
64
66
  /**
@@ -573,6 +573,41 @@ export class KnowledgeStore {
573
573
  return clone(this.appendLessonUpdate(state, lesson, clean, idempotencyKey, requestHash, touch));
574
574
  });
575
575
  }
576
+ /** 记录一次使用反馈,不改变审阅、状态、证据和最后核验时间。 */
577
+ recordFeedback(id, vote, note, expectedRevision, idempotencyKey) {
578
+ assertIdempotencyKey(idempotencyKey);
579
+ if (vote !== 'useful' && vote !== 'not-applicable')
580
+ throw new KnowledgeError('invalid', '反馈必须是 useful / not-applicable');
581
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 1)
582
+ throw new KnowledgeError('invalid', '反馈必须带有效的经验版本');
583
+ if (note !== undefined && (typeof note !== 'string' || note.length > 500))
584
+ throw new KnowledgeError('invalid', '反馈备注最多 500 字符');
585
+ const cleanNote = note === undefined || note.trim() === '' ? undefined : this.mask ? maskSecrets(note.trim()) : note.trim();
586
+ const requestHash = this.hashRequest({ operation: 'lesson.feedback', id, vote, note: cleanNote ?? null, expectedRevision });
587
+ return this.withLock((touch) => {
588
+ const state = this.loadCompleteState(touch);
589
+ const cached = state.idempotency.get(idempotencyKey);
590
+ if (cached !== undefined) {
591
+ this.assertSameRequest(cached, requestHash, idempotencyKey);
592
+ const result = this.resolveIdempotentLesson(state, idempotencyKey);
593
+ if (result === undefined)
594
+ throw new KnowledgeError('io', '无法恢复反馈结果');
595
+ return result;
596
+ }
597
+ const previous = state.lessons.get(id);
598
+ if (previous === undefined)
599
+ throw new KnowledgeError('invalid', '经验不存在:' + id);
600
+ if (previous.revision !== expectedRevision)
601
+ throw new KnowledgeError('revision', '经验已更新,请刷新后再反馈', { currentRevision: previous.revision });
602
+ const old = previous.feedback ?? { useful: 0, notApplicable: 0, recent: [] };
603
+ const feedback = {
604
+ useful: old.useful + (vote === 'useful' ? 1 : 0),
605
+ notApplicable: old.notApplicable + (vote === 'not-applicable' ? 1 : 0),
606
+ recent: [...old.recent, { vote, at: new Date().toISOString(), ...(cleanNote === undefined ? {} : { note: cleanNote }) }].slice(-20),
607
+ };
608
+ return clone(this.appendLessonUpdate(state, previous, { feedback }, idempotencyKey, requestHash, touch));
609
+ });
610
+ }
576
611
  /** 审阅经验:accepted → usable,rejected → rejected,unreviewed → candidate。 */
577
612
  reviewLesson(id, decision, expectedRevision, idempotencyKey, actor, note, resolution) {
578
613
  assertIdempotencyKey(idempotencyKey);
@@ -1419,6 +1454,11 @@ export class KnowledgeStore {
1419
1454
  const refs = [lesson.id, ...(lesson.review.resolution?.affectedIds ?? []), ...(lesson.review.resolution?.targetId ? [lesson.review.resolution.targetId] : [])];
1420
1455
  masked.review = { ...lesson.review, note: maskSecrets(lesson.review.note, refs) };
1421
1456
  }
1457
+ if (lesson.feedback !== undefined)
1458
+ masked.feedback = {
1459
+ ...lesson.feedback,
1460
+ recent: lesson.feedback.recent.map(item => ({ ...item, ...(item.note === undefined ? {} : { note: maskSecrets(item.note) }) })),
1461
+ };
1422
1462
  return masked;
1423
1463
  }
1424
1464
  maskEvidence(evidence) {
@@ -48,6 +48,8 @@ export interface Lesson {
48
48
  note?: string;
49
49
  resolution?: LessonReviewResolution;
50
50
  };
51
+ /** 使用反馈独立于审阅和证据;只保留最近 20 次明细,事件流保留历史。 */
52
+ feedback?: LessonFeedback;
51
53
  createdAt: string;
52
54
  updatedAt: string;
53
55
  lastValidatedAt?: string;
@@ -55,6 +57,15 @@ export interface Lesson {
55
57
  supersedes?: string[];
56
58
  conflictIds: string[];
57
59
  }
60
+ export interface LessonFeedback {
61
+ useful: number;
62
+ notApplicable: number;
63
+ recent: Array<{
64
+ vote: 'useful' | 'not-applicable';
65
+ at: string;
66
+ note?: string;
67
+ }>;
68
+ }
58
69
  /** 一条证据:只存脱敏摘要 + 定位信息。 */
59
70
  export interface Evidence {
60
71
  schemaVersion: 1;
package/lib/knowledge.js CHANGED
@@ -161,6 +161,28 @@ function readReview(value) {
161
161
  review.resolution = resolution;
162
162
  return review;
163
163
  }
164
+ function readFeedback(value) {
165
+ if (value === undefined)
166
+ return undefined;
167
+ if (!isRecord(value))
168
+ fail('feedback', '必须是对象');
169
+ for (const key of ['useful', 'notApplicable']) {
170
+ if (!Number.isSafeInteger(value[key]) || value[key] < 0)
171
+ fail('feedback.' + key, '必须是非负安全整数');
172
+ }
173
+ if (!Array.isArray(value.recent) || value.recent.length > 20)
174
+ fail('feedback.recent', '最多 20 条');
175
+ const recent = value.recent.map((item) => {
176
+ if (!isRecord(item) || (item.vote !== 'useful' && item.vote !== 'not-applicable'))
177
+ fail('feedback.recent.vote', '取值不合法');
178
+ const at = readIsoDate(item.at, 'feedback.recent.at', true);
179
+ const note = readString(item.note, 'feedback.recent.note');
180
+ if (note !== undefined && note.length > REVIEW_NOTE_MAX_LENGTH)
181
+ fail('feedback.recent.note', '超过长度上限');
182
+ return { vote: item.vote, at, ...(note === undefined ? {} : { note }) };
183
+ });
184
+ return { useful: value.useful, notApplicable: value.notApplicable, recent };
185
+ }
164
186
  /** 当前内存模型 / 写盘的 lesson schemaVersion。 */
165
187
  export const LESSON_SCHEMA_VERSION = 1;
166
188
  /** 已知迁移链占位:v2 落地时在此登记;存储层默认只识别不自动迁移。 */
@@ -247,6 +269,9 @@ export function validateLesson(value, options = {}) {
247
269
  lesson.reviewAfter = reviewAfter;
248
270
  if (supersedes !== undefined)
249
271
  lesson.supersedes = supersedes;
272
+ const feedback = readFeedback(value.feedback);
273
+ if (feedback !== undefined)
274
+ lesson.feedback = feedback;
250
275
  return lesson;
251
276
  }
252
277
  /** 校验并归一化一条证据;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
package/lib/tools.d.ts CHANGED
@@ -25,3 +25,5 @@ export interface DreamToolDefinition {
25
25
  export declare function buildDreamTools(config: ResolvedDreamConfig): DreamToolDefinition[];
26
26
  /** 把会话摘要拼成一段可读文本(供模型一次性阅读)。 */
27
27
  export declare function buildDigestText(digest: SessionDigest): string;
28
+ /** actor comes from the trusted caller, never from tool or browser arguments. */
29
+ export declare function executeDreamReview(cfg: ResolvedDreamConfig, rawArgs: unknown, exec: unknown, actor?: 'model' | 'human'): Promise<unknown>;