diff --git a/Agent.md b/Agent.md index 862a4b0c..e99143ca 100644 --- a/Agent.md +++ b/Agent.md @@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request - Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand) - Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端" - Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout) - - Unit tests `npm test` (253: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` - **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711) - Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/.md` → fallback - Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`) @@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` Python: `uv run pytest tests/ -v` (825) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (253: 45 daemon_client + 19 conn-manager + 22 app-commands + 126 renderer smoke + 16 i18n + 7 integration + 3 commands + 6 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +GUI: `cd emrg/gui && npm test` (255: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index cbbfc7e1..8706ebf4 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -387,6 +387,10 @@

重命名对话

+ + + + - - -
diff --git a/emrg/gui/renderer/js/markdown.js b/emrg/gui/renderer/js/markdown.js index c38fb968..d1d99cab 100644 --- a/emrg/gui/renderer/js/markdown.js +++ b/emrg/gui/renderer/js/markdown.js @@ -45,10 +45,33 @@ function ensureConfigured() { window.__emrgMarkedConfigured = true; } +/** AMD-hijack recovery (rant 2026-08-15T10:48:58) — fallback layer: + * monaco loader 的全局 AMD define 会让 UMD marked/dompurify 走 define 分支而非挂 window; + * 顺序修复(index.html marked 先于 loader.js)后正常路径不触发,此处兜底 + 可诊断。 */ +function recoverAmdModule(name) { + try { + // monaco loader 的 require 内部模块表: require.s[name] = { exports } + if (window.require && window.require.s && window.require.s[name]) { + const mod = window.require.s[name].exports; + if (mod) return mod; + } + // 部分打包器形态: define.amd.modules + if (window.define && window.define.amd && window.define.amd.modules && window.define.amd.modules[name]) { + return window.define.amd.modules[name].exports; + } + } catch { /* ignore */ } + return null; +} + renderer.renderMarkdown = async function (mdText) { if (!window.marked) { - console.warn("markdown vendor missing (marked not loaded)"); - return escapeHtml(mdText || ""); + const amdMarked = recoverAmdModule("marked"); + if (amdMarked && typeof amdMarked.parse === "function") { + window.marked = amdMarked; // 兜底取回:AMD 劫持场景仍可渲染 + } else { + console.error("markdown vendor missing (marked not loaded — check index.html order: marked must load before monaco loader.js, which defines global AMD define/require)"); + return escapeHtml(mdText || ""); + } } try { ensureConfigured(); diff --git a/emrg/gui/test/build-config.test.js b/emrg/gui/test/build-config.test.js index 2798aa03..0be335ad 100644 --- a/emrg/gui/test/build-config.test.js +++ b/emrg/gui/test/build-config.test.js @@ -68,13 +68,35 @@ test("vendor ships Monaco editor (loader + editor.main js/css)", () => { } }); -test("renderMarkdown fallback logs when marked is missing", () => { +test("renderMarkdown fallback logs when marked is missing (AMD-hijack diagnostic)", () => { const md = fs.readFileSync(path.join(GUI_ROOT, "renderer", "js", "markdown.js"), "utf-8"); assert.match( md, - /if \(!window\.marked\) \{[\s\S]*console\.warn\("markdown vendor missing \(marked not loaded\)"\)/, - "silent markdown degradation must be diagnostic (console.warn when marked is missing)" + /if \(!window\.marked\) \{[\s\S]*console\.error\("markdown vendor missing \(marked not loaded/, + "silent markdown degradation must be diagnostic (console.error naming the AMD-hijack cause when marked is missing)" ); + assert.match( + md, + /recoverAmdModule\("marked"\)/, + "must attempt AMD module-table recovery before degrading (rant 2026-08-15T10:48:58)" + ); +}); + +test("index.html loads marked/dompurify/highlight BEFORE monaco loader.js (AMD-hijack order guard)", () => { + // rant 2026-08-15T10:48:58:monaco/vs/loader.js 定义全局 AMD define/require; + // UMD marked 见 AMD 走 define 分支 → window.marked 永不设置 → markdown 全退化纯文本。 + // 顺序修复 = marked 等 UMD 库必须先于 loader.js 加载(无 AMD → UMD 分支挂 window)。 + const html = fs.readFileSync(path.join(GUI_ROOT, "renderer", "index.html"), "utf-8"); + const idxMarked = html.indexOf('src="../vendor/marked.min.js"'); + const idxDompurify = html.indexOf('src="../vendor/dompurify.min.js"'); + const idxHighlight = html.indexOf('src="../vendor/highlight.custom.js"'); + const idxMonaco = html.indexOf('src="../vendor/monaco/vs/loader.js"'); + for (const [name, idx] of [["marked", idxMarked], ["dompurify", idxDompurify], ["highlight", idxHighlight], ["monaco loader", idxMonaco]]) { + assert.ok(idx >= 0, `index.html must include ${name} script`); + } + assert.ok(idxMarked < idxMonaco, "marked.min.js must load BEFORE monaco loader.js (else AMD define hijacks UMD marked)"); + assert.ok(idxDompurify < idxMonaco, "dompurify.min.js must load BEFORE monaco loader.js"); + assert.ok(idxHighlight < idxMonaco, "highlight.custom.js must load BEFORE monaco loader.js"); }); test("every main.js/preload.js local require is covered by files whitelist", () => { diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 642b50ad..c3b47ea8 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -824,6 +824,28 @@ test("代码块复制按钮:codeRenderer 输出容器 + 点击复制(设计 assert.ok(chatSrc.includes(".code-copy"), "chat.js 应含复制按钮事件委托"); }); +test("rant 10:48:58:window.marked 缺失时从 Monaco AMD 模块表兜底取回(防退化纯文本)", async () => { + const { ctx, win } = makeSandbox({}); + // 模拟 #801 Monaco 全局 AMD define 劫持后 marked 未挂 window 的场景: + // window.marked = null(沙箱默认),但 monaco require 模块表里有 marked + win.marked = undefined; // 强制缺失(沙箱默认 null → 触发兜底路径) + win.require = { + s: { + marked: { exports: { use: () => {}, parse: async () => "

ok

" } }, + }, + }; + win.DOMPurify = { sanitize: (x) => x }; + const html = await vm.runInContext('window.emrgMarkdown.renderMarkdown("**ok**")', ctx); + assert.ok(String(html).includes("ok"), `AMD 兜底应渲染 marked:${html}`); + assert.ok(win.marked && typeof win.marked.parse === "function", "兜底取回后 window.marked 应被赋值"); + // 无 AMD 表也无 marked → 降级转义 + 可诊断错误(不崩) + const { ctx: ctx2, win: win2 } = makeSandbox({}); + win2.marked = undefined; + win2.require = undefined; + const html2 = await vm.runInContext('window.emrgMarkdown.renderMarkdown("x")', ctx2); + assert.strictEqual(html2, "<b>x</b>", "双缺失 → escapeHtml 降级"); +}); + test("模型切换器:菜单项构建 + 键盘导航 handler 注册", async () => { const { ctx } = makeSandbox({ getSettings: async () => ({