From f6cecc951190baf49dac862e5f6fbc67051e9031 Mon Sep 17 00:00:00 2001 From: yang12535 Date: Wed, 3 Jun 2026 20:29:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(scripts/check-links):=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E8=B5=84=E6=BA=90fallback=20+=20=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=20+=20--warn-external=20=E5=9B=BD=E5=86=85?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/kimi-cli-install-win.md | 2 +- content/posts/win-restore-workflow.md | 5 +++ package.json | 2 +- scripts/check-links.js | 58 ++++++++++++++++++++++++--- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/content/posts/kimi-cli-install-win.md b/content/posts/kimi-cli-install-win.md index 8c92609..8856572 100644 --- a/content/posts/kimi-cli-install-win.md +++ b/content/posts/kimi-cli-install-win.md @@ -280,7 +280,7 @@ Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python.exe" -ErrorAction Si --- -> 如果还有问题,欢迎在评论区留言,或在 [GitHub](https://github.com/your-repo) 提 Issue。 +> 如果还有问题,欢迎在评论区留言,或在 [GitHub](https://github.com/yang12535/blog) 提 Issue。 --- diff --git a/content/posts/win-restore-workflow.md b/content/posts/win-restore-workflow.md index 9745b32..13af586 100644 --- a/content/posts/win-restore-workflow.md +++ b/content/posts/win-restore-workflow.md @@ -207,6 +207,11 @@ if (-not (Test-Path $nodeExe)) { Write-Ok "Node.js 已存在,跳过安装" } +# 关键:Node.js MSI 安装后注册表 PATH 在当前会话不生效,必须立即显式添加 +# 否则后续 Bun 安装(install.js 内部调用 node)会因找不到 node 而失败 +Add-ToPath -Dir $nodeDir +Write-Ok "已确保 Node.js 目录在当前会话 PATH 中" + # 显式定位 npm.cmd(绕过 PS5.1 Restricted 执行策略) $npmCmd = Get-Command "$nodeDir\npm.cmd" -ErrorAction SilentlyContinue if (-not $npmCmd) { diff --git a/package.json b/package.json index 20d689f..fe07f74 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:coverage": "jest --coverage", "prepare": "husky", "check:frontmatter": "node scripts/validate-frontmatter.js", - "check:links": "node scripts/check-links.js", + "check:links": "node scripts/check-links.js --warn-external", "check:bom": "node scripts/check-bom.js", "check:all": "npm run check:frontmatter && npm run check:links && npm run check:bom" }, diff --git a/scripts/check-links.js b/scripts/check-links.js index df6ed9a..d9c3190 100644 --- a/scripts/check-links.js +++ b/scripts/check-links.js @@ -65,11 +65,11 @@ async function checkExternalLinks(links, concurrency = 5, timeoutMs = 10000) { return results; } -function checkSingleUrl(url, timeoutMs) { +function checkSingleUrl(url, timeoutMs, retries = 2) { return new Promise((resolve) => { const client = url.startsWith('https:') ? https : http; - function makeRequest(method) { + function makeRequest(method, attempt = 0) { const req = client.request(url, { method, timeout: timeoutMs }, (res) => { const status = res.statusCode; // 跟随重定向 @@ -82,7 +82,7 @@ function checkSingleUrl(url, timeoutMs) { // 某些服务器不支持 HEAD,降级到 GET if ((status === 403 || status === 405) && method === 'HEAD') { res.resume(); - makeRequest('GET'); + makeRequest('GET', attempt); return; } if (status >= 200 && status < 400) { @@ -95,12 +95,20 @@ function checkSingleUrl(url, timeoutMs) { }); req.on('timeout', () => { req.destroy(); - resolve({ status: 'timeout', error: '请求超时' }); + if (attempt < retries) { + setTimeout(() => makeRequest(method, attempt + 1), 500); + } else { + resolve({ status: 'timeout', error: '请求超时' }); + } }); req.on('error', (err) => { if (method === 'HEAD') { // HEAD 请求失败,尝试 GET - makeRequest('GET'); + makeRequest('GET', attempt); + return; + } + if (attempt < retries) { + setTimeout(() => makeRequest(method, attempt + 1), 500); } else { resolve({ status: 'error', error: err.message }); } @@ -137,6 +145,13 @@ function resolveRelativeLink(url, baseFile) { if (!resolved.startsWith(rootDir + path.sep)) { return null; } + // 构建资源fallback:如果根目录不存在,尝试 src/ 目录(src/assets 会在构建时复制到 dist/assets) + if (!fs.existsSync(resolved) && clean.startsWith('/assets/')) { + const srcResolved = path.resolve(rootDir, 'src', clean.slice(1)); + if (srcResolved.startsWith(rootDir + path.sep) && fs.existsSync(srcResolved)) { + return srcResolved; + } + } return resolved; } if (clean.startsWith('./') || clean.startsWith('../')) { @@ -146,6 +161,17 @@ function resolveRelativeLink(url, baseFile) { if (!resolved.startsWith(rootDir + path.sep)) { return null; } + // 构建资源fallback:如果从 content/posts 解析的 assets 路径不存在,尝试 src/ 目录 + if (!fs.existsSync(resolved) && clean.includes('assets/')) { + // 更通用的方式:把相对路径中指向 assets 的部分映射到 src/assets + const assetsMatch = clean.match(/(?:\.\.\/)*assets\/.*$/); + if (assetsMatch) { + const srcFallback = path.resolve(rootDir, 'src', assetsMatch[0].replace(/^(\.\.\/)+/, '')); + if (fs.existsSync(srcFallback)) { + return srcFallback; + } + } + } return resolved; } return null; @@ -160,7 +186,12 @@ function findLineNumber(content, url) { } async function main() { + const warnExternal = process.argv.includes('--warn-external'); + log(`${COLORS.cyan}🔗 链接检查工具${COLORS.reset}`); + if (warnExternal) { + log(`${COLORS.yellow}⚠️ 外部链接超时/重置将降级为警告(国内网络环境适配)${COLORS.reset}`); + } log(''); if (!fs.existsSync(POSTS_DIR)) { @@ -214,6 +245,7 @@ async function main() { // 检查外部链接 const brokenExternal = []; + const warnExternalLinks = []; const okExternal = []; if (externalLinks.length > 0) { log(`${COLORS.cyan}🌐 正在检查外部链接...${COLORS.reset}`); @@ -223,6 +255,8 @@ async function main() { okExternal.push(r); } else if (r.status === 'redirect') { okExternal.push(r); // 重定向视为可用 + } else if (warnExternal && (r.error === '请求超时' || r.error?.includes('ECONNRESET') || r.error?.includes('ETIMEDOUT') || r.error?.includes('ENOTFOUND'))) { + warnExternalLinks.push(r); // 网络层错误降级为警告 } else { brokenExternal.push(r); } @@ -264,6 +298,13 @@ async function main() { log(` ${COLORS.green}✔${COLORS.reset} ${link.file}:${link.line} → ${link.url}${COLORS.gray}${statusStr}${COLORS.reset}`); } } + if (warnExternalLinks.length > 0) { + log(` ${COLORS.yellow}⚠️ 网络不稳定/超时 (${warnExternalLinks.length})${COLORS.reset}`); + for (const link of warnExternalLinks) { + log(` ${COLORS.yellow}⚠${COLORS.reset} ${link.file}:${link.line} → ${link.url}`); + log(` ${COLORS.yellow}原因: ${link.error}${COLORS.reset}`); + } + } if (brokenExternal.length > 0) { hasError = true; log(` ${COLORS.red}❌ 不可访问 (${brokenExternal.length})${COLORS.reset}`); @@ -279,12 +320,17 @@ async function main() { const totalBroken = brokenRelative.length + brokenExternal.length; log(`${COLORS.cyan}📊 汇总${COLORS.reset}`); log(` 相对链接: ${relativeLinks.length} (${COLORS.green}${okRelative.length} 有效${COLORS.reset}${brokenRelative.length > 0 ? `, ${COLORS.red}${brokenRelative.length} 断链${COLORS.reset}` : ''})`); - log(` 外部链接: ${externalLinks.length} (${COLORS.green}${okExternal.length} 可访问${COLORS.reset}${brokenExternal.length > 0 ? `, ${COLORS.red}${brokenExternal.length} 不可访问${COLORS.reset}` : ''})`); + const externalWarnStr = warnExternalLinks.length > 0 ? `, ${COLORS.yellow}${warnExternalLinks.length} 超时/重置${COLORS.reset}` : ''; + const externalBrokenStr = brokenExternal.length > 0 ? `, ${COLORS.red}${brokenExternal.length} 不可访问${COLORS.reset}` : ''; + log(` 外部链接: ${externalLinks.length} (${COLORS.green}${okExternal.length} 可访问${COLORS.reset}${externalWarnStr}${externalBrokenStr})`); log(''); if (hasError) { log(`${COLORS.red}❌ 发现 ${totalBroken} 个断链,请修复${COLORS.reset}`); process.exit(1); + } else if (warnExternalLinks.length > 0) { + log(`${COLORS.green}🎉 所有链接检查通过!${COLORS.reset} ${COLORS.yellow}(注意: ${warnExternalLinks.length} 个外部链接因网络不稳定未确认)${COLORS.reset}`); + process.exit(0); } else { log(`${COLORS.green}🎉 所有链接检查通过!${COLORS.reset}`); process.exit(0); From a1e6ae44309a6ca8422289778dbe9cfb1778cf58 Mon Sep 17 00:00:00 2001 From: yang12535 Date: Wed, 3 Jun 2026 20:37:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E6=A0=B9=E6=8D=AE=20Copilot=20?= =?UTF-8?q?review=20=E4=BF=AE=E5=A4=8D=E8=B6=85=E6=97=B6=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=20resolve=E3=80=81err.code=20=E4=BC=A0=E9=80=92=E3=80=81?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=BE=B9=E7=95=8C=E6=A3=80=E6=9F=A5=E3=80=81?= =?UTF-8?q?PATH=20=E5=89=8D=E7=BD=AE=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/win-restore-workflow.md | 8 ++++++-- scripts/check-links.js | 12 ++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/content/posts/win-restore-workflow.md b/content/posts/win-restore-workflow.md index 13af586..e503ed4 100644 --- a/content/posts/win-restore-workflow.md +++ b/content/posts/win-restore-workflow.md @@ -209,8 +209,12 @@ if (-not (Test-Path $nodeExe)) { # 关键:Node.js MSI 安装后注册表 PATH 在当前会话不生效,必须立即显式添加 # 否则后续 Bun 安装(install.js 内部调用 node)会因找不到 node 而失败 -Add-ToPath -Dir $nodeDir -Write-Ok "已确保 Node.js 目录在当前会话 PATH 中" +if (Test-Path $nodeDir) { + Add-ToPath -Dir $nodeDir + Write-Ok "已确保 Node.js 目录在当前会话 PATH 中" +} else { + Write-Warn "Node.js 目录不存在,跳过 PATH 添加: $nodeDir" +} # 显式定位 npm.cmd(绕过 PS5.1 Restricted 执行策略) $npmCmd = Get-Command "$nodeDir\npm.cmd" -ErrorAction SilentlyContinue diff --git a/scripts/check-links.js b/scripts/check-links.js index d9c3190..c86abb7 100644 --- a/scripts/check-links.js +++ b/scripts/check-links.js @@ -70,12 +70,14 @@ function checkSingleUrl(url, timeoutMs, retries = 2) { const client = url.startsWith('https:') ? https : http; function makeRequest(method, attempt = 0) { + let settled = false; const req = client.request(url, { method, timeout: timeoutMs }, (res) => { const status = res.statusCode; // 跟随重定向 if (status >= 300 && status < 400 && res.headers.location) { const redirectUrl = new URL(res.headers.location, url).toString(); res.resume(); + settled = true; resolve({ status: 'redirect', redirectTo: redirectUrl, finalStatus: status }); return; } @@ -87,14 +89,18 @@ function checkSingleUrl(url, timeoutMs, retries = 2) { } if (status >= 200 && status < 400) { res.resume(); + settled = true; resolve({ status: 'ok', finalStatus: status }); } else { res.resume(); + settled = true; resolve({ status: 'error', finalStatus: status, error: `HTTP ${status}` }); } }); req.on('timeout', () => { req.destroy(); + if (settled) return; + settled = true; if (attempt < retries) { setTimeout(() => makeRequest(method, attempt + 1), 500); } else { @@ -102,6 +108,8 @@ function checkSingleUrl(url, timeoutMs, retries = 2) { } }); req.on('error', (err) => { + if (settled) return; + settled = true; if (method === 'HEAD') { // HEAD 请求失败,尝试 GET makeRequest('GET', attempt); @@ -110,7 +118,7 @@ function checkSingleUrl(url, timeoutMs, retries = 2) { if (attempt < retries) { setTimeout(() => makeRequest(method, attempt + 1), 500); } else { - resolve({ status: 'error', error: err.message }); + resolve({ status: 'error', error: `[${err.code || 'UNKNOWN'}] ${err.message}` }); } }); req.end(); @@ -167,7 +175,7 @@ function resolveRelativeLink(url, baseFile) { const assetsMatch = clean.match(/(?:\.\.\/)*assets\/.*$/); if (assetsMatch) { const srcFallback = path.resolve(rootDir, 'src', assetsMatch[0].replace(/^(\.\.\/)+/, '')); - if (fs.existsSync(srcFallback)) { + if (srcFallback.startsWith(rootDir + path.sep) && fs.existsSync(srcFallback)) { return srcFallback; } }