package syncer import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time" ) // RestartSpace 请求 Hugging Face 重启一个 Space。 // // 为什么需要它:Space 上的网关只在**启动时**扫一遍凭证目录(没有文件监听), // 所以往桶里推了新账号之后,正在运行的 Space 并不会加载它——签到时候就会漏掉这个号。 // 重启一次(30~60s 冷启动)即可让它从挂载卷重新读齐。 // // 这也是配置成"只在推了 auths/ 下的文件时才做"的原因:改 state.json 之类不必重启, // 白白打断正在进行中的签到任务。 func RestartSpace(ctx context.Context, endpoint, spaceID, token string, timeout time.Duration, insecureSkipVerify bool) error { if strings.TrimSpace(spaceID) == "" { return fmt.Errorf("restart_space 未配置") } if strings.TrimSpace(endpoint) == "" { endpoint = DefaultEndpoint } if timeout <= 0 { timeout = 60 * time.Second } endpoint = strings.TrimRight(endpoint, "/") u := fmt.Sprintf("%s/api/spaces/%s/restart", endpoint, strings.Trim(spaceID, "/")) req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil) if err != nil { return err } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } req.Header.Set("User-Agent", userAgent) cli := newHTTPClient(timeout, insecureSkipVerify, "") resp, err := cli.Do(req) if err != nil { // 把 URL 里的信息去掉,避免日志里出现带着鉴权语义的完整地址。 return fmt.Errorf("请求重启 Space 失败: %w", err) } defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("重启 Space 返回 %d: %s", resp.StatusCode, truncate(string(body), 300)) } return nil } // SpaceIDFromBucket 不是必须的辅助函数,但留一个把桶 ID 归一化的入口, // 便于调用方传 "a3216/wb2api-storage" 或 "https://huggingface.co/..." 两种写法。 func SpaceIDFromBucket(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if strings.Contains(raw, "://") { if u, err := url.Parse(raw); err == nil { raw = strings.Trim(u.Path, "/") } } return strings.Trim(raw, "/") }