feat(agent): 新增通用 {{.Now}} 提示词变量(每轮实时渲染)

- 各 agent 渲染路径(chat/worker/mainagent/goals/planner)统一提供 Now(nowStr());
  自定义会话 agent 每次触发都会重新 renderSystem,故 {{.Now}} 每轮实时刷新
- server 端 globalPromptVars + withGlobalVars:在保存校验/变量列表/预览/详情放行
  {{.Now}},否则引用它的自定义提示词会被 validateTemplate 拒绝
- 无 DB 迁移(handler 层注入通用变量,不动 agent_prompt_vars 表)
- 用途:配合固定的开始时间戳相减,判断"已运行多久/是否进入最后N小时"
- 新增 agent/prompt_now_test.go:{{.Now}} 渲染实时时间、未知变量安全回退
This commit is contained in:
Autumn-27 committed 2026-08-11 10:45:02 -04:00
1 parent 7afff7e5f5
commit 7064216188
8 files changed
+86 -14

No files matched your search

+5 -4
View File
@@ -58,12 +58,13 @@ func chatWorkDirSpec(workDir string) string {
// chatSystem renders the DB-managed prompt body for agentKey. Custom agents have
// no per-key in-code default, so DefaultAssistantPrompt is the render fallback.
func chatSystem(agentKey, workDir string) string {
return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{}) + chatWorkDirSpec(workDir)
return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{Now: nowStr()}) + chatWorkDirSpec(workDir)
}
// chatVars carries no runtime variables today — a custom prompt referencing any
// {{.X}} simply fails to render and falls back to DefaultAssistantPrompt.
type chatVars struct{}
// chatVars carries the runtime variables a custom agent's prompt may reference.
// Now (server wall-clock, refreshed each turn) is the only one today; any other
// {{.X}} fails to render and falls back to DefaultAssistantPrompt.
type chatVars struct{ Now string }
// Chat runs ONE turn of a conversation with the agent identified by agentKey,
// resuming prior history keyed by sessionID. maxTurns is the per-turn agent step
+1 -1
View File
@@ -102,7 +102,7 @@ func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db
// Description rides in the user message (same channel as the goal), NOT via the
// {{.EngagementDescription}} template var — else a prompt that references the var
// would inject the description twice. System prompt stays pure static instructions.
sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{})
sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{Now: nowStr()})
tools := []actool.CoreTool{submitTool}
// Wire add_task_scope only when we have a real asset store + task to write to.
// The scope-extraction tail is appended in lockstep so the prompt never asks for
+1 -1
View File
@@ -55,7 +55,7 @@ const mainAgentDefaultTmpl = `你是一个授权渗透测试系统的"主 agent"
不要编造发现;只根据工具返回的真实数据回答。`
func mainAgentSystem(goal, workDir string) string {
body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal})
body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, Now: nowStr()})
return body + artifactSpec(workDir)
}
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/norma/agentcore"
@@ -160,7 +159,7 @@ const plannerDefaultTmpl = `你是一个授权渗透测试系统的"规划者"
宁可不生成,也不要重复或硬凑。简洁、克制、高效。`
func plannerSystem(goal, workDir string) string {
body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, Now: time.Now().Format("2006-01-02 15:04:05 MST")})
body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, Now: nowStr()})
return body + artifactSpec(workDir)
}
+9 -2
View File
@@ -3,6 +3,7 @@ package agent
import (
"bytes"
"text/template"
"time"
)
// PromptOverride, if set, returns the stored system-prompt template for an agent
@@ -15,10 +16,16 @@ var PromptOverride func(agentKey string) (string, bool)
// user template referencing a catalog variable renders; referencing anything else
// fails template execution and falls back to the built-in default.
type PlannerVars struct{ Goal, Scope, AssetSummary, Now string }
type WorkerVars struct{ ProxyAddr, WorkerName string }
type MainVars struct{ Goal, AssetSummary, FindingsSummary string }
type WorkerVars struct{ ProxyAddr, WorkerName, Now string }
type MainVars struct{ Goal, AssetSummary, FindingsSummary, Now string }
type GoalsVars struct{ EngagementDescription, Now string }
// nowStr is the server-local wall-clock string exposed as the universal {{.Now}}
// prompt variable. renderSystem runs on every agent turn/round, so this is fresh
// each run — a prompt can subtract it from a fixed start stamp to reason about
// elapsed time (e.g. a timed benchmark's "last N hours" window).
func nowStr() string { return time.Now().Format("2006-01-02 15:04:05 MST") }
// renderSystem returns the rendered system-prompt BODY (段 [A]) for agentKey.
// Precedence: the DB-stored template (if any) over the built-in default template
// (def). BOTH are Go templates now — the built-in default is seeded into the DB
+50
View File
@@ -0,0 +1,50 @@
package agent
import (
"strings"
"testing"
"time"
)
// TestChatNowVarRenders verifies the universal {{.Now}} runtime variable: a custom
// (chat) agent prompt referencing it renders the live server time each turn, rather
// than failing template execution and falling back to DefaultAssistantPrompt.
func TestChatNowVarRenders(t *testing.T) {
prev := PromptOverride
defer func() { PromptOverride = prev }()
PromptOverride = func(key string) (string, bool) {
if key == "tec_benchmark" {
return "当前时间:{{.Now}}", true
}
return "", false
}
out := chatSystem("tec_benchmark", "/tmp/x")
if !strings.Contains(out, "当前时间:") {
t.Fatalf("custom prompt body missing, likely fell back to default: %q", out)
}
year := time.Now().Format("2006")
if !strings.Contains(out, year) {
t.Fatalf("{{.Now}} did not render the live time (want year %s): %q", year, out)
}
}
// TestChatUnknownVarFallsBack verifies an out-of-catalog {{.X}} still degrades
// safely to the default assistant prompt (never a half-rendered prompt).
func TestChatUnknownVarFallsBack(t *testing.T) {
prev := PromptOverride
defer func() { PromptOverride = prev }()
PromptOverride = func(key string) (string, bool) {
return "引用了不存在的变量:{{.Bogus}}", true
}
out := chatSystem("whatever", "/tmp/x")
if strings.Contains(out, "引用了不存在的变量") {
t.Fatalf("broken template should have fallen back, got custom body: %q", out)
}
if !strings.Contains(out, DefaultAssistantPrompt) {
t.Fatalf("expected fallback to DefaultAssistantPrompt, got: %q", out)
}
}
+1 -1
View File
@@ -184,7 +184,7 @@ func ensureRunDir(base string, taskID, intentID int64) string {
func cmdOutDir(dir string) string { return filepath.Join(dir, "cmd-output") }
func workerSystem(proxyAddr, runDir string) string {
body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr})
body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, Now: nowStr()})
return body + workerTrafficBlock(proxyAddr) + workerArtifactSpec(runDir)
}