mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 17:13:50 +08:00
feat: Agent 任务名与对话标题双能力自动生成
新增 task_auto_title_enabled / chat_auto_title_enabled(默认关)、 共享 GenerateAutoTitle、spawn_task 同步写 name、对话首条先截断再异步 AI, 设置页「自动标题」双 Switch。closes #55
This commit is contained in:
1 parent
e740334b90
commit
6c781f21f4
11 files changed
+310
-1
No files matched your search
@@ -0,0 +1,14 @@
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAutoTitleMayReplace(t *testing.T) {
|
||||
for _, current := range []string{"", "新对话", "首条消息预览"} {
|
||||
if !autoTitleMayReplace(current, "首条消息预览") {
|
||||
t.Fatalf("expected %q to be replaceable", current)
|
||||
}
|
||||
}
|
||||
if autoTitleMayReplace("用户手动改的标题", "首条消息预览") {
|
||||
t.Fatal("manual title must not be overwritten")
|
||||
}
|
||||
}
|
||||
@@ -446,6 +446,59 @@ func (s *Server) pgSendConversationMessage(w http.ResponseWriter, r *http.Reques
|
||||
title = "附件消息"
|
||||
}
|
||||
_ = pg.RenameConversation(c.ID, title)
|
||||
if s.m.pg.GetBool(db.SettingChatAutoTitle, false) {
|
||||
conversationID := c.ID
|
||||
agentKey := c.AgentKey
|
||||
pinnedProfile := c.LLMProfileID
|
||||
promptContent := msg
|
||||
if len(req.Attachments) > 0 {
|
||||
var names []string
|
||||
for _, attachment := range req.Attachments {
|
||||
if name := strings.TrimSpace(attachment.Name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
if len(names) > 0 {
|
||||
if promptContent != "" {
|
||||
promptContent += "\n"
|
||||
}
|
||||
promptContent += "附件:" + strings.Join(names, "、")
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
profileID := s.effectiveProfileForAgent(agentKey, pinnedProfile)
|
||||
if profileID == nil {
|
||||
if active, err := s.m.pg.ActiveProfile(); err == nil && active != nil {
|
||||
profileID = &active.ID
|
||||
}
|
||||
}
|
||||
if profileID == nil {
|
||||
log.Printf("[auto-title] conversation %d: no effective profile", conversationID)
|
||||
return
|
||||
}
|
||||
prov, _, ok := s.providerForProfile(*profileID)
|
||||
if !ok {
|
||||
log.Printf("[auto-title] conversation %d: provider unavailable for profile %d", conversationID, *profileID)
|
||||
return
|
||||
}
|
||||
generated, err := agent.GenerateAutoTitle(ctx, prov, promptContent)
|
||||
if err != nil {
|
||||
log.Printf("[auto-title] conversation %d: %v", conversationID, err)
|
||||
return
|
||||
}
|
||||
current, err := s.m.pg.GetConversation(conversationID)
|
||||
if err != nil || current == nil {
|
||||
return
|
||||
}
|
||||
if autoTitleMayReplace(current.Title, title) {
|
||||
if err := s.m.pg.RenameConversation(conversationID, generated); err != nil {
|
||||
log.Printf("[auto-title] conversation %d rename failed: %v", conversationID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
_ = pg.TouchConversation(c.ID)
|
||||
|
||||
@@ -458,6 +511,12 @@ func (s *Server) pgSendConversationMessage(w http.ResponseWriter, r *http.Reques
|
||||
writeJSON(w, 202, map[string]any{"status": "accepted"})
|
||||
}
|
||||
|
||||
// autoTitleMayReplace protects a user's later rename while allowing the
|
||||
// asynchronous title completion to replace the initial placeholder/preview.
|
||||
func autoTitleMayReplace(current, initial string) bool {
|
||||
return current == "" || current == "新对话" || current == initial
|
||||
}
|
||||
|
||||
// runConversation runs ONE agent turn on a conversation in a background goroutine
|
||||
// (each conversation is independent → parallel). Steps stream to
|
||||
// conversation_activities. Shared by the chat HTTP handler and the P3 scheduler.
|
||||
|
||||
@@ -18,6 +18,17 @@ func TestIntelligenceHTTPSettingsAndScope(t *testing.T) {
|
||||
if s.settingsPayload()[db.SettingTaskIntelligence] != false {
|
||||
t.Fatal("default must be off")
|
||||
}
|
||||
if s.settingsPayload()[db.SettingTaskAutoTitle] != false || s.settingsPayload()[db.SettingChatAutoTitle] != false {
|
||||
t.Fatal("auto title settings must default to off")
|
||||
}
|
||||
if r := request("PUT", "/api/settings", `{"task_auto_title_enabled":true,"chat_auto_title_enabled":true}`); r.Code != 200 {
|
||||
t.Fatal(r.Code, r.Body)
|
||||
}
|
||||
if !s.m.pg.GetBool(db.SettingTaskAutoTitle, false) || !s.m.pg.GetBool(db.SettingChatAutoTitle, false) {
|
||||
t.Fatal("auto title settings were not persisted")
|
||||
}
|
||||
defer s.m.pg.SetBool(db.SettingTaskAutoTitle, false)
|
||||
defer s.m.pg.SetBool(db.SettingChatAutoTitle, false)
|
||||
if r := request("PUT", "/api/settings", `{"task_intelligence_enabled":true}`); r.Code != 200 {
|
||||
t.Fatal(r.Code, r.Body)
|
||||
}
|
||||
|
||||
+16
-1
@@ -251,7 +251,7 @@ func (s *Server) toolSpawnTask() actool.CoreTool {
|
||||
"plan_heartbeat_seconds": map[string]any{"type": "integer", "description": "可选:planner 心跳触发间隔(秒)。距上轮规划结束/任务开始满该值且期间无触发 → 触发一轮规划(兜底死锁 + 唤醒去监督飞行中的 worker)。留空或 0 = 默认 600(10min);"},
|
||||
"seed_first_intent": map[string]any{"type": "boolean", "description": "可选:对于简单任务可开启,创建时直接下发一条种子意图(内容=描述+目标)让 worker 免等首轮 planner 直接开跑测试;默认 false(走标准先规划再执行)。"},
|
||||
}, "description", "goal"),
|
||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
var a struct {
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
@@ -305,7 +305,22 @@ func (s *Server) toolSpawnTask() actool.CoreTool {
|
||||
if pin != nil {
|
||||
llmIDs = []int64{*pin}
|
||||
}
|
||||
name := ""
|
||||
if s.m.pg.GetBool(db.SettingTaskAutoTitle, false) {
|
||||
if active, activeErr := s.m.pg.ActiveProfile(); activeErr == nil && active != nil {
|
||||
if prov, _, providerOK := s.providerForProfile(active.ID); providerOK {
|
||||
if generated, titleErr := agent.GenerateAutoTitle(ctx, prov, a.Description+"\n"+a.Goal); titleErr == nil {
|
||||
name = generated
|
||||
} else {
|
||||
log.Printf("[auto-title] task %s: %v", a.Description, titleErr)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[auto-title] task provider unavailable for active profile %d", active.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
t, err := s.m.CreateTaskWithOptions(a.Description, a.Goal, db.TaskCreateOptions{
|
||||
Name: name,
|
||||
SourceTaskIDs: sourceIDs,
|
||||
LLMProfileIDs: llmIDs,
|
||||
TimeoutSeconds: a.TimeoutSeconds,
|
||||
|
||||
@@ -3443,6 +3443,8 @@ func (s *Server) settingsPayload() map[string]any {
|
||||
"traffic_capture": s.m.TrafficEnabled(),
|
||||
"agent_traffic_binding": s.m.pg.GetBool(settingAgentTrafficBinding, false),
|
||||
"task_intelligence_enabled": s.m.pg.GetBool(db.SettingTaskIntelligence, false),
|
||||
db.SettingTaskAutoTitle: s.m.pg.GetBool(db.SettingTaskAutoTitle, false),
|
||||
db.SettingChatAutoTitle: s.m.pg.GetBool(db.SettingChatAutoTitle, false),
|
||||
db.SettingCredentialAgent: s.m.pg.GetBool(db.SettingCredentialAgent, false),
|
||||
db.SettingCredentialTraffic: s.m.pg.GetBool(db.SettingCredentialAgent, false) && s.m.pg.GetBool(db.SettingCredentialTraffic, false),
|
||||
"llm_record": s.m.LLMRecordEnabled(),
|
||||
@@ -3517,6 +3519,8 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) {
|
||||
TrafficCapture *bool `json:"traffic_capture"`
|
||||
AgentTrafficBinding *bool `json:"agent_traffic_binding"`
|
||||
TaskIntelligence *bool `json:"task_intelligence_enabled"`
|
||||
TaskAutoTitle *bool `json:"task_auto_title_enabled"`
|
||||
ChatAutoTitle *bool `json:"chat_auto_title_enabled"`
|
||||
CredentialAgent *bool `json:"credential_agent_enabled"`
|
||||
CredentialTraffic *bool `json:"credential_traffic_binding"`
|
||||
LLMRecord *bool `json:"llm_record"` // LLM 录制开关(默认关);即时生效,无需重建 agent
|
||||
@@ -3689,6 +3693,18 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.TaskAutoTitle != nil {
|
||||
if err := s.m.pg.SetBool(db.SettingTaskAutoTitle, *req.TaskAutoTitle); err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.ChatAutoTitle != nil {
|
||||
if err := s.m.pg.SetBool(db.SettingChatAutoTitle, *req.ChatAutoTitle); err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.AgentTrafficBinding != nil {
|
||||
if err := s.m.pg.SetBool(settingAgentTrafficBinding, *req.AgentTrafficBinding); err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
|
||||
Reference in new issue
Block a user