@stardustlc/dsh-dream 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2,15 +2,9 @@
2
2
  // 仓库里没有 src/client.ts,pnpm run build 也不会重写它。改面板请直接改这里,
3
3
  // 改完用 node --check lib/client.js 自检(CI 里也会跑这一步)。
4
4
  //
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 挂载);前端不缓存业务数据,刷新即见。
11
- // 经验区块只展示状态 / 条件 / 范围 / 适用与例外 / 证据摘要 / 最近核验;日志截断或坏行会明确提示,
12
- // 不展示权重、向量或原始 JSON。经验行只提供「复制审阅 / 预览命令」,写操作仍在对话工具里执行。
13
- // 隐私纪律:面板只读,不写文件、不上传;「隐私模式」只做显示层模糊,DOM 仍是明文。
5
+ // 日记、来源、审阅、反馈与规则变更共用同一份数据,界面语言跟随 DSH locale。
6
+ // 读取走同源只读路由,写操作走认证 Connection 通道;规则必须预览后应用。
7
+ // 日记、来源原文和人工输入保留原文;「隐私模式」仅模糊显示,DOM 仍是明文。
14
8
  window.__ModuleLoader__.load({ id: "@stardustlc/dsh-dream", factory: (require) => {
15
9
  var module = { exports: {} }; var exports = module.exports;
16
10
  "use strict";
@@ -19,8 +13,316 @@ const React = require("react");
19
13
  const { useState, useEffect } = React;
20
14
  const h = React.createElement;
21
15
 
16
+ // Interface language follows DSH; user content and stored records keep their original text.
17
+ let localeService;
18
+ const EN = Object.freeze({
19
+ "请求失败(HTTP ": "Request failed (HTTP ",
20
+ "服务器返回的不是 JSON(HTTP ": "The server did not return JSON (HTTP ",
21
+ "登录状态已失效,请重新打开 DSH 页面后重试。": "Your session has expired. Reopen DSH and try again.",
22
+ "当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。": "This host does not support this action. Update DSH or use the chat command below.",
23
+ "候选(未采纳)": "Candidate (not accepted)",
24
+ "可用": "Usable",
25
+ "有冲突": "Disputed",
26
+ "待复核": "Needs review",
27
+ "已驳回": "Rejected",
28
+ "未知状态": "Unknown state",
29
+ "平台 ": "Platform ",
30
+ ";等 ": "; and ",
31
+ " 条适用范围": " more conditions",
32
+ "范围未标注": "Scope not specified",
33
+ "暂无可展示的证据摘要": "No evidence summary available",
34
+ "尚未核验": "Not verified yet",
35
+ "没有 lastValidatedAt(从未核验)": "No lastValidatedAt (never verified)",
36
+ "未加载的经验(": "Lesson not loaded (",
37
+ "共 ": "Total: ",
38
+ " 条": " records",
39
+ "还没有经验": "No lessons yet",
40
+ "日志过大,仅回放最近一段事件": "The log is large; only recent events were replayed",
41
+ "有 ": "",
42
+ " 行损坏已跳过": " damaged lines were skipped",
43
+ ";events.jsonl 未改动。": "; events.jsonl was preserved.",
44
+ "还没有经验记录": "No lesson records yet",
45
+ "经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。": "Lessons are technical conclusions with evidence and conditions. Submit a candidate with dream_learn; records with sufficient evidence and a clear scope appear here.",
46
+ "旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。": "Journal observations do not become lessons automatically. Use dream_review to accept, reject or flag conflicts.",
47
+ "已驳回:不参与默认检索与注入": "Rejected: excluded from default retrieval and injection",
48
+ "待复核:条件或依赖可能已过期,默认不注入": "Needs review: conditions or dependencies may be outdated; not injected by default",
49
+ "冲突未解决:默认不注入,先 resolve-conflict": "Unresolved conflict: not injected by default; use resolve-conflict first",
50
+ "候选:可被检索到,但默认不作为任务建议注入": "Candidate: retrievable, but not injected as task advice by default",
51
+ "证据不足:没有独立证据,默认不注入": "Insufficient independent evidence: not injected by default",
52
+ "版本不匹配:当前依赖版本不满足适用范围": "Version mismatch: the dependency version is outside the applicable range",
53
+ "超出条数预算,本次未取回": "Not retrieved: record limit reached",
54
+ "超出字符预算:按排名整条装入,遇到第一条放不下即停": "Character limit reached: complete records are added by rank until the next one cannot fit",
55
+ "超出字符预算,本次未取回": "Not retrieved: character limit reached",
56
+ "新建": "Created",
57
+ "替换管理块": "Replaced managed block",
58
+ "追加管理块": "Appended managed block",
59
+ "未改动": "Unchanged",
60
+ "回滚": "Rolled back",
61
+ "未知动作": "Unknown action",
62
+ "时间未知": "Unknown time",
63
+ "没有记录时间": "No timestamp recorded",
64
+ "目标未记录": "Target not recorded",
65
+ "没有经验引用": "No lesson references",
66
+ "可回滚": "Rollback available",
67
+ "不可回滚": "Rollback unavailable",
68
+ "还没有写入记录": "No rule changes yet",
69
+ "写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。": "Changes are recorded by dream_bridge preview → apply. This log shows the target, time, lesson revisions and before/after hashes.",
70
+ "在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。": "Preview an eligible project lesson, review the changes and apply them. Chat commands are also available.",
71
+ "没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。": "Candidates, disputed lessons and lessons needing review appear here, with an explanation of why they are held.",
72
+ "刚刚": "Just now",
73
+ " 分钟前": " min ago",
74
+ " 小时前": " h ago",
75
+ " 天前": " days ago",
76
+ "今天": "Today",
77
+ "昨天": "Yesterday",
78
+ "筛选后 ": "Filtered: ",
79
+ " 场": " dreams",
80
+ "」命中 ": "” matches ",
81
+ " 场梦": " dreams",
82
+ "筛选当前已加载的 ": "Filtering the ",
83
+ " 场 · 命中 ": " loaded dreams · Matches: ",
84
+ "新梦在前": "Newest first",
85
+ "(已达路由上限)": " (route limit reached)",
86
+ "最近 ": "Recent: ",
87
+ " 场 / 共 ": " dreams / Total: ",
88
+ "新梦在前 · 最多显示 ": "Newest first · Up to ",
89
+ "搜索结果 · 最多显示 ": "Search results · Up to ",
90
+ "本页内最近 ": "Most recent on this page: ",
91
+ "(统计未给 lastAt,按本页已加载的 ": "(lastAt is missing; calculated from the ",
92
+ " 场现算)": " dreams loaded on this page)",
93
+ "同一天": "Same day",
94
+ "约 ": "About ",
95
+ " 天": " days",
96
+ "第 1 场梦 · ": "First dream · ",
97
+ " · 心境「": " · Mood: “",
98
+ " 场梦 · 本页最近 ": " dreams · Loaded: ",
99
+ "最近:": "Latest: ",
100
+ "心境": "Mood",
101
+ "1 场梦,先不谈分布 —— 再做梦会累积。": "One dream so far. More dreams will build a mood history.",
102
+ "左 → 右:从早到晚": "Left → right: oldest to newest",
103
+ "教训榜": "Recurring observations",
104
+ "还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。": "No observations yet. Include lessons when recording dreams to track recurring observations.",
105
+ "常见教训": "Recurring observation",
106
+ "次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。": "Counts show journal frequency only; ≥3 occurrences marks a recurring observation. Verify sources and review lessons before writing project rules. Recency uses stats.lastAt, or the dreams loaded on this page when missing.",
107
+ "复制命令": "Copy command",
108
+ "已复制": "Copied",
109
+ "剪贴板不可用:请手动选中上面的命令复制。": "Clipboard unavailable. Select and copy the command above manually.",
110
+ "重新送审": "Reopen for review",
111
+ "处理冲突": "Resolve conflict",
112
+ "驳回这条": "Reject this lesson",
113
+ "标记待复核": "Mark for review",
114
+ "驳回": "Reject",
115
+ "采纳": "Accept",
116
+ "管理块内容已一致,无需写入。": "The managed block already matches; no write was needed.",
117
+ "请求超时,请先刷新确认结果后再重试。": "Request timed out. Refresh to check the result before retrying.",
118
+ "已更新「": "Updated the review state of “",
119
+ "」的审阅状态。": "”.",
120
+ "已记录使用反馈;「": "Feedback recorded; the review state of “",
121
+ "」的审阅状态保持原样。": "” is unchanged.",
122
+ "的操作": " — actions",
123
+ "收起审阅": "Hide review",
124
+ "审阅详情": "Review details",
125
+ "预览规则变更": "Preview rule changes",
126
+ "复制项目预览命令": "Copy project preview command",
127
+ "正在处理…": "Working…",
128
+ "已刷新经验列表。": "Lesson list refreshed.",
129
+ "刷新列表": "Refresh list",
130
+ "审阅动作": "Review action",
131
+ "冲突经验": "Conflicting lesson",
132
+ "处理方式": "Resolution",
133
+ "采用这条,驳回对方": "Accept this lesson and reject the other",
134
+ "放弃这条,保留对方": "Reject this lesson and keep the other",
135
+ "合并到这条,对方留作历史": "Merge into this lesson and retain the other as history",
136
+ "处理依据": "Reason for resolution",
137
+ "说明哪个来源支持这次决定…": "Explain which source supports this decision…",
138
+ "确认审阅": "Confirm review",
139
+ "在对话里处理": "Handle in chat",
140
+ "选择冲突经验并填写处理依据后即可处理。": "Select the conflicting lesson and provide a reason first.",
141
+ "复制审阅命令": "Copy review command",
142
+ "规则变更预览": "Rule change preview",
143
+ "目标:": "Target: ",
144
+ "管理块内容已经一致,无需更改。": "The managed block already matches; no changes are needed.",
145
+ "将按此预览重建 Dream 管理块,请核对新增和删除内容。块外内容保留,写入前创建回滚备份。": "Check the additions and deletions before rebuilding the Dream managed block. Content outside the block is preserved; a rollback backup is created before writing.",
146
+ "规则已应用,可在「写入记录」中回滚。": "Rules applied. You can roll back from the Rule changes tab.",
147
+ "应用此变更": "Apply these changes",
148
+ "取消预览": "Cancel preview",
149
+ "使用反馈 · 有用 ": "Usage feedback · Useful: ",
150
+ " · 不适用 ": " · Not applicable: ",
151
+ "场景备注(可选)": "Context note (optional)",
152
+ "反馈场景备注": "Feedback context note",
153
+ "这次在哪个场景使用…": "Where did you use this lesson…",
154
+ "这次有用": "Useful this time",
155
+ "这次不适用": "Not applicable this time",
156
+ "记录使用情况,供之后复核;采纳与驳回由审阅决定。": "Feedback records usage for future review. Acceptance and rejection remain review decisions.",
157
+ "有用": "Useful",
158
+ "不适用": "Not applicable",
159
+ "已恢复上一次的 Dream 管理块,块外内容保留。": "The previous Dream managed block was restored; content outside the block was preserved.",
160
+ "取消回滚": "Cancel rollback",
161
+ "回滚这次变更": "Roll back this change",
162
+ "将恢复「": "Restore the previous Dream managed block in “",
163
+ "」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。": "”. Content outside the block is preserved. Rollback is refused if the managed block has changed.",
164
+ "正在回滚…": "Rolling back…",
165
+ "确认回滚管理块": "Confirm block rollback",
166
+ "隐私模式:点击此条临时查看": "Privacy mode: click to reveal temporarily",
167
+ "最近核验:": "Last verified: ",
168
+ "何时用:": "When to use: ",
169
+ "未标注适用条件": "No conditions specified",
170
+ "怎么做:": "What to do: ",
171
+ "未标注具体动作": "No action specified",
172
+ "适用范围与证据": "Scope and evidence",
173
+ "范围:": "Scope: ",
174
+ "适用:": "Applies to: ",
175
+ "例外:": "Exceptions: ",
176
+ "证据:": "Evidence: ",
177
+ "没有关联来源记录,采纳前请核对依据。": "No linked sources. Check the evidence before accepting.",
178
+ "来源记录 · ": "Source record · ",
179
+ "已读到来源": "Source read",
180
+ "待核验": "Unverified",
181
+ "会话": "Session",
182
+ "用户纠正": "User correction",
183
+ "本地资料": "Local artifact",
184
+ "未提供摘要": "No summary provided",
185
+ "会话:": "Session: ",
186
+ " · 记录 ": " · Record ",
187
+ "记录时间:": "Recorded: ",
188
+ "核验说明:": "Verification: ",
189
+ "来源校验值:": "Source hash: ",
190
+ "为什么被扣下:": "Why held: ",
191
+ "已审阅经验": "Reviewed lessons",
192
+ "重试": "Retry",
193
+ "读取经验失败:": "Could not load lessons: ",
194
+ "旧日记不受影响;修好后点「重试」。": "Your journal is preserved. Retry after resolving the error.",
195
+ "经验": "Lessons",
196
+ "加载中…": "Loading…",
197
+ "查看 ": "Show ",
198
+ "经验状态": "Lesson state",
199
+ "可用经验": "Usable lessons",
200
+ "已驳回历史": "Rejected history",
201
+ "暂无可用经验;待核实的记录在下方。": "No usable lessons yet. Records needing review are below.",
202
+ "没有已驳回记录。": "No rejected records.",
203
+ "显示更多经验": "Show more lessons",
204
+ "只显示最近 ": "Showing the most recent ",
205
+ " 条经验。": " lessons.",
206
+ "采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。": "Accepted lessons must still match the platform, version and project of the current task. A source being read does not mean its conclusions always apply.",
207
+ "待核实": "Needs verification",
208
+ " 条待核实": " need verification",
209
+ "没有待核实的经验": "No lessons awaiting verification",
210
+ "显示更多待核实经验": "Show more pending lessons",
211
+ "这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。": "These lessons are not injected as task advice by default. Check sources and scope before accepting, rejecting or reviewing further.",
212
+ "写入记录": "Rule changes",
213
+ "读取写入记录失败:": "Could not load rule changes: ",
214
+ " 次写入 · ": " changes · ",
215
+ " 次可回滚": " can be rolled back",
216
+ "经验:": "Lessons: ",
217
+ "前哈希:": "Before hash: ",
218
+ "后哈希:": "After hash: ",
219
+ "备份标识与回滚命令": "Backup ID and rollback command",
220
+ " 条记录。": " records.",
221
+ "回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。": "Rollback restores the backed-up Dream managed block and preserves content outside it. External changes to the block prevent rollback.",
222
+ "隐私模式:点击卡片临时查看": "Privacy mode: click the card to reveal temporarily",
223
+ "心境:": "Mood: ",
224
+ "收起": "Collapse",
225
+ "展开全文": "Read more",
226
+ "没有找到「": "No dreams matching “",
227
+ "」相关的梦": "”",
228
+ "换个关键词试试,或者清空搜索看全部梦境。": "Try another keyword, or clear the search to see all dreams.",
229
+ "清空搜索": "Clear search",
230
+ "还没有梦境日记": "No dreams yet",
231
+ "对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。": "Ask your agent to “have a dream”. It will replay recent sessions, reflect and record its first dream here.",
232
+ "对 agent 说「做个梦」": "Ask your agent to “have a dream”",
233
+ "搜索反思与教训…": "Search reflections and observations…",
234
+ "搜索梦境": "Search dreams",
235
+ "筛选作用于已加载窗口,不影响全量统计": "Filters apply to loaded dreams; overall statistics are unchanged",
236
+ "筛选": "Filter",
237
+ "筛选梦境": "Filter dreams",
238
+ " 场,不影响全量统计": " loaded dreams; overall statistics are unchanged",
239
+ "只看有教训的梦": "Only dreams with observations",
240
+ "清空筛选": "Clear filters",
241
+ "完成": "Done",
242
+ "显示层模糊;DOM 仍为明文,不改文件、不加密": "Display blur only; the DOM remains readable. Files are unchanged and not encrypted",
243
+ "隐私模式:开": "Privacy mode: on",
244
+ "隐私模式": "Privacy mode",
245
+ "重新读取梦境日记": "Reload the dream journal",
246
+ "刷新": "Refresh",
247
+ "读取梦境日记失败:": "Could not load the dream journal: ",
248
+ "暂时读不到梦境日记": "The dream journal is unavailable",
249
+ "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。": "Retry after the host is ready. If the problem persists, check DSH logs for [dsh-dream] warnings.",
250
+ "梦境总数": "Total dreams",
251
+ "本页显示最近 ": "Most recent loaded: ",
252
+ "全部梦境日记": "All dreams",
253
+ "时间跨度": "Time span",
254
+ "最近一场": "Latest dream",
255
+ " 种": " moods",
256
+ "最常见:": "Most common: ",
257
+ "梦境时间线": "Dream timeline",
258
+ "筛选结果": "Filtered results",
259
+ "搜索结果": "Search results",
260
+ "没有符合筛选的梦(筛选作用于已加载的 ": "No matching dreams (filters apply to the ",
261
+ " 场)。": " loaded dreams).",
262
+ "梦境面板视图": "Dream views",
263
+ "日记": "Journal",
264
+ "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。": "Privacy mode blurs the display only. The DOM remains readable; files are unchanged and not encrypted. Click a card to reveal it temporarily.",
265
+ "显示更早的梦": "Show older dreams",
266
+ "数据来自梦境日记(dreams.jsonl)": "Data from the dream journal (dreams.jsonl)",
267
+ "梦境日记": "Dream journal",
268
+ "会话回放 → 反思 → 记忆巩固。这里按时间倒序陈列 agent 做过的梦,附心境方块与教训榜。": "Session replay → reflection → memory consolidation. Browse your agent’s dreams, mood history and recurring observations, newest first."
269
+ });
270
+ function languageOf(locale = localeService) {
271
+ try { return String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'; } catch { return 'zh'; }
272
+ }
273
+ function t(zh) { return languageOf() === 'en' ? (EN[zh] ?? zh) : zh; }
274
+ function displayError(error) {
275
+ const message = error?.message || String(error || '');
276
+ if (languageOf() !== 'en') return message;
277
+ if (EN[message]) return EN[message];
278
+ if (!/[\u3400-\u9fff]/.test(message)) return message;
279
+ const hints = {
280
+ revision: 'This lesson or project changed. Refresh and review again; your input is preserved.',
281
+ conflict: 'The target changed. Preview the current rules again before applying.',
282
+ 'preview-mismatch': 'The preview no longer matches. Create a new preview.',
283
+ 'preview-expired': 'The preview expired. Create a new preview.',
284
+ 'not-rollbackable': 'Rollback is unavailable because the managed block changed or its backup is missing.',
285
+ 'ineligible': 'This lesson is not eligible for rule writing. Check its scope and review state.',
286
+ 'read-only': 'The target is read-only. Check its permissions before retrying.',
287
+ 'marker': 'The managed block markers are invalid. Check the file before retrying.',
288
+ 'path-outside-project': 'The target must stay inside the project directory.',
289
+ 'unsupported-encoding': 'The target file encoding is unsupported. Use UTF-8.',
290
+ 'target-is-directory': 'The selected target is a directory; select a file.',
291
+ 'not-found': 'The lesson, target or backup was not found. Refresh and check the project.',
292
+ forbidden: 'This operation must be performed from the authenticated Dream panel.',
293
+ 'too-large': 'The request is too large. Shorten the input and try again.',
294
+ cancelled: 'The operation was cancelled. Refresh to check the result before retrying.',
295
+ io: 'The operation could not access its files. Check the project location and file permissions, then refresh before retrying.',
296
+ internal: 'Could not load the records. Check the DSH logs and retry.',
297
+ invalid: 'The input or selected record is no longer valid. Check the fields and refresh the list.',
298
+ };
299
+ return (hints[error?.code] || 'The operation failed. Check the DSH logs, then refresh before retrying.')
300
+ + (error?.status ? ' (HTTP ' + error.status + (error.code ? ', ' + error.code : '') + ')' : '');
301
+ }
302
+ function rollbackReasonText(reason) {
303
+ if (languageOf() !== 'en') return reason;
304
+ return ({ '目标文件不可读': 'Target file is unreadable', '文件不存在': 'File does not exist',
305
+ '管理块标记异常': 'Invalid managed block markers', '管理块已被移除': 'Managed block was removed',
306
+ '管理块已被外部修改': 'Managed block changed externally' })[reason]
307
+ || (/[\u3400-\u9fff]/.test(reason) ? 'Check the target file and backup' : reason);
308
+ }
309
+ function scopeText(label) {
310
+ if (languageOf() !== 'en') return label;
311
+ return label.replace(/^全局$/, 'Global').replace(/^工作区 /, 'Workspace ').replace(/^项目 /, 'Project ');
312
+ }
313
+ function evidenceText(summary) {
314
+ if (languageOf() !== 'en') return summary;
315
+ return summary.replace(/^独立证据 (\d+) 条(已读 (\d+) \/ 仅声明 (\d+)):/, 'Independent sources: $1 (read: $2 / claimed: $3): ');
316
+ }
317
+ function useLanguage(locale) {
318
+ const [, rerender] = useState(0);
319
+ useEffect(() => { const update = () => rerender(n => n + 1); update(); return locale?.subscribe?.(update); }, [locale]);
320
+ }
321
+
322
+
22
323
  // 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
23
324
  const ROUTE = "_dsh/dsh-dream/journal";
325
+ const ACTION_ROUTE = "api/dsh-dream/actions";
24
326
  const KNOWLEDGE_ROUTE = "_dsh/dsh-dream/knowledge";
25
327
  const BRIDGE_ROUTE = "_dsh/dsh-dream/bridge";
26
328
  const KNOWLEDGE_LIMIT = 50;
@@ -73,7 +375,7 @@ function errorMessageOf(body, status) {
73
375
  if (body.error !== null && typeof body.error === "object" && typeof body.error.message === "string" && body.error.message !== "") return body.error.message;
74
376
  if (typeof body.message === "string" && body.message !== "") return body.message;
75
377
  }
76
- return "请求失败(HTTP " + String(status) + ")";
378
+ return t("请求失败(HTTP ") + String(status) + ")";
77
379
  }
78
380
 
79
381
  /** GET /_dsh/dsh-dream/journal:契约体 { dreams, stats, query, limit }。 */
@@ -86,7 +388,7 @@ async function fetchJournal(query, limit) {
86
388
  const res = await fetch(routeUrl(params.join("&")), init);
87
389
  let body = null;
88
390
  try { body = await res.json(); } catch (error) { body = null; }
89
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
391
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
90
392
  if (res.ok !== true || body.ok === false) {
91
393
  const failure = new Error(errorMessageOf(body, res.status));
92
394
  failure.status = res.status;
@@ -110,7 +412,7 @@ async function fetchKnowledge() {
110
412
  const res = await fetch(routeUrlFor(KNOWLEDGE_ROUTE, ""), init);
111
413
  let body = null;
112
414
  try { body = await res.json(); } catch (error) { body = null; }
113
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
415
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
114
416
  if (res.ok !== true || body.ok === false) {
115
417
  const failure = new Error(errorMessageOf(body, res.status));
116
418
  failure.status = res.status;
@@ -133,7 +435,7 @@ async function fetchBridgeRecords() {
133
435
  const res = await fetch(routeUrlFor(BRIDGE_ROUTE, ""), init);
134
436
  let body = null;
135
437
  try { body = await res.json(); } catch (error) { body = null; }
136
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
438
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
137
439
  if (res.ok !== true || body.ok === false) {
138
440
  const failure = new Error(errorMessageOf(body, res.status));
139
441
  failure.status = res.status;
@@ -147,6 +449,24 @@ async function fetchBridgeRecords() {
147
449
  };
148
450
  }
149
451
 
452
+ async function postAction(body) {
453
+ const init = { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", "x-dsh-dream-action": "1" }, body: JSON.stringify(body) };
454
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") init.signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
455
+ const res = await fetch(routeUrlFor(ACTION_ROUTE, ""), init);
456
+ let value = null;
457
+ try { value = await res.json(); } catch (error) { /* Preserve HTTP status for useful recovery hints. */ }
458
+ if (res.ok !== true || value === null || value.ok === false) {
459
+ const hint = res.status === 401 ? t("登录状态已失效,请重新打开 DSH 页面后重试。")
460
+ : res.status === 404 ? t("当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。")
461
+ : errorMessageOf(value, res.status);
462
+ const failure = new Error(hint);
463
+ failure.code = value && value.error ? value.error.code : undefined;
464
+ failure.status = res.status;
465
+ throw failure;
466
+ }
467
+ return value;
468
+ }
469
+
150
470
  /** 复制命令到剪贴板;没有 clipboard / 被拒绝时返回 false,调用方回退为可选中文本。 */
151
471
  async function copyText(text) {
152
472
  const value = String(text);
@@ -238,12 +558,12 @@ function normalizeLessonState(value) {
238
558
  /** 状态 → 中文标签(面板文案集中在这里)。 */
239
559
  function lessonStateLabel(state) {
240
560
  switch (state) {
241
- case "candidate": return "候选(未采纳)";
242
- case "usable": return "可用";
243
- case "disputed": return "有冲突";
244
- case "stale": return "待复核";
245
- case "rejected": return "已驳回";
246
- default: return "未知状态";
561
+ case "candidate": return t("候选(未采纳)");
562
+ case "usable": return t("可用");
563
+ case "disputed": return t("有冲突");
564
+ case "stale": return t("待复核");
565
+ case "rejected": return t("已驳回");
566
+ default: return t("未知状态");
247
567
  }
248
568
  }
249
569
 
@@ -276,11 +596,11 @@ function applicabilityTextOf(value) {
276
596
  const main = pkg !== "" ? (versions !== "" ? pkg + " " + versions : pkg) : versions;
277
597
  const bits = [];
278
598
  if (main !== "") bits.push(main);
279
- if (platform !== "") bits.push("平台 " + platform);
599
+ if (platform !== "") bits.push(t("平台 ") + platform);
280
600
  if (bits.length > 0) parts.push(bits.join(" · "));
281
601
  }
282
602
  if (parts.length === 0) return "";
283
- if (parts.length > 4) return parts.slice(0, 4).join(";") + ";等 " + String(parts.length) + " 条适用范围";
603
+ if (parts.length > 4) return parts.slice(0, 4).join(";") + t(";等 ") + String(parts.length) + t(" 条适用范围");
284
604
  return parts.join(";");
285
605
  }
286
606
 
@@ -324,19 +644,22 @@ function normalizeKnowledge(value, now, limit) {
324
644
  state: state,
325
645
  stateLabel: lessonStateLabel(state),
326
646
  stateClass: lessonStateClass(state),
327
- scopeLabel: trimmedText(raw.scopeLabel) || "范围未标注",
328
- evidenceSummary: trimmedText(raw.evidenceSummary) || "暂无可展示的证据摘要",
647
+ scopeLabel: scopeText(trimmedText(raw.scopeLabel)) || t("范围未标注"),
648
+ evidenceSummary: evidenceText(trimmedText(raw.evidenceSummary)) || t("暂无可展示的证据摘要"),
649
+ evidence: Array.isArray(source.evidence) && Array.isArray(raw.evidenceIds)
650
+ ? source.evidence.filter(function (item) { return item && raw.evidenceIds.indexOf(item.id) !== -1; }).slice(0, 20) : [],
651
+ feedback: raw.feedback && typeof raw.feedback === "object" ? raw.feedback : { useful: 0, notApplicable: 0, recent: [] },
329
652
  lastValidatedAt: lastValidatedAt,
330
- lastText: lastValidatedAt === "" ? "尚未核验" : relativeTime(lastValidatedAt, now),
331
- lastTitle: lastValidatedAt === "" ? "没有 lastValidatedAt(从未核验)" : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
653
+ lastText: lastValidatedAt === "" ? t("尚未核验") : relativeTime(lastValidatedAt, now),
654
+ lastTitle: lastValidatedAt === "" ? t("没有 lastValidatedAt(从未核验)") : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
332
655
  revision: typeof raw.revision === "number" && Number.isFinite(raw.revision) && raw.revision >= 0 ? Math.floor(raw.revision) : 0,
333
656
  independentSupportCount: typeof raw.independentSupportCount === "number" && raw.independentSupportCount > 0 ? Math.floor(raw.independentSupportCount) : 0,
334
657
  projectId: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.projectId) : "",
335
658
  workspaceRoot: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.workspaceRoot) : "",
336
659
  conflictIds: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }) : [],
337
- 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 + ")" }; }) : [],
660
+ 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) || t("未加载的经验(") + id + ")" }; }) : [],
338
661
  holdReason: trimmedText(raw.holdReason) !== "" ? trimmedText(raw.holdReason) : holdReasonOf(raw),
