@stardustlc/dsh-dream 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2,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,6 +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
+ "自动整理的内容在「经验」页待审阅;日记可让助手复盘时单独生成。": "Automatically collected lessons await review in Lessons. Ask the assistant to reflect when you want a separate journal entry.",
20
+ "查看待审阅经验": "Review candidate lessons",
21
+ "请求失败(HTTP ": "Request failed (HTTP ",
22
+ "服务器返回的不是 JSON(HTTP ": "The server did not return JSON (HTTP ",
23
+ "登录状态已失效,请重新打开 DSH 页面后重试。": "Your session has expired. Reopen DSH and try again.",
24
+ "当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。": "This host does not support this action. Update DSH or use the chat command below.",
25
+ "候选(未采纳)": "Candidate (not accepted)",
26
+ "可用": "Usable",
27
+ "有冲突": "Disputed",
28
+ "待复核": "Needs review",
29
+ "已驳回": "Rejected",
30
+ "未知状态": "Unknown state",
31
+ "平台 ": "Platform ",
32
+ ";等 ": "; and ",
33
+ " 条适用范围": " more conditions",
34
+ "范围未标注": "Scope not specified",
35
+ "暂无可展示的证据摘要": "No evidence summary available",
36
+ "尚未核验": "Not verified yet",
37
+ "没有 lastValidatedAt(从未核验)": "No lastValidatedAt (never verified)",
38
+ "未加载的经验(": "Lesson not loaded (",
39
+ "共 ": "Total: ",
40
+ " 条": " records",
41
+ "还没有经验": "No lessons yet",
42
+ "日志过大,仅回放最近一段事件": "The log is large; only recent events were replayed",
43
+ "有 ": "",
44
+ " 行损坏已跳过": " damaged lines were skipped",
45
+ ";events.jsonl 未改动。": "; events.jsonl was preserved.",
46
+ "还没有经验记录": "No lesson records yet",
47
+ "经验是带证据、有适用条件的技术结论:收尾时用 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.",
48
+ "旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。": "Journal observations do not become lessons automatically. Use dream_review to accept, reject or flag conflicts.",
49
+ "已驳回:不参与默认检索与注入": "Rejected: excluded from default retrieval and injection",
50
+ "待复核:条件或依赖可能已过期,默认不注入": "Needs review: conditions or dependencies may be outdated; not injected by default",
51
+ "冲突未解决:默认不注入,先 resolve-conflict": "Unresolved conflict: not injected by default; use resolve-conflict first",
52
+ "候选:可被检索到,但默认不作为任务建议注入": "Candidate: retrievable, but not injected as task advice by default",
53
+ "证据不足:没有独立证据,默认不注入": "Insufficient independent evidence: not injected by default",
54
+ "版本不匹配:当前依赖版本不满足适用范围": "Version mismatch: the dependency version is outside the applicable range",
55
+ "超出条数预算,本次未取回": "Not retrieved: record limit reached",
56
+ "超出字符预算:按排名整条装入,遇到第一条放不下即停": "Character limit reached: complete records are added by rank until the next one cannot fit",
57
+ "超出字符预算,本次未取回": "Not retrieved: character limit reached",
58
+ "新建": "Created",
59
+ "替换管理块": "Replaced managed block",
60
+ "追加管理块": "Appended managed block",
61
+ "未改动": "Unchanged",
62
+ "回滚": "Rolled back",
63
+ "未知动作": "Unknown action",
64
+ "时间未知": "Unknown time",
65
+ "没有记录时间": "No timestamp recorded",
66
+ "目标未记录": "Target not recorded",
67
+ "没有经验引用": "No lesson references",
68
+ "可回滚": "Rollback available",
69
+ "不可回滚": "Rollback unavailable",
70
+ "还没有写入记录": "No rule changes yet",
71
+ "写入记录来自 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.",
72
+ "在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。": "Preview an eligible project lesson, review the changes and apply them. Chat commands are also available.",
73
+ "没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。": "Candidates, disputed lessons and lessons needing review appear here, with an explanation of why they are held.",
74
+ "刚刚": "Just now",
75
+ " 分钟前": " min ago",
76
+ " 小时前": " h ago",
77
+ " 天前": " days ago",
78
+ "今天": "Today",
79
+ "昨天": "Yesterday",
80
+ "筛选后 ": "Filtered: ",
81
+ " 场": " dreams",
82
+ "」命中 ": "” matches ",
83
+ " 场梦": " dreams",
84
+ "筛选当前已加载的 ": "Filtering the ",
85
+ " 场 · 命中 ": " loaded dreams · Matches: ",
86
+ "新梦在前": "Newest first",
87
+ "(已达路由上限)": " (route limit reached)",
88
+ "最近 ": "Recent: ",
89
+ " 场 / 共 ": " dreams / Total: ",
90
+ "新梦在前 · 最多显示 ": "Newest first · Up to ",
91
+ "搜索结果 · 最多显示 ": "Search results · Up to ",
92
+ "本页内最近 ": "Most recent on this page: ",
93
+ "(统计未给 lastAt,按本页已加载的 ": "(lastAt is missing; calculated from the ",
94
+ " 场现算)": " dreams loaded on this page)",
95
+ "同一天": "Same day",
96
+ "约 ": "About ",
97
+ " 天": " days",
98
+ "第 1 场梦 · ": "First dream · ",
99
+ " · 心境「": " · Mood: “",
100
+ " 场梦 · 本页最近 ": " dreams · Loaded: ",
101
+ "最近:": "Latest: ",
102
+ "心境": "Mood",
103
+ "1 场梦,先不谈分布 —— 再做梦会累积。": "One dream so far. More dreams will build a mood history.",
104
+ "左 → 右:从早到晚": "Left → right: oldest to newest",
105
+ "教训榜": "Recurring observations",
106
+ "还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。": "No observations yet. Include lessons when recording dreams to track recurring observations.",
107
+ "常见教训": "Recurring observation",
108
+ "次数仅表示日记里的出现频率;出现 ≥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.",
109
+ "复制命令": "Copy command",
110
+ "已复制": "Copied",
111
+ "剪贴板不可用:请手动选中上面的命令复制。": "Clipboard unavailable. Select and copy the command above manually.",
112
+ "重新送审": "Reopen for review",
113
+ "处理冲突": "Resolve conflict",
114
+ "驳回这条": "Reject this lesson",
115
+ "标记待复核": "Mark for review",
116
+ "驳回": "Reject",
117
+ "采纳": "Accept",
118
+ "管理块内容已一致,无需写入。": "The managed block already matches; no write was needed.",
119
+ "请求超时,请先刷新确认结果后再重试。": "Request timed out. Refresh to check the result before retrying.",
120
+ "已更新「": "Updated the review state of “",
121
+ "」的审阅状态。": "”.",
122
+ "已记录使用反馈;「": "Feedback recorded; the review state of “",
123
+ "」的审阅状态保持原样。": "” is unchanged.",
124
+ "的操作": " — actions",
125
+ "收起审阅": "Hide review",
126
+ "审阅详情": "Review details",
127
+ "预览规则变更": "Preview rule changes",
128
+ "复制项目预览命令": "Copy project preview command",
129
+ "正在处理…": "Working…",
130
+ "已刷新经验列表。": "Lesson list refreshed.",
131
+ "刷新列表": "Refresh list",
132
+ "审阅动作": "Review action",
133
+ "冲突经验": "Conflicting lesson",
134
+ "处理方式": "Resolution",
135
+ "采用这条,驳回对方": "Accept this lesson and reject the other",
136
+ "放弃这条,保留对方": "Reject this lesson and keep the other",
137
+ "合并到这条,对方留作历史": "Merge into this lesson and retain the other as history",
138
+ "处理依据": "Reason for resolution",
139
+ "说明哪个来源支持这次决定…": "Explain which source supports this decision…",
140
+ "确认审阅": "Confirm review",
141
+ "在对话里处理": "Handle in chat",
142
+ "选择冲突经验并填写处理依据后即可处理。": "Select the conflicting lesson and provide a reason first.",
143
+ "复制审阅命令": "Copy review command",
144
+ "规则变更预览": "Rule change preview",
145
+ "目标:": "Target: ",
146
+ "管理块内容已经一致,无需更改。": "The managed block already matches; no changes are needed.",
147
+ "将按此预览重建 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.",
148
+ "规则已应用,可在「写入记录」中回滚。": "Rules applied. You can roll back from the Rule changes tab.",
149
+ "应用此变更": "Apply these changes",
150
+ "取消预览": "Cancel preview",
151
+ "使用反馈 · 有用 ": "Usage feedback · Useful: ",
152
+ " · 不适用 ": " · Not applicable: ",
153
+ "场景备注(可选)": "Context note (optional)",
154
+ "反馈场景备注": "Feedback context note",
155
+ "这次在哪个场景使用…": "Where did you use this lesson…",
156
+ "这次有用": "Useful this time",
157
+ "这次不适用": "Not applicable this time",
158
+ "记录使用情况,供之后复核;采纳与驳回由审阅决定。": "Feedback records usage for future review. Acceptance and rejection remain review decisions.",
159
+ "有用": "Useful",
160
+ "不适用": "Not applicable",
161
+ "已恢复上一次的 Dream 管理块,块外内容保留。": "The previous Dream managed block was restored; content outside the block was preserved.",
162
+ "取消回滚": "Cancel rollback",
163
+ "回滚这次变更": "Roll back this change",
164
+ "将恢复「": "Restore the previous Dream managed block in “",
165
+ "」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。": "”. Content outside the block is preserved. Rollback is refused if the managed block has changed.",
166
+ "正在回滚…": "Rolling back…",
167
+ "确认回滚管理块": "Confirm block rollback",
168
+ "隐私模式:点击此条临时查看": "Privacy mode: click to reveal temporarily",
169
+ "最近核验:": "Last verified: ",
170
+ "何时用:": "When to use: ",
171
+ "未标注适用条件": "No conditions specified",
172
+ "怎么做:": "What to do: ",
173
+ "未标注具体动作": "No action specified",
174
+ "适用范围与证据": "Scope and evidence",
175
+ "范围:": "Scope: ",
176
+ "适用:": "Applies to: ",
177
+ "例外:": "Exceptions: ",
178
+ "证据:": "Evidence: ",
179
+ "没有关联来源记录,采纳前请核对依据。": "No linked sources. Check the evidence before accepting.",
180
+ "来源记录 · ": "Source record · ",
181
+ "已读到来源": "Source read",
182
+ "待核验": "Unverified",
183
+ "会话": "Session",
184
+ "用户纠正": "User correction",
185
+ "本地资料": "Local artifact",
186
+ "未提供摘要": "No summary provided",
187
+ "会话:": "Session: ",
188
+ " · 记录 ": " · Record ",
189
+ "记录时间:": "Recorded: ",
190
+ "核验说明:": "Verification: ",
191
+ "已核对当前会话原文;不代表结论正确。": "Matched the current session source; this does not prove the conclusion is correct.",
192
+ "来源校验值:": "Source hash: ",
193
+ "为什么被扣下:": "Why held: ",
194
+ "已审阅经验": "Reviewed lessons",
195
+ "重试": "Retry",
196
+ "读取经验失败:": "Could not load lessons: ",
197
+ "旧日记不受影响;修好后点「重试」。": "Your journal is preserved. Retry after resolving the error.",
198
+ "经验": "Lessons",
199
+ "加载中…": "Loading…",
200
+ "查看 ": "Show ",
201
+ "经验状态": "Lesson state",
202
+ "可用经验": "Usable lessons",
203
+ "已驳回历史": "Rejected history",
204
+ "暂无可用经验;待核实的记录在下方。": "No usable lessons yet. Records needing review are below.",
205
+ "没有已驳回记录。": "No rejected records.",
206
+ "显示更多经验": "Show more lessons",
207
+ "只显示最近 ": "Showing the most recent ",
208
+ " 条经验。": " lessons.",
209
+ "采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。": "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.",
210
+ "待核实": "Needs verification",
211
+ " 条待核实": " need verification",
212
+ "没有待核实的经验": "No lessons awaiting verification",
213
+ "显示更多待核实经验": "Show more pending lessons",
214
+ "这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。": "These lessons are not injected as task advice by default. Check sources and scope before accepting, rejecting or reviewing further.",
215
+ "写入记录": "Rule changes",
216
+ "读取写入记录失败:": "Could not load rule changes: ",
217
+ " 次写入 · ": " changes · ",
218
+ " 次可回滚": " can be rolled back",
219
+ "经验:": "Lessons: ",
220
+ "前哈希:": "Before hash: ",
221
+ "后哈希:": "After hash: ",
222
+ "备份标识与回滚命令": "Backup ID and rollback command",
223
+ " 条记录。": " records.",
224
+ "回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。": "Rollback restores the backed-up Dream managed block and preserves content outside it. External changes to the block prevent rollback.",
225
+ "隐私模式:点击卡片临时查看": "Privacy mode: click the card to reveal temporarily",
226
+ "心境:": "Mood: ",
227
+ "收起": "Collapse",
228
+ "展开全文": "Read more",
229
+ "没有找到「": "No dreams matching “",
230
+ "」相关的梦": "”",
231
+ "换个关键词试试,或者清空搜索看全部梦境。": "Try another keyword, or clear the search to see all dreams.",
232
+ "清空搜索": "Clear search",
233
+ "还没有梦境日记": "No dreams yet",
234
+ "对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。": "Ask your agent to “have a dream”. It will replay recent sessions, reflect and record its first dream here.",
235
+ "对 agent 说「做个梦」": "Ask your agent to “have a dream”",
236
+ "搜索反思与教训…": "Search reflections and observations…",
237
+ "搜索梦境": "Search dreams",
238
+ "筛选作用于已加载窗口,不影响全量统计": "Filters apply to loaded dreams; overall statistics are unchanged",
239
+ "筛选": "Filter",
240
+ "筛选梦境": "Filter dreams",
241
+ " 场,不影响全量统计": " loaded dreams; overall statistics are unchanged",
242
+ "只看有教训的梦": "Only dreams with observations",
243
+ "清空筛选": "Clear filters",
244
+ "完成": "Done",
245
+ "显示层模糊;DOM 仍为明文,不改文件、不加密": "Display blur only; the DOM remains readable. Files are unchanged and not encrypted",
246
+ "隐私模式:开": "Privacy mode: on",
247
+ "隐私模式": "Privacy mode",
248
+ "重新读取梦境日记": "Reload the dream journal",
249
+ "刷新": "Refresh",
250
+ "读取梦境日记失败:": "Could not load the dream journal: ",
251
+ "暂时读不到梦境日记": "The dream journal is unavailable",
252
+ "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。": "Retry after the host is ready. If the problem persists, check DSH logs for [dsh-dream] warnings.",
253
+ "梦境总数": "Total dreams",
254
+ "本页显示最近 ": "Most recent loaded: ",
255
+ "全部梦境日记": "All dreams",
256
+ "时间跨度": "Time span",
257
+ "最近一场": "Latest dream",
258
+ " 种": " moods",
259
+ "最常见:": "Most common: ",
260
+ "梦境时间线": "Dream timeline",
261
+ "筛选结果": "Filtered results",
262
+ "搜索结果": "Search results",
263
+ "没有符合筛选的梦(筛选作用于已加载的 ": "No matching dreams (filters apply to the ",
264
+ " 场)。": " loaded dreams).",
265
+ "梦境面板视图": "Dream views",
266
+ "日记": "Journal",
267
+ "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。": "Privacy mode blurs the display only. The DOM remains readable; files are unchanged and not encrypted. Click a card to reveal it temporarily.",
268
+ "显示更早的梦": "Show older dreams",
269
+ "数据来自梦境日记(dreams.jsonl)": "Data from the dream journal (dreams.jsonl)",
270
+ "梦境日记": "Dream journal",
271
+ "自动整理任务经验,审阅后回用;日记保留独立的反思记录。": "Collect lessons from tasks and reuse them after review; journals keep separate reflections."
272
+ });
273
+ function languageOf(locale = localeService) {
274
+ try { return String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'; } catch { return 'zh'; }
275
+ }
276
+ function t(zh) { return languageOf() === 'en' ? (EN[zh] ?? zh) : zh; }
277
+ function displayError(error) {
278
+ const message = error?.message || String(error || '');
279
+ if (languageOf() !== 'en') return message;
280
+ if (EN[message]) return EN[message];
281
+ if (!/[\u3400-\u9fff]/.test(message)) return message;
282
+ const hints = {
283
+ revision: 'This lesson or project changed. Refresh and review again; your input is preserved.',
284
+ conflict: 'The target changed. Preview the current rules again before applying.',
285
+ 'preview-mismatch': 'The preview no longer matches. Create a new preview.',
286
+ 'preview-expired': 'The preview expired. Create a new preview.',
287
+ 'not-rollbackable': 'Rollback is unavailable because the managed block changed or its backup is missing.',
288
+ 'ineligible': 'This lesson is not eligible for rule writing. Check its scope and review state.',
289
+ 'read-only': 'The target is read-only. Check its permissions before retrying.',
290
+ 'marker': 'The managed block markers are invalid. Check the file before retrying.',
291
+ 'path-outside-project': 'The target must stay inside the project directory.',
292
+ 'unsupported-encoding': 'The target file encoding is unsupported. Use UTF-8.',
293
+ 'target-is-directory': 'The selected target is a directory; select a file.',
294
+ 'not-found': 'The lesson, target or backup was not found. Refresh and check the project.',
295
+ forbidden: 'This operation must be performed from the authenticated Dream panel.',
296
+ 'too-large': 'The request is too large. Shorten the input and try again.',
297
+ cancelled: 'The operation was cancelled. Refresh to check the result before retrying.',
298
+ io: 'The operation could not access its files. Check the project location and file permissions, then refresh before retrying.',
299
+ internal: 'Could not load the records. Check the DSH logs and retry.',
300
+ invalid: 'The input or selected record is no longer valid. Check the fields and refresh the list.',
301
+ };
302
+ return (hints[error?.code] || 'The operation failed. Check the DSH logs, then refresh before retrying.')
303
+ + (error?.status ? ' (HTTP ' + error.status + (error.code ? ', ' + error.code : '') + ')' : '');
304
+ }
305
+ function rollbackReasonText(reason) {
306
+ if (languageOf() !== 'en') return reason;
307
+ return ({ '目标文件不可读': 'Target file is unreadable', '文件不存在': 'File does not exist',
308
+ '管理块标记异常': 'Invalid managed block markers', '管理块已被移除': 'Managed block was removed',
309
+ '管理块已被外部修改': 'Managed block changed externally' })[reason]
310
+ || (/[\u3400-\u9fff]/.test(reason) ? 'Check the target file and backup' : reason);
311
+ }
312
+ function scopeText(label) {
313
+ if (languageOf() !== 'en') return label;
314
+ return label.replace(/^全局$/, 'Global').replace(/^工作区 /, 'Workspace ').replace(/^项目 /, 'Project ');
315
+ }
316
+ function evidenceText(summary) {
317
+ if (languageOf() !== 'en') return summary;
318
+ return summary.replace(/^独立证据 (\d+) 条(已读 (\d+) \/ 仅声明 (\d+)):/, 'Independent sources: $1 (read: $2 / claimed: $3): ');
319
+ }
320
+ function useLanguage(locale) {
321
+ const [, rerender] = useState(0);
322
+ useEffect(() => { const update = () => rerender(n => n + 1); update(); return locale?.subscribe?.(update); }, [locale]);
323
+ }
324
+
325
+
22
326
  // 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
23
327
  const ROUTE = "_dsh/dsh-dream/journal";
24
328
  const ACTION_ROUTE = "api/dsh-dream/actions";
@@ -74,7 +378,7 @@ function errorMessageOf(body, status) {
74
378
  if (body.error !== null && typeof body.error === "object" && typeof body.error.message === "string" && body.error.message !== "") return body.error.message;
75
379
  if (typeof body.message === "string" && body.message !== "") return body.message;
76
380
  }
77
- return "请求失败(HTTP " + String(status) + ")";
381
+ return t("请求失败(HTTP ") + String(status) + ")";
78
382
  }
