agentcrew-mcn 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentcrew-mcn
3
+ Version: 0.1.0
4
+ Summary: AI MCN 自动推广工具 — 你的AI营销团队,24小时在线工作,不领工资。
5
+ Author: super-rick
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/super-rick/agent-crew
8
+ Project-URL: Bug Reports, https://github.com/super-rick/agent-crew/issues
9
+ Project-URL: Source, https://github.com/super-rick/agent-crew
10
+ Keywords: ai,mcn,marketing,agent,automation,content-marketing,multi-agent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: openai>=1.0.0
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: click>=8.1.0
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: rich>=13.0.0
27
+ Requires-Dist: chromadb>=0.5.0
28
+ Requires-Dist: schedule>=1.2.0
29
+ Requires-Dist: duckduckgo-search>=6.0.0
30
+ Requires-Dist: streamlit>=1.28.0
31
+ Requires-Dist: pandas>=2.0.0
32
+ Requires-Dist: plotly>=5.0.0
33
+ Dynamic: license-file
34
+
35
+ # AgentCrew 🤖
36
+
37
+ <div align="center">
38
+
39
+ **你的 AI 营销团队,24 小时在线工作,不领工资。**
40
+
41
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)]()
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)]()
43
+
44
+ </div>
45
+
46
+ ---
47
+
48
+ ## 什么是 AgentCrew?
49
+
50
+ AgentCrew 是一个开源的多 Agent 内容营销自动化工具。它由一组 AI"员工"组成,自动完成从内容生成到跨平台分发的全流程。
51
+
52
+ - **Writer Agent** — 文案员工:生成技术文章、帖子、Thread
53
+ - **Publisher Agent** — 运营员工:发布到掘金、知乎、X/Twitter
54
+ - **Analyst Agent** — 数据分析员工:追踪效果、优化策略(v2)
55
+
56
+ ## 核心特性
57
+
58
+ - 🧠 **多 Agent 架构** — 每个"员工"独立部署,可插拔
59
+ - 🔧 **Skills + Tools 系统** — 原子化操作 + 高级能力编排
60
+ - 📚 **RAG 知识库** — 基于历史内容和竞品情报的增强生成
61
+ - 🎯 **跨平台发布** — 支持掘金、知乎、X/Twitter 等平台
62
+ - ⏰ **智能调度** — 带随机抖动的定时发布,避免平台检测
63
+ - 🔮 **MCP 预留** — 架构预留 MCP 协议接口(v2 路线图)
64
+ - 📊 **Dashboard** — Web 面板查看发布数据和 Agent 状态(v0.3)
65
+ - 🔁 **自推广 Meta 闭环** — 项目自己推广自己
66
+
67
+ ## 快速开始
68
+
69
+ ### 安装
70
+
71
+ ```bash
72
+ # 1. 克隆项目
73
+ git clone https://github.com/super-rick/agent-crew.git
74
+ cd agent-crew
75
+
76
+ # 2. 安装依赖
77
+ pip install -r requirements.txt
78
+
79
+ # 3. 配置环境变量
80
+ cp .env.example .env
81
+ # 编辑 .env,填入你的 API Key
82
+ ```
83
+
84
+ ### 配置
85
+
86
+ 编辑 `config.yaml`,填入各平台的认证信息。
87
+
88
+ ```yaml
89
+ # config.yaml 核心配置
90
+ llm:
91
+ api_key: ${DEEPSEEK_API_KEY} # 从 .env 读取
92
+ model: deepseek-chat
93
+
94
+ platforms:
95
+ juejin:
96
+ cookie: ${JUEJIN_COOKIE} # 浏览器登录后导出 Cookie
97
+ ```
98
+
99
+ ### 使用
100
+
101
+ ```bash
102
+ # 生成一篇技术文章
103
+ agent-crew write generate --topic "Python异步编程" --style technical
104
+
105
+ # 发布到掘金
106
+ agent-crew publish post --file article.md --platform juejin
107
+
108
+ # 启动定时发布(每6小时)
109
+ agent-crew schedule start --topic-file topics.txt --platform juejin --interval 6
110
+
111
+ # 管理 RAG 知识库
112
+ agent-crew rag ingest --file article.md --source "my_blog"
113
+ agent-crew rag search --query "AI Agent 架构"
114
+ ```
115
+
116
+ ## 架构概览
117
+
118
+ ```
119
+ ┌─────────────────────────────────────────────────────┐
120
+ │ Orchestrator │
121
+ │ 任务分派 / 调度引擎 / 配置管理 │
122
+ └──┬──────────┬──────────────┬────────────────────────┘
123
+ │ │ │
124
+ ┌──▼──────┐ ┌▼────────┐ ┌──▼────────┐
125
+ │ Writer │ │Publisher│ │ Analyst │
126
+ │ Agent │ │Agent │ │ Agent │
127
+ │ │ │ │ │ │
128
+ │ Skills: │ │ Skills: │ │ Skills: │
129
+ │ 追热点 │ │ 定时分发 │ │ 效果分析 │
130
+ │ 技术文章 │ │ 多平台 │ │ 趋势报告 │
131
+ │ Thread │ │ 批量 │ │ │
132
+ │ │ │ │ │ │
133
+ │ Tools: │ │ Tools: │ │ Tools: │
134
+ │ search │ │ twitter │ │ analytics │
135
+ │ rag │ │ juejin │ │ compare │
136
+ │ compose │ │ zhihu │ │ report │
137
+ └─────────┘ └─────────┘ └───────────┘
138
+ ```
139
+
140
+ ## 技术栈
141
+
142
+ | 组件 | 技术 |
143
+ |------|------|
144
+ | 语言 | Python 3.10+ |
145
+ | CLI | Click + Rich |
146
+ | LLM | DeepSeek API (OpenAI 兼容) |
147
+ | 向量库 | ChromaDB |
148
+ | 自动化 | Playwright |
149
+ | Dashboard | Streamlit + Plotly (v0.3) |
150
+
151
+ ## 开发路线图
152
+
153
+ - **v0.1** ✅ Writer Agent + Publisher Agent + CLI + 掘金/知乎
154
+ - **v0.2** 📝 RAG 深度集成 + X/Twitter + 内容策略引擎
155
+ - **v0.3** 📊 Streamlit Dashboard + Analyst Agent
156
+ - **v1.0** 🎯 MCP 协议 + Skill Store + 社区插件
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,41 @@
1
+ agentcrew_mcn-0.1.0.dist-info/licenses/LICENSE,sha256=pWiDBWmgHOAHWHO7oWN9wrGIk5uHsR62OiDffMNGDhM,1068
2
+ agents/__init__.py,sha256=dW97M43ZI875UMaVpwFrGleTaIE99hCbmLBbXN7Zdx0,120
3
+ agents/analyst.py,sha256=GksoGdySl0UTZeryrn6u8unjbT2ofNmrZ_fA5WuOmE0,14869
4
+ agents/base.py,sha256=AMDS6etwq_KBX0xuz2_g-hnIw-wVto7F4J58DUAqVns,3695
5
+ agents/publisher.py,sha256=8vCl_mIoVfboJDOAzaApn_Eg1fcxxPaY2IT-hTP3up0,9098
6
+ agents/skills.py,sha256=7W1D2W_ZSHkNK_hc9bfZ8CvMgaVfzd0LbeveYod6pFE,7091
7
+ agents/tools.py,sha256=ZxgyOaIlvS-VXVQnDOBTt0sufjm4KBiPbEm04HzvArA,5970
8
+ agents/writer.py,sha256=L9O_VBOjX1bopnFvY1eQrW5qfDj-TTbkW8HaVYXRYWQ,10750
9
+ cli/__init__.py,sha256=hNiA5CRHyJsQDmBH6nTxPsGi8OVAvpoZBR41AE_iMMI,43
10
+ cli/analyst.py,sha256=Fa74cVLIqFH96LotMjc4dHJ9s0K3Yg843-izu5j_7RY,7292
11
+ cli/main.py,sha256=KVWgVizq0OmXabfzTvIWj6AeMk21vX7W0sXxE2iI2zM,6533
12
+ cli/publish.py,sha256=-IiueIBvp5zteYQ_FbzU5YX1dYNizarBhQ-0iny_WrA,6513
13
+ cli/rag_cmd.py,sha256=5qqngrJ2y2mNjRd5KXJCS_0BrfYrFtvnGbfautNcivQ,6242
14
+ cli/schedule.py,sha256=Cslv2uLL_a6SPDw0UypGd0XOv6iZUxz_zOslb08gp7o,5950
15
+ cli/write.py,sha256=1Ekfgsh3mkL4OjH1aY2TZ9PV_NurRIYtu1qGOdFhrPY,5486
16
+ dashboard/__init__.py,sha256=IhhDlwJtlXQ1btV91XY9UCKQ9W8-lMHjGc20AuEdO0c,60
17
+ dashboard/app.py,sha256=Hkhbr-xWDBoT80MumcWt0CzpsrmAMRM_gQ6TqjKsJqk,5412
18
+ dashboard/components.py,sha256=Kcgp7lhT58R4sbpCXSW3Lo9ZD0gH9BMShmBBHu9eTTU,7199
19
+ dashboard/data_loader.py,sha256=ECB9-eKXBWYtUve8Jdvc8GF3dUlvxUi5Ajbe6MT7bm0,6022
20
+ dashboard/pages/__init__.py,sha256=_Q80Xal7cQFs9RD1Rp7CteTNr4Pxzg4Ij2Onx2Lx5Cs,31
21
+ dashboard/pages/analytics.py,sha256=HXJqeU8KEBwkvgBecyOKhlf5YfZS-qT71oh-U2E9Yeo,4383
22
+ dashboard/pages/publishing.py,sha256=6Zv55nqoP9yJTmgjfXAP1fl0489JlJnmGxGfwV1sK5Q,3169
23
+ dashboard/pages/system.py,sha256=el_MQH2spmqtCVWp21IbSQ-wJdwP9YOVq1rQ0XvvE8k,3263
24
+ llm/__init__.py,sha256=nH01TcT4p0OsXfEcwvTkAJuWDALrn1_fs4NGcFhkg5k,129
25
+ llm/client.py,sha256=a-ltaD0-y_X8cGDsARHSbGE5y6lH1Zri2lAq_JFwI3A,2803
26
+ orchestrator/__init__.py,sha256=SeyxV-nEBS0prfA0V4jVnOykfxoQa21L87PeHG_BNTw,52
27
+ orchestrator/manager.py,sha256=n7l6I397__6h_Z7QUEXBZzWsbgzX2l6BR6WGI2DuhEI,8933
28
+ orchestrator/scheduler.py,sha256=pcNaGBFBZ6tPaP8wVSIdjww6adm4LRx-hr-o2cWZh5M,5902
29
+ platforms/__init__.py,sha256=iF3yrPVPUFgmLnYwyCc-XgtiAXeSJhLBCaCXHP02jd8,57
30
+ platforms/base.py,sha256=-iFQOQ99NHCq_LGSaSUAlj0z9OrcdPMCoXhFygqk_Hc,3578
31
+ platforms/juejin.py,sha256=bdscTIVVKmvudcPfkOdOyZRjrwrn-1Q3Jl25grio8uo,7716
32
+ platforms/zhihu.py,sha256=cDL7JDxNmfWKPpsp3netNxDIKCYXnJB-GWq3GKYTMQc,9431
33
+ rag/__init__.py,sha256=5SWzdS-WPJTmrl-ocHlwg1gsbs2ffboNqZu7HVdQlpA,43
34
+ rag/embedder.py,sha256=9Ybw8PCLzJqsEiuS4v6-Y8HbaOpSLYCfBeaTQCYdT6w,1287
35
+ rag/knowledge_base.py,sha256=_0-OPrzKpckJTnIeM-1I4geKySxArB_IW7oMX3_NKm0,4822
36
+ rag/retriever.py,sha256=QqtK3CIoIPy_5HEYAS-S76v-_CMsQ4oookQMnrwZEm8,2027
37
+ agentcrew_mcn-0.1.0.dist-info/METADATA,sha256=Je7izE0alkipUD_3nzyfvAEKhZ-xAgVoYkdxVzpGcoA,5525
38
+ agentcrew_mcn-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
39
+ agentcrew_mcn-0.1.0.dist-info/entry_points.txt,sha256=mtl8Gy_9FaMkpkJMhWUL-RpBXFhNX2rmgMA9KaN9b0I,45
40
+ agentcrew_mcn-0.1.0.dist-info/top_level.txt,sha256=ORi6zM-akZpk2mzc-czgFr7DJj7yvpErn_tscUvzGyY,52
41
+ agentcrew_mcn-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-crew = cli.main:main
@@ -0,0 +1,19 @@
1
+ Copyright (c) {{ year }} {{ organization }}
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
19
+ OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ agents
2
+ cli
3
+ dashboard
4
+ llm
5
+ orchestrator
6
+ platforms
7
+ rag
agents/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Agent module."""
2
+
3
+ from agents.base import BaseAgent, Task, TaskResult
4
+
5
+ __all__ = ["BaseAgent", "Task", "TaskResult"]
agents/analyst.py ADDED
@@ -0,0 +1,370 @@
1
+ from __future__ import annotations
2
+ """
3
+ Analyst Agent — AI 数据分析员工。
4
+
5
+ 负责:
6
+ 1. 效果分析(analyze)— 聚合发布历史数据,统计各平台表现
7
+ 2. 报告生成(report)— 调用 LLM 生成可读的效果报告
8
+ 3. 策略建议(recommend)— 基于数据生成内容策略优化建议
9
+
10
+ 数据源:直接读取 data/post_history.json(独立于 PublisherAgent)。
11
+ 核心流程:
12
+ execute(task) →
13
+ task.params.action: "analyze" | "report" | "recommend"
14
+ → _load_history() → _filter → _calculate_metrics → (LLM 可选) → TaskResult
15
+ """
16
+
17
+ import json
18
+ from collections import defaultdict
19
+ from datetime import datetime, timedelta
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from agents.base import BaseAgent, Task, TaskResult
24
+ from llm.client import LLMClient
25
+
26
+
27
+ class AnalystAgent(BaseAgent):
28
+ """Content performance analysis agent — the AI data analyst employee.
29
+
30
+ Independent of PublisherAgent — reads post history directly from the
31
+ shared history file (data/post_history.json).
32
+ """
33
+
34
+ name = "analyst"
35
+ description = "负责内容效果数据分析、报告生成和策略优化建议"
36
+
37
+ def __init__(
38
+ self,
39
+ llm_client: LLMClient,
40
+ config: dict | None = None,
41
+ ):
42
+ super().__init__(llm_client, config)
43
+ self._history_file: str = (
44
+ config.get("history_file", "data/post_history.json")
45
+ if config
46
+ else "data/post_history.json"
47
+ )
48
+ self._default_days: int = (
49
+ config.get("default_days", 7) if config else 7
50
+ )
51
+
52
+ def get_system_prompt(self) -> str:
53
+ return (
54
+ "你是一个专业的内容运营数据分析师。"
55
+ "你通过分析各平台发布数据,识别内容表现趋势和优化机会。"
56
+ "你的分析结论应当数据驱动、可执行、有具体行动建议。"
57
+ "你使用中文输出,语言简洁专业。"
58
+ )
59
+
60
+ # ── Public API ──────────────────────────────────────────
61
+
62
+ def execute(self, task: Task) -> TaskResult:
63
+ """Execute an analyst task.
64
+
65
+ Task params:
66
+ action (str): analyze | report | recommend (default: analyze)
67
+ days (int): 分析最近多少天的数据,默认取 config.default_days (7)
68
+ platforms (list[str], optional): 按平台过滤
69
+ """
70
+ started_at = datetime.now()
71
+ action = task.params.get("action", "analyze")
72
+ days = task.params.get("days", self._default_days)
73
+ platforms = task.params.get("platforms")
74
+
75
+ try:
76
+ # Step 1: Collect and filter data
77
+ history = self._load_history()
78
+ filtered = self._filter_by_days(history, days)
79
+ if platforms:
80
+ filtered = [r for r in filtered if r.get("platform") in platforms]
81
+
82
+ # Step 2: Calculate metrics
83
+ analysis = self._calculate_metrics(filtered, days)
84
+
85
+ if action == "analyze":
86
+ completed_at = datetime.now()
87
+ return TaskResult(
88
+ task_id=task.task_id,
89
+ success=True,
90
+ data={
91
+ "action": "analyze",
92
+ "period_days": days,
93
+ **analysis,
94
+ },
95
+ started_at=started_at,
96
+ completed_at=completed_at,
97
+ duration_seconds=(completed_at - started_at).total_seconds(),
98
+ agent_name=self.name,
99
+ )
100
+
101
+ elif action == "report":
102
+ report_text = self._generate_report(analysis)
103
+ completed_at = datetime.now()
104
+ return TaskResult(
105
+ task_id=task.task_id,
106
+ success=True,
107
+ data={
108
+ "action": "report",
109
+ "period_days": days,
110
+ "report": report_text,
111
+ **analysis,
112
+ },
113
+ started_at=started_at,
114
+ completed_at=completed_at,
115
+ duration_seconds=(completed_at - started_at).total_seconds(),
116
+ agent_name=self.name,
117
+ )
118
+
119
+ elif action == "recommend":
120
+ recommend_text = self._generate_recommendations(analysis)
121
+ completed_at = datetime.now()
122
+ return TaskResult(
123
+ task_id=task.task_id,
124
+ success=True,
125
+ data={
126
+ "action": "recommend",
127
+ "period_days": days,
128
+ "recommendations": recommend_text,
129
+ **analysis,
130
+ },
131
+ started_at=started_at,
132
+ completed_at=completed_at,
133
+ duration_seconds=(completed_at - started_at).total_seconds(),
134
+ agent_name=self.name,
135
+ )
136
+
137
+ else:
138
+ raise ValueError(
139
+ f"Unknown action for analyst: {action!r}. "
140
+ f"Expected: analyze, report, recommend"
141
+ )
142
+
143
+ except Exception as e:
144
+ completed_at = datetime.now()
145
+ return TaskResult(
146
+ task_id=task.task_id,
147
+ success=False,
148
+ error_message=str(e),
149
+ started_at=started_at,
150
+ completed_at=completed_at,
151
+ duration_seconds=(completed_at - started_at).total_seconds(),
152
+ agent_name=self.name,
153
+ )
154
+
155
+ # ── Data Loading ────────────────────────────────────────
156
+
157
+ def _load_history(self) -> list[dict]:
158
+ """Load post history from the shared history file.
159
+
160
+ Independent of PublisherAgent — reads directly from the JSON file
161
+ that PublisherAgent writes to. Returns empty list if the file
162
+ doesn't exist or is malformed (first run, no posts yet).
163
+ """
164
+ path = Path(self._history_file)
165
+ if not path.exists():
166
+ return []
167
+ try:
168
+ with open(path, "r", encoding="utf-8") as f:
169
+ return json.load(f)
170
+ except (json.JSONDecodeError, OSError):
171
+ return []
172
+
173
+ @staticmethod
174
+ def _filter_by_days(records: list[dict], days: int) -> list[dict]:
175
+ """Keep only records from the last N days."""
176
+ if not records:
177
+ return []
178
+ cutoff = datetime.now() - timedelta(days=days)
179
+ result: list[dict] = []
180
+ for r in records:
181
+ posted_str = r.get("posted_at")
182
+ if not posted_str:
183
+ continue
184
+ try:
185
+ posted_dt = datetime.fromisoformat(posted_str)
186
+ if posted_dt >= cutoff:
187
+ result.append(r)
188
+ except (ValueError, TypeError):
189
+ continue
190
+ return result
191
+
192
+ # ── Metrics Calculation ─────────────────────────────────
193
+
194
+ def _calculate_metrics(self, records: list[dict], days: int) -> dict[str, Any]:
195
+ """Aggregate post records into structured metrics.
196
+
197
+ Returns dict with:
198
+ total_posts, success_count, fail_count, success_rate
199
+ platform_stats: [{platform, total, success, fail, rate}, ...]
200
+ daily_counts: [{date, total, success, fail}, ...]
201
+ recent_fails: [{platform, error, posted_at}, ...]
202
+ error_summary: {error_type: count, ...}
203
+ """
204
+ total = len(records)
205
+ success = sum(1 for r in records if r.get("success"))
206
+ failed = total - success
207
+ success_rate = (success / total * 100) if total else 0.0
208
+
209
+ # Per-platform stats
210
+ platform_buckets: dict[str, dict] = defaultdict(
211
+ lambda: {"total": 0, "success": 0, "fail": 0}
212
+ )
213
+ for r in records:
214
+ plat = r.get("platform", "unknown")
215
+ platform_buckets[plat]["total"] += 1
216
+ if r.get("success"):
217
+ platform_buckets[plat]["success"] += 1
218
+ else:
219
+ platform_buckets[plat]["fail"] += 1
220
+
221
+ platform_stats: list[dict] = []
222
+ for plat, pb in sorted(platform_buckets.items()):
223
+ p_rate = (pb["success"] / pb["total"] * 100) if pb["total"] else 0.0
224
+ platform_stats.append({
225
+ "platform": plat,
226
+ "total": pb["total"],
227
+ "success": pb["success"],
228
+ "fail": pb["fail"],
229
+ "success_rate": round(p_rate, 1),
230
+ })
231
+
232
+ # Daily counts (fill gaps with zero)
233
+ now = datetime.now()
234
+ daily_map: dict[str, dict] = defaultdict(
235
+ lambda: {"total": 0, "success": 0, "fail": 0}
236
+ )
237
+ for r in records:
238
+ posted_str = r.get("posted_at")
239
+ if not posted_str:
240
+ continue
241
+ try:
242
+ d = datetime.fromisoformat(posted_str).strftime("%Y-%m-%d")
243
+ daily_map[d]["total"] += 1
244
+ if r.get("success"):
245
+ daily_map[d]["success"] += 1
246
+ else:
247
+ daily_map[d]["fail"] += 1
248
+ except (ValueError, TypeError):
249
+ continue
250
+
251
+ daily_counts: list[dict] = []
252
+ for i in range(days - 1, -1, -1):
253
+ day_label = (now - timedelta(days=i)).strftime("%Y-%m-%d")
254
+ d = daily_map.get(day_label, {"total": 0, "success": 0, "fail": 0})
255
+ daily_counts.append({
256
+ "date": day_label,
257
+ "total": d["total"],
258
+ "success": d["success"],
259
+ "fail": d["fail"],
260
+ })
261
+
262
+ # Recent failures (up to 5)
263
+ recent_fails = [
264
+ {
265
+ "platform": r.get("platform"),
266
+ "error": r.get("error_message"),
267
+ "posted_at": r.get("posted_at"),
268
+ }
269
+ for r in records
270
+ if not r.get("success")
271
+ ][-5:]
272
+
273
+ # Error type summary
274
+ error_counts: dict[str, int] = defaultdict(int)
275
+ for r in records:
276
+ if not r.get("success") and r.get("error_message"):
277
+ err = r["error_message"]
278
+ key = err.split(":")[0].strip() if ":" in err else err[:80]
279
+ error_counts[key] += 1
280
+
281
+ return {
282
+ "total_posts": total,
283
+ "success_count": success,
284
+ "fail_count": failed,
285
+ "success_rate": round(success_rate, 1),
286
+ "platform_stats": platform_stats,
287
+ "daily_counts": daily_counts,
288
+ "recent_fails": recent_fails,
289
+ "error_summary": dict(
290
+ sorted(error_counts.items(), key=lambda x: -x[1])
291
+ ),
292
+ }
293
+
294
+ # ── LLM-powered Generation ──────────────────────────────
295
+
296
+ def _generate_report(self, metrics: dict[str, Any]) -> str:
297
+ """Generate a human-readable performance report via LLM."""
298
+ prompt = self._build_report_prompt(metrics)
299
+ messages = self._build_messages(prompt)
300
+ return self.llm_client.chat(messages)
301
+
302
+ def _generate_recommendations(self, metrics: dict[str, Any]) -> str:
303
+ """Generate strategy optimisation recommendations via LLM."""
304
+ prompt = self._build_recommend_prompt(metrics)
305
+ messages = self._build_messages(prompt)
306
+ return self.llm_client.chat(messages)
307
+
308
+ # ── Prompt Builders ─────────────────────────────────────
309
+
310
+ def _build_report_prompt(self, metrics: dict) -> str:
311
+ """Build the LLM prompt for report generation."""
312
+ plat_rows = "\n".join(
313
+ f" - {p['platform']}: 共发布 {p['total']} 篇, "
314
+ f"成功 {p['success']} 篇, 失败 {p['fail']} 篇, "
315
+ f"成功率 {p['success_rate']}%"
316
+ for p in (metrics.get("platform_stats") or [])
317
+ )
318
+
319
+ daily_rows = "\n".join(
320
+ f" - {d['date']}: 发布 {d['total']} 篇 "
321
+ f"(成功 {d['success']}, 失败 {d['fail']})"
322
+ for d in (metrics.get("daily_counts") or [])
323
+ )
324
+
325
+ fail_rows = "\n".join(
326
+ f" - [{f.get('platform', '?')}] {f.get('error', '未知错误')}"
327
+ for f in (metrics.get("recent_fails") or [])
328
+ )
329
+
330
+ return (
331
+ "请根据以下发布数据生成一份内容运营周报。\n\n"
332
+ f"## 总览\n"
333
+ f"- 统计周期: 过去 {metrics.get('period_days', '?')} 天\n"
334
+ f"- 发布总数: {metrics['total_posts']} 篇\n"
335
+ f"- 成功: {metrics['success_count']} 篇 ({metrics['success_rate']}%)\n"
336
+ f"- 失败: {metrics['fail_count']} 篇\n\n"
337
+ f"## 各平台表现\n{plat_rows if plat_rows else ' (无数据)'}\n\n"
338
+ f"## 每日发布\n{daily_rows if daily_rows else ' (无数据)'}\n\n"
339
+ f"## 近期失败\n{fail_rows if fail_rows else ' (无失败记录)'}\n\n"
340
+ "请生成周报,包含:\n"
341
+ "1. 核心发现(3-5 条关键结论)\n"
342
+ "2. 各平台表现评价\n"
343
+ "3. 失败原因分析与改进建议\n"
344
+ "4. 下一步行动计划\n\n"
345
+ "格式:Markdown,标题层级分明,数据引用准确。"
346
+ )
347
+
348
+ def _build_recommend_prompt(self, metrics: dict) -> str:
349
+ """Build the LLM prompt for strategy recommendations."""
350
+ plat_rows = "\n".join(
351
+ f" - {p['platform']}: {p['total']} 篇, "
352
+ f"成功率 {p['success_rate']}%"
353
+ for p in (metrics.get("platform_stats") or [])
354
+ )
355
+
356
+ return (
357
+ "你是一个内容运营策略顾问。请根据以下发布数据给出优化建议。\n\n"
358
+ f"## 当前数据\n"
359
+ f"- 统计周期: 过去 {metrics.get('period_days', '?')} 天\n"
360
+ f"- 发布总数: {metrics['total_posts']} 篇\n"
361
+ f"- 成功率: {metrics['success_rate']}%\n\n"
362
+ f"## 各平台\n{plat_rows if plat_rows else ' (无数据)'}\n\n"
363
+ "请从以下维度给出具体可执行的策略建议:\n"
364
+ "1. 平台策略 — 哪些平台应加大/减少投入\n"
365
+ "2. 发布频率 — 建议的节奏\n"
366
+ "3. 内容方向 — 什么内容类型效果更好\n"
367
+ "4. 发布时机 — 最佳发布时间建议\n"
368
+ "5. 技术改进 — 降低发布失败率的措施\n\n"
369
+ "建议应数据驱动、具体可执行,给出优先级排序。"
370
+ )