Org Discussion Router #32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Org Discussion Router | |
| on: | |
| # 组织 Discussion 创建事件(包含 discussion.body 作为分类输入) | |
| discussion: | |
| types: [created] | |
| # 组织 Discussion 新评论事件(comment.body 作为分类输入) | |
| discussion_comment: | |
| types: [created] | |
| # Worker relay 通道(部署后生效,作为备用路径) | |
| repository_dispatch: | |
| types: [org_msg_router] | |
| # 手动触发 / dry-run 灰度验证 | |
| workflow_dispatch: | |
| inputs: | |
| discussion_number: | |
| description: 'Discussion 编号' | |
| required: true | |
| type: string | |
| comment_body: | |
| description: '评论正文(可选,用于手动模拟)' | |
| required: false | |
| type: string | |
| comment_author: | |
| description: '评论作者(可选)' | |
| required: false | |
| type: string | |
| category_name: | |
| description: '分类名(可选)' | |
| required: false | |
| type: string | |
| dry_run: | |
| description: 'Dry-run 模式(只打印不写评论)' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'false' | |
| - 'true' | |
| jobs: | |
| route: | |
| # Bot 自身评论不触发,防止无限循环(仅靠 marker 判断) | |
| if: | | |
| github.event_name != 'discussion_comment' || | |
| !contains(github.event.comment.body, '<!-- csm-qa-bot -->') | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: org-router-${{ github.event.client_payload.discussion_number || github.event.discussion.number || github.event.inputs.discussion_number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| discussions: write | |
| steps: | |
| # ── 基础环境 ────────────────────────────────────────────────────── | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.CSM_QA_GH_TOKEN }} | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Restore venv cache | |
| id: cache-venv | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .venv | |
| key: ${{ runner.os }}-venv-py3.11-${{ hashFiles('requirements-bot.txt') }} | |
| - name: Install dependencies | |
| id: install-deps | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ steps.cache-venv.outputs.cache-hit }}" = "true" ] && [ -x ".venv/bin/python" ]; then | |
| echo "venv cache hit; skipping install" | |
| else | |
| rm -rf .venv | |
| python -m pip install --disable-pip-version-check --quiet uv | |
| python -m uv venv .venv | |
| python -m uv pip install --python .venv/bin/python -r requirements-bot.txt | |
| fi | |
| echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV" | |
| echo "$PWD/.venv/bin" >> "$GITHUB_PATH" | |
| - name: Save venv cache | |
| if: always() && steps.install-deps.outcome == 'success' && steps.cache-venv.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .venv | |
| key: ${{ steps.cache-venv.outputs.cache-primary-key }} | |
| - name: Run tests | |
| run: python -m pytest tests/test_router.py -v | |
| # ── LLM 分类:轻量,仅 LLM 调用 / 正则 ─────────────────────────── | |
| - name: 🔍 Classify intent | |
| id: classify | |
| env: | |
| LLM_API_KEY: ${{ secrets.LLM_API_KEY }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| ROUTER_DISCUSSION_NUMBER: ${{ github.event.client_payload.discussion_number || github.event.discussion.number || github.event.inputs.discussion_number }} | |
| ROUTER_COMMENT_BODY: ${{ github.event.client_payload.comment_body || github.event.comment.body || github.event.discussion.body || github.event.inputs.comment_body || '' }} | |
| ROUTER_DISCUSSION_TITLE: ${{ github.event.discussion.title || '' }} | |
| ROUTER_CATEGORY_NAME: ${{ github.event.client_payload.category_name || github.event.discussion.category.name || github.event.inputs.category_name || '' }} | |
| ROUTER_EVENT_TYPE: ${{ github.event.client_payload.event_type || github.event_name || '' }} | |
| run: | | |
| INTENT=$(python scripts/router.py \ | |
| --classify-only \ | |
| --discussion-number "${ROUTER_DISCUSSION_NUMBER}" \ | |
| --comment-body "${ROUTER_COMMENT_BODY}" \ | |
| --discussion-title "${ROUTER_DISCUSSION_TITLE}" \ | |
| --category-name "${ROUTER_CATEGORY_NAME}" \ | |
| --event-type "${ROUTER_EVENT_TYPE}") | |
| echo "result=${INTENT}" >> "$GITHUB_OUTPUT" | |
| echo "Intent: ${INTENT}" | |
| # ════════════════════════════════════════════════════════════════════ | |
| # JOIN 分支 — 条件检测 + 邀请发送(轻量,无 RAG) | |
| # ════════════════════════════════════════════════════════════════════ | |
| - name: 🤝 JOIN — 条件检测 + 邀请 | |
| if: steps.classify.outputs.result == 'JOIN' | |
| env: | |
| CSM_QA_GH_TOKEN: ${{ secrets.CSM_QA_GH_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| DISCUSSION_SOURCE_REPO: ${{ github.repository_owner }}/.github | |
| JOIN_FOLLOW_ORG: ${{ vars.JOIN_FOLLOW_ORG || 'NEVSTOP-LAB' }} | |
| JOIN_STAR_REPOS: ${{ vars.JOIN_STAR_REPOS || 'Communicable-State-Machine,CSM-API-String-Arguments-Support,CSM-MassData-Parameter-Support,CSM-INI-Static-Variable-Support' }} | |
| ROUTER_DISCUSSION_NUMBER: ${{ github.event.client_payload.discussion_number || github.event.discussion.number || github.event.inputs.discussion_number }} | |
| ROUTER_COMMENT_AUTHOR: ${{ github.event.client_payload.comment_author || github.event.comment.user.login || github.event.discussion.user.login || github.event.inputs.comment_author || '' }} | |
| run: | | |
| python scripts/router.py \ | |
| --intent JOIN \ | |
| --discussion-number "${ROUTER_DISCUSSION_NUMBER}" \ | |
| --comment-author "${ROUTER_COMMENT_AUTHOR}" \ | |
| ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }} | |
| # ════════════════════════════════════════════════════════════════════ | |
| # QA 分支 — RAG 缓存 + CSM_QA 问答(重路径) | |
| # ════════════════════════════════════════════════════════════════════ | |
| # QA 之前才 restore RAG 缓存,JOIN/OTHER 不受影响 | |
| - name: Restore HuggingFace models cache | |
| id: cache-hf | |
| if: steps.classify.outputs.result == 'QA' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ~/.cache/huggingface | |
| key: ${{ runner.os }}-hf-BAAI-bge-small-zh-v1.5 | |
| restore-keys: | | |
| ${{ runner.os }}-hf- | |
| - name: Resolve csm-wiki latest commit SHA | |
| id: wiki-sha | |
| if: steps.classify.outputs.result == 'QA' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| SOURCE_FILE="csm-wiki/wiki_source.json" | |
| REPO_URL=$(python -c "import json; print(json.load(open('$SOURCE_FILE'))['url'])") | |
| SLUG=$(echo "$REPO_URL" | sed -E 's#^https?://github\.com/([^/]+/[^/]+?)(\.git)?/?$#\1#') | |
| REPO_API_URL="https://api.github.com/repos/${SLUG}" | |
| DEFAULT_BRANCH=$(curl -fsSL \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| "$REPO_API_URL" | python -c "import sys,json; print(json.load(sys.stdin)['default_branch'])") | |
| API_URL="https://api.github.com/repos/${SLUG}/commits/${DEFAULT_BRANCH}" | |
| SHA=$(curl -fsSL \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| "$API_URL" | python -c "import sys,json; print(json.load(sys.stdin)['sha'])") | |
| echo "csm-wiki ($SLUG) default branch: $DEFAULT_BRANCH, HEAD: $SHA" | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| - name: Restore vector store cache | |
| id: cache-vs | |
| if: steps.classify.outputs.result == 'QA' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: | | |
| .csm_llm_qa/vector_store | |
| csm-wiki/wiki_source.json | |
| csm-wiki/remote | |
| key: ${{ runner.os }}-vectorstore-${{ steps.wiki-sha.outputs.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-vectorstore- | |
| - name: 💬 QA — RAG 问答 | |
| if: steps.classify.outputs.result == 'QA' | |
| env: | |
| CSM_QA_GH_TOKEN: ${{ secrets.CSM_QA_GH_TOKEN }} | |
| LLM_API_KEY: ${{ secrets.LLM_API_KEY }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| DISCUSSION_SOURCE_REPO: ${{ github.repository_owner }}/.github | |
| ROUTER_DISCUSSION_NUMBER: ${{ github.event.client_payload.discussion_number || github.event.discussion.number || github.event.inputs.discussion_number }} | |
| ROUTER_CATEGORY_NAME: ${{ github.event.client_payload.category_name || github.event.discussion.category.name || github.event.inputs.category_name || '' }} | |
| run: | | |
| python scripts/router.py \ | |
| --intent QA \ | |
| --discussion-number "${ROUTER_DISCUSSION_NUMBER}" \ | |
| --category-name "${ROUTER_CATEGORY_NAME}" \ | |
| ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }} | |
| # QA 之后才保存 RAG 缓存 | |
| - name: Check HuggingFace cache populated | |
| id: check-hf-populated | |
| if: always() && steps.classify.outputs.result == 'QA' && steps.cache-hf.outputs.cache-hit != 'true' | |
| run: | | |
| if [ -d "$HOME/.cache/huggingface" ] && [ -n "$(ls -A "$HOME/.cache/huggingface" 2>/dev/null)" ]; then | |
| echo "populated=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "populated=false" >> "$GITHUB_OUTPUT" | |
| echo "Skipping HF cache save: ~/.cache/huggingface missing or empty" | |
| fi | |
| - name: Save HuggingFace models cache | |
| if: always() && steps.classify.outputs.result == 'QA' && steps.cache-hf.outputs.cache-hit != 'true' && steps.check-hf-populated.outputs.populated == 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ~/.cache/huggingface | |
| key: ${{ steps.cache-hf.outputs.cache-primary-key }} | |
| - name: Check vector store cache populated | |
| id: check-vs-populated | |
| if: always() && steps.classify.outputs.result == 'QA' && steps.cache-vs.outputs.cache-hit != 'true' | |
| run: | | |
| if [ -d ".csm_llm_qa/vector_store" ] && [ -n "$(ls -A .csm_llm_qa/vector_store 2>/dev/null)" ]; then | |
| echo "populated=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "populated=false" >> "$GITHUB_OUTPUT" | |
| echo "Skipping vector store cache save: .csm_llm_qa/vector_store missing or empty" | |
| fi | |
| - name: Save vector store cache | |
| if: always() && steps.classify.outputs.result == 'QA' && steps.cache-vs.outputs.cache-hit != 'true' && steps.check-vs-populated.outputs.populated == 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: | | |
| .csm_llm_qa/vector_store | |
| csm-wiki/wiki_source.json | |
| csm-wiki/remote | |
| key: ${{ steps.cache-vs.outputs.cache-primary-key }} | |
| # ════════════════════════════════════════════════════════════════════ | |
| # OTHER 分支 — 友好引导(轻量) | |
| # ════════════════════════════════════════════════════════════════════ | |
| - name: 💡 OTHER — 友好引导 | |
| if: steps.classify.outputs.result == 'OTHER' | |
| env: | |
| CSM_QA_GH_TOKEN: ${{ secrets.CSM_QA_GH_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| DISCUSSION_SOURCE_REPO: ${{ github.repository_owner }}/.github | |
| ROUTER_DISCUSSION_NUMBER: ${{ github.event.client_payload.discussion_number || github.event.discussion.number || github.event.inputs.discussion_number }} | |
| run: | | |
| python scripts/router.py \ | |
| --intent OTHER \ | |
| --discussion-number "${ROUTER_DISCUSSION_NUMBER}" \ | |
| ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }} |