Skip to content
3 changes: 1 addition & 2 deletions backend/api/router/coze/loop/apis/middleware/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ func SessionMW(ss session.ISessionService, us userservice.Client) app.HandlerFun
return func(ctx context.Context, c *app.RequestContext) {
path := string(c.Path())
if path == "/api/foundation/v1/users/login_by_password" ||
path == "/api/foundation/v1/users/register" ||
path == "/api/foundation/v1/users/reset_password" {
path == "/api/foundation/v1/users/register" {
c.Next(ctx)
return
}
Expand Down
60 changes: 60 additions & 0 deletions backend/infra/db/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2026 coze-dev Authors
// SPDX-License-Identifier: Apache-2.0

package db

import (
"bytes"
)

func QuoteSQLData(data string) string {
return "'" + escapeSQL(data) + "'"
}

func EscapeSQLData(data string) string {
return escapeSQL(data)
}

func escapeSQL(data string) string {
buf := bytes.NewBuffer(nil)
for _, c := range data {
switch c {
case 0x00:
buf.WriteByte('\\')
buf.WriteByte('0')
case '\'':
buf.WriteByte('\\')
buf.WriteByte('\'')
case '"':
buf.WriteByte('\\')
buf.WriteByte('"')
case 0x08:
buf.WriteByte('\\')
buf.WriteByte('b')
case 0x0A:
buf.WriteByte('\\')
buf.WriteByte('n')
case 0x0D:
buf.WriteByte('\\')
buf.WriteByte('r')
case 0x09:
buf.WriteByte('\\')
buf.WriteByte('t')
case 0x1A:
buf.WriteByte('\\')
buf.WriteByte('Z')
case '\\':
buf.WriteByte('\\')
buf.WriteByte('\\')
// case '%':
// buf.WriteByte('\\')
// buf.WriteByte('%')
// case '_':
// buf.WriteByte('\\')
// buf.WriteByte('_')
default:
buf.WriteRune(c)
}
}
return buf.String()
}
60 changes: 60 additions & 0 deletions backend/infra/db/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2026 coze-dev Authors
// SPDX-License-Identifier: Apache-2.0

package db

import "testing"

func TestQuoteSQLData(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: "''"},
{name: "simple", in: "abc", want: "'abc'"},
{name: "single_quote", in: "O'Reilly", want: "'O\\'Reilly'"},
{name: "double_quote", in: "a\"b", want: "'a\\\"b'"},
{name: "backslash", in: `a\b`, want: "'a\\\\b'"},
{name: "newline", in: "line1\nline2", want: "'line1\\nline2'"},
{name: "tab", in: "a\tb", want: "'a\\tb'"},
{name: "carriage_return", in: "a\rb", want: "'a\\rb'"},
{name: "null_byte", in: string([]byte{0}), want: "'\\0'"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := QuoteSQLData(tt.in)
if got != tt.want {
t.Fatalf("QuoteSQLData(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}

func TestEscapeSQLData(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: ""},
{name: "simple", in: "abc", want: "abc"},
{name: "single_quote", in: "O'Reilly", want: "O\\'Reilly"},
{name: "double_quote", in: "a\"b", want: "a\\\"b"},
{name: "backslash", in: `a\b`, want: "a\\\\b"},
{name: "newline", in: "line1\nline2", want: "line1\\nline2"},
{name: "tab", in: "a\tb", want: "a\\tb"},
{name: "carriage_return", in: "a\rb", want: "a\\rb"},
{name: "null_byte", in: string([]byte{0}), want: "\\0"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := EscapeSQLData(tt.in)
if got != tt.want {
t.Fatalf("EscapeSQLData(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -349,28 +349,30 @@ func ToTargetRunStatsDO(status spi.InvokeEvalTargetStatus) entity.EvalTargetRunS
}

func ToInvokeOutputDataDO(req *openapi.ReportEvalTargetInvokeResultRequest) *entity.EvalTargetOutputData {
switch req.GetStatus() {
case spi.InvokeEvalTargetStatus_SUCCESS:
output := req.GetOutput()
usage := req.GetUsage()
output := req.GetOutput()
usage := req.GetUsage()

outputFields := make(map[string]*entity.Content)
outputFields := make(map[string]*entity.Content)
if output != nil {
if output.ActualOutput != nil {
outputFields[consts.OutputSchemaKey] = ToSPIContentDO(output.ActualOutput)
}
for k, v := range output.ExtOutput {
outputFields[k] = ToSPIContentDO(v)
}
}

var evalTargetUsage *entity.EvalTargetUsage
if usage != nil && (usage.InputTokens != nil || usage.OutputTokens != nil) {
evalTargetUsage = &entity.EvalTargetUsage{
InputTokens: getInt64Value(usage.InputTokens),
OutputTokens: getInt64Value(usage.OutputTokens),
}
evalTargetUsage.TotalTokens = evalTargetUsage.InputTokens + evalTargetUsage.OutputTokens
var evalTargetUsage *entity.EvalTargetUsage
if usage != nil && (usage.InputTokens != nil || usage.OutputTokens != nil) {
evalTargetUsage = &entity.EvalTargetUsage{
InputTokens: getInt64Value(usage.InputTokens),
OutputTokens: getInt64Value(usage.OutputTokens),
}
evalTargetUsage.TotalTokens = evalTargetUsage.InputTokens + evalTargetUsage.OutputTokens
}

switch req.GetStatus() {
case spi.InvokeEvalTargetStatus_SUCCESS:
return &entity.EvalTargetOutputData{
OutputFields: outputFields,
EvalTargetUsage: evalTargetUsage,
Expand All @@ -387,6 +389,8 @@ func ToInvokeOutputDataDO(req *openapi.ReportEvalTargetInvokeResultRequest) *ent
}
}
return &entity.EvalTargetOutputData{
OutputFields: outputFields,
EvalTargetUsage: evalTargetUsage,
EvalTargetRunError: evalTargetRunError,
}

Expand Down
41 changes: 40 additions & 1 deletion backend/modules/evaluation/domain/entity/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

package entity

import (
"context"

"github.com/coze-dev/coze-loop/backend/pkg/ctxcache"
)

type ExptScheduleEvent struct {
SpaceID int64
ExptID int64
Expand All @@ -18,6 +24,10 @@ type ExptScheduleEvent struct {
ExecEvalSetItemIDs []int64
}

type ctxTargetCalledCacheKey struct{}

type ctxForceNoRetryKey struct{}

type ExptItemEvalEvent struct {
SpaceID int64
ExptID int64
Expand All @@ -35,7 +45,36 @@ type ExptItemEvalEvent struct {
Session *Session
}

func (e *ExptItemEvalEvent) IgnoreExistedResult() bool {
func (e *ExptItemEvalEvent) WithCtxForceNoRetry(ctx context.Context) {
ctxcache.Store(ctx, ctxForceNoRetryKey{}, struct{}{})
}

func (e *ExptItemEvalEvent) CtxForceNoRetry(ctx context.Context) bool {
_, ok := ctxcache.Get[struct{}](ctx, ctxForceNoRetryKey{})
return ok
}

func (e *ExptItemEvalEvent) IgnoreExistedTargetResult() bool {
return e.ignoreExistedResult()
}

func (e *ExptItemEvalEvent) WithCtxTargetCalled(ctx context.Context) {
ctxcache.Store(ctx, ctxTargetCalledCacheKey{}, struct{}{})
}

func (e *ExptItemEvalEvent) CtxTargetCalled(ctx context.Context) bool {
_, ok := ctxcache.Get[struct{}](ctx, ctxTargetCalledCacheKey{})
return ok
}

func (e *ExptItemEvalEvent) IgnoreExistedEvaluatorResult(ctx context.Context) bool {
if e.CtxTargetCalled(ctx) {
return false
}
return e.ignoreExistedResult()
}

func (e *ExptItemEvalEvent) ignoreExistedResult() bool {
return (e.ExptRunMode == EvaluationModeRetryItems || e.ExptRunMode == EvaluationModeRetryAll) && e.RetryTimes == 0
}

Expand Down
124 changes: 124 additions & 0 deletions backend/modules/evaluation/domain/entity/event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) 2025 coze-dev Authors
// SPDX-License-Identifier: Apache-2.0

package entity

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

"github.com/coze-dev/coze-loop/backend/pkg/ctxcache"
)

func TestExptItemEvalEvent_WithCtxTargetCalled(t *testing.T) {
t.Run("ctx not init: Store is no-op", func(t *testing.T) {
ctx := context.Background()
e := &ExptItemEvalEvent{}

e.WithCtxTargetCalled(ctx)
_, ok := ctxcache.Get[struct{}](ctx, ctxTargetCalledCacheKey{})
assert.False(t, ok)
})

t.Run("ctx init: should be stored and retrievable", func(t *testing.T) {
ctx := ctxcache.Init(context.Background())
e := &ExptItemEvalEvent{}

e.WithCtxTargetCalled(ctx)
_, ok := ctxcache.Get[struct{}](ctx, ctxTargetCalledCacheKey{})
assert.True(t, ok)
})
}

func TestExptItemEvalEvent_CtxTargetCalled(t *testing.T) {
t.Run("ctx not init: always false", func(t *testing.T) {
ctx := context.Background()
e := &ExptItemEvalEvent{}

assert.False(t, e.CtxTargetCalled(ctx))
})

t.Run("ctx init but not marked: false", func(t *testing.T) {
ctx := ctxcache.Init(context.Background())
e := &ExptItemEvalEvent{}

assert.False(t, e.CtxTargetCalled(ctx))
})

t.Run("ctx init and marked: true", func(t *testing.T) {
ctx := ctxcache.Init(context.Background())
e := &ExptItemEvalEvent{}

e.WithCtxTargetCalled(ctx)
assert.True(t, e.CtxTargetCalled(ctx))
})

t.Run("different contexts are isolated", func(t *testing.T) {
ctx1 := ctxcache.Init(context.Background())
ctx2 := ctxcache.Init(context.Background())
e := &ExptItemEvalEvent{}

e.WithCtxTargetCalled(ctx1)
assert.True(t, e.CtxTargetCalled(ctx1))
assert.False(t, e.CtxTargetCalled(ctx2))
})
}

func TestExptItemEvalEvent_IgnoreExistedEvaluatorResult(t *testing.T) {
t.Run("ctx init and target called: always false", func(t *testing.T) {
ctx := ctxcache.Init(context.Background())

tests := []struct {
name string
runMode ExptRunMode
retry int
}{
{name: "retry all with retryTimes=0", runMode: EvaluationModeRetryAll, retry: 0},
{name: "retry items with retryTimes=0", runMode: EvaluationModeRetryItems, retry: 0},
{name: "retry all with retryTimes=1", runMode: EvaluationModeRetryAll, retry: 1},
{name: "submit mode", runMode: EvaluationModeSubmit, retry: 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ExptItemEvalEvent{ExptRunMode: tt.runMode, RetryTimes: tt.retry}
e.WithCtxTargetCalled(ctx)
assert.False(t, e.IgnoreExistedEvaluatorResult(ctx))
})
}
})

t.Run("ctx init but target not called: falls back to ignoreExistedResult", func(t *testing.T) {
ctx := ctxcache.Init(context.Background())

e1 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryAll, RetryTimes: 0}
assert.True(t, e1.IgnoreExistedEvaluatorResult(ctx))

e2 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryItems, RetryTimes: 0}
assert.True(t, e2.IgnoreExistedEvaluatorResult(ctx))

e3 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryAll, RetryTimes: 1}
assert.False(t, e3.IgnoreExistedEvaluatorResult(ctx))

e4 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeSubmit, RetryTimes: 0}
assert.False(t, e4.IgnoreExistedEvaluatorResult(ctx))
})

t.Run("ctx not init: falls back to ignoreExistedResult", func(t *testing.T) {
ctx := context.Background()

e1 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryAll, RetryTimes: 0}
assert.True(t, e1.IgnoreExistedEvaluatorResult(ctx))

e2 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryItems, RetryTimes: 0}
assert.True(t, e2.IgnoreExistedEvaluatorResult(ctx))

e3 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeRetryAll, RetryTimes: 1}
assert.False(t, e3.IgnoreExistedEvaluatorResult(ctx))

e4 := &ExptItemEvalEvent{ExptRunMode: EvaluationModeSubmit, RetryTimes: 0}
assert.False(t, e4.IgnoreExistedEvaluatorResult(ctx))
})
}
3 changes: 2 additions & 1 deletion backend/modules/evaluation/domain/entity/expt_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,12 @@ func (e *ExptTurnRunResult) AbortWithTargetResult(expt *Experiment) bool {
return false
}

func (e *ExptTurnRunResult) AbortWithEvaluatorResults() bool {
func (e *ExptTurnRunResult) AbortWithEvaluatorResults(ctx context.Context, event *ExptItemEvalEvent) bool {
// evaluator async exec, check if any evaluator is in async invoking status
for _, record := range e.EvaluatorResults {
if record != nil && record.Status == EvaluatorRunStatusAsyncInvoking {
e.AsyncAbort = true
event.WithCtxForceNoRetry(ctx)
return true
}
}
Expand Down
Loading
Loading