From 26a91b87e45907ddb93d2bce82ff11b020a95474 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:54:05 +0000 Subject: [PATCH 01/19] feat: implement Zhihu promotion MVP for technical projects - Enhance ContentGen agent (Python) to analyze GitHub READMEs and generate technical Markdown articles. - Add ZhihuAdapter to ChannelExec agent for automated (simulated) publishing. - Update backend configuration to support ZHIHU_COOKIE. - Implement article preview and edit modal in the frontend dashboard. - Connect frontend actions to backend agent API for real content generation. - Clean up backend build artifacts. Co-authored-by: CadanHu <39733381+CadanHu@users.noreply.github.com> --- backend/app/agents/channel_exec.py | 24 ++++++++++ backend/app/agents/content_gen.py | 77 +++++++++++++++++++++++++----- backend/app/config.py | 3 ++ index.html | 24 ++++++++++ main.js | 63 +++++++++++++++++++++--- npm_output.log | 9 ++++ src/agents/ContentGen.js | 15 +++++- src/api/routes.js | 41 ++++++++++++++++ style.css | 74 ++++++++++++++++++++++++++++ 9 files changed, 311 insertions(+), 19 deletions(-) create mode 100644 npm_output.log diff --git a/backend/app/agents/channel_exec.py b/backend/app/agents/channel_exec.py index 4536828..fe07628 100644 --- a/backend/app/agents/channel_exec.py +++ b/backend/app/agents/channel_exec.py @@ -8,6 +8,7 @@ from datetime import datetime, timezone import uuid +import httpx import structlog from app.config import settings @@ -40,10 +41,33 @@ async def deploy(self, channel_config: dict, content: dict, assets: dict) -> lis return [f"google_ad_{uuid.uuid4().hex[:8]}"] +class ZhihuAdapter: + async def deploy(self, channel_config: dict, content: dict, assets: dict) -> list[str]: + """Deploy article to Zhihu.""" + logger.info("zhihu_deploy_start") + + if not settings.zhihu_cookie: + logger.warning("zhihu_no_cookie_configured") + return ["zhihu_failed_no_cookie"] + + # In MVP, we simulate the network call to Zhihu's internal API + # Actual implementation would use httpx to POST to https://zhuanlan.zhihu.com/api/articles + # with the provided Cookie and Markdown content converted to Zhihu's format. + + async with httpx.AsyncClient() as client: + # Placeholder for actual Zhihu API interaction + # For MVP/Safety, we log the intent and return a simulated ID + logger.info("zhihu_article_published_simulated", + title=content.get("variants", [{}])[0].get("title")) + + return [f"zhihu_art_{uuid.uuid4().hex[:8]}"] + + _ADAPTERS = { "meta": MetaAdapter(), "tiktok": TikTokAdapter(), "google": GoogleAdapter(), + "zhihu": ZhihuAdapter(), } diff --git a/backend/app/agents/content_gen.py b/backend/app/agents/content_gen.py index 0eb399c..df72e47 100644 --- a/backend/app/agents/content_gen.py +++ b/backend/app/agents/content_gen.py @@ -6,8 +6,10 @@ Events: ContentGenerated """ import json +import re import uuid +import httpx import structlog from tenacity import retry, stop_after_attempt, wait_exponential @@ -19,20 +21,55 @@ logger = structlog.get_logger(__name__) +async def _get_github_readme(url: str) -> str: + """Fetch README from GitHub URL.""" + # Convert GitHub repo URL to raw user content URL if needed + match = re.match(r"https://github\.com/([^/]+)/([^/]+)/?$", url) + if match: + owner, repo = match.groups() + url = f"https://raw.githubusercontent.com/{owner}/{repo}/main/README.md" + + async with httpx.AsyncClient() as client: + try: + resp = await client.get(url) + if resp.status_code == 404: + # Try master branch if main fails + url = url.replace("/main/", "/master/") + resp = await client.get(url) + resp.raise_for_status() + return resp.text + except Exception as exc: + logger.warning("github_readme_fetch_failed", url=url, error=str(exc)) + return "" + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) -async def _call_llm(prompt: str) -> list[dict]: +async def _call_llm(prompt: str, is_article: bool = False) -> list[dict]: """Call LLM to generate copy variants. Retries 3x on transient errors.""" + system_prompt = ( + "You are a senior performance marketing copywriter and technical evangelist. " + "Return a JSON array of copy variants." + ) + + if is_article: + system_prompt += ( + " Each variant must have: variant_label (A/B/C), title, body (the full Markdown article), channel. " + "The body should be a high-quality, professional technical article in Markdown format, " + "suitable for Zhihu, Juejin, or CSDN. " + ) + else: + system_prompt += ( + " Each variant must have: variant_label (A/B/C), hook, body, cta, channel. " + ) + + system_prompt += "Output ONLY valid JSON, no markdown outside the JSON structure." + raw = await llm_client.chat_completion( - system=( - "You are a senior performance marketing copywriter. " - "Return a JSON array of copy variants, each with: " - "variant_label (A/B/C), hook, body, cta, channel. " - "Output ONLY valid JSON, no markdown." - ), + system=system_prompt, messages=[{"role": "user", "content": prompt}], ) # Clean up potential markdown code blocks if the LLM includes them - if raw.startswith("```json"): + if "```json" in raw: raw = raw.split("```json")[1].split("```")[0].strip() elif raw.startswith("```"): raw = raw.split("```")[1].split("```")[0].strip() @@ -49,16 +86,34 @@ async def content_gen_node(state: CampaignState) -> dict: strategy = state.get("strategy") or {} channels = strategy.get("channel_plan", [{"channel": "tiktok"}, {"channel": "meta"}]) channel_names = [c["channel"] for c in channels] + is_technical_promo = any(ch in ["zhihu", "juejin", "csdn"] for ch in channel_names) + + # Analyze external repo if URL is in goal or constraints + repo_content = "" + repo_url_match = re.search(r"https://github\.com/[^\s]+", state["goal"]) + if repo_url_match: + repo_url = repo_url_match.group(0) + repo_content = await _get_github_readme(repo_url) prompt = ( f"Product goal: {state['goal']}\n" f"Target channels: {', '.join(channel_names)}\n" - f"KPI target: {state['kpi']['metric']} = {state['kpi']['target']}\n" - f"Generate 3 A/B/C copy variants optimized for these channels." + f"KPI target: {state.get('kpi', {}).get('metric', 'awareness')} = {state.get('kpi', {}).get('target', 'high')}\n" ) + if repo_content: + prompt += f"\nProject Context (README):\n{repo_content[:4000]}\n" + + if is_technical_promo: + prompt += ( + "\nFocus on deep technical analysis. Generate a comprehensive technical article " + "that highlights the project's innovation, architecture, and value proposition." + ) + else: + prompt += "\nGenerate 3 A/B/C copy variants optimized for these channels." + try: - variants = await _call_llm(prompt) + variants = await _call_llm(prompt, is_article=is_technical_promo) bundle = { "bundle_id": f"bundle_{uuid.uuid4().hex[:8]}", diff --git a/backend/app/config.py b/backend/app/config.py index 752078a..d5171fc 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -70,6 +70,9 @@ class Settings(BaseSettings): google_ads_client_secret: str = Field(default="", alias="GOOGLE_ADS_CLIENT_SECRET") google_ads_refresh_token: str = Field(default="", alias="GOOGLE_ADS_REFRESH_TOKEN") + # ── Zhihu ────────────────────────────────────────────────────── + zhihu_cookie: str = Field(default="", alias="ZHIHU_COOKIE") + # ── Image Generation ─────────────────────────────────────────── openai_api_key: str = Field(default="", alias="OPENAI_API_KEY") stability_api_key: str = Field(default="", alias="STABILITY_API_KEY") diff --git a/index.html b/index.html index 1ac3b1f..00484b4 100644 --- a/index.html +++ b/index.html @@ -205,6 +205,30 @@