339
- holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? trimmedText(raw.holdReasonLabel) : skippedReasonLabel(holdReasonOf(raw)),
662
+ holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? t(trimmedText(raw.holdReasonLabel)) : skippedReasonLabel(holdReasonOf(raw)),
340
663
  bridgeable: bridgeableOf(raw.state) && trimmedText(raw.id) !== "",
341
664
  candidate: state === "candidate",
342
665
  disputed: state === "disputed",
@@ -365,7 +688,7 @@ function knowledgeCountText(stats) {
365
688
  }
366
689
  if (parts.length === 0) {
367
690
  const total = typeof source.lessons === "number" && source.lessons > 0 ? source.lessons : 0;
368
- return total > 0 ? "共 " + String(total) + " 条" : "还没有经验";
691
+ return total > 0 ? t("共 ") + String(total) + t(" 条") : t("还没有经验");
369
692
  }
370
693
  return parts.join(" · ");
371
694
  }
@@ -377,32 +700,32 @@ function knowledgeNoticeText(stats) {
377
700
  const badLines = typeof source.badLines === "number" && Number.isFinite(source.badLines) && source.badLines > 0 ? Math.floor(source.badLines) : 0;
378
701
  if (truncated !== true && badLines === 0) return "";
379
702
  const parts = [];
380
- if (truncated === true) parts.push("日志过大,仅回放最近一段事件");
381
- if (badLines > 0) parts.push("有 " + String(badLines) + " 行损坏已跳过");
382
- return parts.join(";") + ";events.jsonl 未改动。";
703
+ if (truncated === true) parts.push(t("日志过大,仅回放最近一段事件"));
704
+ if (badLines > 0) parts.push(t("有 ") + String(badLines) + t(" 行损坏已跳过"));
705
+ return parts.join(";") + t(";events.jsonl 未改动。");
383
706
  }
