Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/posts/kimi-cli-install-win.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

---

Expand Down
9 changes: 9 additions & 0 deletions content/posts/win-restore-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ if (-not (Test-Path $nodeExe)) {
Write-Ok "Node.js 已存在,跳过安装"
}

# 关键:Node.js MSI 安装后注册表 PATH 在当前会话不生效,必须立即显式添加
# 否则后续 Bun 安装(install.js 内部调用 node)会因找不到 node 而失败
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
if (-not $npmCmd) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
68 changes: 61 additions & 7 deletions scripts/check-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,44 +65,60 @@ 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) {
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;
}
// 某些服务器不支持 HEAD,降级到 GET
if ((status === 403 || status === 405) && method === 'HEAD') {
res.resume();
makeRequest('GET');
makeRequest('GET', attempt);
return;
}
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();
resolve({ status: 'timeout', error: '请求超时' });
if (settled) return;
settled = true;
if (attempt < retries) {
setTimeout(() => makeRequest(method, attempt + 1), 500);
} else {
Comment on lines 100 to +106
resolve({ status: 'timeout', error: '请求超时' });
}
});
req.on('error', (err) => {
if (settled) return;
settled = true;
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 });
resolve({ status: 'error', error: `[${err.code || 'UNKNOWN'}] ${err.message}` });
}
});
req.end();
Expand Down Expand Up @@ -137,6 +153,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('../')) {
Expand All @@ -146,6 +169,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(/^(\.\.\/)+/, ''));
Comment on lines +173 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject asset relatives that won't resolve from post pages

This fallback maps any relative path containing assets/ to src/assets, so a post link such as ../assets/images/foo.png or ./assets/images/foo.png passes as soon as that file exists under src/assets. The generated post pages are written under dist/posts/<slug>/index.html while assets are copied to dist/assets, so only ../../assets/... (or /assets/...) resolves there in the browser; the shorter relative forms would publish as /posts/assets/... or /posts/<slug>/assets/... and 404 while the checker exits successfully.

Useful? React with 👍 / 👎.

if (srcFallback.startsWith(rootDir + path.sep) && fs.existsSync(srcFallback)) {
return srcFallback;
}
Comment on lines +177 to +180
}
}
return resolved;
}
return null;
Expand All @@ -160,7 +194,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)) {
Expand Down Expand Up @@ -214,6 +253,7 @@ async function main() {

// 检查外部链接
const brokenExternal = [];
const warnExternalLinks = [];
const okExternal = [];
if (externalLinks.length > 0) {
log(`${COLORS.cyan}🌐 正在检查外部链接...${COLORS.reset}`);
Expand All @@ -223,6 +263,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'))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not downgrade DNS failures by default

When npm run check:links now runs with --warn-external, this branch treats any ENOTFOUND as a warning and exits 0. That hides genuinely broken links whose host no longer exists or is misspelled, because Node reports those permanent DNS failures with the same code; HTTP 404s still fail, but a dead domain can now be published without failing check:all. Consider limiting the warning mode to transient errors or keeping DNS failures fatal unless this is an explicitly local-only invocation.

Useful? React with 👍 / 👎.

warnExternalLinks.push(r); // 网络层错误降级为警告
} else {
brokenExternal.push(r);
}
Expand Down Expand Up @@ -264,6 +306,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}`);
Expand All @@ -279,12 +328,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);
Expand Down
Loading