79
383
 
80
384
  /** GET /_dsh/dsh-dream/journal:契约体 { dreams, stats, query, limit }。 */
@@ -87,7 +391,7 @@ async function fetchJournal(query, limit) {
87
391
  const res = await fetch(routeUrl(params.join("&")), init);
88
392
  let body = null;
89
393
  try { body = await res.json(); } catch (error) { body = null; }
90
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
394
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
91
395
  if (res.ok !== true || body.ok === false) {
92
396
  const failure = new Error(errorMessageOf(body, res.status));
93
397
  failure.status = res.status;
@@ -111,7 +415,7 @@ async function fetchKnowledge() {
111
415
  const res = await fetch(routeUrlFor(KNOWLEDGE_ROUTE, ""), init);
112
416
  let body = null;
113
417
  try { body = await res.json(); } catch (error) { body = null; }
114
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
418
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
115
419
  if (res.ok !== true || body.ok === false) {
116
420
  const failure = new Error(errorMessageOf(body, res.status));
117
421
  failure.status = res.status;
@@ -134,7 +438,7 @@ async function fetchBridgeRecords() {
134
438
  const res = await fetch(routeUrlFor(BRIDGE_ROUTE, ""), init);
135
439
  let body = null;
136
440
  try { body = await res.json(); } catch (error) { body = null; }
137
- if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
441
+ if (body === null || typeof body !== "object") throw new Error(t("服务器返回的不是 JSON(HTTP ") + String(res.status) + ")");
138
442
  if (res.ok !== true || body.ok === false) {
139
443
  const failure = new Error(errorMessageOf(body, res.status));
140
444
  failure.status = res.status;
@@ -155,8 +459,8 @@ async function postAction(body) {
155
459
  let value = null;
156
460
  try { value = await res.json(); } catch (error) { /* Preserve HTTP status for useful recovery hints. */ }
157
461
  if (res.ok !== true || value === null || value.ok === false) {
158
- const hint = res.status === 401 ? "登录状态已失效,请重新打开 DSH 页面后重试。"
159
- : res.status === 404 ? "当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。"
462
+ const hint = res.status === 401 ? t("登录状态已失效,请重新打开 DSH 页面后重试。")
463
+ : res.status === 404 ? t("当前宿主尚未提供此操作,请升级 DSH,或使用下方对话命令。")
160
464
  : errorMessageOf(value, res.status);
161
465
  const failure = new Error(hint);
162
466
  failure.code = value && value.error ? value.error.code : undefined;
@@ -257,12 +561,12 @@ function normalizeLessonState(value) {
257
561
  /** 状态 → 中文标签(面板文案集中在这里)。 */
258
562
  function lessonStateLabel(state) {
259
563
  switch (state) {
260
- case "candidate": return "候选(未采纳)";
261
- case "usable": return "可用";
262
- case "disputed": return "有冲突";
263
- case "stale": return "待复核";
264
- case "rejected": return "已驳回";
265
- default: return "未知状态";
564
+ case "candidate": return t("候选(未采纳)");
565
+ case "usable": return t("可用");
566
+ case "disputed": return t("有冲突");
567
+ case "stale": return t("待复核");
568
+ case "rejected": return t("已驳回");
569
+ default: return t("未知状态");
266
570
  }
267
571
  }
268
572
 
@@ -295,11 +599,11 @@ function applicabilityTextOf(value) {
295
599
  const main = pkg !== "" ? (versions !== "" ? pkg + " " + versions : pkg) : versions;
296
600
  const bits = [];
297
601
  if (main !== "") bits.push(main);
298
- if (platform !== "") bits.push("平台 " + platform);
602
+ if (platform !== "") bits.push(t("平台 ") + platform);
299
603
  if (bits.length > 0) parts.push(bits.join(" · "));
300
604
  }
301
605
  if (parts.length === 0) return "";
302
- if (parts.length > 4) return parts.slice(0, 4).join(";") + ";等 " + String(parts.length) + " 条适用范围";
606
+ if (parts.length > 4) return parts.slice(0, 4).join(";") + t(";等 ") + String(parts.length) + t(" 条适用范围");
303
607
  return parts.join(";");
304
608
  }
305
609
 
@@ -343,22 +647,22 @@ function normalizeKnowledge(value, now, limit) {
343
647
  state: state,
344
648
  stateLabel: lessonStateLabel(state),
345
649
  stateClass: lessonStateClass(state),
346
- scopeLabel: trimmedText(raw.scopeLabel) || "范围未标注",
347
- evidenceSummary: trimmedText(raw.evidenceSummary) || "暂无可展示的证据摘要",
650
+ scopeLabel: scopeText(trimmedText(raw.scopeLabel)) || t("范围未标注"),
651
+ evidenceSummary: evidenceText(trimmedText(raw.evidenceSummary)) || t("暂无可展示的证据摘要"),
348
652
  evidence: Array.isArray(source.evidence) && Array.isArray(raw.evidenceIds)
349
653
  ? source.evidence.filter(function (item) { return item && raw.evidenceIds.indexOf(item.id) !== -1; }).slice(0, 20) : [],
350
654
  feedback: raw.feedback && typeof raw.feedback === "object" ? raw.feedback : { useful: 0, notApplicable: 0, recent: [] },
351
655
  lastValidatedAt: lastValidatedAt,
352
- lastText: lastValidatedAt === "" ? "尚未核验" : relativeTime(lastValidatedAt, now),
353
- lastTitle: lastValidatedAt === "" ? "没有 lastValidatedAt(从未核验)" : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
656
+ lastText: lastValidatedAt === "" ? t("尚未核验") : relativeTime(lastValidatedAt, now),
657
+ lastTitle: lastValidatedAt === "" ? t("没有 lastValidatedAt(从未核验)") : formatDate(lastValidatedAt) + (formatTime(lastValidatedAt) === "" ? "" : " " + formatTime(lastValidatedAt)),
354
658
  revision: typeof raw.revision === "number" && Number.isFinite(raw.revision) && raw.revision >= 0 ? Math.floor(raw.revision) : 0,
355
659
  independentSupportCount: typeof raw.independentSupportCount === "number" && raw.independentSupportCount > 0 ? Math.floor(raw.independentSupportCount) : 0,
356
660
  projectId: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.projectId) : "",
357
661
  workspaceRoot: typeof raw.scope === "object" && raw.scope !== null ? trimmedText(raw.scope.workspaceRoot) : "",
358
662
  conflictIds: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }) : [],
359
- conflicts: Array.isArray(raw.conflictIds) ? raw.conflictIds.filter(function (id) { return typeof id === "string" && id.trim() !== ""; }).map(function (id) { return { id: id, title: lessonTitles.get(id) || "未加载的经验(" + id + ")" }; }) : [],
663
+ 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 + ")" }; }) : [],
360
664
  holdReason: trimmedText(raw.holdReason) !== "" ? trimmedText(raw.holdReason) : holdReasonOf(raw),
361
- holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? trimmedText(raw.holdReasonLabel) : skippedReasonLabel(holdReasonOf(raw)),
665
+ holdReasonLabel: trimmedText(raw.holdReasonLabel) !== "" ? t(trimmedText(raw.holdReasonLabel)) : skippedReasonLabel(holdReasonOf(raw)),
362
666
  bridgeable: bridgeableOf(raw.state) && trimmedText(raw.id) !== "",
363
667
  candidate: state === "candidate",
364
668
  disputed: state === "disputed",
@@ -387,7 +691,7 @@ function knowledgeCountText(stats) {
387
691
  }
388
692
  if (parts.length === 0) {
389
693
  const total = typeof source.lessons === "number" && source.lessons > 0 ? source.lessons : 0;
390
- return total > 0 ? "共 " + String(total) + " 条" : "还没有经验";
694
+ return total > 0 ? t("共 ") + String(total) + t(" 条") : t("还没有经验");
391
695
  }
392
696
  return parts.join(" · ");
393
697
  }
@@ -399,32 +703,32 @@ function knowledgeNoticeText(stats) {
399
703
  const badLines = typeof source.badLines === "number" && Number.isFinite(source.badLines) && source.badLines > 0 ? Math.floor(source.badLines) : 0;
400
704
  if (truncated !== true && badLines === 0) return "";
401
705
  const parts = [];
402
- if (truncated === true) parts.push("日志过大,仅回放最近一段事件");
403
- if (badLines > 0) parts.push("有 " + String(badLines) + " 行损坏已跳过");
404
- return parts.join(";") + ";events.jsonl 未改动。";
706
+ if (truncated === true) parts.push(t("日志过大,仅回放最近一段事件"));
707
+ if (badLines > 0) parts.push(t("有 ") + String(badLines) + t(" 行损坏已跳过"));
708
+ return parts.join(";") + t(";events.jsonl 未改动。");
405
709
  }
406
710
 
407
711
  /** 经验区块的独立空态:与「还没有梦境日记」分开,说明经验从哪来。 */
408
712
  function knowledgeEmptyState() {
409
713
  return {
410
- title: "还没有经验记录",
411
- body: "经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。",
412
- hint: "旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。",
714
+ title: t("还没有经验记录"),
715
+ body: t("经验是带证据、有适用条件的技术结论:收尾时用 dream_learn 提交候选,证据足够、范围明确的会出现在这里。"),
716
+ hint: t("旧日记里的教训不会自动升级为经验;采纳、驳回或标记冲突请用 dream_review。"),
413
717
  };
414
718
  }
415
719
 
416
720
  /** 检索层 skipped reason 的中文映射(面板扣留原因文案;与 retrieval.ts 的 reason 常量同名)。 */
417
721
  function skippedReasonLabel(reason) {
418
722
  switch (trimmedText(reason)) {
419
- case "state:rejected": return "已驳回:不参与默认检索与注入";
420
- case "state:stale": return "待复核:条件或依赖可能已过期,默认不注入";
421
- case "state:disputed": return "冲突未解决:默认不注入,先 resolve-conflict";
422
- case "candidate-hold": return "候选:可被检索到,但默认不作为任务建议注入";
423
- case "no-evidence": return "证据不足:没有独立证据,默认不注入";
424
- case "version-mismatch": return "版本不匹配:当前依赖版本不满足适用范围";
425
- case "limit": return "超出条数预算,本次未取回";
426
- case "budget-stop": return "超出字符预算:按排名整条装入,遇到第一条放不下即停";
427
- case "budget": return "超出字符预算,本次未取回";
723
+ case "state:rejected": return t("已驳回:不参与默认检索与注入");
724
+ case "state:stale": return t("待复核:条件或依赖可能已过期,默认不注入");
725
+ case "state:disputed": return t("冲突未解决:默认不注入,先 resolve-conflict");
726
+ case "candidate-hold": return t("候选:可被检索到,但默认不作为任务建议注入");
727
+ case "no-evidence": return t("证据不足:没有独立证据,默认不注入");
728
+ case "version-mismatch": return t("版本不匹配:当前依赖版本不满足适用范围");
729
+ case "limit": return t("超出条数预算,本次未取回");
730
+ case "budget-stop": return t("超出字符预算:按排名整条装入,遇到第一条放不下即停");
731
+ case "budget": return t("超出字符预算,本次未取回");
428
732
  default: return "";
429
733
  }
430
734
  }
@@ -506,12 +810,12 @@ function shortHash(value) {
506
810
 
507
811
  function bridgeActionLabel(action) {
508
812
  switch (trimmedText(action)) {
509
- case "create": return "新建";
510
- case "replace": return "替换管理块";
511
- case "append": return "追加管理块";
512
- case "unchanged": return "未改动";
513
- case "rollback": return "回滚";
514
- default: return trimmedText(action) || "未知动作";
813
+ case "create": return t("新建");
814
+ case "replace": return t("替换管理块");
815
+ case "append": return t("追加管理块");
816
+ case "unchanged": return t("未改动");
817
+ case "rollback": return t("回滚");
818
+ default: return trimmedText(action) || t("未知动作");
515
819
  }
516
820
  }
517
821
 
@@ -549,17 +853,17 @@ function normalizeBridgeRecords(value, now, limit) {
549
853
  rows.push({
550
854
  id: id,
551
855
  at: at,
552
- atText: at === "" ? "时间未知" : relativeTime(at, now),
553
- atTitle: at === "" ? "没有记录时间" : formatDate(at) + (formatTime(at) === "" ? "" : " " + formatTime(at)),
554
- target: target || "目标未记录",
856
+ atText: at === "" ? t("时间未知") : relativeTime(at, now),
857
+ atTitle: at === "" ? t("没有记录时间") : formatDate(at) + (formatTime(at) === "" ? "" : " " + formatTime(at)),
858
+ target: target || t("目标未记录"),
555
859
  action: trimmedText(raw.action),
556
860
  actionLabel: bridgeActionLabel(raw.action),
557
861
  lessons: lessonRefs,
558
- lessonsText: lessonRefs.length === 0 ? "没有经验引用" : lessonRefs.join("、"),
862
+ lessonsText: lessonRefs.length === 0 ? t("没有经验引用") : lessonRefs.join("、"),
559
863
  beforeShort: shortHash(raw.beforeSha256),
560
864
  afterShort: shortHash(raw.afterSha256),
561
865
  rollbackable: rollbackable,
562
- rollbackText: rollbackable ? "可回滚" : ("不可回滚" + (rollbackReason !== "" ? ":" + rollbackReason : "")),
866
+ rollbackText: rollbackable ? t("可回滚") : (t("不可回滚") + (rollbackReason !== "" ? (languageOf() === 'en' ? ': ' : ':') + rollbackReasonText(rollbackReason) : "")),
563
867
  rollbackReason: rollbackReason,
564
868
  });
565
869
  }
@@ -576,15 +880,15 @@ function normalizeBridgeRecords(value, now, limit) {
576
880
  /** 「写入记录」的独立空态。 */
577
881
  function bridgeEmptyState() {
578
882
  return {
579
- title: "还没有写入记录",
580
- body: "写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。",
581
- hint: "在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。",
883
+ title: t("还没有写入记录"),
884
+ body: t("写入记录来自 dream_bridge 的 preview → apply;面板只读展示目标、时间、经验 revision 与前后哈希,不会写文件。"),
885
+ hint: t("在可桥接经验行点「复制预览命令」,回到对话里执行;写操作只在工具中完成。"),
582
886
  };
583
887
  }
584
888
 
585
889
  /** 「待核实」的独立空态。 */
586
890
  function reviewQueueEmptyState() {
587
- return "没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。";
891
+ return t("没有待核实的经验 —— 候选、有冲突、待复核的经验会出现在这里,并写明被扣下的原因。");
588
892
  }
589
893
 
590
894
  function spanOf(dreams) {
@@ -602,7 +906,7 @@ function spanOf(dreams) {
602
906
 
603
907
  function formatDate(iso) {
604
908
  const date = new Date(iso);
605
- if (!Number.isFinite(date.getTime())) return iso === "" ? "时间未知" : iso;
909
+ if (!Number.isFinite(date.getTime())) return iso === "" ? t("时间未知") : iso;
606
910
  return date.getFullYear() + "-" + pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
607
911
  }
608
912
 
@@ -633,10 +937,10 @@ function relativeTime(iso, now) {
633
937
  const minute = 60000;
634
938
  const hour = 3600000;
635
939
  const day = 86400000;
636
- if (diff < minute) return "刚刚";
637
- if (diff < hour) return Math.floor(diff / minute) + " 分钟前";
638
- if (diff < day) return Math.floor(diff / hour) + " 小时前";
639
- if (diff < day * 30) return Math.floor(diff / day) + " 天前";
940
+ if (diff < minute) return t("刚刚");
941
+ if (diff < hour) return Math.floor(diff / minute) + t(" 分钟前");
942
+ if (diff < day) return Math.floor(diff / hour) + t(" 小时前");
943
+ if (diff < day * 30) return Math.floor(diff / day) + t(" 天前");
640
944
  return formatDate(iso);
641
945
  }
642
946
 
@@ -648,9 +952,9 @@ function recentText(iso, now) {
648
952
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
649
953
  const that = new Date(date.getFullYear(), date.getMonth(), date.getDate());
650
954
  const days = Math.round((today.getTime() - that.getTime()) / 86400000);
651
- if (days <= 0) return "今天";
652
- if (days === 1) return "昨天";
653
- if (days < 30) return String(days) + " 天前";
955
+ if (days <= 0) return t("今天");
956
+ if (days === 1) return t("昨天");
957
+ if (days < 30) return String(days) + t(" 天前");
654
958
  return formatDate(iso);
655
959
  }
656
960
 
@@ -744,23 +1048,23 @@ function needsOlderDreams(loaded, total) {
744
1048
  }
745
1049
 
746
1050
  function countTextOf(query, total, count, filterCount, visibleCount) {
747
- if (filterCount > 0) return "筛选后 " + String(visibleCount) + " / " + String(count) + " 场";
748
- if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
749
- return "共 " + String(total) + " 场梦";
1051
+ if (filterCount > 0) return t("筛选后 ") + String(visibleCount) + " / " + String(count) + t(" 场");
1052
+ if (query !== "") return (languageOf() === "en" ? "“" : "「") + query + t("」命中 ") + String(count) + t(" 场");
1053
+ return t("共 ") + String(total) + t(" 场梦");
750
1054
  }
751
1055
 
752
1056
  function listHintOf(query, count, filterCount, visibleCount) {
753
- if (filterCount > 0) return "筛选当前已加载的 " + String(count) + " 场 · 命中 " + String(visibleCount) + " 场";
754
- if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
755
- return "新梦在前";
1057
+ if (filterCount > 0) return t("筛选当前已加载的 ") + String(count) + t(" 场 · 命中 ") + String(visibleCount) + t(" 场");
1058
+ if (query !== "") return (languageOf() === "en" ? "“" : "「") + query + t("」命中 ") + String(count) + t(" 场");
1059
+ return t("新梦在前");
756
1060
  }
757
1061
 
758
1062
  function footerTextOf(query, limit, total, count) {
759
1063
  if (limit > DEFAULT_LIMIT) {
760
- const capped = limit >= MORE_LIMIT && count < total ? "(已达路由上限)" : "";
761
- return "最近 " + String(limit) + " 场 / 共 " + String(total) + " 场" + capped;
1064
+ const capped = limit >= MORE_LIMIT && count < total ? t("(已达路由上限)") : "";
1065
+ return t("最近 ") + String(limit) + t(" 场 / 共 ") + String(total) + t(" 场") + capped;
762
1066
  }
763
- return query === "" ? "新梦在前 · 最多显示 " + String(DEFAULT_LIMIT) + " 场" : "搜索结果 · 最多显示 " + String(DEFAULT_LIMIT) + " 场";
1067
+ return query === "" ? t("新梦在前 · 最多显示 ") + String(DEFAULT_LIMIT) + t(" 场") : t("搜索结果 · 最多显示 ") + String(DEFAULT_LIMIT) + t(" 场");
764
1068
  }
765
1069
 
766
1070
  /** 面板的视图模型:所有文案/数字集中在这里算,组件只负责画。 */
@@ -784,15 +1088,15 @@ function viewModelOf(dreams, stats, now) {
784
1088
  share: Math.max(6, Math.round(row.count / Math.max(lessonTotal, 1) * 100)),
785
1089
  showBar: row.count >= 2,
786
1090
  candidate: row.count >= 3,
787
- lastText: knownAt === "" ? "" : (fromStats ? "最近 " : "本页内最近 ") + recentText(knownAt, now),
1091
+ lastText: knownAt === "" ? "" : (fromStats ? t("最近 ") : t("本页内最近 ")) + recentText(knownAt, now),
788
1092
  lastTitle: knownAt === "" ? "" : formatDate(knownAt) + (formatTime(knownAt) === "" ? "" : " " + formatTime(knownAt))
789
- + (fromStats ? "" : "(统计未给 lastAt,按本页已加载的 " + String(list.length) + " 场现算)"),
1093
+ + (fromStats ? "" : t("(统计未给 lastAt,按本页已加载的 ") + String(list.length) + t(" 场现算)")),
790
1094
  };
791
1095
  });
792
1096
  const span = spanOf(list);
793
1097
  const spanText = span === null ? "" : spanTextOf(span);
794
1098
  const spanFull = span === null ? "" : formatDate(new Date(span.from).toISOString()) + " → " + formatDate(new Date(span.to).toISOString());
795
- const spanHint = span === null ? "" : (span.days === 0 ? "同一天" : "约 " + String(span.days) + " 天");
1099
+ const spanHint = span === null ? "" : (span.days === 0 ? t("同一天") : t("约 ") + String(span.days) + t(" 天"));
796
1100
  const blocks = list.slice().reverse().map((dream, index) => ({
797
1101
  key: "block-" + (dream.id !== "" ? dream.id : "no-id") + "-" + String(index),
798
1102
  tone: toneClass(moodSlot(dream.mood)),
@@ -823,12 +1127,12 @@ function headlineOf(dreams, total, span, now) {
823
1127
  if (dreams.length === 0) return "";
824
1128
  if (dreams.length === 1 && total <= 1) {
825
1129
  const dream = dreams[0];
826
- return "第 1 场梦 · " + formatDate(dream.at) + " · 心境「" + dream.mood + "」";
1130
+ return t("第 1 场梦 · ") + formatDate(dream.at) + t(" · 心境「") + dream.mood + (languageOf() === "en" ? "”" : "」");
827
1131
  }
828
1132
  const parts = [];
829
- parts.push(total > dreams.length ? "共 " + String(total) + " 场梦 · 本页最近 " + String(dreams.length) + " 场" : String(dreams.length) + " 场梦");
1133
+ parts.push(total > dreams.length ? t("共 ") + String(total) + t(" 场梦 · 本页最近 ") + String(dreams.length) + t(" 场") : String(dreams.length) + t(" 场梦"));
830
1134
  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()));
831
- parts.push("最近:" + relativeTime(dreams[0].at, now));
1135
+ parts.push(t("最近:") + relativeTime(dreams[0].at, now));
832
1136
  return parts.join(" · ");
833
1137
  }
834
1138
 
@@ -873,20 +1177,20 @@ function StatTile(props) {
873
1177
  function MoodCard(props) {
874
1178
  const vm = props.vm;
875
1179
  return h("section", { className: "dshd-card" }, [
876
- h("h4", { key: "t" }, "心境"),
1180
+ h("h4", { key: "t" }, t("心境")),
877
1181
  h("div", { className: "dshd-mblocks", key: "blocks" }, vm.blocks.map((block) => h("span", {
878
1182
  key: block.key,
879
1183
  className: "dshd-mblock " + block.tone,
880
1184
  title: block.title,
881
1185
  }))),
882
1186
  vm.count === 1
883
- ? h("div", { className: "dshd-hint", key: "one" }, "1 场梦,先不谈分布 —— 再做梦会累积。")
1187
+ ? h("div", { className: "dshd-hint", key: "one" }, t("1 场梦,先不谈分布 —— 再做梦会累积。"))
884
1188
  : h("div", { key: "legend" }, [
885
- h("div", { className: "dshd-note", key: "cap" }, "左 → 右:从早到晚"),
1189
+ h("div", { className: "dshd-note", key: "cap" }, t("左 → 右:从早到晚")),
886
1190
  h("div", { className: "dshd-legend", key: "rows" }, vm.moodRows.map((row, index) => h("div", { className: "dshd-legend-row", key: "mood-" + String(index) + "-" + row.mood }, [
887
1191
  h("span", { className: "dshd-mblock " + toneClass(moodSlot(row.mood)), key: "d" }),
888
1192
  h("span", { className: "dshd-legend-name", key: "n", title: row.mood }, row.mood),
889
- h("span", { className: "dshd-legend-count", key: "c" }, String(row.count) + " 场"),
1193
+ h("span", { className: "dshd-legend-count", key: "c" }, String(row.count) + t(" 场")),
890
1194
  ]))),
891
1195
  ]),
892
1196
  ]);
@@ -895,9 +1199,9 @@ function MoodCard(props) {
895
1199
  function LessonCard(props) {
896
1200
  const vm = props.vm;
897
1201
  return h("section", { className: "dshd-card" }, [
898
- h("h4", { key: "t" }, "教训榜"),
1202
+ h("h4", { key: "t" }, t("教训榜")),
899
1203
  vm.lessonRows.length === 0
900
- ? h("div", { className: "dshd-hint", key: "e" }, "还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。")
1204
+ ? h("div", { className: "dshd-hint", key: "e" }, t("还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。"))
901
1205
  : h("div", { key: "rows" }, vm.lessonRows.map((row, index) => h("div", { className: "dshd-lesson", key: "lesson-" + String(index) + "-" + row.lesson }, [
902
1206
  h("span", { className: "dshd-rank", key: "r" }, String(row.rank)),
903
1207
  h("span", { className: "dshd-lesson-body", key: "b" }, [
@@ -907,10 +1211,10 @@ function LessonCard(props) {
907
1211
  h("span", { className: "dshd-lesson-meta", key: "m" }, [
908
1212
  h("span", { className: "dshd-times", key: "c" }, "×", h("b", { key: "n" }, String(row.count))),
909
1213
  row.lastText === "" ? null : h("span", { className: "dshd-last", key: "l", title: row.lastTitle }, row.lastText),
910
- row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, "常见教训") : null,
1214
+ row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, t("常见教训")) : null,
911
1215
  ]),
912
1216
  ]))),
913
- h("div", { className: "dshd-note", key: "note" }, "次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。"),
1217
+ h("div", { className: "dshd-note", key: "note" }, t("次数仅表示日记里的出现频率;出现 ≥3 次标为「常见教训」。写入项目规则前仍需核验来源并审阅经验。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。")),
914
1218
  ]);
915
1219
  }
916
1220
 
@@ -926,11 +1230,11 @@ function CopyCommand(props) {
926
1230
  setFailed(ok !== true);
927
1231
  });
928
1232
  };
929
- const label = props.label === undefined ? "复制命令" : props.label;
1233
+ const label = props.label === undefined ? t("复制命令") : props.label;
930
1234
  return h("span", { className: "dshd-copywrap" }, [
931
- h(Btn, { key: "b", onClick: onCopy, disabled: props.disabled === true, title: props.title === undefined ? command : props.title }, copied === true ? "已复制" : label),
1235
+ h(Btn, { key: "b", onClick: onCopy, disabled: props.disabled === true, title: props.title === undefined ? command : props.title }, copied === true ? t("已复制") : label),
932
1236
  failed === true ? h("code", { key: "cmd", className: "dshd-cmd", tabIndex: 0 }, command) : null,
933
- failed === true ? h("span", { key: "hint", className: "dshd-note" }, "剪贴板不可用:请手动选中上面的命令复制。") : null,
1237
+ failed === true ? h("span", { key: "hint", className: "dshd-note" }, t("剪贴板不可用:请手动选中上面的命令复制。")) : null,
934
1238
  ]);
935
1239
  }
936
1240
 
@@ -947,10 +1251,10 @@ function ReviewControls(props) {
947
1251
  const [pending, setPending] = useState(false);
948
1252
  const [error, setError] = useState("");
949
1253
  const [preview, setPreview] = useState(null);
950
- const commands = row.state === "rejected" ? [["reopen", "重新送审"]]
951
- : row.state === "disputed" ? [["resolve-conflict", "处理冲突"], ["reject", "驳回这条"]]
952
- : row.state === "usable" ? [["mark-stale", "标记待复核"], ["reject", "驳回"]]
953
- : [["accept", "采纳"], ["reject", "驳回"], ["mark-stale", "标记待复核"]];
1254
+ const commands = row.state === "rejected" ? [["reopen", t("重新送审")]]
1255
+ : row.state === "disputed" ? [["resolve-conflict", t("处理冲突")], ["reject", t("驳回这条")]]
1256
+ : row.state === "usable" ? [["mark-stale", t("标记待复核")], ["reject", t("驳回")]]
1257
+ : [["accept", t("采纳")], ["reject", t("驳回")], ["mark-stale", t("标记待复核")]];
954
1258
  const effectiveAction = commands.some(function (item) { return item[0] === action; }) ? action : commands[0][0];
955
1259
  const command = reviewCommandOf(row, { action: effectiveAction, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] });
956
1260
  const submit = async function (body, message, isPreview) {
@@ -959,65 +1263,65 @@ function ReviewControls(props) {
959
1263
  try {
960
1264
  const result = await postAction(body);
961
1265
  if (isPreview) setPreview(result);
962
- else { setPreview(null); if (props.onChanged) props.onChanged(result.unchanged === true ? "管理块内容已一致,无需写入。" : message); }
1266
+ else { setPreview(null); if (props.onChanged) props.onChanged(result.unchanged === true ? t("管理块内容已一致,无需写入。") : message); }
963
1267
  } catch (failure) {
964
- setError(failure.name === "TimeoutError" ? "请求超时,请先刷新确认结果后再重试。" : failure.message || String(failure));
1268
+ setError(failure.name === "TimeoutError" ? t("请求超时,请先刷新确认结果后再重试。") : displayError(failure));
965
1269
  if (body.operation === "apply") setPreview(null);
966
1270
  } finally { setPending(false); }
967
1271
  };
968
1272
  const review = function (chosen) {
969
- return submit({ operation: "review", action: chosen, lessonId: row.id, expectedRevision: row.revision, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] }, "已更新「" + row.title + "」的审阅状态。");
1273
+ return submit({ operation: "review", action: chosen, lessonId: row.id, expectedRevision: row.revision, resolution: resolution, note: note, affectedIds: affected === "" ? [] : [affected] }, t("已更新「") + row.title + t("」的审阅状态。"));
970
1274
  };
971
1275
  const feedback = function (vote) {
972
1276
  const signature = row.id + "@" + row.revision + ":" + vote + ":" + feedbackNote;
973
1277
  const requestId = feedbackRetry && feedbackRetry.signature === signature ? feedbackRetry.id
974
1278
  : typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : "feedback_" + Date.now() + "_" + Math.random().toString(36).slice(2);
975
1279
  setFeedbackRetry({ signature: signature, id: requestId });
976
- return submit({ operation: "feedback", lessonId: row.id, expectedRevision: row.revision, vote: vote, requestId: requestId, ...(feedbackNote.trim() === "" ? {} : { note: feedbackNote.trim() }) }, "已记录使用反馈;「" + row.title + "」的审阅状态保持原样。");
1280
+ return submit({ operation: "feedback", lessonId: row.id, expectedRevision: row.revision, vote: vote, requestId: requestId, ...(feedbackNote.trim() === "" ? {} : { note: feedbackNote.trim() }) }, t("已记录使用反馈;「") + row.title + t("」的审阅状态保持原样。"));
977
1281
  };
978
- return h("div", { className: "dshd-review", role: "group", "aria-label": row.title + "的操作", onClick: function (event) { event.stopPropagation(); } }, [
1282
+ return h("div", { className: "dshd-review", role: "group", "aria-label": row.title + t("的操作"), onClick: function (event) { event.stopPropagation(); } }, [
979
1283
  h("div", { className: "dshd-kcmd", key: "buttons" }, [
980
1284
  ...commands.filter(function (item) { return item[0] !== "resolve-conflict"; }).map(function (item) {
981
1285
  return h(Btn, { key: item[0], disabled: pending || row.revision < 1, onClick: function () { review(item[0]); } }, item[1]);
982
1286
  }),
983
- h(Btn, { key: "review", disabled: pending, onClick: function () { setOpen(!open); }, "aria-expanded": open }, open ? "收起审阅" : row.state === "disputed" ? "处理冲突" : "审阅详情"),
984
- row.bridgeable === true && row.workspaceRoot !== "" ? h(Btn, { key: "preview", disabled: pending, onClick: function () { submit({ operation: "preview", lessonId: row.id, expectedRevision: row.revision }, "", true); } }, "预览规则变更") : null,
985
- row.bridgeable === true && row.workspaceRoot === "" ? h(CopyCommand, { key: "preview-command", command: bridgePreviewCommandOf(row.id, row.projectId, row.workspaceRoot), label: "复制项目预览命令" }) : null,
1287
+ h(Btn, { key: "review", disabled: pending, onClick: function () { setOpen(!open); }, "aria-expanded": open }, open ? t("收起审阅") : row.state === "disputed" ? t("处理冲突") : t("审阅详情")),
1288
+ 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,
1289
+ row.bridgeable === true && row.workspaceRoot === "" ? h(CopyCommand, { key: "preview-command", command: bridgePreviewCommandOf(row.id, row.projectId, row.workspaceRoot), label: t("复制项目预览命令") }) : null,
986
1290
  ]),
987
- pending ? h("div", { key: "pending", role: "status", className: "dshd-note" }, "正在处理…") : null,
988
- error !== "" ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, [error, h(Btn, { key: "refresh", disabled: pending, onClick: function () { if (props.onChanged) props.onChanged("已刷新经验列表。"); } }, "刷新列表")]) : null,
1291
+ pending ? h("div", { key: "pending", role: "status", className: "dshd-note" }, t("正在处理…")) : null,
1292
+ 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,
989
1293
  open ? h("div", { className: "dshd-reviewform", key: "form", onKeyDown: function (event) {
990
1294
  if (event.key === "Escape" && !pending) { event.preventDefault(); event.stopPropagation(); setOpen(false); }
991
1295
  } }, [
992
- h("label", { key: "action" }, ["审阅动作", h("select", { "aria-label": "审阅动作", disabled: pending, value: effectiveAction, onChange: function (event) { setAction(event.target.value); } }, commands.map(function (item) { return h("option", { value: item[0], key: item[0] }, item[1]); }))]),
1296
+ 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]); }))]),
993
1297
  effectiveAction === "resolve-conflict" ? h("div", { key: "conflict" }, [
994
- h("label", { key: "against" }, ["冲突经验", h("select", { "aria-label": "冲突经验", disabled: pending, value: affected, onChange: function (event) { setAffected(event.target.value); } }, row.conflicts.map(function (item) { return h("option", { key: item.id, value: item.id, title: item.title }, item.title); }))]),
995
- h("label", { key: "resolution" }, ["处理方式", h("select", { "aria-label": "处理方式", disabled: pending, value: resolution, onChange: function (event) { setResolution(event.target.value); } }, [
996
- h("option", { key: "prefer", value: "prefer" }, "采用这条,驳回对方"),
997
- h("option", { key: "drop", value: "drop" }, "放弃这条,保留对方"),
998
- h("option", { key: "merge", value: "merge" }, "合并到这条,对方留作历史"),
1298
+ 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); }))]),
1299
+ h("label", { key: "resolution" }, [t("处理方式"), h("select", { "aria-label": t("处理方式"), disabled: pending, value: resolution, onChange: function (event) { setResolution(event.target.value); } }, [
1300
+ h("option", { key: "prefer", value: "prefer" }, t("采用这条,驳回对方")),
1301
+ h("option", { key: "drop", value: "drop" }, t("放弃这条,保留对方")),
1302
+ h("option", { key: "merge", value: "merge" }, t("合并到这条,对方留作历史")),
999
1303
  ])]),
1000
- h("label", { key: "note" }, ["处理依据", h("textarea", { "aria-label": "处理依据", disabled: pending, rows: 2, maxLength: 500, value: note, placeholder: "说明哪个来源支持这次决定…", onChange: function (event) { setNote(event.target.value); } })]),
1304
+ 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); } })]),
1001
1305
  ]) : null,