384
707
 
385
708
  /** 经验区块的独立空态:与「还没有梦境日记」分开,说明经验从哪来。 */
386
709
  function knowledgeEmptyState() {
387
710
  return {
388
- title: "还没有经验记录",
389
- body: "经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。",
390
- hint: "旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。",
711
+ title: t("还没有经验记录"),
712
+ body: t("经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。"),
713
+ hint: t("旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。"),
391
714
  };
392
715
  }
393
716
 
394
717
  /** 检索层 skipped reason 的中文映射(面板扣留原因文案;与 retrieval.ts 的 reason 常量同名)。 */
395
718
  function skippedReasonLabel(reason) {
396
719
  switch (trimmedText(reason)) {
397
- case "state:rejected": return "已驳回:不参与默认检索与注入";
398
- case "state:stale": return "待复核:条件或依赖可能已过期,默认不注入";
399
- case "state:disputed": return "冲突未解决:默认不注入,先 resolve-conflict";
400
- case "candidate-hold": return "候选:可被检索到,但默认不作为任务建议注入";
401
- case "no-evidence": return "证据不足:没有独立证据,默认不注入";
402
- case "version-mismatch": return "版本不匹配:当前依赖版本不满足适用范围";
403
- case "limit": return "超出条数预算,本次未取回";
404
- case "budget-stop": return "超出字符预算:按排名整条装入,遇到第一条放不下即停";
405
- case "budget": return "超出字符预算,本次未取回";
720
+ case "state:rejected": return t("已驳回:不参与默认检索与注入");
721
+ case "state:stale": return t("待复核:条件或依赖可能已过期,默认不注入");
722
+ case "state:disputed": return t("冲突未解决:默认不注入,先 resolve-conflict");
723
+ case "candidate-hold": return t("候选:可被检索到,但默认不作为任务建议注入");
724
+ case "no-evidence": return t("证据不足:没有独立证据,默认不注入");
725
+ case "version-mismatch": return t("版本不匹配:当前依赖版本不满足适用范围");
726
+ case "limit": return t("超出条数预算,本次未取回");
727
+ case "budget-stop": return t("超出字符预算:按排名整条装入,遇到第一条放不下即停");
728
+ case "budget": return t("超出字符预算,本次未取回");
406
729
  default: return "";
407
730
  }
408
731
  }
