Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions backend/biz/agentresource/bare_repo_ensurer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package agentresource

import (
"context"
"fmt"

"github.com/google/uuid"

"github.com/chaitin/MonkeyCode/backend/db"
"github.com/chaitin/MonkeyCode/backend/db/agentpluginrepo"
"github.com/chaitin/MonkeyCode/backend/db/agentskillrepo"
)

// BareRepoSourceType / BareRepoScopeType 与 ent enum 字符串严格保持一致。
const (
BareRepoSourceType = "bare"
BareRepoScopeTypeTeam = "team"
BareRepoScopeTypeGlobal = "global"
)

// teamBareRepoName 生成 bare repo 的展示名,与 docs/skill-architecture.md §3
// 锁定的命名一致(`team-<team_id>`)。
func teamBareRepoName(teamID uuid.UUID) string {
return fmt.Sprintf("team-%s", teamID.String())
}

// EnsureTeamBareReposTx 保证给定 team 存在恰好一个 bare skill_repo 与一个 bare
// plugin_repo(对应 docs/skill-architecture.md §3 中的 provision 钩子)。
// 幂等:已存在则原样返回(不重复创建,不更新);不存在则插入。
// 必须在事务内调用,事务由调用方持有(典型场景是 InitTeam 的事务尾部)。
func EnsureTeamBareReposTx(ctx context.Context, tx *db.Tx, teamID, createdBy uuid.UUID) (*db.AgentSkillRepo, *db.AgentPluginRepo, error) {
scopeID := teamID.String()

skillRepo, err := ensureTeamBareSkillRepo(ctx, tx, teamID, scopeID, createdBy)
if err != nil {
return nil, nil, fmt.Errorf("ensure bare skill repo: %w", err)
}
pluginRepo, err := ensureTeamBarePluginRepo(ctx, tx, teamID, scopeID, createdBy)
if err != nil {
return nil, nil, fmt.Errorf("ensure bare plugin repo: %w", err)
}
return skillRepo, pluginRepo, nil
}

func ensureTeamBareSkillRepo(ctx context.Context, tx *db.Tx, teamID uuid.UUID, scopeID string, createdBy uuid.UUID) (*db.AgentSkillRepo, error) {
existing, err := tx.AgentSkillRepo.Query().
Where(
agentskillrepo.ScopeTypeEQ(agentskillrepo.ScopeTypeTeam),
agentskillrepo.ScopeIDEQ(scopeID),
agentskillrepo.SourceTypeEQ(agentskillrepo.SourceTypeBare),
agentskillrepo.IsDeletedEQ(false),
).
First(ctx)
if err == nil {
return existing, nil
}
if !db.IsNotFound(err) {
return nil, err
}
return tx.AgentSkillRepo.Create().
SetID(uuid.New()).
SetName(teamBareRepoName(teamID)).
SetScopeType(agentskillrepo.ScopeTypeTeam).
SetScopeID(scopeID).
SetCreatedBy(createdBy).
SetSourceType(agentskillrepo.SourceTypeBare).
Save(ctx)
}

func ensureTeamBarePluginRepo(ctx context.Context, tx *db.Tx, teamID uuid.UUID, scopeID string, createdBy uuid.UUID) (*db.AgentPluginRepo, error) {
existing, err := tx.AgentPluginRepo.Query().
Where(
agentpluginrepo.ScopeTypeEQ(agentpluginrepo.ScopeTypeTeam),
agentpluginrepo.ScopeIDEQ(scopeID),
agentpluginrepo.SourceTypeEQ(agentpluginrepo.SourceTypeBare),
agentpluginrepo.IsDeletedEQ(false),
).
First(ctx)
if err == nil {
return existing, nil
}
if !db.IsNotFound(err) {
return nil, err
}
return tx.AgentPluginRepo.Create().
SetID(uuid.New()).
SetName(teamBareRepoName(teamID)).
SetScopeType(agentpluginrepo.ScopeTypeTeam).
SetScopeID(scopeID).
SetCreatedBy(createdBy).
SetSourceType(agentpluginrepo.SourceTypeBare).
Save(ctx)
}
27 changes: 27 additions & 0 deletions backend/biz/agentresource/noop_objectstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package agentresource

