feat: add Z12/Z13 bind APIs, stock import, and sync docs

Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
This commit is contained in:
whm
2026-08-05 11:47:20 +08:00
parent b04b180d30
commit cb56e6847e
31 changed files with 1882 additions and 91 deletions

View File

@@ -0,0 +1,85 @@
package meta
import (
"context"
"encoding/json"
"fmt"
"log"
)
// BackfillImportResult Z9e 扫库补齐 import/export 的结果。
type BackfillImportResult struct {
Scanned int `json:"scanned"`
Updated int `json:"updated"`
Skipped int `json:"skipped"`
DryRun bool `json:"dry_run"`
Slugs []string `json:"updated_slugs,omitempty"`
Message string `json:"message,omitempty"`
}
// PublishedLister 列出全部已发布应用(跨租户,供 Z9e 启动扫库)。
type PublishedLister interface {
ListPublishedApps(ctx context.Context) ([]*AppRecord, error)
}
// BackfillDefaultImportExport 遍历已发布蓝图,对可写 resource 补齐 import/export 并落库Z9e
// dryRun=true 只统计不写库。tenantID>0 时仅处理该租户。
func BackfillDefaultImportExport(ctx context.Context, store Store, dryRun bool) (*BackfillImportResult, error) {
return BackfillDefaultImportExportForTenant(ctx, store, 0, dryRun)
}
// BackfillDefaultImportExportForTenant 同 BackfillDefaultImportExporttenantID=0 表示全库。
func BackfillDefaultImportExportForTenant(ctx context.Context, store Store, tenantID int64, dryRun bool) (*BackfillImportResult, error) {
if store == nil {
return nil, fmt.Errorf("meta store nil")
}
lister, ok := store.(PublishedLister)
if !ok {
return nil, fmt.Errorf("meta store does not support ListPublishedApps")
}
apps, err := lister.ListPublishedApps(ctx)
if err != nil {
return nil, err
}
out := &BackfillImportResult{DryRun: dryRun, Slugs: make([]string, 0)}
for _, app := range apps {
if app == nil || app.Blueprint == nil {
continue
}
if tenantID > 0 && app.TenantID != tenantID {
continue
}
out.Scanned++
before, _ := json.Marshal(app.Blueprint)
app.Blueprint.EnsureDefaultImportExport()
after, _ := json.Marshal(app.Blueprint)
if string(before) == string(after) {
out.Skipped++
continue
}
out.Updated++
out.Slugs = append(out.Slugs, fmt.Sprintf("%d/%s", app.TenantID, app.Slug))
if dryRun {
continue
}
if err := store.Save(ctx, app); err != nil {
return out, fmt.Errorf("save %s: %w", app.Slug, err)
}
}
out.Message = fmt.Sprintf("scanned=%d updated=%d skipped=%d dry_run=%v", out.Scanned, out.Updated, out.Skipped, dryRun)
return out, nil
}
// RunBackfillDefaultImportExportOnBoot 启动时扫库(失败只打日志,不阻断启动)。
func RunBackfillDefaultImportExportOnBoot(ctx context.Context, store Store) {
res, err := BackfillDefaultImportExport(ctx, store, false)
if err != nil {
log.Printf("Z9e backfill import ops: %v", err)
return
}
if res != nil && res.Updated > 0 {
log.Printf("Z9e backfill import ops: %s slugs=%v", res.Message, res.Slugs)
} else if res != nil {
log.Printf("Z9e backfill import ops: %s", res.Message)
}
}