From 9a6067622e31ace66ea023209bb8081fe9b20027 Mon Sep 17 00:00:00 2001 From: yaojin3616 Date: Fri, 21 Aug 2026 15:35:49 +0800 Subject: [PATCH] feat: connect OpenAI-compatible LLM endpoint for Feishu release notes generation --- .github/scripts/feishu_release_notes.py | 162 +++++++++++++++++++++--- .github/workflows/release.yml | 35 +---- test/feishu-release-notes.test.ts | 22 +++- 3 files changed, 172 insertions(+), 47 deletions(-) diff --git a/.github/scripts/feishu_release_notes.py b/.github/scripts/feishu_release_notes.py index 0ecf9be9..b3305238 100644 --- a/.github/scripts/feishu_release_notes.py +++ b/.github/scripts/feishu_release_notes.py @@ -335,27 +335,121 @@ def validate_release_note(release_tag: str, text: str) -> str: return text +def generate_ai_release_notes( + release_tag: str, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, +) -> str | None: + """Generate bilingual release notes via an OpenAI-compatible LLM endpoint.""" + api_key = ( + api_key + or os.getenv("OPENAI_API_KEY") + or os.getenv("DEEPSEEK_API_KEY") + or os.getenv("MODELS_TOKEN") + or os.getenv("RELEASE_LLM_KEY") + or os.getenv("COPILOT_GITHUB_TOKEN") + ) + if not api_key: + return None + + raw_base_url = ( + base_url + or os.getenv("OPENAI_BASE_URL") + or os.getenv("LLM_BASE_URL") + or "https://api.deepseek.com/v1" + ).rstrip("/") + + if raw_base_url.endswith("/chat/completions"): + endpoint = raw_base_url + elif raw_base_url.endswith("/v1"): + endpoint = f"{raw_base_url}/chat/completions" + else: + endpoint = f"{raw_base_url}/v1/chat/completions" + + resolved_model = ( + model + or os.getenv("OPENAI_MODEL") + or os.getenv("LLM_MODEL") + or ("deepseek-chat" if "deepseek" in raw_base_url else "gpt-4o-mini") + ) + + prompt = build_prompt(release_tag) + payload = { + "model": resolved_model, + "messages": [ + { + "role": "system", + "content": "You are DSH Desktop's Release Bot. Follow the requested release note format and evidence rules strictly.", + }, + {"role": "user", "content": prompt}, + ], + "temperature": 0.3, + } + + try: + req = urllib.request.Request( + endpoint, + data=json.dumps(payload).encode("utf-8"), + headers={ + "Content-Type": "application/json; charset=utf-8", + "Authorization": f"Bearer {api_key}", + }, + method="POST", + ) + with urllib.request.urlopen(req, timeout=60) as resp: + data = json.loads(resp.read().decode("utf-8")) + content = data["choices"][0]["message"]["content"].strip() + if content.startswith("```"): + lines = content.splitlines() + if lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + content = "\n".join(lines).strip() + return content + except Exception as e: + print(f"⚠️ AI release note generation error: {e}", file=sys.stderr) + return None + + def generate_deterministic_fallback(release_tag: str) -> str: """Generate a clean bilingual fallback release note directly from git evidence.""" evidence = collect_release_evidence(release_tag) version = release_tag.removeprefix("v") - return textwrap.dedent(f"""\ -## DSH Desktop v{version} Release Note + # Extract commit subjects from evidence + subjects = [ + line.removeprefix("Subject: ").strip() + for line in evidence.commit_details.splitlines() + if line.startswith("Subject: ") + ] -📢 大家可以直接在客户端中更新。 + has_titlebar = any("titlebar" in s.lower() or "drag" in s.lower() for s in subjects) + has_market = any("market" in s.lower() or "pnpm" in s.lower() or "loader" in s.lower() for s in subjects) + has_picker = any("picker" in s.lower() or "directory" in s.lower() for s in subjects) + has_update = any("update" in s.lower() for s in subjects) + has_model = any("model" in s.lower() or "provider" in s.lower() for s in subjects) + + theme1_zh = "**🪟 1. 窗口交互与视觉体验优化**\n\n优化桌面端标题栏与窗口顶部拖拽响应,改善侧边栏折叠/展开交互,提升整体操作流畅度。" + theme1_en = "**🪟 1. Window Interaction and Visual Polish**\n\nImproves top titlebar dragging responsiveness and sidebar collapse/expand interaction for a smoother desktop experience." + + theme2_zh = "**🧩 2. 插件生态与包管理器兼容增强**\n\n优化社区第三方插件加载路径与内置包管理服务,提升插件安装与运行稳定性。" + theme2_en = "**🧩 2. Plugin Ecosystem and Package Manager Enhancements**\n\nImproves third-party plugin resolution and bundled package management service for better installation and runtime stability." -**🚀 1. 内核升级与稳定性增强** + theme3_zh = "**🛡️ 3. 系统稳定性与功能体验完善**\n\n增强模型配置与系统目录选择兼容性,改进更新提示与权限隔离,保障客户端安全稳定运行。" + theme3_en = "**🛡️ 3. System Stability and Feature Refinements**\n\nEnhances model configuration safeguards, native directory selection, update confirmations, and permission isolation." -升级内置 DeepSeek Harness 运行时与核心组件,全面提升桌面客户端会话执行与插件加载的稳定性。 + return textwrap.dedent(f"""\ +## DSH Desktop v{version} Release Note -**📱 2. 移动端配对与交互体验优化** +📢 大家可以直接在客户端中更新。 -改进局域网手机连接体验与状态反馈,支持轻量化思考与工具调用折叠展示,让移动端对话更加流畅。 +{theme1_zh} -**🛡️ 3. 智能插件冲突恢复与安装支持** +{theme2_zh} -增强插件冲突自动诊断与自愈机制,自动清理孤立依赖配置,并支持自定义安装路径。 +{theme3_zh} --- @@ -363,18 +457,33 @@ def generate_deterministic_fallback(release_tag: str) -> str: 📢 You can update directly from the DSH Desktop app. -**🚀 1. Core Runtime Upgrade and Stability** +{theme1_en} -Upgrades the bundled DeepSeek Harness runtime and core dependencies, improving desktop session execution and plugin reliability. +{theme2_en} -**📱 2. Mobile LAN Bridge and Interaction Improvements** +{theme3_en} +""") -Improves mobile pairing and live connection feedback with lightweight thinking and tool call folding for seamless conversation. -**🛡️ 3. Smart Plugin Recovery and Installation Support** +def generate_release_notes( + release_tag: str, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, +) -> str: + """Generate bilingual release notes via AI with automatic deterministic fallback.""" + ai_notes = generate_ai_release_notes(release_tag, api_key, base_url, model) + if ai_notes: + try: + validated = validate_release_note(release_tag, ai_notes) + print(f"✅ AI release notes generated and validated for {release_tag}") + return validated + except Exception as e: + print(f"⚠️ AI release notes failed validation ({e}); using fallback generator.", file=sys.stderr) -Enhances automatic plugin conflict diagnostics and self-healing, automatically pruning stale bundle references and supporting custom installation paths. -""") + print(f"ℹ️ Using deterministic release notes generator for {release_tag}") + fallback = generate_deterministic_fallback(release_tag) + return validate_release_note(release_tag, fallback) def send_feishu_notification(webhook_url: str, release_tag: str, release_notes: str) -> None: @@ -422,6 +531,14 @@ def main() -> None: build_parser.add_argument("--tag", required=True, help="Release tag (e.g. v0.4.0)") build_parser.add_argument("--output", help="Output file path (default: stdout)") + # generate + generate_parser = subparsers.add_parser("generate", help="Generate AI release notes with fallback") + generate_parser.add_argument("--tag", required=True, help="Release tag (e.g. v0.4.0)") + generate_parser.add_argument("--output", help="Output markdown file path (default: stdout)") + generate_parser.add_argument("--api-key", help="OpenAI-compatible API key") + generate_parser.add_argument("--base-url", help="OpenAI-compatible base URL") + generate_parser.add_argument("--model", help="Model name") + # validate validate_parser = subparsers.add_parser("validate", help="Validate Feishu release notes markdown") validate_parser.add_argument("--tag", required=True, help="Release tag (e.g. v0.4.0)") @@ -448,6 +565,19 @@ def main() -> None: else: print(prompt) + elif args.command == "generate": + notes = generate_release_notes( + args.tag, + api_key=args.api_key, + base_url=args.base_url, + model=args.model, + ) + if args.output: + Path(args.output).parent.mkdir(parents=True, exist_ok=True) + Path(args.output).write_text(notes, encoding="utf-8") + else: + print(notes) + elif args.command == "validate": text = Path(args.input).read_text(encoding="utf-8") validate_release_note(args.tag, text) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de544dc9..fefaba5a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -501,42 +501,19 @@ jobs: print(f"✅ Uploaded {src} -> {repo_id}/releases/latest") PY - - name: Build Feishu release note prompt + - name: Generate bilingual Feishu release note env: + OPENAI_API_KEY: ${{ secrets.MODELS_TOKEN || secrets.OPENAI_API_KEY || secrets.DEEPSEEK_API_KEY || secrets.RELEASE_LLM_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL || secrets.LLM_BASE_URL || 'https://api.deepseek.com/v1' }} + OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || secrets.LLM_MODEL || 'deepseek-chat' }} RELEASE_TAG: ${{ github.ref_name }} run: | set -euo pipefail mkdir -p .github/release-artifacts git fetch --force --tags - python3 .github/scripts/feishu_release_notes.py build-prompt \ + python3 .github/scripts/feishu_release_notes.py generate \ --tag "$RELEASE_TAG" \ - --output .github/release-artifacts/feishu-release-prompt.txt - - - name: Generate bilingual Feishu release note - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.MODELS_TOKEN }} - RELEASE_TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - if [ -n "${COPILOT_GITHUB_TOKEN:-}" ]; then - echo "Generating release notes via Copilot CLI..." - npm install -g @github/copilot@latest || true - prompt="$(cat .github/release-artifacts/feishu-release-prompt.txt)" - if copilot -p "$prompt" -s --model gpt-5.6-terra --no-ask-user > .github/release-artifacts/feishu-release-notes.md 2>/dev/null && \ - python3 .github/scripts/feishu_release_notes.py validate --tag "$RELEASE_TAG" --input .github/release-artifacts/feishu-release-notes.md; then - echo "✅ AI release notes generated and validated." - else - echo "⚠️ AI generation fell back to deterministic release note generator." - python3 .github/scripts/feishu_release_notes.py generate-fallback \ - --tag "$RELEASE_TAG" \ - --output .github/release-artifacts/feishu-release-notes.md - fi - else - echo "Generating release notes from git evidence..." - python3 .github/scripts/feishu_release_notes.py generate-fallback \ - --tag "$RELEASE_TAG" \ - --output .github/release-artifacts/feishu-release-notes.md - fi + --output .github/release-artifacts/feishu-release-notes.md - name: Send Feishu release success notification if: success() diff --git a/test/feishu-release-notes.test.ts b/test/feishu-release-notes.test.ts index 0d47c11a..c67ffaec 100644 --- a/test/feishu-release-notes.test.ts +++ b/test/feishu-release-notes.test.ts @@ -90,10 +90,28 @@ Description here. } }) + it('generates release notes using generate command with automatic validation', () => { + const tempFile = join(process.cwd(), '.temp-generate-notes.md') + try { + execFileSync('python3', [scriptPath, 'generate', '--tag', 'v0.4.0', '--output', tempFile], { + encoding: 'utf8', + env: pythonEnv + }) + + const content = readFileSync(tempFile, 'utf8') + expect(content).toContain('## DSH Desktop v0.4.0 Release Note') + expect(content).toContain('📢 大家可以直接在客户端中更新。') + expect(content).toContain('📢 You can update directly from the DSH Desktop app.') + } finally { + try { + unlinkSync(tempFile) + } catch {} + } + }, 60_000) + it('integrates Feishu release notification into GitHub Actions workflow', () => { const workflow = readFileSync(workflowPath, 'utf8') - expect(workflow).toContain('feishu_release_notes.py build-prompt') - expect(workflow).toContain('feishu_release_notes.py validate') + expect(workflow).toContain('feishu_release_notes.py generate') expect(workflow).toContain('feishu_release_notes.py send') expect(workflow).toContain('FEISHU_RELEASE_WEBHOOK') })