Download internal/upstream/tasks.go from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/tasks.go
- Command line
-
hf download hf://spaces/a3216/gcli2api/internal/upstream/tasks.go
-
curl -L -o tasks.go https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/tasks.go
11.6 kB
| // tasks.go growth 域「任务」接口:列表查询 / 接受 / 领取奖励。 | |
| // | |
| // 来源:上游 scripts/task_common.py 实测口径(list_tasks/accept_tasks/claim_reward), | |
| // 集成进主程序后不再需要外部脚本。 | |
| // | |
| // 端点(chatBase,BillingHeaders): | |
| // - GET /v2/activity/growth/tasks 全量任务列表(含 progress/accept_status) | |
| // - POST /v2/activity/growth/tasks/accept {"task_codes":[...]} not_accepted → accepted | |
| // - POST /v2/activity/growth/tasks/reward/claim {"task_code":"..."} 完成态领奖 | |
| // | |
| // 语义要点: | |
| // - accept 是"报名",不产生进度;进度由服务端行为事件点亮(如 chat_request_send 上报、 | |
| // 真实对话),故 accept 后可幂等重放。 | |
| // - claim 仅在 progress 达标后可领;重复领返回业务错误(安全,不需要前置状态判断)。 | |
| package upstream | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "io" | |
| "net/http" | |
| "net/url" | |
| "github.com/linguo2625469/workbuddy2api-panel/internal/auth" | |
| ) | |
| // growth 域任务路径(与 scripts/task_common.py 对齐)。 | |
| const ( | |
| tasksListPath = "/v2/activity/growth/tasks" | |
| tasksAcceptPath = "/v2/activity/growth/tasks/accept" | |
| ) | |
| // mpPlatform 小程序口径头值:小程序限定任务(Sequential_Tasks_1 / school_season) | |
| // 的列表下发、accept、claim 全链路要求 X-Client-Platform: miniprogram。 | |
| const mpPlatform = "miniprogram" | |
| // Task 单个任务的对外视图(字段名与上游 JSON 对齐,多余字段不透出)。 | |
| type Task struct { | |
| TaskCode string `json:"task_code"` | |
| Title string `json:"title,omitempty"` | |
| Description string `json:"description,omitempty"` // 操作指引(含跳转说明) | |
| TaskDesc string `json:"task_desc,omitempty"` // 达成条件简述 | |
| Credit int64 `json:"credit,omitempty"` // 奖励积分(上游 reward_credit) | |
| Energy int64 `json:"energy,omitempty"` // 奖励能量(上游 reward_energy) | |
| HasReward bool `json:"has_reward,omitempty"` // 是否带奖励 | |
| RewardBuddy bool `json:"reward_buddy,omitempty"` | |
| TaskType string `json:"task_type,omitempty"` // single(一次性)/ 累计型 | |
| Tag string `json:"tag,omitempty"` // 端标记(PC 等) | |
| JumpURL string `json:"jump_url,omitempty"` // 客户端跳转协议(workbuddy://...) | |
| Locked bool `json:"locked,omitempty"` // 上游标记未解锁 | |
| Target int64 `json:"target"` // 目标次数(恒输出:0 是有效进度值) | |
| Current int64 `json:"current"` // 当前进度(恒输出:0 是有效进度值) | |
| AcceptStatus string `json:"accept_status,omitempty"` | |
| Status string `json:"status,omitempty"` // 上游任务状态(complete 等) | |
| Claimable bool `json:"claimable,omitempty"` // 进度达标且未领取(本地推算) | |
| Claimed bool `json:"claimed,omitempty"` // 已领取(accept_status == claimed) | |
| } | |
| // ListTasks 拉取全量任务列表(默认口径,无端标记头)。 | |
| // 响应形如 data.tasks[],元素字段随任务类型变化(progress 可能是 {current,target} 或平铺), | |
| // 这里做宽松解析:两种形状都尝试。 | |
| func (c *Client) ListTasks(a *auth.Auth) ([]Task, error) { | |
| data, err := c.growthJSON(a, http.MethodGet, tasksListPath, nil) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return parseGrowthTasks(data) | |
| } | |
| // ListTasksMP 拉取小程序口径的任务列表(X-Client-Platform: miniprogram)。 | |
| // 小程序限定任务(Sequential_Tasks_1「小程序首对话」/ school_season「校园日」等) | |
| // 仅在该口径下发——默认列表不出现,accept/claim 同样要求该头(缺头 accept 返回 | |
| // task not found,上游 task_runner 实测)。**实测 mp 列表是默认口径的超集** | |
| // (含 RichMeow/Model_chat 等常规任务 + wb_wechat_oa_subscribe_task 等 mp 专属), | |
| // 合并时调用方须按 task_code 去重。 | |
| func (c *Client) ListTasksMP(a *auth.Auth) ([]Task, error) { | |
| data, err := c.growthJSONMP(a, http.MethodGet, tasksListPath, nil) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return parseGrowthTasks(data) | |
| } | |
| // growthJSONMP 发 growth 域请求(小程序口径:叠加 X-Client-Platform: miniprogram) | |
| // 并解信封。语义同 growthJSON。 | |
| func (c *Client) growthJSONMP(a *auth.Auth, method, path string, body any) (json.RawMessage, error) { | |
| var rdr io.Reader | |
| if body != nil { | |
| raw, err := json.Marshal(body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| rdr = bytes.NewReader(raw) | |
| } | |
| req, err := http.NewRequest(method, c.chatBase(a)+path, rdr) | |
| if err != nil { | |
| return nil, err | |
| } | |
| c.BillingHeaders(req, a) | |
| req.Header.Set("X-Client-Platform", mpPlatform) | |
| return c.doJSON(req) | |
| } | |
| // AcceptTasksMP 接受小程序限定任务(mp 头;缺头实测 task not found)。 | |
| // 幂等语义同 AcceptTasks。 | |
| func (c *Client) AcceptTasksMP(a *auth.Auth, taskCodes []string) error { | |
| _, err := c.growthJSONMP(a, http.MethodPost, tasksAcceptPath, map[string]any{"task_codes": taskCodes}) | |
| return err | |
| } | |
| // ClaimRewardMP 领取小程序限定任务奖励:chat 域 /activity/growth/tasks/{code}/claim | |
| // + mp 头(上游 task_runner claim_one(mp=True) 同款);chat 域 400 时降级 Web 域 | |
| // 领奖端点(ClaimReward,x-client-platform: web 形态)。返回 (credit, energy, err)。 | |
| func (c *Client) ClaimRewardMP(a *auth.Auth, taskCode string) (credit, energy int64, err error) { | |
| req, err := http.NewRequest(http.MethodPost, | |
| c.chatBase(a)+"/activity/growth/tasks/"+url.PathEscape(taskCode)+"/claim", nil) | |
| if err != nil { | |
| return 0, 0, err | |
| } | |
| c.BillingHeaders(req, a) | |
| req.Header.Set("X-Client-Platform", mpPlatform) | |
| data, err := c.doJSON(req) | |
| if err != nil { | |
| // chat 域对该路径 400(部分任务/租户形态)→ Web 域降级(已实测可领)。 | |
| if ue, ok := err.(*Error); ok && ue.Status == http.StatusBadRequest { | |
| return c.ClaimReward(a, taskCode) | |
| } | |
| return 0, 0, err | |
| } | |
| return parseClaimReward(data) | |
| } | |
| // parseClaimReward 解析领奖响应 data:{"already_claimed":bool,"credit":n,"energy":n}。 | |
| func parseClaimReward(data json.RawMessage) (credit, energy int64, err error) { | |
| var resp struct { | |
| AlreadyClaimed bool `json:"already_claimed"` | |
| Credit int64 `json:"credit"` | |
| Energy int64 `json:"energy"` | |
| } | |
| if err := json.Unmarshal(data, &resp); err != nil { | |
| return 0, 0, err | |
| } | |
| return resp.Credit, resp.Energy, nil | |
| } | |
| // parseGrowthTasks 解析 growth 任务列表 data.tasks[](默认与 mp 口径共用)。 | |
| func parseGrowthTasks(data json.RawMessage) ([]Task, error) { | |
| var resp struct { | |
| Tasks []struct { | |
| TaskCode string `json:"task_code"` | |
| Title string `json:"title"` | |
| Description string `json:"description"` | |
| TaskDesc string `json:"task_desc"` | |
| RewardCredit int64 `json:"reward_credit"` // 上游实际字段名(reward_ 前缀) | |
| RewardEnergy int64 `json:"reward_energy"` | |
| HasReward bool `json:"has_reward"` | |
| RewardBuddy bool `json:"reward_buddy"` | |
| TaskType string `json:"task_type"` | |
| Tag string `json:"tag"` | |
| JumpURL string `json:"jump_url"` | |
| Locked bool `json:"locked"` | |
| AcceptStatus string `json:"accept_status"` | |
| Status string `json:"status"` | |
| Target int64 `json:"target"` | |
| Current int64 `json:"current"` | |
| Progress json.RawMessage `json:"progress"` | |
| } `json:"tasks"` | |
| } | |
| if err := json.Unmarshal(data, &resp); err != nil { | |
| return nil, err | |
| } | |
| out := make([]Task, 0, len(resp.Tasks)) | |
| for _, t := range resp.Tasks { | |
| cur, tgt := t.Current, t.Target | |
| // progress 可能是 {current,target} 对象(实测口径),覆盖平铺字段。 | |
| if len(t.Progress) > 0 && string(t.Progress) != "null" { | |
| var pr struct { | |
| Current int64 `json:"current"` | |
| Target int64 `json:"target"` | |
| } | |
| if json.Unmarshal(t.Progress, &pr) == nil && (pr.Target > 0 || pr.Current > 0) { | |
| cur, tgt = pr.Current, pr.Target | |
| } | |
| } | |
| claimed := t.AcceptStatus == "claimed" | |
| out = append(out, Task{ | |
| TaskCode: t.TaskCode, | |
| Title: t.Title, | |
| Description: t.Description, | |
| TaskDesc: t.TaskDesc, | |
| Credit: t.RewardCredit, | |
| Energy: t.RewardEnergy, | |
| HasReward: t.HasReward, | |
| RewardBuddy: t.RewardBuddy, | |
| TaskType: t.TaskType, | |
| Tag: t.Tag, | |
| JumpURL: t.JumpURL, | |
| Locked: t.Locked, | |
| Target: tgt, | |
| Current: cur, | |
| AcceptStatus: t.AcceptStatus, | |
| Status: t.Status, | |
| Claimable: !claimed && tgt > 0 && cur >= tgt, | |
| Claimed: claimed, | |
| }) | |
| } | |
| return out, nil | |
| } | |
| // AcceptTasks 接受任务(幂等:已 accepted 时上游返回成功或业务提示,均不视为致命错误)。 | |
| func (c *Client) AcceptTasks(a *auth.Auth, taskCodes []string) error { | |
| _, err := c.growthJSON(a, http.MethodPost, tasksAcceptPath, map[string]any{"task_codes": taskCodes}) | |
| return err | |
| } | |
| // ClaimReward 领取单个任务奖励。 | |
| // | |
| // 端点来源(实测):Web 成长中心的领奖请求 —— | |
| // | |
| // POST https://www.workbuddy.cn/activity/growth/tasks/<task_code>/claim | |
| // (任务码在**路径**里,无 body;带 x-client-platform: web 头,Bearer 鉴权) | |
| // | |
| // 关键区别:此前误用 CLI 域 copilot.tencent.com 的 | |
| // /v2/activity/growth/tasks/reward/claim(task_code 放 body),该路径**不存在**, | |
| // 一直返回 400 "task not completed",是此前领奖失败的真实原因。 | |
| // 本实现返回 (credit, energy, err):credit/energy 为本次到账奖励(已领取过时为 0)。 | |
| func (c *Client) ClaimReward(a *auth.Auth, taskCode string) (credit, energy int64, err error) { | |
| req, err := http.NewRequest(http.MethodPost, | |
| c.webBase(a)+"/activity/growth/tasks/"+url.PathEscape(taskCode)+"/claim", nil) | |
| if err != nil { | |
| return 0, 0, err | |
| } | |
| // Web 端请求头形状(对照浏览器实际请求):Origin/Referer 指向 workbuddy.cn 成长中心, | |
| // 带 x-client-platform: web 标记来源端。 | |
| req.Header.Set("Authorization", "Bearer "+a.AccessTokenValue()) | |
| req.Header.Set("Accept", "application/json, text/plain, */*") | |
| req.Header.Set("Content-Type", "application/json") | |
| req.Header.Set("Origin", "https://www.workbuddy.cn") | |
| req.Header.Set("Referer", "https://www.workbuddy.cn/profile/growth-center") | |
| req.Header.Set("x-client-platform", "web") | |
| if ua := c.userAgent(a); ua != "" { | |
| req.Header.Set("User-Agent", ua) | |
| } | |
| if a.UID != "" { | |
| req.Header.Set("X-User-Id", a.UID) | |
| } | |
| if a.EnterpriseID != "" { | |
| req.Header.Set("X-Enterprise-Id", a.EnterpriseID) | |
| req.Header.Set("X-Tenant-Id", a.EnterpriseID) | |
| } | |
| if d := a.DomainValue(); d != "" { | |
| req.Header.Set("X-Domain", d) | |
| } | |
| data, err := c.doJSON(req) | |
| if err != nil { | |
| return 0, 0, err | |
| } | |
| // 响应 data:{"already_claimed":bool,"credit":100,"energy":5,...} | |
| var resp struct { | |
| AlreadyClaimed bool `json:"already_claimed"` | |
| Credit int64 `json:"credit"` | |
| Energy int64 `json:"energy"` | |
| } | |
| if err := json.Unmarshal(data, &resp); err != nil { | |
| return 0, 0, err | |
| } | |
| if resp.AlreadyClaimed { | |
| return 0, 0, nil // 幂等:重复领取不算错误,但无新增奖励 | |
| } | |
| return resp.Credit, resp.Energy, nil | |
| } | |