diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7137b65 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.env +.env.local +output +.cache +logs +bot/.env +bot/.venv +bot/.cache +__pycache__ +*.pyc +.idea +tmp_* +tests +.pytest_cache +*.md +!skills/**/*.md diff --git a/.env.example b/.env.example index eef9d5b..6e65f8e 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ # 企微群机器人 webhook(早报推送,与 bot API 模式凭证不同) WECOM_WEBHOOK_KEY=your-webhook-key +# GitHub Actions:将上述 key 与下方可选项写入 repo Secrets / Variables +# 详见 README「部署(GitHub Actions / Docker)」 + # 早报内容 DAILY_TRENDING_LIMIT=150 DAILY_HOT_LIMIT=150 diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml new file mode 100644 index 0000000..18a4acd --- /dev/null +++ b/.github/workflows/daily.yml @@ -0,0 +1,64 @@ +name: daily + +on: + schedule: + # 08:30 CST (UTC+8) = 00:30 UTC + - cron: "30 0 * * *" + workflow_dispatch: + inputs: + skip_push: + description: Skip WeCom push (generate only) + type: boolean + default: false + +concurrency: + group: daily-report + cancel-in-progress: false + +jobs: + report: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + TZ: Asia/Shanghai + WECOM_WEBHOOK_KEY: ${{ secrets.WECOM_WEBHOOK_KEY }} + GITHUB_TOKEN: ${{ secrets.GH_PAT || github.token }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + DAILY_LLM_API_KEY: ${{ secrets.DAILY_LLM_API_KEY }} + DAILY_LLM_API_BASE: ${{ vars.DAILY_LLM_API_BASE }} + DAILY_LLM_MODEL: ${{ vars.DAILY_LLM_MODEL }} + DAILY_REPORT_MODE: ${{ vars.DAILY_REPORT_MODE }} + CURSOR_MODEL: ${{ vars.CURSOR_MODEL }} + DAILY_AI_NEWS: ${{ vars.DAILY_AI_NEWS }} + DAILY_CN_AI_NEWS: ${{ vars.DAILY_CN_AI_NEWS }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Generate and push daily report + run: | + ARGS=() + if [ "${{ inputs.skip_push }}" = "true" ]; then + ARGS+=(--skip-push) + fi + chmod +x ./run-daily.sh + ./run-daily.sh --force "${ARGS[@]}" + + - name: Upload report artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: daily-report-${{ github.run_id }} + path: | + output/*.md + output/*.json + if-no-files-found: ignore + retention-days: 14 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..579dcdd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + TZ=Asia/Shanghai + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends tzdata \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY daily/ daily/ +COPY shared/ shared/ +COPY skills/ skills/ +COPY bot/ bot/ +COPY run-daily.sh . + +RUN chmod +x run-daily.sh \ + && mkdir -p output .cache logs + +VOLUME ["/app/output", "/app/.cache", "/app/logs"] + +ENTRYPOINT ["./run-daily.sh"] diff --git a/README.md b/README.md index 175cf13..d78f39c 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,53 @@ python -m daily push output\2026-07-03.wecom.md # 只推送 ``` +Linux / macOS / CI 等价脚本: + +```bash +chmod +x run-daily.sh +./run-daily.sh # 生成 + 推送 +./run-daily.sh --skip-push # 只生成 +./run-daily.sh --force # 忽略 30 分钟内重复运行锁 +``` + **前置条件**:Python 3.10+、企业微信群机器人 webhook。无需 Bot API 凭证。 --- +## 部署(GitHub Actions / Docker) + +### GitHub Actions(零服务器定时推送) + +仓库已包含 [`.github/workflows/daily.yml`](.github/workflows/daily.yml),默认每天 **08:30(北京时间)** 生成并推送。 + +1. Fork 或启用本仓库的 Actions +2. **Settings → Secrets and variables → Actions** 添加 Secrets: + +| Secret | 必需 | 说明 | +|--------|------|------| +| `WECOM_WEBHOOK_KEY` | 是 | 企微群机器人 webhook key | +| `GH_PAT` | 否 | 个人 GitHub PAT(提高 API 限额;不设则用 Actions 内置 token) | +| `CURSOR_API_KEY` | 否 | Agent 模式(Cursor SDK) | +| `DAILY_LLM_API_KEY` | 否 | Agent / 编辑层(OpenAI 兼容 API) | + +3. 可选 **Variables**(不设则用代码默认值):`DAILY_REPORT_MODE`、`DAILY_LLM_MODEL`、`DAILY_AI_NEWS` 等 +4. **Actions → daily → Run workflow** 可手动触发;勾选 *Skip WeCom push* 则只生成不上报 + +运行产物可在 workflow 的 **Artifacts** 中下载(`.md` / `.json`,保留 14 天)。 + +### Docker + +```bash +cp .env.example .env # 填入 WECOM_WEBHOOK_KEY 等 +docker compose build +docker compose run --rm daily # 生成 + 推送 +docker compose run --rm daily --skip-push # 只生成 +``` + +`output/`、`.cache/`、`logs/` 挂载到宿主机,与本地 `run-daily.ps1` 行为一致。 + +--- + ## Agent 模式(推荐) 默认 `classic` 是分区榜单模板;**推荐 Agent 模式**——导语 + 今日信号 + 精选新闻 + 榜单,全中文叙述。 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2cd836e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + daily: + build: . + env_file: .env + environment: + TZ: Asia/Shanghai + volumes: + - ./output:/app/output + - ./.cache:/app/.cache + - ./logs:/app/logs + # Examples: + # docker compose run --rm daily + # docker compose run --rm daily --skip-push + # docker compose run --rm daily --force diff --git a/run-daily.sh b/run-daily.sh new file mode 100644 index 0000000..74706cf --- /dev/null +++ b/run-daily.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Daily report: generate + push to WeCom webhook +# Usage: +# ./run-daily.sh +# ./run-daily.sh --skip-push +# ./run-daily.sh --skip-generate +# ./run-daily.sh --force + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +LOCK_FILE="$ROOT/.cache/run-daily.lock" +LOCK_MAX_MINUTES=30 +SKIP_PUSH=0 +SKIP_GENERATE=0 +FORCE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-push) SKIP_PUSH=1 ;; + --skip-generate) SKIP_GENERATE=1 ;; + --force) FORCE=1 ;; + -h|--help) + echo "Usage: $0 [--skip-push] [--skip-generate] [--force]" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + shift +done + +load_dotenv() { + local file="$1" + [[ -f "$file" ]] || return 0 + while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ "$line" != *"="* ]] && continue + local name="${line%%=*}" + local value="${line#*=}" + name="${name#"${name%%[![:space:]]*}"}" + name="${name%"${name##*[![:space:]]}"}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + value="${value%\"}" + value="${value#\"}" + value="${value%\'}" + value="${value#\'}" + if [[ -n "$name" && -n "$value" ]]; then + export "$name=$value" + fi + done < "$file" +} + +lock_active() { + [[ -f "$LOCK_FILE" ]] || return 1 + local now lock_ts age_minutes + now="$(date +%s)" + lock_ts="$(date -r "$LOCK_FILE" +%s 2>/dev/null || stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0)" + age_minutes=$(( (now - lock_ts) / 60 )) + [[ "$age_minutes" -lt "$LOCK_MAX_MINUTES" ]] +} + +set_lock() { + mkdir -p "$(dirname "$LOCK_FILE")" + date -Iseconds > "$LOCK_FILE" +} + +clear_lock() { + rm -f "$LOCK_FILE" +} + +if [[ "$FORCE" -eq 0 ]] && lock_active; then + echo "Skip: run-daily already ran within ${LOCK_MAX_MINUTES} minutes (lock: $LOCK_FILE). Use --force to override." + exit 0 +fi + +set_lock +trap clear_lock EXIT + +load_dotenv "$ROOT/.env" +load_dotenv "$ROOT/.env.local" + +PYTHON="${PYTHON:-python}" +DATE="$(TZ=Asia/Shanghai date +%Y-%m-%d)" +REPORT_WECOM="$ROOT/output/${DATE}.wecom.md" + +if [[ "$SKIP_GENERATE" -eq 0 ]]; then + echo "Generating daily report: $DATE" + "$PYTHON" -m daily generate +fi + +if [[ "$SKIP_PUSH" -eq 0 ]]; then + if [[ ! -f "$REPORT_WECOM" ]]; then + echo "Report not found: $REPORT_WECOM" >&2 + exit 1 + fi + echo "Pushing to WeCom webhook..." + "$PYTHON" -m daily push "$REPORT_WECOM" +fi + +echo "Done: $DATE"