Claude Skill

douyin-account-diagnosis

一键诊断抖音账号运营健康度。基于红狐数据服务获取账号画像与近期作品数据,从账号基础画像、内容生产力、互动健康度、内容质量、内容趋势、粉丝质量六个维度进行量化诊断,输出100分制综合评分、维度得分明细、六类风险预警和针对性优化建议。触发词:当用户提到'诊断抖音账号'、'抖音账号分析'、'抖音体检'、'抖音账号评估'、'查看XX抖音数据'、'分析这个抖音号'时使用

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download redfox-data-redfox-community-skills_douyin-account-diagnosis-5e7b435.zip · 74 KB
Part of redfox-data/redfox-community — 66 skills

Install

skills CLI npx skills add https://github.com/redfox-data/redfox-community/tree/main/skills/douyin-account-diagnosis
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install redfox-data-redfox-community@llmmart
Git git clone https://github.com/redfox-data/redfox-community.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole redfox-data/redfox-community collection as a plugin from our marketplace. Git is the plain clone.

README

抖音账号诊断 / douyin-account-diagnosis


简介

一键诊断抖音账号运营健康度。只需提供账号昵称或抖音号,即可自动获取账号画像与近期作品数据,从六个维度完成量化诊断,输出100分制综合评分、得分明细、风险预警和优化方向——几分钟拿到一份结构清晰的诊断报告。

核心价值

  • 全维度量化:覆盖账号基础画像、内容生产力、互动健康度、内容质量、内容趋势、粉丝质量六大维度,不留盲区。
  • 风险自动预警:僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险自动识别并按等级展示。
  • 报告即拿即用:标准格式输出,评分可解释、结论可复核,直接用于团队汇报、竞品分析或合作评估。

适用对象

  • 📊 抖音运营者 — 自查账号健康度,定位短板,获得可落地的优化方向。
  • 🏢 MCN / 品牌方 — 批量评估达人账号质量,筛查合作风险,辅助签约决策。
  • 🔍 内容创作者 — 对标竞品数据,找到差距,制定内容策略。

功能特性

核心功能

  • 一键数据获取:输入昵称或抖音号,自动拉取账号画像与近期作品,无需手动搜集数据。
  • 六维度量化诊断:从账号画像、生产力、互动、质量、趋势、粉丝质量六个维度逐项计分,综合加权得出总分。
  • 100分制综合评分:加权计算综合得分,配合四级评级(优质 / 正常 / 待优化 / 风险),一个数字看懂账号水平。
  • 风险预警:自动识别僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险,并给出具体数据解读。
  • 近期作品明细:默认输出全部近期作品的互动数据,标题可点击跳转直达作品,诊断结论有据可查。

特色亮点

  • 量级自适应基准:评分基准按账号量级(S超头部 / A头部 / B腰部 / C尾部)分档。抖音互动率随粉丝体量自然衰减,同样 1.55% 的互动率,对 670 万粉账号属于良好(单条均互动 10 万+),对 5 万粉账号则偏低——用一套绝对阈值会系统性误判大号。反向亦然:4852 次互动对 2251 粉的尾部号是粉丝数的 2 倍,对超头部号则只是零头。所有绝对量级类子项(爆款率、作品均互动、作品总量、人均获赞)同样分档。
  • 效率与产量双向可比:作品总量按量级分档(精品型明星号单条成本高、产量天然低,不该按日更博主的节奏被扣分),人均获赞也按量级分档(该指标与量级正相关,基准递增)。两者相对化后,93.6 倍的效率差不再被满分封顶抹平——精品型 S 级号与高产型 A 级号可被公平区分,模型不会只奖励产量。
  • 评分透明可复核:每项得分对应明确规则,报告末尾标明本账号适用的基准线,诊断过程清晰可追溯,不是黑箱打分。
  • 预警自动触发:异常信号无需人工盯数据,系统自动识别并按风险等级分类展示;预警阈值与评分基准同源,不会出现"评分良好却报僵尸粉"的矛盾。
  • 数据不足不扣分,也不白拿分:子项在样本过少时若数学上已退化(如 2 条样本时中位数恒等于均值、max/均值恒 ≤2 倍),或在数学上不可计算(互动全为 0 导致比值为 0/0、字段缺失),一律不计入分母并按"有效满分"归一化,报告中标注 (不计入)——既不让账号为缺失数据买单,也不让它白拿满分。
  • "没有内容"不等于"没有数据":若接口确认未返回任何作品,说明账号确实没有可评的内容(属负面信号),相关子项按最低档计分并不剔除,同时输出"ⓘ 无近期作品数据"提示核对——避免"已清空作品"的账号因分母变小而虚高。
  • 无区分度的子项不计分:性别/年龄 原为计分子项,但字段一旦返回必然有效,导致恒定满分且会稀释其他缺失项的扣分;现改为仅展示不计分,账号基础画像卷面由 10 调整为 8。
  • 低置信提示:样本 < 5 条时额外输出"ⓘ 数据置信度"提示,明确告知哪些结论基于过小样本、需要复核。
  • 脏数据自动校正:采集时间字段滞后于最新作品时(两步抓取时间不一致),活跃度与断更判定自动回退当前时间并标注,不会输出"最近-77天前发布"这类异常结果。
  • 趋势不被"快照年龄差"误导:接口返回的是截至采集时刻的累计互动量,作品越新累计必然越少。因此趋势类指标(含衰退预警)只比较已发布满 3 天的作品,成熟样本不足时不计入分母——不会把"刚发布 7 小时"误读成"严重衰退"。
  • 报告即拿即用:标准 Markdown 结构输出,可直接复制到团队文档、周报或合作评估材料中。