1002
- h(Btn, { key: "confirm", disabled: pending || command === "", onClick: function () { review(effectiveAction); }, variant: "primary" }, "确认审阅"),
1003
- h("details", { key: "fallback" }, [h("summary", { key: "summary" }, "在对话里处理"), command === "" ? h("div", { key: "hint", className: "dshd-note" }, "选择冲突经验并填写处理依据后即可处理。") : h(CopyCommand, { key: "copy", command: command, label: "复制审阅命令" })]),
1306
+ h(Btn, { key: "confirm", disabled: pending || command === "", onClick: function () { review(effectiveAction); }, variant: "primary" }, t("确认审阅")),
1307
+ 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("复制审阅命令") })]),
1004
1308
  ]) : null,
1005
- preview ? h("section", { key: "preview", className: "dshd-reviewform", "aria-label": "规则变更预览" }, [
1006
- h("strong", { key: "heading" }, "规则变更预览"),
1007
- h("div", { key: "target", className: "dshd-note" }, "目标:" + preview.resolvedPath),
1008
- h("pre", { key: "diff", className: "dshd-diff", tabIndex: 0 }, preview.diff || "管理块内容已经一致,无需更改。"),
1009
- h("div", { key: "help", className: "dshd-note" }, "将按此预览重建 Dream 管理块,请核对新增和删除内容。块外内容保留,写入前创建回滚备份。"),
1010
- h(Btn, { key: "apply", disabled: pending, variant: "primary", onClick: function () { submit({ operation: "apply", token: preview.token }, "规则已应用,可在「写入记录」中回滚。"); } }, "应用此变更"),
1011
- h(Btn, { key: "cancel", disabled: pending, onClick: function () { setPreview(null); } }, "取消预览"),
1309
+ preview ? h("section", { key: "preview", className: "dshd-reviewform", "aria-label": t("规则变更预览") }, [
1310
+ h("strong", { key: "heading" }, t("规则变更预览")),
1311
+ h("div", { key: "target", className: "dshd-note" }, t("目标:") + preview.resolvedPath),
1312
+ h("pre", { key: "diff", className: "dshd-diff", tabIndex: 0 }, preview.diff || t("管理块内容已经一致,无需更改。")),
1313
+ h("div", { key: "help", className: "dshd-note" }, t("将按此预览重建 Dream 管理块,请核对新增和删除内容。块外内容保留,写入前创建回滚备份。")),
1314
+ h(Btn, { key: "apply", disabled: pending, variant: "primary", onClick: function () { submit({ operation: "apply", token: preview.token }, t("规则已应用,可在「写入记录」中回滚。")); } }, t("应用此变更")),
1315
+ h(Btn, { key: "cancel", disabled: pending, onClick: function () { setPreview(null); } }, t("取消预览")),
1012
1316
  ]) : null,
1013
1317
  h("details", { key: "feedback", className: "dshd-feedback" }, [
1014
- h("summary", { key: "summary" }, "使用反馈 · 有用 " + (row.feedback.useful || 0) + " · 不适用 " + (row.feedback.notApplicable || 0)),
1015
- h("label", { key: "note" }, ["场景备注(可选)", h("textarea", { "aria-label": "反馈场景备注", rows: 2, disabled: pending, maxLength: 500, value: feedbackNote, placeholder: "这次在哪个场景使用…", onChange: function (event) { setFeedbackNote(event.target.value); } })]),
1016
- h(Btn, { key: "useful", disabled: pending, onClick: function () { feedback("useful"); } }, "这次有用"),
1017
- h(Btn, { key: "not", disabled: pending, onClick: function () { feedback("not-applicable"); } }, "这次不适用"),
1018
- h("div", { key: "help", className: "dshd-note" }, "记录使用情况,供之后复核;采纳与驳回由审阅决定。"),
1318
+ h("summary", { key: "summary" }, t("使用反馈 · 有用 ") + (row.feedback.useful || 0) + t(" · 不适用 ") + (row.feedback.notApplicable || 0)),
1319
+ 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); } })]),
1320
+ h(Btn, { key: "useful", disabled: pending, onClick: function () { feedback("useful"); } }, t("这次有用")),
1321
+ h(Btn, { key: "not", disabled: pending, onClick: function () { feedback("not-applicable"); } }, t("这次不适用")),
1322
+ h("div", { key: "help", className: "dshd-note" }, t("记录使用情况,供之后复核;采纳与驳回由审阅决定。")),
1019
1323
  ...(Array.isArray(row.feedback.recent) ? row.feedback.recent.slice(-3).reverse().map(function (item, index) {
1020
- return h("div", { key: "recent-" + index, className: "dshd-note" }, formatDate(item.at) + " · " + (item.vote === "useful" ? "有用" : "不适用") + (item.note ? " · " + item.note : ""));
1324
+ return h("div", { key: "recent-" + index, className: "dshd-note" }, formatDate(item.at) + " · " + (item.vote === "useful" ? t("有用") : t("不适用")) + (item.note ? " · " + item.note : ""));
1021
1325
  }) : []),
