fix: repair Windows release dependencies and loader paths

Bundle required crypto and SSL DLLs and gate releases on a Windows native-load smoke test. Resolve loader paths independently of the working directory and encode file URLs correctly. Include the quick-login launcher built from Windows-Boot 0ff70ce10e34ac26411f9e6bcedaedfacc1e74f0. Validated builds, five path regressions, and live Shell and Windows Node startup to QR login.
This commit is contained in:
手瓜一十雪
2026-10-06 17:51:21 +08:00
parent 297133a1b1
commit 9109b8cfe2
13 changed files with 174 additions and 48 deletions
+26 -3
View File
@@ -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
+5 -1
View File
@@ -1 +1,5 @@
node.exe ./index.js
@echo off
setlocal
cd /d "%~dp0" || exit /b 1
"%~dp0node.exe" "%~dp0index.js" %*
exit /b %errorlevel%
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -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);
@@ -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
pause
+6 -4
View File
@@ -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%"
start "" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%"
Binary file not shown.
@@ -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
@@ -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 <QQ_NUMBER> for quick login, omit for QR code login
@@ -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 <QQ_NUMBER> for quick login, omit for QR code login
+7 -8
View File
@@ -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%" %*
+3 -2
View File
@@ -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);
})();
+96
View File
@@ -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'] });
});
});