Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,266 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/lib/pq"
)
// channelMonitorRequestTemplateRepository 实现 service.ChannelMonitorRequestTemplateRepository。
// 与 channelMonitorRepository 分开一个文件,职责清晰。
type channelMonitorRequestTemplateRepository struct {
client *dbent.Client
db *sql.DB
}
// NewChannelMonitorRequestTemplateRepository 创建模板仓储实例。
func NewChannelMonitorRequestTemplateRepository(client *dbent.Client, db *sql.DB) service.ChannelMonitorRequestTemplateRepository {
return &channelMonitorRequestTemplateRepository{client: client, db: db}
}
// ---------- CRUD ----------
func (r *channelMonitorRequestTemplateRepository) Create(ctx context.Context, t *service.ChannelMonitorRequestTemplate) error {
client := clientFromContext(ctx, r.client)
builder := client.ChannelMonitorRequestTemplate.Create().
SetName(t.Name).
SetProvider(channelmonitorrequesttemplate.Provider(t.Provider)).
SetAPIMode(defaultAPIModeRepo(t.APIMode)).
SetDescription(t.Description).
SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)).
SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode))
if t.BodyOverride != nil {
builder = builder.SetBodyOverride(t.BodyOverride)
}
created, err := builder.Save(ctx)
if err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
}
t.ID = created.ID
t.CreatedAt = created.CreatedAt
t.UpdatedAt = created.UpdatedAt
return nil
}
func (r *channelMonitorRequestTemplateRepository) GetByID(ctx context.Context, id int64) (*service.ChannelMonitorRequestTemplate, error) {
row, err := r.client.ChannelMonitorRequestTemplate.Query().
Where(channelmonitorrequesttemplate.IDEQ(id)).
Only(ctx)
if err != nil {
return nil, translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
}
return entToServiceTemplate(row), nil
}
func (r *channelMonitorRequestTemplateRepository) Update(ctx context.Context, t *service.ChannelMonitorRequestTemplate) error {
client := clientFromContext(ctx, r.client)
updater := client.ChannelMonitorRequestTemplate.UpdateOneID(t.ID).
SetName(t.Name).
SetAPIMode(defaultAPIModeRepo(t.APIMode)).
SetDescription(t.Description).
SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)).
SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode))
if t.BodyOverride != nil {
updater = updater.SetBodyOverride(t.BodyOverride)
} else {
updater = updater.ClearBodyOverride()
}
updated, err := updater.Save(ctx)
if err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
}
t.UpdatedAt = updated.UpdatedAt
return nil
}
func (r *channelMonitorRequestTemplateRepository) Delete(ctx context.Context, id int64) error {
client := clientFromContext(ctx, r.client)
if err := client.ChannelMonitorRequestTemplate.DeleteOneID(id).Exec(ctx); err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
}
return nil
}
func (r *channelMonitorRequestTemplateRepository) List(ctx context.Context, params service.ChannelMonitorRequestTemplateListParams) ([]*service.ChannelMonitorRequestTemplate, error) {
q := r.client.ChannelMonitorRequestTemplate.Query()
if params.Provider != "" {
q = q.Where(channelmonitorrequesttemplate.ProviderEQ(channelmonitorrequesttemplate.Provider(params.Provider)))
}
if params.APIMode != "" {
q = q.Where(channelmonitorrequesttemplate.APIModeEQ(defaultAPIModeRepo(params.APIMode)))
}
rows, err := q.
Order(dbent.Asc(channelmonitorrequesttemplate.FieldProvider), dbent.Asc(channelmonitorrequesttemplate.FieldAPIMode), dbent.Asc(channelmonitorrequesttemplate.FieldName)).
All(ctx)
if err != nil {
return nil, fmt.Errorf("list monitor templates: %w", err)
}
out := make([]*service.ChannelMonitorRequestTemplate, 0, len(rows))
for _, row := range rows {
out = append(out, entToServiceTemplate(row))
}
return out, nil
}
// ApplyToMonitors 把模板当前配置覆盖到 monitorIDs 列表里的关联监控。
// WHERE 双重过滤:template_id = id AND id IN (monitorIDs),防止用户传了未关联本模板的 id
// 就被覆盖。模板字段通过 ent UpdateMany 更新以保留 hooksextra_headers 在同一事务中
// 单独合并,以保留仅用于幂等恢复、绝不会发往上游的内部 operation ID。
func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error) {
if len(monitorIDs) == 0 {
return 0, nil
}
if tx := dbent.TxFromContext(ctx); tx != nil {
return r.applyToMonitorsWithClient(ctx, tx.Client(), id, monitorIDs)
}
tx, err := r.client.Tx(ctx)
if err != nil {
return 0, fmt.Errorf("begin apply template transaction: %w", err)
}
txCtx := dbent.NewTxContext(ctx, tx)
affected, err := r.applyToMonitorsWithClient(txCtx, tx.Client(), id, monitorIDs)
if err != nil {
_ = tx.Rollback()
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit apply template transaction: %w", err)
}
return affected, nil
}
func (r *channelMonitorRequestTemplateRepository) applyToMonitorsWithClient(
ctx context.Context,
client *dbent.Client,
id int64,
monitorIDs []int64,
) (int64, error) {
tpl, err := client.ChannelMonitorRequestTemplate.Query().
Where(channelmonitorrequesttemplate.IDEQ(id)).
Only(ctx)
if err != nil {
return 0, translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
}
updater := client.ChannelMonitor.Update().
Where(
channelmonitor.TemplateIDEQ(id),
channelmonitor.IDIn(monitorIDs...),
channelmonitor.ProviderEQ(channelmonitor.Provider(tpl.Provider)),
channelmonitor.APIModeEQ(defaultAPIModeRepo(tpl.APIMode)),
).
SetAPIMode(defaultAPIModeRepo(tpl.APIMode)).
SetBodyOverrideMode(defaultBodyModeRepo(tpl.BodyOverrideMode))
if tpl.BodyOverride != nil {
updater = updater.SetBodyOverride(tpl.BodyOverride)
} else {
updater = updater.ClearBodyOverride()
}
affected, err := updater.Save(ctx)
if err != nil {
return 0, fmt.Errorf("apply template to monitors: %w", err)
}
if affected == 0 {
return 0, nil
}
templateHeaders := channelMonitorHeadersForPersistence(&service.ChannelMonitor{
ExtraHeaders: tpl.ExtraHeaders,
})
templateHeadersJSON, err := json.Marshal(templateHeaders)
if err != nil {
return 0, fmt.Errorf("marshal template headers: %w", err)
}
result, err := client.ExecContext(ctx, `
UPDATE channel_monitors
SET extra_headers = $1::jsonb || CASE
WHEN COALESCE(extra_headers, '{}'::jsonb) ? ($2::text)
THEN jsonb_build_object($2::text, COALESCE(extra_headers, '{}'::jsonb) -> ($2::text))
ELSE '{}'::jsonb
END
WHERE template_id = $3
AND id = ANY($4)
AND provider = $5
AND api_mode = $6
`, string(templateHeadersJSON), service.ChannelMonitorDuplicateOperationIDMetadataKey,
id, pq.Array(monitorIDs), string(tpl.Provider), defaultAPIModeRepo(tpl.APIMode))
if err != nil {
return 0, fmt.Errorf("apply template headers to monitors: %w", err)
}
headersAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("count applied template headers: %w", err)
}
if headersAffected != int64(affected) {
return 0, fmt.Errorf("apply template headers: affected %d rows, expected %d", headersAffected, affected)
}
return headersAffected, nil
}
// CountAssociatedMonitors 统计关联监控数(UI 展示「N 个配置」用)。
func (r *channelMonitorRequestTemplateRepository) CountAssociatedMonitors(ctx context.Context, id int64) (int64, error) {
count, err := r.client.ChannelMonitor.Query().
Where(channelmonitor.TemplateIDEQ(id)).
Count(ctx)
if err != nil {
return 0, fmt.Errorf("count monitors for template %d: %w", id, err)
}
return int64(count), nil
}
// ListAssociatedMonitors 列出模板关联的所有监控简略字段。
// ORDER BY name 稳定输出方便前端展示。
func (r *channelMonitorRequestTemplateRepository) ListAssociatedMonitors(ctx context.Context, id int64) ([]*service.AssociatedMonitorBrief, error) {
rows, err := r.client.ChannelMonitor.Query().
Where(channelmonitor.TemplateIDEQ(id)).
Order(dbent.Asc(channelmonitor.FieldName)).
All(ctx)
if err != nil {
return nil, fmt.Errorf("list associated monitors for template %d: %w", id, err)
}
out := make([]*service.AssociatedMonitorBrief, 0, len(rows))
for _, row := range rows {
out = append(out, &service.AssociatedMonitorBrief{
ID: row.ID,
Name: row.Name,
Provider: string(row.Provider),
APIMode: defaultAPIModeRepo(row.APIMode),
Enabled: row.Enabled,
})
}
return out, nil
}
// ---------- helpers ----------
func entToServiceTemplate(row *dbent.ChannelMonitorRequestTemplate) *service.ChannelMonitorRequestTemplate {
if row == nil {
return nil
}
headers := row.ExtraHeaders
if headers == nil {
headers = map[string]string{}
}
return &service.ChannelMonitorRequestTemplate{
ID: row.ID,
Name: row.Name,
Provider: string(row.Provider),
APIMode: defaultAPIModeRepo(row.APIMode),
Description: row.Description,
ExtraHeaders: headers,
BodyOverrideMode: row.BodyOverrideMode,
BodyOverride: row.BodyOverride,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}