Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

266 lines
9.4 KiB
Go

package workspace
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
modelplanbiz "github.com/tutti-os/tutti/services/tuttid/biz/modelplan"
)
var (
// ErrModelPlanNotFound reports a missing current model plan row.
ErrModelPlanNotFound = errors.New("model plan not found")
// ErrModelPlanRevisionNotFound reports a missing immutable plan revision.
ErrModelPlanRevisionNotFound = errors.New("model plan revision not found")
// ErrModelPlanRevisionConflict reports a non-monotonic plan write.
ErrModelPlanRevisionConflict = errors.New("model plan revision conflict")
)
// ErrModelPlanReferenced reports a plan delete blocked by a durable binding.
var ErrModelPlanReferenced = errors.New("model plan is referenced")
func (s *SQLiteStore) ListModelPlans(ctx context.Context, workspaceID string) ([]modelplanbiz.Plan, error) {
if s == nil || s.readDB == nil {
return nil, errors.New("workspace database is not initialized")
}
rows, err := s.readDB.QueryContext(ctx, `
SELECT workspace_id, plan_id, revision, name, template_kind, protocol, api_key_ciphertext,
base_url, models_json, default_model, enabled, detection_json, first_use_json,
created_at_unix_ms, updated_at_unix_ms
FROM model_plans
WHERE workspace_id = ?
ORDER BY created_at_unix_ms ASC, plan_id ASC
`, workspaceID)
if err != nil {
return nil, fmt.Errorf("list model plans: %w", err)
}
defer rows.Close()
var plans []modelplanbiz.Plan
for rows.Next() {
plan, err := scanModelPlan(rows)
if err != nil {
return nil, err
}
plans = append(plans, plan)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("list model plan rows: %w", err)
}
return plans, nil
}
func (s *SQLiteStore) GetModelPlan(ctx context.Context, workspaceID string, planID string) (modelplanbiz.Plan, error) {
if s == nil || s.readDB == nil {
return modelplanbiz.Plan{}, errors.New("workspace database is not initialized")
}
row := s.readDB.QueryRowContext(ctx, `
SELECT workspace_id, plan_id, revision, name, template_kind, protocol, api_key_ciphertext,
base_url, models_json, default_model, enabled, detection_json, first_use_json,
created_at_unix_ms, updated_at_unix_ms
FROM model_plans
WHERE workspace_id = ? AND plan_id = ?
`, workspaceID, planID)
plan, err := scanModelPlan(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return modelplanbiz.Plan{}, ErrModelPlanNotFound
}
return modelplanbiz.Plan{}, err
}
return plan, nil
}
// GetModelPlanRevision returns one immutable secret-bearing plan revision.
// Callers must keep the returned credential inside the daemon runtime
// boundary; only its id/revision/fingerprint belongs in session context.
func (s *SQLiteStore) GetModelPlanRevision(ctx context.Context, workspaceID string, planID string, revision uint64) (modelplanbiz.Plan, error) {
if s == nil || s.readDB == nil {
return modelplanbiz.Plan{}, errors.New("workspace database is not initialized")
}
if revision == 0 {
return modelplanbiz.Plan{}, ErrModelPlanRevisionNotFound
}
row := s.readDB.QueryRowContext(ctx, `
SELECT workspace_id, plan_id, revision, name, template_kind, protocol, api_key_ciphertext,
base_url, models_json, default_model, enabled, detection_json, first_use_json,
created_at_unix_ms, updated_at_unix_ms
FROM model_plan_revisions
WHERE workspace_id = ? AND plan_id = ? AND revision = ?
`, workspaceID, planID, revision)
plan, err := scanModelPlan(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return modelplanbiz.Plan{}, ErrModelPlanRevisionNotFound
}
return modelplanbiz.Plan{}, err
}
return plan, nil
}
func (s *SQLiteStore) PutModelPlan(ctx context.Context, plan modelplanbiz.Plan) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
tx, err := s.writeDB.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin put model plan: %w", err)
}
defer func() { _ = tx.Rollback() }()
var currentRevision uint64
err = tx.QueryRowContext(ctx, `
SELECT revision
FROM model_plans
WHERE workspace_id = ? AND plan_id = ?
`, plan.WorkspaceID, plan.ID).Scan(&currentRevision)
switch {
case errors.Is(err, sql.ErrNoRows):
if plan.Revision == 0 {
plan.Revision = 1
}
if plan.Revision != 1 {
return fmt.Errorf("%w: first revision is %d, want 1", ErrModelPlanRevisionConflict, plan.Revision)
}
case err != nil:
return fmt.Errorf("read current model plan revision: %w", err)
default:
// Existing callers historically wrote back the revision they read. Treat
// that as the next revision while still rejecting stale concurrent writes.
if plan.Revision == 0 || plan.Revision == currentRevision {
plan.Revision = currentRevision + 1
}
if plan.Revision != currentRevision+1 {
return fmt.Errorf("%w: got %d after %d", ErrModelPlanRevisionConflict, plan.Revision, currentRevision)
}
}
modelsJSON, err := json.Marshal(modelplanbiz.CloneModels(plan.Models))
if err != nil {
return fmt.Errorf("marshal model plan models: %w", err)
}
detectionJSON, err := json.Marshal(plan.Detection)
if err != nil {
return fmt.Errorf("marshal model plan detection: %w", err)
}
// Keep the retired column populated for database downgrade compatibility.
// Current model-plan readiness ends at successful connection detection.
const retiredFirstUseJSON = `{"status":"completed"}`
ciphertext, err := encryptManagedCredential(plan.APIKey)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `
INSERT INTO model_plans (
workspace_id, plan_id, revision, name, template_kind, protocol, api_key_ciphertext,
base_url, models_json, default_model, enabled, detection_json, first_use_json,
created_at_unix_ms, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, plan_id) DO UPDATE SET
revision = excluded.revision,
name = excluded.name,
template_kind = excluded.template_kind,
protocol = excluded.protocol,
api_key_ciphertext = excluded.api_key_ciphertext,
base_url = excluded.base_url,
models_json = excluded.models_json,
default_model = excluded.default_model,
enabled = excluded.enabled,
detection_json = excluded.detection_json,
first_use_json = excluded.first_use_json,
updated_at_unix_ms = excluded.updated_at_unix_ms
`, plan.WorkspaceID, plan.ID, plan.Revision, plan.Name, string(plan.TemplateKind), string(plan.Protocol), ciphertext,
plan.BaseURL, string(modelsJSON), plan.DefaultModel, boolInt(plan.Enabled), string(detectionJSON), retiredFirstUseJSON,
unixMs(plan.CreatedAt), unixMs(plan.UpdatedAt))
if err != nil {
return fmt.Errorf("put model plan: %w", err)
}
_, err = tx.ExecContext(ctx, `
INSERT INTO model_plan_revisions (
workspace_id, plan_id, revision, name, template_kind, protocol,
api_key_ciphertext, base_url, models_json, default_model, enabled,
detection_json, first_use_json, created_at_unix_ms, updated_at_unix_ms,
recorded_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, plan.WorkspaceID, plan.ID, plan.Revision, plan.Name, string(plan.TemplateKind), string(plan.Protocol), ciphertext,
plan.BaseURL, string(modelsJSON), plan.DefaultModel, boolInt(plan.Enabled), string(detectionJSON), retiredFirstUseJSON,
unixMs(plan.CreatedAt), unixMs(plan.UpdatedAt), unixMs(time.Now().UTC()))
if err != nil {
return fmt.Errorf("put immutable model plan revision: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit model plan revision: %w", err)
}
return nil
}
func (s *SQLiteStore) DeleteModelPlan(ctx context.Context, workspaceID string, planID string) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
result, err := s.writeDB.ExecContext(ctx, `
DELETE FROM model_plans
WHERE workspace_id = ? AND plan_id = ?
`, workspaceID, planID)
if err != nil {
if isSQLiteForeignKeyConstraintError(err) {
return ErrModelPlanReferenced
}
return fmt.Errorf("delete model plan: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("delete model plan result: %w", err)
}
if affected == 0 {
return ErrModelPlanNotFound
}
return nil
}
func scanModelPlan(row managedProviderScanner) (modelplanbiz.Plan, error) {
var plan modelplanbiz.Plan
var revision int64
var templateKind string
var protocol string
var ciphertext string
var modelsJSON string
var enabled int
var detectionJSON string
var retiredFirstUseJSON string
var createdAtUnixMS int64
var updatedAtUnixMS int64
if err := row.Scan(&plan.WorkspaceID, &plan.ID, &revision, &plan.Name, &templateKind, &protocol, &ciphertext,
&plan.BaseURL, &modelsJSON, &plan.DefaultModel, &enabled, &detectionJSON, &retiredFirstUseJSON,
&createdAtUnixMS, &updatedAtUnixMS); err != nil {
return modelplanbiz.Plan{}, err
}
if revision <= 0 {
revision = 1
}
plan.Revision = uint64(revision)
apiKey, err := decryptManagedCredential(ciphertext)
if err != nil {
return modelplanbiz.Plan{}, err
}
plan.TemplateKind = modelplanbiz.TemplateKind(templateKind)
plan.Protocol = modelplanbiz.Protocol(protocol)
plan.APIKey = apiKey
plan.Enabled = enabled != 0
if err := json.Unmarshal([]byte(modelsJSON), &plan.Models); err != nil {
return modelplanbiz.Plan{}, fmt.Errorf("decode model plan models: %w", err)
}
if detectionJSON != "" && detectionJSON != "{}" {
if err := json.Unmarshal([]byte(detectionJSON), &plan.Detection); err != nil {
return modelplanbiz.Plan{}, fmt.Errorf("decode model plan detection: %w", err)
}
}
plan.CreatedAt = time.UnixMilli(createdAtUnixMS).UTC()
plan.UpdatedAt = time.UnixMilli(updatedAtUnixMS).UTC()
return plan, nil
}