密钥获取与安全说明

  • 本技能需要使用环境变量:REDFOX_API_KEY。
  • REDFOX_API_KEY 由 红狐 hub(https://redfox.hk?source=github)提供。
  • 请前往 红狐 hub 注册账号,获取 REDFOX_API_KEY。
  • 配置设备环境变量 REDFOX_API_KEY 后使用本技能。
  • 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
  • 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。

使用指南

直接用自然语言说出要诊断的账号即可,无需记忆固定命令。

常用说法速查

意图 示例话术 效果
按昵称诊断 "诊断抖音账号 XX" 模糊匹配昵称并输出完整诊断报告
按抖音号诊断 "分析这个抖音号 66456544107" 精确匹配抖音号并输出完整报告
账号体检 "给 XX 做个抖音体检" 输出六维度诊断报告
评估合作价值 "评估一下这个账号值不值得合作" 输出评分、评级与风险预警

输出示例

诊断完成后输出结构化报告,包含:

  • 开头总结句:昵称 + 综合评分 + 评级,一眼看懂结论
  • 基本信息表:昵称、抖音号、UID、粉丝数、作品数、地区等
  • 六维度评分表:各维度得分、满分、一句话评价,核心短板加粗标注
  • 风险预警:逐项预警类型 + 具体数据解读
  • 关键发现:优势与短板两组要点,每条附数据佐证
  • 近期作品明细:每条作品的发布时间、点赞/评论/分享/总互动,标题可点击直达作品
  • 优化建议:结合数据给出按优先级排列的改进方向

使用场景

场景 角色 示例问法 收益
账号自检 抖音运营者 "帮我诊断一下我的抖音账号" 快速定位短板,拿到可落地的优化建议
竞品分析 品牌 / MCN "分析一下这个同赛道账号的数据" 对标竞品表现,制定差异化策略
达人筛选 MCN / 品牌方 "评估一下这个达人值不值得签约合作" 基于数据判断账号质量,降低合作风险
账号估值 投资人 / 商务 "看看这个账号的粉丝质量和增长潜力" 量化评估账号商业价值,辅助决策

Skill manifest

抖音账号诊断

📝 简介

抖音账号诊断帮助你快速摸清一个抖音账号的运营底数。只需提供账号昵称或抖音号,即可一键获取账号画像与近期作品数据,从六个维度完成量化诊断,输出100分制综合评分、得分明细、风险预警和优化方向。

无论是自查账号健康度、评估竞品实力,还是筛查合作账号的真实质量,都能在几分钟内拿到一份结构清晰的诊断报告。

✨ 功能特性

核心功能

功能模块 能力描述 核心价值
一键数据获取 输入昵称或抖音号,自动获取账号画像与近期作品数据 无需手动搜集数据,即查即诊
六维度量化诊断 覆盖账号画像、生产力、互动、质量、趋势、粉丝质量 全面体检,不留盲区
100分制综合评分 加权计算综合得分并给出四级评级 一个数字看懂账号水平
风险预警 自动识别僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险 提前发现问题,避免踩坑
近期作品明细 默认输出全部近期作品的互动数据,标题可点击跳转直达作品 诊断结论有据可查

特色亮点

  • 量级自适应基准:评分基准按账号量级(S超头部/A头部/B腰部/C尾部)分档, 同样 1.5% 的互动率在 5 万粉号上算偏低、在 670 万粉号上算良好, 避免用一套绝对阈值误判大号
  • 评分体系透明:每项得分都有明确规则与说明,报告附本账号适用的基准线,诊断过程可解释、可复核
  • 预警自动触发:异常信号自动识别并按风险等级展示,阈值与评分基准同源,不会自相矛盾
  • 报告即拿即用:标准格式输出,可直接用于团队汇报、竞品分析或合作评估

🔧 一键安装

本技能需配置 REDFOX_API_KEY 后使用(前往 红狐hub 注册获取)。

本技能为标准 Skill 文件包,兼容所有支持 Skill 机制的 AI Agent 平台。选择你使用的平台,按对应方式安装即可:

Qoder

  1. 确保已安装 Qoder 客户端
  2. 将 douyin-account-diagnosis 目录放入你的 Skills 文件夹
  3. 重启 Qoder 即可使用

OpenClaw / WorkBuddy

  1. 将 douyin-account-diagnosis 目录放入 OpenClaw 或 WorkBuddy 的 Skills 目录(通常为 ~/.openclaw/skills/ 或 ~/.workbuddy/skills/)
  2. 重启对应客户端即可生效

Codex / Claude Code

  1. 将 douyin-account-diagnosis 目录放入对应的 Skills 目录中
  2. Codex 默认路径:项目根目录下的 .codex/skills/
  3. Claude Code 默认路径:项目根目录下的 .claude/skills/
  4. 重启 Agent 会话即可加载

其他支持 Skill 的平台

只要平台支持标准的 Skill 文件格式(SKILL.md + 配套资源),将 douyin-account-diagnosis 目录放入其 Skills 目录下即可使用。

📖 使用指南

🔑 鉴权

使用前请配置 API Key,可前往 红狐hub 获取(格式 ak_xxxxxxxx),并通过以下任一方式配置:

  1. 设置环境变量 REDFOX_API_KEY
  2. 使用命令行参数 --api-key

基础使用

1. 发起诊断

直接说出要诊断的账号即可:

用户:帮我诊断一下抖音账号「桔桔的茶园小动物」

助手:收到!正在获取该账号的数据并进行六维度诊断,稍等~

2. 查看诊断报告

诊断完成后输出 Markdown 结构化报告,包含:

  • 开头总结句(昵称 + 综合评分 + 评级,一眼看懂结论)
  • 基本信息表格(昵称、抖音号、UID、粉丝数、作品数等)
  • 六维度评分表格(维度、得分、满分、一句话评价,标注核心短板)
  • 风险预警(僵尸粉、刷量、衰退、限流、断更、单条依赖,含具体数据解读)
  • 关键发现(优势与短板两组要点,每条附数据佐证)
  • 近期作品表格(发布时间、标题、赞/评/转/总互动,标题为可点击跳转链接)
  • 优化建议(按优先级排列,结合诊断数据给出具体方向)

3. 补充分析(可选)

如需进一步了解某个维度,可继续追问:

用户:互动健康度偏低,怎么改善?

助手:结合诊断报告中的短板维度与预警信息,给出针对性优化建议。

常用说法速查

意图 示例话术 效果
按昵称诊断 "诊断抖音账号 XX" 模糊匹配昵称并输出完整报告
按抖音号诊断 "分析这个抖音号 66456544107" 精确匹配抖音号并输出完整报告
账号体检 "给 XX 做个抖音体检" 输出六维度诊断报告
查看数据 "查看 XX 的抖音数据" 输出账号画像与近期作品数据
评估账号 "评估一下这个抖音账号值不值得合作" 输出评分、评级与风险预警

🔄 工作流程

  1. 提供账号:输入抖音昵称或抖音号
  2. 数据获取:一键获取账号画像与近期作品数据
  3. 六维度诊断:按评分体系逐项计算得分
  4. 输出报告:综合评分 + 评级 + 风险预警 + 近期作品详情
  5. 补充分析:针对短板维度提供优化建议(可选)

完整执行流程详见 references/core_workflow.md,详细评分规则详见 references/diagnosis_rules.md

📐 评分体系

六维度权重

维度 满分 权重 核心指标
账号基础画像 8 10% 头像、简介、地域信息完整度、抖音号(性别/年龄仅展示、不计分)
内容生产力 15 15% 作品总量、人均获赞、发布频率
互动健康度 30 30% 粉丝获赞比、粉丝互动率、互动结构比、作品均互动
内容质量 20 20% 爆款率、中位/均值偏离、互动稳定性(CV)、零互动占比
内容趋势 15 15% 近期趋势、爆款集中度、最新活跃度
粉丝质量 10 10% 粉丝规模、粉丝互动比、获赞/粉丝背离

量级自适应基准(先定层,再计分)

量级 粉丝数 互动率 优秀/良好/偏低线 爆款线(单条点赞/粉丝) 均互动 强传播线 作品总量 高产线 人均获赞 高效线
S 超头部 ≥100万 2.0% / 1.2% / 0.6% 1.0% 10万 150 条 200,000
A 头部 10万–100万 4.0% / 2.5% / 1.2% 2.0% 2万 600 条 15,000
B 腰部 1万–10万 6.0% / 4.0% / 2.0% 5.0% 5000 300 条 3,000
C 尾部 <1万 8.0% / 5.0% / 2.5% 10.0% 1000 100 条 1,000

为什么必须分层:抖音互动指标与粉丝体量强负相关。同样「单条 10 万次互动」, 对 5 万粉账号是 200% 的爆款,对 670 万粉账号只是 1.5% 的日常。 若用一套绝对阈值评判所有量级,会系统性低估超头部账号。

反向同理:绝对量级类指标也必须随量级缩放。同样是 4852 次互动, 对 2251 粉的尾部号是粉丝数的 2.16 倍(超预期),对 676 万粉的超头部号 则只是零头。所有以「绝对数值」判高低的子项(爆款率、作品均互动、 作品总量、人均获赞)均按本表分档,避免出现「爆款率 100% 却判互动偏弱」 或「人均获赞 478,631 与 5,114 同分」这类自相矛盾。

作品总量与人均获赞的特殊性:前者是绝对计数,后者是绝对量级。 高量级账号以明星/精品制作为主,单条成本高、产量天然低; 人均获赞则与量级正相关,基准递增。实测「作品数/粉丝数」随量级 单调递减(C 0.711% → A 0.24–1.0% → S 0.002–0.003%),印证了分层必要性。

有效满分与样本充分性

判定原则只有一条:子项在数学上不可计算时,不计入分母(不奖不罚);账号确实没有内容可评时,按最低档计分(照常扣分)。 两者不能混为一谈。

① 样本不足 / 数学退化 → 剔除出分母

子项 退化原因 门槛
中位/均值偏离 n=2 时中位数恒等于均值,偏离度恒为 0 → 必然满分 n ≥ 3
爆款集中度 集中度 = max/avg 恒 ∈ [1, n],n=2 时永远 < 3 倍 → 必然满分 n ≥ 5
互动稳定性 CV n<5 时离散度无统计意义,且会与集中度给出相反结论 n ≥ 5
发布频率 「返回条数 / 样本时间窗」,窗口只有几天时毫无代表性 n ≥ 5
近期趋势 / 衰退预警 快照下作品越新累计互动越少,会把「最近发布」读成「衰退」 满3天作品 ≥ 4

② 比值为 0/0(互动全为 0)→ 同样剔除

「互动结构比」(赞/评/转全 0)、「中位/均值偏离」(偏离度 0/0)、「爆款集中度」(max/avg 0/0)、「互动稳定性」(CV 0/0)在数学上均不存在。此时相关子项不计入分母 —— 该事实已由「零互动占比」与「爆款率」充分反映,重复记 0 分属于对同一事实的重复扣分。

⚠️ 实现上有坑:_cv() 在均值为 0 时返回 0.0,与「真的完全稳定」取值相同,会把互动全零的账号误判为「稳定输出 5/5」。调用前必须先拦截。

③ 无作品数据 / 字段缺失

  • 无作品数据(works 为空):账号没有可评估的内容表现,依赖作品的子项一律按最低档计分(0 分),不剔除分母。若剔除,一个「已清空作品」的账号会因分母变小而虚高,掩盖真实风险;报告会额外输出 ⓘ 无近期作品数据 提示核对。
  • 字段缺失(作品缺 createTime、粉丝数为 0 导致比率分母缺失):属不可计算 → 不计入分母。

被剔除的子项按有效满分归一化,报告标注 [有效满分X,满分Y] 并渲染为 (不计入);样本 < 5 条时额外输出 ⓘ 数据置信度 提示。

为什么趋势必须剔除未成熟作品:接口给的是「截至采集时刻的累计互动量」快照, 而互动累积是次线性的、集中在发布后最初几小时,因此越新的作品累计量必然越少。 实测某账号最新一条(0.3 天)累计 18,062 vs 最老一条(6.3 天)累计 38,513, 按累计量看是「暴跌 53%」,按日均看却是上涨约 10 倍 —— 结论完全相反。 故趋势类指标只比较已满 3 天的作品,成熟样本不足时不计入分母; 衰退 / 刷量预警共用同一份样本,不会出现「评分说平稳、预警说衰退」。

「人群标签」为什么不参与计分:它统计接口是否返回了性别/年龄。字段一旦返回必然有效,于是该子项恒拿满分、毫无区分度,还会稀释其他缺失项的扣分——同样资料质量的两个账号,仅因「接口恰好返回了性别」而得分不同。故改为仅展示不计分,账号基础画像卷面由 10 调整为 8。

综合评分

综合评分 = Σ(维度得分 / 维度有效满分 × 100 × 权重),四舍五入取整

舍入用 ROUND_HALF_UP。Python 内置 round() 是银行家舍入 (四舍六入五成双),在 .5 边界偏低,与「四舍五入」约定不符。

评级标准:

  • 85-100:🟢 优质账号
  • 70-84:🟡 正常账号
  • 50-69:🟠 待优化
  • <50:🔴 风险账号

⚠️ 风险预警

预警类型 触发条件 风险等级
🚨 僵尸粉预警 粉丝互动率 < 本量级偏低线 且粉丝数 > 5万 高危
🚨 刷量预警 获赞/粉丝比 > 100 且近期互动骤降 高危
⚠️ 衰退预警 近期作品均互动 < 早期的 50% 中危
⚠️ 限流预警 零互动占比 > 30% 中危
⚠️ 断更预警 最新作品距采集时间 > 14 天 中危
ℹ️ 单条依赖预警 最大爆款互动 > 均值的 10 倍 低危

僵尸粉预警阈值与「粉丝互动率」评分基准同源,不会出现 「互动率被判为良好、却又触发僵尸粉预警」的矛盾。无预警时输出「无」。

🔗 接口文档

接口详情详见 references/api_reference.md

❓ 常见问答

使用相关

Q:昵称和抖音号都能查吗? A:都可以。含中文的输入按昵称模糊匹配,纯数字/英文的输入按抖音号精确匹配。

Q:报告会包含近期作品明细吗? A:会。近期作品详情默认输出,包含每条作品的发布时间、点赞/评论/分享/总互动数,标题为可点击跳转链接,点击即可直达对应作品。

Q:评分是怎么算出来的? A:先按粉丝数确定账号量级(S/A/B/C),六个维度各自计分,再以各维度有效满分归一化到100分制后按权重加权求和。每项规则详见 references/diagnosis_rules.md。

Q:同一个互动率,为什么大号得分更高? A:因为互动率基准是分层的。抖音互动率随粉丝体量自然衰减,670万粉账号做到 1.55% 对应的单条均互动已达 10 万+,属于良好表现;而 5 万粉账号若只有 1.55%,单条互动仅几百,属于偏低。报告末尾会标明该账号适用的基准线,可自行复核。

Q:为什么某个维度的满分显示为 10/10,而不是 15? A:说明该维度有子项因数据不可计算被剔除,已按有效满分归一化,不会让账号为缺失数据扣分。剔除共三类:样本不足导致数学退化、比值为 0/0、字段缺失。

注意:这与「账号确实没有作品数据」不同 —— 后者的相关子项按最低档计分(照常扣分),不剔除,否则会把「已清空作品」的账号分数抬高。

Q:某个子项显示 (不计入) 是什么意思? A:表示该子项在当前数据下不可计算,已从该维度的分母中移除(不奖不罚)。报告会在维度标题上标注 [有效满分X,满分Y]。

Q:可以只诊断某个维度吗? A:诊断默认输出全部六个维度。如果只关注某一项,可以在拿到报告后针对该维度追问。

故障排除

Q:提示"未查询到该抖音账号信息"怎么办? A:输出会列出4条可能原因:① 抖音号不存在或已被注销;② 抖音号输入有误(请核对是否区分大小写,是否为正确的抖音号,非 UID、非昵称);③ 尚未收录(当前仅收录了粉丝数≧1万的账号);④ 申请收录(发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析)。

Q:提示积分不足怎么办? A:前往 红狐hub 充值后重试即可。

Q:某些字段显示"未知"是异常吗? A:不是。平台未返回的字段会按"未知"处理,处理方式分两种:参与计分的字段(省份、城市、简介、头像、抖音号)缺失会如实反映在"账号基础画像"得分上;仅作展示的字段(性别、年龄)缺失不影响任何得分。

Q:报告里出现"ⓘ 无近期作品数据"提示,说明分数不准吗? A:说明接口没有返回任何作品,此时互动健康度 / 内容质量 / 内容趋势 中依赖作品的子项均按最低档计分,分数偏低属预期。请先核对账号是否已清空作品、注销或长期停更;若确认账号正常,可稍后重试(可能是接口侧数据缺失)。

📚 参考文档

  • 详细执行工作流:references/core_workflow.md
  • 详细评分规则:references/diagnosis_rules.md
  • 接口文档:references/api_reference.md

📂 目录结构

douyin-account-diagnosis/
├── SKILL.md                          # 技能定义(本文件)
├── README.md / README.en.md          # 使用说明(中 / 英)
├── references/
│   ├── core_workflow.md              # 完整执行流程
│   ├── diagnosis_rules.md            # 六维度评分规则 + 量级基准表
│   └── api_reference.md              # 接口文档
├── scripts/
│   ├── douyin_diagnosis.py           # 主入口:取数 + 六维度评分 + 文本报告
│   └── regression_test.py            # 评分模型离线回归测试(不耗 API 额度)
└── tests/
    └── fixtures/                     # 回归测试用真实抓取数据
        ├── kaisoso.json              # S 超头部 · 少量作品(n=3,期望 ≥90)
        ├── papijiang.json            # S 超头部 · 极少作品(n=2,期望 ≥92)
        ├── juju.json                 # A 头部 · 真实低互动 0.07%(须触发僵尸粉预警)
        ├── xiaobianbian.json         # A 头部 · 快照年龄偏差(不得判为衰退)
        └── zhongguoyiwei.json        # C 尾部 · 均互动为粉丝数 2 倍(不得判互动偏弱)

改动评分规则后请务必执行回归测试(不消耗 API 额度):

python3 scripts/regression_test.py
Files (redfox-community)
  • references
    • api_reference.md 2.9 KB
      # 抖音账号诊断 - 接口文档
      
      ## 接口地址
      
      | 接口名称 | 请求方式 | 接口地址 |
      |---------|---------|----------|
      | 抖音账号数据查询 | POST | `https://redfox.hk/story/api/dyUser/queryData` |
      
      ## 请求参数
      
      | 参数名 | 类型 | 必填 | 说明 |
      |--------|------|------|------|
      | accountNames | array | 否 | 账号昵称列表,含中文输入时使用,模糊匹配 |
      | accountIds | array | 否 | 抖音号列表,纯数字/英文输入时使用,精确匹配 |
      | source | string | 是 | 接口来源标识,固定值:`"抖音账号诊断-workbuddy"` |
      
      > `accountNames` 与 `accountIds` 二选一,参数必须为复数形式且值为数组。
      
      ## 响应结构
      
      成功返回 `code: 2000`,关键字段如下:
      
      | 字段名 | 类型 | 说明 |
      |--------|------|------|
      | nickname | string | 账号昵称 |
      | accountId | string | 抖音号 |
      | uid | string | UID |
      | followerCount | int | 粉丝数 |
      | totalFavorited | int | 获赞总数 |
      | awemeCount | int | 作品总数 |
      | province / city | string | 省份 / 城市 |
      | ipLocation | string | IP属地 |
      | signature | string | 简介 |
      | gender / age | string / int | 性别 / 年龄 |
      | avatarUrl | string | 头像URL |
      | crawlTime | string | 数据采集时间 |
      | works | array | 近期作品列表(含标题、描述、点赞、评论、分享、总互动、作品链接) |
      
      ## 查询规则
      
      - 输入含中文 → 使用 `accountNames`(模糊匹配昵称)
      - 输入纯数字/英文 → 使用 `accountIds`(精确匹配抖音号)
      
      ## 特殊情况处理
      
      | 场景 | 处理方式 |
      |-----|----------|
      | 未查询到账号 | 输出"未查询到该抖音账号信息"+4条可能原因列表:① 抖音号不存在或已被注销;② 抖音号输入有误—请核对是否区分大小写、是否为正确的抖音号(非 UID、非昵称);③ 尚未收录—当前仅收录了粉丝数≧1万的账号;④ 申请收录—如需收录请发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析。不生成报告 |
      | 积分不足(code=3201) | 输出"[错误] API积分不足,请前往 [redfox.hk](https://redfox.hk/?source=github) 充值" |
      | 网络错误 | 输出"[错误] 网络请求失败:具体错误信息",建议重试 |
      | 作品数据为空 | **无内容可评,属负面信号**:依赖作品的子项按**最低档计分(0 分)**且不剔除分母;近期作品详情输出"无作品数据",报告额外输出 `ⓘ 无近期作品数据` 提示核对账号状态 |
      | 字段缺失(省份 / 城市 / 简介 / 头像 / 抖音号) | 如实反映在「账号基础画像」得分上(该维度考核资料完整度),不影响其他维度计算 |
      | 字段缺失(性别 / 年龄) | 仅作展示、不计分,对得分无任何影响 |
      | 作品缺 createTime / 粉丝数为 0 | 相关比值不可计算,该子项不计入分母(不奖不罚) |
    • core_workflow.md 12.4 KB
      # 抖音账号诊断 - 执行工作流
      
      > 本文档是 SKILL.md 的完整执行指南,Agent 应按步骤逐一执行。
      
      ## Step 0: 鉴权前置检查
      
      1. 检查 API Key 可用性:
         - 命令行参数 `--api-key` > 环境变量 `REDFOX_API_KEY`
      2. 未配置 → 引导用户前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 获取并配置(格式 `ak_xxxxxxxx`)
      3. 鉴权就绪 → 进入 Step 1
      
      ## Step 1: 获取目标账号
      
      1. 从用户输入提取账号标识(昵称或抖音号)
      2. 判断查询方式:
         - 输入含中文 → 按昵称模糊匹配(`accountNames`)
         - 输入纯数字/英文 → 按抖音号精确匹配(`accountIds`)
      3. 未提供账号标识 → 提示:"请提供要诊断的抖音昵称或抖音号~"
      
      ## Step 2: 数据采集
      
      > ⛔ 所有接口调用必须在请求参数中携带 `source` 字段,值为 `"抖音账号诊断-workbuddy"`
      
      1. 执行 `scripts/douyin_diagnosis.py <昵称或抖音号>`,脚本自动完成数据获取与诊断
      2. 接口信息:
         - 接口地址:`POST https://redfox.hk/story/api/dyUser/queryData`
         - 认证方式:HTTP 请求头 `X-API-KEY`
         - 成功返回码:`code: 2000`,超时 30 秒
      3. 采集两个层面的数据:
         - 账号画像:昵称、抖音号、UID、粉丝数、获赞总数、作品总数、地域、IP属地、简介、性别、年龄、头像
         - 近期作品:发布时间、标题/描述、点赞、评论、分享、总互动、作品链接
      
      ### 字段映射表
      
      | 平台原始字段 | 分析字段 | 所属维度 |
      |-------------|---------|---------|
      | avatarUrl、signature、province、ipLocation、accountId | 画像完整度指标(计分) | 账号基础画像 |
      | gender、age | 人群标签(**仅展示,不计分**) | 账号基础画像 |
      | awemeCount、totalFavorited、works[].createTime | 作品总量、人均获赞、发布频率 | 内容生产力 |
      | followerCount、totalFavorited、works[].interactiveCount/diggCount/commentCount/shareCount | 粉丝获赞比、粉丝互动率、互动结构比、作品均互动 | 互动健康度 |
      | works[].diggCount、works[].interactiveCount | 爆款率、中位/均值偏离、互动稳定性、零互动占比 | 内容质量 |
      | works[].createTime、works[].interactiveCount | 近期趋势、爆款集中度、最新活跃度 | 内容趋势 |
      | followerCount、totalFavorited、works[].interactiveCount | 粉丝规模、粉丝互动比、获赞/粉丝背离 | 粉丝质量 |
      
      ## Step 3: 量化评分
      
      ### 评分体系
      
      | 维度 | 满分 | 权重 |
      |------|------|------|
      | 账号基础画像 | 8 | 10% |
      | 内容生产力 | 15 | 15% |
      | 互动健康度 | 30 | 30% |
      | 内容质量 | 20 | 20% |
      | 内容趋势 | 15 | 15% |
      | 粉丝质量 | 10 | 10% |
      
      > 各维度逐项评分规则详见 `references/diagnosis_rules.md`
      
      ### 量级分层(评分基准的前提)
      
      所有互动类基准随账号量级自适应,先定层再计分:
      
      | 量级 | 粉丝数 |
      |------|--------|
      | S 超头部 | ≥ 100 万 |
      | A 头部 | 10 万 – 100 万 |
      | B 腰部 | 1 万 – 10 万 |
      | C 尾部 | < 1 万 |
      
      | 量级 | 互动率优秀 / 良好 / 偏低线 | 爆款线(点赞/粉丝) | 作品总量高产线 | 人均获赞高效线 |
      |------|---------------------------|------------------|---------------|---------------|
      | S | 2.0% / 1.2% / 0.6% | 1.0% | 150 条 | 200,000 |
      | A | 4.0% / 2.5% / 1.2% | 2.0% | 600 条 | 15,000 |
      | B | 6.0% / 4.0% / 2.0% | 5.0% | 300 条 | 3,000 |
      | C | 8.0% / 5.0% / 2.5% | 10.0% | 100 条 | 1,000 |
      
      > 原理:抖音互动指标与粉丝体量强负相关。同样「单条 10 万次互动」,
      > 对 5 万粉账号是 200% 爆款,对 670 万粉账号只是 1.5% 的日常。
      > 用一套绝对阈值评判所有量级,会系统性低估超头部账号。
      >
      > 「作品总量」与「人均获赞」是**绝对计数 / 绝对量级**,同样必须分档:
      > 精品型明星号单条成本高、产量天然低,不该按日更博主的节奏扣分;
      > 人均获赞则与量级正相关,基准随量级**递增**。两者相对化后,
      > 93.6 倍的效率差才不会被满分封顶抹平。
      
      ### 综合评分计算
      
      ```
      维度归一化分 = 维度得分 / 维度有效满分 × 100
      综合评分     = Σ(维度归一化分 × 权重),四舍五入取整
      ```
      
      **有效满分**:子项在数学上**不可计算**时(样本过少导致退化、比值为 0/0、字段缺失),
      该维度有效满分下降,归一化时以有效满分为分母。报告中标注
      `[有效满分X,满分Y]`,明细中被剔除的子项渲染为 `(不计入)`。
      详细判定见 `references/diagnosis_rules.md`。
      
      > ⚠️ 必须与「账号确实没有作品数据」区分开:后者是**负面信号**,相关子项按最低档
      > 计分且**不剔除分母**。若把两者混同,一个「已清空作品」的账号会因分母变小而虚高。
      
      **数据置信度提示**:当返回作品数 < 5 时,报告中额外输出一行
      `ⓘ 数据置信度:…`,提示保留下来的均值型指标(粉丝互动率、作品均互动、
      爆款率等)基于过小样本,结论仅供参考。返回作品数为 0 时改输出
      `ⓘ 无近期作品数据:…`。
      
      **舍入**:综合评分按「四舍五入」取整。注意 Python 内置 `round()` 为银行家舍入
      (在 .5 边界偏低),实现中已显式改用 `ROUND_HALF_UP`。
      
      ### 评级标准
      
      | 评分范围 | 评级 | 标识 |
      |----------|------|------|
      | 85 - 100 | 优质账号 | 🟢 |
      | 70 - 84 | 正常账号 | 🟡 |
      | 50 - 69 | 待优化 | 🟠 |
      | < 50 | 风险账号 | 🔴 |
      
      ## Step 4: 风险预警
      
      预警自动识别,按风险等级展示;同一账号可能同时触发多项预警;无预警时输出"【风险预警】无"。
      **僵尸粉预警的阈值与评分基准同源**,按账号量级取值,避免"评分良好却报僵尸粉"的矛盾。
      
      | 预警类型 | 触发条件 | 风险等级 |
      |----------|----------|----------|
      | 🚨 僵尸粉预警 | 粉丝互动率 < 本量级偏低线 且粉丝数 > 5万 | 高危 |
      | 🚨 刷量预警 | 获赞/粉丝比 > 100 且近期互动骤降 | 高危 |
      | ⚠️ 衰退预警 | 近期作品均互动 < 早期的 50% | 中危 |
      | ⚠️ 限流预警 | 零互动占比 > 30% | 中危 |
      | ⚠️ 断更预警 | 最新作品距采集时间 > 14 天 | 中危 |
      | ℹ️ 单条依赖预警 | 最大爆款互动 > 均值的 10 倍 | 低危 |
      
      ### 优化建议生成规则
      
      根据评分结果与预警信息生成针对性建议:
      
      - 优秀(≥85分):肯定账号优势 + 保持当前策略的优化建议
      - 良好(70-84分):鼓励现状 + 针对得分最低维度的改进方向
      - 待优化(50-69分):指出主要短板 + 按优先级列出重点改进项
      - 风险(<50分):明确风险提示 + 优先处理触发的预警项
      
      ## Step 5: 输出报告
      
      脚本输出的是纯文本原始报告,Agent 必须将其加工为以下 **Markdown 结构化报告** 后呈现给用户,章节顺序与结构固定,不得省略:
      
      ### 报告结构规范
      
      1. **开头总结句**(第一行,正文之前):
         > **「{昵称}」** 抖音账号诊断完成,综合评分 **{评分}/100 {评级标识} {评级}**。
      2. **基本信息**:表格形式,项目包括 昵称、抖音号、UID、粉丝数、作品数、获赞总数、地区、IP属地、**账号量级**、数据时间(地区为空时省略该行)
         - 账号量级按粉丝数取 S超头部/A头部/B腰部/C尾部,用于说明后续评分所用的基准
      3. **六维度评分**:表格形式,列为 维度 | 得分 | 满分 | 评价
         - **得分列必须带量级语境**:涉及量级自适应的指标(粉丝互动率、粉丝互动比、爆款率、
           作品总量、人均获赞),评价里要写明实际值与本量级基准的对比
           (如"1.55%,良好(超头部基准≥1.2%)"、"206,高产(S级线150条)"),
           不能只写"偏低/正常",否则用户无法判断基准是否合理
         - 若某维度存在不计入分母的子项,满分列写 `10/10(有效满分,卷面15)` 并说明原因
         - 评价列基于脚本明细生成一句话概括(如"良好,简介37字完整,但省份字段为空")
         - 得分最低的维度标注 **核心短板**,得满分的维度标注 **满分**,均加粗
      4. **⚠ 风险预警**:逐项输出预警名称 + 具体数据解读(如"粉丝互动率 0.38%(< 0.5%)且粉丝数 19.8万(> 5万),疑似存在不活跃/僵尸粉丝"),无预警时输出"无"
      5. **关键发现**:分 **优势:** 与 **短板:** 两组要点
         - 每条以 emoji 开头(📈📊❤️🔄 / 🔴💬💥 等)+ 加粗要点 + 数据佐证
         - 优势提炼自高分项与正向指标,短板提炼自低分项、预警与异常作品
      6. **近期作品详情(必须默认输出)**:表格形式,列为 # | 发布时间 | 标题 | 赞 | 评 | 转 | 总互动
         - 发布时间简化为 `MM-DD HH:mm` 格式
         - **标题字段必须使用 Markdown 可点击跳转链接格式**:`[标题内容](作品URL)`,点击即可跳转对应作品;标题截取60字以内,禁止输出裸URL
         - 不单独设置作品链接版块
      7. **优化建议**:编号列表,每条格式为 **{建议主题}** — {结合诊断数据的具体建议}
         - 建议按优先级排列:触发的预警项 > 得分最低维度 > 其他短板
      8. **数据来源**:报告末尾注明 [红狐RedFox API](https://redfox.hk/?source=github)
         - 并附**本账号适用的评分基准**一行(量级 + 互动率三档线 + 爆款线),
           让评分可复核、可解释
      
      > 注意:不直接输出脚本的 `[++++-]` 进度条纯文本格式,必须转换为上述表格化结构。
      > 报告须先给出**量级判定**再给分,避免读者误以为所有账号共用一套基准。
      
      ## 异常处理
      
      | 场景 | 处理方式 |
      |-----|----------|
      | 未查询到账号 | 输出"未查询到该抖音账号信息"+4条可能原因列表:① 抖音号不存在或已被注销;② 抖音号输入有误—请核对是否区分大小写、是否为正确的抖音号(非 UID、非昵称);③ 尚未收录—当前仅收录了粉丝数≧1万的账号;④ 申请收录—如需收录请发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析。不生成报告 |
      | 积分不足(code=3201) | 输出"[错误] API积分不足,请前往 [redfox.hk](https://redfox.hk/?source=github) 充值" |
      | 网络错误 | 输出"[错误] 网络请求失败:具体错误信息",建议重试 |
      | 作品数据为空(works 为空) | **无内容可评,属负面信号**:依赖作品的子项一律按**最低档计分(0 分)**,**不剔除分母**;近期作品详情输出"无作品数据",并额外输出 `ⓘ 无近期作品数据` 提示核对账号状态 |
      | 返回作品数 < 5 条 | 「发布频率」「互动稳定性」「爆款集中度」以及 n<3 时的「中位/均值偏离」不计入分母(不奖不罚),报告标注有效满分,并额外输出 `ⓘ 数据置信度` 提示。判定**只看返回条数**,不区分是否被接口截断——样本少到一定程度,这些子项在数学上已退化,与截断与否无关 |
      | 互动全为 0 | 「互动结构比」「中位/均值偏离」「爆款集中度」「互动稳定性」的比值均为 0/0,不可计算 → 不计入分母;「零互动占比」「爆款率」照常按最低档计分(属可计算的事实,不得剔除,否则会把分母做小、分数虚高) |
      | 已满 3 天的作品 < 4 条(快照下趋势不可判定) | 「近期趋势」不计入分母并说明原因;**衰退预警 / 刷量预警的「近期骤降」判定同步不触发**,不得把「最近才发布、累计量天然偏少」读成衰退 |
      | 字段缺失(省份/城市/简介/头像/抖音号) | 如实反映在「账号基础画像」得分上(该维度考核资料完整度,缺失即扣分),但不影响其他维度 |
      | `gender` / `age` 缺失或返回"未知" | 属**仅展示不计分**项,对得分无任何影响(原设计将其计分且"缺失不计入分母",但字段一旦提供必然有效,导致恒定满分、还会稀释其他缺失项的扣分) |
      | 作品缺 `createTime` / 粉丝数为 0 | 相关比值的分子或分母缺失,不可计算 → 该子项不计入分母(不奖不罚) |
      | 字段缺失导致量级基准无法判定 | 粉丝数缺失时按 C 尾部基准兜底 |
      | `crawlTime` 早于最新作品(字段滞后) | 「最新活跃度」与「断更预警」改用当前时间作参考,明细标注「采集时间滞后,按当前时间校正」,天数不出现负数 |
      
    • diagnosis_rules.md 28.4 KB
      # 抖音账号诊断 - 详细评分规则
      
      > **评分设计原则:基准随账号量级自适应。**
      > 抖音的互动指标与粉丝体量强负相关——同样是「单条 10 万次互动」,
      > 对 5 万粉的账号是 200% 的爆款,对 670 万粉的账号只是 1.5% 的日常。
      > 因此所有互动类指标一律按量级分档设定基准,不使用一套绝对阈值
      > 同时评判尾部与超头部账号,否则会系统性低估大号。
      >
      > **第二原则:账号不为「数据可得性」买单,也不因「数据不足」白拿分。**
      > 接口常只返回 2–3 条作品,此时部分子项在数学上已退化(详见下节「样本充分性」),
      > 既可能白拿满分、也可能无故扣分。凡**不可靠或不可计算**的子项一律
      > **不计入分母**(不奖不罚),报告标注 `[有效满分X,满分Y]`。
      >
      > ⚠️ 但有一条边界不能越:**「账号确实没有作品数据」不是「数据不足」**,而是
      > 负面信号,相关子项要按最低档照常扣分、不得剔除,否则会掩盖真实风险。
      > 三者的区分见下节。
      
      ## 样本充分性(SAMPLE_MIN = 5)
      
      接口返回的作品条数常在 2–3 条,以下子项此时在**数学上无法区分好坏**,
      必须剔除出分母,否则等于白拿满分:
      
      | 子项 | 退化原因 | 门槛 |
      |------|----------|------|
      | 中位/均值偏离 | n=2 时中位数**恒等于**均值,偏离度恒为 0 | n ≥ 3 |
      | 爆款集中度 | 集中度 = max/avg **恒 ∈ [1, n]**,n=2 时永远 < 3 倍,无法触及「依赖爆款」档 | n ≥ 5 |
      | 互动稳定性 CV | n<5 时离散度无统计意义,且会与集中度给出**相反结论** | n ≥ 5 |
      | 发布频率 | 用「返回条数 / 样本时间窗」推算,窗口只有几天时只是最后两条的间隔倒数 | n ≥ 5 |
      | **近期趋势 / 衰退预警** | **快照下作品越新累计互动越少,会把「最近发布」读成「衰退」** | **成熟作品(≥3天) ≥ 4 条** |
      
      > 判定**只看返回条数**,不区分是否被接口截断——样本少到一定程度,这些子项在
      > 数学上已退化,与截断与否无关。
      >
      > 被剔除的子项在报告中渲染为 `(不计入)`,维度标题标注有效满分。
      > 同时报告输出「ⓘ 数据置信度」提示——**保留下来的均值型指标**
      > (粉丝互动率、作品均互动、爆款率等)仍由过小样本决定,结论需复核。
      
      ## 不可计算 vs 数据不足 vs 无内容
      
      这三者必须严格区分,处理方式完全不同:
      
      | 情形 | 判定 | 处理 |
      |------|------|------|
      | **样本不足**(返回作品数过少,子项数学退化) | 数据存在但不可靠 | **不计入分母**(不奖不罚) |
      | **比值为 0/0**(互动全为 0,或分母为 0) | 数学上不存在 | **不计入分母**(不奖不罚) |
      | **字段缺失**(作品缺 createTime、粉丝数为 0) | 无法计算 | **不计入分母**(不奖不罚) |
      | **无作品数据**(works 为空) | 账号确实没有内容可评,属**负面信号** | 相关子项按**最低档计分(0 分)**,**不剔除分母** |
      
      > ⚠️ 最后一行是关键:若把「无作品数据」也按「不可计算」剔除,一个「已清空作品」
      > 的账号会因为分母变小而总分虚高,反而掩盖了真实风险。剔除只适用于
      > 「数据存在但不可靠/不可计算」,不适用于「内容本身不存在」。
      
      **0/0 情形的具体清单**(互动全为 0 时触发,均改为不计入分母):
      
      | 子项 | 不可计算的比值 |
      |------|---------------|
      | 互动结构比 | 评/赞、转/赞(赞总数为 0) |
      | 中位/均值偏离 | \|中位 − 均值\| / 均值(均值 = 0) |
      | 爆款集中度 | max / avg(均值 = 0) |
      | 互动稳定性 | 标准差 / 均值(均值 = 0) |
      | 近期趋势 | (近期 − 早期) / 早期(早期均值 = 0) |
      
      > 此时「零互动占比」(100%)与「爆款率」(0%)仍照常按最低档计分 —— 它们是
      > 可计算的事实,必须保留,否则这个账号的失分会被稀释掉。
      
      > ⚠️ **实现陷阱**:`_cv()` 在均值为 0 时返回 `0.0`,与「真的完全稳定」的取值
      > 完全相同,会把互动全零的账号误判为「稳定输出 5/5」。因此必须在调用 `_cv()`
      > 之前先拦截「均值 ≤ 0」的情形。该缺陷已修复并纳入回归测试。
      
      ## 人群标签:为什么不计分
      
      `gender` / `age` 原为「人群标签」子项(2 分),规则是「接口未提供的字段不计入分母」。
      但这条规则有个漏洞:**字段一旦提供,取值必然有效**(不存在「提供了但无效」的情况),
      于是 `gained` 恒等于分母 `full`,该子项恒定拿满、毫无区分度;更糟的是它会
      **稀释其他缺失项的扣分**:
      
      | 账号 | 地域信息 | 性别/年龄 | 归一化得分 |
      |------|----------|-----------|-----------|
      | A | 缺失(1/2) | 均可用 | (7 + 2) / (8 + 2) = **90.0%** |
      | B | 缺失(1/2) | 均缺失 | 7 / 8 = **87.5%** |
      
      A、B 的资料质量完全相同,A 却仅因「接口恰好返回了性别」而得分更高 ——
      这与「账号不为数据可得性买单」的原则直接矛盾(该原则意味着**既不该罚、也不该奖**)。
      
      **现改为仅展示不计分**:报告仍输出 `性别=…, 年龄=…(接口提供 N/2,仅展示不计分)`
      供人工参考,但不参与任何计分,账号基础画像卷面分由 10 调整为 8。
      
      > 取舍原则不变:无法区分「创作者未填写」与「接口不返回」,宁可放过、不冤枉账号。
      
      ## 账号量级分层
      
      | 量级 | 粉丝数 | 说明 |
      |------|--------|------|
      | S 超头部 | ≥ 100 万 | 一线明星、头部达人 |
      | A 头部 | 10 万 – 100 万 | 成熟垂类账号 |
      | B 腰部 | 1 万 – 10 万 | 成长型账号 |
      | C 尾部 | < 1 万 | 新号 / 小号 |
      
      ## 量级基准表
      
      **粉丝互动率基准**(互动率 = 近期作品均互动 / 粉丝数)
      
      | 量级 | 优秀线 | 良好线 | 偏低线 | 低于偏低线 |
      |------|--------|--------|--------|-----------|
      | S 超头部 | ≥ 2.0% | ≥ 1.2% | ≥ 0.6% | 极低 |
      | A 头部 | ≥ 4.0% | ≥ 2.5% | ≥ 1.2% | 极低 |
      | B 腰部 | ≥ 6.0% | ≥ 4.0% | ≥ 2.0% | 极低 |
      | C 尾部 | ≥ 8.0% | ≥ 5.0% | ≥ 2.5% | 极低 |
      
      **爆款判定线**(单条点赞数 / 粉丝数)
      
      | 量级 | 爆款线 | 说明 |
      |------|--------|------|
      | S 超头部 | 粉丝 × 1.0% | 670 万粉 → 单条 6.7 万赞即算爆款 |
      | A 头部 | 粉丝 × 2.0% | 44 万粉 → 单条约 8,800 赞即算爆款 |
      | B 腰部 | 粉丝 × 5.0% | |
      | C 尾部 | 粉丝 × 10.0% | |
      
      > 说明:原规则统一使用「点赞 > 粉丝数 × 10%」,对超头部号意味着单条需破
      > 67 万赞,实际几乎不可能达成,会导致爆款率恒为 0、内容质量维度被误判。
      
      **作品总量基准**(单位:条)
      
      | 量级 | 高产线 | 正常线 | 异常高产线 |
      |------|--------|--------|-----------|
      | S 超头部 | ≥ 150 | ≥ 60 | ≥ 3000 |
      | A 头部 | ≥ 600 | ≥ 200 | ≥ 6000 |
      | B 腰部 | ≥ 300 | ≥ 100 | ≥ 5000 |
      | C 尾部 | ≥ 100 | ≥ 30 | ≥ 3000 |
      
      **人均获赞基准**(单位:次,人均获赞 = 获赞总量 / 作品数)
      
      | 量级 | 高效线 | 良好线 | 正常线 |
      |------|--------|--------|--------|
      | S 超头部 | ≥ 200,000 | ≥ 80,000 | ≥ 15,000 |
      | A 头部 | ≥ 15,000 | ≥ 4,000 | ≥ 1,200 |
      | B 腰部 | ≥ 3,000 | ≥ 1,000 | ≥ 250 |
      | C 尾部 | ≥ 1,000 | ≥ 400 | ≥ 100 |
      
      > **为什么「作品总量」与「人均获赞」也必须分层**
      >
      > 这两项是**绝对计数 / 绝对量级**类指标,与互动率类指标的问题方向相反,
      > 但根因相同:不同量级账号处在完全不同的生产模式里,共用一把绝对尺子必然失真。
      >
      > - **作品总量**:高量级账号以明星、演员、精品制作为主,单条生产成本极高、产量天然偏低;
      >   A 级区间聚集大量职业化日更博主,产量期望最高。拿一把尺子同时量这两类号,
      >   等于要求演员按日更博主的节奏产出。
      > - **人均获赞**:与量级强正相关(粉丝越多、单条触达基数越大),
      >   故基准随量级**递增**,方向与互动率类指标相反。
      >
      > 实测样本的「作品数 / 粉丝数」印证了分层必要性:
      >
      > | 账号 | 量级 | 作品数 / 粉丝数 |
      > |------|------|----------------|
      > | 中国一味 | C | 16 / 2,251 = 0.711% |
      > | 桔桔的茶园小动物 | A | 4,424 / 44.42 万 = 0.996% |
      > | 小边边同志 | A | 1,576 / 66.21 万 = 0.238% |
      > | 许凯 | S | 206 / 676.81 万 = 0.003% |
      > | papi酱 | S | 536 / 2,828.77 万 = 0.002% |
      >
      > 体量越大,单位粉丝对应的作品数越少——因此不能按同一把尺子扣分。
      >
      > 相对化后的直接效果:同样 1,300 次人均获赞,对 C 级是「高效」(线 1,000)、
      > 对 A 级只是「正常」(线 1,200)、对 S 级则属「低效」(线 15,000)。
      > 同样 100 条作品,对 C 级新号算「高产」(线 100),对 A 级只算「偏少」(线 600)。
      >
      > **修复的缺陷(2026-09-23)**:原实现两项都用绝对线,导致
      > ① 精品型 S 级明星号(许凯 206 条)被判「正常 4/5」,而高产型 A 级号(小边边 1,576 条)
      > 拿满分——作品数多 7.6 倍就单方面压过效率;
      > ② 许凯人均获赞 478,631 与小边边 5,114 相差 **93.6 倍**,却都因「≥1000 即满分」被封顶,
      > 效率优势一分未兑现。形成「只奖励量、对效率失明」的不对称模型。
      
      ---
      
      ## 维度1: 账号基础画像 (8分, 权重10%)
      
      | 指标 | 满分 | 评分规则 |
      |------|------|----------|
      | 头像 | 2 | avatarUrl 非空 = 2分; 空 = 0分 |
      | 简介 | 3 | ≥15字 = 3分; >0字且<15字 = 1分; 空 = 0分 |
      | 地域信息完整度 | 2 | province 与 ipLocation 均非空 = 2分; 仅其一 = 1分; 均空 = 0分 |
      | 抖音号 | 1 | accountId 非空 = 1分; 空 = 0分 |
      | ~~人群标签~~ | ~~2~~ | **仅展示不计分**,详见上文「人群标签:为什么不计分」 |
      
      > **卷面分说明**:本维度卷面 8 分(原为 10 分,减去不计分的人群标签 2 分)。
      > 权重仍为 10%,故本维度 1 分的相对分量略高于改造前。
      >
      > **变更说明一**:地域指标由「一致性」改为「完整度」,不再要求
      > `province == ipLocation`。明星号常驻剧组或异地工作、机构号由团队代运营,
      > 注册地与 IP 属地不一致属行业常态,不应据此扣分。
      >
      > **变更说明二**:人群标签改为仅展示不计分,理由见上文专节。原实现下
      > 「接口是否返回性别」会实质影响得分,属数据可得性带来的不公平加分。
      
      ## 维度2: 内容生产力 (15分, 权重15%)
      
      ### 作品总量 (5分) —— 门槛量级自适应
      
      按账号量级(`VOLUME_BENCH`)取 (高产线 / 正常线 / 异常高产线):
      
      | 判定 | 得分 | 说明 |
      |------|------|------|
      | ≥ 异常高产线 | 4 | 极高产(需警惕刷量) |
      | ≥ 高产线 | 5 | 高产 |
      | ≥ 正常线 | 4 | 正常 |
      | < 正常线 | 2 | 偏少 |
      
      各量级门槛见上方「作品总量基准」表。示例:同样 100 条作品,
      C 级新号 → 5/5 高产(线 100);S 级明星号 → 4/5 正常(线 60);A 级职业博主 → 2/5 偏少(线 600)。
      
      > **变更说明**:原实现为固定线(<100→2 / <1000→4 / <3000→5 / ≥3000→4),
      > 不看量级。结果是精品型明星号(许凯 206 条 = 4/5)被按日更博主的节奏评价,
      > 而高产型 A 级号(小边边 1,576 条)拿满分——作品数多 7.6 倍就单方面压过效率优势。
      > 这是所有子项里**最后一个未做量级自适应**的绝对阈值项。
      
      ### 人均获赞 (5分) —— 门槛量级自适应
      
      人均获赞 = totalFavorited / awemeCount,按账号量级(`AVG_LIKE_BENCH`)取 (高效线 / 良好线 / 正常线):
      
      | 判定 | 得分 | 说明 |
      |------|------|------|
      | ≥ 高效线 | 5 | 高效 |
      | ≥ 良好线 | 4 | 良好 |
      | ≥ 正常线 | 3 | 正常 |
      | < 正常线 | 1 | 低效 |
      
      各量级门槛见上方「人均获赞基准」表。示例:同样 1,300 次人均获赞,
      C 级 → 5/5 高效;A 级 → 3/5 正常;S 级 → 1/5 低效。
      
      > **变更说明**:原实现为绝对线(<100→1 / <500→3 / <1000→4 / ≥1000→5)。
      > 该线以「<1000 即未达标」为隐含假设,实际是按尾部/新号水平设定的,
      > 对大号形同虚设——许凯 478,631 与小边边 5,114 之间 **93.6 倍**的差距
      > 被同一档满分抹平,而作品数落后 7.6 倍却照常扣分。
      > 相对化后效率差才能兑现(许凯 5/5 vs 小边边 4/5)。
      
      ### 发布频率 (5分)
      
      从 works 数组的 createTime 推算日均发布数。
      频率 = 返回作品数 / (最新作品时间 - 最早作品时间)天数
      
      | 范围 | 得分 | 说明 |
      |------|------|------|
      | < 0.5条/日 | 2 | 低频 |
      | 0.5 - 2条/日 | 5 | 正常 |
      | 2 - 3条/日 | 4 | 高频 |
      | > 3条/日 | 3 | 极高频 |
      
      > **样本不足时不计入分母**:该指标只反映**样本窗口内**的更新节奏。
      > 接口若只返回 2 条作品,窗口可能只有 5 天,推算结果实际是「最后两条之间的
      > 间隔倒数」,与账号真实产能无关(同一账号被返回更多历史作品时,算出的
      > 「日均」会更低,指标随样本量非单调变化)。故 `返回作品数 < 5` 时剔除。
      > 结果保留时会在明细中标注样本窗天数,便于判断可信度。
      
      ## 维度3: 互动健康度 (30分, 权重30%)
      
      ### 粉丝获赞比 (10分)
      
      粉丝获赞比 = totalFavorited / followerCount
      
      | 范围 | 得分 | 说明 |
      |------|------|------|
      | ≥ 25 | 10 | 优质(长尾效应强) |
      | 15 - 24.99 | 9 | 良好 |
      | 8 - 14.99 | 8 | 正常 |
      | 3 - 7.99 | 6 | 一般 |
      | 1 - 2.99 | 3 | 偏低 |
      | < 1 | 1 | 极低 |
      
      > **变更说明**:原规则为「<5 → 3分; 5~20 → 7分; ≥20 → 10分」三档,
      > 在 5 和 20 处存在评分断崖。改为六档平滑,减少同一水平账号的随机落差。
      
      ### 粉丝互动率 (10分) —— 量级自适应
      
      粉丝互动率 = avg(works.interactiveCount) / followerCount × 100%
      
      按账号所在量级查「粉丝互动率基准表」:
      
      | 条件 | 得分 | 说明 |
      |------|------|------|
      | ≥ 本量级优秀线 | 10 | 优秀 |
      | ≥ 本量级良好线 | 7 | 良好 |
      | ≥ 本量级偏低线 | 4 | 偏低 |
      | < 本量级偏低线 | 1 | 极低 |
      
      > **变更说明**:原规则使用固定阈值「>5% → 10分; 2~5% → 7分; 1~2% → 4分」。
      > 该阈值以中小账号为标尺,导致超头部号即使单条均互动 10 万+,
      > 仅因百分比被体量摊薄就被判为「偏低」,是本次最核心的修正项。
      
      > **数据不足时的处理**:
      > - **无作品数据** → 按最低档计 **0/10**(账号没有内容可评,属负面信号,
      >   与同维度其余「无作品数据」子项口径一致;原实现给中性 3/10,口径不统一)。
      > - 有作品但**粉丝数为 0** → 比值的分母缺失,不可计算 → **不计入分母**。
      
      ### 互动结构比 (5分)
      
      计算所有作品的 diggCount总和 : commentCount总和 : shareCount总和。
      以点赞为100,计算评论占比和分享占比。
      
      | 条件 | 得分 | 说明 |
      |------|------|------|
      | 评论占比 ≥ 4% 且 分享占比 ≥ 4% | 5 | 健康 |
      | 评论占比 ≥ 3% 且 分享占比 ≥ 3% | 4 | 较健康 |
      | 评论占比 ≥ 2% 或 分享占比 ≥ 2% | 3 | 一般 |
      | 均低于 2% | 2 | 互动单一 |
      
      > **变更说明**:原门槛「评论>5% 且 分享>5%」过窄,且对超头部号而言
      > 评论量会被粉丝基数稀释,容易把健康结构误判为「一般」。
      
      > **数据不足时的处理**:无作品数据 → 按最低档计 0/5;有作品但**点赞总数为 0**
      > (评/赞、转/赞 均为 0/0)→ 不可计算,**不计入分母**,该事实由「零互动占比」
      > 与「爆款率」反映,此处不再重复扣分。
      
      ### 作品均互动 (5分) —— 绝对传播量级(量级自适应)
      
      avg(works.interactiveCount) 的绝对量级,分档线同样随账号量级递减:
      
      | 量级 | 现象级(5) | 强传播(4) | 中等(3) | 偏弱(2) | 微弱(1) |
      |------|-----------|-----------|---------|---------|---------|
      | S 超头部 | ≥100万 | ≥10万 | ≥1万 | ≥500 | <500 |
      | A 头部 | ≥20万 | ≥2万 | ≥2000 | ≥200 | <200 |
      | B 腰部 | ≥5万 | ≥5000 | ≥500 | ≥50 | <50 |
      | C 尾部 | ≥1万 | ≥1000 | ≥200 | ≥20 | <20 |
      
      > **变更说明一**:原实现以 `followerCount × 3%` 作基准,与上一项「粉丝互动率」
      > 计算的是同一个比率(avg_inter / followers),等于把同一指标计了两遍、
      > 共占 15/30 分,放大了互动率对总分的影响。现改为衡量绝对传播量级,
      > 与互动率形成互补——互动率看粉丝黏性,绝对量级看单条声量。
      >
      > **变更说明二(量级自适应)**:绝对量级的分档线也必须随账号体量缩放。
      > 若对所有账号套用同一组阈值,会重演「用一套标尺评判所有量级」的错误——
      > 同样是 4852 次互动,对 2251 粉的尾部号是粉丝数的 **2.16 倍**(远超预期,
      > 且其爆款率 100%),却会被判「偏弱(2/5)」,与爆款率结论直接矛盾;
      > 对 676 万粉的超头部号则只是日常水平的零头(理应偏弱)。
      > 现按量级分档后,前者得 4/5「强传播」,后者得 2/5「偏弱」,两者均合理。
      
      ## 维度4: 内容质量 (20分, 权重20%)
      
      ### 爆款率 (6分) —— 爆款线量级自适应
      
      爆款定义: 单作品 diggCount > 粉丝数 × 本量级爆款线
      爆款率 = 爆款作品数 / 作品总数 × 100%
      
      | 范围 | 得分 | 说明 |
      |------|------|------|
      | > 10% | 6 | 优秀 |
      | 5 - 10% | 4 | 良好 |
      | > 0% 且 < 5% | 3 | 偏低 |
      | 0% | 1 | 无爆款 |
      
      ### 中位/均值偏离 (4分)
      
      计算 diggCount 的中位数和均值,偏离度 = |中位数 - 均值| / 均值
      
      | 偏离度 | 得分 | 说明 |
      |--------|------|------|
      | < 0.2 | 4 | 分布均匀 |
      | 0.2 - 0.5 | 3 | 轻度偏离 |
      | ≥ 0.5 | 1 | 严重偏离(靠爆款拉动) |
      
      > **n < 3 时不计入分母**:n=2 时中位数**恒等于**均值(两个数的中位就是它们的
      > 平均),偏离度恒为 0 → 必然满分,无法区分好坏。原实现在 n<2 时给 2/4,
      > 属于「样本不足反而白拿分」,一并改为剔除。
      > 点赞总数为 0 时偏离度为 0/0,同样不计入分母(详见「不可计算 vs 数据不足」节)。
      
      ### 互动稳定性 (5分)
      
      计算 interactiveCount 的变异系数 CV = 标准差 / 均值
      
      | CV 范围 | 得分 | 说明 |
      |---------|------|------|
      | < 0.5 | 5 | 稳定输出 |
      | 0.5 - 1.0 | 3 | 波动一般 |
      | ≥ 1.0 | 1 | 严重依赖爆款 |
      
      > **n < 5 时不计入分母**:2–4 条样本的离散度无统计意义,且实测会与
      > 「爆款集中度」给出**相反结论**——某超头部号 CV=1.19 判「严重依赖爆款」,
      > 而集中度 1.8 倍判「均匀分布」。两个指标互相打架时不能采信任一,故剔除。
      >
      > ⚠️ **互动全为 0 时也必须剔除**:`_cv()` 在均值为 0 时返回 `0.0`,与「真的完全
      > 稳定」取值完全相同,若不拦截会把「互动全零、濒死」的账号误判成「稳定输出 5/5」。
      > 实现上必须在调用 `_cv()` 前先判断「均值 ≤ 0」。
      
      ### 零互动占比 (5分)
      
      零互动作品定义: diggCount < 10
      零互动占比 = 零互动作品数 / 作品总数 × 100%
      
      | 范围 | 得分 | 说明 |
      |------|------|------|
      | < 5% | 5 | 质量稳定 |
      | 5 - 20% | 3 | 部分低质 |
      | ≥ 20% | 1 | 大量低质/限流 |
      
      ## 维度5: 内容趋势 (15分, 权重15%)
      
      ### 近期趋势 (5分)
      
      **只比较「已发布满 3 天」的作品**,按时间排序后取最早 1/3 和最近 1/3,比较均互动变化。
      变化率 = (近期均互动 - 早期均互动) / 早期均互动 × 100%
      
      | 变化率 | 得分 | 说明 |
      |--------|------|------|
      | > +20% | 5 | 增长期 |
      | -20% ~ +20% | 4 | 平稳期 |
      | -50% ~ -20% | 2 | 衰退期 |
      | < -50% | 1 | 严重衰退 |
      
      **为什么必须剔除未成熟作品(重要)**
      
      接口返回的是「截至采集时刻的累计互动量」这一张**快照**,而抖音的互动累积是
      **次线性**的、且高度集中在发布后最初几小时。因此样本里越新的作品,累计量必然越少
      —— 直接比较不同年龄作品的累计量,会**把「最近发布」系统性读成「衰退」**,
      年龄差多大,假的跌幅就有多大。这不是账号的问题,是快照数据的固有偏差。
      
      实测(账号 `bl12233623` 小边边同志,采集于 2026-09-23 02:07:35):
      
      | 作品 | 已发布 | 累计互动 | 日均互动 |
      |------|--------|----------|----------|
      | vlog184 | 6.3 天 | 38,513 | 6,094 |
      | vlog185 | 3.3 天 | 31,798 | 9,578 |
      | vlog187 | 1.3 天 | 20,150 | 15,265 |
      | vlog188 | 0.3 天 | 18,062 | 64,507 |
      
      按累计量比较 → 「暴跌 53%、严重衰退」;按日均比较 → **上涨约 10 倍**。
      **结论完全相反。** 累计量随年龄单调递减是数学必然,与经营好坏无关。
      
      **数据不足的处理**
      
      - 已满 3 天的成熟作品 < 4 条 → **不计入分母**,该维度有效满分相应下降,不奖不罚,
        并在报告中说明「已满3天的作品仅 N 条,快照下趋势不可判定」。
      - 早期成熟作品均互动为 0 → 变化率为 Δ/0,不可计算 → 同样不计入分母。
      - **衰退预警**与**刷量预警的「近期骤降」判定**共用这份成熟样本,
        避免出现「评分判定为平稳、预警却报衰退」的自相矛盾。
      
      > **变更说明(两个阶段)**:
      > ① 原规则在作品数不足时统一给 3/5,等于让账号为「接口没返回足够数据」买单;
      > 现改为不计入分母,避免因数据可得性差异造成评分不公。
      > ② 原规则只按「样本是否被截断」判断,未考虑**作品年龄差**,导致最新一条
      > 发布仅 7 小时的账号被误判为「严重衰退」并触发衰退预警 —— 方向完全错误。
      > 现引入 `TREND_MATURE_DAYS = 3` 的成熟门槛;`TREND_MIN_SAMPLE = 4`
      > 为成熟样本的最小条数。
      
      ### 爆款集中度 (5分)
      
      集中度 = max(works.interactiveCount) / avg(works.interactiveCount)
      
      | 集中度 | 得分 | 说明 |
      |--------|------|------|
      | < 3倍 | 5 | 均匀分布 |
      | 3 - 5倍 | 4 | 较为均匀 |
      | 5 - 10倍 | 2 | 依赖爆款 |
      | ≥ 10倍 | 1 | 严重依赖单条 |
      
      > **n < 5 时不计入分母**:集中度 = max/avg,数学上**恒 ∈ [1, n]**。
      > 只有 2 条样本时集中度永远 ≤ 2 倍,必然落进「< 3倍 = 均匀分布」拿满分——
      > 它**在结构上无法表达「依赖爆款」**。一个不可能判负的子项给满分就是白送分,
      > 必须剔除。门槛取 n ≥ 5 才有足够档位(max/avg 可达 5 倍)辨别依赖程度。
      > 互动总数为 0 时 max/avg = 0/0,同样不计入分母。
      
      ### 最新活跃度 (5分)
      
      最新活跃天数 = 参考时间 - 最新作品 createTime
      
      | 天数 | 得分 | 说明 |
      |------|------|------|
      | ≤ 3天 | 5 | 活跃 |
      | 4 - 7天 | 4 | 正常 |
      | 8 - 14天 | 2 | 偏沉默 |
      | > 14天 | 1 | 已断更 |
      
      **参考时间的取法(重要)**
      
      - 默认用 `crawlTime`(账号采集时间)。
      - 若 `crawlTime` **早于**最新作品时间,说明该字段滞后(基础信息与作品列表
        不是同一次抓取),此时改用**当前时间**,并在明细中标注
        「采集时间滞后,按当前时间校正」。天数恒取 `max(0, …)`,不出现负数。
      - 同一条逻辑也用于「断更预警」的判定,保证评分与预警口径一致。
      
      > **变更说明**:原实现直接相减,遇到滞后数据会算出负数天数(如 `最近-77天前
      > 发布`)。由于负数必然 ≤ 3,结果被错误地落入「活跃」分支——分数碰巧正确,
      > 但标签明显异常,且一旦滞后被修正就会掩盖真实断更。现显式识别该情形并回退到
      > 当前时间,同时把校正动作标注出来,让使用者能直接判断数据时效。
      
      > **数据不足时的处理**:作品缺 `createTime` → 活跃天数不可计算 → **不计入分母**
      > (不奖不罚)。原实现给中性 3/5,与其余「字段缺失」的处理口径不一致。
      
      ## 维度6: 粉丝质量 (10分, 权重10%)
      
      ### 粉丝规模 (4分)
      
      | 粉丝数 | 得分 | 等级 |
      |--------|------|------|
      | < 1万 | 1 | 尾部 |
      | 1万 - 10万 | 2 | 腰部 |
      | 10万 - 100万 | 3 | 头部 |
      | ≥ 100万 | 4 | 超头部 |
      
      > 本项的粉丝数门槛与「账号量级分层」(`TIER_LEVELS`)完全一致,实现上直接由
      > 量级派生(S→4 / A→3 / B→2 / C→1),不再重复定义一组魔数——两处各写一份
      > 一旦改动漏改,量级判定与粉丝规模得分就会互相矛盾。
      
      ### 粉丝互动比 (3分) —— 量级自适应
      
      与「粉丝互动率」同源,查量级基准表的良好线 / 偏低线:
      
      | 条件 | 得分 | 说明 |
      |------|------|------|
      | ≥ 本量级良好线 | 3 | 粉丝活跃 |
      | ≥ 本量级偏低线 | 2 | 粉丝一般 |
      | < 本量级偏低线 | 1 | 粉丝不活跃 |
      
      > **数据不足时的处理**:与「粉丝互动率」同口径——无作品数据按最低档 0/3 计;
      > 粉丝数为 0 时比率不可计算,不计入分母。
      
      ### 获赞/粉丝背离 (3分)
      
      | 比值 | 得分 | 说明 |
      |--------|------|------|
      | ≤ 50 | 3 | 正常 |
      | 50 - 100 | 2 | 偏高 |
      | > 100 | 1 | 异常高(疑似刷赞/搬运) |
      
      ## 有效满分与综合评分计算
      
      **有效满分**:若某维度的某个子项**不可计算**(样本退化 / 比值为 0/0 / 字段缺失),
      该维度的有效满分随之下降,报告中会标注 `[有效满分X,满分Y]`,
      明细中被剔除的子项渲染为 `(不计入)`。归一化时以**有效满分**为分母:
      
      ```
      维度归一化分 = 维度得分 / 维度有效满分 × 100
      综合评分     = Σ(维度归一化分 × 权重),四舍五入取整
      ```
      
      权重:账号基础画像 10% / 内容生产力 15% / 互动健康度 30% /
      内容质量 20% / 内容趋势 15% / 粉丝质量 10%
      
      > ⚠️ **不要把「无作品数据」也剔除**:那是账号确实没有内容可评,属负面信号,
      > 相关子项按最低档计分且保留在分母中。详见上文「不可计算 vs 数据不足 vs 无内容」。
      
      > **各维度卷面分**:账号基础画像 8 / 内容生产力 15 / 互动健康度 30 /
      > 内容质量 20 / 内容趋势 15 / 粉丝质量 10。各维度内部若出现不可计算子项,
      > 卷面分不变,仅**有效满分**下降。
      
      > **舍入实现注意**:文档约定为「四舍五入」,但 Python 内置 `round()` 是
      > **银行家舍入**(四舍六入五成双),在 `.5` 边界会给出偏低结果
      > (如 94.5 → 94)。因此实现显式使用 `ROUND_HALF_UP`,并先把累计浮点误差
      > 归一到 6 位小数,避免 94.499999 被误判。
      
      **数据置信度提示**:样本过少的子项虽已剔除,但保留下来的**均值型指标**
      (粉丝互动率、作品均互动、爆款率等)仍由极少数样本决定。
      当 `返回作品数 < 5` 时,报告额外输出一行 `ⓘ 数据置信度:…`,提示结论仅供参考。
      返回作品数为 0 时改输出 `ⓘ 无近期作品数据:…`,说明分数偏低属预期并提示核对账号状态。
      
      ## 风险预警阈值(与评分基准保持同源)
      
      僵尸粉预警阈值与「粉丝互动率」评分采用**同一套量级线**,
      避免出现「互动率被判为良好、却又触发僵尸粉预警」的自相矛盾。
      
      | 预警类型 | 触发条件 | 风险等级 |
      |----------|----------|----------|
      | 🚨 僵尸粉预警 | 粉丝互动率 < 本量级偏低线 且粉丝数 > 5万 | 高危 |
      | 🚨 刷量预警 | 获赞/粉丝比 > 100 且近期互动骤降 | 高危 |
      | ⚠️ 衰退预警 | 近期作品均互动 < 早期的 50%(**仅比较已满 3 天的作品,且需 ≥4 条**) | 中危 |
      | ⚠️ 限流预警 | 零互动占比 > 30% | 中危 |
      | ⚠️ 断更预警 | 最新作品距采集时间 > 14 天 | 中危 |
      | ℹ️ 单条依赖预警 | 最大爆款互动 > 均值的 10 倍 | 低危 |
      
      > **衰退 / 刷量预警与「近期趋势」共用同一份成熟样本**,因此不会出现
      > 「评分判定为平稳、预警却报衰退」的自相矛盾;成熟样本不足时**不触发**,
      > 而不是拿未成熟作品给出可能反向的结论。
      >
      > 示例:S 级超头部账号互动率 1.55%(高于偏低线 0.6%)→ 不触发僵尸粉预警;
      > A 级头部账号互动率 0.07%(低于偏低线 1.2%)→ 触发僵尸粉预警。
      
      ## 评级标准
      
      | 评分范围 | 评级 | 标识 |
      |----------|------|------|
      | 85 - 100 | 优质账号 | 🟢 |
      | 70 - 84 | 正常账号 | 🟡 |
      | 50 - 69 | 待优化 | 🟠 |
      | < 50 | 风险账号 | 🔴 |
      
  • scripts
    • douyin_api_client.py 15 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      抖音账号诊断 - API调用脚本
      基于红狐API接口 /story/api/dyUser/query 查询抖音账号数据和作品数据
      """
      
      import os
      import json
      from typing import List, Dict, Optional
      
      
      class DouyinUserAPI:
          """抖音用户API调用类 - 红狐API /story/api/dyUser/query"""
      
          # API基础地址
          BASE_URL = "https://redfox.hk"
      
          # 接口路径(已验证可用)
          QUERY_ENDPOINT = "/story/api/dyUser/query"
      
          # 环境变量名
          ENV_VAR = "REDFOX_API_KEY"
      
          def __init__(self, api_key: Optional[str] = None):
              """
              初始化API客户端
      
              Args:
                  api_key: API密钥(X-API-KEY),格式 ak_xxx,不传则从环境变量读取
              """
              self.api_key = api_key or os.environ.get(self.ENV_VAR, "")
              self.headers = {
                  "Content-Type": "application/json",
                  "X-API-KEY": self.api_key
              }
      
          def query_accounts(
              self,
              account_ids: Optional[List[str]] = None,
              account_names: Optional[List[str]] = None,
              source: str = "抖音账号诊断宗师-GitHub"
          ) -> Dict:
              """
              查询抖音账号信息
      
              Args:
                  account_ids: 抖音号列表(unique_id、short_id、uid)
                  account_names: 抖音昵称列表(模糊匹配nickname)
                  source: 来源标识
      
              Returns:
                  dict: {
                      "success": bool,
                      "data": list or None,
                      "error": str or None,
                      "fallback_needed": bool  # 是否需要降级为联网搜索
                  }
              """
              import requests
      
              url = f"{self.BASE_URL}{self.QUERY_ENDPOINT}"
      
              # 构建请求体
              payload = {"source": source}
              if account_ids:
                  payload["accountIds"] = account_ids
              if account_names:
                  payload["accountNames"] = account_names
      
              if not account_ids and not account_names:
                  return {
                      "success": False,
                      "data": None,
                      "error": "accountIds 和 accountNames 至少提供一个",
                      "fallback_needed": False
                  }
      
              try:
                  response = requests.post(url, json=payload, headers=self.headers, timeout=30)
                  result = response.json()
      
                  code = result.get("code")
                  msg = result.get("msg", "")
                  data = result.get("data")
      
                  # 成功(红狐API成功码为200或2000,兼容新旧版本)
                  if code in (200, 2000):
                      accounts = data if isinstance(data, list) else ([data] if data else [])
                      return {
                          "success": True,
                          "data": accounts,
                          "error": None,
                          "fallback_needed": False
                      }
      
                  # 积分不足 / 调用次数达上限
                  if code == 3201 or (code == 500 and ("积分" in msg or "次数" in msg or "上限" in msg)):
                      return {
                          "success": False,
                          "data": None,
                          "error": f"API业务错误: {msg}",
                          "fallback_needed": True
                      }
      
                  # 其他业务错误
                  return {
                      "success": False,
                      "data": None,
                      "error": f"API错误(code={code}): {msg}",
                      "fallback_needed": True
                  }
      
              except requests.exceptions.ConnectionError:
                  return {
                      "success": False,
                      "data": None,
                      "error": "连接失败,请检查网络",
                      "fallback_needed": True
                  }
              except requests.exceptions.Timeout:
                  return {
                      "success": False,
                      "data": None,
                      "error": "请求超时",
                      "fallback_needed": True
                  }
              except Exception as e:
                  return {
                      "success": False,
                      "data": None,
                      "error": f"查询失败: {e}",
                      "fallback_needed": True
                  }
      
      
          def diagnose_account(
              self,
              account_name: Optional[str] = None,
              account_id: Optional[str] = None
          ) -> Dict:
              """
              诊断单个抖音账号(查询+数据整合)
      
              Args:
                  account_name: 抖音昵称
                  account_id: 抖音号
      
              Returns:
                  dict: {
                      "success": bool,
                      "account": dict or None,  # 账号基本信息
                      "works": list,            # 近7天作品列表
                      "similar_accounts": list, # 相似账号列表
                      "error": str or None,
                      "fallback_needed": bool
                  }
              """
              if not account_name and not account_id:
                  return {
                      "success": False,
                      "account": None,
                      "works": [],
                      "similar_accounts": [],
                      "error": "请提供抖音昵称或抖音号",
                      "fallback_needed": False
                  }
      
              # 构建查询参数
              account_ids = [account_id] if account_id else None
              account_names = [account_name] if account_name else None
      
              print(f"🔍 正在查询抖音账号: {account_name or account_id}")
      
              # 查询账号信息
              result = self.query_accounts(
                  account_ids=account_ids,
                  account_names=account_names
              )
      
              if not result["success"]:
                  print(f"❌ 查询失败: {result['error']}")
                  return {
                      "success": False,
                      "account": None,
                      "works": [],
                      "similar_accounts": [],
                      "error": result["error"],
                      "fallback_needed": result["fallback_needed"]
                  }
      
              accounts = result["data"]
              if not accounts:
                  print(f"❌ 未查询到抖音账号: {account_name or account_id}")
                  print(f"- 抖音号 {account_name or account_id} 不存在或已被注销")
                  print("- 抖音号输入有误 — 请核对是否区分大小写,是否为正确的抖音号(非 UID、非昵称)")
                  print("- 尚未收录 — 当前仅收录了粉丝数≧1万的账号")
                  print("- 申请收录 — 如需收录请发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析")
                  return {
                      "success": False,
                      "account": None,
                      "works": [],
                      "similar_accounts": [],
                      "error": "未查询到账号",
                      "fallback_needed": True
                  }
      
              # 取第一个匹配结果
              account_data = accounts[0]
      
              # 提取数据
              account_info = {
                  "nickname": account_data.get("nickname", ""),
                  "accountId": account_data.get("accountId", ""),
                  "uid": account_data.get("uid", ""),
                  "avatarUrl": account_data.get("avatarUrl", ""),
                  "signature": account_data.get("signature", ""),
                  "gender": account_data.get("gender", ""),
                  "age": account_data.get("age"),
                  "country": account_data.get("country", ""),
                  "province": account_data.get("province", ""),
                  "city": account_data.get("city", ""),
                  "ipLocation": account_data.get("ipLocation", ""),
                  "followerCount": account_data.get("followerCount", 0),
                  "awemeCount": account_data.get("awemeCount", 0),
                  "totalFavorited": account_data.get("totalFavorited", 0),
                  "redfoxIndex": account_data.get("redfoxIndex"),
                  "crawlTime": account_data.get("crawlTime", ""),
              }
      
              works = account_data.get("works", [])
              similar_accounts = account_data.get("similarAccounts", [])
      
              print(f"✅ 查询成功: {account_info['nickname']} (粉丝: {format_number(account_info['followerCount'])})")
              print(f"   作品数: {account_info['awemeCount']} | 近7天作品: {len(works)} | 相似账号: {len(similar_accounts)}")
      
              return {
                  "success": True,
                  "account": account_info,
                  "works": works,
                  "similar_accounts": similar_accounts,
                  "error": None,
                  "fallback_needed": False
              }
      
          def calculate_works_stats(self, works: List[Dict]) -> Dict:
              """
              基于近7天作品数据计算v4.0统计指标
              四维度:账号体量(35) + 内容表现(35) + 运营活跃度(20) + 平台指数(10)
              不依赖playCount,使用diggCount/commentCount/shareCount
      
              Args:
                  works: 作品列表(DyWorkVO格式)
      
              Returns:
                  dict: 统计数据
              """
              if not works:
                  return {
                      "avg_digg_count": 0,
                      "avg_comment_count": 0,
                      "avg_share_count": 0,
                      "spread_coefficient": 0,  # 传播系数 = 分享/点赞
                      "update_frequency": 0,     # 近7天发布数
                      "publish_periods": [],     # 发布时段列表
                  }
      
              total_digg = sum(w.get("diggCount", 0) or 0 for w in works)
              total_comment = sum(w.get("commentCount", 0) or 0 for w in works)
              total_share = sum(w.get("shareCount", 0) or 0 for w in works)
      
              n = len(works)
              avg_digg = total_digg / n
              avg_comment = total_comment / n
              avg_share = total_share / n
      
              # 传播系数 = 分享数/点赞数
              spread_coefficient = (total_share / total_digg * 100) if total_digg > 0 else 0
      
              # 发布时段
              publish_periods = []
              for w in works:
                  create_time = w.get("createTime", "")
                  if create_time:
                      try:
                          hour = int(create_time.split(" ")[1].split(":")[0])
                          publish_periods.append(hour)
                      except (IndexError, ValueError):
                          pass
      
              return {
                  "avg_digg_count": int(avg_digg),
                  "avg_comment_count": int(avg_comment),
                  "avg_share_count": int(avg_share),
                  "spread_coefficient": round(spread_coefficient, 2),  # 百分比
                  "update_frequency": n,
                  "publish_periods": publish_periods,
              }
      
      
      def format_number(num) -> str:
          """格式化数字显示"""
          if num is None:
              return "-"
          try:
              num = int(num)
          except (ValueError, TypeError):
              return str(num)
      
          if num >= 100000000:
              return f"{num/100000000:.1f}亿"
          elif num >= 10000:
              return f"{num/10000:.1f}万"
          else:
              return f"{num:,}"
      
      
      # 账号分类映射:API原词 → 输出新词
      CATEGORY_MAP = {
          "全部": "全部",
          "才艺技能": "个人才艺",
          "生活": "生活vlog",
          "财经": "财富理财",
          "二次元": "二次元",
          "家居家装": "居家装修",
          "教育培训": "学习教育",
          "剧情演绎": "小剧场",
          "科技数码": "数码科技",
          "旅游": "旅行",
          "美食": "美食",
          "美妆": "化妆美容",
          "萌宠": "动物",
          "母婴亲子": "亲子",
          "汽车": "汽车",
          "情感心理": "情感",
          "三农": "三农",
          "医疗健康": "健康医学",
          "时尚": "潮流风尚",
          "舞蹈": "舞蹈才艺",
          "颜值": "颜值造型",
          "人文社科": "人文",
          "音乐": "音乐",
          "影视综艺": "影视",
          "健身": "身体锻炼",
          "体育": "体育",
          "明星八卦": "明星娱乐",
          "游戏": "游戏",
      }
      
      
      def map_category(raw_category: str) -> str:
          """将API原词分类映射为输出新词"""
          return CATEGORY_MAP.get(raw_category, raw_category)
      
      
      def print_account_summary(result: Dict):
          """打印账号数据摘要"""
          account = result.get("account", {})
          works = result.get("works", [])
          similar = result.get("similar_accounts", [])
      
          print("\n" + "=" * 60)
          print(f"📋 账号数据摘要")
          print("=" * 60)
          print(f"账号昵称: {account.get('nickname', '-')}")
          print(f"抖音号: {account.get('accountId', '-')}")
          print(f"UID: {account.get('uid', '-')}")
          print(f"账号简介: {account.get('signature', '-')}")
          print(f"性别: {account.get('gender', '-')}")
          print(f"年龄: {account.get('age', '-')}")
          print(f"地域: {account.get('province', '')}{account.get('city', '')}")
          print(f"IP属地: {account.get('ipLocation', '-')}")
          print(f"粉丝数: {format_number(account.get('followerCount'))}")
          print(f"获赞总数: {format_number(account.get('totalFavorited'))}")
          print(f"作品总数: {account.get('awemeCount', '-')}")
          print(f"数据更新: {account.get('crawlTime', '-')}")
      
          # 作品数据
          if works:
              api = DouyinUserAPI()
              stats = api.calculate_works_stats(works)
              print(f"\n📊 近7天作品统计(共{len(works)}条):")
              print(f"  平均点赞数: {format_number(stats['avg_digg_count'])}")
              print(f"  平均评论数: {format_number(stats['avg_comment_count'])}")
              print(f"  平均分享数: {format_number(stats['avg_share_count'])}")
              print(f"  传播系数: {stats['spread_coefficient']}%")
              print(f"  更新频率: {stats['update_frequency']}条/7天")
      
              print(f"\n🎬 作品列表:")
              sorted_works = sorted(works, key=lambda w: w.get("diggCount", 0) or 0, reverse=True)
              for i, work in enumerate(sorted_works[:5], 1):
                  title = work.get('title', '无标题')[:30]
                  digg = format_number(work.get('diggCount'))
                  comment = format_number(work.get('commentCount'))
                  share = format_number(work.get('shareCount'))
                  print(f"  {i}. {title} | 点赞:{digg} | 评论:{comment} | 分享:{share}")
      
          # 相似账号
          if similar:
              print(f"\n👥 相似账号(共{len(similar)}个):")
              for s in similar[:5]:
                  classify = CATEGORY_MAP.get(s.get('accountClassifyFirst', ''), s.get('accountClassifyFirst', '-'))
                  print(f"  - {s.get('nickname', '-')} | 粉丝:{format_number(s.get('followerCount'))} | 分类:{classify} | 红狐指数:{s.get('redfoxIndex', '-')}")
      
      
      # 使用示例
      if __name__ == "__main__":
          api = DouyinUserAPI()
      
          if not api.api_key:
              print(f"❌ 未设置环境变量 {DouyinUserAPI.ENV_VAR}")
              print(f"💡 请先运行: export {DouyinUserAPI.ENV_VAR}=你的API密钥值")
              exit(1)
      
          print(f"✅ API Key已配置: {api.api_key[:8]}...")
      
          # 示例1:通过名称查询
          print("\n=== 示例1:通过名称查询 ===")
          result = api.diagnose_account(account_name="疯狂小杨哥")
          if result["success"]:
              print_account_summary(result)
          elif result["fallback_needed"]:
              print(f"⚠️ API不可用,需降级为联网搜索: {result['error']}")
      
          print("\n" + "=" * 60 + "\n")
      
          # 示例2:通过抖音号查询
          print("=== 示例2:通过抖音号查询 ===")
          result = api.diagnose_account(account_id="yangge_520")
          if result["success"]:
              print_account_summary(result)
          elif result["fallback_needed"]:
              print(f"⚠️ API不可用,需降级为联网搜索: {result['error']}")
      
    • douyin_diagnosis.py 49.5 KB
      #!/usr/bin/env python3
      """
      抖音账号诊断工具
      用于「抖音账号诊断」技能的数据获取、六维度评分与报告生成。
      通过红狐(RedFox) API 获取抖音账号数据,进行六维度诊断分析并输出报告。
      
      用法: python douyin_diagnosis.py <抖音昵称或抖音号> [--api-key <你的API Key>]
      API Key 优先级: 命令行 --api-key > 环境变量 REDFOX_API_KEY
      """
      
      import os
      import sys
      import json
      import urllib.request
      import urllib.error
      from datetime import datetime
      from decimal import Decimal, ROUND_HALF_UP
      import statistics
      
      # ============================================================
      # API 配置
      # ============================================================
      API_URL = "https://redfox.hk/story/api/dyUser/queryData"
      
      # 技能来源标识(所有接口调用必须携带)
      SOURCE = "抖音账号诊断-workbuddy"
      
      
      def resolve_api_key(cli_key=None):
          """解析使用的 API Key:命令行参数 > 环境变量 REDFOX_API_KEY。未配置则返回 None。"""
          return cli_key or os.environ.get("REDFOX_API_KEY") or None
      
      
      def require_api_key(cli_key=None):
          """解析 API Key;未配置时输出引导信息并退出。"""
          api_key = resolve_api_key(cli_key)
          if api_key:
              return api_key
          print("[错误] 未配置 API Key")
          print("[hint] 获取: https://redfox.hk/settings/api-keys?source=github")
          print("[hint] 配置: export REDFOX_API_KEY=ak_xxxxxxxx")
          print("[hint] 或: python douyin_diagnosis.py <账号> --api-key ak_xxxxxxxx")
          sys.exit(1)
      
      # ============================================================
      # 工具函数
      # ============================================================
      
      def query_account(keyword, api_key):
          """调用红狐API查询抖音账号数据。keyword 可为昵称或抖音号。"""
          headers = {
              "Content-Type": "application/json",
              "X-API-KEY": api_key,
          }
          # 优先用 accountIds(精确匹配),若含中文则用 accountNames(模糊匹配)
          if _is_chinese(keyword):
              payload_key = "accountNames"
          else:
              payload_key = "accountIds"
          # 请求体必须携带 source 字段(技能来源标识)
          payload = json.dumps({payload_key: [keyword], "source": SOURCE}).encode("utf-8")
          req = urllib.request.Request(API_URL, data=payload, headers=headers, method="POST")
          try:
              with urllib.request.urlopen(req, timeout=30) as resp:
                  result = json.loads(resp.read().decode("utf-8"))
                  if result.get("code") == 2000 and result.get("data"):
                      return result["data"][0]
                  elif result.get("code") == 3201:
                      print("[错误] API积分不足,请前往 https://redfox.hk/?source=github 充值。")
                      return None
                  else:
                      print(f"[错误] API返回异常: code={result.get('code')}, msg={result.get('msg', '')}")
                      return None
          except urllib.error.URLError as e:
              print(f"[错误] 网络请求失败: {e}")
              return None
          except Exception as e:
              print(f"[错误] {e}")
              return None
      
      
      def _is_chinese(s):
          return any("\u4e00" <= ch <= "\u9fff" for ch in s)
      
      
      def _parse_dt(s):
          if not s:
              return None
          for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
              try:
                  return datetime.strptime(s, fmt)
              except ValueError:
                  continue
          return None
      
      
      def _safe_div(a, b):
          if not b or b == 0:
              return 0.0
          return a / b
      
      
      def _work_age_days(work, ref_time):
          """作品从发布到参考时间已过去的整天数。时间缺失返回 -1(视为未成熟)。"""
          t = _parse_dt(work.get("createTime", ""))
          if not t or not ref_time:
              return -1.0
          return (ref_time - t).total_seconds() / 86400.0
      
      
      def _trend_ref_time(works, crawl_time):
          """趋势类指标共用的参考时间:采集时间滞后于作品时回退当前时间。
      
          与「最新活跃度」保持同一套逻辑,避免用早于作品的采集时间去算"过去多久"。
          """
          times = [_parse_dt(w.get("createTime", "")) for w in works]
          times = [t for t in times if t]
          if not times:
              return crawl_time
          latest = max(times)
          if not crawl_time or crawl_time < latest:
              return datetime.now()
          return crawl_time
      
      
      # 接口对「未提供」的字段会返回占位值,"未知"/空串一律视为未提供。
      _PLACEHOLDER = {"", "未知", "null", "None", "N/A"}
      
      
      def _field_provided(value):
          """判断接口是否真的提供了该字段(占位值视为未提供)。
      
          注意:无法区分「创作者未填写」与「接口不返回」。实测 gender 在 4/4 账号上
          均为"未知",判断为接口不返回,故不据此扣分——宁可放过,不冤枉账号。
          """
          if value is None:
              return False
          return str(value).strip() not in _PLACEHOLDER
      
      
      def _fmt_num(n):
          """数字格式化,大数加万/亿后缀。"""
          if n is None:
              return "N/A"
          if n >= 100_000_000:
              return f"{n / 100_000_000:.2f}亿"
          if n >= 10_000:
              return f"{n / 10_000:.2f}万"
          return str(n)
      
      
      def _cv(values):
          """变异系数 = 标准差 / 均值。
      
          注意:均值为 0 时返回 0.0,与「真的完全稳定」取值相同,调用方
          必须先自行排除「全部为 0」的情形,不能直接拿 0.0 当作「稳定」。
          """
          if len(values) < 2:
              return 0.0
          m = statistics.mean(values)
          if m == 0:
              return 0.0
          return statistics.stdev(values) / m
      
      
      # ============================================================
      # 账号量级分层 —— 所有互动类基准随量级自适应
      # ------------------------------------------------------------
      # 抖音的互动指标与粉丝体量强负相关:同样是「单条 10 万次互动」,
      # 对 5 万粉的账号是 200% 的爆款,对 670 万粉的账号只是 1.5% 的日常。
      # 因此互动率、爆款线等基准必须按量级分档,不能用一套绝对阈值同时
      # 评判尾部和超头部账号,否则会系统性低估大号。
      # ============================================================
      
      TIER_LEVELS = [
          ("S", "超头部", 1_000_000),
          ("A", "头部", 100_000),
          ("B", "腰部", 10_000),
          ("C", "尾部", 0),
      ]
      
      
      def account_tier(followers):
          """按粉丝数返回账号量级 (code, label)。"""
          for code, label, floor in TIER_LEVELS:
              if followers >= floor:
                  return code, label
          return "C", "尾部"
      
      
      # 各量级「粉丝互动率」基准线 (满分线, 良好线, 偏低线),单位 %。
      # 体量越大自然互动率越低,故基准随量级递减。
      ENGAGEMENT_BENCH = {
          "S": (2.0, 1.2, 0.6),
          "A": (4.0, 2.5, 1.2),
          "B": (6.0, 4.0, 2.0),
          "C": (8.0, 5.0, 2.5),
      }
      
      # 各量级「爆款」判定线:单条点赞数 / 粉丝数,单位 %。
      # 超头部号「点赞超过粉丝数 10%」几乎不可能达成,故按量级下调。
      HIT_BENCH = {"S": 1.0, "A": 2.0, "B": 5.0, "C": 10.0}
      
      # 各量级「单条作品平均互动量」分档线 (现象级, 强传播, 中等, 偏弱),单位:次。
      # 绝对量级必须与账号体量匹配,否则会重演「用一套阈值评判所有量级」的错误:
      # 同样是 5 千次互动,对 2251 粉的尾部号是粉丝数的 2 倍(超预期),
      # 对 676 万粉的超头部号只是日常水平的零头(不合格)。
      INTERACTION_BENCH = {
          "S": (1_000_000, 100_000, 10_000, 500),
          "A": (200_000, 20_000, 2_000, 200),
          "B": (50_000, 5_000, 500, 50),
          "C": (10_000, 1_000, 200, 20),
      }
      
      # 各量级「作品总量」分档线 (高产线, 正常线, 异常高产线),单位:条。
      #
      # 为什么也要分层:作品条数是「绝对计数」类指标,不同量级账号的生产模式完全不同。
      # 高量级账号以明星/演员/精品制作为主,单条生产成本极高,产量天然偏低;
      # A 级区间聚集了大量职业化日更博主,产量期望最高;B/C 级多为个人号与新号。
      # 拿一把绝对尺子同时量这两类号,等于要求演员按日更博主的节奏产出。
      #
      # 实测样本的「作品数 / 粉丝数」印证了这一分层:
      #   中国一味(C) 16/2251 = 0.711%   桔桔(A) 4424/44.42万 = 0.996%
      #   小边边(A) 1576/66.21万 = 0.238%   许凯(S) 206/676.81万 = 0.003%
      #   papi酱(S) 536/2828.77万 = 0.002%
      # 即体量越大,单位粉丝对应的作品数越少——故不能按同一把尺子扣分。
      VOLUME_BENCH = {
          "S": (150, 60, 3000),
          "A": (600, 200, 6000),
          "B": (300, 100, 5000),
          "C": (100, 30, 3000),
      }
      
      # 各量级「人均获赞」分档线 (高效线, 良好线, 正常线),单位:次。
      #
      # 人均获赞 = 获赞总量 / 作品数,即「单条作品平均拿到多少赞」。
      # 它与量级强正相关(粉丝越多、单条触达基数越大),故基准随量级**递增**——
      # 方向与互动率类指标相反(后者随量级递减)。
      #
      # 为什么必须相对化:原实现用绝对线(≥1000 即满分),导致
      #   许凯(S) 478,631 与 小边边(A) 5,114 都拿满分 → 93.6 倍的效率差被完全抹平,
      #   而作品数落后 7.6 倍却照常扣分,形成「只奖励量、对效率失明」的不对称。
      # 相对化后,同样是 5,114 次人均获赞:对 A 级只算「良好」,对 C 级则是「高效」——
      # 因为 C 级账号 1,086 次(中国一味)已属远超预期。
      AVG_LIKE_BENCH = {
          "S": (200_000, 80_000, 15_000),
          "A": (15_000, 4_000, 1_200),
          "B": (3_000, 1_000, 250),
          "C": (1_000, 400, 100),
      }
      
      # 样本充分性门槛:接口返回的作品条数少于此值时,凡「依赖样本推算」的子项
      # 一律不计入分母(不奖不罚),避免账号为「接口没给足数据」买单或白拿分。
      # 背景:接口常只返回 2–3 条作品,此时多个子项在数学上已退化——
      #   · 中位/均值偏离:n=2 时中位数恒等于均值,偏离度恒为 0 → 必然满分
      #   · 爆款集中度:max/avg ∈ [1, n],n=2 时永远 <3 倍 → 必然满分(无法触及"依赖爆款"档)
      #   · 互动稳定性 CV:2 点样本的离散度无统计意义,且会与集中度结论互相矛盾
      #   · 发布频率:用「返回条数 / 样本时间窗」推算,窗口只有几天时只是两条之间的间隔倒数
      SAMPLE_MIN = 5
      
      # 「中位/均值偏离」退化更早:n=2 时中位数恒等于均值,故只需 n>=3 即可判别
      MEDIAN_SAMPLE_MIN = 3
      
      # 趋势类指标(近期趋势 / 衰退预警 / 刷量预警的"近期骤降")的样本门槛与作品年龄门槛。
      #
      # 背景:接口只给「截至采集时刻的累计互动量」这一张快照,而抖音的互动累积是
      # 次线性的,且高度集中在发布后最初几小时。因此样本里越新的作品,累计量必然越少。
      # 直接拿不同年龄作品的累计量做「近期 vs 早期」比较,会把「最近发布」系统性
      # 误判成「衰退」——年龄差多大,假的跌幅就有多大。
      #
      # 实测(账号 小边边同志,采集于 2026-09-23 02:07):
      #   最老一条 6.3 天 / 38,513 累计  →  日均 6,094
      #   最新一条 0.3 天 / 18,062 累计  →  日均 64,507
      #   累计量看「暴跌 53%、严重衰退」,换算日均实为「上涨 10 倍」——结论完全相反。
      #
      # 故趋势比较只纳入「已跑够时间」的作品;剩余成熟样本不足时,该子项不计入分母
      # (不奖不罚),而不是拿未成熟的作品给出错误的衰退结论。
      TREND_MATURE_DAYS = 3
      TREND_MIN_SAMPLE = 4
      
      
      # ============================================================
      # 维度1: 账号基础画像 (10分)
      # ============================================================
      
      def diagnose_profile(acc):
          score = 0
          details = []
          # 卷面 8 分:头像2 + 简介3 + 地域2 + 抖音号1
          # (性别/年龄仅作信息展示,不参与计分,见下方「人群标签」说明)
          effective_max = 8
      
          # 头像 (2分)
          avatar = acc.get("avatarUrl", "")
          if avatar:
              score += 2
              details.append(("头像", "已设置", 2, 2))
          else:
              details.append(("头像", "未设置", 0, 2))
      
          # 简介 (3分)
          sig = acc.get("signature", "") or ""
          if len(sig) >= 15:
              score += 3
              details.append(("简介", f"{len(sig)}字,内容完整", 3, 3))
          elif len(sig) > 0:
              score += 1
              details.append(("简介", f"仅{len(sig)}字,偏短", 1, 3))
          else:
              details.append(("简介", "空简介", 0, 3))
      
          # 地域信息完整度 (2分)
          # 注意:不再要求 province == ipLocation。明星号常驻剧组/异地工作,
          # 机构号由团队代运营,注册地与 IP 属地不一致属行业常态,不应据此扣分。
          province = acc.get("province", "") or ""
          ip_loc = acc.get("ipLocation", "") or ""
          if province and ip_loc:
              score += 2
              details.append(("地域信息完整度", f"{province} / IP({ip_loc}),信息完整", 2, 2))
          elif province or ip_loc:
              score += 1
              details.append(("地域信息完整度", f"仅{'省份' if province else 'IP属地'}有值,信息不全", 1, 2))
          else:
              details.append(("地域信息完整度", "地域信息缺失", 0, 2))
      
          # 人群标签 (不计分,仅信息展示)
          #
          # 该子项原为 2 分,且「接口未提供的字段不计入分母」。但接口一旦提供了字段,
          # 取值必然有效(不存在「提供了但无效」的情况),于是 gained 恒等于分母 full ——
          # 子项恒定拿满,毫无区分度;更糟的是它会**稀释其他缺失项的扣分**:
          #   A:地域缺失(1/2) + 性别年龄可用  → (7+2)/(8+2) = 90.0%
          #   B:地域缺失(1/2) + 性别年龄缺失  →  7/8        = 87.5%
          # 两者资料质量完全相同,A 却仅因「接口恰好返回了性别」而得分更高,
          # 与「账号不为数据可得性买单」的原则相悖(该原则意味着既不该罚、也不该奖)。
          #
          # 故改为**不计分的展示项**:既保留数据完整度信息供人工判断,
          # 又不再影响任何账号的得分。账号基础画像卷面分相应由 10 调整为 8。
          gender = acc.get("gender", "")
          age = acc.get("age")
          avail = int(_field_provided(gender)) + int(_field_provided(age))
          detail_str = f"性别={gender if _field_provided(gender) else '未知'}, " \
                       f"年龄={age if _field_provided(age) else '未知'}"
          detail_str += f"(接口提供 {avail}/2,仅展示不计分)"
          details.append(("人群标签", detail_str, 0, 0))
      
          # 账号ID完整度 (1分)
          account_id = acc.get("accountId", "")
          if account_id:
              score += 1
              details.append(("抖音号", account_id, 1, 1))
          else:
              details.append(("抖音号", "未设置", 0, 1))
      
          return score, details, effective_max
      
      
      # ============================================================
      # 维度2: 内容生产力 (15分)
      # ============================================================
      
      def diagnose_productivity(acc):
          score = 0
          details = []
          effective_max = 15
          aweme_count = acc.get("awemeCount", 0) or 0
          total_favorited = acc.get("totalFavorited", 0) or 0
          works = acc.get("works", []) or []
          tier = account_tier(acc.get("followerCount", 0) or 0)[0]
      
          # 作品总量 (5分) —— 门槛随量级自适应,见 VOLUME_BENCH
          v_high, v_normal, v_over = VOLUME_BENCH[tier]
          if aweme_count >= v_over:
              s = 4
              label = f"{aweme_count},极高产(超{v_over}条,需警惕刷量)"
          elif aweme_count >= v_high:
              s = 5
              label = f"{aweme_count},高产({tier}级线{v_high}条)"
          elif aweme_count >= v_normal:
              s = 4
              label = f"{aweme_count},正常({tier}级线{v_normal}条)"
          else:
              s = 2
              label = f"{aweme_count},偏少(低于{tier}级线{v_normal}条)"
          score += s
          details.append(("作品总量", label, s, 5))
      
          # 人均获赞 (5分) —— 门槛随量级自适应,见 AVG_LIKE_BENCH
          avg_likes = _safe_div(total_favorited, aweme_count)
          l_high, l_good, l_normal = AVG_LIKE_BENCH[tier]
          if avg_likes >= l_high:
              s = 5
              label = f"{avg_likes:,.0f},高效({tier}级线{l_high:,})"
          elif avg_likes >= l_good:
              s = 4
              label = f"{avg_likes:,.0f},良好({tier}级线{l_good:,})"
          elif avg_likes >= l_normal:
              s = 3
              label = f"{avg_likes:,.0f},正常({tier}级线{l_normal:,})"
          else:
              s = 1
              label = f"{avg_likes:,.0f},低效(低于{l_normal:,})"
          score += s
          details.append(("人均获赞", label, s, 5))
      
          # 发布频率 (5分)
          # 用「返回样本条数 / 样本时间窗」推算。注意该指标只反映样本窗口内的更新节奏,
          # 样本过少时窗口只有几天,推算结果实际是「最后两条之间的间隔倒数」,不代表账号产能,
          # 故 n < SAMPLE_MIN 时不计入分母。
          if len(works) < SAMPLE_MIN:
              details.append(("发布频率", f"仅返回{len(works)}条作品,样本不足,不计入分母", 0, 0))
              effective_max -= 5
          else:
              times = sorted([_parse_dt(w.get("createTime", "")) for w in works if _parse_dt(w.get("createTime", ""))])
              if len(times) >= 2:
                  span_days = (times[-1] - times[0]).total_seconds() / 86400
                  if span_days == 0:
                      span_days = 1
                  freq = len(works) / span_days
                  if freq < 0.5:
                      s = 2
                      label = f"日均{freq:.2f}条,低频"
                  elif freq <= 2:
                      s = 5
                      label = f"日均{freq:.2f}条,正常"
                  elif freq <= 3:
                      s = 4
                      label = f"日均{freq:.2f}条,高频"
                  else:
                      s = 3
                      label = f"日均{freq:.2f}条,极高频"
                  score += s
                  details.append(("发布频率", f"{label}(样本窗{span_days:.0f}天)", s, 5))
              else:
                  details.append(("发布频率", "时间数据不足,不计入分母", 0, 0))
                  effective_max -= 5
      
          return score, details, effective_max
      
      
      # ============================================================
      # 维度3: 互动健康度 (30分)
      # ============================================================
      
      def diagnose_engagement(acc):
          score = 0
          details = []
          effective_max = 30
          followers = acc.get("followerCount", 0) or 0
          total_favorited = acc.get("totalFavorited", 0) or 0
          works = acc.get("works", []) or []
          tier, tier_label = account_tier(followers)
      
          # 粉丝获赞比 (10分) —— 逐档平滑,消除原「5~20 一刀切」造成的评分跳变
          fan_like_ratio = _safe_div(total_favorited, followers)
          if fan_like_ratio >= 25:
              s, label = 10, f"{fan_like_ratio:.2f},优质(长尾效应强)"
          elif fan_like_ratio >= 15:
              s, label = 9, f"{fan_like_ratio:.2f},良好"
          elif fan_like_ratio >= 8:
              s, label = 8, f"{fan_like_ratio:.2f},正常"
          elif fan_like_ratio >= 3:
              s, label = 6, f"{fan_like_ratio:.2f},一般"
          elif fan_like_ratio >= 1:
              s, label = 3, f"{fan_like_ratio:.2f},偏低"
          else:
              s, label = 1, f"{fan_like_ratio:.2f},极低"
          score += s
          details.append(("粉丝获赞比", label, s, 10))
      
          # 粉丝互动率 (10分) —— 基准随账号量级自适应(核心修正)
          if works and followers > 0:
              avg_inter = statistics.mean([w.get("interactiveCount", 0) or 0 for w in works])
              engagement_rate = _safe_div(avg_inter, followers) * 100
              full, good, low = ENGAGEMENT_BENCH[tier]
              if engagement_rate >= full:
                  s = 10
                  label = f"{engagement_rate:.2f}%,优秀({tier_label}基准≥{full}%)"
              elif engagement_rate >= good:
                  s = 7
                  label = f"{engagement_rate:.2f}%,良好({tier_label}基准≥{good}%)"
              elif engagement_rate >= low:
                  s = 4
                  label = f"{engagement_rate:.2f}%,偏低({tier_label}基准≥{low}%)"
              else:
                  s = 1
                  label = f"{engagement_rate:.2f}%,极低({tier_label}基准<{low}%)"
              score += s
              details.append(("粉丝互动率", label, s, 10))
          elif followers <= 0:
              # 有作品但粉丝数为 0:比值的分母缺失,不可计算 → 不计入分母
              details.append(("粉丝互动率", "粉丝数为 0,比率不可计算,不计入分母", 0, 0))
              effective_max -= 10
          else:
              # 无近期作品:账号没有可评估的内容表现,按最低档计分。
              # 「无作品数据」与「样本不足」是两回事——后者是数据存在但不可靠,故不计入
              # 分母;此处是账号确实没有内容可评,属于负面信号,应与同维度其余
              # 「无作品数据」子项口径一致(均为 0 分),不能一处给 0、一处给中性 3 分。
              details.append(("粉丝互动率", "无作品数据,无法计算", 0, 10))
      
          # 互动结构比 (5分) —— 放宽门槛
          # 超头部号评论/转发被体量稀释,原「评论>5% 且 分享>5%」门槛过窄,
          # 会误判健康的结构为「一般」。
          if works:
              total_digg = sum(w.get("diggCount", 0) or 0 for w in works)
              total_comment = sum(w.get("commentCount", 0) or 0 for w in works)
              total_share = sum(w.get("shareCount", 0) or 0 for w in works)
              if total_digg > 0:
                  comment_ratio = total_comment / total_digg * 100
                  share_ratio = total_share / total_digg * 100
                  ratio_text = f"赞:评:转 = 100:{comment_ratio:.1f}:{share_ratio:.1f}"
                  if comment_ratio >= 4 and share_ratio >= 4:
                      s, label = 5, f"{ratio_text},健康"
                  elif comment_ratio >= 3 and share_ratio >= 3:
                      s, label = 4, f"{ratio_text},较健康"
                  elif comment_ratio >= 2 or share_ratio >= 2:
                      s, label = 3, f"{ratio_text},一般"
                  else:
                      s, label = 2, f"{ratio_text},互动单一"
                  score += s
                  details.append(("互动结构比", label, s, 5))
              else:
                  # 分子分母同时为 0(赞/评/转全为 0),比值在数学上不存在,
                  # 即不可计算 → 不计入分母。该情形已由「零互动占比」与「爆款率」
                  # 充分反映,此处再记 0 分属于对同一事实的重复扣分。
                  details.append(("互动结构比", "点赞总数为 0,结构比不可计算,不计入分母", 0, 0))
                  effective_max -= 5
          else:
              details.append(("互动结构比", "无作品数据", 0, 5))
      
          # 作品均互动 (5分)
          # 修正:原实现用 followerCount×3% 作基准,与上一项「粉丝互动率」计算的
          # 是同一个比率(avg_inter/followers),等于把同一指标计了两遍、共占15/30分。
          # 改为衡量「绝对传播量级」,与互动率形成互补:
          # 互动率看粉丝黏性,绝对量级看单条声量。
          if works:
              avg_inter = statistics.mean([w.get("interactiveCount", 0) or 0 for w in works])
              shown = _fmt_num(int(avg_inter))
              # 分档线随量级自适应(见 INTERACTION_BENCH),避免尾部号因绝对量级小被误判
              b_epic, b_strong, b_mid, b_weak = INTERACTION_BENCH[tier]
              if avg_inter >= b_epic:
                  s, label = 5, f"均{shown}互动,现象级(超头部/头部基准)"
              elif avg_inter >= b_strong:
                  s, label = 4, f"均{shown}互动,强传播"
              elif avg_inter >= b_mid:
                  s, label = 3, f"均{shown}互动,中等"
              elif avg_inter >= b_weak:
                  s, label = 2, f"均{shown}互动,偏弱"
              else:
                  s, label = 1, f"均{shown}互动,微弱"
              score += s
              details.append(("作品均互动", label, s, 5))
          else:
              details.append(("作品均互动", "无作品数据", 0, 5))
      
          return score, details, effective_max
      
      
      # ============================================================
      # 维度4: 内容质量 (20分)
      # ============================================================
      
      def diagnose_quality(acc):
          score = 0
          details = []
          effective_max = 20
          followers = acc.get("followerCount", 0) or 0
          works = acc.get("works", []) or []
          tier, tier_label = account_tier(followers)
      
          if not works:
              # 无近期作品 → 无内容表现可言,四个子项均按最低档(0 分)计。
              # 子项名必须与下方正式路径完全一致(原实现此处写作「中位互动」,
              # 与别处的「中位/均值偏离」不符,会导致报告出现两个不同名字的同一指标)。
              details.append(("爆款率", "无作品数据", 0, 6))
              details.append(("中位/均值偏离", "无作品数据", 0, 4))
              details.append(("互动稳定性", "无作品数据", 0, 5))
              details.append(("零互动占比", "无作品数据", 0, 5))
              return 0, details, effective_max
      
          digg_counts = [w.get("diggCount", 0) or 0 for w in works]
          inter_counts = [w.get("interactiveCount", 0) or 0 for w in works]
      
          # 爆款率 (6分) —— 爆款线随量级自适应(核心修正)
          # 原规则统一要求「点赞 > 粉丝数×10%」:670 万粉的账号需单条破 67 万赞
          # 才算爆款,实际上超头部号几乎不可能达成,会导致爆款率恒为 0。
          # 改为按量级设定点赞率线(S 级 1%、A 级 2%、B 级 5%、C 级 10%)。
          hit_line = HIT_BENCH[tier]
          threshold = followers * hit_line / 100
          hits = sum(1 for d in digg_counts if d > threshold)
          hit_rate = hits / len(works) * 100
          if hit_rate > 10:
              s = 6
              label = f"{hit_rate:.1f}%,优秀(爆款线={tier_label}粉丝×{hit_line}%)"
          elif hit_rate > 5:
              s = 4
              label = f"{hit_rate:.1f}%,良好(爆款线={tier_label}粉丝×{hit_line}%)"
          elif hit_rate > 0:
              s = 3
              label = f"{hit_rate:.1f}%,偏低(爆款线={tier_label}粉丝×{hit_line}%)"
          else:
              s = 1
              label = f"{hit_rate:.1f}%,无爆款(爆款线={tier_label}粉丝×{hit_line}%)"
          score += s
          details.append(("爆款率", label, s, 6))
      
          # 中位互动 vs 均值偏离 (4分)
          # 何时不计入分母:n=2 时中位数恒等于均值,偏离度恒为 0 → 必然满分、无法区分好坏。
          # 注意原实现的 n<2 分支给 2/4,属于「样本不足反而白拿分」,一并改为不计入。
          if len(digg_counts) < MEDIAN_SAMPLE_MIN:
              details.append(("中位/均值偏离",
                              f"仅{len(digg_counts)}条作品,中位数恒等于均值,无法区分,不计入分母", 0, 0))
              effective_max -= 4
          else:
              med = statistics.median(digg_counts)
              avg = statistics.mean(digg_counts)
              if avg > 0:
                  deviation = abs(med - avg) / avg
                  if deviation < 0.2:
                      s = 4
                      label = f"中位{_fmt_num(int(med))} vs 均值{_fmt_num(int(avg))},分布均匀"
                  elif deviation < 0.5:
                      s = 3
                      label = f"中位{_fmt_num(int(med))} vs 均值{_fmt_num(int(avg))},轻度偏离"
                  else:
                      s = 1
                      label = f"中位{_fmt_num(int(med))} vs 均值{_fmt_num(int(avg))},严重偏离(靠爆款拉动)"
                  score += s
                  details.append(("中位/均值偏离", label, s, 4))
              else:
                  # 点赞全为 0 → 偏离度 = 0/0,在数学上不存在,不计入分母。
                  # 与「互动结构比」同理,该事实已由「零互动占比」「爆款率」反映。
                  details.append(("中位/均值偏离", "点赞总数为 0,偏离度不可计算,不计入分母", 0, 0))
                  effective_max -= 4
      
          # 互动稳定性 (5分) - 变异系数
          # 何时不计入分母:n < SAMPLE_MIN 时 CV 由极少数样本决定,无统计意义,
          # 且实测会与「爆款集中度」给出相反结论(本例 CV 判"严重依赖爆款"、
          # 集中度判"均匀分布"),两个指标互相打架时不能采信任一。
          if len(inter_counts) < SAMPLE_MIN:
              details.append(("互动稳定性",
                              f"仅{len(inter_counts)}条作品,离散度无统计意义,不计入分母", 0, 0))
              effective_max -= 5
          elif statistics.mean(inter_counts) <= 0:
              # 互动全为 0:CV = 标准差/均值 = 0/0,不可计算。注意 `_cv()` 在均值为 0 时
              # 返回 0.0,与「真正稳定」的取值无法区分,会把这个账号误判成「稳定输出 5/5」,
              # 故必须在调用前先拦截。
              details.append(("互动稳定性", "互动总数为 0,变异系数不可计算,不计入分母", 0, 0))
              effective_max -= 5
          else:
              cv = _cv(inter_counts)
              if cv < 0.5:
                  s = 5
                  label = f"CV={cv:.2f},稳定输出"
              elif cv < 1.0:
                  s = 3
                  label = f"CV={cv:.2f},波动一般"
              else:
                  s = 1
                  label = f"CV={cv:.2f},严重依赖爆款"
              score += s
              details.append(("互动稳定性", label, s, 5))
      
          # 零互动占比 (5分)
          zero_count = sum(1 for d in digg_counts if d < 10)
          zero_rate = zero_count / len(works) * 100
          if zero_rate < 5:
              s = 5
              label = f"{zero_rate:.1f}%,质量稳定"
          elif zero_rate < 20:
              s = 3
              label = f"{zero_rate:.1f}%,部分低质"
          else:
              s = 1
              label = f"{zero_rate:.1f}%,大量低质/限流"
          score += s
          details.append(("零互动占比", label, s, 5))
      
          return score, details, effective_max
      
      
      # ============================================================
      # 维度5: 内容趋势 (15分)
      # ============================================================
      
      def diagnose_trend(acc):
          score = 0
          details = []
          works = acc.get("works", []) or []
          crawl_time = _parse_dt(acc.get("crawlTime", ""))
          # 有效满分:数据不足的指标不计入分母,避免账号为「接口没给数据」买单
          effective_max = 15
      
          if not works:
              details.append(("近期趋势", "无作品数据", 0, 5))
              details.append(("爆款集中度", "无作品数据", 0, 5))
              details.append(("最新活跃度", "无作品数据", 0, 5))
              return 0, details, effective_max
      
          # 按时间排序
          sorted_works = sorted(works, key=lambda w: _parse_dt(w.get("createTime", "")) or datetime.min)
      
          # 近期趋势 (5分) - 最近30% vs 最早30%
          n = len(sorted_works)
          aweme_count = acc.get("awemeCount", 0) or 0
      
          # 只比较「已跑够时间」的作品:接口给的是一张累计量的快照,
          # 越新的作品累计时间越短、累计量必然越少,直接比较会把「最近发布」
          # 误判成「衰退」。详见 TREND_MATURE_DAYS 处的说明。
          trend_ref = _trend_ref_time(sorted_works, crawl_time)
          mature = [w for w in sorted_works if _work_age_days(w, trend_ref) >= TREND_MATURE_DAYS]
      
          if len(mature) >= TREND_MIN_SAMPLE:
              m = len(mature)
              early = mature[:max(1, m // 3)]
              recent = mature[-(max(1, m // 3)):]
              early_avg = statistics.mean([w.get("interactiveCount", 0) or 0 for w in early])
              recent_avg = statistics.mean([w.get("interactiveCount", 0) or 0 for w in recent])
              if early_avg <= 0:
                  # 早期均互动为 0 → 变化率 = Δ/0,不可计算 → 不计入分母(同 0/0 原则)
                  effective_max -= 5
                  details.append(("近期趋势", "早期作品互动为 0,变化率不可计算,不计入分母", 0, 0))
              else:
                  change = (recent_avg - early_avg) / early_avg * 100
                  if change > 20:
                      s = 5
                      label = f"近期上升{change:.0f}%,增长期"
                  elif change > -20:
                      s = 4
                      label = f"近期变化{change:+.0f}%,平稳期"
                  elif change > -50:
                      s = 2
                      label = f"近期下降{abs(change):.0f}%,衰退期"
                  else:
                      s = 1
                      label = f"近期暴跌{abs(change):.0f}%,严重衰退"
                  score += s
                  details.append(("近期趋势", label, s, 5))
          else:
              # 成熟样本不足:不奖不罚,也不给出可能反向的结论。
              effective_max -= 5
              details.append((
                  "近期趋势",
                  f"已满{TREND_MATURE_DAYS}天的作品仅{len(mature)}条(返回{n}条/共{aweme_count}条),"
                  f"快照下趋势不可判定,不计入分母",
                  0, 0,
              ))
      
          # 爆款集中度 (5分)
          # 两种情形不计入分母:
          #   ① 集中度 = max/avg,数学上恒 ∈ [1, n]。样本只有 2 条时永远 < 3 倍、
          #      必然拿到「均匀分布」满分——它根本无法触及「依赖爆款」档,属于
          #      「无法区分好坏却给满分」的退化子项。
          #   ② 互动全为 0 时 max/avg = 0/0,比值在数学上不存在(该事实已由
          #      「零互动占比」反映,此处再记 0 分属重复扣分)。
          inter_counts = [w.get("interactiveCount", 0) or 0 for w in works]
          if len(inter_counts) < SAMPLE_MIN:
              details.append(("爆款集中度",
                              f"仅{len(inter_counts)}条作品,max/均值≤{len(inter_counts)}倍,无法区分,不计入分母", 0, 0))
              effective_max -= 5
          elif statistics.mean(inter_counts) <= 0:
              details.append(("爆款集中度", "互动总数为 0,集中度不可计算,不计入分母", 0, 0))
              effective_max -= 5
          else:
              max_inter = max(inter_counts)
              avg_inter = statistics.mean(inter_counts)
              concentration = max_inter / avg_inter
              if concentration < 3:
                  s = 5
                  label = f"最大/均值={concentration:.1f}倍,均匀分布"
              elif concentration < 5:
                  s = 4
                  label = f"最大/均值={concentration:.1f}倍,较为均匀"
              elif concentration < 10:
                  s = 2
                  label = f"最大/均值={concentration:.1f}倍,依赖爆款"
              else:
                  s = 1
                  label = f"最大/均值={concentration:.1f}倍,严重依赖单条"
              score += s
              details.append(("爆款集中度", label, s, 5))
      
          # 最新活跃度 (5分)
          latest_time = _parse_dt(sorted_works[-1].get("createTime", ""))
          if latest_time:
              # 参考时间取账号采集时间,但若采集时间早于最新作品(基础信息字段滞后,
              # 如 crawlTime=2026-07-07 而作品已更新到 2026-09-21),该字段不可信,
              # 回退到当前时间,避免算出负数天数被误判为「活跃」。
              stale = bool(crawl_time) and crawl_time < latest_time
              ref_time = datetime.now() if (not crawl_time or stale) else crawl_time
              days_since = max(0, (ref_time - latest_time).days)
              if days_since <= 3:
                  s = 5
                  label = f"最近{days_since}天前发布,活跃"
              elif days_since <= 7:
                  s = 4
                  label = f"最近{days_since}天前发布,正常"
              elif days_since <= 14:
                  s = 2
                  label = f"最近{days_since}天前发布,偏沉默"
              else:
                  s = 1
                  label = f"最近{days_since}天前发布,已断更"
              if stale:
                  label += "(采集时间滞后,按当前时间校正)"
              score += s
              details.append(("最新活跃度", label, s, 5))
          else:
              # 作品缺 createTime → 活跃天数不可计算。这是「字段缺失」而非「账号没内容」,
              # 按不奖不罚原则不计入分母(原实现给中性 3/5,口径与其余字段缺失处理不一致)。
              effective_max -= 5
              details.append(("最新活跃度", "作品时间缺失,活跃天数不可计算,不计入分母", 0, 0))
      
          return score, details, effective_max
      
      
      # ============================================================
      # 维度6: 粉丝质量 (10分)
      # ============================================================
      
      def diagnose_fans(acc):
          score = 0
          details = []
          effective_max = 10
          followers = acc.get("followerCount", 0) or 0
          total_favorited = acc.get("totalFavorited", 0) or 0
          works = acc.get("works", []) or []
          tier, tier_label = account_tier(followers)
      
          # 粉丝规模 (4分) —— 直接由量级派生,避免与 TIER_LEVELS 重复定义同一组粉丝数门槛
          # (重复定义一旦改动漏改一处,量级判定与粉丝规模得分就会互相矛盾)
          s = {"S": 4, "A": 3, "B": 2, "C": 1}[tier]
          score += s
          details.append(("粉丝规模", f"{_fmt_num(followers)},{tier_label}", s, 4))
      
          # 粉丝互动比 (3分) —— 同样改用量级自适应基准
          if works and followers > 0:
              avg_inter = statistics.mean([w.get("interactiveCount", 0) or 0 for w in works])
              fan_eng = _safe_div(avg_inter, followers) * 100
              _, good, low = ENGAGEMENT_BENCH[tier]
              if fan_eng >= good:
                  s = 3
                  label = f"{fan_eng:.2f}%,粉丝活跃({tier_label}基准≥{good}%)"
              elif fan_eng >= low:
                  s = 2
                  label = f"{fan_eng:.2f}%,粉丝一般({tier_label}基准≥{low}%)"
              else:
                  s = 1
                  label = f"{fan_eng:.2f}%,粉丝不活跃({tier_label}基准<{low}%)"
              score += s
              details.append(("粉丝互动比", label, s, 3))
          elif followers <= 0:
              details.append(("粉丝互动比", "粉丝数为 0,比率不可计算,不计入分母", 0, 0))
              effective_max -= 3
          else:
              # 无作品数据:与「粉丝互动率」同口径,按最低档计分而非给中性分
              details.append(("粉丝互动比", "无作品数据,无法计算", 0, 3))
      
          # 获赞/粉丝背离 (3分)
          ratio = _safe_div(total_favorited, followers)
          if ratio > 100:
              s = 1
              label = f"获赞/粉丝={ratio:.1f},异常高(疑似刷赞/搬运)"
          elif ratio > 50:
              s = 2
              label = f"获赞/粉丝={ratio:.1f},偏高"
          elif ratio > 0:
              s = 3
              label = f"获赞/粉丝={ratio:.1f},正常"
          else:
              s = 1
              label = "数据异常"
          score += s
          details.append(("获赞/粉丝背离", label, s, 3))
      
          return score, details, effective_max
      
      
      # ============================================================
      # 预警检测
      # ============================================================
      
      def check_warnings(acc):
          warnings = []
          followers = acc.get("followerCount", 0) or 0
          total_favorited = acc.get("totalFavorited", 0) or 0
          works = acc.get("works", []) or []
          tier, tier_label = account_tier(followers)
      
          # 僵尸粉预警阈值与「粉丝互动率」评分基准保持同一套量级线,
          # 避免出现「互动率被判定为良好、却又触发僵尸粉预警」的自相矛盾。
          if works and followers > 50000:
              avg_inter = statistics.mean([w.get("interactiveCount", 0) or 0 for w in works])
              rate = _safe_div(avg_inter, followers) * 100
              _, _, low = ENGAGEMENT_BENCH[tier]
              if rate < low:
                  warnings.append(
                      f"僵尸粉预警: 粉丝互动率 {rate:.2f}% < {low}%({tier_label}基准)且粉丝数 > 5万"
                  )
      
          # 趋势类预警(刷量骤降 / 衰退)与「近期趋势」共用同一份成熟样本,
          # 避免出现「评分判定为平稳、预警却报衰退」的自相矛盾,
          # 以及把「最近才发布、累计量天然偏少」误读为骤降。详见 TREND_MATURE_DAYS 说明。
          _ref = _trend_ref_time(works, _parse_dt(acc.get("crawlTime", "")))
          mature_works = sorted(
              [w for w in works if _work_age_days(w, _ref) >= TREND_MATURE_DAYS],
              key=lambda w: _parse_dt(w.get("createTime", "")) or datetime.min,
          )
          declining = False
          if len(mature_works) >= TREND_MIN_SAMPLE:
              m = len(mature_works)
              _early_avg = statistics.mean(
                  [w.get("interactiveCount", 0) or 0 for w in mature_works[:max(1, m // 3)]])
              _recent_avg = statistics.mean(
                  [w.get("interactiveCount", 0) or 0 for w in mature_works[-(max(1, m // 3)):]])
              if _early_avg > 0:
                  declining = _recent_avg < _early_avg * 0.5
      
          ratio = _safe_div(total_favorited, followers)
          if ratio > 100 and declining:
              warnings.append("刷量预警: 获赞/粉丝比 > 100 且近期互动骤降")
      
          if declining:
              warnings.append("衰退预警: 近期作品均互动 < 早期的 50%(仅基于已满"
                              f"{TREND_MATURE_DAYS}天的 {len(mature_works)} 条作品)")
      
          zero_count = sum(1 for w in works if (w.get("diggCount", 0) or 0) < 10)
          if works and zero_count / len(works) > 0.3:
              warnings.append("限流预警: 零互动占比 > 30%")
      
          crawl_time = _parse_dt(acc.get("crawlTime", ""))
          if works:
              latest = max(_parse_dt(w.get("createTime", "")) or datetime.min for w in works)
              # 与「最新活跃度」保持同一套参考时间逻辑:采集时间滞后于作品时回退当前时间
              if latest != datetime.min:
                  ref_time = datetime.now() if (not crawl_time or crawl_time < latest) else crawl_time
                  if (ref_time - latest).days > 14:
                      warnings.append("断更预警: 最新作品距数据采集时间 > 14 天")
      
          inter_counts = [w.get("interactiveCount", 0) or 0 for w in works]
          if inter_counts:
              max_inter = max(inter_counts)
              avg_inter = statistics.mean(inter_counts)
              if avg_inter > 0 and max_inter / avg_inter > 10:
                  warnings.append("单条依赖预警: 最大爆款互动 > 均值的 10 倍")
      
          return warnings
      
      
      # ============================================================
      # 报告生成
      # ============================================================
      
      WEIGHTS = {
          "profile": 0.10,
          "productivity": 0.15,
          "engagement": 0.30,
          "quality": 0.20,
          "trend": 0.15,
          "fans": 0.10,
      }
      
      DIM_NAMES = {
          "profile": "账号基础画像",
          "productivity": "内容生产力",
          "engagement": "互动健康度",
          "quality": "内容质量",
          "trend": "内容趋势",
          "fans": "粉丝质量",
      }
      
      DIM_MAX = {
          "profile": 8,
          "productivity": 15,
          "engagement": 30,
          "quality": 20,
          "trend": 15,
          "fans": 10,
      }
      
      
      def composite_score(diagnoses):
          """由各维度 (score, details, effective_max) 计算综合评分。
      
          综合评分 = Σ(维度得分 / 维度有效满分 × 100 × 维度权重),按文档约定四舍五入取整。
          统一在此实现,避免报告与测试各写一套导致口径漂移。
          """
          total = 0.0
          for key in ("profile", "productivity", "engagement", "quality", "trend", "fans"):
              score, _details, eff_max = diagnoses[key]
              total += _safe_div(score, eff_max) * 100 * WEIGHTS[key]
          # Python 内置 round() 为银行家舍入(四舍六入五成双),在 .5 边界偏低,
          # 与「四舍五入」的中文约定不符,故显式用 ROUND_HALF_UP;
          # 先把累计浮点误差归一到 6 位小数,避免 94.499999 被误判为 94。
          return int(Decimal(str(round(total, 6))).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
      
      
      def generate_report(acc):
          lines = []
          lines.append("=" * 60)
          lines.append("               抖音账号诊断报告")
          lines.append("=" * 60)
      
          # 基本信息
          lines.append("")
          lines.append("【基本信息】")
          lines.append(f"  昵称:     {acc.get('nickname', 'N/A')}")
          lines.append(f"  抖音号:   {acc.get('accountId', 'N/A')}")
          lines.append(f"  UID:      {acc.get('uid', 'N/A')}")
          lines.append(f"  粉丝数:   {_fmt_num(acc.get('followerCount', 0))}")
          lines.append(f"  作品数:   {_fmt_num(acc.get('awemeCount', 0))}")
          lines.append(f"  获赞总数: {_fmt_num(acc.get('totalFavorited', 0))}")
          lines.append(f"  地区:     {(acc.get('province') or '')}{(acc.get('city') or '') or 'N/A'}")
          lines.append(f"  IP属地:   {acc.get('ipLocation', 'N/A')}")
          tier_code, tier_label = account_tier(acc.get("followerCount", 0) or 0)
          lines.append(f"  账号量级: {tier_code} 级 {tier_label}")
          lines.append(f"  数据时间: {acc.get('crawlTime', 'N/A')}")
      
          # 执行诊断
          diagnoses = {
              "profile": diagnose_profile(acc),
              "productivity": diagnose_productivity(acc),
              "engagement": diagnose_engagement(acc),
              "quality": diagnose_quality(acc),
              "trend": diagnose_trend(acc),
              "fans": diagnose_fans(acc),
          }
      
          # 各维度明细
          lines.append("")
          lines.append("【维度评分明细】")
          for key in ["profile", "productivity", "engagement", "quality", "trend", "fans"]:
              score, details, effective_max = diagnoses[key]
              lines.append("")
              suffix = "" if effective_max == DIM_MAX[key] else f" [有效满分{effective_max},满分{DIM_MAX[key]}]"
              lines.append(f"  {DIM_NAMES[key]} ({score}/{effective_max}){suffix}")
              for item_name, desc, gained, full in details:
                  if full == 0:
                      lines.append(f"    [-----] {item_name}: {desc} (不计入)")
                      continue
                  bar = "+" * gained + "-" * (full - gained)
                  lines.append(f"    [{bar}] {item_name}: {desc} ({gained}/{full})")
      
          # 综合评分(统一由 composite_score 计算,避免与测试各写一套导致口径漂移)
          lines.append("")
          lines.append("=" * 60)
          total_score = composite_score(diagnoses)
          if total_score >= 85:
              grade = "优质账号"
              emoji = "🟢"
          elif total_score >= 70:
              grade = "正常账号"
              emoji = "🟡"
          elif total_score >= 50:
              grade = "待优化"
              emoji = "🟠"
          else:
              grade = "风险账号"
              emoji = "🔴"
          lines.append(f"  综合诊断评分: {total_score}/100  {emoji} {grade}")
          lines.append("=" * 60)
      
          # 样本置信度提示
          # 样本过少的子项虽已剔除出分母,但**保留下来的均值型指标**(粉丝互动率、
          # 作品均互动、爆款率等)仍由极少数样本决定,必须提示复核,
          # 避免使用者把「2 条作品的结论」当成账号全貌。
          works_conf = acc.get("works", []) or []
          if not works_conf:
              lines.append("")
              lines.append(
                  f"  ⓘ 无近期作品数据:接口未返回任何作品(账号标注共 {acc.get('awemeCount', 0)} 条)。"
                  "互动健康度 / 内容质量 / 内容趋势 中依赖作品的子项均按最低档计分,"
                  "分数偏低属预期,请先核对账号是否已清空作品、注销或长期停更。"
              )
          elif len(works_conf) < SAMPLE_MIN:
              lines.append("")
              lines.append(
                  f"  ⓘ 数据置信度:接口仅返回 {len(works_conf)} 条作品"
                  f"(账号共 {acc.get('awemeCount', 0)} 条),"
                  "互动健康度 / 内容质量中的均值型指标基于过小样本,结论仅供参考。"
              )
      
          # 预警
          warnings = check_warnings(acc)
          if warnings:
              lines.append("")
              lines.append("【风险预警】")
              for w in warnings:
                  lines.append(f"  ⚠ {w}")
          else:
              lines.append("")
              lines.append("【风险预警】无")
      
          # 近期作品(必须输出项)
          works = acc.get("works", []) or []
          lines.append("")
          lines.append("【近期作品详情】")
          if works:
              sorted_works = sorted(works, key=lambda w: _parse_dt(w.get("createTime", "")) or datetime.min, reverse=True)
              lines.append(f"  共 {len(sorted_works)} 条近期作品")
              lines.append("")
              for i, w in enumerate(sorted_works, 1):
                  t = w.get("createTime", "N/A")
                  d = w.get("diggCount", 0)
                  c = w.get("commentCount", 0)
                  s = w.get("shareCount", 0)
                  inter = w.get("interactiveCount", 0)
                  desc = (w.get("desc", "") or w.get("title", ""))[:60]
                  url = w.get("workUrl", "")
                  lines.append(f"  {i}. [{t}] 赞{d} 评{c} 转{s} 总{inter}")
                  lines.append(f"     {desc}")
                  lines.append(f"     {url}")
          else:
              lines.append("  无作品数据")
      
          lines.append("")
          lines.append("-" * 60)
          full, good, low = ENGAGEMENT_BENCH[tier_code]
          lines.append(
              f"  评分基准: 按「{tier_code} 级 {tier_label}」自适应 —— "
              f"互动率 优秀≥{full}% / 良好≥{good}% / 偏低≥{low}%;"
              f"爆款线=粉丝×{HIT_BENCH[tier_code]}%"
          )
          lines.append("  数据来源: 红狐RedFox API (https://redfox.hk/?source=github)")
          lines.append("-" * 60)
      
          return "\n".join(lines)
      
      
      # ============================================================
      # 主入口
      # ============================================================
      
      def main():
          args = sys.argv[1:]
          cli_key = None
          if "--api-key" in args:
              idx = args.index("--api-key")
              if idx + 1 < len(args):
                  cli_key = args[idx + 1]
                  args = args[:idx] + args[idx + 2:]
              else:
                  print("[错误] --api-key 参数后需要提供密钥值")
                  sys.exit(1)
      
          if not args:
              print("用法: python douyin_diagnosis.py <抖音昵称或抖音号> [--api-key <你的API Key>]")
              print("示例: python douyin_diagnosis.py 桔桔的茶园小动物")
              print("示例: python douyin_diagnosis.py xiaojuju8")
              sys.exit(1)
      
          keyword = args[0]
          print(f"正在查询抖音账号: {keyword} ...")
      
          acc = query_account(keyword, require_api_key(cli_key))
          if not acc:
              print("未查询到该抖音账号信息")
              print(f"- 抖音号 {keyword} 不存在或已被注销")
              print("- 抖音号输入有误 — 请核对是否区分大小写,是否为正确的抖音号(非 UID、非昵称)")
              print("- 尚未收录 — 当前仅收录了粉丝数≧1万的账号")
              print("- 申请收录 — 如需收录请发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析")
              sys.exit(1)
      
          report = generate_report(acc)
          print(report)
      
      
      if __name__ == "__main__":
          main()
      
    • douyin_diagnosis_report_v3.md 2.9 KB
      ## 📋 [人民日报](https://www.douyin.com/user/104255897823) - 抖音账号诊断报告
      
      ### 基本信息
      
      | 昵称 | 抖音号 | 地域 | 粉丝数 | 获赞 | 作品数 | 数据时间 |
      |------|--------|------|--------|------|--------|---------|
      | [人民日报](https://www.douyin.com/user/104255897823) | rmrbxmt | 北京·北京 | 2.0亿 | 181.1亿 | 10,992 | 2026-06-02 00:17:31 |
      
      **简介:** 参与、沟通、记录时代。
      
      ---
      
      ### 📊 核心数据
      
      **账号体量:**
      - 粉丝数 2.0亿 | 获赞 181.1亿 | 获赞/粉丝比 90.0
      
      **近7天作品(20条,展示最近7条):**
      
      | # | 发布时间 | 标题 | 点赞 | 评论 | 分享 | 互动 | 传播系数 |
      |---|---------|------|------|------|------|------|---------|
      | 1 | 2026-06-03 21:57 | [盗狗嫌疑人落网!小狗等主人下班时…](https://www.douyin.com/video/7647167835424263478) | 10.1万 | 54 | 1.1万 | 11.5万 | 11.1% |
      | 2 | 2026-06-03 21:12 | [抓捕现场,在逃人员突然逃窜…](https://www.douyin.com/video/7647156256674680105) | 26.2万 | 32 | 5191 | 27.5万 | 2.0% |
      | 3 | 2026-06-03 21:02 | [什么是正确政绩观?](https://www.douyin.com/video/7647153886305832232) | 6821 | 119 | 214 | 7562 | 3.1% |
      | 4 | 2026-06-03 20:59 | ["世界向东看"!100秒看大国外交](https://www.douyin.com/video/7647152982408203555) | 1.6万 | 137 | 728 | 1.8万 | 4.6% |
      | 5 | 2026-06-03 20:01 | [女子网购填写假地址调包退货…](https://www.douyin.com/video/7647138018230291748) | 2.9万 | 128 | 2386 | 3.3万 | 8.1% |
      | 6 | 2026-06-03 19:59 | [货车备胎掉落高速,路过司机搬离](https://www.douyin.com/video/7647137552830385427) | 7.7万 | 122 | 938 | 8.1万 | 1.2% |
      | 7 | 2026-06-03 18:52 | [女子被困阳台向派出所"隔空喊话"](https://www.douyin.com/video/7647120288076827945) | 11.7万 | 61 | 1617 | 12.3万 | 1.4% |
      
      > 若playCount非null,追加"播放量"和"点赞率"列
      
      ---
      
      ### 🏆 综合评分
      
      **总分:93.0 / 100分** — 🏆 标杆账号 S级
      
      | 维度 | 得分 | 满分 | 得分率 |
      |------|------|------|--------|
      | 账号体量 | 40.0 | 40分 | 100.0% |
      | 内容表现 | 33.0 | 40分 | 82.5% |
      | 运营活跃度 | 20.0 | 20分 | 100.0% |
      | **综合** | **93.0** | **100分** | **93.0%** |
      
      **💪 优势:** 账号体量雄厚(2亿粉丝满级);内容数据突出(传播系数17.2%极强自发传播)
      **⚠️ 短板:** 整体表现均衡,持续优化即可
      
      ---
      
      ### 💡 优化建议
      
      1. **平均点赞有提升空间** → 增加轻量化、情感化选题比例,提升单条作品爆款率,拉高平均点赞至50万+
      2. **评论互动深度可强化** → 在热点新闻末尾增加互动引导话术(如"你怎么看?"),提升用户评论参与度
      3. **持续优化方向** → 深耕垂直内容建立差异化标签,开启直播增强粉丝粘性
      
      ---
      
      *诊断时间:2026-06-04 | 数据来源:红狐数据API | 数据更新:2026-06-02 00:17:31*
      
    • generate_diagnosis_report.py 32.9 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      抖音账号诊断报告生成器 v4.0
      基于红狐API /story/api/dyUser/query 接口返回的数据生成标准诊断报告
      四维度评估:账号体量(35分) + 内容表现(35分) + 运营活跃度(20分) + 平台指数(10分)
      """
      
      import json
      from datetime import datetime
      from typing import Dict, List, Optional
      
      
          # \u8d26\u53f7\u5206\u7c7b\u6620\u5c04: API\u539f\u8bcd \u2192 \u8f93\u51fa\u65b0\u8bcd
      CATEGORY_MAP = {
          "全部": "全部",
          "才艺技能": "个人才艺",
          "生活": "生活vlog",
          "财经": "财富理财",
          "二次元": "二次元",
          "家居家装": "居家装修",
          "教育培训": "学习教育",
          "剧情演绎": "小剧场",
          "科技数码": "数码科技",
          "旅游": "旅行",
          "美食": "美食",
          "美妆": "化妆美容",
          "萌宠": "动物",
          "母婴亲子": "亲子",
          "汽车": "汽车",
          "情感心理": "情感",
          "三农": "三农",
          "医疗健康": "健康医学",
          "时尚": "潮流风尚",
          "舞蹈": "舞蹈才艺",
          "颜值": "颜值造型",
          "人文社科": "人文",
          "音乐": "音乐",
          "影视综艺": "影视",
          "健身": "身体锻炼",
          "体育": "体育",
          "明星八卦": "明星娱乐",
          "游戏": "游戏",
      }
      
      
      def map_category(raw: str) -> str:
          """将API原词分类映射为输出新词"""
          return CATEGORY_MAP.get(raw, raw)
      
      
      def format_number(num) -> str:
          """格式化数字显示"""
          if num is None:
              return "-"
          try:
              num = int(num)
          except (ValueError, TypeError):
              return str(num)
          if num >= 100000000:
              return f"{num / 100000000:.1f}亿"
          elif num >= 10000:
              return f"{num / 10000:.1f}万"
          else:
              return f"{num:,}"
      
      
      class DouyinDiagnosisReportV3:
          """抖音账号诊断宗师报告生成器 v4.1 - 四维度评估体系
          账号体量(35分) + 内容表现(35分) + 运营活跃度(20分) + 平台指数(10分)
          """
      
          # 全角冒号常量,避免Python 3.12+源码全角标点SyntaxError
          _C = "\uff1a"
      
          def __init__(self, account_data: Dict):
              """
              初始化报告生成器
      
              Args:
                  account_data: 红狐API /story/api/dyUser/query 返回的账号数据(单条)
              """
              self.data = account_data
              self.works = account_data.get("works", [])
              self.similar_accounts = account_data.get("similarAccounts", [])
      
          # ==================== 主入口 ====================
      
          def generate_report(self) -> str:
              """生成完整诊断报告,严格按固定模板输出"""
              sections = [
                  self._section_scoring_breakdown(),
                  self._section_data_disclaimer(),
                  self._section_basic_info(),
                  self._section_core_data(),
                  self._section_comprehensive_score(),
                  self._section_optimization(),
                  self._section_footer(),
              ]
              return "\n".join(sections)
      
          # ==================== 章节0:评分计算明细 ====================
      
          @staticmethod
          def _fmt(num) -> str:
              """格式化数字显示,带千位分隔符"""
              if num is None:
                  return "-"
              try:
                  num = int(num)
              except (ValueError, TypeError):
                  return str(num)
              return f"{num:,}"
      
          def _section_scoring_breakdown(self) -> str:
              """生成评分计算明细,在诊断报告之前输出"""
              follower = self.data.get("followerCount", 0) or 0
              favorited = self.data.get("totalFavorited", 0) or 0
              ratio = favorited / follower if follower > 0 else 0
              works = self.works
              n = len(works)
              aweme_count = self.data.get("awemeCount", 0) or 0
              sig = self.data.get("signature", "") or ""
              redfox_index = self.data.get("redfoxIndex")
      
              # ---- 维度一:账号体量(35分) ----
              follower_levels = [
                  (50000000, 22, "\u22655000\u4e07"), (10000000, 19, "1000-5000\u4e07"),
                  (5000000, 16, "500-1000\u4e07"), (1000000, 12, "100-500\u4e07"),
                  (500000, 9, "50-100\u4e07"), (100000, 5, "10-50\u4e07"),
                  (10000, 2, "1-10\u4e07"), (0, 0, "<1\u4e07"),
              ]
              s1_f = 0; cond_f = ""
              for th, sc, cond in follower_levels:
                  if follower >= th:
                      s1_f, cond_f = sc, cond; break
      
              favorited_levels = [
                  (1000000000, 8, "\u226510\u4ebf"), (100000000, 6, "1-10\u4ebf"),
                  (10000000, 5, "1000\u4e07-1\u4ebf"), (1000000, 3, "100-1000\u4e07"),
                  (100000, 1, "10-100\u4e07"), (0, 0, "<10\u4e07"),
              ]
              s2_fav = 0; cond_fav = ""
              for th, sc, cond in favorited_levels:
                  if favorited >= th:
                      s2_fav, cond_fav = sc, cond; break
      
              ratio_levels = [
                  (15, 5, "\u226515"), (10, 4, "10-15"),
                  (5, 3, "5-10"), (2, 2, "2-5"), (0, 1, "<2"),
              ]
              s3_r = 0; cond_r = ""
              for th, sc, cond in ratio_levels:
                  if ratio >= th:
                      s3_r, cond_r = sc, cond; break
              body_sub = s1_f + s2_fav + s3_r
      
              # ---- 维度二:内容表现(35分) ----
              total_digg = sum(w.get("diggCount", 0) or 0 for w in works)
              total_comment = sum(w.get("commentCount", 0) or 0 for w in works)
              total_share = sum(w.get("shareCount", 0) or 0 for w in works)
              avg_digg = total_digg / n if n > 0 else 0
              avg_comment = total_comment / n if n > 0 else 0
              avg_share = total_share / n if n > 0 else 0
              spread_coeff = (total_share / total_digg * 100) if total_digg > 0 else 0
      
              digg_levels = [
                  (1000000, 13, "\u2265100\u4e07"), (500000, 11, "50-100\u4e07"),
                  (100000, 9, "10-50\u4e07"), (10000, 7, "1-10\u4e07"),
                  (1000, 4, "1000-1\u4e07"), (100, 2, "100-1000"), (0, 0, "<100"),
              ]
              s1_d = 0; cond_d = ""
              for th, sc, cond in digg_levels:
                  if avg_digg >= th:
                      s1_d, cond_d = sc, cond; break
      
              comment_levels = [
                  (10000, 9, "\u22651\u4e07"), (5000, 7, "5000-1\u4e07"),
                  (1000, 5, "1000-5000"), (100, 3, "100-1000"),
                  (10, 1, "10-100"), (0, 0, "<10"),
              ]
              s2_c = 0; cond_c = ""
              for th, sc, cond in comment_levels:
                  if avg_comment >= th:
                      s2_c, cond_c = sc, cond; break
      
              share_levels = [
                  (100000, 7, "\u226510\u4e07"), (10000, 6, "1-10\u4e07"),
                  (1000, 4, "1000-1\u4e07"), (100, 2, "100-1000"),
                  (10, 1, "10-100"), (0, 0, "<10"),
              ]
              s3_s = 0; cond_s = ""
              for th, sc, cond in share_levels:
                  if avg_share >= th:
                      s3_s, cond_s = sc, cond; break
      
              spread_levels = [
                  (15, 6, "\u226515%"), (10, 5, "10-15%"),
                  (5, 4, "5-10%"), (2, 2, "2-5%"), (0, 1, "<2%"),
              ]
              s4_sp = 0; cond_sp = ""
              for th, sc, cond in spread_levels:
                  if spread_coeff >= th:
                      s4_sp, cond_sp = sc, cond; break
              content_sub = s1_d + s2_c + s3_s + s4_sp
      
              # ---- 维度三:运营活跃度(20分) ----
              freq_levels = [
                  (7, 8, "\u22657\u6761"), (4, 6, "4-6\u6761"),
                  (2, 4, "2-3\u6761"), (1, 2, "1\u6761"), (0, 0, "0\u6761"),
              ]
              s1_fr = 0; cond_fr = ""
              for th, sc, cond in freq_levels:
                  if n >= th:
                      s1_fr, cond_fr = sc, cond; break
      
              # 发布时段
              period_score, period_desc, golden_count = self._get_publish_period_detail()
              s2_pp = period_score
      
              # 账号完整度
              if sig.strip() and len(sig.strip()) > 5:
                  s3_sig, sig_desc = 4, "\u7b80\u4ecb\u975e\u7a7a\u4e14>5\u5b57"
              elif sig.strip():
                  s3_sig, sig_desc = 2, "\u7b80\u4ecb\u975e\u7a7a\u4f46\u22645\u5b57"
              else:
                  s3_sig, sig_desc = 0, "\u7b80\u4ecb\u4e3a\u7a7a"
      
              aweme_levels = [
                  (300, 4, "\u2265300"), (100, 3, "100-300"),
                  (30, 2, "30-100"), (10, 1, "10-30"), (0, 0, "<10"),
              ]
              s4_aw = 0; cond_aw = ""
              for th, sc, cond in aweme_levels:
                  if aweme_count >= th:
                      s4_aw, cond_aw = sc, cond; break
              op_sub = s1_fr + s2_pp + s3_sig + s4_aw
      
              # ---- 维度四:平台指数(10分) ----
              if redfox_index is None:
                  s1_rx, cond_rx = 0, "\u672a\u8fd4\u56de"
              else:
                  redfox_levels = [
                      (900, 10, "\u2265900"), (800, 8, "800-900"),
                      (700, 6, "700-800"), (600, 4, "600-700"),
                      (500, 2, "500-600"), (0, 0, "<500"),
                  ]
                  s1_rx = 0; cond_rx = ""
                  for th, sc, cond in redfox_levels:
                      if redfox_index >= th:
                          s1_rx, cond_rx = sc, cond; break
              platform_sub = s1_rx
      
              total = body_sub + content_sub + op_sub + platform_sub
              grade_icon, grade_name, grade_letter = self._get_grade(total)
      
              # 构建输出文本
              fmt = self._fmt
              lines = []
              lines.append(f"**维度一:账号体量(35分)**")
              lines.append(f"+ 粉丝数 {fmt(follower)}({cond_f})→ {s1_f}分")
              lines.append(f"+ 获赞总数 {fmt(favorited)}({cond_fav})→ {s2_fav}分")
              lines.append(f"+ 获赞/粉丝比 ≈ {ratio:.1f}({cond_r})→ {s3_r}分")
              if body_sub == 35:
                  lines.append(f"+ **小计:{body_sub}分(满分)**")
              else:
                  lines.append(f"+ **小计:{body_sub}分**")
      
              lines.append("")
              lines.append(f"**维度二:内容表现(35分)**")
              if n > 0:
                  lines.append(f"+ {n}条作品:总点赞{fmt(total_digg)} / 总评论{fmt(total_comment)} / 总分享{fmt(total_share)}")
                  lines.append(f"+ 平均点赞 ≈ {fmt(int(avg_digg))}({cond_d})→ {s1_d}分")
                  lines.append(f"+ 平均评论 ≈ {fmt(int(avg_comment))}({cond_c})→ {s2_c}分")
                  lines.append(f"+ 平均分享 ≈ {fmt(int(avg_share))}({cond_s})→ {s3_s}分")
                  lines.append(f"+ 传播系数 ≈ {spread_coeff:.1f}%({cond_sp})→ {s4_sp}分")
              else:
                  lines.append("+ 近7天无作品数据")
              lines.append(f"+ **小计:{content_sub}分**")
      
              lines.append("")
              lines.append(f"**维度三:运营活跃度(20分)**")
              lines.append(f"+ 更新频率 {n}条({cond_fr})→ {s1_fr}分")
              lines.append(f"+ 发布时段 {golden_count}/{n}条为黄金时段 → {s2_pp}分")
              lines.append(f"+ 账号完整度 {sig_desc} → {s3_sig}分")
              lines.append(f"+ 作品总量 {fmt(aweme_count)}({cond_aw})→ {s4_aw}分")
              if op_sub == 20:
                  lines.append(f"+ **小计:{op_sub}分(满分)**")
              else:
                  lines.append(f"+ **小计:{op_sub}分**")
      
              lines.append("")
              lines.append(f"**维度四:平台指数(10分)**")
              if redfox_index is not None:
                  lines.append(f"+ 红狐指数 {redfox_index:.1f}({cond_rx})→ {s1_rx}分")
              else:
                  lines.append(f"+ 红狐指数 未返回 → 0分")
              if platform_sub == 10:
                  lines.append(f"+ **小计:{platform_sub}分(满分)**")
              else:
                  lines.append(f"+ **小计:{platform_sub}分**")
      
              lines.append("")
              lines.append(f"**综合评分:{total:.1f}分 — {grade_icon} {grade_letter} {grade_name}**")
      
              return "\n".join(lines) + "\n"
      
          def _get_publish_period_detail(self):
              """获取发布时段评分详情
              Returns: (score, desc, golden_count)"""
              if not self.works:
                  return 0, "\u65e0\u4f5c\u54c1\u53ef\u5224\u65ad", 0
              hours = []
              for w in self.works:
                  ct = w.get("createTime", "")
                  if ct:
                      try:
                          hour = int(ct.split(" ")[1].split(":")[0])
                          hours.append(hour)
                      except (IndexError, ValueError):
                          pass
              if not hours:
                  return 0, "\u65e0\u4f5c\u54c1\u53ef\u5224\u65ad", 0
              n = len(hours)
              golden = sum(1 for h in hours if (11 <= h <= 13) or (18 <= h <= 21))
              suboptimal = sum(1 for h in hours if (7 <= h <= 9) or (21 <= h <= 23))
              if golden >= n / 2:
                  return 4, "\u9ec4\u91d1\u65f6\u6bb5(11-13/18-21)", golden
              elif golden + suboptimal >= n / 2:
                  return 3, "\u6b21\u4f18\u65f6\u6bb5(7-9/21-23)", golden
              else:
                  return 1, "\u5176\u4ed6\u65f6\u6bb5", golden
      
          # ==================== 章节0b:数据说明 ====================
      
          def _section_data_disclaimer(self) -> str:
              nickname = self.data.get("nickname", "未知")
              return f"> 以下诊断结果是基于{nickname}上周在抖音平台发布的作品进行分析得出。\n\n"
      
          # ==================== 章节1:基本信息 ====================
      
          def _section_basic_info(self) -> str:
              nickname = self.data.get("nickname", "未知")
              account_id = self.data.get("accountId", "-")
              country = self.data.get("country", "")
              province = self.data.get("province", "")
              city = self.data.get("city", "")
              location_parts = [p for p in [country, province, city] if p]
              location = "·".join(location_parts) if location_parts else "-"
              follower_count = format_number(self.data.get("followerCount", 0))
              total_favorited = format_number(self.data.get("totalFavorited", 0))
              aweme_count = self.data.get("awemeCount", 0)
              redfox_index = self.data.get("redfoxIndex")
              redfox_display = f"{redfox_index:.1f}" if redfox_index is not None else "-"
              crawl_time = self.data.get("crawlTime", "-")
              signature = self.data.get("signature", "")
              sig_display = signature if signature and signature.strip() else "未设置"
      
              c = self._C
              return f"""## 📋 {nickname} - 抖音账号诊断报告
      
      ### 基本信息
      
      | 昵称 | 抖音号 | 地域 | 粉丝数 | 获赞 | 作品数 | 红狐指数 | 数据时间 |
      |------|--------|------|--------|------|--------|---------|----------|
      | {nickname} | {account_id} | {location} | {follower_count} | {total_favorited} | {aweme_count} | {redfox_display} | {crawl_time} |
      
      **简介{c}** {sig_display}
      
      ---"""
      
          # ==================== 章节2:核心数据 ====================
      
          def _section_core_data(self) -> str:
              follower_count = self.data.get("followerCount", 0)
              total_favorited = self.data.get("totalFavorited", 0)
              ratio = (total_favorited / follower_count) if follower_count > 0 else 0
      
              # 账号体量数据
              c = self._C
              body = (
                  f"### \U0001f4ca \u6838\u5fc3\u6570\u636e\n\n"
                  f"**\u8d26\u53f7\u4f53\u91cf{c}**\n"
                  f"- \u7c89\u4e1d\u6570 {format_number(follower_count)} | \u83b7\u8d5e {format_number(total_favorited)} | \u83b7\u8d5e/\u7c89\u4e1d\u6bd4 {ratio:.1f}\n\n"
                  f"**\u8fd17\u5929\u4f5c\u54c1({len(self.works)}\u6761){c}**"
              )
              if not self.works:
                  body += "> 近7天无作品数据\n"
              else:
                  # 作品表格
                  header = "| # | 发布时间 | 标题 | 点赞 | 评论 | 分享 | 互动 | 传播系数 |"
                  sep = "|---|---------|------|------|------|------|------|---------|"
                  rows = []
                  for i, w in enumerate(self.works, 1):
                      title = (w.get("title") or w.get("desc") or "\u65e0\u6807\u9898")[:20]
                      create_time = w.get("createTime", "-")
                      if create_time and len(create_time) > 16:
                          create_time = create_time[:16]
                      digg = format_number(w.get("diggCount", 0))
                      comment = format_number(w.get("commentCount", 0))
                      share = format_number(w.get("shareCount", 0))
                      interactive = format_number(w.get("interactiveCount", 0))
                      digg_count = w.get("diggCount", 0) or 0
                      share_count = w.get("shareCount", 0) or 0
                      spread = f"{share_count / digg_count * 100:.1f}%" if digg_count > 0 else "-"
                      # \u524d3\u540d\u7528\u5956\u724c emoji\uff0c\u5176\u4f59\u7528\u6570\u5b57
                      medal = {1: "\U0001f947", 2: "\U0001f948", 3: "\U0001f949"}
                      rank_display = medal.get(i, str(i))
                      rows.append(f"| {rank_display} | {create_time} | {title} | {digg} | {comment} | {share} | {interactive} | {spread} |")
      
                  body += header + "\n" + sep + "\n" + "\n".join(rows) + "\n"
      
                  # playCount非null时追加说明
                  has_play = any(w.get("playCount") is not None for w in self.works)
                  if has_play:
                      body += '\n> playCount有值时,可追加"播放量"和"点赞率"列'
      
              return body + "\n---"
      
          # ==================== 章节3:综合评分 ====================
      
          def _section_comprehensive_score(self) -> str:
              # 计算四维度得分
              body_score, _ = self._score_body()
              content_score, _ = self._score_content()
              operation_score, _ = self._score_operation()
              platform_score, _ = self._score_platform()
              total = body_score + content_score + operation_score + platform_score
      
              # 评级
              grade_icon, grade_name, grade_letter = self._get_grade(total)
      
              # 得分率
              body_rate = f"{body_score / 35 * 100:.1f}%"
              content_rate = f"{content_score / 35 * 100:.1f}%"
              op_rate = f"{operation_score / 20 * 100:.1f}%"
              platform_rate = f"{platform_score / 10 * 100:.1f}%" if platform_score > 0 else "0.0%"
              total_rate = f"{total:.1f}%"
      
              c = self._C
      
              # 优势/短板(列表格式)
              advantages = self._get_advantages(body_score, content_score, operation_score, platform_score)
              weaknesses = self._get_weaknesses(body_score, content_score, operation_score, platform_score)
              adv_lines = "\n".join(f"+ {a}" for a in advantages)
              weak_lines = "\n".join(f"+ {w}" for w in weaknesses)
      
              return f"""### 🏆 综合评分
      
      **总分{c}{total:.1f} / 100分** — {grade_icon} {grade_name} {grade_letter}
      
      | 维度 | 得分 | 满分 | 得分率 |
      |------|------|------|--------|
      | 账号体量 | {body_score:.1f} | 35分 | {body_rate} |
      | 内容表现 | {content_score:.1f} | 35分 | {content_rate} |
      | 运营活跃度 | {operation_score:.1f} | 20分 | {op_rate} |
      | 平台指数 | {platform_score:.1f} | 10分 | {platform_rate} |
      | **综合** | **{total:.1f}** | **100分** | **{total_rate}** |
      
      **💪 优势{c}**
      
      {adv_lines}
      
      **⚠️ 短板{c}**
      
      {weak_lines}
      
      ---"""
      
          # ==================== 章节4:优化建议 ====================
      
          def _section_optimization(self) -> str:
              suggestions = self._generate_suggestions()
              items = []
              for i, (problem, advice) in enumerate(suggestions[:3], 1):
                  items.append(f"{i}. **{problem}** → {advice}")
              return "### 💡 优化建议\n\n" + "\n".join(items) + "\n\n---"
      
          # ==================== 页脚 ====================
      
          def _section_footer(self) -> str:
              today = datetime.now().strftime("%Y-%m-%d")
              crawl_time = self.data.get("crawlTime", "-")
              return f"\n*诊断时间:{today} | 数据来源:红狐API `/story/api/dyUser/query` | 数据更新:{crawl_time}*"
      
          # ==================== 维度一:账号体量(40分) ====================
      
          def _score_body(self):
              """账号体量 = 粉丝数(22) + 获赞总数(8) + 获赞/粉丝比(5)
              Returns: (score, details_list)"""
              follower = self.data.get("followerCount", 0) or 0
              favorited = self.data.get("totalFavorited", 0) or 0
              ratio = favorited / follower if follower > 0 else 0
              details = []
      
              # 粉丝数(22分)
              follower_levels = [
                  (50000000, 22, "超头部"), (10000000, 19, "头部"),
                  (5000000, 16, "准头部"), (1000000, 12, "中腰部"),
                  (500000, 9, "中小"), (100000, 5, "小型"),
                  (10000, 2, "微型"), (0, 0, "新号"),
              ]
              s1, level = 0, ""
              for threshold, score, label in follower_levels:
                  if follower >= threshold:
                      s1, level = score, label
                      break
              details.append(f"粉丝数 {format_number(follower)} → {s1}/22分({level})")
      
              # 获赞总数(8分)
              favorited_levels = [
                  (1000000000, 8), (100000000, 6), (10000000, 5),
                  (1000000, 3), (100000, 1), (0, 0),
              ]
              s2 = 0
              for threshold, score in favorited_levels:
                  if favorited >= threshold:
                      s2 = score
                      break
              details.append(f"获赞总数 {format_number(favorited)} → {s2}/8分")
      
              # 获赞/粉丝比(5分)
              ratio_levels = [
                  (15, 5, "粉丝质量极高"), (10, 4, "粉丝质量高"),
                  (5, 3, "正常"), (2, 2, "偏低"), (0, 1, "极低"),
              ]
              s3, ratio_desc = 0, ""
              for threshold, score, desc in ratio_levels:
                  if ratio >= threshold:
                      s3, ratio_desc = score, desc
                      break
              details.append(f"获赞/粉丝比 {ratio:.1f} → {s3}/5分({ratio_desc})")
      
              return s1 + s2 + s3, details
      
          # ==================== 维度二:内容表现(40分) ====================
      
          def _score_content(self):
              """内容表现 = 平均点赞(13) + 平均评论(9) + 平均分享(7) + 传播系数(6)
              若近7天无作品,得0分
              Returns: (score, details_list)"""
              if not self.works:
                  return 0, ["近7天无作品,内容表现得0分"]
      
              n = len(self.works)
              total_digg = sum(w.get("diggCount", 0) or 0 for w in self.works)
              total_comment = sum(w.get("commentCount", 0) or 0 for w in self.works)
              total_share = sum(w.get("shareCount", 0) or 0 for w in self.works)
      
              avg_digg = total_digg / n
              avg_comment = total_comment / n
              avg_share = total_share / n
              spread_coeff = (total_share / total_digg * 100) if total_digg > 0 else 0
              details = []
      
              # 平均点赞数(13分)
              digg_levels = [
                  (1000000, 13, "现象级"), (500000, 11, "超爆款"),
                  (100000, 9, "爆款"), (10000, 7, "优秀"),
                  (1000, 4, "中等"), (100, 2, "偏低"), (0, 0, "较低"),
              ]
              s1, digg_label = 0, ""
              for threshold, score, label in digg_levels:
                  if avg_digg >= threshold:
                      s1, digg_label = score, label
                      break
              details.append(f"平均点赞数 {format_number(int(avg_digg))} → {s1}/13分({digg_label})")
      
              # 平均评论数(9分)
              comment_levels = [
                  (10000, 9), (5000, 7), (1000, 5), (100, 3), (10, 1), (0, 0),
              ]
              s2 = 0
              for threshold, score in comment_levels:
                  if avg_comment >= threshold:
                      s2 = score
                      break
              details.append(f"平均评论数 {format_number(int(avg_comment))} → {s2}/9分")
      
              # 平均分享数(7分)
              share_levels = [
                  (100000, 7), (10000, 6), (1000, 4), (100, 2), (10, 1), (0, 0),
              ]
              s3 = 0
              for threshold, score in share_levels:
                  if avg_share >= threshold:
                      s3 = score
                      break
              details.append(f"平均分享数 {format_number(int(avg_share))} → {s3}/7分")
      
              # 传播系数(6分)
              spread_levels = [
                  (15, 6, "极强自发传播"), (10, 5, "强传播力"),
                  (5, 4, "良好"), (2, 2, "中等"), (0, 1, "偏弱"),
              ]
              s4, spread_desc = 0, ""
              for threshold, score, desc in spread_levels:
                  if spread_coeff >= threshold:
                      s4, spread_desc = score, desc
                      break
              details.append(f"传播系数 {spread_coeff:.1f}% → {s4}/6分({spread_desc})")
      
              return s1 + s2 + s3 + s4, details
      
          # ==================== 维度三:运营活跃度(20分) ====================
      
          def _score_operation(self):
              """运营活跃度 = 更新频率(8) + 发布时段(4) + 账号完整度(4) + 作品总量(4)
              Returns: (score, details_list)"""
              details = []
      
              # 更新频率(8分)
              work_count = len(self.works)
              freq_levels = [
                  (7, 8, "日更"), (4, 6, "高频"),
                  (2, 4, "中频"), (1, 2, "低频"), (0, 0, "停更"),
              ]
              s1, freq_desc = 0, ""
              for threshold, score, desc in freq_levels:
                  if work_count >= threshold:
                      s1, freq_desc = score, desc
                      break
              details.append(f"更新频率 {work_count}条/7天 → {s1}/8分({freq_desc})")
      
              # 发布时段(4分)
              s2, period_desc = self._score_publish_period()
              details.append(f"发布时段 {period_desc} → {s2}/4分")
      
              # 账号完整度(4分)
              sig = self.data.get("signature", "") or ""
              if sig.strip() and len(sig.strip()) > 5:
                  s3, comp_desc = 4, "简介完整(>5字)"
              elif sig.strip():
                  s3, comp_desc = 2, "简介较短(<=5字)"
              else:
                  s3, comp_desc = 0, "无简介"
              details.append(f"账号完整度 {comp_desc} → {s3}/4分")
      
              # 作品总量(4分)
              aweme_count = self.data.get("awemeCount", 0) or 0
              aweme_levels = [
                  (300, 4), (100, 3), (30, 2), (10, 1), (0, 0),
              ]
              s4 = 0
              for threshold, score in aweme_levels:
                  if aweme_count >= threshold:
                      s4 = score
                      break
              details.append(f"作品总量 {aweme_count} → {s4}/4分")
      
              return s1 + s2 + s3 + s4, details
      
          def _score_publish_period(self):
              """发布时段评分:黄金时段11-13/18-21得4分,次优7-9/21-23得3分
              Returns: (score, period_desc)"""
              if not self.works:
                  return 0, "无作品可判断"
              hours = []
              for w in self.works:
                  ct = w.get("createTime", "")
                  if ct:
                      try:
                          hour = int(ct.split(" ")[1].split(":")[0])
                          hours.append(hour)
                      except (IndexError, ValueError):
                          pass
              if not hours:
                  return 0, "无作品可判断"
              # 取多数时段
              golden = sum(1 for h in hours if (11 <= h <= 13) or (18 <= h <= 21))
              suboptimal = sum(1 for h in hours if (7 <= h <= 9) or (21 <= h <= 23))
              if golden >= len(hours) / 2:
                  return 4, "黄金时段(11-13/18-21)"
              elif golden + suboptimal >= len(hours) / 2:
                  return 3, "次优时段(7-9/21-23)"
              else:
                  return 1, "其他时段"
      
          # ==================== 维度四:平台指数(10分) ====================
      
          def _score_platform(self):
              """平台指数 = 红狐指数(10)
              若API未返回redfoxIndex(值为null),得0分
              Returns: (score, details_list)"""
              redfox_index = self.data.get("redfoxIndex")
              if redfox_index is None:
                  return 0, ["红狐指数 API未返回 → 0/10分"]
      
              # 红狐指数(10分)
              redfox_levels = [
                  (900, 10, "顶级影响力"), (800, 8, "头部影响力"),
                  (700, 6, "中上影响力"), (600, 4, "中等影响力"),
                  (500, 2, "初级影响力"), (0, 0, "待提升"),
              ]
              s1, level = 0, ""
              for threshold, score, label in redfox_levels:
                  if redfox_index >= threshold:
                      s1, level = score, label
                      break
              return s1, [f"红狐指数 {redfox_index:.1f} → {s1}/10分({level})"]
      
          # ==================== 辅助方法 ====================
      
          def _get_grade(self, score: float) -> tuple:
              """评级:S/A/B/C/D/E"""
              if score >= 90:
                  return ("🏆", "标杆账号", "S级")
              elif score >= 80:
                  return ("⭐", "优质账号", "A级")
              elif score >= 70:
                  return ("✅", "健康账号", "B级")
              elif score >= 60:
                  return ("📊", "中等账号", "C级")
              elif score >= 50:
                  return ("⚠️", "亚健康", "D级")
              else:
                  return ("❌", "问题账号", "E级")
      
          def _get_advantages(self, body: float, content: float, operation: float, platform: float) -> list:
              """获取1-2个核心优势"""
              items = []
              if body >= 28:
                  items.append("账号体量雄厚")
              elif body >= 18:
                  items.append("粉丝基础扎实")
              if content >= 28:
                  items.append("内容数据突出")
              elif content >= 18:
                  items.append("内容表现良好")
              if operation >= 16:
                  items.append("运营节奏健康")
              if platform >= 8:
                  items.append("平台指数优秀")
              elif platform >= 4:
                  items.append("平台影响力良好")
              if not items:
                  items.append("有基础运营框架")
              return items[:2]
      
          def _get_weaknesses(self, body: float, content: float, operation: float, platform: float) -> list:
              """获取1-2个核心短板"""
              items = []
              if body < 12:
                  items.append("账号体量偏小")
              if content < 10:
                  items.append("近期内容表现较弱")
              elif content == 0:
                  items.append("近7天无作品数据")
              if operation < 8:
                  items.append("运营活跃度不足")
              if platform == 0:
                  items.append("无平台指数数据")
              if not items:
                  items.append("整体表现均衡,持续优化即可")
              return items[:2]
      
          def _generate_suggestions(self) -> List[tuple]:
              """基于评分数据生成3条优化建议"""
              suggestions = []
              body, _ = self._score_body()
              content, _ = self._score_content()
              operation, _ = self._score_operation()
              platform, _ = self._score_platform()
      
              # 按短板优先排序
              if content == 0:
                  suggestions.append(("近7天无作品更新", "恢复稳定发布节奏,建议至少周更2-3条"))
              elif content < 12:
                  suggestions.append(("内容互动数据偏低", "优化视频前3秒吸引力,增加互动引导话术"))
      
              if body < 12:
                  suggestions.append(("粉丝基数偏小", "通过热点内容+Dou+投放加速涨粉,聚焦垂直领域深耕"))
              elif body < 22:
                  suggestions.append(("账号体量有提升空间", "持续产出爆款内容扩大影响力,探索跨平台引流"))
      
              if operation < 10:
                  suggestions.append(("运营活跃度不足", "制定内容排期表保持日更或高频更新,完善账号简介"))
              elif operation < 16:
                  suggestions.append(("运营节奏可优化", "聚焦黄金时段(11-13点/18-21点)发布,提升内容曝光概率"))
      
              if platform == 0:
                  suggestions.append(("平台指数数据缺失", "持续稳定运营以积累平台指数,提升内容质量与互动率"))
              elif platform < 4:
                  suggestions.append(("平台指数偏低", "提升内容质量与互动率,保持稳定更新以提升红狐指数"))
      
              # 至少3条
              if len(suggestions) < 3:
                  suggestions.append(("持续优化方向", "深耕垂直内容建立差异化标签,开启直播增强粉丝粘性"))
      
              return suggestions[:3]
      
      
      # ==================== 使用示例 ====================
      
      if __name__ == "__main__":
          # 模拟范丞丞的API返回数据
          sample_data = {
              "nickname": "范丞丞",
              "avatarUrl": "https://example.com/avatar.jpg",
              "accountId": "CCHardErThAnEVER",
              "uid": "84676974127",
              "signature": "",
              "gender": "未知",
              "age": 25,
              "country": "中国",
              "province": "浙江",
              "city": "金华",
              "ipLocation": "浙江",
              "followerCount": 20366567,
              "awemeCount": 236,
              "totalFavorited": 390844290,
              "crawlTime": "2026-06-02 01:57:18",
              "redfoxIndex": 780.5,
              "works": [
                  {
                      "awemeId": "7481xxx1",
                      "title": "新作品来了",
                      "coverUrl": "https://example.com/cover1.jpg",
                      "desc": "",
                      "createTime": "2026-05-28 19:30:00",
                      "diggCount": 2165213,
                      "commentCount": 38286,
                      "shareCount": 260216,
                      "playCount": None,
                      "interactiveCount": 2443715,
                      "workUrl": "https://www.douyin.com/video/7481xxx1"
                  },
                  {
                      "awemeId": "7481xxx2",
                      "title": "日常分享",
                      "coverUrl": "https://example.com/cover2.jpg",
                      "desc": "",
                      "createTime": "2026-05-26 12:15:00",
                      "diggCount": 1222713,
                      "commentCount": 33662,
                      "shareCount": 87111,
                      "playCount": None,
                      "interactiveCount": 1343486,
                      "workUrl": "https://www.douyin.com/video/7481xxx2"
                  }
              ],
              "similarAccounts": []
          }
      
          generator = DouyinDiagnosisReportV3(sample_data)
          report = generator.generate_report()
          print(report)
      
          with open("douyin_diagnosis_report_v3.md", "w", encoding="utf-8") as f:
              f.write(report)
          print("\n✅ 报告已保存到 douyin_diagnosis_report_v3.md")
      
    • regression_test.py 23.1 KB
      #!/usr/bin/env python3
      """
      评分模型回归测试 —— 不调用 API,用 tests/fixtures 下的真实抓取数据校验评分逻辑。
      
      用法:
          python3 scripts/regression_test.py
      
      覆盖关键场景,防止后续改规则时出现「改一处、崩一处」:
        1. S 级超头部明星号应被正确识别为优质账号(≥90),
           旧版绝对阈值会把它误判为 76 分。
        2. A 级但互动率真实异常偏低(0.07%)的账号不得被无差别抬分,
           否则说明分层基准失效、变成了整体放水。
        3. C 级尾部号(2251 粉、均互动 4852 = 粉丝数 2 倍)不得因「绝对量级小」
           被误判为互动偏弱 —— 这是修 S 级偏差时引入的反向偏差。
        4. 采集时间(crawlTime)滞后于最新作品时,「最新活跃度」必须按当前时间校正,
           不得出现「最近-77天前发布」这类负数天数。
        5. S 级超头部 papi酱:接口只返回 2 条作品时,若干子项在数学上已退化
           (中位≡均值、max/均值≤2),必须改为不计入分母,不得让账号白拿满分;
           同时「性别」字段接口不返回,不得据此扣分。
        6. 快照累计量的年龄偏差:越新的作品累计互动必然越少,直接比较会把
           「最近发布」系统性读成「衰退」。趋势类指标只能比较「已跑够时间」的作品,
           成熟样本不足时不计入分母 —— 不得输出与事实相反的衰退结论或预警。
        7. 作品总量与人均获赞必须随量级自适应:前者是「绝对计数」、后者是「绝对量级」,
           原实现都用一把绝对尺子,导致 ① 精品型明星号按日更博主的节奏被扣分、
           ② 93.6 倍的效率差被满分封顶抹平。同时必须保证排序正确 ——
           精品型 S 级号(许凯)不得低于高产型 A 级号(小边边)。
        8. 「人群标签」必须只是展示项、不影响得分。它原本是「缺失字段不计入分母」的
           计分子项,但字段一旦提供必然有效,于是恒拿满分、毫无区分度,还会稀释其他
           缺失项的扣分 —— 同样资料质量的两个账号,仅因「接口恰好返回了性别」而得分不同。
        9. 「不可计算」与「数据不足」必须分开处理:
           · 比值为 0/0(互动全零)、字段缺失 → 不计入分母(不奖不罚);
           · 账号确实没有作品数据 → 相关子项按最低档计分,不得剔除(剔除会让
             「已清空作品」的账号因分母变小而虚高,掩盖真实风险)。
           `_cv()` 在均值为 0 时返回 0.0,会被误读成「稳定输出」,必须在调用前拦截。
      """
      import json
      import sys
      from pathlib import Path
      
      SKILL_ROOT = Path(__file__).resolve().parent.parent
      sys.path.insert(0, str(SKILL_ROOT / "scripts"))
      import douyin_diagnosis as dd  # noqa: E402
      
      FIXTURES = SKILL_ROOT / "tests" / "fixtures"
      
      DIMS = ["profile", "productivity", "engagement", "quality", "trend", "fans"]
      FN = {
          "profile": dd.diagnose_profile,
          "productivity": dd.diagnose_productivity,
          "engagement": dd.diagnose_engagement,
          "quality": dd.diagnose_quality,
          "trend": dd.diagnose_trend,
          "fans": dd.diagnose_fans,
      }
      
      
      def evaluate(acc):
          """返回 (总分, 各维度明细, 预警列表, 量级)。总分走与报告一致的 composite_score。"""
          diagnoses = {k: FN[k](acc) for k in DIMS}
          rows = []
          for k in DIMS:
              score, _details, eff_max = diagnoses[k]
              norm = score / eff_max * 100 if eff_max else 0.0
              rows.append((dd.DIM_NAMES[k], score, eff_max, dd.DIM_MAX[k], norm))
          total = dd.composite_score(diagnoses)
          return total, rows, dd.check_warnings(acc), dd.account_tier(acc.get("followerCount", 0) or 0)
      
      
      def run_case(slug, expect_tier, min_score, max_score, expect_warning=None):
          path = FIXTURES / f"{slug}.json"
          acc = json.loads(path.read_text(encoding="utf-8"))
          total, rows, warns, (tier_code, tier_label) = evaluate(acc)
      
          print("=" * 70)
          print(f"[CASE] {slug}  {acc.get('nickname')} ({acc.get('accountId')})")
          print(f"       粉丝={dd._fmt_num(acc.get('followerCount', 0))}  "
                f"量级={tier_code} {tier_label}  作品={acc.get('awemeCount')}  "
                f"抓取作品={len(acc.get('works') or [])}")
          for name, s, eff, full, norm in rows:
              flag = "" if eff == full else f"  [有效满分{eff}/卷面{full}]"
              print(f"       {name:<10} {s:>4}/{eff:<3} 归一化{norm:>6.1f}%{flag}")
          print(f"       合计 {total}/100   预警: {warns if warns else '无'}")
      
          failures = []
          if tier_code != expect_tier:
              failures.append(f"量级判定 {tier_code} != 期望 {expect_tier}")
          if not (min_score <= total <= max_score):
              failures.append(f"总分 {total} 不在期望区间 [{min_score}, {max_score}]")
          if expect_warning:
              if not any(expect_warning in w for w in warns):
                  failures.append(f"未触发期望预警「{expect_warning}」,实际 {warns}")
          elif warns:
              failures.append(f"期望无预警,实际 {warns}")
      
          if failures:
              print("       ❌ FAIL: " + "; ".join(failures))
          else:
              print("       ✅ PASS")
          print()
          return not failures
      
      
      def main():
          ok = True
      
          # 场景1:S 级明星号 —— 旧版绝对阈值下仅得 76 分,分层后应进入优质区间
          ok &= run_case("kaisoso", expect_tier="S", min_score=90, max_score=100)
      
          # 场景2:A 级但互动率 0.07% 的真实异常号 —— 必须仍被识别为短板,
          #        总分不得虚高(若涨到 85+ 说明基准整体放水)
          ok &= run_case(
              "juju", expect_tier="A", min_score=65, max_score=78, expect_warning="僵尸粉预警"
          )
      
          # 场景3:C 级尾部号,均互动 4852(粉丝数的 2 倍)—— 不得被判「互动偏弱」
          ok &= run_case("zhongguoyiwei", expect_tier="C", min_score=82, max_score=92)
      
          # 场景4:S 级超头部明星号 papi酱(2828 万粉)—— 小样本退化子项修正后应进入 92+
          #        修正前 86 分:其中 -2 来自性别字段恒不可得、-5 来自 n=2 时数学退化的子项
          ok &= run_case("papijiang", expect_tier="S", min_score=92, max_score=100)
      
          # 场景5:单元校验 —— 小样本退化子项必须不计入分母(不得白拿满分)
          print("=" * 70)
          print("[CASE] 小样本退化子项(n=2)应不计入分母")
          deg_acc = {
              "followerCount": 1_000_000, "totalFavorited": 10_000_000,
              "awemeCount": 500, "crawlTime": "2026-09-23 10:00:00",
              "works": [
                  {"diggCount": 1000, "commentCount": 10, "shareCount": 5,
                   "interactiveCount": 1015, "createTime": "2026-09-21 10:00:00"},
                  {"diggCount": 9000, "commentCount": 90, "shareCount": 50,
                   "interactiveCount": 9140, "createTime": "2026-09-22 10:00:00"},
              ],
          }
          _, q_details, q_eff = dd.diagnose_quality(deg_acc)
          _, t_details, t_eff = dd.diagnose_trend(deg_acc)
          _, _p_details, p_eff = dd.diagnose_productivity(deg_acc)
      
          checks = [
              ("内容质量 有效满分须降到 11(剔除中位偏离4 + 互动稳定性5)", q_eff, 11),
              ("内容趋势 有效满分须降到 5(剔除趋势5 + 集中度5)", t_eff, 5),
              ("内容生产力 有效满分须降到 10(剔除发布频率5)", p_eff, 10),
          ]
          for desc, got, want in checks:
              mark = "✅" if got == want else "❌"
              if got != want:
                  ok = False
              print(f"       {mark} {desc}:实际 {got}")
      
          # 被剔除的子项必须以 full==0 标记,报告才会渲染成「不计入」
          for name, dets in (("内容质量", q_details), ("内容趋势", t_details)):
              for item, _lbl, _g, full in dets:
                  if item in ("中位/均值偏离", "互动稳定性", "爆款集中度") and full != 0:
                      ok = False
                      print(f"       ❌ {name} 的「{item}」未标记为不计入(full={full})")
          print()
      
          # 场景6:单元校验 —— 人群标签必须为「仅展示不计分」项
          # 该子项原为 2 分且「接口未提供的字段不计入分母」,但字段一旦提供必然有效,
          # 于是 gained 恒等于分母、恒拿满分、毫无区分度,还会稀释其他缺失项的扣分:
          #     地域缺失 + 性别年龄可用 → (7+2)/(8+2) = 90.0%
          #     地域缺失 + 性别年龄缺失 →  7/8        = 87.5%
          # 同样的资料质量只因「接口恰好返回了性别」而得分不同,与「不为数据可得性
          # 买单」的原则相悖(该原则意味着既不该罚、也不该奖)。故改为不计分展示项,
          # 账号基础画像卷面由 10 调整为 8。
          print("=" * 70)
          print("[CASE] 人群标签不得影响账号基础画像得分")
          for label, extra in [
              ("性别与年龄均缺失", {"gender": "未知", "age": None}),
              ("仅年龄可用", {"gender": "未知", "age": 31}),
              ("两者均可用", {"gender": "男", "age": 31}),
              ("年龄为占位符「未知」", {"gender": "未知", "age": "未知"}),
          ]:
              a = {"avatarUrl": "x", "signature": "这是一段超过十五个字的账号简介内容",
                   "province": "北京", "ipLocation": "北京", "accountId": "abc"}
              a.update(extra)
              sc, _d, eff = dd.diagnose_profile(a)
              good = (sc == 8 and eff == 8)
              if not good:
                  ok = False
              print(f"       {'✅' if good else '❌'} {label} -> {sc}/{eff}(期望 8/8,卷面恒为 8)")
      
          # 反向校验:地域信息缺失时,不得因性别/年龄可用而少扣分
          a1 = {"avatarUrl": "x", "signature": "这是一段超过十五个字的账号简介内容",
                "province": None, "ipLocation": "北京", "accountId": "abc",
                "gender": "男", "age": 31}
          a2 = dict(a1, gender="未知", age=None)
          s1, _d1, e1 = dd.diagnose_profile(a1)
          s2, _d2, e2 = dd.diagnose_profile(a2)
          good = (s1 / e1) == (s2 / e2)
          if not good:
              ok = False
          print(f"       {'✅' if good else '❌'} 地域缺失时,性别年龄是否可用不影响归一化得分"
                f"({s1}/{e1} = {s2}/{e2})")
          print()
      
          # 场景7:单元校验 —— 总分按「四舍五入」而非银行家舍入
          print("=" * 70)
          print("[CASE] 综合评分舍入方式(文档约定四舍五入)")
          # 构造总分恰为 94.5 的维度组合:各维度归一化后加权求和
          # profile 1.0*0.10 + productivity 0.9*0.15 + engagement 0.8667*0.30
          #  + quality 1.0*0.20 + trend 1.0*0.15 + fans 1.0*0.10 = 94.5
          stub = {
              "profile": (8, [], 8),          # 100%
              "productivity": (9, [], 10),    # 90%
              "engagement": (26, [], 30),     # 86.667%
              "quality": (11, [], 11),        # 100%
              "trend": (5, [], 5),            # 100%
              "fans": (10, [], 10),           # 100%
          }
          got = dd.composite_score(stub)
          mark = "✅" if got == 95 else "❌"
          if got != 95:
              ok = False
          print(f"       {mark} 94.5 分 -> {got}(四舍五入应为 95,Python 内置 round 会给出 94)")
          print()
      
          # 场景8:单元校验 —— 均互动分档随量级自适应
          #        同样的 5000 次互动,尾部号应远高于超头部号得分
          print("=" * 70)
          print("[CASE] 作品均互动分档随量级自适应(同样 4852 次互动)")
          def _eng(followers):
              acc = {
                  "followerCount": followers,
                  "totalFavorited": followers * 8,
                  "works": [{
                      "diggCount": 4000, "commentCount": 300, "shareCount": 500,
                      "interactiveCount": 4852, "createTime": "2026-09-01 10:00:00",
                  }],
              }
              _, details, _ = dd.diagnose_engagement(acc)
              for name, label, s, _f in details:
                  if name == "作品均互动":
                      return s, label
              return None, ""
      
          for followers, want_min in [(2251, 4), (6768100, 1)]:
              s, label = _eng(followers)
              mark = "✅" if s >= want_min else "❌"
              if s < want_min:
                  ok = False
              print(f"       {mark} {followers:>9,} 粉 -> {label} ({s}/5,期望 ≥{want_min})")
          print()
      
          # 场景9:单元校验 —— 采集时间滞后时活跃度不得出现负数天数
          print("=" * 70)
          print("[CASE] 采集时间滞后于作品时的活跃度校正")
          stale_acc = {
              "followerCount": 2251, "totalFavorited": 17400,
              "crawlTime": "2026-07-07 12:41:41",  # 早于下方作品时间,字段滞后
              "works": [{
                  "diggCount": 3880, "commentCount": 349, "shareCount": 1825,
                  "interactiveCount": 7244, "createTime": "2026-09-19 16:26:46",
              }],
          }
          _, trend_details, _ = dd.diagnose_trend(stale_acc)
          act_label = next(lbl for name, lbl, _, _ in trend_details if name == "最新活跃度")
          bad = "-" in act_label.split("天前")[0]
          if bad:
              ok = False
          print(f"       {'❌' if bad else '✅'} {act_label}  (不得出现负数天数)")
          if "校正" not in act_label:
              ok = False
              print("       ❌ 未标注「按当前时间校正」,用户无法判断数据时效")
          print()
      
          # 场景10:单元校验 —— 量级分档边界
          print("=" * 70)
          print("[CASE] 量级分档边界")
          borders = [
              (999, "C"), (10_000, "B"), (99_999, "B"),
              (100_000, "A"), (999_999, "A"), (1_000_000, "S"),
          ]
          for followers, expect in borders:
              got = dd.account_tier(followers)[0]
              mark = "✅" if got == expect else "❌"
              if got != expect:
                  ok = False
              print(f"       {mark} {followers:>9,} 粉 -> {got} (期望 {expect})")
          print()
      
          # 场景11:A 级头部号,4 条作品跨 6 天,最新一条发布仅 7 小时 ——
          #         累计互动必然「越新越小」,但换算日均其实是上升的。
          #         必须①判定「近期趋势不可判定」而非「严重衰退」;②不触发衰退预警。
          ok &= run_case("xiaobianbian", expect_tier="A", min_score=88, max_score=100)
      
          # 场景12:单元校验 —— 快照累计量的年龄偏差不得被读成「衰退」
          print("=" * 70)
          print("[CASE] 快照年龄偏差(累计互动随作品新旧递减)不得判为衰退")
          # 构造:越新的作品累计互动越小(这正是真实的快照形态),
          # 若规则直接比较累计量,会得出「暴跌」结论 —— 属于典型的反向误判。
          snap = {
              "followerCount": 662_100, "totalFavorited": 8_059_100,
              "awemeCount": 1576, "crawlTime": "2026-09-23 02:07:35",
              "works": [
                  {"diggCount": 31507, "commentCount": 589, "shareCount": 4808,
                   "interactiveCount": 38513, "createTime": "2026-09-16 18:20:41"},  # 6.3 天
                  {"diggCount": 26997, "commentCount": 559, "shareCount": 2656,
                   "interactiveCount": 31798, "createTime": "2026-09-19 18:18:00"},  # 3.3 天
                  {"diggCount": 17701, "commentCount": 316, "shareCount": 1177,
                   "interactiveCount": 20150, "createTime": "2026-09-21 18:20:24"},  # 1.3 天
                  {"diggCount": 15067, "commentCount": 367, "shareCount": 1456,
                   "interactiveCount": 18062, "createTime": "2026-09-22 19:21:00"},  # 0.3 天
              ],
          }
          _, snap_details, snap_eff = dd.diagnose_trend(snap)
          trend_label = next(lbl for name, lbl, _, _ in snap_details if name == "近期趋势")
          snap_warns = dd.check_warnings(snap)
          bad_trend = ("暴" in trend_label or "衰退" in trend_label)
          bad_warn = any("衰退预警" in w for w in snap_warns)
          if bad_trend:
              ok = False
          if bad_warn:
              ok = False
          print(f"       {'❌' if bad_trend else '✅'} 近期趋势: {trend_label}")
          print(f"       {'❌' if bad_warn else '✅'} 预警: {snap_warns if snap_warns else '无'}"
                "  (不得出现衰退预警)")
          # 内容趋势有效满分:15 - 5(趋势,成熟样本不足)- 5(集中度,n<5)= 5
          if snap_eff != 5:
              ok = False
              print(f"       ❌ 内容趋势有效满分应为 5(趋势与集中度均剔除),实际 {snap_eff}")
          else:
              print(f"       ✅ 内容趋势有效满分 {snap_eff}(趋势 5 + 集中度 5 均已剔除,不奖不罚)")
          print()
      
          # 场景13:单元校验 —— 作品总量与人均获赞分档随量级自适应
          print("=" * 70)
          print("[CASE] 作品总量 / 人均获赞 分档随量级自适应")
      
          def _vol(followers, aweme_count):
              acc = {"followerCount": followers, "awemeCount": aweme_count,
                     "totalFavorited": 0, "works": []}
              _, details, _ = dd.diagnose_productivity(acc)
              return next((s, lbl) for name, lbl, s, _f in details if name == "作品总量")
      
          def _avg_like(followers, avg_likes):
              acc = {"followerCount": followers, "awemeCount": 100,
                     "totalFavorited": avg_likes * 100, "works": []}
              _, details, _ = dd.diagnose_productivity(acc)
              return next((s, lbl) for name, lbl, s, _f in details if name == "人均获赞")
      
          # 同样 100 条作品:C 级新号算高产(线 100),A 级(职业日更博主区间,
          # 线 600)只算偏少,S 级(明星/精品型,线 150)算正常 ——
          # 正是因为三类账号的生产模式不同,不能共用一把尺子。
          for followers, want in [(2_251, 5), (1_000_000, 4), (662_100, 2)]:
              s, lbl = _vol(followers, 100)
              mark = "✅" if s == want else "❌"
              if s != want:
                  ok = False
              print(f"       {mark} 100 条作品 @ {followers:>9,} 粉 -> {lbl}({s}/5,期望 {want})")
      
          # 同样 1,300 次人均获赞:对 C 级是高效(线 1,000),对 A 级只是正常(线 1,200),
          # 对 S 级则属低效(线 15,000)—— 绝对量级只有放在量级坐标系里才有意义。
          for followers, want in [(2_251, 5), (662_100, 3), (1_000_000, 1)]:
              s, lbl = _avg_like(followers, 1300)
              mark = "✅" if s == want else "❌"
              if s != want:
                  ok = False
              print(f"       {mark} 1,300 人均获赞 @ {followers:>9,} 粉 -> {lbl}({s}/5,期望 {want})")
          print()
      
          # 场景14:跨账号排序 —— 精品型 S 级号不得低于高产型 A 级号
          #         许凯(S):206 条 / 人均获赞 478,631(效率型)
          #         小边边(A):1,576 条 / 人均获赞 5,114(产量型)
          #         改革前模型对效率的 93.6 倍差异完全失明,导致「产量多 7.6 倍」
          #         单方面压过效率优势,排序倒挂。
          print("=" * 70)
          print("[CASE] 效率 vs 产量:许凯(S 精品型) 与 小边边(A 高产型) 的排序")
          ks_total, _r, _w, _t = evaluate(
              json.loads((FIXTURES / "kaisoso.json").read_text(encoding="utf-8"))
          )
          xb_total, _r2, _w2, _t2 = evaluate(
              json.loads((FIXTURES / "xiaobianbian.json").read_text(encoding="utf-8"))
          )
          ks_prod = FN["productivity"](
              json.loads((FIXTURES / "kaisoso.json").read_text(encoding="utf-8"))
          )
          xb_prod = FN["productivity"](
              json.loads((FIXTURES / "xiaobianbian.json").read_text(encoding="utf-8"))
          )
          ks_like = next(s for n, _l, s, _f in ks_prod[1] if n == "人均获赞")
          xb_like = next(s for n, _l, s, _f in xb_prod[1] if n == "人均获赞")
          print(f"       许凯    总分 {ks_total:>3}   生产力 {ks_prod[0]}/{ks_prod[2]}"
                f"   人均获赞子项 {ks_like}/5")
          print(f"       小边边  总分 {xb_total:>3}   生产力 {xb_prod[0]}/{xb_prod[2]}"
                f"   人均获赞子项 {xb_like}/5")
      
          if ks_total < xb_total:
              ok = False
              print("       ❌ 精品型 S 级号得分低于高产型 A 级号:效率优势未兑现,模型仍只奖励产量")
          else:
              print("       ✅ 排序正确:精品型 S 级号 ≥ 高产型 A 级号")
      
          if ks_like == xb_like:
              ok = False
              print(f"       ❌ 两人均获赞子项得分相同(均 {ks_like}/5):"
                    "478,631 vs 5,114 的 93.6 倍效率差仍被封顶抹平")
          else:
              print(f"       ✅ 效率差已兑现:许凯 {ks_like}/5 > 小边边 {xb_like}/5")
          print()
      
          # 场景15:单元校验 —— 「不可计算」的子项必须不计入分母(不奖不罚),
          #         但「账号确实没有内容」不得被当成「数据不足」而免罚。
          print("=" * 70)
          print("[CASE] 不可计算子项的处理(全零互动 / 无作品数据)")
      
          # 15a 有作品但互动全为 0:三个比值类子项均为 0/0,必须剔除;
          #     而爆款率、零互动占比是可计算的事实,须照常按最低档计分,
          #     否则一个「互动全零」的账号会因为三处剔除而把分母做小、总分被抬高。
          zero_acc = {
              "followerCount": 1_000_000, "totalFavorited": 50_000_000, "awemeCount": 300,
              "crawlTime": "2026-09-23 10:00:00",
              "works": [{"diggCount": 0, "commentCount": 0, "shareCount": 0,
                         "interactiveCount": 0, "createTime": f"2026-09-{d:02d} 10:00:00"}
                        for d in range(5, 11)],
          }
          _, zq_det, _zq_eff = dd.diagnose_quality(zero_acc)
          zq = {n: f for n, _l, _g, f in zq_det}
          for desc, item, want in [
              ("互动稳定性应剔除(CV=0/0 不可计算,_cv() 会返回 0.0 被误读为「稳定输出」)",
               "互动稳定性", 0),
              ("中位/均值偏离应剔除(偏离度=0/0 不可计算)", "中位/均值偏离", 0),
              ("零互动占比应照常计分(100% 是可计算的事实,不得剔除)", "零互动占比", 5),
          ]:
              good = zq.get(item) == want
              if not good:
                  ok = False
              print(f"       {'✅' if good else '❌'} {desc}:full={zq.get(item)}(期望 {want})")
      
          _, zt_det, _zt_eff = dd.diagnose_trend(zero_acc)
          zt = {n: f for n, _l, _g, f in zt_det}
          good = zt.get("爆款集中度") == 0
          if not good:
              ok = False
          print(f"       {'✅' if good else '❌'} 爆款集中度应剔除"
                f"(max/avg=0/0 不可计算):full={zt.get('爆款集中度')}(期望 0)")
      
          # 15b 无作品数据:依赖作品的子项按最低档计分(0 分),**不剔除**。
          #     若剔除,一个「已清空作品」的账号会因分母变小而白白抬高总分,
          #     掩盖掉「账号已无内容可评」这一真实风险。
          empty_acc = dict(zero_acc, works=[])
          _, eq_det, eq_eff = dd.diagnose_quality(empty_acc)
          _, _et_det, et_eff = dd.diagnose_trend(empty_acc)
          for label, got, want in [("内容质量", eq_eff, 20), ("内容趋势", et_eff, 15)]:
              good = got == want
              if not good:
                  ok = False
              print(f"       {'✅' if good else '❌'} 无作品时{label}仍为满卷 {want}(子项不剔除):实际 {got}")
          names_ok = all(any(n == want for n, *_ in eq_det) for want in
                         ("爆款率", "中位/均值偏离", "互动稳定性", "零互动占比"))
          if not names_ok:
              ok = False
          print(f"       {'✅' if names_ok else '❌'} 无作品分支的子项名须与正式路径一致"
                "(原写作「中位互动」,与「中位/均值偏离」实为同一指标却两个名字)")
          print()
      
          print("=" * 70)
          print("回归测试结果: " + ("✅ 全部通过" if ok else "❌ 存在失败用例"))
          return 0 if ok else 1
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • README.en.md 8.1 KB
    # Douyin Account Diagnosis / douyin-account-diagnosis
    
    ---
    
    ## Overview
    
    One-click diagnosis of your Douyin account's operational health. Simply provide an account nickname or Douyin ID to automatically fetch account profile and recent content data. Receive a quantified diagnosis across six dimensions, with a 100-point composite score, dimension breakdown, risk alerts, and optimization directions — all in a well-structured report ready within minutes.
    
    **Core Value**
    
    - **Full-dimensional quantification**: Covers six dimensions — account profile, content productivity, engagement health, content quality, content trends, and follower quality — leaving no blind spots.
    - **Automatic risk alerts**: Six risk types (zombie followers, engagement manipulation, decline, shadow-ban, inactivity, single-hit dependency) are automatically identified and ranked by severity.
    - **Ready-to-use reports**: Standard formatted output with explainable scores and verifiable conclusions, ready for team presentations, competitive analysis, or partnership evaluations.
    
    **Intended Users**
    
    - 📊 **Douyin operators** — Self-audit account health, identify weak spots, and get actionable optimization directions.
    - 🏢 **MCNs / Brands** — Evaluate creator account quality at scale, screen for collaboration risks, and support signing decisions.
    - 🔍 **Content creators** — Benchmark against competitors, identify gaps, and shape content strategy.
    
    ---
    
    ## Features
    
    ### Core Capabilities
    
    - **One-click data retrieval**: Enter a nickname or Douyin ID to automatically pull account profile and recent content — no manual data collection needed.
    - **Six-dimension quantified diagnosis**: Scores are calculated for account profile, productivity, engagement, quality, trends, and follower quality, then weighted into a composite score.
    - **100-point composite scoring**: A weighted composite score with a four-tier rating (Excellent / Normal / Needs Improvement / At Risk) — understand account health at a glance.
    - **Risk alerts**: Automatically detects zombie followers, engagement manipulation, decline, shadow-ban, inactivity, and single-hit dependency, with detailed data interpretation.
    - **Recent content details**: All recent content with engagement data is displayed by default, with clickable titles linking directly to each piece — every conclusion is backed by evidence.
    
    ### Highlights
    
    - **Tier-adaptive benchmarks**: thresholds are tiered by account scale (S / A / B / C). A 1.55% engagement rate counts as *good* for a 6.7M-follower account (≈100K engagements per post) but as *low* for a 50K-follower one — a single absolute threshold systematically misjudges large accounts. The reverse also holds: absolute-volume sub-items (hit rate, average engagements, total works, average likes) are tiered too, so a 4,852-engagement post is not penalised for a small account nor flattered for a huge one.
    - **Output volume vs. efficiency are both comparable**: total works and average likes per work are tiered by scale as well (niche-production accounts post rarely by nature; average likes rise with scale). After relativisation, a **93.6× efficiency gap is no longer flattened by a capped score** — an S-tier boutique account and an A-tier high-volume account can finally be told apart.
    - **Transparent, auditable scoring**: every score follows clear rules; the report states the benchmarks applied to that account, so the diagnosis is traceable rather than a black box.
    - **Automatic alert triggering**: anomalies are detected automatically without manual monitoring, categorised by risk level; alert thresholds share the same tier lines as the scoring, so "good score but zombie-follower alert" contradictions cannot occur.
    - **Insufficient data neither penalises nor rewards**: sub-items that are **mathematically degenerate** on small samples (with 2 posts the median always equals the mean; max/mean can never exceed 2×) or **mathematically undefined** (0/0 ratios when engagement is all zero, missing fields) are excluded from the denominator and renormalised, marked `(not counted)` in the report.
    - **"No content" is not "no data"**: if the API returns no posts at all, the account genuinely has nothing to evaluate — a negative signal. Those sub-items are scored at their floor and **not** excluded, with a `ⓘ no recent content data` note, so an account that cleared its posts cannot inflate its score by shrinking the denominator.
    - **Non-discriminating sub-items are no longer scored**: `gender`/`age` used to be scored, but a returned field is always valid, so it always scored full marks and diluted the penalties for other missing fields. It is now display-only; the account-profile dimension is scored out of 8 instead of 10.
    - **Low-confidence notice**: when fewer than 5 posts are returned, an extra "ⓘ data confidence" line flags which conclusions rest on too small a sample.
    - **Dirty data auto-correction**: when the crawl timestamp lags behind the newest post, activity and inactivity checks fall back to the current time and label the correction — no more "posted -77 days ago".
    - **Trends are not fooled by snapshot age**: the API returns a snapshot of cumulative engagement, so newer posts necessarily show less. Trend metrics (including the decline alert) therefore compare only posts **at least 3 days old**; with too few mature posts the sub-item is excluded rather than reporting a false decline.
    - **Ready-to-use reports**: standard Markdown output that can be copied directly into team docs, weekly reports, or partnership evaluation materials.
    
    ---
    
    ## API Key Acquisition & Security
    
    - This skill requires the environment variable: `REDFOX_API_KEY`.
    - `REDFOX_API_KEY` is issued by [RedFoxHub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk?source=github`).
    - Register at [RedFoxHub](https://redfox.hk?source=github) to obtain `REDFOX_API_KEY`.
    - Configure `REDFOX_API_KEY` on your device before using this skill.
    - Before providing your key, confirm its source, scope, validity period, and whether it can be reset or revoked.
    - Do not hard-code or expose keys in plain text in code, prompts, logs, or output files.
    
    ---
    
    ## Usage Guide
    
    Simply describe the account you want to diagnose in natural language — no commands to memorize.
    
    ### Quick Reference
    
    | Intent | Example Phrase | Result |
    | ------ | -------------- | ------ |
    | Diagnose by nickname | "Diagnose the Douyin account XX" | Fuzzy match by nickname and output full diagnostic report |
    | Diagnose by Douyin ID | "Analyze this Douyin ID 66456544107" | Exact match by ID and output full report |
    | Account health check | "Give XX a Douyin health check" | Six-dimension diagnostic report |
    | Evaluate for partnership | "Evaluate if this account is worth collaborating with" | Score, rating, and risk alerts |
    
    ### Output Example
    
    The diagnostic report includes the following sections:
    
    - **Summary line**: Nickname + composite score + rating — understand the conclusion at a glance
    - **Basic info table**: Nickname, Douyin ID, UID, follower count, content count, region, etc.
    - **Six-dimension score table**: Score, max points, and one-line evaluation per dimension; weakest dimensions are bolded
    - **Risk alerts**: Each alert type with specific data interpretation
    - **Key findings**: Strengths and weaknesses in two groups, each with supporting data
    - **Recent content details**: Publish time, likes/comments/shares/total engagement per piece; titles link directly to content
    - **Optimization suggestions**: Prioritized improvement directions based on diagnostic data
    
    ---
    
    ## Use Cases
    
    | Scenario | Role | Example Question | Benefit |
    | -------- | ---- | ---------------- | ------- |
    | Account self-audit | Douyin operator | "Diagnose my Douyin account for me" | Quickly identify weak spots and get actionable suggestions |
    | Competitive analysis | Brand / MCN | "Analyze this competitor account's data" | Benchmark performance and develop differentiation strategies |
    | Creator screening | MCN / Brand | "Evaluate if this creator is worth signing" | Data-driven quality assessment to reduce partnership risk |
    | Account valuation | Investor / BD | "Check this account's follower quality and growth potential" | Quantify commercial value to support decisions |
    
    ---
  • README.md 7.3 KB
    # 抖音账号诊断 / douyin-account-diagnosis
    
    ---
    
    ## 简介
    
    一键诊断抖音账号运营健康度。只需提供账号昵称或抖音号,即可自动获取账号画像与近期作品数据,从六个维度完成量化诊断,输出100分制综合评分、得分明细、风险预警和优化方向——几分钟拿到一份结构清晰的诊断报告。
    
    **核心价值**
    
    - **全维度量化**:覆盖账号基础画像、内容生产力、互动健康度、内容质量、内容趋势、粉丝质量六大维度,不留盲区。
    - **风险自动预警**:僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险自动识别并按等级展示。
    - **报告即拿即用**:标准格式输出,评分可解释、结论可复核,直接用于团队汇报、竞品分析或合作评估。
    
    **适用对象**
    
    - 📊 **抖音运营者** — 自查账号健康度,定位短板,获得可落地的优化方向。
    - 🏢 **MCN / 品牌方** — 批量评估达人账号质量,筛查合作风险,辅助签约决策。
    - 🔍 **内容创作者** — 对标竞品数据,找到差距,制定内容策略。
    
    ---
    
    ## 功能特性
    
    ### 核心功能
    
    - **一键数据获取**:输入昵称或抖音号,自动拉取账号画像与近期作品,无需手动搜集数据。
    - **六维度量化诊断**:从账号画像、生产力、互动、质量、趋势、粉丝质量六个维度逐项计分,综合加权得出总分。
    - **100分制综合评分**:加权计算综合得分,配合四级评级(优质 / 正常 / 待优化 / 风险),一个数字看懂账号水平。
    - **风险预警**:自动识别僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险,并给出具体数据解读。
    - **近期作品明细**:默认输出全部近期作品的互动数据,标题可点击跳转直达作品,诊断结论有据可查。
    
    ### 特色亮点
    
    - **量级自适应基准**:评分基准按账号量级(S超头部 / A头部 / B腰部 / C尾部)分档。抖音互动率随粉丝体量自然衰减,同样 1.55% 的互动率,对 670 万粉账号属于良好(单条均互动 10 万+),对 5 万粉账号则偏低——用一套绝对阈值会系统性误判大号。反向亦然:4852 次互动对 2251 粉的尾部号是粉丝数的 2 倍,对超头部号则只是零头。所有绝对量级类子项(爆款率、作品均互动、作品总量、人均获赞)同样分档。
    - **效率与产量双向可比**:作品总量按量级分档(精品型明星号单条成本高、产量天然低,不该按日更博主的节奏被扣分),人均获赞也按量级分档(该指标与量级正相关,基准递增)。两者相对化后,**93.6 倍的效率差不再被满分封顶抹平**——精品型 S 级号与高产型 A 级号可被公平区分,模型不会只奖励产量。
    - **评分透明可复核**:每项得分对应明确规则,报告末尾标明本账号适用的基准线,诊断过程清晰可追溯,不是黑箱打分。
    - **预警自动触发**:异常信号无需人工盯数据,系统自动识别并按风险等级分类展示;预警阈值与评分基准同源,不会出现"评分良好却报僵尸粉"的矛盾。
    - **数据不足不扣分,也不白拿分**:子项在样本过少时若**数学上已退化**(如 2 条样本时中位数恒等于均值、max/均值恒 ≤2 倍),或在数学上**不可计算**(互动全为 0 导致比值为 0/0、字段缺失),一律不计入分母并按"有效满分"归一化,报告中标注 `(不计入)`——既不让账号为缺失数据买单,也不让它白拿满分。
    - **"没有内容"不等于"没有数据"**:若接口确认未返回任何作品,说明账号确实没有可评的内容(属负面信号),相关子项按最低档计分并**不剔除**,同时输出"ⓘ 无近期作品数据"提示核对——避免"已清空作品"的账号因分母变小而虚高。
    - **无区分度的子项不计分**:`性别/年龄` 原为计分子项,但字段一旦返回必然有效,导致恒定满分且会稀释其他缺失项的扣分;现改为仅展示不计分,账号基础画像卷面由 10 调整为 8。
    - **低置信提示**:样本 < 5 条时额外输出"ⓘ 数据置信度"提示,明确告知哪些结论基于过小样本、需要复核。
    - **脏数据自动校正**:采集时间字段滞后于最新作品时(两步抓取时间不一致),活跃度与断更判定自动回退当前时间并标注,不会输出"最近-77天前发布"这类异常结果。
    - **趋势不被"快照年龄差"误导**:接口返回的是截至采集时刻的累计互动量,作品越新累计必然越少。因此趋势类指标(含衰退预警)只比较**已发布满 3 天**的作品,成熟样本不足时不计入分母——不会把"刚发布 7 小时"误读成"严重衰退"。
    - **报告即拿即用**:标准 Markdown 结构输出,可直接复制到团队文档、周报或合作评估材料中。
    
    ---
    
    ## 密钥获取与安全说明
    
    - 本技能需要使用环境变量:`REDFOX_API_KEY`。
    - `REDFOX_API_KEY` 由 [红狐 hub](https://redfox.hk/settings/api-keys?source=github)(`https://redfox.hk?source=github`)提供。
    - 请前往 [红狐 hub](https://redfox.hk?source=github) 注册账号,获取 `REDFOX_API_KEY`。
    - 配置设备环境变量 `REDFOX_API_KEY` 后使用本技能。
    - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
    - 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
    
    ---
    
    ## 使用指南
    
    直接用自然语言说出要诊断的账号即可,无需记忆固定命令。
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    | -------- | ---------------------------- | -------- |
    | 按昵称诊断 | "诊断抖音账号 XX" | 模糊匹配昵称并输出完整诊断报告 |
    | 按抖音号诊断 | "分析这个抖音号 66456544107" | 精确匹配抖音号并输出完整报告 |
    | 账号体检 | "给 XX 做个抖音体检" | 输出六维度诊断报告 |
    | 评估合作价值 | "评估一下这个账号值不值得合作" | 输出评分、评级与风险预警 |
    
    ### 输出示例
    
    诊断完成后输出结构化报告,包含:
    
    - **开头总结句**:昵称 + 综合评分 + 评级,一眼看懂结论
    - **基本信息表**:昵称、抖音号、UID、粉丝数、作品数、地区等
    - **六维度评分表**:各维度得分、满分、一句话评价,核心短板加粗标注
    - **风险预警**:逐项预警类型 + 具体数据解读
    - **关键发现**:优势与短板两组要点,每条附数据佐证
    - **近期作品明细**:每条作品的发布时间、点赞/评论/分享/总互动,标题可点击直达作品
    - **优化建议**:结合数据给出按优先级排列的改进方向
    
    ---
    
    ## 使用场景
    
    | 场景 | 角色 | 示例问法 | 收益 |
    | -------- | -------- | -------- | -------- |
    | 账号自检 | 抖音运营者 | "帮我诊断一下我的抖音账号" | 快速定位短板,拿到可落地的优化建议 |
    | 竞品分析 | 品牌 / MCN | "分析一下这个同赛道账号的数据" | 对标竞品表现,制定差异化策略 |
    | 达人筛选 | MCN / 品牌方 | "评估一下这个达人值不值得签约合作" | 基于数据判断账号质量,降低合作风险 |
    | 账号估值 | 投资人 / 商务 | "看看这个账号的粉丝质量和增长潜力" | 量化评估账号商业价值,辅助决策 |
    
    ---
  • SKILL.md 17.6 KB
    ---
    name: douyin-account-diagnosis
    description: "一键诊断抖音账号运营健康度。基于红狐数据服务获取账号画像与近期作品数据,从账号基础画像、内容生产力、互动健康度、内容质量、内容趋势、粉丝质量六个维度进行量化诊断,输出100分制综合评分、维度得分明细、六类风险预警和针对性优化建议。触发词:当用户提到'诊断抖音账号'、'抖音账号分析'、'抖音体检'、'抖音账号评估'、'查看XX抖音数据'、'分析这个抖音号'时使用"
    ---
    
    # 抖音账号诊断
    
    ## 📝 简介
    
    抖音账号诊断帮助你快速摸清一个抖音账号的运营底数。只需提供账号昵称或抖音号,即可一键获取账号画像与近期作品数据,从六个维度完成量化诊断,输出100分制综合评分、得分明细、风险预警和优化方向。
    
    无论是自查账号健康度、评估竞品实力,还是筛查合作账号的真实质量,都能在几分钟内拿到一份结构清晰的诊断报告。
    
    ## ✨ 功能特性
    
    ### 核心功能
    
    | 功能模块 | 能力描述 | 核心价值 |
    |---------|---------|----------|
    | 一键数据获取 | 输入昵称或抖音号,自动获取账号画像与近期作品数据 | 无需手动搜集数据,即查即诊 |
    | 六维度量化诊断 | 覆盖账号画像、生产力、互动、质量、趋势、粉丝质量 | 全面体检,不留盲区 |
    | 100分制综合评分 | 加权计算综合得分并给出四级评级 | 一个数字看懂账号水平 |
    | 风险预警 | 自动识别僵尸粉、刷量、衰退、限流、断更、单条依赖六类风险 | 提前发现问题,避免踩坑 |
    | 近期作品明细 | 默认输出全部近期作品的互动数据,标题可点击跳转直达作品 | 诊断结论有据可查 |
    
    ### 特色亮点
    
    - **量级自适应基准**:评分基准按账号量级(S超头部/A头部/B腰部/C尾部)分档,
      同样 1.5% 的互动率在 5 万粉号上算偏低、在 670 万粉号上算良好,
      避免用一套绝对阈值误判大号
    - **评分体系透明**:每项得分都有明确规则与说明,报告附本账号适用的基准线,诊断过程可解释、可复核
    - **预警自动触发**:异常信号自动识别并按风险等级展示,阈值与评分基准同源,不会自相矛盾
    - **报告即拿即用**:标准格式输出,可直接用于团队汇报、竞品分析或合作评估
    
    ## 🔧 一键安装
    
    > 本技能需配置 `REDFOX_API_KEY` 后使用(前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 注册获取)。
    
    本技能为标准 Skill 文件包,兼容所有支持 Skill 机制的 AI Agent 平台。选择你使用的平台,按对应方式安装即可:
    
    ### Qoder
    
    1. 确保已安装 Qoder 客户端
    2. 将 `douyin-account-diagnosis` 目录放入你的 Skills 文件夹
    3. 重启 Qoder 即可使用
    
    ### OpenClaw / WorkBuddy
    
    1. 将 `douyin-account-diagnosis` 目录放入 OpenClaw 或 WorkBuddy 的 Skills 目录(通常为 `~/.openclaw/skills/` 或 `~/.workbuddy/skills/`)
    2. 重启对应客户端即可生效
    
    ### Codex / Claude Code
    
    1. 将 `douyin-account-diagnosis` 目录放入对应的 Skills 目录中
    2. Codex 默认路径:项目根目录下的 `.codex/skills/`
    3. Claude Code 默认路径:项目根目录下的 `.claude/skills/`
    4. 重启 Agent 会话即可加载
    
    ### 其他支持 Skill 的平台
    
    只要平台支持标准的 Skill 文件格式(SKILL.md + 配套资源),将 `douyin-account-diagnosis` 目录放入其 Skills 目录下即可使用。
    
    ## 📖 使用指南
    
    ### 🔑 鉴权
    
    > 使用前请配置 API Key,可前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 获取(格式 `ak_xxxxxxxx`),并通过以下任一方式配置:
    >
    > 1. 设置环境变量 `REDFOX_API_KEY`
    > 2. 使用命令行参数 `--api-key`
    
    ### 基础使用
    
    #### 1. 发起诊断
    
    直接说出要诊断的账号即可:
    
    > 用户:帮我诊断一下抖音账号「桔桔的茶园小动物」
    >
    > 助手:收到!正在获取该账号的数据并进行六维度诊断,稍等~
    
    #### 2. 查看诊断报告
    
    诊断完成后输出 Markdown 结构化报告,包含:
    
    - 开头总结句(昵称 + 综合评分 + 评级,一眼看懂结论)
    - 基本信息表格(昵称、抖音号、UID、粉丝数、作品数等)
    - 六维度评分表格(维度、得分、满分、一句话评价,标注核心短板)
    - 风险预警(僵尸粉、刷量、衰退、限流、断更、单条依赖,含具体数据解读)
    - 关键发现(优势与短板两组要点,每条附数据佐证)
    - 近期作品表格(发布时间、标题、赞/评/转/总互动,标题为可点击跳转链接)
    - 优化建议(按优先级排列,结合诊断数据给出具体方向)
    
    #### 3. 补充分析(可选)
    
    如需进一步了解某个维度,可继续追问:
    
    > 用户:互动健康度偏低,怎么改善?
    >
    > 助手:结合诊断报告中的短板维度与预警信息,给出针对性优化建议。
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    |------|---------|------|
    | 按昵称诊断 | "诊断抖音账号 XX" | 模糊匹配昵称并输出完整报告 |
    | 按抖音号诊断 | "分析这个抖音号 66456544107" | 精确匹配抖音号并输出完整报告 |
    | 账号体检 | "给 XX 做个抖音体检" | 输出六维度诊断报告 |
    | 查看数据 | "查看 XX 的抖音数据" | 输出账号画像与近期作品数据 |
    | 评估账号 | "评估一下这个抖音账号值不值得合作" | 输出评分、评级与风险预警 |
    
    ## 🔄 工作流程
    
    1. **提供账号**:输入抖音昵称或抖音号
    2. **数据获取**:一键获取账号画像与近期作品数据
    3. **六维度诊断**:按评分体系逐项计算得分
    4. **输出报告**:综合评分 + 评级 + 风险预警 + 近期作品详情
    5. **补充分析**:针对短板维度提供优化建议(可选)
    
    > 完整执行流程详见 `references/core_workflow.md`,详细评分规则详见 `references/diagnosis_rules.md`
    
    ## 📐 评分体系
    
    ### 六维度权重
    
    | 维度 | 满分 | 权重 | 核心指标 |
    |------|------|------|----------|
    | 账号基础画像 | 8 | 10% | 头像、简介、地域信息完整度、抖音号(性别/年龄仅展示、不计分) |
    | 内容生产力 | 15 | 15% | 作品总量、人均获赞、发布频率 |
    | 互动健康度 | 30 | 30% | 粉丝获赞比、粉丝互动率、互动结构比、作品均互动 |
    | 内容质量 | 20 | 20% | 爆款率、中位/均值偏离、互动稳定性(CV)、零互动占比 |
    | 内容趋势 | 15 | 15% | 近期趋势、爆款集中度、最新活跃度 |
    | 粉丝质量 | 10 | 10% | 粉丝规模、粉丝互动比、获赞/粉丝背离 |
    
    ### 量级自适应基准(先定层,再计分)
    
    | 量级 | 粉丝数 | 互动率 优秀/良好/偏低线 | 爆款线(单条点赞/粉丝) | 均互动 强传播线 | 作品总量 高产线 | 人均获赞 高效线 |
    |------|--------|------------------------|----------------------|-----------------|----------------|----------------|
    | S 超头部 | ≥100万 | 2.0% / 1.2% / 0.6% | 1.0% | 10万 | 150 条 | 200,000 |
    | A 头部 | 10万–100万 | 4.0% / 2.5% / 1.2% | 2.0% | 2万 | 600 条 | 15,000 |
    | B 腰部 | 1万–10万 | 6.0% / 4.0% / 2.0% | 5.0% | 5000 | 300 条 | 3,000 |
    | C 尾部 | <1万 | 8.0% / 5.0% / 2.5% | 10.0% | 1000 | 100 条 | 1,000 |
    
    > **为什么必须分层**:抖音互动指标与粉丝体量强负相关。同样「单条 10 万次互动」,
    > 对 5 万粉账号是 200% 的爆款,对 670 万粉账号只是 1.5% 的日常。
    > 若用一套绝对阈值评判所有量级,会系统性低估超头部账号。
    >
    > 反向同理:**绝对量级类指标也必须随量级缩放**。同样是 4852 次互动,
    > 对 2251 粉的尾部号是粉丝数的 2.16 倍(超预期),对 676 万粉的超头部号
    > 则只是零头。所有以「绝对数值」判高低的子项(爆款率、作品均互动、
    > 作品总量、人均获赞)均按本表分档,避免出现「爆款率 100% 却判互动偏弱」
    > 或「人均获赞 478,631 与 5,114 同分」这类自相矛盾。
    >
    > 作品总量与人均获赞的特殊性:前者是**绝对计数**,后者是**绝对量级**。
    > 高量级账号以明星/精品制作为主,单条成本高、产量天然低;
    > 人均获赞则与量级正相关,基准**递增**。实测「作品数/粉丝数」随量级
    > 单调递减(C 0.711% → A 0.24–1.0% → S 0.002–0.003%),印证了分层必要性。
    
    ### 有效满分与样本充分性
    
    判定原则只有一条:**子项在数学上不可计算时,不计入分母(不奖不罚);账号确实没有内容可评时,按最低档计分(照常扣分)。** 两者不能混为一谈。
    
    **① 样本不足 / 数学退化 → 剔除出分母**
    
    | 子项 | 退化原因 | 门槛 |
    |------|----------|------|
    | 中位/均值偏离 | n=2 时中位数恒等于均值,偏离度恒为 0 → 必然满分 | n ≥ 3 |
    | 爆款集中度 | 集中度 = max/avg 恒 ∈ [1, n],n=2 时永远 < 3 倍 → 必然满分 | n ≥ 5 |
    | 互动稳定性 CV | n<5 时离散度无统计意义,且会与集中度给出相反结论 | n ≥ 5 |
    | 发布频率 | 「返回条数 / 样本时间窗」,窗口只有几天时毫无代表性 | n ≥ 5 |
    | **近期趋势 / 衰退预警** | **快照下作品越新累计互动越少,会把「最近发布」读成「衰退」** | **满3天作品 ≥ 4** |
    
    **② 比值为 0/0(互动全为 0)→ 同样剔除**
    
    「互动结构比」(赞/评/转全 0)、「中位/均值偏离」(偏离度 0/0)、「爆款集中度」(max/avg 0/0)、「互动稳定性」(CV 0/0)在数学上均不存在。此时相关子项不计入分母 —— 该事实已由「零互动占比」与「爆款率」充分反映,重复记 0 分属于对同一事实的重复扣分。
    
    > ⚠️ 实现上有坑:`_cv()` 在均值为 0 时返回 `0.0`,与「真的完全稳定」取值相同,会把互动全零的账号误判为「稳定输出 5/5」。调用前必须先拦截。
    
    **③ 无作品数据 / 字段缺失**
    
    - **无作品数据**(works 为空):账号没有可评估的内容表现,依赖作品的子项一律按**最低档计分(0 分)**,**不剔除分母**。若剔除,一个「已清空作品」的账号会因分母变小而虚高,掩盖真实风险;报告会额外输出 `ⓘ 无近期作品数据` 提示核对。
    - **字段缺失**(作品缺 `createTime`、粉丝数为 0 导致比率分母缺失):属不可计算 → 不计入分母。
    
    被剔除的子项按**有效满分**归一化,报告标注 `[有效满分X,满分Y]` 并渲染为 `(不计入)`;样本 < 5 条时额外输出 `ⓘ 数据置信度` 提示。
    
    > **为什么趋势必须剔除未成熟作品**:接口给的是「截至采集时刻的累计互动量」快照,
    > 而互动累积是次线性的、集中在发布后最初几小时,因此越新的作品累计量必然越少。
    > 实测某账号最新一条(0.3 天)累计 18,062 vs 最老一条(6.3 天)累计 38,513,
    > 按累计量看是「暴跌 53%」,按日均看却是**上涨约 10 倍** —— 结论完全相反。
    > 故趋势类指标只比较**已满 3 天**的作品,成熟样本不足时不计入分母;
    > 衰退 / 刷量预警共用同一份样本,不会出现「评分说平稳、预警说衰退」。
    
    **「人群标签」为什么不参与计分**:它统计接口是否返回了性别/年龄。字段一旦返回必然有效,于是该子项恒拿满分、毫无区分度,还会**稀释其他缺失项的扣分**——同样资料质量的两个账号,仅因「接口恰好返回了性别」而得分不同。故改为**仅展示不计分**,账号基础画像卷面由 10 调整为 8。
    
    ### 综合评分
    
    ```
    综合评分 = Σ(维度得分 / 维度有效满分 × 100 × 权重),四舍五入取整
    ```
    
    > 舍入用 `ROUND_HALF_UP`。Python 内置 `round()` 是银行家舍入
    > (四舍六入五成双),在 .5 边界偏低,与「四舍五入」约定不符。
    
    **评级标准:**
    - 85-100:🟢 优质账号
    - 70-84:🟡 正常账号
    - 50-69:🟠 待优化
    - <50:🔴 风险账号
    
    ## ⚠️ 风险预警
    
    | 预警类型 | 触发条件 | 风险等级 |
    |----------|----------|----------|
    | 🚨 僵尸粉预警 | 粉丝互动率 < **本量级偏低线** 且粉丝数 > 5万 | 高危 |
    | 🚨 刷量预警 | 获赞/粉丝比 > 100 且近期互动骤降 | 高危 |
    | ⚠️ 衰退预警 | 近期作品均互动 < 早期的 50% | 中危 |
    | ⚠️ 限流预警 | 零互动占比 > 30% | 中危 |
    | ⚠️ 断更预警 | 最新作品距采集时间 > 14 天 | 中危 |
    | ℹ️ 单条依赖预警 | 最大爆款互动 > 均值的 10 倍 | 低危 |
    
    > 僵尸粉预警阈值与「粉丝互动率」评分基准同源,不会出现
    > 「互动率被判为良好、却又触发僵尸粉预警」的矛盾。无预警时输出「无」。
    
    ## 🔗 接口文档
    
    > 接口详情详见 `references/api_reference.md`
    
    ## ❓ 常见问答
    
    ### 使用相关
    
    **Q:昵称和抖音号都能查吗?**
    A:都可以。含中文的输入按昵称模糊匹配,纯数字/英文的输入按抖音号精确匹配。
    
    **Q:报告会包含近期作品明细吗?**
    A:会。近期作品详情默认输出,包含每条作品的发布时间、点赞/评论/分享/总互动数,标题为可点击跳转链接,点击即可直达对应作品。
    
    **Q:评分是怎么算出来的?**
    A:先按粉丝数确定账号量级(S/A/B/C),六个维度各自计分,再以各维度**有效满分**归一化到100分制后按权重加权求和。每项规则详见 `references/diagnosis_rules.md`。
    
    **Q:同一个互动率,为什么大号得分更高?**
    A:因为互动率基准是分层的。抖音互动率随粉丝体量自然衰减,670万粉账号做到 1.55% 对应的单条均互动已达 10 万+,属于良好表现;而 5 万粉账号若只有 1.55%,单条互动仅几百,属于偏低。报告末尾会标明该账号适用的基准线,可自行复核。
    
    **Q:为什么某个维度的满分显示为 10/10,而不是 15?**
    A:说明该维度有子项因数据不可计算被剔除,已按**有效满分**归一化,不会让账号为缺失数据扣分。剔除共三类:样本不足导致数学退化、比值为 0/0、字段缺失。
    > 注意:这与「账号确实没有作品数据」不同 —— 后者的相关子项按最低档计分(照常扣分),**不剔除**,否则会把「已清空作品」的账号分数抬高。
    
    **Q:某个子项显示 `(不计入)` 是什么意思?**
    A:表示该子项在当前数据下不可计算,已从该维度的分母中移除(不奖不罚)。报告会在维度标题上标注 `[有效满分X,满分Y]`。
    
    **Q:可以只诊断某个维度吗?**
    A:诊断默认输出全部六个维度。如果只关注某一项,可以在拿到报告后针对该维度追问。
    
    ### 故障排除
    
    **Q:提示"未查询到该抖音账号信息"怎么办?**
    A:输出会列出4条可能原因:① 抖音号不存在或已被注销;② 抖音号输入有误(请核对是否区分大小写,是否为正确的抖音号,非 UID、非昵称);③ 尚未收录(当前仅收录了粉丝数≧1万的账号);④ 申请收录(发送邮件至 redfoxdata@proton.me,申请通过后可进行每日定时数据追踪与分析)。
    
    **Q:提示积分不足怎么办?**
    A:前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 充值后重试即可。
    
    **Q:某些字段显示"未知"是异常吗?**
    A:不是。平台未返回的字段会按"未知"处理,处理方式分两种:**参与计分的字段**(省份、城市、简介、头像、抖音号)缺失会如实反映在"账号基础画像"得分上;**仅作展示的字段**(性别、年龄)缺失不影响任何得分。
    
    **Q:报告里出现"ⓘ 无近期作品数据"提示,说明分数不准吗?**
    A:说明接口没有返回任何作品,此时互动健康度 / 内容质量 / 内容趋势 中依赖作品的子项均按最低档计分,分数偏低属预期。请先核对账号是否已清空作品、注销或长期停更;若确认账号正常,可稍后重试(可能是接口侧数据缺失)。
    
    ## 📚 参考文档
    
    - 详细执行工作流:`references/core_workflow.md`
    - 详细评分规则:`references/diagnosis_rules.md`
    - 接口文档:`references/api_reference.md`
    
    ## 📂 目录结构
    
    ```
    douyin-account-diagnosis/
    ├── SKILL.md                          # 技能定义(本文件)
    ├── README.md / README.en.md          # 使用说明(中 / 英)
    ├── references/
    │   ├── core_workflow.md              # 完整执行流程
    │   ├── diagnosis_rules.md            # 六维度评分规则 + 量级基准表
    │   └── api_reference.md              # 接口文档
    ├── scripts/
    │   ├── douyin_diagnosis.py           # 主入口:取数 + 六维度评分 + 文本报告
    │   └── regression_test.py            # 评分模型离线回归测试(不耗 API 额度)
    └── tests/
        └── fixtures/                     # 回归测试用真实抓取数据
            ├── kaisoso.json              # S 超头部 · 少量作品(n=3,期望 ≥90)
            ├── papijiang.json            # S 超头部 · 极少作品(n=2,期望 ≥92)
            ├── juju.json                 # A 头部 · 真实低互动 0.07%(须触发僵尸粉预警)
            ├── xiaobianbian.json         # A 头部 · 快照年龄偏差(不得判为衰退)
            └── zhongguoyiwei.json        # C 尾部 · 均互动为粉丝数 2 倍(不得判互动偏弱)
    ```
    
    **改动评分规则后请务必执行回归测试**(不消耗 API 额度):
    
    ```bash
    python3 scripts/regression_test.py
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related