1022
1326
  ]),
1023
1327
  ]);
@@ -1033,15 +1337,15 @@ function RollbackControl(props) {
1033
1337
  try {
1034
1338
  await postAction({ operation: "rollback", backupId: props.row.id });
1035
1339
  setConfirm(false);
1036
- if (props.onChanged) props.onChanged("已恢复上一次的 Dream 管理块,块外内容保留。");
1037
- } catch (failure) { setError(failure.message || String(failure)); }
1340
+ if (props.onChanged) props.onChanged(t("已恢复上一次的 Dream 管理块,块外内容保留。"));
1341
+ } catch (failure) { setError(displayError(failure)); }
1038
1342
  finally { setPending(false); }
1039
1343
  };
1040
1344
  return h("div", { className: "dshd-reviewform" }, [
1041
- h(Btn, { key: "start", disabled: pending, onClick: function () { setConfirm(!confirm); } }, confirm ? "取消回滚" : "回滚这次变更"),
1345
+ h(Btn, { key: "start", disabled: pending, onClick: function () { setConfirm(!confirm); } }, confirm ? t("取消回滚") : t("回滚这次变更")),
1042
1346
  confirm ? h("div", { key: "confirm" }, [
1043
- h("p", { key: "target", className: "dshd-note" }, "将恢复「" + props.row.target + "」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。"),
1044
- h(Btn, { key: "apply", variant: "primary", disabled: pending, onClick: rollback }, pending ? "正在回滚…" : "确认回滚管理块"),
1347
+ h("p", { key: "target", className: "dshd-note" }, t("将恢复「") + props.row.target + t("」的前一个 Dream 管理块。块外内容保留;管理块已被修改时会拒绝回滚。")),
1348
+ h(Btn, { key: "apply", variant: "primary", disabled: pending, onClick: rollback }, pending ? t("正在回滚…") : t("确认回滚管理块")),
1045
1349
  ]) : null,
1046
1350
  error ? h("div", { key: "error", role: "alert", className: "dshd-hint" }, error) : null,
1047
1351
  ]);
