gcli2api / internal /upstream /school.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
9.84 kB
// school.go 开学季活动(school-season,活动期 2026-09-13 ~ 09-24)纯 API 自动化。
//
// 判据(2026-09-13 小程序 MCP 逆向 + 三账号实测,protocol.md §7.11):
// - share_invite(每日 +100c +1抽奖):POST /tasks/share-complete {channel:"wechat"}
// 即点亮——纯前端上报,服务端不校验真实分享回执。本模块的主目标。
// - chat_3_times / expert_use:判据绑定小程序原生沙箱会话(e2b runtime),
// webchat 普通会话不计数,纯 API 不做(需小程序内人工对话)。
// - 抽奖:POST /wheel/draw {draw_uuid}(前端生成 uuid,消耗 1 chance)。
//
// 端点基址 www.codebuddy.cn(billing 同域);信封 {code,msg,data},code=0 成功。
package upstream
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/linguo2625469/workbuddy2api-panel/internal/auth"
)
const schoolBase = "/portal/activity/school"
// schoolJSON 学院活动 API 请求(剥信封,业务 code≠0 返回带 msg 的 error)。
func (c *Client) schoolJSON(a *auth.Auth, method, path string, body map[string]any, out any) error {
var raw []byte
if body != nil {
raw, _ = json.Marshal(body)
}
req, err := http.NewRequest(method, c.billingBase(a)+schoolBase+path, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+a.AccessTokenValue())
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if a.UID != "" {
req.Header.Set("X-User-Id", a.UID)
}
data, err := c.doJSON(req)
if err != nil {
return err
}
if out != nil {
return json.Unmarshal(data, out)
}
return nil
}
// - chat_3_times:3 条 chat_request_send 即 3/3(conversationId 任意、桌面/mp
// 头族均可计数,无需真实沙箱会话)。
// - expert_use:mp 指纹事件链 expert_summon_click + expert_summoned +
// expert_actual_use + chat_request_send(开学季分类专家)即点亮。
const mpReportPath = "/v2/report"
// schoolOpenDayActivityID 开学季/校园日活动 id(事件 activityId 字段值,两域共用)。
const schoolOpenDayActivityID = "school_open_day_2026"
// mpEventBase 小程序埋点公共指纹(appservice wQ()+Ao() 对齐)。
func mpEventBase(a *auth.Auth) map[string]any {
return map[string]any{
"timestamp": time.Now().UnixMilli(),
"ideType": "WorkBuddy_MP",
"ideVersion": "2.4.0",
"extName": "workbuddy-mp",
"extVersion": "2.4.0",
"product": "SaaS",
"ideName": "wx_app_cloud",
"platform": "mini_program",
"os": "windows",
"osVersion": "11",
"arch": "x64",
"machineId": "0655736a-607f-4d9d-b430-58176ee9a090",
"timezone": "Asia/Shanghai",
"userId": a.UID,
"userNickname": a.Nickname,
}
}
// ReportMPEvent 以小程序指纹向 www.codebuddy.cn/v2/report 批量上报事件。
func (c *Client) ReportMPEvent(a *auth.Auth, events ...map[string]any) error {
if len(events) == 0 {
return fmt.Errorf("mp report: no events")
}
base := mpEventBase(a)
arr := make([]map[string]any, 0, len(events))
for _, ev := range events {
m := map[string]any{}
for k, v := range base {
m[k] = v
}
for k, v := range ev {
m[k] = v
}
arr = append(arr, m)
}
raw, err := json.Marshal(arr)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, c.BillingBaseCN+mpReportPath, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+a.AccessTokenValue())
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if a.UID != "" {
req.Header.Set("X-User-Id", a.UID)
}
req.Header.Set("X-Client-Product", "workbuddy-mp")
req.Header.Set("X-Client-Version", "2.4.0")
req.Header.Set("X-Client-Platform", "mp-weixin")
req.Header.Set("X-Platform", "wechatmp")
_, err = c.doJSON(req)
return err
}
// SchoolChatTimesEvents 构造一条 chat_request_send 事件(chat_3_times 计数)。
func SchoolChatTimesEvents(conversationID string) map[string]any {
rid := "wb2api-" + clientToken()
return map[string]any{
"eventCode": "chat_request_send",
"inputLength": 14, "isPlan": false, "isAutoExecuteTerminal": false,
"isAutoModify": false, "codebaseEnable": false, "maxToken": 0,
"maxSteps": 500, "temperature": 0, "maxRetries": 0,
"mentionContexts": []any{}, "knowledgeId": []any{}, "knowledgeName": []any{},
"codebaseId": "", "mentionContextCount": 0, "command": "",
"recommendId": "", "skillId": "", "skillCount": 0, "totalCount": 0,
"traceId": rid, "rootRequestId": rid,
"parentConversationId": conversationID, "conversationId": conversationID,
"messageId": "msg-" + rid[len(rid)-8:],
"agentName": "mp", "agentType": "main",
"codebuddy.session_id": conversationID,
"codebuddy.conversation_request_id": rid,
}
}
// SchoolSeasonChatEvent 构造 growth 域「校园日」(school_season)判据事件:
// mini 指纹 chat_request_send + activityId=school_open_day_2026(与 school 域
// 开学季同 activityId 关联;实测无 activityId 的事件不点亮)。事件形状与
// SchoolChatTimesEvents 同构(school 域 chat_3_times 同款),仅追加 activityId。
func SchoolSeasonChatEvent(conversationID string) map[string]any {
ev := SchoolChatTimesEvents(conversationID)
ev["activityId"] = schoolOpenDayActivityID
return ev
}
// MiniExpertUseEvent 构造 growth 域 Sequential_Tasks_2「在小程序内选中专家并完成
// 有效对话」的判据事件:mp 指纹 expert_actual_use。形状对齐小程序源码
// app-service.js 的真实发射点(上游 task_runner 实测 2026-09-23:上报即 completed,
// claim +200c+5e)。与 school 域的 SchoolExpertUseEvents 是**两套口径**,勿照抄:
// - 不带 conversationId/activityId——真实事件就是这两个字段都不带;
// - extVersion 用小程序自身版本 2.2.8(覆盖 mpEventBase 的 2.4.0);
// - source=mini_program + type 固定 "send_message"(小程序恒发此值)。
//
// expertID 必须是专家市场真实 ex_ id(ListMarketExperts),空 id 服务端不入账。
func MiniExpertUseEvent(expertID, expertName, expertType string) map[string]any {
if expertType == "" {
expertType = "agent"
}
if expertName == "" {
expertName = expertID
}
return map[string]any{
"eventCode": "expert_actual_use", "reportDelay": 0,
"extVersion": "2.2.8", "source": "mini_program",
"id": expertID, "name": expertID,
"expertTitle": expertName, "type": "send_message",
"characterCount": 12, "expertType": expertType,
}
}
// MiniChatModelEvent mp 对话事件 + 模型字段(Sequential_Tasks_5「使用 GLM5.2」判据
// 载体):小程序 chat_request_send 真实发射点(mpsrc main 32904 模块)带
// requestModelId / requestModelName——Tasks_1/3 的裸对话事件不带模型,模型任务
// 须用本形态(判据待解锁实测验证)。
func MiniChatModelEvent(conversationID, modelID, modelName string) map[string]any {
ev := SchoolChatTimesEvents(conversationID)
ev["requestModelId"] = modelID
ev["requestModelName"] = modelName
return ev
}
// MiniPlaybookEvents mp 指纹灵感事件组(Sequential_Tasks_7「体验灵感功能」判据
// 载体,形状对齐 mpsrc main 73640/73665 发射点:playbook_cta_click →
// playbook_prompt_send)。issue #42 称该任务为 PC 口径(+500c+5e)——PC 序列
// (DesktopPlaybookPromptSequence)已实测点亮 playbook_prompt,本组作为 mp 形态
// 补充(任务在 mp 链上,判据究竟认哪侧待解锁实测)。
func MiniPlaybookEvents(caseID, caseName string) []map[string]any {
base := map[string]any{
"id": caseID, "name": caseName, "type": "document",
"categoryId": "", "categoryName": "",
"skills": "", "skillNames": "",
}
cta := map[string]any{
"eventCode": "playbook_cta_click", "source": "discover", "position": 1,
"extVersion": "2.2.8",
}
for k, v := range base {
cta[k] = v
}
send := map[string]any{
"eventCode": "playbook_prompt_send", "source": "discover",
"promptLength": 96, "isOfficial": 1,
"conversationId": "wb2api-mp-pb-" + clientToken(),
"extVersion": "2.2.8",
}
for k, v := range base {
send[k] = v
}
return []map[string]any{cta, send}
}
// ---- 我的券码(#/prizes?tab=vouchers,2026-09-16 接入)----
// SchoolVoucher 开学季抽奖抽中的第三方券(KFC/瑞幸/酷狗等)。
// 字段结构按真实响应样本:GET /vouchers 单次拉全(无分页),data.items[]。
type SchoolVoucher struct {
GrantID int64 `json:"grant_id"`
DrawUUID string `json:"draw_uuid,omitempty"`
SKUCode string `json:"sku_code,omitempty"` // kfc_ice_cream / voucher_luckin / voucher_kugou …
PrizeName string `json:"prize_name,omitempty"` // 肯德基冰淇淋
Code string `json:"code"` // 券码本体(复制给店员核销)
ValidFrom string `json:"valid_from,omitempty"` // 上游常为空
ValidTo string `json:"valid_to,omitempty"` // "2026-10-24"
GrantedAt string `json:"granted_at,omitempty"` // RFC3339
}
// SchoolVouchers 查询账号的开学季券码列表(只读)。
// 抽到积分的记录不在此端点(那是 /rewards 的 type=credit 条目)。
func (c *Client) SchoolVouchers(a *auth.Auth) ([]SchoolVoucher, error) {
var out struct {
Items []SchoolVoucher `json:"items"`
}
if err := c.schoolJSON(a, http.MethodGet, "/vouchers", nil, &out); err != nil {
return nil, err
}
return out.Items, nil
}