From 0181700c3bbc1559bc4f042074ea71690172d022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Thu, 13 Nov 2025 18:31:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=8C=96version?= =?UTF-8?q?=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/prompt/release_note_prompt.txt | 61 +++++++ .github/workflows/auto-release.yml | 166 ++++++++++++++++++++ packages/napcat-common/src/env.d.ts | 9 ++ packages/napcat-common/src/version.ts | 2 +- packages/napcat-shell/package.json | 3 +- packages/napcat-shell/vite.config.ts | 3 + packages/napcat-vite/package.json | 31 ++++ packages/napcat-vite/tsconfig.json | 49 ++++++ packages/napcat-vite/vite-plugin-version.js | 117 ++++++++++++++ 9 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 .github/prompt/release_note_prompt.txt create mode 100644 .github/workflows/auto-release.yml create mode 100644 packages/napcat-common/src/env.d.ts create mode 100644 packages/napcat-vite/package.json create mode 100644 packages/napcat-vite/tsconfig.json create mode 100644 packages/napcat-vite/vite-plugin-version.js diff --git a/.github/prompt/release_note_prompt.txt b/.github/prompt/release_note_prompt.txt new file mode 100644 index 00000000..bad20524 --- /dev/null +++ b/.github/prompt/release_note_prompt.txt @@ -0,0 +1,61 @@ +注意:输出必须严格使用 NapCat 的发布说明格式,并用简体中文。 + +格式规则: +1. 第一行:# V{TAG} +2. 第二行:[使用文档](https://napneko.github.io/) +3. 空行后,按下面的节顺序输出(存在则输出,不存在则省略该节): + +## Windows 一键包 +- 简短一句话介绍一键包用途 +- 列出可下载的文件名(只列文件名,不写下载链接) + +## 警告 +- 如果有需要特别提醒的兼容/运行库/版本要求,写成加粗警告句 + +## 如果WinX64缺少运行库或者xxx.dll? +- 常见运行库建议 + +## 更新 +按数字序列列出主要变更项,每条尽量一句话 +- 前缀短 commit id,例如:1. a1b2c3d 修复 get_essence_msg_list 崩溃 +- 保持 4-18 条要点 + +## 开发者注意 +- 列出迁移/接口断裂/配置变更;若无则省略 + +额外约束: +- 语言简体中文,面向最终用户 +- 不输出 stack trace、密钥、敏感信息 +- 只列文件名作为 assets,不写链接 +- 若提交为空,输出简短默认说明 + +下面为示例 + +# V?.?.? +[使用文档](https://napneko.github.io/) + +## Windows 一键包 +我们为提供了的轻量化一键部署方案 +相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考 + +你可以下载 + +NapCat.Shell.Windows.OneKey.zip (无头) + +启动后可自动化部署一键包,教程参考使用文档安装部分 + +## 警告 +**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本** +**默认WebUi密钥为随机密码 控制台查看** + +**[9.9.22-40990 X64 Win](https://dldir1v6.qq.com/qqfile/qq/QQNT/2c9d3f6c/QQ9.9.22.40990_x64.exe)** +[LinuxX64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_amd64.deb) +[LinuxX64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_x86_64.rpm) +[LinuxArm64 DEB 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_arm64.deb) +[LinuxArm64 RPM 40990 ](https://dldir1.qq.com/qqfile/qq/QQNT/ec800879/linuxqq_3.2.20-40990_aarch64.rpm) +[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg) +## 如果WinX64缺少运行库或者xxx.dll? +[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe) + +## 更新 +1. xxxx \ No newline at end of file diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 00000000..5ddad9a5 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,166 @@ +name: Build Release (OpenRouter AI notes on tag) + +on: + push: + tags: + - '*' # 任意 tag push 时触发,可改为 'v*.*.*' + +permissions: + contents: write + +env: + OPENROUTER_API_URL: https://openrouter.ai/api/v1/chat/completions + OPENROUTER_MODEL: "deepseek/deepseek-chat-v3-0324:free" + RELEASE_NAME: "NapCat" + +jobs: + Build-LiteLoader: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Build NapCat.Framework + run: | + npm i -g pnpm + pnpm i + pnpm --filter napcat-webui-frontend run build || exit 1 + pnpm run build:framework + mv packages/napcat-framework/dist framework-dist + cd framework-dist + npm install --omit=dev + rm -f package-lock.json + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: NapCat.Framework + path: framework-dist + + Build-Shell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Build NapCat.Shell + run: | + npm i -g pnpm + pnpm i + pnpm --filter napcat-webui-frontend run build || exit 1 + pnpm run build:shell + mv packages/napcat-shell/dist shell-dist + cd shell-dist + npm install --omit=dev + rm -f package-lock.json + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: NapCat.Shell + path: shell-dist + + release-napcat: + needs: [Build-LiteLoader, Build-Shell] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: ./artifacts + + - name: Make zips + run: | + cd artifacts || exit 0 + [ -d NapCat.Shell ] && (cd NapCat.Shell && zip -qr ../NapCat.Shell.zip .) + [ -d NapCat.Framework ] && (cd NapCat.Framework && zip -qr ../NapCat.Framework.zip .) + cd .. + ls -la + + - name: Prepare commits list + run: | + TAG="${GITHUB_REF#refs/tags/}" + git fetch --all --tags || true + PREV_TAG=$(git tag --sort=-creatordate | grep -v "^$" | awk -v t="$TAG" '$0!=t{print; exit}') + if [ -n "$PREV_TAG" ]; then + git log --pretty=format:'%h %s (%an)' "$PREV_TAG..$TAG" > /tmp/commits.txt || git log --pretty=format:'%h %s (%an)' -n 30 > /tmp/commits.txt + else + git log --pretty=format:'%h %s (%an)' -n 30 > /tmp/commits.txt + fi + echo "=== commits ===" + sed -n '1,200p' /tmp/commits.txt || true + + - name: Generate release note via OpenRouter + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }} + OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }} + run: | + set -euo pipefail + PROMPT_FILE=".github/prompt/release_note_prompt.txt" + TAG="${GITHUB_REF#refs/tags/}" + ARTIFACTS_LIST=$(ls ./artifacts 2>/dev/null | tr '\n' ',' | sed 's/,$//') + COMMITS=$(sed 's/"/\\"/g' /tmp/commits.txt || echo "无提交信息") + USER_CONTENT="$(printf "TAG: %s\nARTIFACTS: %s\n\n提交列表:\n%s" "$TAG" "$ARTIFACTS_LIST" "$COMMITS")" + SYSTEM_PROMPT="$(jq -Rs . < "$PROMPT_FILE")" + + BODY=$(jq -n \ + --arg system "$SYSTEM_PROMPT" \ + --arg user "$USER_CONTENT" \ + '{model: env.OPENROUTER_MODEL, messages: [{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:800}') + + RESPONSE=$(curl -s -X POST "$OPENROUTER_API_URL" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENROUTER_API_KEY" \ + -d "$BODY") + + echo "$RESPONSE" | jq -r '.choices[0].message.content // .choices[0].text // ""' > /tmp/release_body.txt || true + echo "=== generated release note ===" + sed -n '1,200p' /tmp/release_body.txt || true + + - name: Create or update release & upload assets + env: + GHTOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: "${{ github.ref_name }}" + run: | + set -euo pipefail + BODY=$(sed 's/"/\\"/g' /tmp/release_body.txt || echo "Automated release") + # check existing release for this tag + EXIST=$(curl -s -H "Authorization: token $GHTOKEN" "https://api.github.com/repos/$REPO/releases/tags/$TAG" || true) + RID=$(echo "$EXIST" | jq -r '.id // empty') + if [ -n "$RID" ]; then + echo "Update existing release id $RID" + jq -n --arg body "$BODY" '{body:$body}' > /tmp/update.json + curl -s -X PATCH -H "Authorization: token $GHTOKEN" -H "Content-Type: application/json" \ + "https://api.github.com/repos/$REPO/releases/$RID" -d @/tmp/update.json | jq -r '.id' + else + echo "Create release for tag $TAG" + jq -n --arg tag "$TAG" --arg name "${RELEASE_NAME} $TAG" --arg body "$BODY" \ + '{tag_name:$tag, name:$name, body:$body, draft:true, prerelease:false}' > /tmp/create.json + CREATE_RESP=$(curl -s -X POST -H "Authorization: token $GHTOKEN" -H "Content-Type: application/json" \ + "https://api.github.com/repos/$REPO/releases" -d @/tmp/create.json) + RID=$(echo "$CREATE_RESP" | jq -r '.id') + fi + + upload() { + f="$1" + [ -f "$f" ] || { echo "skip $f"; return; } + NAME=$(basename "$f") + UPLOAD_URL=$(curl -s -H "Authorization: token $GHTOKEN" "https://api.github.com/repos/$REPO/releases/$RID" | jq -r '.upload_url') + UPLOAD_URL="${UPLOAD_URL%\{*}?name=${NAME}" + echo "Uploading $NAME..." + curl -s -X POST -H "Authorization: token $GHTOKEN" -H "Content-Type: application/zip" --data-binary @"$f" "$UPLOAD_URL" | jq -r '.id' + } + + upload "./artifacts/NapCat.Framework.zip" + upload "./artifacts/NapCat.Shell.zip" + echo "done" diff --git a/packages/napcat-common/src/env.d.ts b/packages/napcat-common/src/env.d.ts new file mode 100644 index 00000000..f2363fca --- /dev/null +++ b/packages/napcat-common/src/env.d.ts @@ -0,0 +1,9 @@ +/// + +declare global { + interface ImportMetaEnv { + readonly VITE_NAPCAT_VERSION: string; + } +} + +export {}; diff --git a/packages/napcat-common/src/version.ts b/packages/napcat-common/src/version.ts index 3be1513b..fda5cf2f 100644 --- a/packages/napcat-common/src/version.ts +++ b/packages/napcat-common/src/version.ts @@ -1 +1 @@ -export const napCatVersion = '4.9.42'; +export const napCatVersion = import.meta.env.VITE_NAPCAT_VERSION || 'alpha'; diff --git a/packages/napcat-shell/package.json b/packages/napcat-shell/package.json index 7df2fd00..b264b51d 100644 --- a/packages/napcat-shell/package.json +++ b/packages/napcat-shell/package.json @@ -23,7 +23,8 @@ "napcat-qrcode": "workspace:*" }, "devDependencies": { - "@types/node": "^22.0.1" + "@types/node": "^22.0.1", + "napcat-vite": "workspace:*" }, "engines": { "node": ">=18.0.0" diff --git a/packages/napcat-shell/vite.config.ts b/packages/napcat-shell/vite.config.ts index 0cb96550..9b0371e4 100644 --- a/packages/napcat-shell/vite.config.ts +++ b/packages/napcat-shell/vite.config.ts @@ -3,6 +3,8 @@ import { defineConfig, PluginOption, UserConfig } from 'vite'; import path, { resolve } from 'path'; import nodeResolve from '@rollup/plugin-node-resolve'; import { builtinModules } from 'module'; +import napcatVersion from "napcat-vite/vite-plugin-version.js"; + //依赖排除 const external = [ 'silk-wasm', @@ -21,6 +23,7 @@ const ShellBaseConfigPlugin: PluginOption[] = [ ], }), nodeResolve(), + napcatVersion(), ]; const ShellBaseConfig = () => defineConfig({ diff --git a/packages/napcat-vite/package.json b/packages/napcat-vite/package.json new file mode 100644 index 00000000..0357c872 --- /dev/null +++ b/packages/napcat-vite/package.json @@ -0,0 +1,31 @@ +{ + "name": "napcat-vite", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "build": "vite build" + }, + "exports": { + ".": { + "import": "./index.ts" + }, + "./*": { + "import": "./*" + } + }, + "dependencies": { + "napcat-core": "workspace:*", + "napcat-common": "workspace:*", + "napcat-onebot": "workspace:*", + "napcat-webui-backend": "workspace:*", + "napcat-qrcode": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.0.1" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/packages/napcat-vite/tsconfig.json b/packages/napcat-vite/tsconfig.json new file mode 100644 index 00000000..9e2d7942 --- /dev/null +++ b/packages/napcat-vite/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "Node", + "lib": [ + "ES2021" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "esModuleInterop": true, + "outDir": "dist", + "rootDir": "./", + "noEmit": false, + "sourceMap": true, + "strict": true, + "noImplicitAny": false, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "alwaysStrict": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "forceConsistentCasingInFileNames": true, + "useUnknownInCatchVariables": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "useDefineForClassFields": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "baseUrl": "./", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "allowJs": true + }, + "include": [ + "vite-plugin-version.js" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/napcat-vite/vite-plugin-version.js b/packages/napcat-vite/vite-plugin-version.js new file mode 100644 index 00000000..9bcea274 --- /dev/null +++ b/packages/napcat-vite/vite-plugin-version.js @@ -0,0 +1,117 @@ +import fs from "fs"; +import path from "path"; +import https from "https"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * NapCat Vite Plugin: fetches latest GitHub tag (not release) and injects into import.meta.env + */ +export default function vitePluginNapcatVersion() { + const pluginDir = path.resolve(__dirname, "dist"); + const cacheFile = path.join(pluginDir, ".napcat-version.json"); + const owner = "NapNeko"; + const repo = "NapCatQQ"; + const maxAgeMs = 24 * 60 * 60 * 1000; // cache 1 day + const githubToken = process.env.GITHUB_TOKEN; + + fs.mkdirSync(pluginDir, { recursive: true }); + + function readCache() { + try { + const stat = fs.statSync(cacheFile); + if (Date.now() - stat.mtimeMs < maxAgeMs) { + const data = JSON.parse(fs.readFileSync(cacheFile, "utf8")); + if (data?.tag) return data.tag; + } + } catch {} + return null; + } + + function writeCache(tag) { + try { + fs.writeFileSync( + cacheFile, + JSON.stringify({ tag, time: new Date().toISOString() }, null, 2) + ); + } catch {} + } + + async function fetchLatestTag() { + const url = `https://api.github.com/repos/${owner}/${repo}/tags`; + return new Promise((resolve, reject) => { + const req = https.get( + url, + { + headers: { + "User-Agent": "vite-plugin-napcat-version", + Accept: "application/vnd.github.v3+json", + ...(githubToken ? { Authorization: `token ${githubToken}` } : {}), + }, + }, + (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => { + try { + const json = JSON.parse(data); + if (Array.isArray(json) && json[0]?.name) { + resolve(json[0].name); + } else reject(new Error("Invalid GitHub tag response")); + } catch (e) { + reject(e); + } + }); + } + ); + req.on("error", reject); + }); + } + + async function getVersion() { + const cached = readCache(); + if (cached) return cached; + try { + const tag = await fetchLatestTag(); + writeCache(tag); + return tag; + } catch (e) { + console.warn("[vite-plugin-napcat-version] Failed to fetch tag:", e.message); + return cached ?? "v0.0.0"; + } + } + + let lastTag = null; + + return { + name: "vite-plugin-napcat-version", + enforce: "pre", + + async config(userConfig) { + const tag = await getVersion(); + console.log(`[vite-plugin-napcat-version] Using version: ${tag}`); + lastTag = tag; + return { + define: { + ...(userConfig.define || {}), + "import.meta.env.VITE_NAPCAT_VERSION": JSON.stringify(tag), + }, + }; + }, + + handleHotUpdate(ctx) { + if (path.resolve(ctx.file) === cacheFile) { + try { + const json = JSON.parse(fs.readFileSync(cacheFile, "utf8")); + const tag = json?.tag; + if (tag && tag !== lastTag) { + lastTag = tag; + ctx.server?.ws.send({ type: "full-reload" }); + } + } catch {} + } + }, + }; +}