fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持

- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对
- agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式
- agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch
- webui: server 输出通道适配器(保留 reasoning_content/disable_thinking)
- GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
This commit is contained in:
JianFeeeee
2026-08-14 00:48:40 +08:00
parent 816597caac
commit 147d0baaf9
43 changed files with 4670 additions and 1478 deletions

View File

@ -193,11 +193,15 @@ func (d *Distiller) distillLoop() {
func (d *Distiller) distillOnce() {
d.mu.Lock()
cutoff := time.Now().AddDate(0, 0, -d.cfg.RetentionDays)
batchSize := d.cfg.BatchSize
if batchSize <= 0 {
batchSize = 50
}
// 每 tick 取前 N 条未蒸馏记录(无 RetentionDays 门槛),蒸馏成功才标记/移除
var toDistill []RawRecord
var remaining []RawRecord
for _, r := range d.records {
if r.CreatedAt.Before(cutoff) && !r.Distilled {
if !r.Distilled && len(toDistill) < batchSize {
toDistill = append(toDistill, r)
} else {
remaining = append(remaining, r)
@ -210,22 +214,29 @@ func (d *Distiller) distillOnce() {
return
}
batchSize := d.cfg.BatchSize
if batchSize <= 0 {
batchSize = 50
}
distilled := 0
for i := 0; i < len(toDistill); i += batchSize {
end := i + batchSize
if end > len(toDistill) {
end = len(toDistill)
}
d.distillBatch(toDistill[i:end])
if d.distillBatch(toDistill[i:end]) {
distilled += end - i
} else {
// 蒸馏失败:记录写回待处理队列,下次 tick 重试
d.mu.Lock()
d.records = append(toDistill[i:end], d.records...)
d.mu.Unlock()
}
}
d.cleanupRawFiles()
log.Printf("[memory] distilled %d records", len(toDistill))
if distilled > 0 {
log.Printf("[memory] distilled %d records", distilled)
}
}
func (d *Distiller) distillBatch(batch []RawRecord) {
// distillBatch 蒸馏一批记录,全部成功返回 true任一失败返回 false调用方重试
func (d *Distiller) distillBatch(batch []RawRecord) bool {
var userContent, assistantContent string
sessionIDs := make(map[string]bool)
for _, r := range batch {
@ -245,8 +256,10 @@ func (d *Distiller) distillBatch(batch []RawRecord) {
}
if _, _, err := d.db.Commit(triples, sessionID, 0); err != nil {
log.Printf("[memory] distill commit: %v", err)
return false
}
}
return true
}
func (d *Distiller) cleanupRawFiles() {

View File

@ -1,6 +1,7 @@
package pipeline
import (
"fmt"
"os"
"path/filepath"
"testing"
@ -117,6 +118,71 @@ func TestDistillOnce(t *testing.T) {
}
}
// Phase 4: 新记录无需等待 RetentionDays下一 tick 立即蒸馏(文档所述 10min 频率)
func TestDistillOnceFreshRecords(t *testing.T) {
db, err := memory.NewGraphDB(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
dir := t.TempDir()
d := NewDistiller(db, dir, DistillerConfig{
Interval: 10 * time.Minute,
RetentionDays: 7,
BatchSize: 50,
})
d.Append("sess1", "user", "我的名字是李四")
d.Append("sess1", "assistant", "你好李四!")
if len(d.records) != 2 {
t.Fatalf("expected 2 fresh records, got %d", len(d.records))
}
d.distillOnce()
if len(d.records) != 0 {
t.Errorf("fresh records should be distilled on next tick (no retention gate), got %d remaining", len(d.records))
}
// 二次蒸馏不重复(已蒸馏记录已被移除)
d.distillOnce()
if len(d.records) != 0 {
t.Errorf("second distill should be no-op, got %d records", len(d.records))
}
}
// Phase 4: BatchSize 限制每 tick 处理前 N 条,未蒸馏记录留待下个 tick
func TestDistillOnceBatchLimit(t *testing.T) {
db, err := memory.NewGraphDB(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
dir := t.TempDir()
d := NewDistiller(db, dir, DistillerConfig{
Interval: 10 * time.Minute,
RetentionDays: 7,
BatchSize: 3,
})
for i := 0; i < 10; i++ {
d.Append("sess1", "user", fmt.Sprintf("第 %d 条消息内容", i))
}
d.distillOnce()
if len(d.records) != 7 {
t.Fatalf("expected 7 records remaining after batch 3, got %d", len(d.records))
}
// 后续 tick 继续消化,最终全部蒸馏
for i := 0; i < 5 && len(d.records) > 0; i++ {
d.distillOnce()
}
if len(d.records) != 0 {
t.Errorf("all records should be distilled after several ticks, got %d remaining", len(d.records))
}
}
func TestExtractKeyTriples(t *testing.T) {
tests := []struct {
user string