mslxdff 0.1.225 → 0.1.227

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.
@@ -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
- // 注意:领奖路径 /activity/growth/tasks/{code}/claim 无 /v2 前缀(上游实测形态)。
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 = growthHeaders({ at, uid, domain, enterpriseId });
87
+ const headers = headersFn({ at, uid, domain, enterpriseId });
32
88
  let last = null;
33
- for (const base of GROWTH_BASES) {
34
- const url = base + path;
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 (res.status === 401 || res.status === 403) return { ...out, needRefresh: true };
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 admin/growth_plans.py 的逐条实测结论(1131 事件枚举 + 客户端机制证据)。
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 === PLAN_LEVELS.AUTO || level === PLAN_LEVELS.MULTI) && eventCodes.length > 0,
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
- // —— 机制上必须客户端(上游枚举 1131 事件未命中)——
22
- RichMeow_Chat: plan("RichMeow_Chat", PLAN_LEVELS.MANUAL, [], "桌面端对话:上游按客户端类型判定,需真实桌面端"),
23
- expert_5: plan("expert_5", PLAN_LEVELS.MANUAL, [], "召唤专家:客户端本地专家包下载+激活,非服务端事件"),
24
- expert_5_paid: plan("expert_5_paid", PLAN_LEVELS.MANUAL, [], "召唤专家(付费版):同上"),
25
- Expert_team_use_3: plan("Expert_team_use_3", PLAN_LEVELS.MANUAL, [], "召唤专家团:同上"),
26
- Hp_Appearance: plan("Hp_Appearance", PLAN_LEVELS.MANUAL, [], "和平精英主题:桌面端本地设置状态"),
27
- create_canvas: plan("create_canvas", PLAN_LEVELS.MANUAL, [], "设计创意模式:需真实创建画布"),
28
- template_5: plan("template_5", PLAN_LEVELS.MANUAL, ["agent_task_created_with_template"], "使用模板:尝试模板事件未验证,暂按人工"),
29
- playbook_prompt: plan("playbook_prompt", PLAN_LEVELS.MANUAL, [], "灵感案例:未找到有效事件"),
30
- Library_read: plan("Library_read", PLAN_LEVELS.MANUAL, [], "资料库:未找到有效事件"),
31
- Expert_Philanthropy: plan("Expert_Philanthropy", PLAN_LEVELS.MANUAL, [], "公益专家:未找到有效事件"),
32
- Buddy_App: plan("Buddy_App", PLAN_LEVELS.MANUAL, [], "发现应用:未找到有效事件"),
33
- Buddy_App_QQ: plan("Buddy_App_QQ", PLAN_LEVELS.MANUAL, [], "企鹅教师助手:未找到有效事件"),
34
- Expert_lighthouse: plan("Expert_lighthouse", PLAN_LEVELS.MANUAL, [], "腾讯轻量云专家:未找到有效事件"),
35
- // —— 无收益 ——
36
- black_cat: plan("black_cat", PLAN_LEVELS.SKIP, [], "夜猫子折扣活动,奖励为 0"),
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 === PLAN_LEVELS.AUTO || level === PLAN_LEVELS.MULTI) && p.eventCodes.length > 0,
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
- // WorkBuddy 成长计划任务:拉列表 → 参与(accept) → 触发(growthEvent) → 领奖(claim)。
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
- const r = await growthRequest({ ...api, method: "GET", path: "/v2/activity/growth/tasks" });
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 growthRequest({ ...api, method: "POST", path: "/v2/activity/growth/tasks/accept", body: { task_codes: [...codes] } });
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
- return growthRequest({ ...api, method: "POST", path: `/activity/growth/tasks/${code}/claim` });
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
- for (let i = 0; i < info.need_times; i++) {
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
- if (r.ok) fired += 1;
125
- step({ phase: "fire", task_code: code, i: i + 1, times: info.need_times, ok: r.ok, msg: r.msg });
126
- if (i + 1 < info.need_times) await sleepFn(g.eventMs);
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
- await waitFor(async () => {
130
- const t = await latestOf(code);
131
- if (!t) return false;
132
- if (t.accept_status === "completed" || t.accept_status === "claimed") return true;
133
- return Number(t.progress?.current ?? 0) > Number(base);
134
- }, { timeoutMs: g.verifyTimeoutMs, intervalMs: g.pollMs, sleepFn });
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
- ok: fired > 0,
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 });