refactor: чистка мёртвого кода, лимит ходов D3, HTML-экранирование и UTF-8 обрезка в Telegram
This commit is contained in:
@@ -66,7 +66,6 @@ func (c *Core) ProcessTurn(ctx context.Context, taskID int64, text string) (Resu
|
||||
if task.Status == storage.StatusApproved {
|
||||
return Result{
|
||||
Reply: "Задача уже одобрена и передана на выполнение. Следите за статусом: /status " + itoa(task.ID),
|
||||
Action: "send",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -106,7 +105,6 @@ func (c *Core) handleCommand(ctx context.Context, taskID int64, text string) (Re
|
||||
default:
|
||||
return Result{
|
||||
Reply: "Неизвестная команда. Доступно: /start /cancel /skip /retry N /status N",
|
||||
Action: "send",
|
||||
TaskID: taskID,
|
||||
}, nil
|
||||
}
|
||||
@@ -128,7 +126,6 @@ func (c *Core) handleStart(ctx context.Context, taskID int64) (Result, error) {
|
||||
}
|
||||
return Result{
|
||||
Reply: greeting,
|
||||
Action: "greeting",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -146,7 +143,6 @@ func (c *Core) handleCancel(ctx context.Context, taskID int64) (Result, error) {
|
||||
}
|
||||
return Result{
|
||||
Reply: "🚫 Отменил.",
|
||||
Action: "drop",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -161,7 +157,6 @@ func (c *Core) handleSkip(ctx context.Context, taskID int64) (Result, error) {
|
||||
if task.Status == storage.StatusReady {
|
||||
return Result{
|
||||
Reply: "Напишите «создавай» — или правьте текст.",
|
||||
Action: "send",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -191,8 +186,7 @@ func (c *Core) handleRetry(ctx context.Context, rest string) (Result, error) {
|
||||
id, ok := parseTaskID(rest)
|
||||
if !ok {
|
||||
return Result{
|
||||
Reply: "Укажите номер задачи: `/retry 5`.",
|
||||
Action: "send",
|
||||
Reply: "Укажите номер задачи: `/retry 5`.",
|
||||
}, nil
|
||||
}
|
||||
task, err := c.Store.GetTask(ctx, id)
|
||||
@@ -204,7 +198,6 @@ func (c *Core) handleRetry(ctx context.Context, rest string) (Result, error) {
|
||||
if storage.IsTerminal(task.Status) {
|
||||
return Result{
|
||||
Reply: "Задачу #" + itoa(id) + " нельзя перезапустить — она завершена (" + string(task.Status) + "). Создайте новую через /start.",
|
||||
Action: "send",
|
||||
TaskID: id,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -218,7 +211,6 @@ func (c *Core) handleRetry(ctx context.Context, rest string) (Result, error) {
|
||||
}
|
||||
return Result{
|
||||
Reply: "Задача перезапущена. Опишите, что меняем:",
|
||||
Action: "send",
|
||||
TaskID: id,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -229,8 +221,7 @@ func (c *Core) handleStatus(ctx context.Context, rest string) (Result, error) {
|
||||
id, ok := parseTaskID(rest)
|
||||
if !ok {
|
||||
return Result{
|
||||
Reply: "Укажите номер задачи: `/status 5`.",
|
||||
Action: "send",
|
||||
Reply: "Укажите номер задачи: `/status 5`.",
|
||||
}, nil
|
||||
}
|
||||
task, err := c.Store.GetTask(ctx, id)
|
||||
@@ -246,7 +237,6 @@ func (c *Core) handleStatus(ctx context.Context, rest string) (Result, error) {
|
||||
}
|
||||
return Result{
|
||||
Reply: reply,
|
||||
Action: "send",
|
||||
TaskID: id,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -257,8 +247,7 @@ func (c *Core) handleContinue(ctx context.Context, rest string) (Result, error)
|
||||
id, ok := parseTaskID(rest)
|
||||
if !ok {
|
||||
return Result{
|
||||
Reply: "Укажите номер задачи: `/continue 5`.",
|
||||
Action: "send",
|
||||
Reply: "Укажите номер задачи: `/continue 5`.",
|
||||
}, nil
|
||||
}
|
||||
task, err := c.Store.GetTask(ctx, id)
|
||||
@@ -268,7 +257,6 @@ func (c *Core) handleContinue(ctx context.Context, rest string) (Result, error)
|
||||
return Result{
|
||||
Reply: "Задача #" + itoa(id) + " в статусе " + string(task.Status) +
|
||||
". Резюм сессии — пока не реализован.",
|
||||
Action: "send",
|
||||
TaskID: id,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -277,8 +265,7 @@ func (c *Core) handleContinue(ctx context.Context, rest string) (Result, error)
|
||||
// notFoundReply формирует reply для ненайденной задачи.
|
||||
func (c *Core) notFoundReply(ctx context.Context, id int64, err error) (Result, error) {
|
||||
return Result{
|
||||
Reply: "Задача #" + itoa(id) + " не найдена.",
|
||||
Action: "send",
|
||||
Reply: "Задача #" + itoa(id) + " не найдена.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -291,7 +278,6 @@ func (c *Core) handleConsent(ctx context.Context, task *storage.Task) (Result, e
|
||||
}
|
||||
return Result{
|
||||
Reply: "✅ Задача #" + itoa(task.ID) + " одобрена. Запускаю выполнение.",
|
||||
Action: "created:" + itoa(task.ID),
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -333,6 +319,23 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
if !force && c.MaxTurns > 0 {
|
||||
userTurns := 0
|
||||
for _, h := range history {
|
||||
if h.Role == "user" {
|
||||
userTurns++
|
||||
}
|
||||
}
|
||||
if userTurns > c.MaxTurns {
|
||||
return Result{
|
||||
Reply: "Превышен лимит ходов сбора (" + itoa(int64(c.MaxTurns)) + "). Используйте /skip чтобы сформулировать черновик, или /start для новой задачи.",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
msgs := make([]Message, 0, len(history))
|
||||
for _, h := range history {
|
||||
msgs = append(msgs, Message{Role: h.Role, Content: h.Content})
|
||||
@@ -353,7 +356,7 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R
|
||||
if reply == "" {
|
||||
reply = "Недостаточно данных. Начните заново (/start)."
|
||||
}
|
||||
return Result{Reply: reply, Action: "drop", TaskID: task.ID, Status: task.Status}, nil
|
||||
return Result{Reply: reply, TaskID: task.ID, Status: task.Status}, nil
|
||||
|
||||
case "propose", "ready":
|
||||
// применяем черновик (для ready — текущий, без изменений)
|
||||
@@ -365,7 +368,7 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R
|
||||
return Result{}, err
|
||||
}
|
||||
reply := decChatReply(decision, "Укажи, в каком репозитории(ях) вести работу.")
|
||||
return Result{Reply: reply, Action: "send", TaskID: task.ID, Status: task.Status}, nil
|
||||
return Result{Reply: reply, TaskID: task.ID, Status: task.Status}, nil
|
||||
}
|
||||
task.Status = storage.StatusReady
|
||||
if err := c.Store.UpdateTask(ctx, task); err != nil {
|
||||
@@ -373,7 +376,6 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R
|
||||
}
|
||||
return Result{
|
||||
Reply: formatSummary(*task),
|
||||
Action: "summary",
|
||||
TaskID: task.ID,
|
||||
Status: task.Status,
|
||||
}, nil
|
||||
@@ -385,7 +387,7 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R
|
||||
return Result{}, err
|
||||
}
|
||||
reply := buildAskReply(decision, c.MaxQuestionsPerTurn)
|
||||
return Result{Reply: reply, Action: "send", TaskID: task.ID, Status: task.Status}, nil
|
||||
return Result{Reply: reply, TaskID: task.ID, Status: task.Status}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kamelion/ratatoskr-go/internal/storage"
|
||||
@@ -44,13 +45,11 @@ func TestStartCreatesCollecting(t *testing.T) {
|
||||
c, ctx, store := setupCore(t, nil)
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
res, err := c.ProcessTurn(ctx, id, "/start")
|
||||
_, err := c.ProcessTurn(ctx, id, "/start")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn /start: %v", err)
|
||||
}
|
||||
if res.Action != "greeting" {
|
||||
t.Fatalf("action = %q, want greeting", res.Action)
|
||||
}
|
||||
|
||||
task, _ := store.GetTask(ctx, id)
|
||||
if task.Status != storage.StatusCollecting {
|
||||
t.Fatalf("status = %s, want collecting", task.Status)
|
||||
@@ -61,13 +60,11 @@ func TestCancelSetsCancelled(t *testing.T) {
|
||||
c, ctx, store := setupCore(t, nil)
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
res, err := c.ProcessTurn(ctx, id, "/cancel")
|
||||
_, err := c.ProcessTurn(ctx, id, "/cancel")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn /cancel: %v", err)
|
||||
}
|
||||
if res.Action != "drop" {
|
||||
t.Fatalf("action = %q, want drop", res.Action)
|
||||
}
|
||||
|
||||
task, _ := store.GetTask(ctx, id)
|
||||
if task.Status != storage.StatusCancelled {
|
||||
t.Fatalf("status = %s, want cancelled", task.Status)
|
||||
@@ -84,13 +81,11 @@ func TestSingleTurnPropose(t *testing.T) {
|
||||
})
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
res, err := c.ProcessTurn(ctx, id, "Сделай калькулятор")
|
||||
_, err := c.ProcessTurn(ctx, id, "Сделай калькулятор")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn: %v", err)
|
||||
}
|
||||
if res.Action != "summary" {
|
||||
t.Fatalf("action = %q, want summary", res.Action)
|
||||
}
|
||||
|
||||
task, _ := store.GetTask(ctx, id)
|
||||
if task.Status != storage.StatusReady {
|
||||
t.Fatalf("status = %s, want ready", task.Status)
|
||||
@@ -114,9 +109,7 @@ func TestAskReturnsQuestions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn: %v", err)
|
||||
}
|
||||
if res.Action != "send" {
|
||||
t.Fatalf("action = %q, want send", res.Action)
|
||||
}
|
||||
|
||||
if res.Reply != "Уточню\n1. Какой язык?\n2. Какой срок?" {
|
||||
t.Fatalf("reply = %q", res.Reply)
|
||||
}
|
||||
@@ -133,13 +126,11 @@ func TestAbortReturnsDrop(t *testing.T) {
|
||||
})
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
res, err := c.ProcessTurn(ctx, id, "привет")
|
||||
_, err := c.ProcessTurn(ctx, id, "привет")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn: %v", err)
|
||||
}
|
||||
if res.Action != "drop" {
|
||||
t.Fatalf("action = %q, want drop", res.Action)
|
||||
}
|
||||
|
||||
task, _ := store.GetTask(ctx, id)
|
||||
if task.Status != storage.StatusAborted {
|
||||
t.Fatalf("status = %s, want aborted", task.Status)
|
||||
@@ -153,13 +144,11 @@ func TestConsentInReady(t *testing.T) {
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
_, _ = c.ProcessTurn(ctx, id, "сделай задачу")
|
||||
res, err := c.ProcessTurn(ctx, id, "создавай")
|
||||
_, err := c.ProcessTurn(ctx, id, "создавай")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn создавай: %v", err)
|
||||
}
|
||||
if res.Action != "created:"+itoa(id) {
|
||||
t.Fatalf("action = %q, want created:%d", res.Action, id)
|
||||
}
|
||||
|
||||
task, _ := store.GetTask(ctx, id)
|
||||
if task.Status != storage.StatusApproved {
|
||||
t.Fatalf("status после создавай = %s, want approved", task.Status)
|
||||
@@ -176,13 +165,11 @@ func TestEditInReadyGoesCollecting(t *testing.T) {
|
||||
|
||||
_, _ = c.ProcessTurn(ctx, id, "сделай X")
|
||||
// в ready пишем правку, не согласие
|
||||
res, err := c.ProcessTurn(ctx, id, "нет, лучше Y")
|
||||
_, err := c.ProcessTurn(ctx, id, "нет, лучше Y")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn edit: %v", err)
|
||||
}
|
||||
if res.Action != "summary" {
|
||||
t.Fatalf("action = %q, want summary", res.Action)
|
||||
}
|
||||
|
||||
if calls != 2 {
|
||||
t.Fatalf("decide calls = %d, want 2", calls)
|
||||
}
|
||||
@@ -194,25 +181,59 @@ func TestEditInReadyGoesCollecting(t *testing.T) {
|
||||
|
||||
func TestRetryNotFound(t *testing.T) {
|
||||
c, ctx, _ := setupCore(t, nil)
|
||||
res, err := c.ProcessTurn(ctx, 999, "/retry 999")
|
||||
_, err := c.ProcessTurn(ctx, 999, "/retry 999")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn /retry: %v", err)
|
||||
}
|
||||
if res.Action != "send" {
|
||||
t.Fatalf("action = %q, want send", res.Action)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUnknownCommand(t *testing.T) {
|
||||
c, ctx, store := setupCore(t, nil)
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
res, err := c.ProcessTurn(ctx, id, "/bogus")
|
||||
_, err := c.ProcessTurn(ctx, id, "/bogus")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessTurn: %v", err)
|
||||
}
|
||||
if res.Action != "send" {
|
||||
t.Fatalf("action = %q, want send", res.Action)
|
||||
|
||||
}
|
||||
|
||||
func TestMaxTurnsBlocksExcessCollection(t *testing.T) {
|
||||
var calls int
|
||||
c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) {
|
||||
calls++
|
||||
return Decision{Phase: "ask", ChatReply: "Ещё вопрос"}, nil
|
||||
})
|
||||
c.MaxTurns = 2
|
||||
id := mkTask(t, store, ctx, "u1")
|
||||
|
||||
_, err := c.ProcessTurn(ctx, id, "первый факт")
|
||||
if err != nil {
|
||||
t.Fatalf("1-й ход: %v", err)
|
||||
}
|
||||
_, err = c.ProcessTurn(ctx, id, "второй факт")
|
||||
if err != nil {
|
||||
t.Fatalf("2-й ход: %v", err)
|
||||
}
|
||||
res, err := c.ProcessTurn(ctx, id, "третий факт")
|
||||
if err != nil {
|
||||
t.Fatalf("3-й ход: %v", err)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("decide calls = %d, want 2", calls)
|
||||
}
|
||||
if !strings.Contains(res.Reply, "лимит") {
|
||||
t.Fatalf("reply = %q, want упоминание лимита", res.Reply)
|
||||
}
|
||||
|
||||
// /skip — принудительный вызов, лимит не мешает
|
||||
_, err = c.ProcessTurn(ctx, id, "/skip")
|
||||
if err != nil {
|
||||
t.Fatalf("/skip: %v", err)
|
||||
}
|
||||
if calls != 3 {
|
||||
t.Fatalf("decide calls after /skip = %d, want 3", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,4 +250,4 @@ func TestParseTaskID(t *testing.T) {
|
||||
if _, ok := parseTaskID("abc"); ok {
|
||||
t.Fatal("parseTaskID(abc) should be invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ type Message struct {
|
||||
|
||||
// Decision — вердикт аналитика.
|
||||
type Decision struct {
|
||||
Phase string // "ask" | "propose" | "ready" | "abort"
|
||||
Draft storage.Task // обновлённые поля черновика
|
||||
Questions []string // вопросы для phase=ask
|
||||
ChatReply string // ответ пользователю
|
||||
Phase string // "ask" | "propose" | "ready" | "abort"
|
||||
Draft storage.Task // обновлённые поля черновика
|
||||
Questions []string // вопросы для phase=ask
|
||||
ChatReply string // ответ пользователю
|
||||
}
|
||||
|
||||
// Decider — интерфейс для вызова аналитика (opencode).
|
||||
@@ -29,7 +29,6 @@ type Decider interface {
|
||||
// Result — результат одного хода.
|
||||
type Result struct {
|
||||
Reply string
|
||||
Action string // send | summary | created:N | abort | drop | greeting
|
||||
TaskID int64
|
||||
Status storage.Status
|
||||
}
|
||||
@@ -46,4 +45,4 @@ func isConsent(text string) bool {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user