mslxdff 0.1.225 → 0.1.226
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli_help_mini.md +4 -1
- package/package.json +1 -1
- package/src/cli/commands/provider/index.js +2 -0
- package/src/cli/commands/provider/workbuddy-quota.js +154 -0
- package/src/providers/workbuddy/activity.js +204 -0
- package/src/providers/workbuddy/desktop-report.js +286 -0
- package/src/providers/workbuddy/expert-tasks.js +205 -0
- package/src/providers/workbuddy/growth-api.js +66 -32
- package/src/providers/workbuddy/growth-plans.js +32 -21
- package/src/providers/workbuddy/growth.js +83 -17
- package/src/runtime/workbuddy-growth.js +25 -3
package/docs/cli_help_mini.md
CHANGED
|
@@ -80,7 +80,10 @@
|
|
|
80
80
|
| raccoon 接入 | `-provider raccoon login` | Raccoon(商汤小浣熊)扫码登录:生成 `qrcode_code`→终端二维码 + `login/mp?code=` 链接→**用微信「扫一扫」扫**并确认(浏览器直接打开链接会 404,站点 SPA 无该路由)→轮询拿 token,落盘 `auths/raccoon-<uid>.json`+state(`keys` 一律以文档为准,续期后自愈)并把兜底 6 模型并入 allowlist;`raccoon/<modelId>` 前缀路由(**OpenAI 兼容网关**,**免验证码**);多号自动轮换:请求内撞积分不足/限流就当场换下一个号,**暂不支持手动指定用哪个号**;积分不足长冷 1h、限流短冷 30s;恒 local-only 不借出 |
|
|
81
81
|
| raccoon 密码登录 | `-provider raccoon login --password` | Raccoon **手机号+密码登录**(多号免扫码):走官方同款 `POST /api/web/auth/v1/login_with_password`,**phone/password 须 AES-128-CFB 加密**(key 硬编码于前端,见 `crypto.js`;发明文会报「手机号或密码不正确」);TTY 下密码**关回显**(逐键 `*`),非 TTY 须 `--password=<密码>`;默认 `--nation 86`;失败给人话(「手机号或密码不正确」),回执回显手机号但**打码**、**不回显密码**;落盘/确认/领奖与扫码登录完全同路 |
|
|
82
82
|
| raccoon 积分 | `-provider raccoon quota [--json]` / `checkin` | 逐号查积分五分项 / 领「桌面端登录奖励」——**这家没有每日签到**:每日 300 与注册礼包 3000 由服务端自动发(无端点,每日 300 当天 23:59:59 清零),唯一要领的登录奖励 3000 是**每号一次性**且 `login` 时已自动领掉,`checkin` 只是补领兜底;判据只认账单、不认上游 `code:0`(重复领会虚回成功);未登录给 login 指引;daemon 每日 09:00 兜底扫描未领过的号(`MSLXDFF_RACCOON_CHECKIN=0` 关,`_CHECKIN_HOUR` 改点) |
|
|
83
|
-
| WorkBuddy 成长任务 | `-workbuddy growth [--json]` / `-wb growth` | 成长任务全自动(拉列表→参与→触发→领奖,串行 1.2s/1s
|
|
83
|
+
| WorkBuddy 成长任务 | `-workbuddy growth [--json]` / `-wb growth` | 成长任务全自动(拉列表→参与→触发→领奖,串行 1.2s/1s,已领幂等跳过);三种触发通道分派——`growthEvent` 对话(`chat_5`/`automation_1`/`skill_1`/`Model_chat_GLM5.2`)、桌面指纹事件链(`RichMeow_Chat`/`Buddy_App`/`Buddy_App_QQ`/`create_canvas`/`template_5`/`playbook_prompt`)、专家真实对话回执(`expert_5`/`Expert_team_use_3`);**任务列表与领奖只在 `www.workbuddy.cn` 命中**,accept 前上游不下发 target 须 accept 后重读;仍 MANUAL 的 5 项(`Library_read`/`Expert_lighthouse`/`Hp_Appearance`/`WorkBuddy_Valorant`/`wb_wechat_oa_subscribe_task`)只认客户端真实行为,不空烧对话;daemon 每日 09:30 自动(`MSLXDFF_WORKBUDDY_GROWTH=0` 关,`_GROWTH_HOUR`/`_GROWTH_MODEL` 可配) |
|
|
84
|
+
| WorkBuddy 积分盘点 | `-provider workbuddy quota [--json]` / `credits` / `balance` | **只读**逐号列未领任务(含积分/进度/状态)、连登天数、可兑档位、抽奖次数、未领合计;不发任何写请求 |
|
|
85
|
+
| WorkBuddy 补领 | `-provider workbuddy checkin [--json]` / `claim` | 补领闭环:活跃上报点亮连登 → 兑换 7/14/28 档位 → 抽完全部次数 → 成长任务全量(含桌面/专家通道);输出到账项数/积分/跳过项/未计分告警 |
|
|
86
|
+
| WorkBuddy 连登 | daemon 每日随成长排程 | **签到不点亮连登**——`streak.days` 只认 `/v2/report` 的 `chat_request_send` 事件;不发则 7/14/28 档位积分与抽奖次数永远拿不到(2026-10-20 实测签到成功但 streak 恒 0) |
|
|
84
87
|
| WorkBuddy 猫猫旅行 | `-workbuddy travel [--json]` / `-wb travel` | 无猫自动同意协议+领养(+300,门槛未达自动补一次对话解锁);到站领奖 / 空闲派出(location 4)/ 旅行中跳过 |
|
|
85
88
|
| WorkBuddy 余额 | `-workbuddy balance [--json]` / `-wb balance` | 查多号余额(`total/dailyPacks/nextExpire`,TTL 5min) |
|
|
86
89
|
| WorkBuddy 列表 | `-workbuddy list` / `-wb list` | 列账号(`uid/domain/enterpriseId`) |
|
package/package.json
CHANGED
|
@@ -76,6 +76,8 @@ export async function handleProvider(args) {
|
|
|
76
76
|
if (await handleClineLogin(id, sub)) return true;
|
|
77
77
|
const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
|
|
78
78
|
if (await handleWorkbuddyLogin(id, sub, rest)) return true;
|
|
79
|
+
const { handleWorkbuddyQuota } = await import("./workbuddy-quota.js");
|
|
80
|
+
if (await handleWorkbuddyQuota(id, sub, rest)) return true;
|
|
79
81
|
const { handleCodeartsLogin } = await import("./codearts-login.js");
|
|
80
82
|
if (await handleCodeartsLogin(id, sub, rest)) return true;
|
|
81
83
|
const { handleTraeworkLogin } = await import("./traework-login.js");
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// mslxdff -provider workbuddy quota|checkin —— 查还漏领多少积分 / 手动补领
|
|
2
|
+
//
|
|
3
|
+
// 子命令:
|
|
4
|
+
// quota|credits|balance [--json] 只读:逐号列出未领任务、连登天数、可兑换档位、抽奖次数
|
|
5
|
+
// checkin|claim 补领:连登兑换 + 抽奖 + 成长任务闭环(含桌面/专家类)
|
|
6
|
+
//
|
|
7
|
+
// ⚠ 只读口径(2026-10-20 实测):
|
|
8
|
+
// - 签到每天成功(code 0)**不等于**连登点亮;连登只认 /v2/report 的
|
|
9
|
+
// chat_request_send 事件,所以「签到 OK + 连登 0 天」是常态,不是异常。
|
|
10
|
+
// - 任务列表与领奖只在 www.workbuddy.cn 命中(web 成长中心口径)。
|
|
11
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
12
|
+
import { compatFetch } from "../../../compat.js";
|
|
13
|
+
|
|
14
|
+
const QUOTA_SUBS = new Set(["quota", "credits", "balance"]);
|
|
15
|
+
const CLAIM_SUBS = new Set(["checkin", "claim", "signin"]);
|
|
16
|
+
|
|
17
|
+
async function loadCreds() {
|
|
18
|
+
const { loadProviderConfig, loadProviderKeys } = await import("../../../state.js");
|
|
19
|
+
const cfg = loadProviderConfig("workbuddy") || {};
|
|
20
|
+
const keys = loadProviderKeys("workbuddy") || [];
|
|
21
|
+
const authList = Array.isArray(cfg.auths) ? cfg.auths : [];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (let i = 0; i < Math.max(keys.length, authList.length); i++) {
|
|
24
|
+
const a = authList[i] || {};
|
|
25
|
+
const at = keys[i] || "";
|
|
26
|
+
if (!a.uid || !at) continue;
|
|
27
|
+
out.push({ uid: a.uid, at, domain: a.domain, enterpriseId: a.enterpriseId || "" });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const short = (uid) => String(uid).slice(0, 8);
|
|
33
|
+
|
|
34
|
+
export async function handleWorkbuddyQuota(id, sub, rest = [], deps = {}) {
|
|
35
|
+
const pid = normalizeProviderId(id) || String(id || "").toLowerCase();
|
|
36
|
+
if (pid !== "workbuddy") return false;
|
|
37
|
+
const s = String(sub || "").toLowerCase();
|
|
38
|
+
const isClaim = CLAIM_SUBS.has(s);
|
|
39
|
+
if (!isClaim && !QUOTA_SUBS.has(s)) return false;
|
|
40
|
+
|
|
41
|
+
const log = deps.log || ((m) => console.log(m));
|
|
42
|
+
const exit = deps.exit || ((code) => process.exit(code));
|
|
43
|
+
const json = rest.includes("--json") || rest.includes("-json");
|
|
44
|
+
const fetchImpl = deps.fetchImpl || compatFetch;
|
|
45
|
+
const accounts = await loadCreds();
|
|
46
|
+
if (!accounts.length) {
|
|
47
|
+
if (json) log(JSON.stringify({ error: "not_logged_in", hint: "mslxdff -provider workbuddy login" }));
|
|
48
|
+
else {
|
|
49
|
+
log("未登录:还没有 workbuddy 账号");
|
|
50
|
+
log(" 先运行: mslxdff -provider workbuddy login");
|
|
51
|
+
}
|
|
52
|
+
exit(1);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { growthTasks, runGrowthAccount } = await import("../../../providers/workbuddy/growth.js");
|
|
57
|
+
const { growthStreak, lotteryChances, reportChatActivity, runStreakBonus } = await import("../../../providers/workbuddy/activity.js");
|
|
58
|
+
|
|
59
|
+
const rows = [];
|
|
60
|
+
for (const a of accounts) {
|
|
61
|
+
const row = { uid: a.uid, ok: true };
|
|
62
|
+
try {
|
|
63
|
+
if (isClaim) {
|
|
64
|
+
const rep = await reportChatActivity({ ...a, fetchImpl });
|
|
65
|
+
const sb = await runStreakBonus({ ...a, fetchImpl });
|
|
66
|
+
const g = await runGrowthAccount({ ...a, fetchImpl });
|
|
67
|
+
row.activity = rep.ok;
|
|
68
|
+
row.streak = { days: sb.days, redeemed: sb.redeemed, drawn: sb.drawn, credit: sb.credit, errors: sb.errors };
|
|
69
|
+
const claimed = g.tasks.filter((t) => t.status === "claimed");
|
|
70
|
+
row.growth = {
|
|
71
|
+
credit: g.credit, energy: g.energy,
|
|
72
|
+
claimed: claimed.map((t) => t.task_code),
|
|
73
|
+
ignored: g.tasks.filter((t) => t.fired > 0 && !["claimed", "completed"].includes(String(t.status || ""))).map((t) => t.task_code),
|
|
74
|
+
skipped: g.tasks.filter((t) => t.skipped).length,
|
|
75
|
+
error: g.error,
|
|
76
|
+
};
|
|
77
|
+
row.ok = rep.ok || g.ok;
|
|
78
|
+
} else {
|
|
79
|
+
const listed = await growthTasks({ ...a, fetchImpl });
|
|
80
|
+
const tasks = listed.tasks || [];
|
|
81
|
+
const pending = tasks.filter((t) => t.accept_status !== "claimed");
|
|
82
|
+
row.tasksError = listed.ok ? null : listed.msg || "拉取失败";
|
|
83
|
+
row.pending = pending.map((t) => ({
|
|
84
|
+
code: t.task_code,
|
|
85
|
+
credit: Number(t.reward_credit || 0),
|
|
86
|
+
energy: Number(t.reward_energy || 0),
|
|
87
|
+
status: t.accept_status,
|
|
88
|
+
progress: `${t.progress?.current ?? t.current ?? 0}/${t.progress?.target ?? t.target ?? 0}`,
|
|
89
|
+
}));
|
|
90
|
+
row.pendingCredit = row.pending.reduce((n, t) => n + t.credit, 0);
|
|
91
|
+
row.claimed = tasks.filter((t) => t.accept_status === "claimed").length;
|
|
92
|
+
const st = await growthStreak({ ...a, fetchImpl });
|
|
93
|
+
row.streak = { days: st.days, tiers: st.tiers, cards: st.cards, error: st.ok ? undefined : st.msg };
|
|
94
|
+
const lot = await lotteryChances({ ...a, fetchImpl });
|
|
95
|
+
row.chances = lot.chances;
|
|
96
|
+
row.ok = !!listed.ok;
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
row.ok = false;
|
|
100
|
+
row.error = String(e?.message || e).slice(0, 160);
|
|
101
|
+
}
|
|
102
|
+
rows.push(row);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const failed = rows.filter((r) => !r.ok).length;
|
|
106
|
+
|
|
107
|
+
if (json) {
|
|
108
|
+
log(JSON.stringify({ provider: "workbuddy", mode: isClaim ? "claim" : "quota", accounts: rows, ok: rows.length - failed, total: rows.length }));
|
|
109
|
+
exit(failed === rows.length ? 1 : 0);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isClaim) {
|
|
114
|
+
log(`workbuddy 补领 · 共 ${rows.length} 个号`);
|
|
115
|
+
for (const r of rows) {
|
|
116
|
+
log(` [${short(r.uid)}]`);
|
|
117
|
+
if (r.error) { log(` ❌ ${r.error}`); continue; }
|
|
118
|
+
log(` 活跃上报: ${r.activity ? "✓ 已点亮连登" : "✗ 失败"}`);
|
|
119
|
+
const s = r.streak || {};
|
|
120
|
+
const tiers = (s.redeemed || []).map((x) => `${x.tier}(+${x.credit}c)`).join(" ") || "-";
|
|
121
|
+
log(` 连登: ${s.days ?? "?"} 天 · 兑换 ${tiers} · 抽奖 ${s.drawn ?? 0} 次 · +${s.credit ?? 0} 分`);
|
|
122
|
+
if (s.errors?.length) log(` ⚠ ${s.errors.join(";")}`);
|
|
123
|
+
const g = r.growth || {};
|
|
124
|
+
log(` 成长任务: 到账 ${g.claimed?.length ?? 0} 项 · +${g.credit ?? 0} 分 · 跳过 ${g.skipped ?? 0} 项`);
|
|
125
|
+
if (g.claimed?.length) log(` 已领: ${g.claimed.join(", ")}`);
|
|
126
|
+
if (g.ignored?.length) log(` ⚠ 上报未计分(该任务只认客户端真实行为): ${g.ignored.join(", ")}`);
|
|
127
|
+
if (g.error) log(` ⚠ ${g.error}`);
|
|
128
|
+
}
|
|
129
|
+
log("");
|
|
130
|
+
log(" 复核: mslxdff -provider workbuddy quota");
|
|
131
|
+
exit(failed === rows.length ? 1 : 0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
log(`workbuddy 已领 / 未领 · 共 ${rows.length} 个号`);
|
|
136
|
+
let totalPending = 0;
|
|
137
|
+
for (const r of rows) {
|
|
138
|
+
if (r.error) { log(` [${short(r.uid)}] ❌ ${r.error}`); continue; }
|
|
139
|
+
totalPending += r.pendingCredit || 0;
|
|
140
|
+
const st = r.streak || {};
|
|
141
|
+
const unlocked = (st.tiers || []).filter((t) => !["locked", "claimed"].includes(t.status));
|
|
142
|
+
log(` [${short(r.uid)}] 已领 ${r.claimed} 项 · 未领 ${r.pending.length} 项 合计 ${r.pendingCredit} 分`);
|
|
143
|
+
log(` 连登 ${st.days} 天${st.cards ? ` · 补签卡 ${st.cards}` : ""} · 可兑档位 ${unlocked.map((t) => t.tier).join(",") || "无"} · 抽奖次数 ${r.chances}`);
|
|
144
|
+
for (const t of r.pending) {
|
|
145
|
+
log(` · ${t.code.padEnd(28)} +${String(t.credit).padEnd(4)} c ${t.status} ${t.progress}`);
|
|
146
|
+
}
|
|
147
|
+
if (r.tasksError) log(` ⚠ 任务列表拉取失败:${r.tasksError}`);
|
|
148
|
+
}
|
|
149
|
+
log("");
|
|
150
|
+
log(` 未领合计 ${totalPending} 分(不含连登档位与抽奖)`);
|
|
151
|
+
log(" 补领: mslxdff -provider workbuddy checkin");
|
|
152
|
+
exit(failed === rows.length ? 1 : 0);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
3
|
+
import { growthRequest } from "./growth-api.js";
|
|
4
|
+
// WorkBuddy 每日活跃上报 + 连登管家(兑换档位 + 抽奖)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么不靠签到:实测(2026-10-20,本机三号)签到每天成功(code 0)但
|
|
7
|
+
// GET /activity/growth/streak 的 days 恒为 0 —— 连登只认 /v2/report 的
|
|
8
|
+
// chat_request_send 事件,签到不点亮。连登档位 7/14/28 天的积分、补签卡、
|
|
9
|
+
// 抽奖次数全部拿不到,属于「不报错但持续漏领」。
|
|
10
|
+
//
|
|
11
|
+
// 上报形状照抄客户端(字段宁全勿简,防上游加严);userId 必填,缺失服务端
|
|
12
|
+
// 200 但静默丢弃。每号每天 1 次即可(按天去重,重复上报无额外收益)。
|
|
13
|
+
|
|
14
|
+
const REPORT_BASES = ["https://www.codebuddy.cn", "https://copilot.tencent.com"];
|
|
15
|
+
|
|
16
|
+
function reportHeaders({ at, uid, domain, enterpriseId }) {
|
|
17
|
+
return {
|
|
18
|
+
Authorization: `Bearer ${at}`,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Accept: "application/json",
|
|
21
|
+
"X-User-Id": uid,
|
|
22
|
+
"X-Domain": domain || "www.codebuddy.cn",
|
|
23
|
+
"X-Product": "SaaS",
|
|
24
|
+
"User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
|
|
25
|
+
Origin: "https://www.codebuddy.cn",
|
|
26
|
+
Referer: "https://www.codebuddy.cn/",
|
|
27
|
+
...(enterpriseId ? { "X-Enterprise-Id": enterpriseId } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function chatActivityEvent({ uid, conversationId, requestId, model = "deepseek-v4-flash", modelName = "DeepSeek V4 Flash" }) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
return {
|
|
34
|
+
eventCode: "chat_request_send",
|
|
35
|
+
timestamp: now,
|
|
36
|
+
reportDelay: 0,
|
|
37
|
+
mode: "craft",
|
|
38
|
+
conversationId,
|
|
39
|
+
requestId,
|
|
40
|
+
inputLength: 12,
|
|
41
|
+
requestModelId: model,
|
|
42
|
+
requestModelName: modelName,
|
|
43
|
+
isPlan: false,
|
|
44
|
+
isAutoExecuteTerminal: false,
|
|
45
|
+
isAutoModify: false,
|
|
46
|
+
codebaseEnable: false,
|
|
47
|
+
maxToken: 0,
|
|
48
|
+
maxSteps: 0,
|
|
49
|
+
temperature: 0,
|
|
50
|
+
maxRetries: 0,
|
|
51
|
+
mentionContexts: [],
|
|
52
|
+
knowledgeId: [],
|
|
53
|
+
knowledgeName: [],
|
|
54
|
+
codebaseId: "",
|
|
55
|
+
mentionContextCount: 0,
|
|
56
|
+
command: "",
|
|
57
|
+
expertId: "",
|
|
58
|
+
recommendId: "",
|
|
59
|
+
skillId: "",
|
|
60
|
+
skillCount: 0,
|
|
61
|
+
totalCount: 0,
|
|
62
|
+
fileUri: "",
|
|
63
|
+
presentAt: now,
|
|
64
|
+
traceId: "",
|
|
65
|
+
rootRequestId: requestId,
|
|
66
|
+
parentConversationId: conversationId,
|
|
67
|
+
agentName: "default",
|
|
68
|
+
agentType: "conversation",
|
|
69
|
+
userId: uid,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 一条对话活跃上报:点亮当日连登 + 解锁 first_buddy(领养前置)。双域容灾,不抛异常。
|
|
74
|
+
export async function reportChatActivity({ uid, at, domain, enterpriseId, conversationId, requestId, model, modelName, fetchImpl = compatFetch, timeoutMs = 15000 } = {}) {
|
|
75
|
+
if (!uid || !at) return { ok: false, msg: "缺少 uid / accessToken" };
|
|
76
|
+
const conv = conversationId || `mslxdff-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
77
|
+
const req = requestId || conv;
|
|
78
|
+
const body = JSON.stringify([chatActivityEvent({ uid, conversationId: conv, requestId: req, model, modelName })]);
|
|
79
|
+
let last = { ok: false, msg: "无可用域" };
|
|
80
|
+
for (const base of REPORT_BASES) {
|
|
81
|
+
let res;
|
|
82
|
+
try {
|
|
83
|
+
res = await fetchImpl(`${base}/v2/report`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: reportHeaders({ at, uid, domain, enterpriseId }),
|
|
86
|
+
body,
|
|
87
|
+
signal: timeoutSignal(timeoutMs),
|
|
88
|
+
});
|
|
89
|
+
} catch (e) {
|
|
90
|
+
last = { ok: false, status: 0, msg: `网络失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let j = null;
|
|
94
|
+
try { j = await res.json(); } catch {}
|
|
95
|
+
const out = { ok: res.status === 200 && j?.code === 0, status: res.status, code: j?.code, msg: j?.msg || "", base };
|
|
96
|
+
if (out.ok) return out;
|
|
97
|
+
// 401/403 是登录态问题,换域也没用
|
|
98
|
+
if (res.status === 401 || res.status === 403) return { ...out, needRefresh: true };
|
|
99
|
+
last = out;
|
|
100
|
+
}
|
|
101
|
+
return last;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TIER_KEYS = ["tier_7d", "tier_14d", "tier_28d"];
|
|
105
|
+
|
|
106
|
+
// 连登完整状态(days / 档位 / 补签卡)。
|
|
107
|
+
export async function growthStreak(api = {}) {
|
|
108
|
+
const r = await growthRequest({ ...api, method: "GET", path: "/activity/growth/streak" });
|
|
109
|
+
if (!r.ok) return { ok: false, msg: r.msg || `HTTP ${r.status}`, days: 0, tiers: [], cards: 0 };
|
|
110
|
+
const data = r.data || {};
|
|
111
|
+
const rs = data.redemption_status || {};
|
|
112
|
+
const tiers = TIER_KEYS
|
|
113
|
+
.map((k) => ({ tier: k.slice(5), status: rs[`${k}_status`] || "", days: Number(rs[`${k}_count`] || 0) }))
|
|
114
|
+
.filter((t) => t.status);
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
days: Number(data.streak?.days || 0),
|
|
118
|
+
monthTotalDays: Number(data.streak?.month_total_days || 0),
|
|
119
|
+
nextTier: data.streak?.next_tier || "",
|
|
120
|
+
nextTierRemaining: Number(data.streak?.next_tier_remaining || 0),
|
|
121
|
+
tiers,
|
|
122
|
+
cards: Number(data.makeup_cards?.balance || 0),
|
|
123
|
+
rewards: Array.isArray(rs.tiers) ? rs.tiers : [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 兑换一个已解锁档位(tier: "7d"|"14d"|"28d");未解锁上游 403,调用方按 status 跳过即可。
|
|
128
|
+
export async function redeemStreakTier(api = {}, tier) {
|
|
129
|
+
const r = await growthRequest({
|
|
130
|
+
...api,
|
|
131
|
+
method: "POST",
|
|
132
|
+
path: "/activity/growth/redeem",
|
|
133
|
+
body: { tier, client_token: randomUUID() },
|
|
134
|
+
});
|
|
135
|
+
return { ok: r.ok, status: r.status, code: r.code, msg: r.msg || "", data: r.data || {} };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function lotteryChances(api = {}) {
|
|
139
|
+
const r = await growthRequest({ ...api, method: "GET", path: "/activity/growth/lottery/summary" });
|
|
140
|
+
if (!r.ok) return { ok: false, chances: 0, enabled: false, msg: r.msg || `HTTP ${r.status}` };
|
|
141
|
+
return { ok: true, chances: Number(r.data?.chances || 0), enabled: r.data?.module?.enabled !== false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function lotteryDraw(api = {}) {
|
|
145
|
+
const r = await growthRequest({
|
|
146
|
+
...api,
|
|
147
|
+
method: "POST",
|
|
148
|
+
path: "/activity/growth/lottery/draw",
|
|
149
|
+
body: { client_token: randomUUID() },
|
|
150
|
+
});
|
|
151
|
+
return { ok: r.ok, status: r.status, msg: r.msg || "", prize: r.data || null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 连登管家闭环:兑换所有已解锁未领取的档位 → 抽完全部次数。幂等,未解锁静默跳过。
|
|
155
|
+
export async function runStreakBonus({ uid, at, domain, enterpriseId, fetchImpl = compatFetch, onStep = null, maxDraws = 20 } = {}) {
|
|
156
|
+
const api = { uid, at, domain, enterpriseId, fetchImpl };
|
|
157
|
+
const step = (e) => { try { onStep?.(e); } catch {} };
|
|
158
|
+
const out = { uid, redeemed: [], drawn: 0, credit: 0, energy: 0, prizes: [], errors: [] };
|
|
159
|
+
|
|
160
|
+
const st = await growthStreak(api);
|
|
161
|
+
if (!st.ok) {
|
|
162
|
+
out.errors.push(`读连登状态失败:${st.msg}`);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
out.days = st.days;
|
|
166
|
+
|
|
167
|
+
for (const t of st.tiers) {
|
|
168
|
+
// locked = 天数不够;claimed = 已领过。其余状态视为可兑换。
|
|
169
|
+
if (!["locked", "claimed"].includes(String(t.status))) {
|
|
170
|
+
const r = await redeemStreakTier(api, t.tier);
|
|
171
|
+
if (r.ok) {
|
|
172
|
+
const credit = Number(r.data?.credit || 0);
|
|
173
|
+
const energy = Number(r.data?.energy || 0);
|
|
174
|
+
out.credit += credit;
|
|
175
|
+
out.energy += energy;
|
|
176
|
+
out.redeemed.push({ tier: t.tier, credit, energy });
|
|
177
|
+
step({ phase: "redeem", ok: true, tier: t.tier, credit, energy });
|
|
178
|
+
} else {
|
|
179
|
+
out.errors.push(`兑换 ${t.tier} 失败:${r.msg || `HTTP ${r.status}`}`);
|
|
180
|
+
step({ phase: "redeem", ok: false, tier: t.tier, msg: r.msg });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 每轮重读次数(兑换与抽奖都可能再发次数),抽到 0 或达上限为止。
|
|
186
|
+
for (;;) {
|
|
187
|
+
if (out.drawn >= maxDraws) break;
|
|
188
|
+
const left = (await lotteryChances(api)).chances;
|
|
189
|
+
if (left <= 0) break;
|
|
190
|
+
const r = await lotteryDraw(api);
|
|
191
|
+
if (!r.ok) {
|
|
192
|
+
out.errors.push(`抽奖失败:${r.msg || `HTTP ${r.status}`}`);
|
|
193
|
+
step({ phase: "draw", ok: false, msg: r.msg });
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
out.drawn += 1;
|
|
197
|
+
const name = r.prize?.prize_name || r.prize?.name || r.prize?.title || "";
|
|
198
|
+
const credit = Number(r.prize?.credit || 0);
|
|
199
|
+
out.credit += credit;
|
|
200
|
+
out.prizes.push({ name, credit });
|
|
201
|
+
step({ phase: "draw", ok: true, remaining: left - 1, prize: name, credit });
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
3
|
+
// WorkBuddy 桌面客户端行为指纹上报(/v2/report)。
|
|
4
|
+
//
|
|
5
|
+
// 为什么需要:桌面端类任务(RichMeow_Chat / Buddy_App / create_canvas / template_5 /
|
|
6
|
+
// playbook_prompt / Library_read)上游按**客户端指纹**判据,CLI 域的 growthEvent
|
|
7
|
+
// 发了也不计数。判据是同一 POST /v2/report 通道上不同的事件链 + 桌面 UA。
|
|
8
|
+
// 来源:workbuddy2api-panel internal/upstream/desktop.go 三账号实测点亮记录。
|
|
9
|
+
//
|
|
10
|
+
// 幂等:行为事件按天去重,重复推进不会重复扣资源;已达标任务由调用方跳过。
|
|
11
|
+
|
|
12
|
+
const DESKTOP_BASE = "https://copilot.tencent.com";
|
|
13
|
+
const DESKTOP_UA = "WorkBuddy/5.5.6 WorkBuddy/5.5.6 CLI/2.137.1";
|
|
14
|
+
const WEB_BASE = "https://www.workbuddy.cn";
|
|
15
|
+
const WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36";
|
|
16
|
+
|
|
17
|
+
// 稳定派生设备标识:同一账号每次同值,模拟固定设备。
|
|
18
|
+
function deriveId(uid, salt) {
|
|
19
|
+
return createHash("sha256").update(`${salt}:${uid}`).digest("hex").slice(0, 36);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function desktopFingerprint(uid, nickname = "") {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
return {
|
|
25
|
+
timezone: "Asia/Shanghai",
|
|
26
|
+
reportDelay: 2000,
|
|
27
|
+
userId: uid,
|
|
28
|
+
username: nickname,
|
|
29
|
+
userNickname: nickname,
|
|
30
|
+
product: "SaaS",
|
|
31
|
+
releaseDate: 1789036585355,
|
|
32
|
+
commit: "5f9692923c93033111c51ad7b003eb80204a9b75",
|
|
33
|
+
ideName: "WorkBuddy",
|
|
34
|
+
ideType: "WorkBuddy",
|
|
35
|
+
ideVersion: "5.5.6",
|
|
36
|
+
machineId: deriveId(uid, "machine"),
|
|
37
|
+
sessionId: deriveId(uid, "session"),
|
|
38
|
+
extName: "workbuddy-desktop",
|
|
39
|
+
extVersion: "5.5.6",
|
|
40
|
+
os: "win32",
|
|
41
|
+
arch: "x64",
|
|
42
|
+
osVersion: "10.0.26220",
|
|
43
|
+
cpuCores: 20,
|
|
44
|
+
memorySize: 24,
|
|
45
|
+
timestamp: now,
|
|
46
|
+
presentAt: now,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 以桌面指纹上报一批事件(业务字段覆盖公共指纹)。
|
|
51
|
+
export async function reportDesktopEvents({ uid, at, nickname, events = [], fetchImpl = compatFetch, timeoutMs = 20000 } = {}) {
|
|
52
|
+
if (!events.length) return { ok: false, msg: "无事件" };
|
|
53
|
+
const fp = desktopFingerprint(uid, nickname);
|
|
54
|
+
const body = JSON.stringify(events.map((ev) => ({ ...fp, ...ev })));
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetchImpl(`${DESKTOP_BASE}/v2/report`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${at}`,
|
|
60
|
+
Accept: "application/json, text/plain, */*",
|
|
61
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
62
|
+
"User-Agent": DESKTOP_UA,
|
|
63
|
+
"X-Domain": DESKTOP_BASE,
|
|
64
|
+
"X-Product": "SaaS",
|
|
65
|
+
"X-Request-ID": `${deriveId(uid, "req")}${Date.now() % 1e6}`,
|
|
66
|
+
"X-User-Id": uid,
|
|
67
|
+
},
|
|
68
|
+
body,
|
|
69
|
+
signal: timeoutSignal(timeoutMs),
|
|
70
|
+
});
|
|
71
|
+
let j = null;
|
|
72
|
+
try { j = await res.json(); } catch {}
|
|
73
|
+
return { ok: res.status === 200 && (j?.code === undefined || j?.code === 0), status: res.status, code: j?.code, msg: j?.msg || "" };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { ok: false, status: 0, msg: `网络失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// web 域单事件上报(Library_read 这类页面行为任务)。
|
|
80
|
+
export async function reportWebEvent({ uid, at, enterpriseId, eventCode, pageURL, elementId = "", elementName = "", fetchImpl = compatFetch, timeoutMs = 20000 } = {}) {
|
|
81
|
+
const ev = {
|
|
82
|
+
eventCode,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
reportDelay: 0,
|
|
85
|
+
pageURL,
|
|
86
|
+
elementId,
|
|
87
|
+
elementName,
|
|
88
|
+
os: "Win32",
|
|
89
|
+
arch: "",
|
|
90
|
+
osVersion: "10.0",
|
|
91
|
+
userAgent: WEB_UA,
|
|
92
|
+
machineId: deriveId(uid, "webmachine"),
|
|
93
|
+
userId: uid,
|
|
94
|
+
enterpriseId: enterpriseId || "",
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetchImpl(`${WEB_BASE}/v2/report`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${at}`,
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
"x-client-platform": "web",
|
|
104
|
+
Origin: WEB_BASE,
|
|
105
|
+
Referer: pageURL,
|
|
106
|
+
"User-Agent": WEB_UA,
|
|
107
|
+
"X-User-Id": uid,
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify([ev]),
|
|
110
|
+
signal: timeoutSignal(timeoutMs),
|
|
111
|
+
});
|
|
112
|
+
let j = null;
|
|
113
|
+
try { j = await res.json(); } catch {}
|
|
114
|
+
return { ok: res.status === 200 && (j?.code === undefined || j?.code === 0), status: res.status, msg: j?.msg || "" };
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return { ok: false, status: 0, msg: `网络失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 一次「桌面端成功对话」六事件链(点亮 RichMeow_Chat;也是模板/画布/灵感链的前缀)。
|
|
121
|
+
export function chatSequence({ conversationId, requestId, messageId }) {
|
|
122
|
+
const mk = (code, extra) => ({ eventCode: code, ...extra });
|
|
123
|
+
return [
|
|
124
|
+
mk("agent_task_created", {
|
|
125
|
+
source: "LOCAL", name: "working", task_target: "local", mode: "craft",
|
|
126
|
+
requestModelId: "fast-model", requestModelName: "fast-model",
|
|
127
|
+
has_repo: false, repo_type: "none", workspace_type: "empty",
|
|
128
|
+
has_connector: false, connector_types: [], has_mention: false, mention_types: [],
|
|
129
|
+
has_template: false, action: "", template_name: "",
|
|
130
|
+
has_expert: false, expert_id: "", expert_name: "", expert_industry_id: "",
|
|
131
|
+
has_skill: false, skill_names: [],
|
|
132
|
+
conversationId, messageId, buddyId: "", buddyName: "",
|
|
133
|
+
}),
|
|
134
|
+
mk("chat_message_send", {
|
|
135
|
+
messageId: `${messageId}-assistant`, historyCount: 0, isContextTruncated: false,
|
|
136
|
+
currentStepCount: 1, traceId: requestId, rootRequestId: requestId,
|
|
137
|
+
parentConversationId: conversationId, agentName: "cli", agentType: "main",
|
|
138
|
+
}),
|
|
139
|
+
mk("chat_request_send", {
|
|
140
|
+
inputLength: 24, isPlan: false, isAutoExecuteTerminal: false, isAutoModify: false,
|
|
141
|
+
codebaseEnable: false, maxToken: 0, maxSteps: 500, temperature: 0, maxRetries: 0,
|
|
142
|
+
mentionContexts: [], knowledgeId: [], knowledgeName: [], codebaseId: "",
|
|
143
|
+
mentionContextCount: 0, command: "", recommendId: "", skillId: "", skillCount: 0, totalCount: 0,
|
|
144
|
+
traceId: requestId, rootRequestId: requestId, parentConversationId: conversationId,
|
|
145
|
+
agentName: "cli", agentType: "main",
|
|
146
|
+
"codebuddy.session_id": conversationId, "codebuddy.conversation_request_id": requestId,
|
|
147
|
+
}),
|
|
148
|
+
mk("chat_message_response", {
|
|
149
|
+
messageId: `${messageId}-assistant`, responseModelId: "fast-model",
|
|
150
|
+
inputToken: 120, outputToken: 80, totalToken: 200,
|
|
151
|
+
cachedTokens: 0, cachedWriteTokens: 0, cachedMissTokens: 0,
|
|
152
|
+
isSuccessful: true, messageErrorCode: "", finishReason: "stop",
|
|
153
|
+
firstTokenAt: Date.now(), traceId: requestId, conversationId,
|
|
154
|
+
rootRequestId: requestId, parentConversationId: conversationId, agentName: "cli", agentType: "main",
|
|
155
|
+
"codebuddy.session_id": conversationId, "codebuddy.conversation_request_id": requestId,
|
|
156
|
+
}),
|
|
157
|
+
mk("chat_message_status", {
|
|
158
|
+
messageId: `${messageId}-assistant`, messageErrorCode: "0", traceId: requestId,
|
|
159
|
+
rootRequestId: requestId, parentConversationId: conversationId, agentName: "cli", agentType: "main",
|
|
160
|
+
}),
|
|
161
|
+
mk("chat_request_response", {
|
|
162
|
+
mode: "craft", toolCallCount: 0, inputToken: 120, outputToken: 80, totalToken: 200,
|
|
163
|
+
cachedTokens: 0, cachedWriteTokens: 0, cachedMissTokens: 0,
|
|
164
|
+
isSuccessful: true, messageErrorCode: "", finishReason: "stop",
|
|
165
|
+
rootRequestId: requestId, parentConversationId: conversationId,
|
|
166
|
+
}),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 企鹅教师助手(Buddy_App_QQ 判据应用;同时满足 Buddy_App「进入任一应用」)。
|
|
171
|
+
const BUDDY_APP = { id: "cb_y5Dy46tPQGGWtueMxXbe", name: "企鹅教师助手" };
|
|
172
|
+
|
|
173
|
+
function buddyAppSequence() {
|
|
174
|
+
const mk = (code, extra = {}) => ({ eventCode: code, mode: "LOCAL", buddyId: BUDDY_APP.id, buddyName: BUDDY_APP.name, ...extra });
|
|
175
|
+
return [
|
|
176
|
+
mk("buddyapp_discover_click"),
|
|
177
|
+
mk("buddyapp_show", { elementId: BUDDY_APP.id, elementName: BUDDY_APP.name, position: 2 }),
|
|
178
|
+
mk("buddyapp_enter_click", { elementId: BUDDY_APP.id, elementName: BUDDY_APP.name, position: 2, isFirstPage: "1" }),
|
|
179
|
+
mk("buddyapp_auth_confirm_click", { elementId: BUDDY_APP.id, elementName: BUDDY_APP.name }),
|
|
180
|
+
mk("buddyapp_bindaccount_skip_click", { elementId: BUDDY_APP.id, elementName: BUDDY_APP.name }),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const TEMPLATES = [
|
|
185
|
+
"代码审查", "单元测试生成", "重构建议", "技术文档撰写", "接口设计",
|
|
186
|
+
"SQL 优化", "缺陷分诊", "性能分析", "安全审计", "日志排查",
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// 模板 id 只用于去重计数:上游不校验 id 是否真实存在(实测任意不同 id 都计数),
|
|
190
|
+
// 但**必须每次都不同**——固定 id 打到第 5 次就再也不涨。
|
|
191
|
+
function freshTemplateId(used) {
|
|
192
|
+
for (let i = 0; i < 50; i++) {
|
|
193
|
+
const id = `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
194
|
+
if (!used.includes(id)) return id;
|
|
195
|
+
}
|
|
196
|
+
return `tpl-${Date.now()}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 每个任务构造一批事件。round 用于需要重复的任务(模板 ×5)区分不同对象。
|
|
200
|
+
function buildEvents(code, round, ids, used = []) {
|
|
201
|
+
const { conversationId, requestId, messageId } = ids;
|
|
202
|
+
const chat = () => chatSequence({ conversationId, requestId, messageId });
|
|
203
|
+
switch (code) {
|
|
204
|
+
case "RichMeow_Chat":
|
|
205
|
+
return { events: chat(), via: "desktop" };
|
|
206
|
+
case "Buddy_App":
|
|
207
|
+
case "Buddy_App_QQ":
|
|
208
|
+
return { events: buddyAppSequence(), via: "desktop" };
|
|
209
|
+
case "create_canvas":
|
|
210
|
+
return {
|
|
211
|
+
events: [
|
|
212
|
+
...chat(),
|
|
213
|
+
{ eventCode: "wbx_design_canvas_task_create", conversationId, requestId, source: "summon_keyword", cost: 12000, isSuccessful: true },
|
|
214
|
+
{ eventCode: "wbx_design_canvas_open", conversationId, requestId, id: `ardot-file-${requestId.slice(-8)}`, source: "summon_keyword", type: "page", cost: 13000, isSuccessful: true },
|
|
215
|
+
],
|
|
216
|
+
via: "desktop",
|
|
217
|
+
};
|
|
218
|
+
case "template_5": {
|
|
219
|
+
const id = freshTemplateId(used);
|
|
220
|
+
const name = TEMPLATES[used.length % TEMPLATES.length];
|
|
221
|
+
return {
|
|
222
|
+
events: [
|
|
223
|
+
...chat(),
|
|
224
|
+
{ eventCode: "agent_task_created_with_template", mode: "working", isCustomModel: false, id, name, requestId },
|
|
225
|
+
{ eventCode: "template_used", template_id: id, task_mode: "working" },
|
|
226
|
+
],
|
|
227
|
+
via: "desktop",
|
|
228
|
+
obj: id,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case "playbook_prompt": {
|
|
232
|
+
// 同理:案例 id 只需每次不同,用真实案例名做展示字段。
|
|
233
|
+
const id = `pb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
234
|
+
const name = TEMPLATES[used.length % TEMPLATES.length];
|
|
235
|
+
const payload = { id, name, type: "document", categoryId: "", categoryName: "" };
|
|
236
|
+
return {
|
|
237
|
+
events: [
|
|
238
|
+
...chat(),
|
|
239
|
+
{ eventCode: "web_element_click", pageName: "playbook_detail", elementId: "playbook_ctaClick", elementName: name, source: "discover" },
|
|
240
|
+
{ eventCode: "playbook_cta_click", source: "discover", position: 0, ...payload },
|
|
241
|
+
{ eventCode: "playbook_prompt_send", conversationId, requestId, ...payload },
|
|
242
|
+
],
|
|
243
|
+
via: "desktop",
|
|
244
|
+
obj: id,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case "Library_read":
|
|
248
|
+
return {
|
|
249
|
+
via: "web",
|
|
250
|
+
event: {
|
|
251
|
+
eventCode: "library_doc_intro_click",
|
|
252
|
+
pageURL: "https://www.workbuddy.cn/library",
|
|
253
|
+
elementId: "library_doc",
|
|
254
|
+
elementName: "资料库",
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
default:
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 桌面/网页类任务执行器:按 needTimes 重复上报,任一失败即中断并回传原因。
|
|
263
|
+
export async function runDesktopTask(api = {}, { code, needTimes = 1, round = 0, sleepFn, onStep } = {}) {
|
|
264
|
+
const { uid, at, enterpriseId, fetchImpl = compatFetch } = api;
|
|
265
|
+
const step = (e) => { try { onStep?.(e); } catch {} };
|
|
266
|
+
let done = 0;
|
|
267
|
+
const used = [];
|
|
268
|
+
for (let i = 0; i < needTimes; i++) {
|
|
269
|
+
const stamp = `${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`;
|
|
270
|
+
const built = buildEvents(code, round + i, {
|
|
271
|
+
conversationId: `mslxdff-conv-${stamp}`,
|
|
272
|
+
requestId: `mslxdff-req-${stamp}`,
|
|
273
|
+
messageId: `mslxdff-msg-${stamp}`,
|
|
274
|
+
}, used);
|
|
275
|
+
if (!built) return { ok: false, done, msg: `任务 ${code} 没有对应的事件构造器` };
|
|
276
|
+
const r = built.via === "web"
|
|
277
|
+
? await reportWebEvent({ uid, at, enterpriseId, fetchImpl, ...built.event })
|
|
278
|
+
: await reportDesktopEvents({ uid, at, fetchImpl, events: built.events });
|
|
279
|
+
step({ phase: "fire", task_code: code, i: i + 1, times: needTimes, ok: r.ok, msg: r.msg });
|
|
280
|
+
if (!r.ok) return { ok: false, done, msg: r.msg || `HTTP ${r.status}` };
|
|
281
|
+
done += 1;
|
|
282
|
+
if (built.obj) used.push(built.obj);
|
|
283
|
+
if (i + 1 < needTimes) await sleepFn?.(1200);
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, done };
|
|
286
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
2
|
+
import { reportDesktopEvents } from "./desktop-report.js";
|
|
3
|
+
// WorkBuddy 专家类任务(expert_5 / Expert_team_use_3 / Expert_lighthouse)。
|
|
4
|
+
//
|
|
5
|
+
// 判据(workbuddy2api-panel internal/upstream/desktop.go 三账号实测):
|
|
6
|
+
// 1) 专家 id 必须是专家市场里**真实存在**的(编造 id 不计数),先拉市场列表;
|
|
7
|
+
// 2) expert_actual_use 的 requestId 必须是**服务端返回的**幂等 id(自造 UUID 不计数),
|
|
8
|
+
// 所以要先发一次真实对话并从 SSE 里抓 data.id;
|
|
9
|
+
// 3) 事件链:web_element_click + expert_summon_click + expert_summoned + expert_actual_use。
|
|
10
|
+
// Expert_lighthouse 额外要求 expert_actual_use 的 mode=LOCAL。
|
|
11
|
+
//
|
|
12
|
+
// 代价:每个任务一次真实短对话(fast-model,一条 "1+1"),与夜猫子同口径。
|
|
13
|
+
|
|
14
|
+
const CHAT_BASE = "https://copilot.tencent.com";
|
|
15
|
+
const DESKTOP_UA = "WorkBuddy/5.5.6 WorkBuddy/5.5.6 CLI/2.137.1";
|
|
16
|
+
const ID_RE = /^(cmb-)?[0-9a-f]{32}$/;
|
|
17
|
+
|
|
18
|
+
async function fetchExpertList({ uid, at, expertType, fetchImpl = compatFetch, timeoutMs = 20000 }) {
|
|
19
|
+
const body = { page: 1, page_size: 20, sort_by: "reco_rank", sort_order: "desc" };
|
|
20
|
+
if (expertType) body.expert_type = expertType;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetchImpl(`${CHAT_BASE}/portal/operation-platform/market/expert/list`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${at}`,
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"User-Agent": DESKTOP_UA,
|
|
28
|
+
"X-Domain": CHAT_BASE,
|
|
29
|
+
"X-Product": "SaaS",
|
|
30
|
+
"X-User-Id": uid,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal: timeoutSignal(timeoutMs),
|
|
34
|
+
});
|
|
35
|
+
const j = await res.json().catch(() => null);
|
|
36
|
+
if (res.status !== 200) return { ok: false, msg: `专家市场 HTTP ${res.status}` };
|
|
37
|
+
const list = j?.data?.experts || j?.experts || [];
|
|
38
|
+
if (!list.length) return { ok: false, msg: "专家市场返回空列表" };
|
|
39
|
+
return { ok: true, experts: list };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { ok: false, msg: `专家市场请求失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 发一条真实短对话,从 SSE 抓服务端 requestId(自造的不计数)。
|
|
46
|
+
async function chatForRequestId({ uid, at, expertId, fetchImpl = compatFetch, timeoutMs = 60000 }) {
|
|
47
|
+
const conversationId = `mslxdff-conv-${Date.now()}`;
|
|
48
|
+
const headers = {
|
|
49
|
+
Authorization: `Bearer ${at}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
Accept: "text/event-stream",
|
|
52
|
+
"User-Agent": DESKTOP_UA,
|
|
53
|
+
"X-Domain": CHAT_BASE,
|
|
54
|
+
"X-Product": "SaaS",
|
|
55
|
+
"X-User-Id": uid,
|
|
56
|
+
"X-Conversation-ID": conversationId,
|
|
57
|
+
"X-Request-ID": String(Date.now()),
|
|
58
|
+
"X-Agent-Intent": "craft",
|
|
59
|
+
"X-Agent-Type": "main",
|
|
60
|
+
"X-IDE-Name": "WorkBuddy",
|
|
61
|
+
"X-IDE-Type": "WorkBuddy",
|
|
62
|
+
"X-IDE-Version": "5.5.6",
|
|
63
|
+
"x-codebuddy-request": "1",
|
|
64
|
+
};
|
|
65
|
+
if (expertId) headers["X-Expert-Id"] = expertId;
|
|
66
|
+
let res;
|
|
67
|
+
try {
|
|
68
|
+
res = await fetchImpl(`${CHAT_BASE}/v2/chat/completions`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers,
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
model: "fast-model",
|
|
73
|
+
messages: [
|
|
74
|
+
{ role: "system", content: "You are a helpful assistant. 当前处于中文环境,使用简体中文回答。" },
|
|
75
|
+
{ role: "user", content: "1+1等于几?直接回答。" },
|
|
76
|
+
],
|
|
77
|
+
agent: "cli",
|
|
78
|
+
temperature: 1,
|
|
79
|
+
stream: true,
|
|
80
|
+
stream_options: { include_usage: true },
|
|
81
|
+
}),
|
|
82
|
+
signal: timeoutSignal(timeoutMs),
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { ok: false, msg: `对话请求失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
86
|
+
}
|
|
87
|
+
if (res.status !== 200) return { ok: false, msg: `对话 HTTP ${res.status}` };
|
|
88
|
+
// 只需要开头一段就能拿到 data.id;读完主动 disconnect,不消费整条流。
|
|
89
|
+
let buf = "";
|
|
90
|
+
const reader = res.body?.getReader?.();
|
|
91
|
+
if (!reader) return { ok: false, msg: "响应无 body 流" };
|
|
92
|
+
try {
|
|
93
|
+
while (buf.length < 256 * 1024) {
|
|
94
|
+
const { value, done } = await reader.read();
|
|
95
|
+
if (done) break;
|
|
96
|
+
buf += new TextDecoder().decode(value, { stream: true });
|
|
97
|
+
const m = buf.match(/"id"\s*:\s*"([^"]+)"/);
|
|
98
|
+
if (m && ID_RE.test(m[1])) return { ok: true, conversationId, requestId: m[1] };
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
} finally {
|
|
102
|
+
try { await reader.cancel?.(); } catch {}
|
|
103
|
+
}
|
|
104
|
+
const m = buf.match(/"id"\s*:\s*"([^"]+)"\s*[,}]/);
|
|
105
|
+
if (m && ID_RE.test(m[1])) return { ok: true, conversationId, requestId: m[1] };
|
|
106
|
+
return { ok: false, msg: "SSE 中未找到服务端 requestId" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function expertPayload(e, conversationId, requestId, mode) {
|
|
110
|
+
const cat = Array.isArray(e.categories) && e.categories.length ? String(e.categories[0]) : "expert-all";
|
|
111
|
+
const ver = e.version || "1.0.0";
|
|
112
|
+
return {
|
|
113
|
+
id: e.expert_id,
|
|
114
|
+
name: e.display_name_zh || e.expert_id,
|
|
115
|
+
expertTitle: e.profession_zh || "",
|
|
116
|
+
type: cat,
|
|
117
|
+
expertType: e.expert_type || "agent",
|
|
118
|
+
source: "builtin",
|
|
119
|
+
version: ver,
|
|
120
|
+
mode,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function expertEvents(e, conversationId, requestId, mode) {
|
|
125
|
+
const base = expertPayload(e, conversationId, requestId, mode);
|
|
126
|
+
const cat = base.type;
|
|
127
|
+
const ver = base.version;
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
eventCode: "web_element_click", source: e.expert_id, type: cat, version: ver,
|
|
131
|
+
elementId: "expert_summon_click", elementName: "立即召唤",
|
|
132
|
+
pageURL: "/C:/Program%20Files/WorkBuddy/resources/app.asar/renderer/index.html",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
eventCode: "expert_summon_click", id: e.expert_id, name: base.name,
|
|
136
|
+
expertTitle: base.expertTitle, type: "expert-all", position: 0,
|
|
137
|
+
expertType: base.expertType, version: ver, mode: "LOCAL",
|
|
138
|
+
},
|
|
139
|
+
{ eventCode: "expert_summoned", id: e.expert_id, name: base.name, expertTitle: base.expertTitle, type: "expert-all" },
|
|
140
|
+
{
|
|
141
|
+
eventCode: "expert_actual_use",
|
|
142
|
+
id: e.expert_id, name: base.name, expertTitle: base.expertTitle,
|
|
143
|
+
type: cat, expertType: base.expertType, source: "builtin", version: ver,
|
|
144
|
+
cost: mode === "LOCAL" ? 0 : 9000, characterCount: 14,
|
|
145
|
+
conversationId, requestId, messageId: `msg-${requestId.slice(-8)}`,
|
|
146
|
+
requestModelId: "fast-model", requestModelName: "fast-model",
|
|
147
|
+
mode,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const EXPERT_TYPE = {
|
|
153
|
+
expert_5: "agent",
|
|
154
|
+
Expert_team_use_3: "team",
|
|
155
|
+
Expert_lighthouse: "agent",
|
|
156
|
+
WorkBuddy_Valorant: "agent",
|
|
157
|
+
Hp_Appearance: "agent",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// 指定专家名(任务描述点名要某个专家时用);不匹配就退回推荐序前几个。
|
|
161
|
+
const EXPERT_NAME_HINT = {
|
|
162
|
+
Hp_Appearance: "吉事通", // 《和平精英》主题任务,市场里对应「吉事通」
|
|
163
|
+
WorkBuddy_Valorant: "无畏契约拼豆",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export async function runExpertTask(api = {}, { code, needTimes = 1, round = 0, sleepFn, onStep } = {}) {
|
|
167
|
+
const { uid, at, fetchImpl = compatFetch } = api;
|
|
168
|
+
const step = (e) => { try { onStep?.(e); } catch {} };
|
|
169
|
+
const mode = code === "Expert_lighthouse" ? "LOCAL" : "craft";
|
|
170
|
+
const wanted = EXPERT_TYPE[code] || "agent";
|
|
171
|
+
|
|
172
|
+
const listed = await fetchExpertList({ uid, at, expertType: wanted, fetchImpl });
|
|
173
|
+
if (!listed.ok) return { ok: false, done: 0, msg: listed.msg };
|
|
174
|
+
// 每次换一个专家(上游按 id 去重,同一个打多少次只算一次)。
|
|
175
|
+
// 专家团任务优先挑 team 类型;市场没给类型时退回全量。
|
|
176
|
+
const pool = listed.experts.filter((e) => !e.expert_type || e.expert_type === wanted);
|
|
177
|
+
let experts = pool.length ? pool : listed.experts;
|
|
178
|
+
// 任务点名了某个专家:把它排到最前(找不到就照常走推荐序)。
|
|
179
|
+
const hint = EXPERT_NAME_HINT[code];
|
|
180
|
+
if (hint) {
|
|
181
|
+
const hit = experts.filter((e) => String(e.display_name_zh || "").includes(hint));
|
|
182
|
+
if (hit.length) experts = [...hit, ...experts.filter((e) => !hit.includes(e))];
|
|
183
|
+
else return { ok: false, done: 0, msg: `专家市场里找不到「${hint}」(任务点名要它)` };
|
|
184
|
+
}
|
|
185
|
+
if (needTimes > experts.length) needTimes = experts.length;
|
|
186
|
+
|
|
187
|
+
let done = 0;
|
|
188
|
+
for (let i = 0; i < needTimes; i++) {
|
|
189
|
+
const e = experts[(round + i) % experts.length];
|
|
190
|
+
const chat = await chatForRequestId({ uid, at, expertId: e.expert_id, fetchImpl });
|
|
191
|
+
if (!chat.ok) {
|
|
192
|
+
step({ phase: "fire", task_code: code, i: i + 1, ok: false, msg: chat.msg, stage: "chat" });
|
|
193
|
+
return { ok: false, done, msg: chat.msg };
|
|
194
|
+
}
|
|
195
|
+
const r = await reportDesktopEvents({
|
|
196
|
+
uid, at, fetchImpl,
|
|
197
|
+
events: expertEvents(e, chat.conversationId, chat.requestId, mode),
|
|
198
|
+
});
|
|
199
|
+
step({ phase: "fire", task_code: code, i: i + 1, times: needTimes, ok: r.ok, msg: r.msg, expert: e.display_name_zh });
|
|
200
|
+
if (!r.ok) return { ok: false, done, msg: r.msg || `HTTP ${r.status}` };
|
|
201
|
+
done += 1;
|
|
202
|
+
if (i + 1 < needTimes) await sleepFn?.(1500);
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, done };
|
|
205
|
+
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
2
2
|
// WorkBuddy growth 域(/activity/growth/* 与 /v2/activity/growth/*)底层请求。
|
|
3
3
|
// 双域容灾与签到同款;业务失败结构化返回(ok:false + code/msg),不抛异常。
|
|
4
|
-
//
|
|
4
|
+
//
|
|
5
|
+
// 域名口径(本机 2026-10-20 实测):
|
|
6
|
+
// - 任务列表与领奖只在 www.workbuddy.cn 命中(web 成长中心口径);probe 时
|
|
7
|
+
// copilot.tencent.com / www.codebuddy.cn 这两个端点均未返回 code 0。
|
|
8
|
+
// - 猫猫旅行等 buddy 端点在 copilot.tencent.com 命中。
|
|
9
|
+
// 所以 bases 是「候选集」,按顺序试到第一个成功为止,不按业务分类硬编码。
|
|
5
10
|
|
|
6
11
|
export const GROWTH_BASES = [
|
|
12
|
+
"https://www.workbuddy.cn",
|
|
7
13
|
"https://copilot.tencent.com",
|
|
8
14
|
"https://www.codebuddy.cn",
|
|
9
15
|
];
|
|
10
16
|
|
|
17
|
+
// WEB_BASES 领奖专用:web 成长中心形态(x-client-platform: web)。
|
|
18
|
+
export const WEB_BASES = ["https://www.workbuddy.cn"];
|
|
19
|
+
|
|
11
20
|
export function growthHeaders({ at, uid, domain, enterpriseId }) {
|
|
12
21
|
return {
|
|
13
22
|
Authorization: `Bearer ${at}`,
|
|
@@ -23,45 +32,70 @@ export function growthHeaders({ at, uid, domain, enterpriseId }) {
|
|
|
23
32
|
};
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
// webHeaders 浏览器成长中心形态:部分端点(任务列表 / 领奖)只认这套头。
|
|
36
|
+
export function webHeaders({ at, uid, domain, enterpriseId }) {
|
|
37
|
+
return {
|
|
38
|
+
Authorization: `Bearer ${at}`,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
Accept: "application/json, text/plain, */*",
|
|
41
|
+
"X-User-Id": uid,
|
|
42
|
+
"X-Domain": domain || "www.codebuddy.cn",
|
|
43
|
+
"X-Product": "SaaS",
|
|
44
|
+
"x-client-platform": "web",
|
|
45
|
+
Origin: "https://www.workbuddy.cn",
|
|
46
|
+
Referer: "https://www.workbuddy.cn/profile/growth-center",
|
|
47
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
|
|
48
|
+
...(enterpriseId ? { "X-Enterprise-Id": enterpriseId, "X-Tenant-Id": enterpriseId } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function requestOn(base, headers, { method, path, body, fetchImpl, timeoutMs }) {
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetchImpl(base + path, {
|
|
56
|
+
method,
|
|
57
|
+
headers,
|
|
58
|
+
...(method.toUpperCase() === "GET" ? {} : { body: JSON.stringify(body ?? {}) }),
|
|
59
|
+
signal: timeoutSignal(timeoutMs),
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return { ok: false, status: 0, code: null, msg: `网络失败: ${String(e?.message || e).slice(0, 200)}`, data: {}, url: base + path };
|
|
63
|
+
}
|
|
64
|
+
let payload = null;
|
|
65
|
+
try { payload = await res.json(); } catch { payload = null; }
|
|
66
|
+
if (!payload || typeof payload !== "object") {
|
|
67
|
+
const out = { ok: false, status: res.status, code: null, msg: `非 JSON 响应 HTTP ${res.status}`, data: {}, url: base + path };
|
|
68
|
+
return res.status === 401 || res.status === 403 ? { ...out, needRefresh: true } : out;
|
|
69
|
+
}
|
|
70
|
+
const out = {
|
|
71
|
+
ok: res.status === 200 && payload.code === 0,
|
|
72
|
+
status: res.status,
|
|
73
|
+
code: payload.code,
|
|
74
|
+
msg: payload.msg || "",
|
|
75
|
+
data: payload.data || {},
|
|
76
|
+
url: base + path,
|
|
77
|
+
};
|
|
78
|
+
return res.status === 401 || res.status === 403 ? { ...out, needRefresh: true } : out;
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
export async function growthRequest({
|
|
27
82
|
uid, at, domain, enterpriseId,
|
|
28
83
|
method = "GET", path, body,
|
|
29
84
|
fetchImpl = compatFetch, timeoutMs = 15000,
|
|
85
|
+
bases = GROWTH_BASES, headers: headersFn = growthHeaders,
|
|
30
86
|
} = {}) {
|
|
31
|
-
const headers =
|
|
87
|
+
const headers = headersFn({ at, uid, domain, enterpriseId });
|
|
32
88
|
let last = null;
|
|
33
|
-
for (const base of
|
|
34
|
-
const
|
|
35
|
-
let res;
|
|
36
|
-
try {
|
|
37
|
-
res = await fetchImpl(url, {
|
|
38
|
-
method,
|
|
39
|
-
headers,
|
|
40
|
-
...(method.toUpperCase() === "GET" ? {} : { body: JSON.stringify(body ?? {}) }),
|
|
41
|
-
signal: timeoutSignal(timeoutMs),
|
|
42
|
-
});
|
|
43
|
-
} catch (e) {
|
|
44
|
-
last = { ok: false, status: 0, code: null, msg: `网络失败: ${String(e?.message || e).slice(0, 200)}`, data: {}, url };
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
let payload = null;
|
|
48
|
-
try { payload = await res.json(); } catch { payload = null; }
|
|
49
|
-
if (!payload || typeof payload !== "object") {
|
|
50
|
-
last = { ok: false, status: res.status, code: null, msg: `非 JSON 响应 HTTP ${res.status}`, data: {}, url };
|
|
51
|
-
if (res.status === 401 || res.status === 403) return { ...last, needRefresh: true };
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const out = {
|
|
55
|
-
ok: res.status === 200 && payload.code === 0,
|
|
56
|
-
status: res.status,
|
|
57
|
-
code: payload.code,
|
|
58
|
-
msg: payload.msg || "",
|
|
59
|
-
data: payload.data || {},
|
|
60
|
-
url,
|
|
61
|
-
};
|
|
89
|
+
for (const base of bases) {
|
|
90
|
+
const out = await requestOn(base, headers, { method, path, body, fetchImpl, timeoutMs });
|
|
62
91
|
if (out.ok) return out;
|
|
63
|
-
if (
|
|
92
|
+
if (out.needRefresh) return out;
|
|
64
93
|
last = out;
|
|
65
94
|
}
|
|
66
95
|
return last || { ok: false, status: 0, code: null, msg: "无可用域", data: {} };
|
|
67
96
|
}
|
|
97
|
+
|
|
98
|
+
// Web 成长中心口径(任务列表 / 领奖)。
|
|
99
|
+
export function webRequest(opts = {}) {
|
|
100
|
+
return growthRequest({ ...opts, bases: WEB_BASES, headers: webHeaders });
|
|
101
|
+
}
|
|
@@ -1,39 +1,50 @@
|
|
|
1
1
|
// WorkBuddy 成长任务策略表:哪些任务可自动完成 / 用什么事件 / 何时只能人工。
|
|
2
|
-
// 来源:workbuddy2api
|
|
2
|
+
// 来源:workbuddy2api-panel internal/upstream/{desktop,tasks}.go 的三账号实测点亮记录;
|
|
3
|
+
// 本机 2026-10-20 复核任务列表与领奖入口。
|
|
3
4
|
// 未登记任务默认 MANUAL——宁可漏做,不盲发请求污染上游日志。
|
|
4
5
|
|
|
5
|
-
export const PLAN_LEVELS = { AUTO: "auto", MULTI: "multi", MANUAL: "manual", SKIP: "skip" };
|
|
6
|
+
export const PLAN_LEVELS = { AUTO: "auto", MULTI: "multi", DESKTOP: "desktop", EXPERT: "expert", MANUAL: "manual", SKIP: "skip" };
|
|
6
7
|
export const MAX_TIMES = 10;
|
|
7
8
|
|
|
9
|
+
// DESKTOP(桌面指纹事件链)与 EXPERT(真实专家 id + 对话回执)各有独立实现,
|
|
10
|
+
// 不走 growthEvent 通道,所以 eventCodes 为空也算可执行。
|
|
11
|
+
const ACTIONABLE = new Set([PLAN_LEVELS.AUTO, PLAN_LEVELS.MULTI, PLAN_LEVELS.DESKTOP, PLAN_LEVELS.EXPERT]);
|
|
12
|
+
|
|
8
13
|
function plan(code, level, eventCodes = [], reason = "", model = "") {
|
|
9
14
|
return {
|
|
10
15
|
code, level, eventCodes, model, reason,
|
|
11
|
-
actionable: (level
|
|
16
|
+
actionable: ACTIONABLE.has(level),
|
|
12
17
|
};
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export const TASK_PLANS = {
|
|
16
|
-
// ——
|
|
21
|
+
// —— growthEvent 通道(带 growthEvent 的真实对话)——
|
|
17
22
|
chat_5: plan("chat_5", PLAN_LEVELS.MULTI, ["chat_request_send"], "对话类:带 growthEvent 的对话请求,按 target 次数重复"),
|
|
18
23
|
automation_1: plan("automation_1", PLAN_LEVELS.AUTO, ["automated_task_create_suc"], "自动化任务:创建成功事件"),
|
|
19
24
|
skill_1: plan("skill_1", PLAN_LEVELS.AUTO, ["skill_info"], "尝鲜技能:skill_info 事件"),
|
|
20
25
|
"Model_chat_GLM5.2": plan("Model_chat_GLM5.2", PLAN_LEVELS.AUTO, ["chat_request_send"], "模型体验:必须真实调用 glm-5.2(发事件包无效)", "glm-5.2"),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Expert_lighthouse: plan("Expert_lighthouse", PLAN_LEVELS.MANUAL, [], "
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
|
|
27
|
+
// —— 桌面指纹事件链(/v2/report,UA=WorkBuddy/5.5.6,X-Domain=copilot.tencent.com)——
|
|
28
|
+
RichMeow_Chat: plan("RichMeow_Chat", PLAN_LEVELS.DESKTOP, [], "桌面端对话:agent_task_created + 成功回复回执六事件链"),
|
|
29
|
+
Buddy_App: plan("Buddy_App", PLAN_LEVELS.DESKTOP, [], "发现应用:discover→show→enter_click→auth_confirm→bindaccount_skip 五连"),
|
|
30
|
+
Buddy_App_QQ: plan("Buddy_App_QQ", PLAN_LEVELS.DESKTOP, [], "企鹅教师助手:与 Buddy_App 共用事件链(固定 buddyID)"),
|
|
31
|
+
create_canvas: plan("create_canvas", PLAN_LEVELS.DESKTOP, [], "设计创意画布:chat 链 + wbx_design_canvas_{task_create,open}"),
|
|
32
|
+
template_5: plan("template_5", PLAN_LEVELS.DESKTOP, [], "使用模板:chat 链 + agent_task_created_with_template + template_used,按 target 重复"),
|
|
33
|
+
playbook_prompt: plan("playbook_prompt", PLAN_LEVELS.DESKTOP, [], "灵感案例:web_element_click + playbook_cta_click + playbook_prompt_send"),
|
|
34
|
+
Library_read: plan("Library_read", PLAN_LEVELS.MANUAL, [], "资料库:web 域页面行为(2026-10-20 实测多个事件名均不计数,需客户端真读回执)"),
|
|
35
|
+
|
|
36
|
+
// —— 专家类:拉市场真实专家 id + 一次真实对话回执 ——
|
|
37
|
+
expert_5: plan("expert_5", PLAN_LEVELS.EXPERT, [], "召唤专家:expert_summon 链 + 真实对话 requestId + expert_actual_use,按 target 重复"),
|
|
38
|
+
Expert_team_use_3: plan("Expert_team_use_3", PLAN_LEVELS.EXPERT, [], "召唤专家团:同专家链,expert_type=team"),
|
|
39
|
+
Expert_lighthouse: plan("Expert_lighthouse", PLAN_LEVELS.MANUAL, [], "轻量云专家:需在对话框上方授权连接器后对话(连接器授权无 API 判据,2026-10-20 实测不计数)"),
|
|
40
|
+
|
|
41
|
+
// —— 判据未通过 ——
|
|
42
|
+
Expert_Philanthropy: plan("Expert_Philanthropy", PLAN_LEVELS.MANUAL, [], "公益专家:领奖校验真实捐赠回执,无法自动"),
|
|
43
|
+
expert_5_paid: plan("expert_5_paid", PLAN_LEVELS.MANUAL, [], "召唤专家(付费版):需真实付费"),
|
|
44
|
+
Hp_Appearance: plan("Hp_Appearance", PLAN_LEVELS.MANUAL, [], "和平精英主题:需客户端真实切主题(2026-10-20 实测召唤吉事通专家不计数)"),
|
|
45
|
+
WorkBuddy_Valorant: plan("WorkBuddy_Valorant", PLAN_LEVELS.MANUAL, [], "无畏契约拼豆:市场当前查无此专家,暂不可自动"),
|
|
46
|
+
wb_wechat_oa_subscribe_task: plan("wb_wechat_oa_subscribe_task", PLAN_LEVELS.MANUAL, [], "关注公众号满 24 小时:需真实关注,无 API 判据"),
|
|
47
|
+
black_cat: plan("black_cat", PLAN_LEVELS.SKIP, [], "夜猫子活动:任务列表不下发,无积分收益"),
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
const DEFAULT_PLAN = plan("", PLAN_LEVELS.MANUAL, [], "未登记的任务类型,需人工确认");
|
|
@@ -55,7 +66,7 @@ export function classifyTask(task = {}) {
|
|
|
55
66
|
return {
|
|
56
67
|
...task,
|
|
57
68
|
level,
|
|
58
|
-
actionable: (level
|
|
69
|
+
actionable: ACTIONABLE.has(level),
|
|
59
70
|
strategy: p.reason,
|
|
60
71
|
model: p.model,
|
|
61
72
|
need_times: Math.max(1, Math.min(rawNeed, MAX_TIMES)),
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { compatFetch, timeoutSignal, uuid } from "../../compat.js";
|
|
2
|
-
import { GROWTH_BASES, growthHeaders, growthRequest } from "./growth-api.js";
|
|
2
|
+
import { GROWTH_BASES, growthHeaders, growthRequest, webRequest } from "./growth-api.js";
|
|
3
3
|
import { PLAN_LEVELS, MAX_TIMES, planFor, classifyTask } from "./growth-plans.js";
|
|
4
|
-
|
|
4
|
+
import { runDesktopTask } from "./desktop-report.js";
|
|
5
|
+
import { runExpertTask } from "./expert-tasks.js";
|
|
6
|
+
// WorkBuddy 成长计划任务:拉列表 → 参与(accept) → 触发(按任务类型分派) → 领奖(claim)。
|
|
5
7
|
// 机制(上游实测):进度由请求体 extra_vars.growthEvent 驱动;未 accept 不累计;
|
|
6
8
|
// completed ≠ claimed(要另调领奖)。串行 + 节流是硬要求,同账号禁并行。
|
|
7
9
|
// 策略表(哪些任务可自动 / 用什么事件)见 growth-plans.js。
|
|
10
|
+
//
|
|
11
|
+
// 三种触发通道(按 growth-plans 的 level 分派):
|
|
12
|
+
// growthEvent → 带 extra_vars.growthEvent 的真实对话(本文件 fireGrowthEvent)
|
|
13
|
+
// DESKTOP → 桌面指纹事件链(desktop-report.js)
|
|
14
|
+
// EXPERT → 专家市场真实 id + 真实对话回执(expert-tasks.js)
|
|
8
15
|
|
|
9
16
|
export { PLAN_LEVELS, MAX_TIMES, TASK_PLANS, planFor, classifyTask } from "./growth-plans.js";
|
|
10
17
|
|
|
@@ -24,16 +31,18 @@ export async function waitFor(fn, { timeoutMs = 8000, intervalMs = 500, sleepFn
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export async function growthTasks(api = {}) {
|
|
27
|
-
|
|
34
|
+
// 任务列表只认 web 成长中心口径(www.workbuddy.cn + x-client-platform: web)。
|
|
35
|
+
const r = await webRequest({ ...api, method: "GET", path: "/v2/activity/growth/tasks" });
|
|
28
36
|
return { ...r, tasks: r.ok ? (r.data?.tasks || []) : [] };
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export async function growthAccept(api = {}, codes = []) {
|
|
32
|
-
return
|
|
40
|
+
return webRequest({ ...api, method: "POST", path: "/v2/activity/growth/tasks/accept", body: { task_codes: [...codes] } });
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export async function growthClaim(api = {}, code) {
|
|
36
|
-
|
|
44
|
+
// 领奖走 web 域:/activity/growth/tasks/<code>/claim(任务码在路径里,无 body,无 /v2 前缀)。
|
|
45
|
+
return webRequest({ ...api, method: "POST", path: `/activity/growth/tasks/${encodeURIComponent(code)}/claim` });
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
// 通过带 growthEvent 的模型请求触发进度:正常 200 + 免费模型 + max_tokens=1(成本 0)。
|
|
@@ -108,6 +117,18 @@ export async function runGrowthAccount({
|
|
|
108
117
|
step({ phase: "task", task_code: code, skipped: info.strategy });
|
|
109
118
|
continue;
|
|
110
119
|
}
|
|
120
|
+
// 进度已达标但还没领奖:直接进领奖环节,别再打一次(会白耗一次对话)。
|
|
121
|
+
const cur = Number(info.progress?.current ?? 0);
|
|
122
|
+
const tgt = Number(info.progress?.target ?? 0);
|
|
123
|
+
if (info.accept_status === "completed" || (tgt > 0 && cur >= tgt)) {
|
|
124
|
+
out.tasks.push({
|
|
125
|
+
task_code: code, title: info.title, ok: true, fired: 0,
|
|
126
|
+
status: "completed", progress: `${cur}/${tgt}`, note: "进度已达标,待领奖",
|
|
127
|
+
});
|
|
128
|
+
step({ phase: "task", ...out.tasks[out.tasks.length - 1] });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let needTimes = info.need_times;
|
|
111
132
|
if (info.accept_status === "not_accepted") {
|
|
112
133
|
const ar = await growthAccept(api, [code]);
|
|
113
134
|
step({ phase: "accept", task_code: code, ok: ar.ok, msg: ar.msg });
|
|
@@ -117,30 +138,75 @@ export async function runGrowthAccount({
|
|
|
117
138
|
return !!t && !!t.accept_status && t.accept_status !== "not_accepted";
|
|
118
139
|
}, { timeoutMs: g.acceptTimeoutMs, intervalMs: g.pollMs, sleepFn });
|
|
119
140
|
}
|
|
141
|
+
// accept 之前上游不下发真实 target(progress 恒 0/0),必须 accept 后重读,
|
|
142
|
+
// 否则「召唤 5 次专家」这类会被当成 1 次,永远停在 1/5。
|
|
143
|
+
const fresh = await latestOf(code);
|
|
144
|
+
if (fresh) {
|
|
145
|
+
const re = classifyTask(fresh);
|
|
146
|
+
if (re.need_times > needTimes) needTimes = re.need_times;
|
|
147
|
+
}
|
|
120
148
|
}
|
|
149
|
+
// 需要重复的任务:每打一次就重读真实进度,边打边补差额。
|
|
150
|
+
// (accept 前 target 恒 0/0;且上一次运行可能已经推进了一部分,
|
|
151
|
+
// 只按开跑时的快照算次数会永远差一口气。)
|
|
152
|
+
const remaining = async () => {
|
|
153
|
+
const t = await latestOf(code);
|
|
154
|
+
if (!t) return 0;
|
|
155
|
+
if (t.accept_status === "completed" || t.accept_status === "claimed") return 0;
|
|
156
|
+
const cur = Number(t.progress?.current ?? 0);
|
|
157
|
+
const tgt = Number(t.progress?.target ?? 0);
|
|
158
|
+
return tgt > cur ? tgt - cur : 0;
|
|
159
|
+
};
|
|
160
|
+
// 封顶 MAX_TIMES 次(单任务最多打 10 次,防任务 target 异常时打爆上游)。
|
|
161
|
+
// 循环条件走 remaining():每次重读真实进度,用完差额即停。
|
|
121
162
|
let fired = 0;
|
|
122
|
-
|
|
163
|
+
const level = info.level || planFor(code).level;
|
|
164
|
+
const fireOnce = async (round = 0) => {
|
|
165
|
+
if (level === PLAN_LEVELS.DESKTOP) return runDesktopTask(api, { code, needTimes: 1, round, sleepFn, onStep: step });
|
|
166
|
+
if (level === PLAN_LEVELS.EXPERT) return runExpertTask(api, { code, needTimes: 1, round, sleepFn, onStep: step });
|
|
123
167
|
const r = await fireGrowthEvent(api, { eventCodes: info.eventCodes || planFor(code).eventCodes, model: info.model || model });
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
step({ phase: "fire", task_code: code, i: fired + 1, ok: r.ok, msg: r.msg });
|
|
169
|
+
return { ok: r.ok, msg: r.msg };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < MAX_TIMES; i++) {
|
|
173
|
+
const left = await remaining();
|
|
174
|
+
if (!left) break;
|
|
175
|
+
const r = await fireOnce(i);
|
|
176
|
+
if (!r.ok) break;
|
|
177
|
+
fired += 1;
|
|
178
|
+
// 上游按「对象 + 天」去重:同一个模板/专家打第二次不加进度。
|
|
179
|
+
// 事件是异步计分的,多等一轮再判「没涨」,避免把慢计分误判成去重。
|
|
180
|
+
let after = left;
|
|
181
|
+
for (let w = 0; w < 3; w++) {
|
|
182
|
+
await sleepFn(Math.max(g.eventMs, 1500));
|
|
183
|
+
after = await remaining();
|
|
184
|
+
if (after < left) break;
|
|
185
|
+
}
|
|
186
|
+
if (after >= left) break; // 进度没动:大概率是按对象去重,别再空打
|
|
127
187
|
}
|
|
188
|
+
const final = await latestOf(code);
|
|
128
189
|
const base = info.progress?.current ?? 0;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
190
|
+
if (final && final.accept_status !== "completed") {
|
|
191
|
+
await waitFor(async () => {
|
|
192
|
+
const t = await latestOf(code);
|
|
193
|
+
if (!t) return false;
|
|
194
|
+
if (t.accept_status === "completed" || t.accept_status === "claimed") return true;
|
|
195
|
+
return Number(t.progress?.current ?? 0) > Number(base);
|
|
196
|
+
}, { timeoutMs: g.verifyTimeoutMs, intervalMs: g.pollMs, sleepFn });
|
|
197
|
+
}
|
|
135
198
|
const latest = (await latestOf(code)) || info;
|
|
199
|
+
const doneNow = ["completed", "claimed"].includes(String(latest.accept_status || ""));
|
|
136
200
|
const item = {
|
|
137
201
|
task_code: code,
|
|
138
202
|
title: latest.title || info.title || code,
|
|
139
|
-
|
|
203
|
+
// 上报成功 ≠ 计分成功:进度没动就别报 ok(面板会显示"已完成",等于谎报)。
|
|
204
|
+
ok: doneNow || fired > 0,
|
|
140
205
|
fired,
|
|
141
|
-
times: info.need_times,
|
|
206
|
+
times: latest.progress?.target ?? info.need_times,
|
|
142
207
|
status: latest.accept_status,
|
|
143
208
|
progress: `${latest.progress?.current ?? 0}/${latest.progress?.target ?? "?"}`,
|
|
209
|
+
...(fired > 0 && !doneNow ? { note: "事件已上报但进度未走(该任务判据可能只认客户端真实行为)" } : {}),
|
|
144
210
|
};
|
|
145
211
|
out.tasks.push(item);
|
|
146
212
|
step({ phase: "task", ...item });
|
|
@@ -46,6 +46,7 @@ export async function setupWorkbuddyGrowth({ bus, logs } = {}) {
|
|
|
46
46
|
const stateMod = await import("../state.js");
|
|
47
47
|
const { runGrowthAll } = await import("../providers/workbuddy/growth.js");
|
|
48
48
|
const { runCatTravel } = await import("../providers/workbuddy/cat-travel.js");
|
|
49
|
+
const { reportChatActivity, runStreakBonus } = await import("../providers/workbuddy/activity.js");
|
|
49
50
|
const { createAuthService, decodeJwtExp } = await import("../providers/workbuddy/auth.js");
|
|
50
51
|
|
|
51
52
|
let running = false;
|
|
@@ -83,6 +84,23 @@ export async function setupWorkbuddyGrowth({ bus, logs } = {}) {
|
|
|
83
84
|
tasks: row.tasks.filter((t) => t.ok && !t.skipped).length, reason,
|
|
84
85
|
}),
|
|
85
86
|
});
|
|
87
|
+
// 活跃上报必须单独发:签到不点亮连登(2026-10-20 实测 days 恒 0),
|
|
88
|
+
// 不报就连登档位 7/14/28 天的积分与抽奖次数永远拿不到。
|
|
89
|
+
const activity = [];
|
|
90
|
+
const streak = [];
|
|
91
|
+
for (const a of accounts) {
|
|
92
|
+
const r = await reportChatActivity(a);
|
|
93
|
+
activity.push({ uid: a.uid, ok: r.ok, msg: r.msg });
|
|
94
|
+
emit("workbuddy-activity", { uid: String(a.uid).slice(0, 8), ok: r.ok, msg: r.msg, reason });
|
|
95
|
+
const s = await runStreakBonus({ ...a, onStep: (e) => emit("workbuddy-streak", { uid: String(a.uid).slice(0, 8), ...e }) });
|
|
96
|
+
streak.push({ uid: a.uid, days: s.days, redeemed: s.redeemed, drawn: s.drawn, credit: s.credit, errors: s.errors });
|
|
97
|
+
emit("workbuddy-streak-done", {
|
|
98
|
+
uid: String(a.uid).slice(0, 8), days: s.days,
|
|
99
|
+
redeemed: s.redeemed.map((x) => x.tier).join(",") || "-", drawn: s.drawn, credit: s.credit,
|
|
100
|
+
errors: s.errors.length ? s.errors : undefined, reason,
|
|
101
|
+
});
|
|
102
|
+
await sleep(1000);
|
|
103
|
+
}
|
|
86
104
|
const travel = [];
|
|
87
105
|
for (const a of accounts) {
|
|
88
106
|
try {
|
|
@@ -95,17 +113,21 @@ export async function setupWorkbuddyGrowth({ bus, logs } = {}) {
|
|
|
95
113
|
await sleep(1000);
|
|
96
114
|
}
|
|
97
115
|
const date = todayKey();
|
|
98
|
-
const
|
|
116
|
+
const streakCredit = streak.reduce((s, r) => s + (r.credit || 0), 0);
|
|
117
|
+
const creditTotal = growthRes.creditTotal + travel.reduce((s, r) => s + (r.credits || 0), 0) + streakCredit;
|
|
99
118
|
try {
|
|
100
119
|
stateMod.writeStateImmediate(stateMod.defaultStateFile(), {
|
|
101
120
|
workbuddyGrowth: {
|
|
102
121
|
date, at: Date.now(), ok: growthRes.ok, credit: creditTotal, accounts: accounts.length,
|
|
122
|
+
streakCredit,
|
|
123
|
+
activity: activity.map((a) => ({ uid: a.uid, ok: a.ok })),
|
|
124
|
+
streak: streak.map((s) => ({ uid: s.uid, days: s.days, drawn: s.drawn, credit: s.credit })),
|
|
103
125
|
travel: travel.map((t) => ({ uid: t.uid, outcome: t.outcome, credits: t.credits })),
|
|
104
126
|
},
|
|
105
127
|
});
|
|
106
128
|
} catch {}
|
|
107
|
-
emit("workbuddy-growth-done", { date, accounts: accounts.length, credit: creditTotal, ok: growthRes.ok, reason });
|
|
108
|
-
return { ok: true, creditTotal, growthRes, travel };
|
|
129
|
+
emit("workbuddy-growth-done", { date, accounts: accounts.length, credit: creditTotal, streakCredit, ok: growthRes.ok, reason });
|
|
130
|
+
return { ok: true, creditTotal, streakCredit, growthRes, travel, streak };
|
|
109
131
|
} catch (e) {
|
|
110
132
|
emit("workbuddy-growth-failed", { error: String(e?.message || e).slice(0, 200), reason });
|
|
111
133
|
return { ok: false, error: String(e?.message || e).slice(0, 200) };
|