feat: 抓取任务队列 + 媒体机直传 + 向量回填
crawl(品牌抓取任务队列) - /admin/internal/crawl/tasks 领取 / :id/result 回报,spider 可常驻拉任务 - 后台品牌列表支持「加入抓取队列」,队列页可看状态与重试 - 租约超时的 running 自动回收为 queued,避免 worker 崩溃后任务卡死 media(媒体机直传) - ingest 载荷新增 main_key/detail_keys/main_size/detail_sizes: 有 key 就直接落库,跳过下载与上传;与 URL 形态可逐图混用 - 细节图按 Details/DetailKeys 两者较长者遍历(只给 key 时不再丢图) - 媒体机上传的 key 不进失败回滚清单(内容寻址、可能与他人共享) - /admin/internal/upload-url 签发 S4 PUT 预签名(spider 不持有 AK/SK) embeddings(向量回填) - GET /admin/internal/embeddings/pending 派发缺向量的图(含可直接下载的 url) - POST /admin/internal/embeddings 回写向量并重算近邻;失败项计入 image_embedding_failures,连续失败到上限后退出队列(坏图不占位) - 近邻重算改为异步串行:同步会让 100 张的批次 POST 超时(实测 >60s) - 新增 key 分支单测:混用 key/url、细节图只给 key、字节数、回滚清单、向量占位
This commit is contained in:
@ -65,6 +65,16 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
|
||||
internal.Use(middleware.IngestAuth(*opt.IngestAuth))
|
||||
internal.POST("/ingest", opt.Ingest.Submit)
|
||||
internal.GET("/crawl/brands", opt.Ingest.CrawlBrands)
|
||||
// spider 服务化:领取品牌抓取任务 / 回报结果(与 ingest 共用同一套 HMAC 验签)。
|
||||
internal.GET("/crawl/tasks", opt.Backstage.WorkerTask)
|
||||
internal.POST("/crawl/tasks/:id/result", opt.Backstage.WorkerReport)
|
||||
// 媒体机直传:签发 S4 PUT 预签名 URL(媒体机不持有 AK/SK)。
|
||||
internal.POST("/upload-url", opt.Backstage.WorkerUploadURL)
|
||||
// 向量回填:媒体机直传后后端不碰图片字节,向量由外部 worker 拉图编码后回传。
|
||||
// GET /embeddings/pending → 缺向量的图片(含可直接下载的 url)
|
||||
// POST /embeddings → 回传向量(写库 + 重算近邻)与失败(累加计数)
|
||||
internal.GET("/embeddings/pending", opt.Backstage.WorkerPendingEmbeddings)
|
||||
internal.POST("/embeddings", opt.Backstage.WorkerEmbeddings)
|
||||
} else {
|
||||
r.POST("/admin/internal/ingest", func(c *gin.Context) {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "ingest disabled"})
|
||||
@ -118,6 +128,14 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
|
||||
admin.POST("/street-snaps/:id/images/:img/delete", opt.Backstage.StreetSnapImageDelete)
|
||||
admin.POST("/street-snaps/:id/cover", opt.Backstage.StreetSnapSetCover)
|
||||
admin.POST("/street-snaps/:id/toggle", opt.Backstage.StreetSnapToggleDeleted)
|
||||
// 品牌抓取任务控制面:后台决定爬哪些品牌,spider 服务来领取执行。
|
||||
// 列表 + 批量按前缀入队 + 单品牌入队 + 取消/重试/删除。
|
||||
admin.GET("/crawl", opt.Backstage.CrawlList)
|
||||
admin.POST("/crawl/bulk", opt.Backstage.CrawlBulk)
|
||||
admin.POST("/crawl/brands/:uid", opt.Backstage.CrawlEnqueue)
|
||||
admin.POST("/crawl/tasks/:id/retry", opt.Backstage.CrawlRetry)
|
||||
admin.POST("/crawl/tasks/:id/cancel", opt.Backstage.CrawlCancel)
|
||||
admin.POST("/crawl/tasks/:id/delete", opt.Backstage.CrawlDelete)
|
||||
// 爬虫入库任务监控:列出处理进度 + 失败重试
|
||||
admin.GET("/ingest-jobs", opt.Backstage.IngestJobs)
|
||||
admin.POST("/ingest-jobs/:id/retry", opt.Backstage.IngestJobRetry)
|
||||
|
||||
@ -85,6 +85,11 @@ func (f *fakeBrand) CrawlTasks(ctx context.Context, brandID uint32) ([]dto.Crawl
|
||||
return []dto.CrawlBrand{{BrandUID: "001DESke", Name: "Acne Studios"}}, nil
|
||||
}
|
||||
|
||||
// StatsByIDs 后台品牌列表的数据量统计(测试里返回空表即可)。
|
||||
func (f *fakeBrand) StatsByIDs(ctx context.Context, ids []uint32) (map[uint32]repository.BrandStats, error) {
|
||||
return map[uint32]repository.BrandStats{}, nil
|
||||
}
|
||||
|
||||
// 编译期接口满足性检查
|
||||
var (
|
||||
_ service.ArticleService = (*fakeArticle)(nil)
|
||||
|
||||
Reference in New Issue
Block a user