Skip to content

Org Discussion Router #32

Org Discussion Router

Org Discussion Router #32

Workflow file for this run

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' || '' }}