File size: 9,609 Bytes
6d60378 8bab601 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | #!/usr/bin/env python3
"""一次性积分任务脚本的共享函数库.
与 probe_active.py 同风格:从 auths/ 读账号凭证,封装 growth 域 / report 域
请求,供 task_*.py 复用。全部默认 dry-run(写动作由调用脚本显式 --yes 放行)。
端点权威来源(Go 代码实测 + 本次实测确认):
- chat 域(copilot.tencent.com):growth / tasks / buddy / streak / chat/completions
- billing 域(www.codebuddy.cn):/v2/report
- accept : POST /v2/activity/growth/tasks/accept {"task_codes":[code]}
- claim : POST /v2/activity/growth/tasks/reward/claim {"task_code":code}
"""
import json, os, time, glob, urllib.request, urllib.error
AUTHS = "/root/workbuddy2api/auths"
CHAT_BASE = "https://copilot.tencent.com" # growth / tasks / buddy / streak / chat
BILL_BASE = "https://www.codebuddy.cn" # report / billing
# growth 域常量(travel.go / report.go 与本次实测对齐)
PATH_LIST_TASKS = "/v2/activity/growth/tasks"
PATH_ACCEPT_TASKS = "/v2/activity/growth/tasks/accept"
PATH_CLAIM_REWARD = "/v2/activity/growth/tasks/reward/claim"
PATH_BUDDY_FIRST = "/activity/growth/buddy/first"
PATH_BUDDY_AGREEMENT = "/activity/growth/buddy/agreement"
PATH_STREAK = "/activity/growth/streak"
PATH_REPORT = "/v2/report"
PATH_CHAT = "/v2/chat/completions"
CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2"
def load_auth(uid_or_file: str) -> dict:
"""从 auths/ 加载账号凭证,uid_or_file 为 uid 前缀或 auths 文件名。
返回 {token, uid, domain, nick, file} 五元组。
"""
if os.path.sep in uid_or_file or uid_or_file.endswith(".json"):
p = uid_or_file
if not os.path.isabs(p):
p = os.path.join(AUTHS, p)
else:
pre = uid_or_file
hits = glob.glob(os.path.join(AUTHS, f"workbuddy-{pre}*.json"))
if not hits:
raise SystemExit(f"no auth for {pre}")
p = hits[0]
# encoding 必须显式指定:Windows 上 open() 默认用 locale 代码页(中文系统为
# GBK),而 auth 文件是 UTF-8,非 ASCII 昵称会 UnicodeDecodeError 中断全部任务。
d = json.load(open(p, encoding="utf-8"))
a, acc = d["auth"], d["account"]
return {"token": a["accessToken"], "domain": a.get("domain") or "",
"uid": acc["uid"], "nick": acc.get("nickname", ""),
"file": os.path.basename(p)}
def chat_base(auth: dict) -> str:
return auth.get("chat_base") or CHAT_BASE
def billing_base(auth: dict) -> str:
return auth.get("billing_base") or BILL_BASE
def _headers(auth: dict) -> dict:
hdr = {"Authorization": "Bearer " + auth["token"],
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": CLIENT_UA,
"Origin": "https://www.codebuddy.cn",
"Referer": "https://www.codebuddy.cn/"}
if auth.get("uid"):
hdr["X-User-Id"] = auth["uid"]
if auth.get("domain"):
hdr["X-Domain"] = auth["domain"]
return hdr
def _request(auth, method, base, path, body=None, headers=None, timeout=30):
url = path if path.startswith("http") else base + path
hdr = _headers(auth)
if headers:
hdr.update(headers)
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=hdr, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read().decode("utf-8", "replace"))
except urllib.error.HTTPError as e:
t = e.read().decode("utf-8", "replace")
try:
return e.code, json.loads(t)
except Exception:
return e.code, {"raw": t[:300]}
except Exception as e:
return -1, {"err": repr(e)}
def do_get(auth, base, path, headers=None) -> tuple:
"""GET 读请求,返回 (status, dict)。"""
return _request(auth, "GET", base, path, None, headers)
def do_post(auth, base, path, body, headers=None) -> tuple:
"""POST 写请求,返回 (status, dict)。body 为 dict。"""
return _request(auth, "POST", base, path, body, headers)
def list_tasks(auth) -> list:
"""GET /v2/activity/growth/tasks 全量任务列表(元素为原始 dict)。"""
st, d = do_get(auth, chat_base(auth), PATH_LIST_TASKS)
if st != 200:
raise RuntimeError(f"list_tasks http={st}")
tasks = (d.get("data", {}) or {}).get("tasks") or []
return tasks
def task_status(auth, task_code) -> dict | None:
"""查单个任务当前状态;找不到返回 None。"""
for t in list_tasks(auth):
if t.get("task_code") == task_code:
return t
return None
def accept_tasks(auth, task_codes) -> tuple:
"""POST accept 任务(not_accepted → accepted)。返回 (status, resp)。"""
return do_post(auth, chat_base(auth), PATH_ACCEPT_TASKS,
{"task_codes": task_codes})
def claim_reward(auth, task_code) -> tuple:
"""POST claim 领取奖励(任务已 complete 后可领)。重复领返回业务错误,安全。"""
return do_post(auth, chat_base(auth), PATH_CLAIM_REWARD,
{"task_code": task_code})
def get_streak(auth) -> int:
"""GET /activity/growth/streak 连登天数(只读 oracle)。失败返回 -1 记日志。"""
st, d = do_get(auth, chat_base(auth), PATH_STREAK)
if st != 200:
return -1
return (d.get("data", {}).get("streak", {}) or {}).get("days", 0)
def chat_event(auth, conversation_id=None, model_id="deepseek-v4-flash",
model_name="DeepSeek V4 Flash", mode="craft"):
"""客户端 chat_request_send 事件完整形状(照抄 report.go / probe_active.py)。
必须带 userId(=账号 uid),缺失则服务端 200 但静默丢弃。
model_id/name 可换(如 GLM-5.2),供 model_chat 对齐实际模型。
"""
now = int(time.time() * 1000)
cid = conversation_id or f"task-{now}"
return {"eventCode": "chat_request_send", "timestamp": now, "reportDelay": 0,
"mode": mode, "conversationId": cid, "requestId": cid,
"inputLength": 12, "requestModelId": model_id,
"requestModelName": model_name, "isPlan": False,
"isAutoExecuteTerminal": False, "isAutoModify": False,
"codebaseEnable": False, "maxToken": 0, "maxSteps": 0, "temperature": 0,
"maxRetries": 0, "mentionContexts": [], "knowledgeId": [],
"knowledgeName": [], "codebaseId": "", "mentionContextCount": 0,
"command": "", "expertId": "", "recommendId": "", "skillId": "",
"skillCount": 0, "totalCount": 0, "fileUri": "", "presentAt": now,
"traceId": "", "rootRequestId": cid, "parentConversationId": cid,
"agentName": "default", "agentType": "conversation", "userId": auth["uid"]}
def report_activity(auth, count=1, gap=1.05, model_id="deepseek-v4-flash",
model_name="DeepSeek V4 Flash", mode="craft") -> list:
"""向 {billing}/v2/report 上报 count 条 chat_request_send。
每次间隔 >= gap 秒(默认 1.05,匹配 probe_active.py 的同接口限速口径)。
返回 [(status, code), ...] 汇总。
"""
out = []
for i in range(count):
ev = chat_event(auth, model_id=model_id, model_name=model_name, mode=mode)
st, r = do_post(auth, billing_base(auth), PATH_REPORT, [ev])
out.append((st, r.get("code") if isinstance(r, dict) else None))
if i < count - 1:
time.sleep(gap)
return out
def chat_completion(auth, model_id="glm-5.2", prompt="hi", max_tokens=32,
timeout=60) -> tuple:
"""POST {chat}/v2/chat/completions 真实对话一次(stream:true)。
服务端强制流式(payload.go 同款口径),这里逐行读 SSE 直到 done。
返回 (status, first_content)。用于 Model_chat_GLM5.2 的“真实对话一次”。
"""
body = {"model": model_id, "messages": [{"role": "user", "content": prompt}],
"stream": True, "max_tokens": max_tokens}
hdr = {"Accept": "text/event-stream"} # SSE
url = chat_base(auth) + PATH_CHAT
req_headers = _headers(auth)
req_headers.update(hdr)
req = urllib.request.Request(url, data=json.dumps(body).encode(),
headers=req_headers, method="POST")
first = ""
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
status = r.status
for raw in r:
line = raw.decode("utf-8", "replace")
if line.startswith("data: "):
payload = line[6:].strip()
if payload in ("[DONE]", ""):
continue
try:
obj = json.loads(payload)
delta = (obj.get("choices") or [{}])[0].get("delta") or {}
content = delta.get("content") or ""
if content and not first:
first = content
except Exception:
pass
return status, first
except urllib.error.HTTPError as e:
t = e.read().decode("utf-8", "replace")
return e.code, t[:200]
except Exception as e:
return -1, repr(e)[:200] |