mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
fix: 自动标题取消语义、清洗循环与拒绝占位回写
spawn_task 标题 20s 超时且父 ctx 取消不建任务;清洗循环剥前缀内外引号; 异步标题拒绝「新对话」并用条件更新防手改竞态;会话 profile 失败回退全局。
This commit is contained in:
1 parent
6c781f21f4
commit
3d3ee8827e
6 files changed
+81
-27
No files matched your search
+10
-9
@@ -51,22 +51,23 @@ func cleanAutoTitle(raw string) string {
|
||||
if i := strings.IndexAny(title, "\r\n"); i >= 0 {
|
||||
title = strings.TrimSpace(title[:i])
|
||||
}
|
||||
for len(title) >= 2 {
|
||||
for {
|
||||
previous := title
|
||||
if len(title) >= 2 {
|
||||
first, firstSize := utf8.DecodeRuneInString(title)
|
||||
last, lastSize := utf8.DecodeLastRuneInString(title)
|
||||
if (first == '"' && last == '"') || (first == '\'' && last == '\'') || (first == '“' && last == '”') || (first == '「' && last == '」') {
|
||||
title = strings.TrimSpace(title[firstSize : len(title)-lastSize])
|
||||
continue
|
||||
title = title[firstSize : len(title)-lastSize]
|
||||
}
|
||||
break
|
||||
}
|
||||
lines := strings.FieldsFunc(title, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||
if len(lines) > 0 {
|
||||
title = strings.TrimSpace(lines[0])
|
||||
}
|
||||
for _, prefix := range []string{"标题:", "标题:"} {
|
||||
if strings.HasPrefix(title, prefix) {
|
||||
title = strings.TrimSpace(strings.TrimPrefix(title, prefix))
|
||||
title = strings.TrimPrefix(title, prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
title = strings.TrimSpace(title)
|
||||
if title == previous {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,15 @@ func TestGenerateAutoTitleCleansResponse(t *testing.T) {
|
||||
if p.req.Thinking != "disabled" || p.req.MaxTokens != 96 || len(p.req.Tools) != 0 {
|
||||
t.Fatalf("request options: %+v", p.req)
|
||||
}
|
||||
|
||||
quoted := &autoTitleProvider{reply: `标题:"整理项目设置"`}
|
||||
got, err = GenerateAutoTitle(t.Context(), quoted, "整理项目设置")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "整理项目设置" {
|
||||
t.Fatalf("nested quote/prefix cleanup: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAutoTitleProviderError(t *testing.T) {
|
||||
|
||||
@@ -197,6 +197,15 @@ func (d *DB) RenameConversation(id int64, title string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RenameConversationIfTitle updates a thread only while its title is still an
|
||||
// initial placeholder or preview. This keeps asynchronous auto-title writes
|
||||
// from overwriting a concurrent manual rename.
|
||||
func (d *DB) RenameConversationIfTitle(id int64, title, initial string) error {
|
||||
_, err := d.Exec(`UPDATE conversations SET title=$2
|
||||
WHERE id=$1 AND title IN ('', '新对话', $3)`, id, title, initial)
|
||||
return err
|
||||
}
|
||||
|
||||
// TouchConversation bumps updated_at so the thread floats to the top of the list.
|
||||
func (d *DB) TouchConversation(id int64) error {
|
||||
_, err := d.Exec(`UPDATE conversations SET updated_at=now() WHERE id=$1`, id)
|
||||
|
||||
@@ -12,3 +12,14 @@ func TestAutoTitleMayReplace(t *testing.T) {
|
||||
t.Fatal("manual title must not be overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoTitleIsUsable(t *testing.T) {
|
||||
for _, generated := range []string{"", "新对话"} {
|
||||
if autoTitleIsUsable(generated) {
|
||||
t.Fatalf("placeholder title %q must be rejected", generated)
|
||||
}
|
||||
}
|
||||
if !autoTitleIsUsable("整理项目设置") {
|
||||
t.Fatal("generated title should be accepted")
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/intercept"
|
||||
"github.com/Autumn-27/artex/sidequestion"
|
||||
"github.com/Autumn-27/norma/llm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -469,34 +470,35 @@ func (s *Server) pgSendConversationMessage(w http.ResponseWriter, r *http.Reques
|
||||
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 {
|
||||
var prov llm.Provider
|
||||
providerOK := false
|
||||
if profileID != nil {
|
||||
prov, _, providerOK = s.providerForProfile(*profileID)
|
||||
}
|
||||
if !providerOK {
|
||||
if active, activeErr := s.m.pg.ActiveProfile(); activeErr == nil && active != nil {
|
||||
profileID = &active.ID
|
||||
prov, _, providerOK = s.providerForProfile(active.ID)
|
||||
} else if activeErr != nil {
|
||||
log.Printf("[auto-title] conversation %d active profile lookup failed: %v", conversationID, activeErr)
|
||||
}
|
||||
}
|
||||
if profileID == nil {
|
||||
if profileID == nil || !providerOK || prov == 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 {
|
||||
if !autoTitleIsUsable(generated) {
|
||||
log.Printf("[auto-title] conversation %d: generated title is empty or placeholder", conversationID)
|
||||
return
|
||||
}
|
||||
if autoTitleMayReplace(current.Title, title) {
|
||||
if err := s.m.pg.RenameConversation(conversationID, generated); err != nil {
|
||||
if err := s.m.pg.RenameConversationIfTitle(conversationID, generated, title); err != nil {
|
||||
log.Printf("[auto-title] conversation %d rename failed: %v", conversationID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -517,6 +519,10 @@ func autoTitleMayReplace(current, initial string) bool {
|
||||
return current == "" || current == "新对话" || current == initial
|
||||
}
|
||||
|
||||
func autoTitleIsUsable(generated string) bool {
|
||||
return generated != "" && generated != "新对话"
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
+23
-5
@@ -306,18 +306,36 @@ func (s *Server) toolSpawnTask() actool.CoreTool {
|
||||
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 {
|
||||
autoTitleEnabled := s.m.pg.GetBool(db.SettingTaskAutoTitle, false)
|
||||
if autoTitleEnabled {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
active, activeErr := s.m.pg.ActiveProfile()
|
||||
if activeErr != nil {
|
||||
log.Printf("[auto-title] active profile lookup failed: %v", activeErr)
|
||||
} else if active == nil {
|
||||
log.Printf("[auto-title] no active profile")
|
||||
} else if prov, _, providerOK := s.providerForProfile(active.ID); providerOK {
|
||||
titleCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
generated, titleErr := agent.GenerateAutoTitle(titleCtx, prov, a.Description+"\n"+a.Goal)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
if titleErr == nil {
|
||||
name = generated
|
||||
} else {
|
||||
log.Printf("[auto-title] task %s: %v", a.Description, titleErr)
|
||||
log.Printf("[auto-title] task title generation failed: %v", titleErr)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[auto-title] task provider unavailable for active profile %d", active.ID)
|
||||
}
|
||||
}
|
||||
if autoTitleEnabled {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
}
|
||||
t, err := s.m.CreateTaskWithOptions(a.Description, a.Goal, db.TaskCreateOptions{
|
||||
Name: name,
|
||||
|
||||
Reference in new issue
Block a user