mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
refactor(prompt): 提示词去工具化 + WebUI 插件状态按运行期真相展示
## 1. 系统提示词/人格瘦身(工具用法搬去 tooldef 与 skill) 用户指出:默认系统提示词与 personal 里有大量本该属于 skill / 工具描述的内容。 核对属实,且已造成**真实故障**:提示词教模型用 cmd_run,而 cmd 外置按需后 工具根本不在清单里 ⇒ 模型第一轮调用不存在的工具,拿到「工具 cmd_run 不存在」。 改动: - kernelmod/agent.defaultSystemPrompt:删掉 output_* 用法、工具分类清单、 cmd_run/terminal/files 策略、并行策略、媒体工具清单、emoji 风格要求。 只留:身份 + 「工具描述与技能索引会注入,先读再用」+「只依据工具返回值作答」。 - config.DefaultPersonaPrompt:只留身份/称呼/语气 + 版本以运行时快照为准 (后者由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住)。 - 命令与文件策略下沉到工具描述:cmd_run 补「完整 bash,支持管道/heredoc/重定向」, files_write 补「原子替换 + 覆盖受保护路径前留档」。 - 对话时序块自带语义说明(原先靠提示词解释,提示词一精简就丢语义)。 - 文档新增「提示词分层」章节(中英),写明归属判据与这次的反面教材。 ## 2. WebUI:插件「启动状态」按运行期真相展示 旧实现只看内核 status 的 loaded 一个布尔,三种形态被混成同一个「未加载」: auto_start=false 的按需待命(正常)、该起没起(异常)、plugin_stop 后的运行态停止。 用户看到一屏「未加载」以为插件全坏了。 - sdk.PluginRuntimeInfo 新增 AutoStart / RuntimeStopped;registry 从清单填充。 - pluginmgr /plugins 列表暴露 auto_start / runtime_stopped / builtin / tool_count。 - webui 新增 POST /plugins/<name>/start|stop(运行态,不动持久禁用清单)。 - dashboard 状态列改为五态:已禁用 / 进程丢失 / 已运行 / 按需待命 / 未运行, 并额外标注「自启 / 按需」;按钮改为 启动/停止(运行态)与 禁用/启用(持久态)分列。 - 新增两条回归判据(引用 auto_start/runtime_stopped/alive 字段;startPlugin 走 /start)。 ## 验收(本机实例实测) - files 保持 auto_start=false(按需);问 agent 写文件 → 它先 plugin_start cmd+files, 再 files_write/files_read,内容校验一致;未用 shell 代替文件操作。 - WebUI 插件页截图确认:a2a/acp/agentcli 显示「已运行 自启」, ai_image 显示「按需待命 按需」+「启动」按钮。 - go test(config/agent core/plugin/pluginmgr/webui/sdk)全绿。
This commit is contained in:
@ -109,7 +109,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
s.RegisterTool("cmd_run", sdk.ToolDef{
|
||||
Name: "cmd_run",
|
||||
Description: "执行一条系统命令并返回输出。适用于查询系统信息、运行脚本、操作文件等单次命令场景。命令在临时 shell 中执行,不支持交互。如需交互式终端(如 vim、ssh、top),请使用 terminal_create 相关工具。",
|
||||
Description: "执行一条系统命令并返回输出。适用于查询系统信息、运行脚本、操作文件等单次命令场景。经完整 bash 执行,支持管道、分号、&&、命令替换、heredoc、重定向;不支持交互(vim/ssh/top 这类需要持续输入的程序请用 terminal_create,不要硬等它退出)。",
|
||||
NoMemory: true,
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
|
||||
@ -111,7 +111,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
s.RegisterTool(tp+"write", sdk.ToolDef{
|
||||
Name: tp + "write",
|
||||
Description: fmt.Sprintf("写入文件。自动创建父目录。沙箱路径: %s", p.filesDir),
|
||||
Description: fmt.Sprintf("写入文件(原子替换:先写临时副本再 rename,不会留下半截文件;覆盖受保护系统路径前自动留档原文)。自动创建父目录。生成多行内容直接传全文,不要用 echo 拼接。沙箱路径: %s", p.filesDir),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
|
||||
@ -889,17 +889,25 @@ func (p *Plugin) listPlugins() (interface{}, error) {
|
||||
"entry": m.Entry,
|
||||
"deprecated": m.Deprecated,
|
||||
}
|
||||
// auto_start 来自清单本身(即使插件从未被内核记录过也要能读到):
|
||||
// 前端要区分「按需待命」与「该起没起」,不能只看 loaded。
|
||||
item["auto_start"] = m.ShouldAutoStart()
|
||||
if rt, ok := runtimes[m.Name]; ok {
|
||||
item["loaded"] = rt.Loaded
|
||||
item["alive"] = rt.Alive
|
||||
item["disabled"] = rt.Disabled
|
||||
item["channel"] = rt.Channel
|
||||
item["runtime_stopped"] = rt.RuntimeStopped
|
||||
item["builtin"] = rt.Builtin
|
||||
if rt.PID > 0 {
|
||||
item["pid"] = rt.PID
|
||||
}
|
||||
if rt.CrashCount > 0 {
|
||||
item["crash_count"] = rt.CrashCount
|
||||
}
|
||||
if len(rt.Tools) > 0 {
|
||||
item["tool_count"] = len(rt.Tools)
|
||||
}
|
||||
}
|
||||
plugins = append(plugins, item)
|
||||
}
|
||||
|
||||
@ -5700,6 +5700,13 @@ function renderPlugins() {
|
||||
disabledNames[d.name] = d;
|
||||
});
|
||||
var installedNames = (state.installedPlugins || []).map((p) => p.name);
|
||||
// 安装清单(plugin.json + 运行期)按名字索引:它带着 auto_start /
|
||||
// runtime_stopped / alive,是区分「按需待命」「运行态已停」「该起没起」
|
||||
// 的唯一数据源。内核 status 只给 loaded,光看它三者会被混成「未加载」。
|
||||
var installedByName = {};
|
||||
(state.installedPlugins || []).forEach((p) => {
|
||||
installedByName[p.name] = p;
|
||||
});
|
||||
var allPluginNames = {};
|
||||
plugins.forEach((p) => {
|
||||
allPluginNames[p.name] = true;
|
||||
@ -5707,6 +5714,9 @@ function renderPlugins() {
|
||||
state.disabledPlugins.forEach((d) => {
|
||||
allPluginNames[d.name] = true;
|
||||
});
|
||||
installedNames.forEach((n) => {
|
||||
allPluginNames[n] = true;
|
||||
});
|
||||
html +=
|
||||
'<div class="card"><h2>' +
|
||||
__("所有插件", "All Plugins") +
|
||||
@ -5731,38 +5741,90 @@ function renderPlugins() {
|
||||
names.forEach((name) => {
|
||||
var isExternal = installedNames.indexOf(name) >= 0;
|
||||
var isDisabled = disabledNames[name];
|
||||
var meta = installedByName[name] || {};
|
||||
var loaded = plugins.some((p) => p.name === name);
|
||||
var statusHtml =
|
||||
loaded && !isDisabled
|
||||
? '<span class="badge badge-green">' +
|
||||
__("已加载", "Loaded") +
|
||||
"</span>"
|
||||
: loaded && isDisabled
|
||||
? '<span class="badge badge-yellow">' +
|
||||
__("运行中(禁用待生效)", "Running (disable pending)") +
|
||||
"</span>"
|
||||
: isDisabled
|
||||
? '<span class="badge badge-red">' +
|
||||
__("已禁用", "Disabled") +
|
||||
"</span>"
|
||||
: '<span class="badge badge-gray">' +
|
||||
__("未加载", "Not Loaded") +
|
||||
"</span>";
|
||||
// auto_start 缺省 true(内置插件与老清单都按自启算)。
|
||||
var autoStart = meta.auto_start !== false;
|
||||
var runtimeStopped = meta.runtime_stopped === true;
|
||||
var crashed = loaded && meta.alive === false;
|
||||
// 状态判定必须覆盖五种真实形态,缺一种就会把正常状态显示成故障:
|
||||
// 已禁用 / 进程丢失 / 已运行 / 按需待命 / 该起没起
|
||||
var statusHtml;
|
||||
if (isDisabled && loaded) {
|
||||
statusHtml =
|
||||
'<span class="badge badge-yellow">' +
|
||||
__("运行中(禁用待生效)", "Running (disable pending)") +
|
||||
"</span>";
|
||||
} else if (isDisabled) {
|
||||
statusHtml =
|
||||
'<span class="badge badge-red">' +
|
||||
__("已禁用", "Disabled") +
|
||||
"</span>";
|
||||
} else if (crashed) {
|
||||
statusHtml =
|
||||
'<span class="badge badge-red">' +
|
||||
__("进程丢失", "Process lost") +
|
||||
"</span>";
|
||||
} else if (loaded) {
|
||||
statusHtml =
|
||||
'<span class="badge badge-green">' +
|
||||
(runtimeStopped
|
||||
? __("已运行(停止待生效)", "Running (stop pending)")
|
||||
: __("已运行", "Running")) +
|
||||
"</span>";
|
||||
} else if (!autoStart) {
|
||||
// 按需插件不运行是**正常待命**,不是故障。旧版一律显示
|
||||
// 「未加载」,用户以为插件坏了,实际它在等 plugin_start。
|
||||
statusHtml =
|
||||
'<span class="badge badge-blue">' +
|
||||
__("按需待命", "On-demand") +
|
||||
"</span>";
|
||||
} else if (runtimeStopped) {
|
||||
statusHtml =
|
||||
'<span class="badge badge-yellow">' +
|
||||
__("已停止(运行态)", "Stopped (runtime)") +
|
||||
"</span>";
|
||||
} else {
|
||||
statusHtml =
|
||||
'<span class="badge badge-gray">' +
|
||||
__("未运行", "Not running") +
|
||||
"</span>";
|
||||
}
|
||||
statusHtml +=
|
||||
' <span class="badge badge-' +
|
||||
(autoStart ? "green" : "gray") +
|
||||
'">' +
|
||||
(autoStart ? __("自启", "auto") : __("按需", "on-demand")) +
|
||||
"</span>";
|
||||
var actionsHtml = "";
|
||||
if (loaded) {
|
||||
actionsHtml +=
|
||||
'<button class="btn btn-sm btn-warning" onclick="disablePlugin(\'' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="stopPlugin(\'' +
|
||||
escHtml(name) +
|
||||
'\')" style="margin-right:4px">' +
|
||||
__("停止", "Stop") +
|
||||
"</button>";
|
||||
actionsHtml +=
|
||||
'<button class="btn btn-sm btn-ghost" onclick="disablePlugin(\'' +
|
||||
escHtml(name) +
|
||||
'\')" style="margin-right:4px">' +
|
||||
__("禁用", "Disable") +
|
||||
"</button>";
|
||||
} else {
|
||||
} else if (isDisabled) {
|
||||
actionsHtml +=
|
||||
'<button class="btn btn-sm btn-success" onclick="enablePlugin(\'' +
|
||||
escHtml(name) +
|
||||
'\')" style="margin-right:4px">' +
|
||||
__("启用", "Enable") +
|
||||
"</button>";
|
||||
} else {
|
||||
// 未运行但未禁用:一律走**运行态启动**,不碰持久禁用清单。
|
||||
actionsHtml +=
|
||||
'<button class="btn btn-sm btn-success" onclick="startPlugin(\'' +
|
||||
escHtml(name) +
|
||||
'\')" style="margin-right:4px">' +
|
||||
__("启动", "Start") +
|
||||
"</button>";
|
||||
}
|
||||
if (isExternal) {
|
||||
actionsHtml +=
|
||||
@ -6043,6 +6105,46 @@ async function disablePlugin(name) {
|
||||
}
|
||||
}
|
||||
|
||||
// startPlugin / stopPlugin 是**运行态**启停(对应 plugin_start / plugin_stop):
|
||||
// 只改现在跑不跑,不写持久禁用清单。与 enable/disable(持久态)分开,
|
||||
// 避免「点了一下启动,重启后行为却变了」。
|
||||
async function startPlugin(name) {
|
||||
try {
|
||||
await api("/plugins/" + encodeURIComponent(name) + "/start", {
|
||||
method: "POST",
|
||||
});
|
||||
toast(__("已启动: ", "Started: ") + name);
|
||||
state.kernel = await api("/kernel");
|
||||
await loadInstalledPlugins();
|
||||
renderPlugins();
|
||||
} catch (e) {
|
||||
toast(__("启动失败: ", "Start failed: ") + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopPlugin(name) {
|
||||
if (name === "webui") {
|
||||
var r = confirm(
|
||||
__(
|
||||
"停止 WebUI 插件后此管理面板会立即断开,只能通过 CLI 命令 /plugin start webui 恢复。\n\n确定要停止吗?",
|
||||
"Stopping the WebUI plugin disconnects this panel immediately. Recover with CLI /plugin start webui.\n\nStop it?",
|
||||
),
|
||||
);
|
||||
if (!r) return;
|
||||
}
|
||||
try {
|
||||
await api("/plugins/" + encodeURIComponent(name) + "/stop", {
|
||||
method: "POST",
|
||||
});
|
||||
toast(__("已停止: ", "Stopped: ") + name);
|
||||
state.kernel = await api("/kernel");
|
||||
await loadInstalledPlugins();
|
||||
renderPlugins();
|
||||
} catch (e) {
|
||||
toast(__("停止失败: ", "Stop failed: ") + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function enablePlugin(name) {
|
||||
try {
|
||||
var r = await api("/plugins/" + encodeURIComponent(name) + "/enable", {
|
||||
|
||||
@ -326,6 +326,40 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
|
||||
return
|
||||
|
||||
case "start":
|
||||
// 运行态启动:只拉起,不写持久禁用清单 —— 与「启用」区分。
|
||||
// 前端「按需待命」的插件点「启动」走这里,不该顺手改持久配置。
|
||||
if h.pluginMgr == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
if !validPluginName(name) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid plugin name"})
|
||||
return
|
||||
}
|
||||
if err := h.pluginMgr.StartPlugin(name); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
|
||||
return
|
||||
|
||||
case "stop":
|
||||
if h.pluginMgr == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
if !validPluginName(name) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid plugin name"})
|
||||
return
|
||||
}
|
||||
if err := h.pluginMgr.StopPlugin(name); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
|
||||
return
|
||||
|
||||
case "enable":
|
||||
if h.pluginMgr == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
|
||||
|
||||
72
internal/plugins/webui/plugin_status_render_test.go
Normal file
72
internal/plugins/webui/plugin_status_render_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 插件列表「启动状态」渲染的回归判据(2026-10-20 生产反馈)。
|
||||
//
|
||||
// ## 原始问题
|
||||
//
|
||||
// 面板只拿内核 status 的 loaded 一个布尔来判断,于是三种完全不同的形态
|
||||
// 被显示成同一个「未加载」:
|
||||
//
|
||||
// · auto_start=false 且未运行 → **按需插件正常待命**(不是故障)
|
||||
// · auto_start=true 且未运行 → 该起没起(需要关注)
|
||||
// · 运行态 plugin_stop 之后 → 用户自己停的(重启会恢复)
|
||||
//
|
||||
// 用户看到一屏「未加载」,以为插件全坏了 —— 实际它们只是在等 plugin_start。
|
||||
//
|
||||
// ## 判据思路
|
||||
//
|
||||
// 与 cmd_history_render_test.go 同款:不执行 JS,只守住源码结构 ——
|
||||
// 渲染函数必须消费 auto_start / runtime_stopped / alive 三个字段,
|
||||
// 并给出「按需待命」这一独立分支;否则回归立刻红。
|
||||
func TestPluginStatusUsesRuntimeFields(t *testing.T) {
|
||||
src := dashboardJSSource(t)
|
||||
start := strings.Index(src, "function renderPlugins()")
|
||||
if start < 0 {
|
||||
t.Fatal("找不到 renderPlugins —— 函数被重命名或删除,请同步更新本判据")
|
||||
}
|
||||
end := strings.Index(src[start+1:], "\nfunction ")
|
||||
var body string
|
||||
if end < 0 {
|
||||
body = src[start:]
|
||||
} else {
|
||||
body = src[start : start+1+end]
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
"auto_start", // 区分自启/按需
|
||||
"runtime_stopped", // 区分运行态停止与持久禁用
|
||||
"alive", // 区分「已加载」与「进程丢失」
|
||||
"按需待命", // 按需插件待命是独立状态,不能混进「未运行」
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("renderPlugins 未引用 %q —— 状态判定退化,"+
|
||||
"按需插件会被误显示成故障。见本文件头部说明", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 「启动」按钮必须走运行态接口 /start,不能复用持久态的 /enable ——
|
||||
// 后者会清掉持久禁用标记,让「点一下启动」变成「以后都自启」。
|
||||
func TestPluginStartUsesRuntimeEndpoint(t *testing.T) {
|
||||
src := dashboardJSSource(t)
|
||||
i := strings.Index(src, "async function startPlugin(")
|
||||
if i < 0 {
|
||||
t.Fatal("找不到 startPlugin")
|
||||
}
|
||||
body := src[i:]
|
||||
if j := strings.Index(body[1:], "\nasync function "); j > 0 {
|
||||
body = body[:j+1]
|
||||
}
|
||||
if !strings.Contains(body, `"/plugins/" + encodeURIComponent(name) + "/start"`) {
|
||||
t.Fatal("startPlugin 必须调用 /plugins/<name>/start(运行态)," +
|
||||
"不得复用 /enable(持久态会改动开机行为)")
|
||||
}
|
||||
if strings.Contains(body, "\"/enable\"") {
|
||||
t.Fatal("startPlugin 里出现了 /enable —— 运行态启动不得动持久禁用清单")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user