diff --git a/app/growth_chains.py b/app/growth_chains.py index 9e0c5f1..71fc625 100644 --- a/app/growth_chains.py +++ b/app/growth_chains.py @@ -36,6 +36,28 @@ QQ_BUDDY_ID = "cb_y5Dy46tPQGGWtueMxXbe" QQ_BUDDY_NAME = "企鹅教师助手" LIGHTHOUSE_EXPERT_ID = "ex_2cvvUZQhDyeJ" +NAMED_EXPERT_TASKS = { + "WorkBuddy_Valorant": { + "expert_id": "ex_xQHHD1u5Jkua", "expert_type": "agent", + "display_name_zh": "无畏契约拼豆专家", "profession_zh": "无畏契约拼豆专家", + "version": "1.2.7", "categories": ["03-GameSpatial", "06-ContentCreative"]}, + "expert_use_bowangwang": { + "expert_id": "ex_WZY136wV5aMt", "expert_type": "agent", + "display_name_zh": "播旺旺", "profession_zh": "直播电商教练", + "version": "1.0.2", "categories": ["05-MarketingGrowth", "18-Double11"]}, + "expert_use_douyinyin": { + "expert_id": "ex_FbY5nlWTqDod", "expert_type": "agent", + "display_name_zh": "斗音音", "profession_zh": "抖音策略师", + "version": "1.0.2", "categories": ["06-ContentCreative", "18-Double11"]}, + "expert_use_maidehao": { + "expert_id": "ex_GDl31WahyIH0", "expert_type": "agent", + "display_name_zh": "卖得好", "profession_zh": "中国电商运营专家", + "version": "1.0.1", "categories": ["05-MarketingGrowth", "18-Double11"]}, + "expert_use_shangjia_xuanchuan": { + "expert_id": "ex_Weo3x2EZuSDm", "expert_type": "agent", + "display_name_zh": "商家宣传设计师", "profession_zh": "商家宣传页与海报设计师", + "version": "1.6.0", "categories": ["01-ProductDesign", "05-MarketingGrowth", "18-Double11"]}, +} SKILL_NAME = "润泽小馆·日报撰写" SKILL_ID = "skill_2097350077599879168" THEME_KEY = "theme-tkmw7j" @@ -279,18 +301,16 @@ def run_expert_team_3(ctx, task): return _run_expert_batch(ctx, "team", 3) -def run_expert_lighthouse(ctx, task): - expert = {"expert_id": LIGHTHOUSE_EXPERT_ID, "expert_type": "agent", - "display_name_zh": "腾讯轻量云专家", "profession_zh": "腾讯轻量云专家", - "version": "1.0.2", "categories": []} +def _run_named_expert(ctx, expert, *, local=False): + expert = dict(expert) try: for candidate in growth_events.market_expert_list(ctx.base, ctx.token, ctx.uid, "agent", domain=ctx.domain): - if candidate.get("expert_id") == LIGHTHOUSE_EXPERT_ID: + if candidate.get("expert_id") == expert["expert_id"]: expert = candidate break except growth_events.Failure: - pass # 市场不可用时用内置专家信息(id 为平台真实 id) + pass # Fall back to the verified task-specific id when the market is unavailable. _report_desktop(ctx, growth_events.expert_summon_sequence(expert)) conversation_id, request_id = _real_chat(ctx, expert_id=expert["expert_id"]) events = growth_events.chat_sequence(conversation_id, request_id, @@ -299,11 +319,40 @@ def run_expert_lighthouse(ctx, task): if event["eventCode"] == "agent_task_created": event.update(has_expert=True, expert_id=expert["expert_id"], expert_name=expert.get("display_name_zh") or "", expert_industry_id="") - events.append(growth_events.expert_actual_use(expert, conversation_id, request_id, mode="LOCAL")) + events.append(growth_events.expert_actual_use( + expert, conversation_id, request_id, mode="LOCAL" if local else "craft")) _report_desktop(ctx, events) + return f"已上报「{expert.get('display_name_zh') or expert['expert_id']}」召唤+使用链(真实对话 requestId)" + + +def run_expert_lighthouse(ctx, task): + _run_named_expert(ctx, { + "expert_id": LIGHTHOUSE_EXPERT_ID, "expert_type": "agent", + "display_name_zh": "腾讯轻量云专家", "profession_zh": "腾讯轻量云专家", + "version": "1.0.2", "categories": []}, local=True) return "已上报轻量云专家召唤+使用链(真实对话 requestId)" +def run_workbuddy_valorant(ctx, task): + return _run_named_expert(ctx, NAMED_EXPERT_TASKS["WorkBuddy_Valorant"]) + + +def run_expert_bowangwang(ctx, task): + return _run_named_expert(ctx, NAMED_EXPERT_TASKS["expert_use_bowangwang"]) + + +def run_expert_douyinyin(ctx, task): + return _run_named_expert(ctx, NAMED_EXPERT_TASKS["expert_use_douyinyin"]) + + +def run_expert_maidehao(ctx, task): + return _run_named_expert(ctx, NAMED_EXPERT_TASKS["expert_use_maidehao"]) + + +def run_expert_shangjia_xuanchuan(ctx, task): + return _run_named_expert(ctx, NAMED_EXPERT_TASKS["expert_use_shangjia_xuanchuan"]) + + def run_black_cat(ctx, task): if not in_night_window(): return "当前不在 23:00–08:00 计数窗口,行为不计分;窗口内自动补足" @@ -522,6 +571,11 @@ def events(): "Hp_Appearance": run_hp_appearance, "Model_chat_GLM5.2": run_model_chat_glm, "skill_1": run_skill_1, "expert_5": run_expert_5, "Expert_team_use_3": run_expert_team_3, "Expert_lighthouse": run_expert_lighthouse, + "WorkBuddy_Valorant": run_workbuddy_valorant, + "expert_use_bowangwang": run_expert_bowangwang, + "expert_use_douyinyin": run_expert_douyinyin, + "expert_use_maidehao": run_expert_maidehao, + "expert_use_shangjia_xuanchuan": run_expert_shangjia_xuanchuan, "black_cat": run_black_cat, "school_season": run_school_season, "Sequential_Tasks_1": run_sequential_1, "Sequential_Tasks_2": run_sequential_2, "Sequential_Tasks_3": run_sequential_3, diff --git a/app/growth_tasks.py b/app/growth_tasks.py index 3ae9eba..cc38e87 100644 --- a/app/growth_tasks.py +++ b/app/growth_tasks.py @@ -30,6 +30,9 @@ "Hp_Appearance": "behavior", "Model_chat_GLM5.2": "chat", "skill_1": "chat", "expert_5": "chat", "Expert_team_use_3": "chat", "Expert_lighthouse": "chat", "black_cat": "chat", + "WorkBuddy_Valorant": "chat", "expert_use_bowangwang": "chat", + "expert_use_douyinyin": "chat", "expert_use_maidehao": "chat", + "expert_use_shangjia_xuanchuan": "chat", "school_season": "miniprogram", "Sequential_Tasks_1": "miniprogram", "Sequential_Tasks_2": "miniprogram", "Sequential_Tasks_3": "miniprogram", "Sequential_Tasks_4": "miniprogram", "Sequential_Tasks_5": "miniprogram", diff --git a/docs/advanced.md b/docs/advanced.md index 553de88..6373ddb 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -142,7 +142,7 @@ Upstream client identities follow verified official packages: domestic CLI 2.165 ## Automation and rewards Automatic check-in and Buddy travel are per-account switches (WebUI credentials page): on by default for domestic personal accounts, off internationally, applied live without restart. The growth center is personal-only, so enterprise accounts never travel and no longer surface the upstream 403. -The growth-center sweep (`auto_growth`, on by default for domestic personal accounts) rides the hourly maintenance pass as pure accept/claim: it claims finished tasks for credit and energy and never starts a behavior chain by itself. Per-task `growth_auto` switches (off by default) opt specific tasks into automation, so the sweep also drives the matching chain — real conversations, app opens or expert summons that may spend credits. The credential drawer lists live tasks with one-click manual runs for a single task or all enabled ones, and task switches can be copied to selected accounts (`POST /admin/credentials/growth-batch`); `GET /admin/credentials/{id}/growth-tasks`, `.../growth-tasks/auto` and `.../auto-all` back the panel. Manual runs are server-side jobs, one per account: the request returns the job at once, closing the drawer or the page does not stop it, starting again while it runs returns the running job, and `GET /admin/growth-jobs` lists every account's current or last job with the task in progress for the page banner. Manual runs and sweep claims are audited; enterprise or international accounts never see the panel. +The growth-center sweep (`auto_growth`, on by default for domestic personal accounts) rides the hourly maintenance pass as pure accept/claim: it claims finished tasks for credit and energy and never starts a behavior chain by itself. Per-task `growth_auto` switches (off by default) opt specific tasks into automation, so the sweep also drives the matching chain — real conversations, app opens or expert summons that may spend credits. The credential drawer lists live tasks with one-click manual runs for a single task or all enabled ones, and task switches can be copied to selected accounts (`POST /admin/credentials/growth-batch`); `GET /admin/credentials/{id}/growth-tasks`, `.../growth-tasks/auto` and `.../auto-all` back the panel. Manual runs are server-side jobs, one per account: the request returns the job at once, closing the drawer or the page does not stop it, starting again while it runs returns the running job, and `GET /admin/growth-jobs` lists every account's current or last job with the task in progress for the page banner. Named expert tasks use verified task-specific expert IDs through the same opt-in real-conversation chain. Manual runs and sweep claims are audited; enterprise or international accounts never see the panel. Check-in, travel, growth and daily-chat results or exceptions carrying 11140 use the same persistent two-hour restricted state as inference. The current sweep stops immediately, including remaining growth claims, and later sweeps skip the account until the rest expires or an operator releases it; other codes retain their existing logging and retry behavior. `CODEBUDDY2API_AUTO_ACCEPT_BUDDY` is startup-only and defaults to `false`. It preauthorizes enabled domestic accounts for first-Buddy onboarding, agreement and travel; automatic travel still respects its account switch. `first_buddy` needs no acceptance API: pending states, including `not_accepted`, allow one real domestic WorkBuddy conversation on that account. Prefer an eligible zero-rate model, otherwise the lowest known rate; request at most 32 output tokens with possible credit usage. Other reward tasks, paid boxes, pet switching and international trials are excluded. diff --git a/docs/advanced.zh-CN.md b/docs/advanced.zh-CN.md index 5f2ae1a..694ba89 100644 --- a/docs/advanced.zh-CN.md +++ b/docs/advanced.zh-CN.md @@ -143,7 +143,7 @@ scoped 模式可选传入 `X-Codebuddy-Session-ID`、`metadata.conversation_id` 自动签到与 Buddy 旅行是按账号设置的开关(WebUI 凭证页):国内个人账号默认开启,国际默认关闭,保存即生效,无需重启。成长中心仅面向个人账号,企业账号不参与旅行(也不再显示上游 403)。 -成长中心补领(`auto_growth`,国内个人账号默认开启)随每小时维护扫描执行,只做接取与领取:领取已完成任务的积分与能量,自身不启动任何行为链。任务级 `growth_auto` 开关(默认全部关闭)把指定任务纳入自动化,扫描随后驱动对应行为链——可能消耗积分的真实对话、应用体验或专家召唤。凭证页展示实时任务列表,支持单任务与已开开关任务的一键手动执行,任务开关可复制到已选账号(`POST /admin/credentials/growth-batch`);面板由 `GET /admin/credentials/{id}/growth-tasks`、`.../growth-tasks/auto` 与 `.../auto-all` 支撑。手动执行是服务端后台任务(每个账号同时只有一个):请求立即返回任务,关闭抽屉或页面不影响执行,运行中再次发起会返回当前任务;`GET /admin/growth-jobs` 列出各账号当前或最近一次任务及正在执行的任务,供页面横幅显示。手动执行与扫描领取均入审计;企业与国际账号不显示该面板。 +成长中心补领(`auto_growth`,国内个人账号默认开启)随每小时维护扫描执行,只做接取与领取:领取已完成任务的积分与能量,自身不启动任何行为链。任务级 `growth_auto` 开关(默认全部关闭)把指定任务纳入自动化,扫描随后驱动对应行为链——可能消耗积分的真实对话、应用体验或专家召唤。凭证页展示实时任务列表,支持单任务与已开开关任务的一键手动执行,任务开关可复制到已选账号(`POST /admin/credentials/growth-batch`);面板由 `GET /admin/credentials/{id}/growth-tasks`、`.../growth-tasks/auto` 与 `.../auto-all` 支撑。手动执行是服务端后台任务(每个账号同时只有一个):请求立即返回任务,关闭抽屉或页面不影响执行,运行中再次发起会返回当前任务;`GET /admin/growth-jobs` 列出各账号当前或最近一次任务及正在执行的任务,供页面横幅显示。指定专家任务使用已核实的任务专属专家 ID,沿用需主动开启的真实对话链。手动执行与扫描领取均入审计;企业与国际账号不显示该面板。 签到、旅行、成长和活跃打卡的结果或异常携带 11140 时,与推理共用持久两小时官方限制状态。当前扫描立即停止,包括剩余成长领取;限制到期或人工解除前,后续扫描跳过该账号;其他业务码保持原有日志和重试行为。 `CODEBUDDY2API_AUTO_ACCEPT_BUDDY` 仅启动读取,默认 `false`;预授权已启用国内账号完成首次领猫任务、协议及旅行,自动旅行仍受账号开关控制。`first_buddy` 无需单独接取,包含 `not_accepted` 在内的待完成状态可直接发起一次本账号的真实国内 WorkBuddy 对话;优先可用零倍率模型,否则取最低已知倍率,最多请求 32 个输出 token,可能消耗少量积分。不执行其他奖励任务、不付费开盒、不切猫、不领取国际试用积分。 diff --git a/tests/test_growth_chains.py b/tests/test_growth_chains.py index 6ae59d9..d0be9b1 100755 --- a/tests/test_growth_chains.py +++ b/tests/test_growth_chains.py @@ -220,6 +220,54 @@ def test_real_chat_request_id_shape(self): self.assertTrue(growth_events.SERVER_ID_RE.match("a" * 32)) +class NamedExpertTests(ChainHarness): + def exercise(self, code, expert): + reports, seen = [], {} + + def real_chat(_ctx, expert_id="", model=growth_chains.DEFAULT_CHAT_MODEL): + seen["expert_id"] = expert_id + return "wb2api-conv-x", "cmb-" + "a" * 32 + + # Same-name market entries must never replace the task-specific expert id. + wrong = {"expert_id": "ex_wrong", "display_name_zh": expert["display_name_zh"]} + ctx = growth_chains.Context(base="https://www.workbuddy.cn", token="tok", uid="u1") + with patch.object(growth_events, "market_expert_list", return_value=[wrong]), \ + patch.object(growth_chains, "_real_chat", side_effect=real_chat), \ + patch.object(growth_chains, "_report_desktop", + side_effect=lambda _ctx, events: reports.append(events)): + message = growth_chains.RUNNERS[code](ctx, raw_task(code)) + return message, reports, seen + + def test_cloud_expert_tasks_keep_their_verified_ids(self): + for code, expert in growth_chains.NAMED_EXPERT_TASKS.items(): + with self.subTest(code=code): + message, reports, seen = self.exercise(code, expert) + self.assertEqual(seen["expert_id"], expert["expert_id"]) + self.assertEqual(growth_tasks.GROUPS[code], "chat") + self.assertIn(expert["display_name_zh"], message) + summoned = next(event for report in reports for event in report + if event["eventCode"] == "expert_summoned") + actual = next(event for report in reports for event in report + if event["eventCode"] == "expert_actual_use") + self.assertEqual(summoned["id"], expert["expert_id"]) + self.assertEqual(actual["id"], expert["expert_id"]) + self.assertEqual(actual["name"], expert["display_name_zh"]) + self.assertEqual(actual["version"], expert["version"]) + self.assertEqual(actual["type"], expert["categories"][0]) + self.assertEqual(actual["mode"], "craft") + + def test_lighthouse_keeps_its_local_event_shape(self): + expert = {"expert_id": growth_chains.LIGHTHOUSE_EXPERT_ID, + "display_name_zh": "腾讯轻量云专家"} + _message, reports, seen = self.exercise("Expert_lighthouse", expert) + self.assertEqual(seen["expert_id"], growth_chains.LIGHTHOUSE_EXPERT_ID) + actual = next(event for report in reports for event in report + if event["eventCode"] == "expert_actual_use") + self.assertEqual(actual["mode"], "LOCAL") + self.assertEqual(actual["type"], "") + self.assertEqual(actual["cost"], 0) + + class NightWindowTests(ChainHarness): def test_window_boundaries(self): for hour, expected in ((22, False), (23, True), (0, True), (7, True), (8, False)):