diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 42b2ae65..cbb72dd7 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -153,9 +153,14 @@ jobs: # ----------------------------- # 5) 拷贝 QQ 文件到 NapCat.Shell.Windows.Node # ----------------------------- - QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node" "LightQuic.dll") + QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "crypto.dll" "ssl.dll" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node" "LightQuic.dll") for name in "${QQ_TARGETS[@]}"; do - find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true + source_file=$(find "$QQ_EXTRACT" -type f -iname "$name" -print -quit) + if [ -z "$source_file" ]; then + echo "Missing required QQ file: $name" >&2 + exit 1 + fi + cp -a "$source_file" "$OUT_DIR/" done # ----------------------------- @@ -183,8 +188,26 @@ jobs: name: NapCat.Shell.Windows.Node path: NapCat.Shell.Windows.Node + validate-windows-node: + needs: assemble-windows-node + runs-on: windows-latest + steps: + - uses: actions/download-artifact@v7 + with: + name: NapCat.Shell.Windows.Node + path: windows-node + + - name: Verify bundled Node and QQ native dependencies + shell: pwsh + working-directory: windows-node + run: | + & .\node.exe --check .\index.js + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & .\node.exe -e "const wrapper = require('./wrapper.node'); if (!wrapper.NodeQQNTWrapperUtil) throw new Error('Missing NodeQQNTWrapperUtil'); console.log('wrapper.node native-load PASS'); process.exit(0);" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + release-napcat: - needs: [build, assemble-windows-node] + needs: [build, assemble-windows-node, validate-windows-node] runs-on: ubuntu-latest steps: - name: Checkout diff --git a/packages/napcat-develop/napcat.bat b/packages/napcat-develop/napcat.bat index 0c15cd10..8215fa44 100644 --- a/packages/napcat-develop/napcat.bat +++ b/packages/napcat-develop/napcat.bat @@ -1 +1,5 @@ -node.exe ./index.js \ No newline at end of file +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +"%~dp0node.exe" "%~dp0index.js" %* +exit /b %errorlevel% diff --git a/packages/napcat-framework/napcat.cjs b/packages/napcat-framework/napcat.cjs index 7ead94ba..dbfdaba7 100644 --- a/packages/napcat-framework/napcat.cjs +++ b/packages/napcat-framework/napcat.cjs @@ -1,6 +1,7 @@ const process = require('process'); const os = require('os'); const path = require('path'); +const { pathToFileURL } = require('url'); // 保存原始dlopen const dlopenOrig = process.dlopen; @@ -50,7 +51,7 @@ async function initializeNapCat () { console.log('[NapCat] [Info] 开始初始化NapCat'); try { const currentPath = path.dirname(__filename); - const { NCoreInitFramework, getWebUiUrl } = await import('file://' + path.join(currentPath, './napcat.mjs')); + const { NCoreInitFramework, getWebUiUrl } = await import(pathToFileURL(path.join(currentPath, './napcat.mjs')).href); getWebUiUrlFunc = getWebUiUrl; await NCoreInitFramework(wrapperSession, wrapperLoginService, (callback) => { ncCallback = callback; }); } catch (error) { diff --git a/packages/napcat-framework/nativeLoader.cjs b/packages/napcat-framework/nativeLoader.cjs index b644cd0d..56a91479 100644 --- a/packages/napcat-framework/nativeLoader.cjs +++ b/packages/napcat-framework/nativeLoader.cjs @@ -1,5 +1,6 @@ // const fs = require('fs'); const path = require('path'); +const { pathToFileURL } = require('url'); async function initializeNapCat (session, loginService, registerCallback) { // const logFile = path.join(currentPath, 'napcat.log'); @@ -12,7 +13,7 @@ async function initializeNapCat (session, loginService, registerCallback) { try { const currentPath = path.dirname(__filename); - const { NCoreInitFramework } = await import('file://' + path.join(currentPath, './napcat.mjs')); + const { NCoreInitFramework } = await import(pathToFileURL(path.join(currentPath, './napcat.mjs')).href); await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback); }); } catch (error) { console.log('[NapCat] [Error] 初始化NapCat', error); diff --git a/packages/napcat-napi-loader/napiLoader-debug.bat b/packages/napcat-napi-loader/napiLoader-debug.bat index 89f66bd3..39d5cdf8 100644 --- a/packages/napcat-napi-loader/napiLoader-debug.bat +++ b/packages/napcat-napi-loader/napiLoader-debug.bat @@ -1,8 +1,10 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 -set NAPCAT_INJECT_PATH=%cd%\napiloader.dll -set NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe -set NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs +set "NAPCAT_INJECT_PATH=%cd%\napiloader.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe" +set "NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs" set NAPCAT_DEBUG_CONSOLE=1 :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( @@ -27,4 +29,4 @@ set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%" -pause \ No newline at end of file +pause diff --git a/packages/napcat-napi-loader/napiLoader.bat b/packages/napcat-napi-loader/napiLoader.bat index 7fecb175..bcb97c80 100644 --- a/packages/napcat-napi-loader/napiLoader.bat +++ b/packages/napcat-napi-loader/napiLoader.bat @@ -1,8 +1,10 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 -set NAPCAT_INJECT_PATH=%cd%\napiloader.dll -set NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe -set NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs +set "NAPCAT_INJECT_PATH=%cd%\napiloader.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe" +set "NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs" :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( set "RetString=%%~b" @@ -24,4 +26,4 @@ if not exist "%QQpath%" ( set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% -start "" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%" \ No newline at end of file +start "" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%" diff --git a/packages/napcat-shell-loader/NapCatWinBootMain.exe b/packages/napcat-shell-loader/NapCatWinBootMain.exe index 99a69b9e..4e930e16 100644 Binary files a/packages/napcat-shell-loader/NapCatWinBootMain.exe and b/packages/napcat-shell-loader/NapCatWinBootMain.exe differ diff --git a/packages/napcat-shell-loader/launcher-user.bat b/packages/napcat-shell-loader/launcher-user.bat index f6ed9d22..fbf28945 100644 --- a/packages/napcat-shell-loader/launcher-user.bat +++ b/packages/napcat-shell-loader/launcher-user.bat @@ -1,10 +1,12 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 >nul -set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json -set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js -set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll -set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe -set NAPCAT_MAIN_PATH=%cd%\napcat.mjs +set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json" +set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js" +set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe" +set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs" :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( set "RetString=%%~b" @@ -24,9 +26,6 @@ if not exist "%QQpath%" ( exit /b ) -set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% -echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" - "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %* pause diff --git a/packages/napcat-shell-loader/launcher-win10-user.bat b/packages/napcat-shell-loader/launcher-win10-user.bat index 6c615d12..abfad977 100644 --- a/packages/napcat-shell-loader/launcher-win10-user.bat +++ b/packages/napcat-shell-loader/launcher-win10-user.bat @@ -1,10 +1,12 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 >nul -set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json -set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js -set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll -set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe -set NAPCAT_MAIN_PATH=%cd%\napcat.mjs +set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json" +set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js" +set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe" +set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs" :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( set "RetString=%%~b" @@ -23,9 +25,6 @@ if not exist "%QQpath%" ( pause exit /b ) -set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% -echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" - "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %* REM Optional: -q for quick login, omit for QR code login diff --git a/packages/napcat-shell-loader/launcher-win10.bat b/packages/napcat-shell-loader/launcher-win10.bat index 271c6112..a56fcdb7 100644 --- a/packages/napcat-shell-loader/launcher-win10.bat +++ b/packages/napcat-shell-loader/launcher-win10.bat @@ -1,4 +1,6 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 >nul net session >nul 2>&1 if %ERRORLEVEL% == 0 ( @@ -9,11 +11,11 @@ if %ERRORLEVEL% == 0 ( exit ) -set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json -set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js -set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll -set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe -set NAPCAT_MAIN_PATH=%cd%\napcat.mjs +set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json" +set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js" +set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe" +set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs" :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( set "RetString=%%~b" @@ -32,9 +34,6 @@ if not exist "%QQPath%" ( pause exit /b ) -set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% -echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" - "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %* REM Optional: -q for quick login, omit for QR code login diff --git a/packages/napcat-shell-loader/launcher.bat b/packages/napcat-shell-loader/launcher.bat index be9dda08..17e30db8 100644 --- a/packages/napcat-shell-loader/launcher.bat +++ b/packages/napcat-shell-loader/launcher.bat @@ -1,4 +1,6 @@ @echo off +setlocal DisableDelayedExpansion +cd /d "%~dp0" || exit /b 1 chcp 65001 >nul net session >nul 2>&1 if %ERRORLEVEL% == 0 ( @@ -9,11 +11,11 @@ if %ERRORLEVEL% == 0 ( exit ) -set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json -set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js -set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll -set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe -set NAPCAT_MAIN_PATH=%cd%\napcat.mjs +set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json" +set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js" +set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll" +set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe" +set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs" :loop_read for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do ( set "RetString=%%~b" @@ -33,7 +35,4 @@ if not exist "%QQPath%" ( exit /b ) -set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/% -echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" - "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %* diff --git a/packages/napcat-shell-loader/loadNapCat.js b/packages/napcat-shell-loader/loadNapCat.js index 25e1ed19..f3ee9d25 100644 --- a/packages/napcat-shell-loader/loadNapCat.js +++ b/packages/napcat-shell-loader/loadNapCat.js @@ -1,5 +1,6 @@ const path = require('path'); -const CurrentPath = path.dirname(__filename); +const { pathToFileURL } = require('url'); +const mainPath = process.env.NAPCAT_MAIN_PATH || path.join(__dirname, 'napcat.mjs'); (async () => { - await import('file://' + path.join(CurrentPath, './napcat/napcat.mjs')); + await import(pathToFileURL(mainPath).href); })(); diff --git a/packages/napcat-test/loaderPaths.test.ts b/packages/napcat-test/loaderPaths.test.ts new file mode 100644 index 00000000..051fdbbd --- /dev/null +++ b/packages/napcat-test/loaderPaths.test.ts @@ -0,0 +1,96 @@ +import { afterEach, describe, expect, test } from 'vitest'; +import { copyFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageRoot = fileURLToPath(new URL('../', import.meta.url)); +const fixtureRoots: string[] = []; + +function createFixture () { + const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'napcat-loader-')); + fixtureRoots.push(fixtureRoot); + const installDir = path.join(fixtureRoot, '中文 space # percent%'); + mkdirSync(installDir); + return { fixtureRoot, installDir }; +} + +afterEach(() => { + for (const fixtureRoot of fixtureRoots.splice(0)) { + rmSync(fixtureRoot, { recursive: true, force: true }); + } +}); + +describe('loader paths', () => { + test('Shell loads the flat package from an unrelated working directory', () => { + const { fixtureRoot, installDir } = createFixture(); + const loaderPath = path.join(installDir, 'loadNapCat.cjs'); + copyFileSync(path.join(packageRoot, 'napcat-shell-loader/loadNapCat.js'), loaderPath); + writeFileSync(path.join(installDir, 'napcat.mjs'), 'console.log("SHELL_LOADED");'); + const environment = { ...process.env }; + delete environment['NAPCAT_MAIN_PATH']; + const output = execFileSync(process.execPath, [loaderPath], { cwd: fixtureRoot, env: environment, encoding: 'utf8' }); + expect(output).toContain('SHELL_LOADED'); + }); + + test('Shell uses the actual entry path when the loader is redirected into QQ', () => { + const { fixtureRoot, installDir } = createFixture(); + const loaderPath = path.join(fixtureRoot, 'loadNapCat.cjs'); + const entryPath = path.join(installDir, 'napcat.mjs'); + copyFileSync(path.join(packageRoot, 'napcat-shell-loader/loadNapCat.js'), loaderPath); + writeFileSync(entryPath, 'console.log("REDIRECTED_SHELL_LOADED");'); + const output = execFileSync(process.execPath, [loaderPath], { + cwd: fixtureRoot, + env: { ...process.env, NAPCAT_MAIN_PATH: entryPath }, + encoding: 'utf8', + }); + expect(output).toContain('REDIRECTED_SHELL_LOADED'); + }); + + test('Framework native loader preserves URL-sensitive path characters', () => { + const { fixtureRoot, installDir } = createFixture(); + const loaderPath = path.join(installDir, 'nativeLoader.cjs'); + copyFileSync(path.join(packageRoot, 'napcat-framework/nativeLoader.cjs'), loaderPath); + writeFileSync(path.join(installDir, 'napcat.mjs'), 'export async function NCoreInitFramework () { console.log("FRAMEWORK_LOADED"); }'); + const output = execFileSync(process.execPath, ['-e', 'require(process.argv[1]).initializeNapCat({}, {}, () => {});', loaderPath], { + cwd: fixtureRoot, + encoding: 'utf8', + }); + expect(output).toContain('FRAMEWORK_LOADED'); + expect(output).not.toContain('[Error]'); + }); + + test('Framework dlopen loader preserves URL-sensitive path characters', () => { + const { fixtureRoot, installDir } = createFixture(); + const loaderPath = path.join(installDir, 'napcat.cjs'); + copyFileSync(path.join(packageRoot, 'napcat-framework/napcat.cjs'), loaderPath); + writeFileSync(path.join(installDir, 'napcat.mjs'), 'export async function NCoreInitFramework () { console.log("DLOPEN_FRAMEWORK_LOADED"); } export function getWebUiUrl () { return ""; }'); + const script = 'process.dlopen = (module) => { module.exports = { NodeIKernelLoginService: { get: () => ({}) }, NodeIQQNTWrapperSession: { create: () => ({}) } }; }; require(process.argv[1]); process.dlopen({ exports: {} }, "wrapper.node");'; + const output = execFileSync(process.execPath, ['-e', script, loaderPath], { cwd: fixtureRoot, encoding: 'utf8' }); + expect(output).toContain('DLOPEN_FRAMEWORK_LOADED'); + expect(output).not.toContain('[Error]'); + }); + + test.skipIf(process.platform !== 'win32')('Node batch uses the bundled runtime, forwards arguments and returns the exit code', () => { + const { fixtureRoot, installDir } = createFixture(); + copyFileSync(process.execPath, path.join(installDir, 'node.exe')); + copyFileSync(path.join(packageRoot, 'napcat-develop/napcat.bat'), path.join(installDir, 'napcat.bat')); + writeFileSync(path.join(installDir, 'index.js'), 'console.log(JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) })); process.exitCode = 7;'); + let exitStatus: number | null = null; + let output = ''; + try { + output = execFileSync('cmd.exe', ['/d', '/s', '/c', `""${path.join(installDir, 'napcat.bat')}" -q 123456"`], { + cwd: fixtureRoot, + encoding: 'utf8', + windowsVerbatimArguments: true, + }); + } catch (error) { + const result = error as { status: number; stdout: string }; + exitStatus = result.status; + output = result.stdout; + } + expect(exitStatus).toBe(7); + expect(JSON.parse(output)).toEqual({ cwd: installDir, args: ['-q', '123456'] }); + }); +});