@@ -1055,36 +1359,36 @@ function KnowledgeRow(props) {
1055
1359
  return h("div", {
1056
1360
  className: "dshd-knowledge " + row.stateClass + (privacy === true && peek === true ? " dshd-peek" : ""),
1057
1361
  onClick: reveal,
1058
- title: privacy === true ? "隐私模式:点击此条临时查看" : undefined,
1362
+ title: privacy === true ? t("隐私模式:点击此条临时查看") : undefined,
1059
1363
  }, [
1060
1364
  h("div", { className: "dshd-khead", key: "h" }, [
1061
1365
  h("span", { className: "dshd-kbadge", key: "b" }, row.stateLabel),
1062
1366
  h("span", { className: "dshd-ktitle", key: "t", title: row.title }, row.title),
1063
1367
  ]),
1064
1368
  h("div", { className: "dshd-kmeta", key: "m" }, [
1065
- h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, "最近核验:" + row.lastText),
1369
+ h("span", { className: "dshd-klast", key: "l", title: row.lastTitle }, t("最近核验:") + row.lastText),
1066
1370
  ]),
1067
1371
  h("div", { className: "dshd-kbody", key: "body" }, [
1068
- h("div", { key: "when" }, "何时用:" + (row.when !== "" ? row.when : "未标注适用条件")),
1069
- h("div", { key: "action", className: "dshd-action" }, "怎么做:" + (row.action || "未标注具体动作")),
1372
+ h("div", { key: "when" }, t("何时用:") + (row.when !== "" ? row.when : t("未标注适用条件"))),
1373
+ h("div", { key: "action", className: "dshd-action" }, t("怎么做:") + (row.action || t("未标注具体动作"))),
1070
1374
  h("details", { key: "details", onClick: function (event) { event.stopPropagation(); } }, [
1071
- h("summary", { key: "summary" }, "适用范围与证据"),
1072
- h("div", { key: "scope" }, "范围:" + row.scopeLabel),
1073
- row.applicability !== "" ? h("div", { key: "applicability" }, "适用:" + row.applicability) : null,
1074
- row.exceptions.length > 0 ? h("div", { key: "exceptions" }, "例外:" + row.exceptions.join(";")) : null,
1075
- h("div", { key: "evidence" }, "证据:" + row.evidenceSummary),
1076
- row.evidence.length === 0 ? h("div", { key: "sources-empty", className: "dshd-note" }, "没有关联来源记录,采纳前请核对依据。") : h("div", { key: "sources", className: "dshd-sources" }, row.evidence.map(function (item) {
1375
+ h("summary", { key: "summary" }, t("适用范围与证据")),
1376
+ h("div", { key: "scope" }, t("范围:") + row.scopeLabel),
1377
+ row.applicability !== "" ? h("div", { key: "applicability" }, t("适用:") + row.applicability) : null,
1378
+ row.exceptions.length > 0 ? h("div", { key: "exceptions" }, t("例外:") + row.exceptions.join(";")) : null,
1379
+ h("div", { key: "evidence" }, t("证据:") + row.evidenceSummary),
1380
+ 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) {
1077
1381
  return h("details", { key: item.id }, [
1078
- h("summary", { key: "summary" }, "来源记录 · " + (item.verification === "read" ? "已读到来源" : "待核验") + " · " + (item.kind === "session" ? "会话" : item.kind === "user-correction" ? "用户纠正" : "本地资料")),
1079
- h("div", { key: "summary-text" }, item.summary || "未提供摘要"),
1080
- item.sessionId ? h("div", { key: "session", className: "dshd-note" }, "会话:" + item.sessionId + (Number.isInteger(item.recordSeq) ? " · 记录 " + item.recordSeq : "")) : null,
1081
- h("div", { key: "date", className: "dshd-note" }, "记录时间:" + formatDate(item.observedAt)),
1082
- item.verificationReason ? h("div", { key: "reason", className: "dshd-note" }, "核验说明:" + item.verificationReason) : null,
1083
- item.sourceHash ? h("code", { key: "hash", className: "dshd-cmd", title: item.sourceHash }, "来源校验值:" + item.sourceHash) : null,
1382
+ h("summary", { key: "summary" }, t("来源记录 · ") + (item.verification === "read" ? t("已读到来源") : t("待核验")) + " · " + (item.kind === "session" ? t("会话") : item.kind === "user-correction" ? t("用户纠正") : t("本地资料"))),
1383
+ h("div", { key: "summary-text" }, item.summary || t("未提供摘要")),
1384
+ item.sessionId ? h("div", { key: "session", className: "dshd-note" }, t("会话:") + item.sessionId + (Number.isInteger(item.recordSeq) ? t(" · 记录 ") + item.recordSeq : "")) : null,
1385
+ h("div", { key: "date", className: "dshd-note" }, t("记录时间:") + formatDate(item.observedAt)),
1386
+ item.verificationReason ? h("div", { key: "reason", className: "dshd-note" }, t("核验说明:") + (item.verificationReason === 'live-session-source-verified' ? t("已核对当前会话原文;不代表结论正确。") : item.verificationReason)) : null,
1387
+ item.sourceHash ? h("code", { key: "hash", className: "dshd-cmd", title: item.sourceHash }, t("来源校验值:") + item.sourceHash) : null,
1084
1388
  ]);
1085
1389
  })),
1086
1390
  ]),
1087
- props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, "为什么被扣下:" + row.holdReasonLabel) : null,
1391
+ props.showHoldReason === true && row.holdReasonLabel !== "" ? h("div", { key: "hold", className: "dshd-holdreason" }, t("为什么被扣下:") + row.holdReasonLabel) : null,
1088
1392
  ]),
1089
1393
  h(ReviewControls, { row: row, key: row.id + "@" + row.revision, onChanged: props.onChanged }),
1090
1394
  ]);
