gcli2api / scripts /task_common.py
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
9.61 kB
#!/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]