@@ -484,12 +807,12 @@ function shortHash(value) {
484
807
 
485
808
  function bridgeActionLabel(action) {
486
809
  switch (trimmedText(action)) {
487
- case "create": return "新建";
488
- case "replace": return "替换管理块";
489
- case "append": return "追加管理块";
490
- case "unchanged": return "未改动";
491
- case "rollback": return "回滚";
492
- default: return trimmedText(action) || "未知动作";
810
+ case "create": return t("新建");
811
+ case "replace": return t("替换管理块");
812
+ case "append": return t("追加管理块");
813
+ case "unchanged": return t("未改动");
814
+ case "rollback": return t("回滚");
815
+ default: return trimmedText(action) || t("未知动作");
493
816
  }
494
817
  }
495
818
 
@@ -527,17 +850,17 @@ function normalizeBridgeRecords(value, now, limit) {
527
850
  rows.push({
528
851
  id: id,
529
852
  at: at,
530
- atText: at === "" ? "时间未知" : relativeTime(at, now),
531
- atTitle: at === "" ? "没有记录时间" : formatDate(at) + (formatTime(at) === "" ? "" : " " + formatTime(at)),
532
- target: target || "目标未记录",
853
+ atText: at === "" ? t("时间未知") : relativeTime(at, now),
854
+ atTitle: at === "" ? t("没有记录时间") : formatDate(at) + (formatTime(at) === "" ? "" : " " + formatTime(at)),
855
+ target: target || t("目标未记录"),
533
856
  action: trimmedText(raw.action),
534
857
  actionLabel: bridgeActionLabel(raw.action),
535
858
  lessons: lessonRefs,
536
- lessonsText: lessonRefs.length === 0 ? "没有经验引用" : lessonRefs.join("、"),
859
+ lessonsText: lessonRefs.length === 0 ? t("没有经验引用") : lessonRefs.join("、"),
537
860
  beforeShort: shortHash(raw.beforeSha256),
538
861
  afterShort: shortHash(raw.afterSha256),
539
862
  rollbackable: rollbackable,
540
- rollbackText: rollbackable ? "可回滚" : ("不可回滚" + (rollbackReason !== "" ? ":" + rollbackReason : "")),
863
+ rollbackText: rollbackable ? t("可回滚") : (t("不可回滚") + (rollbackReason !== "" ? (languageOf() === 'en' ? ': ' : ':') + rollbackReasonText(rollbackReason) : "")),
541
864
  rollbackReason: rollbackReason,
542
865
  });
543
866
  }
@@ -554,15 +877,15 @@ function normalizeBridgeRecords(value, now, limit) {
554
877
  /** 「写入记录」的独立空态。 */
555
878
  function bridgeEmptyState() {
556
879
  return {
557
- title: "还没有写入记录",
558
- body: "写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。",
559
- hint: "在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。",
880
+ title: t("还没有写入记录"),
881
+ body: t("写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。"),
882
+ hint: t("在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。"),
560
883
  };
561
884
  }
562
885
 
563
886
  /** 「待核实」的独立空态。 */
564
887
  function reviewQueueEmptyState() {
565
- return "没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。";
888
+ return t("没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。");
566
889
  }
567
890
 
568
891
  function spanOf(dreams) {
@@ -580,7 +903,7 @@ function spanOf(dreams) {
580
903
 
581
904
  function formatDate(iso) {
582
905
  const date = new Date(iso);
583
- if (!Number.isFinite(date.getTime())) return iso === "" ? "时间未知" : iso;
906
+ if (!Number.isFinite(date.getTime())) return iso === "" ? t("时间未知") : iso;
584
907
  return date.getFullYear() + "-" + pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
585
908
  }
586
909
 
@@ -611,10 +934,10 @@ function relativeTime(iso, now) {
611
934
  const minute = 60000;
612
935
  const hour = 3600000;
613
936
  const day = 86400000;
614
- if (diff < minute) return "刚刚";
615
- if (diff < hour) return Math.floor(diff / minute) + " 分钟前";
616
- if (diff < day) return Math.floor(diff / hour) + " 小时前";
617
- if (diff < day * 30) return Math.floor(diff / day) + " 天前";
937
+ if (diff < minute) return t("刚刚");
938
+ if (diff < hour) return Math.floor(diff / minute) + t(" 分钟前");
939
+ if (diff < day) return Math.floor(diff / hour) + t(" 小时前");
940
+ if (diff < day * 30) return Math.floor(diff / day) + t(" 天前");
618
941
  return formatDate(iso);
619
942
  }
620
943
 
@@ -626,9 +949,9 @@ function recentText(iso, now) {
626
949
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
627
950
  const that = new Date(date.getFullYear(), date.getMonth(), date.getDate());
628
951
  const days = Math.round((today.getTime() - that.getTime()) / 86400000);
629
- if (days <= 0) return "今天";
630
- if (days === 1) return "昨天";
631
- if (days < 30) return String(days) + " 天前";
952
+ if (days <= 0) return t("今天");
953
+ if (days === 1) return t("昨天");
954
+ if (days < 30) return String(days) + t(" 天前");
632
955
  return formatDate(iso);
633
956
  }
634
957
 
@@ -722,23 +1045,23 @@ function needsOlderDreams(loaded, total) {
722
1045
  }
723
1046
 
724
1047
  function countTextOf(query, total, count, filterCount, visibleCount) {
725
- if (filterCount > 0) return "筛选后 " + String(visibleCount) + " / " + String(count) + " 场";
726
- if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
727
- return "共 " + String(total) + " 场梦";
1048
+ if (filterCount > 0) return t("筛选后 ") + String(visibleCount) + " / " + String(count) + t(" 场");
1049
+ if (query !== "") return (languageOf() === "en" ? "“" : "「") + query + t("」命中 ") + String(count) + t(" 场");
1050
+ return t("共 ") + String(total) + t(" 场梦");
728
1051
  }
729
1052
 
730
1053
  function listHintOf(query, count, filterCount, visibleCount) {
731
- if (filterCount > 0) return "筛选当前已加载的 " + String(count) + " 场 · 命中 " + String(visibleCount) + " 场";
732
- if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
733
- return "新梦在前";
1054
+ if (filterCount > 0) return t("筛选当前已加载的 ") + String(count) + t(" 场 · 命中 ") + String(visibleCount) + t(" 场");
1055
+ if (query !== "") return (languageOf() === "en" ? "“" : "「") + query + t("」命中 ") + String(count) + t(" 场");
1056
+ return t("新梦在前");
734
1057
  }
735
1058
 
736
1059
  function footerTextOf(query, limit, total, count) {
737
1060
  if (limit > DEFAULT_LIMIT) {
738
- const capped = limit >= MORE_LIMIT && count < total ? "(已达路由上限)" : "";
739
- return "最近 " + String(limit) + " 场 / 共 " + String(total) + " 场" + capped;
1061
+ const capped = limit >= MORE_LIMIT && count < total ? t("(已达路由上限)") : "";
1062
+ return t("最近 ") + String(limit) + t(" 场 / 共 ") + String(total) + t(" 场") + capped;
740
1063
  }
741
- return query === "" ? "新梦在前 · 最多显示 " + String(DEFAULT_LIMIT) + " 场" : "搜索结果 · 最多显示 " + String(DEFAULT_LIMIT) + " 场";
1064
+ return query === "" ? t("新梦在前 · 最多显示 ") + String(DEFAULT_LIMIT) + t(" 场") : t("搜索结果 · 最多显示 ") + String(DEFAULT_LIMIT) + t(" 场");
742
1065
  }
743
1066
 
744
1067
  /** 面板的视图模型:所有文案/数字集中在这里算,组件只负责画。 */
@@ -762,15 +1085,15 @@ function viewModelOf(dreams, stats, now) {
762
1085
  share: Math.max(6, Math.round(row.count / Math.max(lessonTotal, 1) * 100)),
763
1086
  showBar: row.count >= 2,
764
1087
  candidate: row.count >= 3,
765
- lastText: knownAt === "" ? "" : (fromStats ? "最近 " : "本页内最近 ") + recentText(knownAt, now),
1088
+ lastText: knownAt === "" ? "" : (fromStats ? t("最近 ") : t("本页内最近 ")) + recentText(knownAt, now),
766
1089
  lastTitle: knownAt === "" ? "" : formatDate(knownAt) + (formatTime(knownAt) === "" ? "" : " " + formatTime(knownAt))
767
- + (fromStats ? "" : "(统计未给 lastAt,按本页已加载的 " + String(list.length) + " 场现算)"),
1090
+ + (fromStats ? "" : t("(统计未给 lastAt,按本页已加载的 ") + String(list.length) + t(" 场现算)")),
768
1091
  };
769
1092
  });
770
1093
  const span = spanOf(list);
771
1094
  const spanText = span === null ? "" : spanTextOf(span);
772
1095
  const spanFull = span === null ? "" : formatDate(new Date(span.from).toISOString()) + " → " + formatDate(new Date(span.to).toISOString());
773
- const spanHint = span === null ? "" : (span.days === 0 ? "同一天" : "约 " + String(span.days) + " 天");
1096
+ const spanHint = span === null ? "" : (span.days === 0 ? t("同一天") : t("约 ") + String(span.days) + t(" 天"));
774
1097
  const blocks = list.slice().reverse().map((dream, index) => ({
775
1098
  key: "block-" + (dream.id !== "" ? dream.id : "no-id") + "-" + String(index),
776
1099
  tone: toneClass(moodSlot(dream.mood)),
@@ -801,12 +1124,12 @@ function headlineOf(dreams, total, span, now) {
801
1124
  if (dreams.length === 0) return "";
802
1125
  if (dreams.length === 1 && total <= 1) {
803
1126
  const dream = dreams[0];
804
- return "第 1 场梦 · " + formatDate(dream.at) + " · 心境「" + dream.mood + "」";
1127
+ return t("第 1 场梦 · ") + formatDate(dream.at) + t(" · 心境「") + dream.mood + (languageOf() === "en" ? "”" : "」");
805
1128
  }
806
1129
  const parts = [];
807
- parts.push(total > dreams.length ? "共 " + String(total) + " 场梦 · 本页最近 " + String(dreams.length) + " 场" : String(dreams.length) + " 场梦");
1130
+ parts.push(total > dreams.length ? t("共 ") + String(total) + t(" 场梦 · 本页最近 ") + String(dreams.length) + t(" 场") : String(dreams.length) + t(" 场梦"));
808
1131
  if (span !== null) parts.push(span.from === span.to ? formatDate(new Date(span.from).toISOString()) : formatDate(new Date(span.from).toISOString()) + " → " + formatDate(new Date(span.to).toISOString()));
809
- parts.push("最近:" + relativeTime(dreams[0].at, now));
1132
+ parts.push(t("最近:") + relativeTime(dreams[0].at, now));
810
1133
  return parts.join(" · ");
811
1134
  }
812
1135
 
@@ -851,20 +1174,20 @@ function StatTile(props) {
851
1174
  function MoodCard(props) {
852
1175
  const vm = props.vm;
853
1176
  return h("section", { className: "dshd-card" }, [
854
- h("h4", { key: "t" }, "心境"),
1177
+ h("h4", { key: "t" }, t("心境")),
855
1178
  h("div", { className: "dshd-mblocks", key: "blocks" }, vm.blocks.map((block) => h("span", {
856
1179
  key: block.key,
857
1180
  className: "dshd-mblock " + block.tone,
858
1181
  title: block.title,
859
1182
  }))),
860
1183
  vm.count === 1
861
- ? h("div", { className: "dshd-hint", key: "one" }, "1 场梦,先不谈分布 —— 再做梦会累积。")
1184
+ ? h("div", { className: "dshd-hint", key: "one" }, t("1 场梦,先不谈分布 —— 再做梦会累积。"))
862
1185
  : h("div", { key: "legend" }, [
863
- h("div", { className: "dshd-note", key: "cap" }, "左 → 右:从早到晚"),
1186
+ h("div", { className: "dshd-note", key: "cap" }, t("左 → 右:从早到晚")),
864
1187
  h("div", { className: "dshd-legend", key: "rows" }, vm.moodRows.map((row, index) => h("div", { className: "dshd-legend-row", key: "mood-" + String(index) + "-" + row.mood }, [
865
1188
  h("span", { className: "dshd-mblock " + toneClass(moodSlot(row.mood)), key: "d" }),
866
1189
  h("span", { className: "dshd-legend-name", key: "n", title: row.mood }, row.mood),
867
- h("span", { className: "dshd-legend-count", key: "c" }, String(row.count) + " 场"),
1190
+ h("span", { className: "dshd-legend-count", key: "c" }, String(row.count) + t(" 场")),
868
1191
  ]))),
869
1192
  ]),
870
1193
  ]);
@@ -873,9 +1196,9 @@ function MoodCard(props) {
873
1196
  function LessonCard(props) {
874
1197
  const vm = props.vm;
875
1198
  return h("section", { className: "dshd-card" }, [
876
- h("h4", { key: "t" }, "教训榜"),
1199
+ h("h4", { key: "t" }, t("教训榜")),
877
1200
  vm.lessonRows.length === 0
878
- ? h("div", { className: "dshd-hint", key: "e" }, "还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。")
1201
+ ? h("div", { className: "dshd-hint", key: "e" }, t("还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。"))
879
1202
  : h("div", { key: "rows" }, vm.lessonRows.map((row, index) => h("div", { className: "dshd-lesson", key: "lesson-" + String(index) + "-" + row.lesson }, [
880
1203
  h("span", { className: "dshd-rank", key: "r" }, String(row.rank)),
881
1204
  h("span", { className: "dshd-lesson-body", key: "b" }, [
@@ -885,10 +1208,10 @@ function LessonCard(props) {
885
1208
  h("span", { className: "dshd-lesson-meta", key: "m" }, [
886
1209
  h("span", { className: "dshd-times", key: "c" }, "×", h("b", { key: "n" }, String(row.count))),
887
1210
  row.lastText === "" ? null : h("span", { className: "dshd-last", key: "l", title: row.lastTitle }, row.lastText),
888
- row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, "常见教训") : null,
1211
+ row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, t("常见教训")) : null,
889
1212
  ]),
890
1213
  ]))),
891
- h("div", { className: "dshd-note", key: "note" }, "次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。"),
1214
+ h("div", { className: "dshd-note", key: "note" }, t("次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。")),
892
1215
  ]);
893
1216
  }
894
1217
 
@@ -904,11 +1227,11 @@ function CopyCommand(props) {
904
1227
  setFailed(ok !== true);
905
1228
  });
906
1229
  };
907
- const label = props.label === undefined ? "复制命令" : props.label;
1230
+ const label = props.label === undefined ? t("复制命令") : props.label;
908
1231
  return h("span", { className: "dshd-copywrap" }, [
909
- h(Btn, { key: "b", onClick: onCopy, disabled: props.disabled === true, title: props.title === undefined ? command : props.title }, copied === true ? "已复制" : label),
1232
+ h(Btn, { key: "b", onClick: onCopy, disabled: props.disabled === true, title: props.title === undefined ? command : props.title }, copied === true ? t("已复制") : label),
910
1233
  failed === true ? h("code", { key: "cmd", className: "dshd-cmd", tabIndex: 0 }, command) : null,
911
- failed === true ? h("span", { key: "hint", className: "dshd-note" }, "剪贴板不可用:请手动选中上面的命令复制。") : null,
1234
+ failed === true ? h("span", { key: "hint", className: "dshd-note" }, t("剪贴板不可用:请手动选中上面的命令复制。")) : null,
912
1235
  ]);
913
1236
  }
914
1237
 
@@ -916,38 +1239,112 @@ function CopyCommand(props) {
916
1239
  function ReviewControls(props) {
917
1240
  const row = props.row;
918
1241
  const [open, setOpen] = useState(false);
919
- const [action, setAction] = useState(function () { return reviewActionFor(row.state); });
1242
+ const [action, setAction] = useState(reviewActionFor(row.state));
920
1243
  const [resolution, setResolution] = useState("prefer");
921
1244
  const [note, setNote] = useState("");
1245
+ const [feedbackNote, setFeedbackNote] = useState("");
1246
+ const [feedbackRetry, setFeedbackRetry] = useState(null);
922
1247
  const [affected, setAffected] = useState(function () { return row.conflictIds[0] || ""; });
923
- const commands = row.state === "rejected" ? [["reopen", "重新送审"]]
924
- : row.state === "disputed" ? [["resolve-conflict", "处理冲突"], ["reject", "驳回这条"]]
925
- : [["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(); } }, [
1248
+ const [pending, setPending] = useState(false);
1249
+ const [error, setError] = useState("");
1250
+ const [preview, setPreview] = useState(null);
1251
+ const commands = row.state === "rejected" ? [["reopen", t("重新送审")]]
1252
+ : row.state === "disputed" ? [["resolve-conflict", t("处理冲突")], ["reject", t("驳回这条")]]
1253
+ : row.state === "usable" ? [["mark-stale", t("标记待复核")], ["reject", t("驳回")]]
1254
+ : [["accept", t("采纳")], ["reject", t("驳回")], ["mark-stale", t("标记待复核")]];
1255
+ const effectiveAction = commands.some(function (item) { return item[0] === action; }) ? action : commands[0][0];
1256
+ const command = reviewCommandOf(row, { action: effectiveAction, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] });
1257
+ const submit = async function (body, message, isPreview) {
1258
+ if (pending) return;
1259
+ setPending(true); setError("");
1260
+ try {
1261
+ const result = await postAction(body);
1262
+ if (isPreview) setPreview(result);
1263
+ else { setPreview(null); if (props.onChanged) props.onChanged(result.unchanged === true ? t("管理块内容已一致,无需写入。") : message); }
1264
+ } catch (failure) {
1265
+ setError(failure.name === "TimeoutError" ? t("请求超时,请先刷新确认结果后再重试。") : displayError(failure));
1266
+ if (body.operation === "apply") setPreview(null);
1267
+ } finally { setPending(false); }
1268
+ };
1269
+ const review = function (chosen) {
1270
+ return submit({ operation: "review", action: chosen, lessonId: row.id, expectedRevision: row.revision, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] }, t("已更新「") + row.title + t("」的审阅状态。"));
1271
+ };
1272
+ const feedback = function (vote) {
1273
+ const signature = row.id + "@" + row.revision + ":" + vote + ":" + feedbackNote;
1274
+ const requestId = feedbackRetry && feedbackRetry.signature === signature ? feedbackRetry.id
1275
+ : typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : "feedback_" + Date.now() + "_" + Math.random().toString(36).slice(2);
1276
+ setFeedbackRetry({ signature: signature, id: requestId });
1277
+ return submit({ operation: "feedback", lessonId: row.id, expectedRevision: row.revision, vote: vote, requestId: requestId, ...(feedbackNote.trim() === "" ? {} : { note: feedbackNote.trim() }) }, t("已记录使用反馈;「") + row.title + t("」的审阅状态保持原样。"));
1278
+ };
1279
+ return h("div", { className: "dshd-review", role: "group", "aria-label": row.title + t("的操作"), onClick: function (event) { event.stopPropagation(); } }, [
928
1280
  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,
1281
+ ...commands.filter(function (item) { return item[0] !== "resolve-conflict"; }).map(function (item) {
1282
+ return h(Btn, { key: item[0], disabled: pending || row.revision < 1, onClick: function () { review(item[0]); } }, item[1]);
1283
+ }),
1284
+ h(Btn, { key: "review", disabled: pending, onClick: function () { setOpen(!open); }, "aria-expanded": open }, open ? t("收起审阅") : row.state === "disputed" ? t("处理冲突") : t("审阅详情")),
1285
+ row.bridgeable === true && row.workspaceRoot !== "" ? h(Btn, { key: "preview", disabled: pending, onClick: function () { submit({ operation: "preview", lessonId: row.id, expectedRevision: row.revision }, "", true); } }, t("预览规则变更")) : null,
1286
+ row.bridgeable === true && row.workspaceRoot === "" ? h(CopyCommand, { key: "preview-command", command: bridgePreviewCommandOf(row.id, row.projectId, row.workspaceRoot), label: t("复制项目预览命令") }) : null,
931
1287
  ]),
1288
+ pending ? h("div", { key: "pending", role: "status", className: "dshd-note" }, t("正在处理…")) : null,
1289
+ error !== "" ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, [error, h(Btn, { key: "refresh", disabled: pending, onClick: function () { if (props.onChanged) props.onChanged(t("已刷新经验列表。")); } }, t("刷新列表"))]) : null,
932
1290
  open ? h("div", { className: "dshd-reviewform", key: "form", onKeyDown: function (event) {
933
- if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); setOpen(false); }
1291
+ if (event.key === "Escape" && !pending) { event.preventDefault(); event.stopPropagation(); setOpen(false); }
934
1292
  } }, [
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); } }, [
939
- h("option", { key: "prefer", value: "prefer" }, "采用这条,驳回对方"),
940
- h("option", { key: "drop", value: "drop" }, "放弃这条,保留对方"),
941
- h("option", { key: "merge", value: "merge" }, "合并到这条,对方留作历史"),
1293
+ h("label", { key: "action" }, [t("审阅动作"), h("select", { "aria-label": t("审阅动作"), 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]); }))]),
1294
+ effectiveAction === "resolve-conflict" ? h("div", { key: "conflict" }, [
1295
+ h("label", { key: "against" }, [t("冲突经验"), h("select", { "aria-label": t("冲突经验"), 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); }))]),
1296
+ h("label", { key: "resolution" }, [t("处理方式"), h("select", { "aria-label": t("处理方式"), disabled: pending, value: resolution, onChange: function (event) { setResolution(event.target.value); } }, [
1297
+ h("option", { key: "prefer", value: "prefer" }, t("采用这条,驳回对方")),
1298
+ h("option", { key: "drop", value: "drop" }, t("放弃这条,保留对方")),
1299
+ h("option", { key: "merge", value: "merge" }, t("合并到这条,对方留作历史")),
942
1300
  ])]),
943
- h("label", { key: "note" }, ["处理依据", h("textarea", { "aria-label": "处理依据", rows: 2, value: note, placeholder: "说明哪个来源支持这次决定…", onChange: function (event) { setNote(event.target.value); } })]),
1301
+ h("label", { key: "note" }, [t("处理依据"), h("textarea", { "aria-label": t("处理依据"), disabled: pending, rows: 2, maxLength: 500, value: note, placeholder: t("说明哪个来源支持这次决定…"), onChange: function (event) { setNote(event.target.value); } })]),
944
1302
  ]) : 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 对话,请助手执行;完成后刷新。命令带版本校验,记录变化时会拒绝旧命令。"),
