diff --git a/emrg/gui/main.js b/emrg/gui/main.js index d2f1ce6..a419f5c 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -403,6 +403,36 @@ vision = false return skills; }); + ipcMain.handle("emrg:listProjects", async () => { + // GUI / 指令 P4:/rant 项目下拉 — daemon list_projects → projects_list + const frame = await client.sendCommandAndWait("list_projects", {}, 5000); + return frame.projects || []; + }); + + ipcMain.handle("emrg:listTasks", async () => { + // GUI / 指令 P4:/trigger — daemon list_tasks → tasks_list + const frame = await client.sendCommandAndWait("list_tasks", {}, 5000); + return frame.tasks || []; + }); + + ipcMain.handle("emrg:triggerTask", async (_e, { name }) => { + // GUI / 指令 P4:/trigger — daemon trigger_task → trigger_result + if (typeof name !== "string" || !name.trim()) throw new Error("invalid task name"); + const frame = await client.sendCommandAndWait("trigger_task", { name: name.trim() }, 5000); + return frame; + }); + + ipcMain.handle("emrg:sendRant", async (_e, { message, project = "" } = {}) => { + // GUI / 指令 P4:/rant — 提交反馈到演化系统(daemon rant 协议,字段序与 rants.jsonl 一致) + if (typeof message !== "string" || !message.trim()) throw new Error("invalid rant message"); + const frame = await client.sendCommandAndWait("rant", { + message: message.trim().slice(0, 10000), + project: String(project || "").trim(), + timestamp: new Date().toISOString(), + }, 5000); + return { ok: true, count: frame.count ?? 0 }; + }); + ipcMain.handle("emrg:setModel", async (_e, { model }) => { await client.sendCommandAndWait("set_model", { model }, 5000); return { ok: true }; diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js index ebe5ad2..87b5ada 100644 --- a/emrg/gui/preload.js +++ b/emrg/gui/preload.js @@ -22,6 +22,10 @@ const api = { listMemories: (payload) => ipcRenderer.invoke("emrg:listMemories", payload), readMemory: (payload) => ipcRenderer.invoke("emrg:readMemory", payload), listSkills: () => ipcRenderer.invoke("emrg:listSkills"), + listProjects: () => ipcRenderer.invoke("emrg:listProjects"), + listTasks: () => ipcRenderer.invoke("emrg:listTasks"), + triggerTask: (payload) => ipcRenderer.invoke("emrg:triggerTask", payload), + sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload), listModels: () => ipcRenderer.invoke("emrg:listModels"), saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload), getSettings: () => ipcRenderer.invoke("emrg:getSettings"), diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index 367059c..b326c1e 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -244,6 +244,36 @@

技能

+ + +
+

🧬 进化 — 告诉 EMRG 往哪里走

+

你的输入会驱动 EMRG 的自我进化——它会认真读,并据此改进自己。

+ + +
+ + +
+
+
+ + + +
+

后台任务

+

点击任务立即触发一次运行。

+
+
+ +
+
+
+ diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 0f0363e..232295b 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -112,14 +112,13 @@ const App = (() => { } } - // ── / 指令(rant 19:44 P1/P2/P3)────────────────── - /** 执行 / 指令。phase 1 纯操作 + phase 2 会话 + phase 3 模型/记忆/技能已实现;phase 4 提示未开放。 */ + // ── / 指令(rant 19:44 P1-P4)────────────────── + /** 执行 / 指令。全部 4 阶段已实现(phase 4 = 演化类 /rant /trigger)。 */ async function handleCommand(parsed) { const cmd = parsed.cmd; const meta = Commands.COMMANDS[cmd]; - if (!meta || meta.phase > 3) { - const phase = meta ? `(阶段 ${meta.phase},后续版本开放)` : ""; - Chat.addSystemMessage(`指令 ${cmd} 暂未开放${phase}。`); + if (!meta) { + Chat.addSystemMessage(`指令 ${cmd} 暂未开放。`); return; } try { @@ -198,6 +197,22 @@ const App = (() => { // P3:技能列表对话框 showSkillsDialog(); break; + case "/rant": + // P4:/rant 直接跟内容 → 快速提交;无参数 → 进化对话框 + if (parsed.args.length > 0) { + await submitRant(parsed.args.join(" "), ""); + } else { + showRantDialog(); + } + break; + case "/trigger": + // P4:/trigger 直接触发;无参数 → 任务列表对话框 + if (parsed.args.length > 0) { + await doTrigger(parsed.args[0]); + } else { + showTasksDialog(); + } + break; default: Chat.addSystemMessage(`指令 ${cmd} 暂未开放。`); } @@ -358,6 +373,90 @@ const App = (() => { } } + // /rant:进化对话框(项目下拉 + 文本输入 → daemon rant 协议) + async function showRantDialog() { + const dialog = $("rant-dialog"); + const msgInput = $("rant-message"); + const projSel = $("rant-project"); + if (!dialog || !msgInput) return; + // 加载项目列表填充下拉 + try { + const projects = await window.emrg.listProjects(); + projSel.innerHTML = ``; + for (const p of projects) { + const name = typeof p === "string" ? p : (p.name || ""); + if (name) projSel.appendChild(el("option", { value: name }, name)); + } + } catch { /* 项目加载失败则只留全局项 */ } + msgInput.value = ""; + dialog.showModal(); + msgInput.focus(); + } + + async function submitRant(message, project) { + const text = String(message || "").trim(); + if (!text) { + Chat.addSystemMessage("写点内容再提交吧。"); + return; + } + try { + const res = await window.emrg.sendRant({ message: text, project }); + Chat.addSystemMessage(`✓ 收到!EMRG 会据此进化${res.count ? `(已累计 ${res.count} 条反馈)` : ""}。`); + } catch (e) { + Chat.addSystemMessage(`提交失败了:${e.message}`); + } + } + + // /trigger:任务列表对话框(点击立即触发) + async function showTasksDialog() { + const list = $("tasks-list"); + const dialog = $("tasks-dialog"); + if (!list || !dialog) return; + list.innerHTML = `
加载中…
`; + dialog.showModal(); + try { + const tasks = await window.emrg.listTasks(); + list.innerHTML = ""; + if (!tasks || tasks.length === 0) { + list.innerHTML = `
没有可触发的任务。
`; + return; + } + for (const t of tasks) { + const row = el("button", { + class: "help-row", + type: "button", + style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;", + }); + const name = el("span", { class: "help-cmd" }, t.name || t.type || "(任务)" ); + const hint = el("span", { class: "help-hint" }, t.enabled === false ? "已停用" : `间隔 ${t.interval ?? "-"}s`); + row.appendChild(name); + row.appendChild(hint); + row.addEventListener("click", async () => { + dialog.close(); + await doTrigger(t.name); + }); + list.appendChild(row); + } + } catch (e) { + list.innerHTML = `
加载任务失败:${escapeHtml(e.message)}
`; + } + } + + async function doTrigger(name) { + const n = String(name || "").trim(); + if (!n) return; + try { + const res = await window.emrg.triggerTask({ name: n }); + if (res.error) { + Chat.addSystemMessage(`触发失败:${res.error}`); + } else { + Chat.addSystemMessage(`已触发任务 ${n}。`); + } + } catch (e) { + Chat.addSystemMessage(`触发失败:${e.message}`); + } + } + // / 补全菜单:输入以 / 开头 → 显示匹配指令;↑↓ 导航、Enter/点击选择填充 function showCmdMenu(prefix) { const items = Commands.getCompletions(prefix); @@ -815,6 +914,14 @@ const App = (() => { $("rewind-close").addEventListener("click", () => $("rewind-dialog").close()); $("memory-close").addEventListener("click", () => $("memory-dialog").close()); $("skills-close").addEventListener("click", () => $("skills-dialog").close()); + $("rant-cancel").addEventListener("click", () => $("rant-dialog").close()); + $("rant-submit").addEventListener("click", async () => { + const msg = $("rant-message")?.value || ""; + const proj = $("rant-project")?.value || ""; + $("rant-dialog").close(); + await submitRant(msg, proj); + }); + $("tasks-close").addEventListener("click", () => $("tasks-dialog").close()); // 设置/首启对话框:Enter 提交(与重命名/模型表单一致的交互) const enterToSave = (fn) => (e) => { diff --git a/emrg/gui/renderer/js/commands.js b/emrg/gui/renderer/js/commands.js index ac53629..9801ecd 100644 --- a/emrg/gui/renderer/js/commands.js +++ b/emrg/gui/renderer/js/commands.js @@ -6,8 +6,8 @@ * 阶段(分期): * phase 1 — 纯操作类(/clear /compact /version /help /image 提示)→ 已实现 * phase 2 — 会话类(/delete /rename /resume /rewind /sessions)→ 已实现 - * phase 3 — 模型/记忆/技能类(/model /memory /skills)→ P3 已实现 - * phase 4 — 演化类(/rant /trigger)→ P4 + * phase 3 — 模型/记忆/技能类(/model /memory /skills)→ 已实现 + * phase 4 — 演化类(/rant /trigger)→ 已实现(全部 15 指令完成) */ const Commands = (() => { diff --git a/emrg/gui/test/app-commands.test.js b/emrg/gui/test/app-commands.test.js index 9a5f76a..957cd50 100644 --- a/emrg/gui/test/app-commands.test.js +++ b/emrg/gui/test/app-commands.test.js @@ -174,11 +174,42 @@ test("P3:/skills 打开技能列表对话框并调用 listSkills", async () => assert.ok(els["skills-dialog"] && els["skills-dialog"].__open === true, "skills dialog opened"); }); -test("P3:phase 4 指令(/rant)仍提示未开放", async () => { - const { ctx, win } = makeSandbox(); - const out = await vm.runInContext( - "(async () => { let m = ''; const orig = EMRG_Chat.addSystemMessage; EMRG_Chat.addSystemMessage = (x) => { m = x; }; await App.handleCommand({ type: 'command', cmd: '/rant', args: [] }); EMRG_Chat.addSystemMessage = orig; return m; })()", - ctx - ); - assert.ok(String(out).includes("暂未开放"), `phase 4 应提示未开放,实际: ${out}`); +test("P4:/rant 无参数打开进化对话框(项目下拉加载)", async () => { + const { ctx, els } = makeSandbox({ + listProjects: async () => [{ name: "emrg" }], + sendRant: async () => ({ ok: true, count: 5 }), + }); + await tick(); + await vm.runInContext("App.handleCommand({ type: 'command', cmd: '/rant', args: [] })", ctx); + assert.ok(els["rant-dialog"] && els["rant-dialog"].__open === true, "rant dialog opened"); +}); + +test("P4:/rant 直接跟内容快速提交(不打开对话框)", async () => { + const { ctx, els } = makeSandbox({ + listProjects: async () => [{ name: "emrg" }], + sendRant: async () => ({ ok: true, count: 5 }), + }); + await tick(); + await vm.runInContext("App.handleCommand({ type: 'command', cmd: '/rant', args: ['希望支持主题切换'] })", ctx); + assert.ok(!(els["rant-dialog"] && els["rant-dialog"].__open), "direct rant submit should not open dialog"); +}); + +test("P4:/trigger 无参数打开任务列表对话框", async () => { + const { ctx, els } = makeSandbox({ + listTasks: async () => [{ name: "emrg-task", type: "evolution", interval: 60 }], + triggerTask: async () => ({ ok: true }), + }); + await tick(); + await vm.runInContext("App.handleCommand({ type: 'command', cmd: '/trigger', args: [] })", ctx); + assert.ok(els["tasks-dialog"] && els["tasks-dialog"].__open === true, "tasks dialog opened"); +}); + +test("P4:/trigger 直接触发(不打开对话框)", async () => { + const { ctx, els } = makeSandbox({ + listTasks: async () => [{ name: "emrg-task", type: "evolution", interval: 60 }], + triggerTask: async () => ({ ok: true }), + }); + await tick(); + await vm.runInContext("App.handleCommand({ type: 'command', cmd: '/trigger', args: ['emrg-task'] })", ctx); + assert.ok(!(els["tasks-dialog"] && els["tasks-dialog"].__open), "direct trigger should not open dialog"); }); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index f56903e..6d55992 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -75,6 +75,8 @@ const ELEMENT_IDS = [ "rewind-dialog", "rewind-list", "rewind-close", "memory-dialog", "memory-list", "memory-detail", "memory-close", "skills-dialog", "skills-list", "skills-close", + "rant-dialog", "rant-message", "rant-project", "rant-cancel", "rant-submit", + "tasks-dialog", "tasks-list", "tasks-close", ]; /** 构造浏览器沙箱(win 即全局对象) */