@@ -1099,22 +1403,22 @@ function KnowledgeCard(props) {
1099
1403
  const noticeNode = notice === "" ? null : h("div", { className: "dshd-knotice", key: "notice", role: "status" }, notice);
1100
1404
  const head = function (extra) {
1101
1405
  return h("div", { className: "dshd-khead", key: "head" }, [
1102
- h("h4", { key: "t" }, "已审阅经验"),
1406
+ h("h4", { key: "t" }, t("已审阅经验")),
1103
1407
  h("span", { className: "dshd-spacer", key: "s" }),
1104
1408
  extra,
1105
1409
  ]);
1106
1410
  };
1107
1411
  if (props.error !== null && props.error !== undefined) {
1108
1412
  return h("section", { className: "dshd-card dshd-kcard" }, [
1109
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1110
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1111
- h("div", { className: "dshd-note", key: "n" }, "旧日记不受影响;修好后点「重试」。"),
1413
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1414
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取经验失败:") + displayError(props.error)),
1415
+ h("div", { className: "dshd-note", key: "n" }, t("旧日记不受影响;修好后点「重试」。")),
1112
1416
  ]);
1113
1417
  }
1114
1418
  if (props.empty === true) {
1115
1419
  const empty = knowledgeEmptyState();
1116
1420
  return h("section", { className: "dshd-card dshd-kcard" }, [
1117
- h("h4", { key: "t" }, "经验"),
1421
+ h("h4", { key: "t" }, t("经验")),
1118
1422
  noticeNode,
1119
1423
  h("div", { className: "dshd-kempty", key: "e" }, [
1120
1424
  h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
@@ -1125,21 +1429,21 @@ function KnowledgeCard(props) {
1125
1429
  }
1126
1430
  return h("section", { className: "dshd-card dshd-kcard" }, [
1127
1431
  head(props.loading === true
1128
- ? h("span", { className: "dshd-hint", key: "loading" }, "加载中…")
1432
+ ? h("span", { className: "dshd-hint", key: "loading" }, t("加载中…"))
1129
1433
  : h("span", { className: "dshd-hint", key: "count" }, knowledgeCountText(vm.stats))),
1130
1434
  noticeNode,
1131
- h("label", { className: "dshd-note", key: "filter" }, ["查看 ", h("select", { "aria-label": "经验状态", value: stateFilter, onChange: function (event) { setStateFilter(event.target.value); setPageSize(5); } }, [
1132
- h("option", { key: "usable", value: "usable" }, "可用经验"), h("option", { key: "rejected", value: "rejected" }, "已驳回历史"),
1435
+ h("label", { className: "dshd-note", key: "filter" }, [t("查看 "), h("select", { "aria-label": t("经验状态"), value: stateFilter, onChange: function (event) { setStateFilter(event.target.value); setPageSize(5); } }, [
1436
+ h("option", { key: "usable", value: "usable" }, t("可用经验")), h("option", { key: "rejected", value: "rejected" }, t("已驳回历史")),
1133
1437
  ])]),
1134
- reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? "暂无可用经验;待核实的记录在下方。" : "没有已驳回记录。") : null,
1438
+ reviewed.length === 0 ? h("div", { className: "dshd-hint", key: "none" }, stateFilter === "usable" ? t("暂无可用经验;待核实的记录在下方。") : t("没有已驳回记录。")) : null,
1135
1439
  h("div", { className: "dshd-krows", key: "rows" }, reviewed.slice(0, pageSize).map(function (row, index) {
1136
1440
  return h(KnowledgeRow, { key: row.id !== "" ? row.id : "k" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true });
1137
1441
  })),
1138
- reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多经验") : null,
1442
+ reviewed.length > pageSize ? h(Btn, { key: "next", onClick: function () { setPageSize(pageSize + 5); } }, t("显示更多经验")) : null,
1139
1443
  vm.shown < vm.total
1140
- ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条经验。")
1444
+ ? h("div", { className: "dshd-note", key: "more" }, t("只显示最近 ") + String(vm.shown) + " / " + String(vm.total) + t(" 条经验。"))
1141
1445
  : null,
1142
- h("div", { className: "dshd-note", key: "note" }, "采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。"),
1446
+ h("div", { className: "dshd-note", key: "note" }, t("采纳记录仍需满足当前任务的平台、版本与项目条件;来源已读不代表结论永远适用。")),
1143
1447
  ]);
1144
1448
  }
1145
1449
 
@@ -1152,34 +1456,34 @@ function ReviewQueueCard(props) {
1152
1456
  : [];
1153
1457
  const head = function (extra) {
1154
1458
  return h("div", { className: "dshd-khead", key: "head" }, [
1155
- h("h4", { key: "t" }, "待核实"),
1459
+ h("h4", { key: "t" }, t("待核实")),
1156
1460
  h("span", { className: "dshd-spacer", key: "s" }),
1157
1461
  extra,
1158
1462
  ]);
1159
1463
  };
1160
1464
  if (props.error !== null && props.error !== undefined) {
1161
1465
  return h("section", { className: "dshd-card dshd-kcard" }, [
1162
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1163
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取经验失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1466
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1467
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取经验失败:") + displayError(props.error)),
1164
1468
  ]);
1165
1469
  }
1166
1470
  if (props.loading === true && rows.length === 0) {
1167
1471
  return h("section", { className: "dshd-card dshd-kcard" }, [
1168
- head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1472
+ head(h("span", { className: "dshd-hint", key: "l" }, t("加载中…"))),
1169
1473
  ]);
1170
1474
  }
1171
1475
  return h("section", { className: "dshd-card dshd-kcard" }, [
1172
- head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + " 条待核实")),
1476
+ head(rows.length === 0 ? null : h("span", { className: "dshd-hint", key: "c" }, String(rows.length) + t(" 条待核实"))),
1173
1477
  rows.length === 0
1174
1478
  ? h("div", { className: "dshd-kempty", key: "empty" }, [
1175
- h("div", { className: "dshd-kempty-title", key: "t" }, "没有待核实的经验"),
1479
+ h("div", { className: "dshd-kempty-title", key: "t" }, t("没有待核实的经验")),
1176
1480
  h("p", { className: "dshd-kempty-body", key: "b" }, reviewQueueEmptyState()),
1177
1481
  ])
1178
1482
  : h("div", { className: "dshd-krows", key: "rows" }, rows.slice(0, pageSize).map(function (row, index) {
1179
1483
  return h(KnowledgeRow, { key: row.id !== "" ? row.id : "held-" + String(index), row: row, onChanged: props.onChanged, privacy: props.privacy === true, showHoldReason: true });
1180
1484
  })),
1181
- rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, "显示更多待核实经验") : null,
1182
- h("div", { className: "dshd-note", key: "note" }, "这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。"),
1485
+ rows.length > pageSize ? h(Btn, { key: "more", onClick: function () { setPageSize(pageSize + 5); } }, t("显示更多待核实经验")) : null,
1486
+ h("div", { className: "dshd-note", key: "note" }, t("这些经验默认不会作为任务建议注入。先核对来源与适用范围,再决定采纳、驳回或继续复核。")),
1183
1487
  ]);
1184
1488
  }
1185
1489
 
@@ -1188,26 +1492,26 @@ function BridgeLogCard(props) {
1188
1492
  const vm = props.vm;
1189
1493
  const head = function (extra) {
1190
1494
  return h("div", { className: "dshd-khead", key: "head" }, [
1191
- h("h4", { key: "t" }, "写入记录"),
1495
+ h("h4", { key: "t" }, t("写入记录")),
1192
1496
  h("span", { className: "dshd-spacer", key: "s" }),
1193
1497
  extra,
1194
1498
  ]);
1195
1499
  };
1196
1500
  if (props.error !== null && props.error !== undefined) {
1197
1501
  return h("section", { className: "dshd-card dshd-kcard" }, [
1198
- head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, "重试")),
1199
- h("div", { className: "dshd-hint", key: "e", role: "alert" }, "读取写入记录失败:" + (props.error.message === "" ? String(props.error) : props.error.message)),
1502
+ head(h(Btn, { key: "retry", onClick: props.onRetry, disabled: props.loading === true }, t("重试"))),
1503
+ h("div", { className: "dshd-hint", key: "e", role: "alert" }, t("读取写入记录失败:") + displayError(props.error)),
1200
1504
  ]);
1201
1505
  }
1202
1506
  if (props.loading === true) {
1203
1507
  return h("section", { className: "dshd-card dshd-kcard" }, [
1204
- head(h("span", { className: "dshd-hint", key: "l" }, "加载中…")),
1508
+ head(h("span", { className: "dshd-hint", key: "l" }, t("加载中…"))),
1205
1509
  ]);
1206
1510
  }
1207
1511
  if (vm.rows.length === 0) {
1208
1512
  const empty = bridgeEmptyState();
1209
1513
  return h("section", { className: "dshd-card dshd-kcard" }, [
1210
- h("h4", { key: "t" }, "写入记录"),
1514
+ h("h4", { key: "t" }, t("写入记录")),
1211
1515
  h("div", { className: "dshd-kempty", key: "empty" }, [
1212
1516
  h("div", { className: "dshd-kempty-title", key: "t2" }, empty.title),
1213
1517
  h("p", { className: "dshd-kempty-body", key: "b" }, empty.body),
@@ -1216,7 +1520,7 @@ function BridgeLogCard(props) {
1216
1520
  ]);
1217
1521
  }
1218
1522
  return h("section", { className: "dshd-card dshd-kcard" }, [
1219
- head(h("span", { className: "dshd-hint", key: "c" }, String(vm.total) + " 次写入 · " + String(vm.rollbackable) + " 次可回滚")),
1523
+ head(h("span", { className: "dshd-hint", key: "c" }, String(vm.total) + t(" 次写入 · ") + String(vm.rollbackable) + t(" 次可回滚"))),
1220
1524
  h("div", { className: "dshd-krows", key: "rows" }, vm.rows.map(function (row, index) {
1221
1525
  return h("div", { className: "dshd-bridge", key: row.id !== "" ? row.id : "bridge-" + String(index) }, [
1222
1526
  h("div", { className: "dshd-bridge-head", key: "h" }, [
@@ -1226,21 +1530,21 @@ function BridgeLogCard(props) {
1226
1530
  h("span", { className: "dshd-klast", key: "at", title: row.atTitle }, row.atText),
1227
1531
  ]),
1228
1532
  h("div", { className: "dshd-bridge-meta", key: "m" }, [
1229
- h("span", { key: "lessons", title: row.lessonsText }, "经验:" + row.lessonsText),
1230
- row.beforeShort === "" ? null : h("span", { key: "before" }, "前哈希:" + row.beforeShort),
1231
- row.afterShort === "" ? null : h("span", { key: "after" }, "后哈希:" + row.afterShort),
1533
+ h("span", { key: "lessons", title: row.lessonsText }, t("经验:") + row.lessonsText),
1534
+ row.beforeShort === "" ? null : h("span", { key: "before" }, t("前哈希:") + row.beforeShort),
1535
+ row.afterShort === "" ? null : h("span", { key: "after" }, t("后哈希:") + row.afterShort),
1232
1536
  h("span", { key: "rb", className: row.rollbackable === true ? "dshd-rollback-ok" : "dshd-rollback-no" }, row.rollbackText),
1233
1537
  ]),
1234
1538
  h("details", { key: "backup" }, [
1235
- h("summary", { key: "summary" }, "备份标识与回滚命令"),
1539
+ h("summary", { key: "summary" }, t("备份标识与回滚命令")),
1236
1540
  h("code", { key: "id", className: "dshd-cmd" }, row.id),
1237
1541
  row.rollbackable === true ? h("code", { key: "command", className: "dshd-cmd", tabIndex: 0 }, bridgeRollbackCommandOf(row)) : null,
1238
1542
  ]),
1239
1543
  row.rollbackable === true ? h(RollbackControl, { key: "rollback", row: row, onChanged: props.onChanged }) : null,
1240
1544
  ]);
1241
1545
  })),
1242
- vm.shown < vm.total ? h("div", { className: "dshd-note", key: "more" }, "只显示最近 " + String(vm.shown) + " / " + String(vm.total) + " 条记录。") : null,
1243
- h("div", { className: "dshd-note", key: "note" }, "回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。"),
1546
+ vm.shown < vm.total ? h("div", { className: "dshd-note", key: "more" }, t("只显示最近 ") + String(vm.shown) + " / " + String(vm.total) + t(" 条记录。")) : null,
1547
+ h("div", { className: "dshd-note", key: "note" }, t("回滚恢复对应备份的 Dream 管理块;块外内容保留,外部修改的管理块会停止回滚。")),
1244
1548
  ]);
1245
1549
  }
1246
1550
 
@@ -1255,12 +1559,12 @@ function DreamCard(props) {
1255
1559
  return h("article", {
1256
1560
  className: "dshd-dream dshd-mood-" + String(moodSlot(dream.mood)) + (privacy && peek === true ? " dshd-peek" : ""),
1257
1561
  onClick: clickCard,
1258
- title: privacy ? "隐私模式:点击卡片临时查看" : undefined,
1562
+ title: privacy ? t("隐私模式:点击卡片临时查看") : undefined,
1259
1563
  }, [
1260
1564
  h("header", { className: "dshd-dream-head", key: "h" }, [
1261
1565
  h("time", { className: "dshd-time", key: "t", dateTime: dream.at === "" ? undefined : dream.at, title: dream.at === "" ? timeText : timeText + " · " + dream.at }, timeText),
1262
1566
  dreamAgo(dream, props.now) === "" ? null : h("span", { className: "dshd-ago", key: "a" }, dreamAgo(dream, props.now)),
1263
- h("span", { className: "dshd-mood", key: "m", title: "心境:" + dream.mood }, [
1567
+ h("span", { className: "dshd-mood", key: "m", title: t("心境:") + dream.mood }, [
1264
1568
  h("i", { className: "dshd-mooddot " + toneClass(moodSlot(dream.mood)), key: "d" }),
1265
1569
  dream.mood,
1266
1570
  ]),
@@ -1270,7 +1574,7 @@ function DreamCard(props) {
1270
1574
  className: "dshd-linkbtn",
1271
1575
  key: "btn",
1272
1576
  onClick: function (event) { if (event !== undefined && event.stopPropagation !== undefined) event.stopPropagation(); setExpanded(expanded !== true); },
1273
- }, expanded === true ? "收起" : "展开全文") : null,
1577
+ }, expanded === true ? t("收起") : t("展开全文")) : null,
1274
1578
  ]),
1275
1579
  h("div", { className: "dshd-reflection" + (collapsible && expanded !== true ? " dshd-clamp" : ""), key: "r" }, inlineNodes(dream.reflection, "reflection-" + String(props.index))),
1276
1580
  dream.lessons.length === 0 ? null : h("ol", { className: "dshd-lessons", key: "l" }, dream.lessons.map((lesson, index) => h("li", { key: "li-" + String(index) + "-" + lesson }, [
@@ -1287,17 +1591,17 @@ function EmptyState(props) {
1287
1591
  if (props.query !== undefined && props.query !== "") {
1288
1592
  return h("div", { className: "dshd-empty" }, [
1289
1593
  moon,
1290
- h("h3", { key: "t" }, "没有找到「" + props.query + "」相关的梦"),
1291
- h("p", { key: "p" }, "换个关键词试试,或者清空搜索看全部梦境。"),
1292
- props.onClear ? h("div", { key: "a" }, h(Btn, { onClick: props.onClear }, "清空搜索")) : null,
1594
+ h("h3", { key: "t" }, t("没有找到「") + props.query + t("」相关的梦")),
1595
+ h("p", { key: "p" }, t("换个关键词试试,或者清空搜索看全部梦境。")),
1596
+ props.onClear ? h("div", { key: "a" }, h(Btn, { onClick: props.onClear }, t("清空搜索"))) : null,
1293
1597
  ]);
1294
1598
  }
1295
1599
  return h("div", { className: "dshd-empty" }, [
1296
1600
  moon,
1297
- h("h3", { key: "t" }, "还没有梦境日记"),
1298
- h("p", { key: "p" }, "对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。"),
1601
+ h("h3", { key: "t" }, t("还没有梦境日记")),
1602
+ h("p", { key: "p" }, t("自动整理的内容在「经验」页待审阅;日记可让助手复盘时单独生成。")),
1299
1603
  h("div", { className: "dshd-chips", key: "c" }, [
1300
- h("span", { className: "dshd-chip", key: "s1" }, "对 agent 说「做个梦」"),
1604
+ h("span", { className: "dshd-chip", key: "s1" }, t("对 agent 说「做个梦」")),
1301
1605
  h("span", { className: "dshd-chip", key: "s2" }, "dream_digest → dream_save"),
1302
1606
  ]),
1303
1607
  ]);
@@ -1307,6 +1611,76 @@ function Skeleton() {
1307
1611
  return h("div", null, [0, 1, 2].map((index) => h("div", { className: "dshd-skel", key: String(index) })));
1308
1612
  }
1309
1613
 
1614
+ function AutomaticMemoryCard(props) {
1615
+ const [value, setValue] = useState(null);
1616
+ const [failure, setFailure] = useState(null);
1617
+ const [pending, setPending] = useState(false);
1618
+ const a = function (zh, en) { return languageOf() === 'en' ? en : zh; };
1619
+ useEffect(function () {
1620
+ let alive = true, timer;
1621
+ const controller = new AbortController();
1622
+ async function refresh() {
1623
+ try {
1624
+ const response = await fetch(routeUrlFor('api/dsh-dream/automatic', ''), { credentials: 'same-origin', signal: controller.signal });
1625
+ if (!response.ok) throw new Error(a('自动功能状态暂时无法读取,请刷新重试。', 'Automatic memory status is unavailable. Refresh to retry.'));
1626
+ const payload = await response.json();
1627
+ if (alive) { setValue(payload.automatic); setFailure(null); }
1628
+ } catch (error) { if (alive) setFailure(error); }
1629
+ if (alive) timer = setTimeout(refresh, 15000);
1630
+ }
1631
+ refresh();
1632
+ return function () { alive = false; clearTimeout(timer); controller.abort(); };
1633
+ }, [props.reloadToken]);
1634
+ async function toggle(key, next) {
1635
+ setPending(true); setFailure(null);
1636
+ try { const result = await postAction({ operation: 'automatic', [key]: next }); setValue(result.automatic); }
1637
+ catch (error) { setFailure(error); }
1638
+ finally { setPending(false); }
1639
+ }
1640
+ const reasons = {
1641
+ saved: a('已保存新候选,等待审阅', 'New candidates saved; review required'),
1642
+ merged: a('已合并重复经验', 'Duplicate lessons merged'),
1643
+ 'nothing-useful': a('没有可复用的新经验', 'No reusable new lessons'),
1644
+ 'ordinary-chat': a('普通闲聊,已跳过', 'Ordinary chat skipped'),
1645
+ 'no-source': a('缺少可见来源,已跳过', 'No visible sources; skipped'),
1646
+ 'daily-budget': a('今天的整理预算已用完', 'Daily consolidation budget reached'),
1647
+ cooldown: a('处于冷却时间,已跳过', 'Cooling down; skipped'),
1648
+ disabled: a('自动整理已暂停', 'Automatic collection paused'),
1649
+ unfinished: a('任务未正常完成,已跳过', 'Turn did not complete normally; skipped'),
1650
+ 'no-model': a('当前会话尚无可用模型', 'No model route for this session'),
1651
+ recording: a('正在整理候选经验', 'Collecting candidate lessons'),
1652
+ timeout: a('整理超时,本次已停止', 'Consolidation timed out and stopped'),
1653
+ cancelled: a('新输入或关闭操作已停止整理', 'New input or a shutdown stopped consolidation'),
1654
+ superseded: a('已有新任务,旧轮次不再整理', 'New work superseded this turn; skipped'),
1655
+ 'model-error': a('模型返回失败或无法核验,请检查 DSH 模型连接', 'Model output failed or could not be verified. Check the DSH model connection.'),
1656
+ 'input-limit': a('内容超过预算,已跳过', 'Input exceeds the budget; skipped'),
1657
+ };
1658
+ const last = value && value.last;
1659
+ const minutes = Math.max(0, Number(value && value.cooldownMinutes) || 0);
1660
+ const seconds = Math.ceil(minutes * 60);
1661
+ const interval = minutes < 1 ? String(seconds) + a(' 秒', seconds === 1 ? ' second' : ' seconds') : new Intl.NumberFormat(languageOf() === 'en' ? 'en-US' : 'zh-CN', { maximumFractionDigits: 2 }).format(minutes) + a(' 分钟', minutes === 1 ? ' minute' : ' minutes');
1662
+ return h('section', { className: 'dshd-card dshd-kcard', 'aria-label': a('自动记忆', 'Automatic memory') }, [
1663
+ h('h3', { key: 'title' }, a('自动记忆', 'Automatic memory')),
1664
+ h('p', { key: 'intro' }, a('任务结束后整理可复用经验,不用记住“做梦”口令。新经验先待审阅,不自动修改项目规则。', 'Collect reusable lessons after tasks, without a trigger phrase. New lessons await review; project rules are not changed automatically.')),
1665
+ failure ? h('p', { key: 'error', role: 'alert' }, displayError(failure)) : null,
1666
+ !value ? (failure ? null : h('p', { key: 'loading' }, a('正在读取状态…', 'Loading status…'))) : h('div', { key: 'controls' }, [
1667
+ h('p', { key: 'status', role: 'status' }, value.problem ? a('状态异常,请检查 DSH 日志。', 'Status error. Check the DSH logs.') : value.available ? (value.enabled === false ? a('自动整理已暂停', 'Automatic collection paused') : value.running ? a('正在整理', 'Consolidating') : a('已就绪,等待符合条件的任务', 'Ready; waiting for a qualifying task')) : a('当前宿主服务尚未就绪;手动工具仍可使用。', 'Host services are not ready; manual tools remain available.')),
1668
+ h('label', { key: 'collect', style: { display: 'block', marginBottom: '8px' } }, [
1669
+ h('input', { key: 'input', type: 'checkbox', checked: value.enabled === true, disabled: pending, onChange: event => toggle('enabled', event.target.checked) }),
1670
+ ' ' + a('自动整理候选经验', 'Collect candidate lessons automatically'),
1671
+ ]),
1672
+ h('label', { key: 'retrieve', style: { display: 'block' } }, [
1673
+ h('input', { key: 'input', type: 'checkbox', checked: value.retrievalEnabled === true, disabled: pending, onChange: event => toggle('retrievalEnabled', event.target.checked) }),
1674
+ ' ' + a('自动取回已采纳经验', 'Retrieve accepted lessons automatically'),
1675
+ ]),
1676
+ h('p', { key: 'budget' }, a('今天已调用模型 ', 'Model calls today: ') + String(value.callsToday ?? 0) + ' / ' + String(value.maxCallsPerDay ?? 0) + a(' 次;每次间隔至少 ', '; at least ') + interval + a('。', ' between calls.')),
1677
+ h('p', { key: 'cost', className: 'dshd-note' }, a('沿用当前会话模型,会消耗模型额度;普通闲聊跳过。每次最多 ', 'Uses the current session model and its quota; ordinary chat is skipped. Up to ') + String(value.maxOutputTokens ?? 0) + a(' 个输出 token。', ' output tokens per call.')),
1678
+ last ? h('p', { key: 'last' }, a('最近检查:', 'Last check: ') + formatDate(new Date(last.at).toISOString()) + ' ' + formatTime(new Date(last.at).toISOString()) + ' · ' + (reasons[last.reason] || a('已检查', 'Checked'))) : null,
1679
+ h(Btn, { key: 'review', onClick: props.onReview }, a('待审阅 ', 'Awaiting review: ') + String(value.candidates ?? 0)),
1680
+ ]),
1681
+ ]);
1682
+ }
1683
+
1310
1684
  // ───────────────────────── 主面板 ─────────────────────────
1311
1685
 
1312
1686
  function DreamPanel() {
@@ -1363,7 +1737,7 @@ function DreamPanel() {
1363
1737
  setKnowledgeError(null);
1364
1738
  fetchKnowledge().then(function (payload) {
1365
1739
  if (alive !== true) return;
1366
- setKnowledge(normalizeKnowledge(payload, new Date()));
1740
+ setKnowledge(payload);
1367
1741
  setKnowledgeLoading(false);
1368
1742
  }).catch(function (failure) {
1369
1743
  if (alive !== true) return;
@@ -1381,7 +1755,7 @@ function DreamPanel() {
1381
1755
  setBridgeError(null);
1382
1756
  fetchBridgeRecords().then(function (payload) {
1383
1757
  if (alive !== true) return;
1384
- setBridge(normalizeBridgeRecords(payload, new Date()));
1758
+ setBridge(payload);
1385
1759
  setBridgeLoading(false);
1386
1760
  }).catch(function (failure) {
1387
1761
  if (alive !== true) return;
@@ -1393,8 +1767,8 @@ function DreamPanel() {
1393
1767
  }, [reloadToken]);
1394
1768
 
1395
1769
  const vm = viewModelOf(dreams, stats, now);
1396
- const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : knowledge;
1397
- const bridgeVm = bridge === null ? { rows: [], total: 0, shown: 0, rollbackable: 0 } : bridge;
1770
+ const knowledgeVm = knowledge === null ? { rows: [], total: 0, shown: 0, stats: null } : normalizeKnowledge(knowledge, now);
1771
+ const bridgeVm = bridge === null ? { rows: [], total: 0, shown: 0, rollbackable: 0 } : normalizeBridgeRecords(bridge, now);
1398
1772
  const pristine = vm.count === 0 && query === "";
1399
1773
  const filterCount = filterCountOf(filters);
1400
1774
  const visibleDreams = applyFilters(dreams, filters);
@@ -1486,8 +1860,8 @@ function DreamPanel() {
1486
1860
  className: "dshd-input",
1487
1861
  type: "text",
1488
1862
  value: queryInput,
1489
- placeholder: "搜索反思与教训…",
1490
- "aria-label": "搜索梦境",
1863
+ placeholder: t("搜索反思与教训…"),
1864
+ "aria-label": t("搜索梦境"),
1491
1865
  onChange: function (event) { setQueryInput(event.target.value); },
1492
1866
  onKeyDown: function (event) { if (event.key === "Enter") setQuery(event.target.value.trim()); },
1493
1867
  }),
@@ -1495,13 +1869,13 @@ function DreamPanel() {
1495
1869
  key: "clear-search",
1496
1870
  type: "button",
1497
1871
  className: "dshd-clear",
1498
- "aria-label": "清空搜索",
1872
+ "aria-label": t("清空搜索"),
1499
1873
  onClick: clearSearch,
1500
1874
  }, "×"),
1501
1875
  ]),
1502
1876
  h("span", { className: "dshd-spacer", key: "spacer" }),
1503
1877
  pristine || activeTab !== "journal" ? null : h("span", { className: "dshd-hint", key: "count" }, loading === true
1504
- ? "加载中…"
1878
+ ? t("加载中…")
1505
1879
  : countTextOf(query, vm.total, vm.count, filterCount, visibleDreams.length)),
1506
1880
  showFilterButton === true && activeTab === "journal" ? h("div", { className: "dshd-filter", key: "filter" }, [
1507
1881
  h(Btn, {
@@ -1509,10 +1883,10 @@ function DreamPanel() {
1509
1883
  on: filterCount > 0,
1510
1884
  pressed: filterCount > 0,
1511
1885
  onClick: function () { setFilterOpen(filterOpen !== true); },
1512
- title: "筛选作用于已加载窗口,不影响全量统计",
1513
- }, "筛选" + (filterCount > 0 ? " · " + String(filterCount) : "") + " ▾"),
1514
- filterOpen === true ? h("div", { className: "dshd-filterpop", key: "pop", role: "group", "aria-label": "筛选梦境" }, [
1515
- h("div", { className: "dshd-filtertitle", key: "title" }, "筛选当前已加载的 " + String(vm.count) + " 场,不影响全量统计"),
1886
+ title: t("筛选作用于已加载窗口,不影响全量统计"),
1887
+ }, t("筛选") + (filterCount > 0 ? " · " + String(filterCount) : "") + " ▾"),
1888
+ filterOpen === true ? h("div", { className: "dshd-filterpop", key: "pop", role: "group", "aria-label": t("筛选梦境") }, [
1889
+ h("div", { className: "dshd-filtertitle", key: "title" }, t("筛选当前已加载的 ") + String(vm.count) + t(" 场,不影响全量统计")),
1516
1890
  h("div", { key: "moods" }, vm.moodRows.map(function (row) {
1517
1891
  const on = filters.moods.indexOf(row.mood) !== -1;
1518
1892
  return h("button", {
@@ -1528,10 +1902,10 @@ function DreamPanel() {
1528
1902
  className: "dshd-chipbtn" + (filters.withLessons === true ? " on" : ""),
1529
1903
  "aria-pressed": filters.withLessons === true ? "true" : "false",
1530
1904
  onClick: toggleOnlyLessons,
1531
- }, "只看有教训的梦")),
1905
+ }, t("只看有教训的梦"))),
1532
1906
  h("div", { className: "dshd-filterfoot", key: "foot" }, [
1533
- h("button", { key: "clearbtn", type: "button", className: "dshd-linkbtn", onClick: clearFilters, disabled: filterCount === 0 }, "清空筛选"),
1534
- h(Btn, { key: "done", onClick: function () { setFilterOpen(false); } }, "完成"),
1907
+ h("button", { key: "clearbtn", type: "button", className: "dshd-linkbtn", onClick: clearFilters, disabled: filterCount === 0 }, t("清空筛选")),
1908
+ h(Btn, { key: "done", onClick: function () { setFilterOpen(false); } }, t("完成")),
1535
1909
  ]),
1536
1910
  ]) : null,
1537
1911
  ]) : null,
@@ -1540,15 +1914,15 @@ function DreamPanel() {
1540
1914
  onClick: togglePrivacy,
1541
1915
  on: privacy === true,
1542
1916
  pressed: privacy === true,
1543
- title: "显示层模糊;DOM 仍为明文,不改文件、不加密",
1544
- }, privacy === true ? "隐私模式:开" : "隐私模式"),
1545
- h(Btn, { key: "refresh", onClick: reload, disabled: loading === true, title: "重新读取梦境日记" }, "刷新"),
1917
+ title: t("显示层模糊;DOM 仍为明文,不改文件、不加密"),
1918
+ }, privacy === true ? t("隐私模式:开") : t("隐私模式")),
1919
+ h(Btn, { key: "refresh", onClick: reload, disabled: loading === true, title: t("重新读取梦境日记") }, t("刷新")),
1546
1920
  ]);
1547
1921
 
1548
1922
  const errorBox = error === null ? null : h("div", { className: "dshd-alert error", role: "alert", key: "error" }, [
1549
- h("span", { key: "m" }, "读取梦境日记失败:" + (error.message === "" ? String(error) : error.message)),
1923
+ h("span", { key: "m" }, t("读取梦境日记失败:") + displayError(error)),
1550
1924
  h("span", { className: "dshd-spacer", key: "s" }),
1551
- h(Btn, { key: "retry", onClick: reload, variant: "primary" }, "重试"),
1925
+ h(Btn, { key: "retry", onClick: reload, variant: "primary" }, t("重试")),
1552
1926
  ]);
1553
1927
 
1554
1928
  const body = function () {
@@ -1558,8 +1932,8 @@ function DreamPanel() {
1558
1932
  if (vm.count === 0) {
1559
1933
  if (error !== null) return h("div", { key: "failed-wrap" }, [
1560
1934
  h("div", { className: "dshd-empty", key: "failed" }, [
1561
- h("h3", { key: "t" }, "暂时读不到梦境日记"),
1562
- h("p", { key: "p" }, "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。"),
1935
+ h("h3", { key: "t" }, t("暂时读不到梦境日记")),
1936
+ h("p", { key: "p" }, t("等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。")),
1563
1937
  ]),
1564
1938
  ]);
1565
1939
  return h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch });
@@ -1567,10 +1941,10 @@ function DreamPanel() {
1567
1941
  return h("div", { key: "content" }, [
1568
1942
  vm.showTiles
1569
1943
  ? h("div", { className: "dshd-tiles", key: "tiles" }, [
1570
- h(StatTile, { key: "total", label: "梦境总数", value: String(vm.total) + " 场", hint: vm.total > vm.count ? "本页显示最近 " + String(vm.count) + " 场" : "全部梦境日记" }),
1571
- h(StatTile, { key: "span", label: "时间跨度", value: vm.spanText, hint: vm.spanHint, title: vm.spanFull }),
1572
- h(StatTile, { key: "latest", label: "最近一场", value: vm.latestText, hint: vm.latestDate }),
1573
- h(StatTile, { key: "mood", label: "心境", value: String(vm.moodCount) + " 种", hint: vm.moodCount === 0 ? "" : "最常见:" + vm.moodRows[0].mood }),
1944
+ h(StatTile, { key: "total", label: t("梦境总数"), value: String(vm.total) + t(" 场"), hint: vm.total > vm.count ? t("本页显示最近 ") + String(vm.count) + t(" 场") : t("全部梦境日记") }),
1945
+ h(StatTile, { key: "span", label: t("时间跨度"), value: vm.spanText, hint: vm.spanHint, title: vm.spanFull }),
1946
+ h(StatTile, { key: "latest", label: t("最近一场"), value: vm.latestText, hint: vm.latestDate }),
1947
+ h(StatTile, { key: "mood", label: t("心境"), value: String(vm.moodCount) + t(" 种"), hint: vm.moodCount === 0 ? "" : t("最常见:") + vm.moodRows[0].mood }),
1574
1948
  ])
1575
1949
  : h("div", { className: "dshd-headline", key: "headline" }, vm.headline),
1576
1950
  h("div", { className: "dshd-two", key: "two" }, [
@@ -1578,14 +1952,14 @@ function DreamPanel() {
1578
1952
  h(LessonCard, { key: "lessons", vm: vm }),
1579
1953
  ]),
1580
1954
  h("div", { className: "dshd-listhead", key: "listhead" }, [
1581
- h("h4", { key: "t" }, query === "" && filterCount === 0 ? "梦境时间线" : (query === "" ? "筛选结果" : "搜索结果")),
1955
+ h("h4", { key: "t" }, query === "" && filterCount === 0 ? t("梦境时间线") : (query === "" ? t("筛选结果") : t("搜索结果"))),
1582
1956
  h("span", { className: "dshd-hint", key: "n" }, listHintOf(query, vm.count, filterCount, visibleDreams.length)),
1583
1957
  ]),
1584
1958
  visibleDreams.length === 0 && filterCount > 0
1585
1959
  ? h("div", { className: "dshd-filterempty", key: "filtered" }, [
1586
- h("span", { key: "t" }, "没有符合筛选的梦(筛选作用于已加载的 " + String(vm.count) + " 场)。"),
1960
+ h("span", { key: "t" }, t("没有符合筛选的梦(筛选作用于已加载的 ") + String(vm.count) + t(" 场)。")),
1587
1961
  h("span", { className: "dshd-spacer", key: "s" }),
1588
- h(Btn, { key: "clearbtn", onClick: clearFilters }, "清空筛选"),
1962
+ h(Btn, { key: "clearbtn", onClick: clearFilters }, t("清空筛选")),
1589
1963
  ])
1590
1964
  : h("div", { className: "dshd-timeline", key: "timeline" }, visibleDreams.map(function (dream, index) {
1591
1965
  return h(DreamCard, { key: (dream.id !== "" ? dream.id : "dream") + "#" + String(index), dream: dream, now: now, privacy: privacy, index: index });
@@ -1596,26 +1970,28 @@ function DreamPanel() {
1596
1970
  return h("div", { className: "dshd-root" + (privacy === true ? " dshd-privacy" : ""), key: "root" }, [
1597
1971
  h("div", { className: "dshd-shell", key: "shell" }, [
1598
1972
  errorBox,
1599
- h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": "梦境面板视图" }, [["journal", "日记"], ["knowledge", "经验"], ["bridge", "写入记录"]].map(function (item) {
1973
+ h(AutomaticMemoryCard, { key: 'automatic', reloadToken: reloadToken, onReview: function () { setActiveTab('knowledge'); reload(); } }),
1974
+ h("nav", { className: "dshd-tabs", key: "tabs", "aria-label": t("梦境面板视图") }, [["journal", t("日记")], ["knowledge", t("经验")], ["bridge", t("写入记录")]].map(function (item) {
1600
1975
  return h("button", { key: item[0], type: "button", className: "dshd-chipbtn" + (activeTab === item[0] ? " on" : ""), "aria-pressed": activeTab === item[0], onClick: function () { setFilterOpen(false); setActiveTab(item[0]); } }, item[1]);
1601
1976
  })),
1602
1977
  toolbar,
1603
- privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。") : null,
1978
+ privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, t("隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。")) : null,
1604
1979
  body(),
1605
1980
  activeTab !== "journal" ? null : h("div", { className: "dshd-footer", key: "footer" }, [
1606
1981
  h("span", { key: "l" }, footerTextOf(query, limit, vm.total, vm.count)),
1607
1982
  h("span", { className: "dshd-spacer", key: "sp" }),
1608
- canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? "加载中…" : "显示更早的梦") : null,
1609
- h("span", { key: "r" }, "数据来自梦境日记(dreams.jsonl)"),
1983
+ canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? t("加载中…") : t("显示更早的梦")) : null,
1984
+ h("span", { key: "r" }, t("数据来自梦境日记(dreams.jsonl)")),
1610
1985
  ]),
1611
1986
  ]),
1612
1987
  ]);
1613
1988
  }
1614
1989
 
1615
- function DreamSection() {
1990
+ function DreamSection({ locale } = {}) {
1991
+ useLanguage(locale);
1616
1992
  return h("div", null, [
1617
- h("h2", { key: "t" }, "梦境日记"),
1618
- h("p", { className: "dshd-sub", key: "p" }, "会话回放 → 反思 → 记忆巩固。这里按时间倒序陈列 agent 做过的梦,附心境方块与教训榜。"),
1993
+ h("h2", { key: "t" }, t("梦境日记")),
1994
+ h("p", { className: "dshd-sub", key: "p" }, t("自动整理任务经验,审阅后回用;日记保留独立的反思记录。")),
1619
1995
  h(DreamPanel, { key: "panel" }),
1620
1996
  ]);
1621
1997
  }
@@ -1761,9 +2137,10 @@ const CSS = [
1761
2137
 
1762
2138
  // ───────────────────────── 挂载 ─────────────────────────
1763
2139
 
1764
- const inject = ["slots"];
2140
+ const inject = ["slots", "locale"];
1765
2141
 
1766
2142
  function apply(ctx) {
2143
+ try { localeService = ctx.get?.("locale") || ctx.locale; } catch { localeService = ctx.locale; }
1767
2144
  try {
1768
2145
  ctx.effect(function () {
1769
2146
  const id = "dsh-dream/client";
@@ -1783,8 +2160,8 @@ function apply(ctx) {
1783
2160
  name: "settings.section",
1784
2161
  id: "dsh-dream",
1785
2162
  order: 55,
1786
- label: function () { return "梦境日记"; },
1787
- inject: function () { return {}; },
2163
+ label: function () { return t("梦境日记"); },
2164
+ inject: function () { return { locale: localeService }; },
1788
2165
  }, DreamSection);
1789
2166
  });
1790
2167
  } catch (error) { /* 没有 slots 服务:仍是合法插件,只是没有设置页 */ }
@@ -1794,6 +2171,7 @@ exports.apply = apply;
1794
2171
  exports.inject = inject;
1795
2172
  // 纯函数内部件:验证脚本可直接取来测,不必另抄一份算法。
1796
2173
  exports.__internals = {
2174
+ displayError: displayError,
1797
2175
  routeUrl: routeUrl,
1798
2176
  routeUrlFor: routeUrlFor,
1799
2177
  KNOWLEDGE_ROUTE: KNOWLEDGE_ROUTE,