1303
+ h(Btn, { key: "confirm", disabled: pending || command === "", onClick: function () { review(effectiveAction); }, variant: "primary" }, t("确认审阅")),
1304
+ h("details", { key: "fallback" }, [h("summary", { key: "summary" }, t("在对话里处理")), command === "" ? h("div", { key: "hint", className: "dshd-note" }, t("选择冲突经验并填写处理依据后即可处理。")) : h(CopyCommand, { key: "copy", command: command, label: t("复制审阅命令") })]),
1305
+ ]) : null,
1306
+ preview ? h("section", { key: "preview", className: "dshd-reviewform", "aria-label": t("规则变更预览") }, [
1307
+ h("strong", { key: "heading" }, t("规则变更预览")),
1308
+ h("div", { key: "target", className: "dshd-note" }, t("目标:") + preview.resolvedPath),
1309
+ h("pre", { key: "diff", className: "dshd-diff", tabIndex: 0 }, preview.diff || t("管理块内容已经一致,无需更改。")),
1310
+ h("div", { key: "help", className: "dshd-note" }, t("将按此预览重建 Dream 管理块,请核对新增和删除内容。块外内容保留,写入前创建回滚备份。")),
1311
+ h(Btn, { key: "apply", disabled: pending, variant: "primary", onClick: function () { submit({ operation: "apply", token: preview.token }, t("规则已应用,可在「写入记录」中回滚。")); } }, t("应用此变更")),
1312
+ h(Btn, { key: "cancel", disabled: pending, onClick: function () { setPreview(null); } }, t("取消预览")),
1313
+ ]) : null,
1314
+ h("details", { key: "feedback", className: "dshd-feedback" }, [
1315
+ h("summary", { key: "summary" }, t("使用反馈 · 有用 ") + (row.feedback.useful || 0) + t(" · 不适用 ") + (row.feedback.notApplicable || 0)),
1316
+ h("label", { key: "note" }, [t("场景备注(可选)"), h("textarea", { "aria-label": t("反馈场景备注"), rows: 2, disabled: pending, maxLength: 500, value: feedbackNote, placeholder: t("这次在哪个场景使用…"), onChange: function (event) { setFeedbackNote(event.target.value); } })]),
1317
+ h(Btn, { key: "useful", disabled: pending, onClick: function () { feedback("useful"); } }, t("这次有用")),
1318
+ h(Btn, { key: "not", disabled: pending, onClick: function () { feedback("not-applicable"); } }, t("这次不适用")),
1319
+ h("div", { key: "help", className: "dshd-note" }, t("记录使用情况,供之后复核;采纳与驳回由审阅决定。")),
1320
+ ...(Array.isArray(row.feedback.recent) ? row.feedback.recent.slice(-3).reverse().map(function (item, index) {
1321
+ return h("div", { key: "recent-" + index, className: "dshd-note" }, formatDate(item.at) + " · " + (item.vote === "useful" ? t("有用") : t("不适用")) + (item.note ? " · " + item.note : ""));
1322
+ }) : []),
1323
+ ]),
1324
+ ]);
1325
+ }
1326
+
1327
+ function RollbackControl(props) {
1328
+ const [confirm, setConfirm] = useState(false);
1329
+ const [pending, setPending] = useState(false);
1330
+ const [error, setError] = useState("");
1331
+ const rollback = async function () {
1332
+ if (pending) return;
1333
+ setPending(true); setError("");
1334
+ try {
1335
+ await postAction({ operation: "rollback", backupId: props.row.id });
1336
+ setConfirm(false);
1337
+ if (props.onChanged) props.onChanged(t("已恢复上一次的 Dream 管理块,块外内容保留。"));
1338
+ } catch (failure) { setError(displayError(failure)); }
1339
+ finally { setPending(false); }
1340
+ };
1341
+ return h("div", { className: "dshd-reviewform" }, [
1342
+ h(Btn, { key: "start", disabled: pending, onClick: function () { setConfirm(!confirm); } }, confirm ? t("取消回滚") : t("回滚这次变更")),
1343
+ confirm ? h("div", { key: "confirm" }, [
1344
+ h("p", { key: "target", className: "dshd-note" }, t("将恢复「") + props.row.target + t("」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。")),
1345
+ h(Btn, { key: "apply", variant: "primary", disabled: pending, onClick: rollback }, pending ? t("正在回滚…") : t("确认回滚管理块")),
950
1346
  ]) : null,
1347
+ error ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, error) : null,
951
1348
  ]);
952
1349
  }
953
1350
 
@@ -959,28 +1356,38 @@ function KnowledgeRow(props) {
959
1356
  return h("div", {
960
1357
  className: "dshd-knowledge " + row.stateClass + (privacy === true && peek === true ? " dshd-peek" : ""),
961
1358
  onClick: reveal,
962
- title: privacy === true ? "隐私模式:点击此条临时查看" : undefined,
1359
+ title: privacy === true ? t("隐私模式:点击此条临时查看") : undefined,
963
1360
  }, [
964
1361
  h("div", { className: "dshd-khead", key: "h" }, [
965
1362
  h("span", { className: "dshd-kbadge", key: "b" }, row.stateLabel),
966
1363
  h("span", { className: "dshd-ktitle", key: "t", title: row.title }, row.title),
967
1364
  ]),
968
1365
  h("div", { className: "dshd-kmeta", key: "m" }, [
969
- h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, "最近核验:" + row.lastText),
1366
+ h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, t("最近核验:") + row.lastText),
970
1367
  ]),
971
1368
  h("div", { className: "dshd-kbody", key: "body" }, [
972
- h("div", { key: "when" }, "何时用:" + (row.when !== "" ? row.when : "未标注适用条件")),
973
- h("div", { key: "action", className: "dshd-action" }, "怎么做:" + (row.action || "未标注具体动作")),
1369
+ h("div", { key: "when" }, t("何时用:") + (row.when !== "" ? row.when : t("未标注适用条件"))),
1370
+ h("div", { key: "action", className: "dshd-action" }, t("怎么做:") + (row.action || t("未标注具体动作"))),
974
1371
  h("details", { key: "details", onClick: function (event) { event.stopPropagation(); } }, [
975
- h("summary", { key: "summary" }, "适用范围与证据"),
976
- h("div", { key: "scope" }, "范围:" + row.scopeLabel),
977
- row.applicability !== "" ? h("div", { key: "applicability" }, "适用:" + row.applicability) : null,
978
- row.exceptions.length > 0 ? h("div", { key: "exceptions" }, "例外:" + row.exceptions.join(";")) : null,
979
- h("div", { key: "evidence" }, "证据:" + row.evidenceSummary),
1372
+ h("summary", { key: "summary" }, t("适用范围与证据")),
1373
+ h("div", { key: "scope" }, t("范围:") + row.scopeLabel),
1374
+ row.applicability !== "" ? h("div", { key: "applicability" }, t("适用:") + row.applicability) : null,
1375
+ row.exceptions.length > 0 ? h("div", { key: "exceptions" }, t("例外:") + row.exceptions.join(";")) : null,
1376
+ h("div", { key: "evidence" }, t("证据:") + row.evidenceSummary),
1377
+ row.evidence.length === 0 ? h("div", { key: "sources-empty", className: "dshd-note" }, t("没有关联来源记录,采纳前请核对依据。")) : h("div", { key: "sources", className: "dshd-sources" }, row.evidence.map(function (item) {
1378
+ return h("details", { key: item.id }, [
1379
+ h("summary", { key: "summary" }, t("来源记录 · ") + (item.verification === "read" ? t("已读到来源") : t("待核验")) + " · " + (item.kind === "session" ? t("会话") : item.kind === "user-correction" ? t("用户纠正") : t("本地资料"))),
1380
+ h("div", { key: "summary-text" }, item.summary || t("未提供摘要")),
1381
+ item.sessionId ? h("div", { key: "session", className: "dshd-note" }, t("会话:") + item.sessionId + (Number.isInteger(item.recordSeq) ? t(" · 记录 ") + item.recordSeq : "")) : null,
1382
+ h("div", { key: "date", className: "dshd-note" }, t("记录时间:") + formatDate(item.observedAt)),
1383
+ item.verificationReason ? h("div", { key: "reason", className: "dshd-note" }, t("核验说明:") + item.verificationReason) : null,
1384
+ item.sourceHash ? h("code", { key: "hash", className: "dshd-cmd", title: item.sourceHash }, t("来源校验值:") + item.sourceHash) : null,
1385
+ ]);
1386
+ })),
980
1387
  ]),
981
- props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, "为什么被扣下:" + row.holdReasonLabel) : null,
1388
+ props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, t("为什么被扣下:") + row.holdReasonLabel) : null,
982
1389
  ]),