import (
"context"
"errors"
"fmt"
"io"
"time"
)

// noopObjectStore is used when ObjectStorage is disabled in config. Every
// fetch fails with a stable sentinel-ish error so resolver-level warn logs
// stay informative ("skill skipped: object storage disabled") rather than
// crashing with a nil deref.
type noopObjectStore struct{}

func (noopObjectStore) GetObject(_ context.Context, _ string) (io.ReadCloser, error) {
return nil, fmt.Errorf("object storage disabled")
}

func (noopObjectStore) PresignGet(_ context.Context, _ string, _ time.Duration) (string, error) {
return "", errors.New("object storage disabled")
}

func (noopObjectStore) PutFile(_ context.Context, _, _ string, _ io.Reader) error {
return errors.New("object storage disabled")
}
77 changes: 77 additions & 0 deletions backend/biz/agentresource/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package agentresource — DI wiring for the agent-resource read-only stack.
//
// The Repo / Resolver pair is consumed by:
// - biz/task/usecase (rule + skill + plugin injection into ConfigFile slice)
// - biz/skill/handler/v1 (/api/v1/skills picker)
// - biz/plugin/handler/v1 (/api/v1/plugins picker)
package agentresource

import (
"context"
"log/slog"

"github.com/samber/do"

"github.com/chaitin/MonkeyCode/backend/config"
"github.com/chaitin/MonkeyCode/backend/db"
"github.com/chaitin/MonkeyCode/backend/pkg/oss"
)

// ProvideAgentResource wires the agent-resource module. The ObjectStore is
// picked from whichever bucket block is configured:
//
// - object_storage.enabled = true → AWS-SDK-v2 client (any S3-compatible
// store: MinIO, RustFS, real AWS). Reuses the same client avatar / repo /
// spec / temp uploads use, so single SDK in the binary.
// - aliyun.public_oss.bucket set → aliyun-oss-go-sdk client. AWS SDK's
// SigV4 signer is incompatible with Aliyun OSS (SignatureDoesNotMatch +
// bucket double-prefix in path-style URLs), so we wire a native client
// just for this code path. Existing pkg/oss.Client is untouched; avatar
// etc. still go through the AWS SDK path when object_storage is on.
// - neither configured → nil ObjectStore. Resolver downgrades
// to noopObjectStore; fetch/presign each fail and the per-asset skip
// pipeline keeps the task creating without rule/skill/plugin assets.
func ProvideAgentResource(i *do.Injector) {
do.Provide(i, func(i *do.Injector) (Repo, error) {
return NewRepo(do.MustInvoke[*db.Client](i)), nil
})

do.Provide(i, func(i *do.Injector) (ObjectStore, error) {
cfg := do.MustInvoke[*config.Config](i)
logger := do.MustInvoke[*slog.Logger](i)

// Primary: ObjectStorage block — AWS-SDK-v2 client.
if cfg.ObjectStorage.Enabled {
opt := oss.S3Option{
ForcePathStyle: cfg.ObjectStorage.ForcePathStyle,
InitBucket: cfg.ObjectStorage.InitBucket,
}
client, err := oss.NewS3Compatible(context.Background(), cfg.ObjectStorage, opt)
if err != nil {
return nil, err
}
return client, nil
}

// Fallback: aliyun.public_oss — native aliyun-oss-go-sdk client.
if pub := cfg.Aliyun.PublicOSS; pub.Bucket != "" && pub.Endpoint != "" {
logger.Info("agentresource: using aliyun.public_oss (native SDK)",
"endpoint", pub.Endpoint, "bucket", pub.Bucket)
client, err := oss.NewAliyunOSS(pub)
if err != nil {
return nil, err
}
return client, nil
}

// Neither block configured — Resolver downgrades to noopObjectStore.
return noopObjectStore{}, nil
})

do.Provide(i, func(i *do.Injector) (ResolverInterface, error) {
repo := do.MustInvoke[Repo](i)
store := do.MustInvoke[ObjectStore](i)
logger := do.MustInvoke[*slog.Logger](i)
return NewResolver(repo, store, logger), nil
})
}
Loading