chore: sync local project state

This commit is contained in:
Your Name
2026-05-12 18:49:52 +08:00
parent afdbea6fb5
commit 1c0084afe8
105 changed files with 13221 additions and 420 deletions

181
cmd/sub2api-bridge/main.go Normal file
View File

@@ -0,0 +1,181 @@
package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
func main() {
supplyURL := os.Getenv("SUPPLY_URL")
if supplyURL == "" {
supplyURL = "http://127.0.0.1:8081"
}
consumer := os.Getenv("CONSUMER")
if consumer == "" {
consumer = "sub2api-bridge"
}
dbConn := os.Getenv("SUB2API_DB")
if dbConn == "" {
dbConn = "postgres://sub2api:***@localhost:5432/sub2api?sslmode=disable"
}
db, err := sql.Open("postgres", dbConn)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("ping db: %v", err)
}
log.Println("connected to sub2api db")
if err := ensureBridgeTable(db); err != nil {
log.Fatalf("ensure table: %v", err)
}
cursor := ""
for {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
events, nextCursor, err := fetchPackageChanges(ctx, supplyURL, cursor)
cancel()
if err != nil {
log.Printf("fetch error: %v", err)
time.Sleep(10 * time.Second)
continue
}
for _, evt := range events {
if evt.GatewaySyncStatus != "pending" {
log.Printf("skip non-pending event: %s status=%s", evt.EventID, evt.GatewaySyncStatus)
continue
}
log.Printf("bridge event: %s package=%d model=%s", evt.EventID, evt.PackageID, evt.Model)
if err := bridgeToSub2API(db, evt); err != nil {
log.Printf("bridge error: %v", err)
continue
}
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
ackErr := ackPackageChange(ctx2, supplyURL, evt.EventID, consumer, "applied", "synced to sub2api")
cancel2()
if ackErr != nil {
log.Printf("ack error for %s: %v", evt.EventID, ackErr)
continue
}
log.Printf("acked event: %s", evt.EventID)
}
if nextCursor == "" {
log.Println("no more events, sleeping 10s")
time.Sleep(10 * time.Second)
} else {
cursor = nextCursor
}
}
}
type PackageChangeEvent struct {
EventID string `json:"event_id"`
AccountID int64 `json:"account_id"`
EventType string `json:"event_type"`
PackageID int64 `json:"package_id"`
Platform string `json:"platform"`
Model string `json:"model"`
OccurredAt string `json:"occurred_at"`
Version int `json:"version"`
GatewaySyncStatus string `json:"gateway_sync_status"`
RetryCount int `json:"retry_count"`
NextRetryAt string `json:"next_retry_at,omitempty"`
LastFailureCategory string `json:"last_failure_category,omitempty"`
}
func fetchPackageChanges(ctx context.Context, baseURL, cursor string) ([]PackageChangeEvent, string, error) {
url := fmt.Sprintf("%s/internal/supply-intelligence/gateway/package-changes", baseURL)
if cursor != "" {
url += "?cursor=" + cursor
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result struct {
Items []PackageChangeEvent `json:"items"`
NextCursor string `json:"next_cursor"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, "", err
}
return result.Items, result.NextCursor, nil
}
func ackPackageChange(ctx context.Context, baseURL, eventID, consumer, result, detail string) error {
url := fmt.Sprintf("%s/internal/supply-intelligence/gateway/package-changes/%s/ack", baseURL, eventID)
payload := map[string]string{
"consumer": consumer,
"result": result,
"detail": detail,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}
return nil
}
func ensureBridgeTable(db *sql.DB) error {
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS supply_bridge_log (
id SERIAL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
package_id BIGINT,
platform TEXT,
model TEXT,
status TEXT,
result TEXT,
detail TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)`)
return err
}
func bridgeToSub2API(db *sql.DB, evt PackageChangeEvent) error {
_, err := db.Exec(
`INSERT INTO supply_bridge_log (event_id, package_id, platform, model, status, result, detail)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (event_id) DO UPDATE SET
status = EXCLUDED.status,
result = EXCLUDED.result,
detail = EXCLUDED.detail,
created_at = NOW()`,
evt.EventID, evt.PackageID, evt.Platform, evt.Model, evt.GatewaySyncStatus, "applied", "synced to sub2api",
)
return err
}

View File

@@ -4,15 +4,35 @@ import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"supply-intelligence/internal/app"
"supply-intelligence/internal/domain"
"supply-intelligence/internal/repository"
)
func main() {
application := app.New()
application.Repo.UpsertRoutingState(domain.AccountRoutingState{
ctx := context.Background()
// Use PostgreSQL if DATABASE_URL is set, otherwise in-memory.
var application *app.Application
if connString := os.Getenv("DATABASE_URL"); connString != "" {
var err error
application, err = app.NewWithPostgres(ctx, connString)
if err != nil {
log.Fatalf("failed to connect to postgres: %v", err)
}
log.Println("supply-intelligence: using PostgreSQL backend")
} else {
application = app.New()
log.Println("supply-intelligence: using in-memory backend (DATABASE_URL not set)")
}
// Seed a sample routing state for account 1 (works with both backends)
application.Repo.UpsertRoutingState(ctx, domain.AccountRoutingState{
AccountID: 1,
Platform: "openai",
AccountStatus: domain.AccountStatusActive,
@@ -22,10 +42,77 @@ func main() {
LastProbeAt: time.Now().UTC(),
Version: 1,
})
// Seed a supply account with API key for discovery
application.Repo.UpsertSupplyAccount(ctx, domain.SupplyAccount{
AccountID: 1,
Platform: "openai",
APIKey: os.Getenv("OPENAI_API_KEY"),
ConsumerTag: "gateway",
Status: "active",
})
// Seed local demo data so smoke / inspect / rollback can run without external API keys
if os.Getenv("SEED_LOCAL_DEMO") == "1" {
seedLocalDemo(application)
}
// Start all background runtimes: gateway consumer poller, discovery, admission
application.StartBackground(context.Background())
defer application.StopBackground()
log.Println("supply-intelligence listening on :8080")
if err := http.ListenAndServe(":8080", application.Server.Routes()); err != nil {
log.Println("background workers started")
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
log.Println("shutting down supply-intelligence...")
application.Close()
os.Exit(0)
}()
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("supply-intelligence listening on :%s", port)
if err := http.ListenAndServe(":"+port, application.Server.Routes()); err != nil {
log.Fatal(err)
}
}
// Verify at compile time that *MemoryRepository implements repository.Repository
var _ repository.Repository = (*repository.MemoryRepository)(nil)
func seedLocalDemo(application *app.Application) {
ctx := context.Background()
now := time.Now().UTC()
// Seed a test-passed discovery candidate
application.Repo.UpsertDiscoveryCandidate(ctx, domain.DiscoveryCandidate{
CandidateID: "demo-cand-001",
AccountID: 1,
Platform: "openai",
Model: "gpt-4.1-mini",
Source: "demo",
Status: domain.DiscoveryCandidateStatusTestPassed,
DiscoveredAt: now,
UpdatedAt: now,
Version: 1,
})
// Seed a draft supply package
application.Repo.UpsertSupplyPackage(ctx, domain.SupplyPackage{
PackageID: 1001,
Platform: "openai",
Model: "gpt-4.1-mini",
Status: "draft",
Source: "demo",
CreatedAt: now,
UpdatedAt: now,
Version: 1,
})
log.Println("seedLocalDemo: inserted demo candidate and draft package")
}