983
- h(ReviewControls, { row: row, key: row.id + "@" + row.revision }),
1390
+ h(ReviewControls, { row: row, key: row.id + "@" + row.revision, onChanged: props.onChanged }),
984
1391
  ]);
985
1392
  }
986
1393
 
@@ -993,22 +1400,22 @@ function KnowledgeCard(props) {
993
1400
  const noticeNode = notice === "" ? null : h("div", { className: "dshd-knotice", key: "notice", role: "status" }, notice);
994
1401
  const head = function (extra) {
995
1402
  return h("div", { className: "dshd-khead", key: "head" }, [
996
- h("h4", { key: "t" }, "已审阅经验"),
1403
+ h("h4", { key: "t" }, t("已审阅经验")),
997
1404
  h("span", { className: "dshd-spacer", key: "s" }),
998
1405
  extra,
999
1406
  ]);
1000
1407
  };
1001
1408
  if (props.error !== null && props.error !== undefined) {
1002
1409
  return h("section", { className: "dshd-card dshd-kcard" }, [
1003
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1004
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1005
- h("div", { className: "dshd-note", key: "n" }, "旧日记不受影响;修好后点「重试」。"),
1410
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1411
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取经验失败:") + displayError(props.error)),
1412
+ h("div", { className: "dshd-note", key: "n" }, t("旧日记不受影响;修好后点「重试」。")),
1006
1413
  ]);
1007
1414
  }
1008
1415
  if (props.empty === true) {
1009
1416
  const empty = knowledgeEmptyState();
1010
1417
  return h("section", { className: "dshd-card dshd-kcard" }, [
1011
- h("h4", { key: "t" }, "经验"),
1418
+ h("h4", { key: "t" }, t("经验")),
1012
1419
  noticeNode,
1013
1420
  h("div", { className: "dshd-kempty", key: "e" }, [
1014
1421
  h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
@@ -1019,21 +1426,21 @@ function KnowledgeCard(props) {
1019
1426
  }
1020
1427
  return h("section", { className: "dshd-card dshd-kcard" }, [
1021
1428
  head(props.loading === true
1022
- ? h("span", { className: "dshd-hint", key: "loading" }, "加载中…")
1429
+ ? h("span", { className: "dshd-hint", key: "loading" }, t("加载中…"))
1023
1430
  : h("span", { className: "dshd-hint", key: "count" }, knowledgeCountText(vm.stats))),
1024
1431
  noticeNode,
1025
- h("label", { className: "dshd-note", key: "filter" }, ["查看 ", h("select", { "aria-label": "经验状态", value: stateFilter, onChange: function (event) { setStateFilter(event.target.value); setPageSize(5); } }, [
1026
- h("option", { key: "usable", value: "usable" }, "可用经验"), h("option", { key: "rejected", value: "rejected" }, "已驳回历史"),
1432
+ h("label", { className: "dshd-note", key: "filter" }, [t("查看 "), h("select", { "aria-label": t("经验状态"), value: stateFilter, onChange: function (event) { setStateFilter(event.target.value); setPageSize(5); } }, [
1433
+ h("option", { key: "usable", value: "usable" }, t("可用经验")), h("option", { key: "rejected", value: "rejected" }, t("已驳回历史")),
1027
1434
  ])]),
1028
- reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? "暂无可用经验;待核实的记录在下方。" : "没有已驳回记录。") : null,
1435
+ reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? t("暂无可用经验;待核实的记录在下方。") : t("没有已驳回记录。")) : null,
1029
1436
  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 });
1437
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true });
1031
1438
  })),
1032
- reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多经验") : null,
1439
+ reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, t("显示更多经验")) : null,
1033
1440
  vm.shown < vm.total
1034
- ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条经验。")
1441
+ ? h("div", { className: "dshd-note", key: "more" }, t("只显示最近 ") + String(vm.shown) + " / " + String(vm.total) + t(" 条经验。"))
1035
1442
  : null,
1036
- h("div", { className: "dshd-note", key: "note" }, "采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。"),
1443
+ h("div", { className: "dshd-note", key: "note" }, t("采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。")),
1037
1444
  ]);
1038
1445
  }
1039
1446
 
@@ -1046,34 +1453,34 @@ function ReviewQueueCard(props) {
1046
1453
  : [];
1047
1454
  const head = function (extra) {
1048
1455
  return h("div", { className: "dshd-khead", key: "head" }, [
1049
- h("h4", { key: "t" }, "待核实"),
1456
+ h("h4", { key: "t" }, t("待核实")),
1050
1457
  h("span", { className: "dshd-spacer", key: "s" }),
1051
1458
  extra,
1052
1459
  ]);
1053
1460
  };
1054
1461
  if (props.error !== null && props.error !== undefined) {
1055
1462
  return h("section", { className: "dshd-card dshd-kcard" }, [
1056
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1057
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1463
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1464
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取经验失败:") + displayError(props.error)),
1058
1465
  ]);
1059
1466
  }
1060
- if (props.loading === true) {
1467
+ if (props.loading === true && rows.length === 0) {
1061
1468
  return h("section", { className: "dshd-card dshd-kcard" }, [
1062
- head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1469
+ head(h("span", { className: "dshd-hint", key: "l" }, t("加载中…"))),
1063
1470
  ]);
1064
1471
  }
1065
1472
  return h("section", { className: "dshd-card dshd-kcard" }, [
1066
- head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + " 条待核实")),
1473
+ head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + t(" 条待核实"))),
1067
1474
  rows.length === 0
1068
1475
  ? h("div", { className: "dshd-kempty", key: "empty" }, [
1069
- h("div", { className: "dshd-kempty-title", key: "t" }, "没有待核实的经验"),
1476
+ h("div", { className: "dshd-kempty-title", key: "t" }, t("没有待核实的经验")),
1070
1477
  h("p", { className: "dshd-kempty-body", key: "b" }, reviewQueueEmptyState()),
1071
1478
  ])
1072
1479
  : 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 });
1480
+ return h(KnowledgeRow, { key: row.id !== "" ? row.id : "held-" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true, showHoldReason: true });
1074
1481
  })),
1075
- rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多待核实经验") : null,
1076
- h("div", { className: "dshd-note", key: "note" }, "这些经验默认不会作为任务建议注入;扣留原因来自检索层的 skipped reason。复制审阅命令后到对话里执行,面板不会写文件。"),
1482
+ rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, t("显示更多待核实经验")) : null,
1483
+ h("div", { className: "dshd-note", key: "note" }, t("这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。")),
1077
1484
  ]);
1078
1485
  }
1079
1486
 
