44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package logic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"img_infinite_canvas/internal/domain/design"
|
|
)
|
|
|
|
const streamingThinkingThrottle = 450 * time.Millisecond
|
|
|
|
type messageEventState struct {
|
|
fingerprints map[string]string
|
|
emittedAt map[string]time.Time
|
|
}
|
|
|
|
func newMessageEventState() *messageEventState {
|
|
return &messageEventState{
|
|
fingerprints: make(map[string]string),
|
|
emittedAt: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
func (s *messageEventState) shouldEmit(message design.Message, now time.Time) bool {
|
|
if s == nil {
|
|
return true
|
|
}
|
|
key := messageEventKey(message)
|
|
fingerprint := messageEventFingerprint(message)
|
|
if s.fingerprints[key] == fingerprint {
|
|
return false
|
|
}
|
|
if _, seen := s.fingerprints[key]; !seen || messageEventBypassesThrottle(message) {
|
|
s.record(key, fingerprint, now)
|
|
return true
|
|
}
|
|
if last := s.emittedAt[key]; last.IsZero() || now.Sub(last) >= streamingThinkingThrottle {
|
|
s.record(key, fingerprint, now)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *messageEventState) record(key string, fingerprint string, now time.Time) {
|
|
s.fingerprints[key] = fingerprint
|
|
s.emittedAt[key] = now
|
|
}
|
|
|
|
func messageEventKey(message design.Message) string {
|
|
if key := strings.TrimSpace(message.ID); key != "" {
|
|
return key
|
|
}
|
|
return fmt.Sprintf("%s:%s:%s:%s:%s", message.ThreadID, message.Role, message.Type, message.Title, message.CreatedAt.UTC().Format(time.RFC3339Nano))
|
|
}
|
|
|
|
func messageEventBypassesThrottle(message design.Message) bool {
|
|
if message.Type == "text_extraction_result" {
|
|
return true
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(message.Status)) {
|
|
case "running", "streaming":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func projectEventFingerprint(project design.Project) string {
|
|
payload := struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"userId"`
|
|
Title string `json:"title"`
|
|
Brief string `json:"brief"`
|
|
Status design.ProjectStatus `json:"status"`
|
|
Thumbnail string `json:"thumbnail"`
|
|
LastThreadID string `json:"lastThreadId"`
|
|
Viewport design.Viewport `json:"viewport"`
|
|
Nodes []design.Node `json:"nodes"`
|
|
Connections []design.Connection `json:"connections"`
|
|
}{
|
|
ID: project.ID,
|
|
UserID: project.UserID,
|
|
Title: project.Title,
|
|
Brief: project.Brief,
|
|
Status: project.Status,
|
|
Thumbnail: project.Thumbnail,
|
|
LastThreadID: project.LastThreadID,
|
|
Viewport: project.Viewport,
|
|
Nodes: append([]design.Node(nil), project.Nodes...),
|
|
Connections: append([]design.Connection(nil), project.Connections...),
|
|
}
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Sprintf("%s:%s:%s:%d:%d", project.ID, project.Status, project.LastThreadID, len(project.Nodes), len(project.Connections))
|
|
}
|
|
return string(data)
|
|
}
|