@@ -1082,26 +1489,26 @@ function BridgeLogCard(props) {
1082
1489
  const vm = props.vm;
1083
1490
  const head = function (extra) {
1084
1491
  return h("div", { className: "dshd-khead", key: "head" }, [
1085
- h("h4", { key: "t" }, "写入记录"),
1492
+ h("h4", { key: "t" }, t("写入记录")),
1086
1493
  h("span", { className: "dshd-spacer", key: "s" }),
1087
1494
  extra,
1088
1495
  ]);
1089
1496
  };
1090
1497
  if (props.error !== null && props.error !== undefined) {
1091
1498
  return h("section", { className: "dshd-card dshd-kcard" }, [
1092
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1093
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取写入记录失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1499
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1500
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取写入记录失败:") + displayError(props.error)),
1094
1501
  ]);
1095
1502
  }
1096
1503
  if (props.loading === true) {
1097
1504
  return h("section", { className: "dshd-card dshd-kcard" }, [
1098
- head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1505
+ head(h("span", { className: "dshd-hint", key: "l" }, t("加载中…"))),
1099
1506
  ]);
1100
1507
  }
1101
1508
  if (vm.rows.length === 0) {
1102
1509
  const empty = bridgeEmptyState();
1103
1510
  return h("section", { className: "dshd-card dshd-kcard" }, [
1104
- h("h4", { key: "t" }, "写入记录"),
1511
+ h("h4", { key: "t" }, t("写入记录")),
1105
1512
  h("div", { className: "dshd-kempty", key: "empty" }, [
1106
1513
  h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
1107
1514
  h("p", { className: "dshd-kempty-body", key: "b" }, empty.body),
@@ -1110,7 +1517,7 @@ function BridgeLogCard(props) {
1110
1517
  ]);
1111
1518
  }
1112
1519
  return h("section", { className: "dshd-card dshd-kcard" }, [
1113
- head(h("span", { className: "dshd-hint", key: "c" }, String(vm.total) + " 次写入 · " + String(vm.rollbackable) + " 次可回滚")),
1520
+ head(h("span", { className: "dshd-hint", key: "c" }, String(vm.total) + t(" 次写入 · ") + String(vm.rollbackable) + t(" 次可回滚"))),
1114
1521
  h("div", { className: "dshd-krows", key: "rows" }, vm.rows.map(function (row, index) {
1115
1522
  return h("div", { className: "dshd-bridge", key: row.id !== "" ? row.id : "bridge-" + String(index) }, [
1116
1523
  h("div", { className: "dshd-bridge-head", key: "h" }, [
@@ -1120,21 +1527,21 @@ function BridgeLogCard(props) {
1120
1527
  h("span", { className: "dshd-klast", key: "at", title: row.atTitle }, row.atText),
1121
1528
  ]),
1122
1529
  h("div", { className: "dshd-bridge-meta", key: "m" }, [
1123
- h("span", { key: "lessons", title: row.lessonsText }, "经验:" + row.lessonsText),
1124
- row.beforeShort === "" ? null : h("span", { key: "before" }, "前哈希:" + row.beforeShort),
1125
- row.afterShort === "" ? null : h("span", { key: "after" }, "后哈希:" + row.afterShort),
1530
+ h("span", { key: "lessons", title: row.lessonsText }, t("经验:") + row.lessonsText),
1531
+ row.beforeShort === "" ? null : h("span", { key: "before" }, t("前哈希:") + row.beforeShort),
1532
+ row.afterShort === "" ? null : h("span", { key: "after" }, t("后哈希:") + row.afterShort),
1126
1533
  h("span", { key: "rb", className: row.rollbackable === true ? "dshd-rollback-ok" : "dshd-rollback-no" }, row.rollbackText),
1127
1534
  ]),
1128
1535
  h("details", { key: "backup" }, [
1129
- h("summary", { key: "summary" }, "备份标识与回滚命令"),
1536
+ h("summary", { key: "summary" }, t("备份标识与回滚命令")),
1130
1537
  h("code", { key: "id", className: "dshd-cmd" }, row.id),
1131
1538
  row.rollbackable === true ? h("code", { key: "command", className: "dshd-cmd", tabIndex: 0 }, bridgeRollbackCommandOf(row)) : null,
1132
1539
  ]),
1133
- row.rollbackable === true ? h(CopyCommand, { key: "rollback", label: "复制回滚命令", command: bridgeRollbackCommandOf(row), title: "粘贴到对应项目对话执行;只恢复管理块,保留块外人工内容" }) : null,
1540
+ row.rollbackable === true ? h(RollbackControl, { key: "rollback", row: row, onChanged: props.onChanged }) : null,
1134
1541
  ]);
1135
1542
  })),
1136
- 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 返回的备份标识在对话里执行,面板不会写文件。"),
1543
+ vm.shown < vm.total ? h("div", { className: "dshd-note", key: "more" }, t("只显示最近 ") + String(vm.shown) + " / " + String(vm.total) + t(" 条记录。")) : null,
1544
+ h("div", { className: "dshd-note", key: "note" }, t("回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。")),
1138
1545
  ]);
1139
1546
  }
1140
1547
 
@@ -1149,12 +1556,12 @@ function DreamCard(props) {
1149
1556
  return h("article", {
1150
1557
  className: "dshd-dream dshd-mood-" + String(moodSlot(dream.mood)) + (privacy && peek === true ? " dshd-peek" : ""),
1151
1558
  onClick: clickCard,
1152
- title: privacy ? "隐私模式:点击卡片临时查看" : undefined,
1559
+ title: privacy ? t("隐私模式:点击卡片临时查看") : undefined,
1153
1560
  }, [
1154
1561
  h("header", { className: "dshd-dream-head", key: "h" }, [
1155
1562
  h("time", { className: "dshd-time", key: "t", dateTime: dream.at === "" ? undefined : dream.at, title: dream.at === "" ? timeText : timeText + " · " + dream.at }, timeText),
1156
1563
  dreamAgo(dream, props.now) === "" ? null : h("span", { className: "dshd-ago", key: "a" }, dreamAgo(dream, props.now)),
1157
- h("span", { className: "dshd-mood", key: "m", title: "心境:" + dream.mood }, [
1564
+ h("span", { className: "dshd-mood", key: "m", title: t("心境:") + dream.mood }, [
1158
1565
  h("i", { className: "dshd-mooddot " + toneClass(moodSlot(dream.mood)), key: "d" }),
1159
1566
  dream.mood,
1160
1567
  ]),
@@ -1164,7 +1571,7 @@ function DreamCard(props) {
1164
1571
  className: "dshd-linkbtn",
1165
1572
  key: "btn",
1166
1573
  onClick: function (event) { if (event !== undefined && event.stopPropagation !== undefined) event.stopPropagation(); setExpanded(expanded !== true); },
1167
- }, expanded === true ? "收起" : "展开全文") : null,
1574
+ }, expanded === true ? t("收起") : t("展开全文")) : null,
1168
1575
  ]),
1169
1576
  h("div", { className: "dshd-reflection" + (collapsible && expanded !== true ? " dshd-clamp" : ""), key: "r" }, inlineNodes(dream.reflection, "reflection-" + String(props.index))),
1170
1577
  dream.lessons.length === 0 ? null : h("ol", { className: "dshd-lessons", key: "l" }, dream.lessons.map((lesson, index) => h("li", { key: "li-" + String(index) + "-" + lesson }, [
@@ -1181,17 +1588,17 @@ function EmptyState(props) {
1181
1588
  if (props.query !== undefined && props.query !== "") {
1182
1589
  return h("div", { className: "dshd-empty" }, [
1183
1590
  moon,
1184
- h("h3", { key: "t" }, "没有找到「" + props.query + "」相关的梦"),
1185
- h("p", { key: "p" }, "换个关键词试试,或者清空搜索看全部梦境。"),
1186
- props.onClear ? h("div", { key: "a" }, h(Btn, { onClick: props.onClear }, "清空搜索")) : null,
1591
+ h("h3", { key: "t" }, t("没有找到「") + props.query + t("」相关的梦")),
1592
+ h("p", { key: "p" }, t("换个关键词试试,或者清空搜索看全部梦境。")),
1593
+ props.onClear ? h("div", { key: "a" }, h(Btn, { onClick: props.onClear }, t("清空搜索"))) : null,
1187
1594
  ]);
1188
1595
  }
1189
1596
  return h("div", { className: "dshd-empty" }, [
1190
1597
  moon,
1191
- h("h3", { key: "t" }, "还没有梦境日记"),
1192
- h("p", { key: "p" }, "对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。"),
1598
+ h("h3", { key: "t" }, t("还没有梦境日记")),
1599
+ h("p", { key: "p" }, t("对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。")),
1193
1600
  h("div", { className: "dshd-chips", key: "c" }, [
1194
- h("span", { className: "dshd-chip", key: "s1" }, "对 agent 说「做个梦」"),
1601
+ h("span", { className: "dshd-chip", key: "s1" }, t("对 agent 说「做个梦」")),
1195
1602
  h("span", { className: "dshd-chip", key: "s2" }, "dream_digest → dream_save"),
1196
1603
  ]),
1197
1604
  ]);
@@ -1212,6 +1619,7 @@ function DreamPanel() {
1212
1619
  const [queryInput, setQueryInput] = useState("");
1213
1620
  const [query, setQuery] = useState("");
1214
1621
  const [reloadToken, setReloadToken] = useState(0);
1622
+ const [actionNotice, setActionNotice] = useState("");
1215
1623
  const [now, setNow] = useState(function () { return new Date(); });
1216
1624
  const [privacy, setPrivacy] = useState(loadPrivacy);
1217
1625
  const [filters, setFilters] = useState({ moods: [], withLessons: false });
@@ -1256,7 +1664,7 @@ function DreamPanel() {
1256
1664
  setKnowledgeError(null);
1257
1665
  fetchKnowledge().then(function (payload) {
1258
1666
  if (alive !== true) return;
1259
- setKnowledge(normalizeKnowledge(payload, new Date()));
1667
+ setKnowledge(payload);
1260
1668
  setKnowledgeLoading(false);
1261
1669
  }).catch(function (failure) {
1262
1670
  if (alive !== true) return;
@@ -1274,7 +1682,7 @@ function DreamPanel() {
1274
1682
  setBridgeError(null);
1275
1683
  fetchBridgeRecords().then(function (payload) {
1276
1684
  if (alive !== true) return;
1277
- setBridge(normalizeBridgeRecords(payload, new Date()));
1685
+ setBridge(payload);
1278
1686
  setBridgeLoading(false);
1279
1687
  }).catch(function (failure) {
1280
1688
  if (alive !== true) return;
@@ -1286,8 +1694,8 @@ function DreamPanel() {
1286
1694
  }, [reloadToken]);
1287
1695
 
1288
1696
  const vm = viewModelOf(dreams, stats, now);
1289
- const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : knowledge;
1290
- const bridgeVm = bridge === null ? { rows: [], total: 0, shown: 0, rollbackable: 0 } : bridge;
1697
+ const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : normalizeKnowledge(knowledge, now);
1698
+ const bridgeVm = bridge === null ? { rows: [], total: 0, shown: 0, rollbackable: 0 } : normalizeBridgeRecords(bridge, now);
1291
1699
  const pristine = vm.count === 0 && query === "";
1292
1700
  const filterCount = filterCountOf(filters);
1293
1701
  const visibleDreams = applyFilters(dreams, filters);
@@ -1312,6 +1720,7 @@ function DreamPanel() {
1312
1720
  };
1313
1721
  const clearFilters = function () { setFilters({ moods: [], withLessons: false }); };
1314
1722
  const showMore = function () { if (limit < MORE_LIMIT) setLimit(MORE_LIMIT); };
1723
+ const onChanged = function (message) { setActionNotice(message); setReloadToken(function (value) { return value + 1; }); };
1315
1724
  const showKnowledge = function () { if (knowledgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
1316
1725
  const knowledgeCard = h(KnowledgeCard, {
1317
1726
  key: "knowledge",
@@ -1321,6 +1730,7 @@ function DreamPanel() {
1321
1730
  empty: knowledgeError === null && knowledgeLoading !== true && knowledgeVm.rows.length === 0,
1322
1731
  privacy: privacy === true,
1323
1732
  onRetry: showKnowledge,
1733
+ onChanged: onChanged,
1324
1734
  });
1325
1735
  const reviewQueueCard = h(ReviewQueueCard, {
1326
1736
  key: "review-queue",
@@ -1329,6 +1739,7 @@ function DreamPanel() {
1329
1739
  error: knowledgeError,
1330
1740
  privacy: privacy === true,
1331
1741
  onRetry: showKnowledge,
1742
+ onChanged: onChanged,
1332
1743
  });
1333
1744
  const showBridge = function () { if (bridgeLoading !== true) setReloadToken(function (value) { return value + 1; }); };
1334
1745
  const bridgeLogCard = h(BridgeLogCard, {
@@ -1337,6 +1748,7 @@ function DreamPanel() {
1337
1748
  loading: bridgeLoading === true,
1338
1749
  error: bridgeError,
1339
1750
  onRetry: showBridge,
1751
+ onChanged: onChanged,
1340
1752
  });
1341
1753
 
1342
1754
  // 筛选 popover:点面板外任意处收起;纯 node 冒烟没有事件系统就跳过。
@@ -1375,8 +1787,8 @@ function DreamPanel() {
1375
1787
  className: "dshd-input",
1376
1788
  type: "text",
1377
1789
  value: queryInput,
1378
- placeholder: "搜索反思与教训…",
1379
- "aria-label": "搜索梦境",
1790
+ placeholder: t("搜索反思与教训…"),
1791
+ "aria-label": t("搜索梦境"),
1380
1792
  onChange: function (event) { setQueryInput(event.target.value); },
1381
1793
  onKeyDown: function (event) { if (event.key === "Enter") setQuery(event.target.value.trim()); },
1382
1794
  }),
@@ -1384,13 +1796,13 @@ function DreamPanel() {
1384
1796
  key: "clear-search",
1385
1797
  type: "button",
1386
1798
  className: "dshd-clear",
1387
- "aria-label": "清空搜索",
1799
+ "aria-label": t("清空搜索"),
1388
1800
  onClick: clearSearch,
1389
1801
  }, "×"),
1390
1802
  ]),
1391
1803
  h("span", { className: "dshd-spacer", key: "spacer" }),
1392
1804
  pristine || activeTab !== "journal" ? null : h("span", { className: "dshd-hint", key: "count" }, loading === true
1393
- ? "加载中…"
1805
+ ? t("加载中…")
1394
1806
  : countTextOf(query, vm.total, vm.count, filterCount, visibleDreams.length)),
1395
1807
  showFilterButton === true && activeTab === "journal" ? h("div", { className: "dshd-filter", key: "filter" }, [
1396
1808
  h(Btn, {
@@ -1398,10 +1810,10 @@ function DreamPanel() {
1398
1810
  on: filterCount > 0,
1399
1811
  pressed: filterCount > 0,
1400
1812
  onClick: function () { setFilterOpen(filterOpen !== true); },
1401
- title: "筛选作用于已加载窗口,不影响全量统计",
1402
- }, "筛选" + (filterCount > 0 ? " · " + String(filterCount) : "") + " ▾"),
1403
- filterOpen === true ? h("div", { className: "dshd-filterpop", key: "pop", role: "group", "aria-label": "筛选梦境" }, [
1404
- h("div", { className: "dshd-filtertitle", key: "title" }, "筛选当前已加载的 " + String(vm.count) + " 场,不影响全量统计"),
1813
+ title: t("筛选作用于已加载窗口,不影响全量统计"),
1814
+ }, t("筛选") + (filterCount > 0 ? " · " + String(filterCount) : "") + " ▾"),
1815
+ filterOpen === true ? h("div", { className: "dshd-filterpop", key: "pop", role: "group", "aria-label": t("筛选梦境") }, [
1816
+ h("div", { className: "dshd-filtertitle", key: "title" }, t("筛选当前已加载的 ") + String(vm.count) + t(" 场,不影响全量统计")),
1405
1817
  h("div", { key: "moods" }, vm.moodRows.map(function (row) {
1406
1818
  const on = filters.moods.indexOf(row.mood) !== -1;
1407
1819
  return h("button", {
@@ -1417,10 +1829,10 @@ function DreamPanel() {
1417
1829
  className: "dshd-chipbtn" + (filters.withLessons === true ? " on" : ""),
1418
1830
  "aria-pressed": filters.withLessons === true ? "true" : "false",
1419
1831
  onClick: toggleOnlyLessons,
1420
- }, "只看有教训的梦")),
1832
+ }, t("只看有教训的梦"))),
1421
1833
  h("div", { className: "dshd-filterfoot", key: "foot" }, [
1422
- h("button", { key: "clearbtn", type: "button", className: "dshd-linkbtn", onClick: clearFilters, disabled: filterCount === 0 }, "清空筛选"),
1423
- h(Btn, { key: "done", onClick: function () { setFilterOpen(false); } }, "完成"),
1834
+ h("button", { key: "clearbtn", type: "button", className: "dshd-linkbtn", onClick: clearFilters, disabled: filterCount === 0 }, t("清空筛选")),
1835
+ h(Btn, { key: "done", onClick: function () { setFilterOpen(false); } }, t("完成")),
1424
1836
  ]),
1425
1837
  ]) : null,
1426
1838
  ]) : null,
@@ -1429,26 +1841,26 @@ function DreamPanel() {
1429
1841
  onClick: togglePrivacy,
1430
1842
  on: privacy === true,
1431
1843
  pressed: privacy === true,
1432
- title: "显示层模糊;DOM 仍为明文,不改文件、不加密",
1433
- }, privacy === true ? "隐私模式:开" : "隐私模式"),
1434
- h(Btn, { key: "refresh", onClick: reload, disabled: loading === true, title: "重新读取梦境日记" }, "刷新"),
1844
+ title: t("显示层模糊;DOM 仍为明文,不改文件、不加密"),
1845
+ }, privacy === true ? t("隐私模式:开") : t("隐私模式")),
1846
+ h(Btn, { key: "refresh", onClick: reload, disabled: loading === true, title: t("重新读取梦境日记") }, t("刷新")),
1435
1847
  ]);
1436
1848
 
1437
1849
  const errorBox = error === null ? null : h("div", { className: "dshd-alert error", role: "alert", key: "error" }, [
1438
- h("span", { key: "m" }, "读取梦境日记失败:" + (error.message === "" ? String(error) : error.message)),
1850
+ h("span", { key: "m" }, t("读取梦境日记失败:") + displayError(error)),
1439
1851
  h("span", { className: "dshd-spacer", key: "s" }),
1440
- h(Btn, { key: "retry", onClick: reload, variant: "primary" }, "重试"),
1852
+ h(Btn, { key: "retry", onClick: reload, variant: "primary" }, t("重试")),
1441
1853
  ]);
1442
1854
 
1443
1855
  const body = function () {
1444
- if (activeTab === "knowledge") return h("div", { key: "knowledge-tab" }, [knowledgeCard, reviewQueueCard]);
1445
- if (activeTab === "bridge") return bridgeLogCard;
1856
+ if (activeTab === "knowledge") return h("div", { key: "knowledge-tab" }, [actionNotice ? h("div", { key: "notice", className: "dshd-knotice", role: "status" }, actionNotice) : null, knowledgeCard, reviewQueueCard]);
1857
+ if (activeTab === "bridge") return h("div", { key: "bridge-tab" }, [actionNotice ? h("div", { key: "notice", className: "dshd-knotice", role: "status" }, actionNotice) : null, bridgeLogCard]);
1446
1858
  if (loading === true && vm.count === 0) return h(Skeleton, { key: "skeleton" });
1447
1859
  if (vm.count === 0) {
1448
1860
  if (error !== null) return h("div", { key: "failed-wrap" }, [
1449
1861
  h("div", { className: "dshd-empty", key: "failed" }, [
1450
- h("h3", { key: "t" }, "暂时读不到梦境日记"),
1451
- h("p", { key: "p" }, "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。"),
1862
+ h("h3", { key: "t" }, t("暂时读不到梦境日记")),
1863
+ h("p", { key: "p" }, t("等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。")),
1452
1864
  ]),
1453
1865
  ]);
1454
1866
  return h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch });
@@ -1456,10 +1868,10 @@ function DreamPanel() {
1456
1868
  return h("div", { key: "content" }, [
1457
1869
  vm.showTiles
1458
1870
  ? h("div", { className: "dshd-tiles", key: "tiles" }, [
1459
- h(StatTile, { key: "total", label: "梦境总数", value: String(vm.total) + " 场", hint: vm.total > vm.count ? "本页显示最近 " + String(vm.count) + " 场" : "全部梦境日记" }),
1460
- h(StatTile, { key: "span", label: "时间跨度", value: vm.spanText, hint: vm.spanHint, title: vm.spanFull }),
1461
- h(StatTile, { key: "latest", label: "最近一场", value: vm.latestText, hint: vm.latestDate }),
1462
- h(StatTile, { key: "mood", label: "心境", value: String(vm.moodCount) + " 种", hint: vm.moodCount === 0 ? "" : "最常见:" + vm.moodRows[0].mood }),
1871
+ h(StatTile, { key: "total", label: t("梦境总数"), value: String(vm.total) + t(" 场"), hint: vm.total > vm.count ? t("本页显示最近 ") + String(vm.count) + t(" 场") : t("全部梦境日记") }),
1872
+ h(StatTile, { key: "span", label: t("时间跨度"), value: vm.spanText, hint: vm.spanHint, title: vm.spanFull }),
1873
+ h(StatTile, { key: "latest", label: t("最近一场"), value: vm.latestText, hint: vm.latestDate }),
1874
+ h(StatTile, { key: "mood", label: t("心境"), value: String(vm.moodCount) + t(" 种"), hint: vm.moodCount === 0 ? "" : t("最常见:") + vm.moodRows[0].mood }),
1463
1875
  ])
1464
1876
  : h("div", { className: "dshd-headline", key: "headline" }, vm.headline),
1465
1877
  h("div", { className: "dshd-two", key: "two" }, [
@@ -1467,14 +1879,14 @@ function DreamPanel() {
1467
1879
  h(LessonCard, { key: "lessons", vm: vm }),
1468
1880
  ]),
1469
1881
  h("div", { className: "dshd-listhead", key: "listhead" }, [
1470
- h("h4", { key: "t" }, query === "" && filterCount === 0 ? "梦境时间线" : (query === "" ? "筛选结果" : "搜索结果")),
1882
+ h("h4", { key: "t" }, query === "" && filterCount === 0 ? t("梦境时间线") : (query === "" ? t("筛选结果") : t("搜索结果"))),
1471
1883
  h("span", { className: "dshd-hint", key: "n" }, listHintOf(query, vm.count, filterCount, visibleDreams.length)),
1472
1884
  ]),
1473
1885
  visibleDreams.length === 0 && filterCount > 0
1474
1886
  ? h("div", { className: "dshd-filterempty", key: "filtered" }, [
1475
- h("span", { key: "t" }, "没有符合筛选的梦(筛选作用于已加载的 " + String(vm.count) + " 场)。"),
1887
+ h("span", { key: "t" }, t("没有符合筛选的梦(筛选作用于已加载的 ") + String(vm.count) + t(" 场)。")),
1476
1888
  h("span", { className: "dshd-spacer", key: "s" }),
1477
- h(Btn, { key: "clearbtn", onClick: clearFilters }, "清空筛选"),
1889
+ h(Btn, { key: "clearbtn", onClick: clearFilters }, t("清空筛选")),
1478
1890
  ])
1479
1891
  : h("div", { className: "dshd-timeline", key: "timeline" }, visibleDreams.map(function (dream, index) {
1480
1892
  return h(DreamCard, { key: (dream.id !== "" ? dream.id : "dream") + "#" + String(index), dream: dream, now: now, privacy: privacy, index: index });
@@ -1485,26 +1897,27 @@ function DreamPanel() {
1485
1897
  return h("div", { className: "dshd-root" + (privacy === true ? " dshd-privacy" : ""), key: "root" }, [
1486
1898
  h("div", { className: "dshd-shell", key: "shell" }, [
1487
1899
  errorBox,
1488
- h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": "梦境面板视图" }, [["journal", "日记"], ["knowledge", "经验"], ["bridge", "写入记录"]].map(function (item) {
1900
+ h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": t("梦境面板视图") }, [["journal", t("日记")], ["knowledge", t("经验")], ["bridge", t("写入记录")]].map(function (item) {
1489
1901
  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]);
1490
1902
  })),
1491
1903
  toolbar,
1492
- privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。") : null,
1904
+ privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, t("隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。")) : null,
1493
1905
  body(),
1494
1906
  activeTab !== "journal" ? null : h("div", { className: "dshd-footer", key: "footer" }, [
1495
1907
  h("span", { key: "l" }, footerTextOf(query, limit, vm.total, vm.count)),
1496
1908
  h("span", { className: "dshd-spacer", key: "sp" }),
1497
- canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? "加载中…" : "显示更早的梦") : null,
1498
- h("span", { key: "r" }, "数据来自梦境日记(dreams.jsonl)"),
1909
+ canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? t("加载中…") : t("显示更早的梦")) : null,
1910
+ h("span", { key: "r" }, t("数据来自梦境日记(dreams.jsonl)")),
1499
1911
  ]),
1500
1912
  ]),
1501
1913
  ]);
1502
1914
  }
1503
1915
 
1504
- function DreamSection() {
1916
+ function DreamSection({ locale } = {}) {
1917
+ useLanguage(locale);
1505
1918
  return h("div", null, [
1506
- h("h2", { key: "t" }, "梦境日记"),
1507
- h("p", { className: "dshd-sub", key: "p" }, "会话回放 → 反思 → 记忆巩固。这里按时间倒序陈列 agent 做过的梦,附心境方块与教训榜。"),
1919
+ h("h2", { key: "t" }, t("梦境日记")),
1920
+ h("p", { className: "dshd-sub", key: "p" }, t("会话回放 → 反思 → 记忆巩固。这里按时间倒序陈列 agent 做过的梦,附心境方块与教训榜。")),
1508
1921
  h(DreamPanel, { key: "panel" }),
1509
1922
  ]);
1510
1923
  }
@@ -1628,6 +2041,11 @@ const CSS = [
1628
2041
  ".dshd-kbody details summary,.dshd-bridge-row details summary{cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}",
1629
2042
  ".dshd-reviewform{margin-top:8px;padding:9px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:8px}",
1630
2043
  ".dshd-reviewform label{display:flex;flex-direction:column;gap:3px;margin-bottom:6px;font-size:12px}",
2044
+ ".dshd-reviewform .dshd-btn,.dshd-feedback .dshd-btn{margin:4px 5px 0 0}",
2045
+ ".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}",
2046
+ ".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}",
2047
+ ".dshd-sources{padding:6px 0}.dshd-sources details{margin-top:6px}.dshd-reviewform{min-width:0}",
2048
+ ".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
2049
  ".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
2050
  ".dshd-copywrap{display:inline-flex;flex-direction:column;gap:3px;max-width:100%;align-items:flex-start}",
1633
2051
  ".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,15 +2056,17 @@ const CSS = [
1638
2056
  ".dshd-bridge-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:3px}",
1639
2057
  ".dshd-rollback-ok{color:var(--dsw-alias-state-success-primary,#16a34a)}",
1640
2058
  ".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}}"
2059
+ "@container (max-width:620px){.dshd-two{grid-template-columns:1fr}.dshd-toolbar .dshd-hint{display:none}}",
2060
+ "@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
2061
  ].join("");
1643
2062
  /* @ui-css-end */
1644
2063
 
1645
2064
  // ───────────────────────── 挂载 ─────────────────────────
1646
2065
 
1647
- const inject = ["slots"];
2066
+ const inject = ["slots", "locale"];
1648
2067
 
1649
2068
  function apply(ctx) {
2069
+ try { localeService = ctx.get?.("locale") || ctx.locale; } catch { localeService = ctx.locale; }
1650
2070
  try {
1651
2071
  ctx.effect(function () {
1652
2072
  const id = "dsh-dream/client";
@@ -1666,8 +2086,8 @@ function apply(ctx) {
1666
2086
  name: "settings.section",
1667
2087
  id: "dsh-dream",
1668
2088
  order: 55,
1669
- label: function () { return "梦境日记"; },
1670
- inject: function () { return {}; },
2089
+ label: function () { return t("梦境日记"); },
2090
+ inject: function () { return { locale: localeService }; },
1671
2091
  }, DreamSection);
1672
2092
  });
1673
2093
  } catch (error) { /* 没有 slots 服务:仍是合法插件,只是没有设置页 */ }
@@ -1677,12 +2097,14 @@ exports.apply = apply;
1677
2097
  exports.inject = inject;
1678
2098
  // 纯函数内部件:验证脚本可直接取来测,不必另抄一份算法。
1679
2099
  exports.__internals = {
2100
+ displayError: displayError,
1680
2101
  routeUrl: routeUrl,
1681
2102
  routeUrlFor: routeUrlFor,
1682
2103
  KNOWLEDGE_ROUTE: KNOWLEDGE_ROUTE,
1683
2104
  KNOWLEDGE_LIMIT: KNOWLEDGE_LIMIT,
1684
2105
  fetchKnowledge: fetchKnowledge,
1685
2106
  fetchBridgeRecords: fetchBridgeRecords,
2107
+ postAction: postAction,
1686
2108
  copyText: copyText,
1687
2109
  normalizeBridgeRecords: normalizeBridgeRecords,
1688
2110
  skippedReasonLabel: skippedReasonLabel,