feat(seo): 固化 m.tkmind.cn 百度验证、推送与 sitemap 去重
Memind CI / Test, build, and release guards (pull_request) Failing after 3m21s
Memind CI / Test, build, and release guards (pull_request) Failing after 3m21s
让 Portal 首页稳定注入站长验证 meta,并提供 sitemap 批量推送脚本与 LaunchAgent 安装入口;sitemap/llms 对同页优先保留 /u/ canonical URL。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WRAPPER="${MEMIND_BAIDU_PUSH_WRAPPER:-$ROOT/scripts/run-baidu-sitemap-push-prod.sh}"
|
||||
LABEL="${MEMIND_BAIDU_PUSH_LABEL:-cn.tkmind.memind-baidu-sitemap-push}"
|
||||
PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
|
||||
LOG="$HOME/Library/Logs/memind-baidu-sitemap-push.log"
|
||||
GUI="gui/$(id -u)"
|
||||
HOUR="${MEMIND_BAIDU_PUSH_HOUR:-1}"
|
||||
MINUTE="${MEMIND_BAIDU_PUSH_MINUTE:-0}"
|
||||
STATE_FILE="${BAIDU_PUSH_STATE_FILE:-$ROOT/data/baidu-push-state.json}"
|
||||
|
||||
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs" "$(dirname "$STATE_FILE")"
|
||||
|
||||
if [[ ! -x "$WRAPPER" ]]; then
|
||||
chmod +x "$WRAPPER"
|
||||
fi
|
||||
if [[ ! -f "$WRAPPER" ]]; then
|
||||
echo "baidu push wrapper not found: $WRAPPER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > "$PLIST" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>$LABEL</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>$WRAPPER</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$ROOT</string>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>$HOUR</integer>
|
||||
<key>Minute</key>
|
||||
<integer>$MINUTE</integer>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>$LOG</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$LOG</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
|
||||
<key>BAIDU_PUSH_STATE_FILE</key>
|
||||
<string>$STATE_FILE</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
plutil -lint "$PLIST"
|
||||
launchctl bootout "$GUI/$LABEL" 2>/dev/null || true
|
||||
launchctl bootstrap "$GUI" "$PLIST"
|
||||
launchctl enable "$GUI/$LABEL"
|
||||
|
||||
echo "installed $PLIST"
|
||||
echo "wrapper: $WRAPPER"
|
||||
echo "schedule: daily ${HOUR}:$(printf '%02d' "$MINUTE") (local time)"
|
||||
echo "state_file: $STATE_FILE"
|
||||
echo "log: $LOG"
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 从 sitemap 批量推送 URL 到百度(受每日配额限制)。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/push-baidu-sitemap.mjs
|
||||
* node scripts/push-baidu-sitemap.mjs --dry-run
|
||||
* node scripts/push-baidu-sitemap.mjs --sitemap https://m.tkmind.cn/sitemap.xml
|
||||
*
|
||||
* 环境变量:
|
||||
* MINDSPACE_BAIDU_SITE 默认 m.tkmind.cn
|
||||
* MINDSPACE_BAIDU_PUSH_TOKEN 必填
|
||||
* BAIDU_PUSH_STATE_FILE 默认 data/baidu-push-state.json
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
dryRun: false,
|
||||
sitemap: process.env.BAIDU_PUSH_SITEMAP ?? 'https://m.tkmind.cn/sitemap.xml',
|
||||
batchSize: Number(process.env.BAIDU_PUSH_BATCH_SIZE ?? 10),
|
||||
stateFile:
|
||||
process.env.BAIDU_PUSH_STATE_FILE ??
|
||||
path.join(ROOT, 'data', 'baidu-push-state.json'),
|
||||
};
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--dry-run') args.dryRun = true;
|
||||
else if (arg === '--sitemap') args.sitemap = argv[++i] ?? args.sitemap;
|
||||
else if (arg === '--batch-size') args.batchSize = Number(argv[++i] ?? args.batchSize);
|
||||
else if (arg === '--state-file') args.stateFile = argv[++i] ?? args.stateFile;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function loadState(stateFile) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
} catch {
|
||||
return { pushed: {}, lastRunAt: null, history: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function saveState(stateFile, state) {
|
||||
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
||||
fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function extractSitemapUrls(xml) {
|
||||
return [...String(xml ?? '').matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].trim());
|
||||
}
|
||||
|
||||
function isArchivedDiscoveryUrl(url) {
|
||||
return /\/_archived/i.test(String(url ?? ''));
|
||||
}
|
||||
|
||||
function prioritizeUrls(urls) {
|
||||
const filtered = urls.filter((url) => url && !isArchivedDiscoveryUrl(url));
|
||||
const uUrls = filtered.filter((url) => url.includes('/u/'));
|
||||
const mindspaceUrls = filtered.filter((url) => url.includes('/MindSpace/'));
|
||||
const otherUrls = filtered.filter(
|
||||
(url) => !url.includes('/u/') && !url.includes('/MindSpace/'),
|
||||
);
|
||||
const seen = new Set();
|
||||
const ordered = [];
|
||||
for (const url of [...uUrls, ...otherUrls, ...mindspaceUrls]) {
|
||||
if (!url || seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
ordered.push(url);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
async function fetchSitemap(url) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
||||
if (!response.ok) {
|
||||
throw new Error(`sitemap fetch failed: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async function pingBaidu({ site, token, urls }) {
|
||||
const endpoint = `http://data.zz.baidu.com/urls?site=${encodeURIComponent(site)}&token=${encodeURIComponent(token)}`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: urls.join('\n'),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const error = new Error(payload?.message || 'baidu_push_failed');
|
||||
error.code = payload?.error ?? response.status;
|
||||
error.details = payload;
|
||||
throw error;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const site = String(process.env.MINDSPACE_BAIDU_SITE ?? 'm.tkmind.cn').trim();
|
||||
const token = String(
|
||||
process.env.MINDSPACE_BAIDU_PUSH_TOKEN ??
|
||||
process.env.PLAZA_BAIDU_PUSH_TOKEN ??
|
||||
'',
|
||||
).trim();
|
||||
if (!token) {
|
||||
console.error('缺少 MINDSPACE_BAIDU_PUSH_TOKEN');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const xml = await fetchSitemap(args.sitemap);
|
||||
const allUrls = prioritizeUrls(extractSitemapUrls(xml));
|
||||
const state = loadState(args.stateFile);
|
||||
const pending = allUrls.filter((url) => !state.pushed[url]);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
site,
|
||||
sitemap: args.sitemap,
|
||||
totalInSitemap: allUrls.length,
|
||||
alreadyPushed: allUrls.length - pending.length,
|
||||
pending: pending.length,
|
||||
dryRun: args.dryRun,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
if (args.dryRun || pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pushedThisRun = 0;
|
||||
const runHistory = {
|
||||
at: new Date().toISOString(),
|
||||
attempted: 0,
|
||||
success: 0,
|
||||
batches: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < pending.length; i += args.batchSize) {
|
||||
const batch = pending.slice(i, i + args.batchSize);
|
||||
runHistory.attempted += batch.length;
|
||||
try {
|
||||
const result = await pingBaidu({ site, token, urls: batch });
|
||||
const successCount = Number(result.success ?? 0);
|
||||
runHistory.success += successCount;
|
||||
runHistory.batches.push({ size: batch.length, result });
|
||||
for (const url of batch.slice(0, successCount)) {
|
||||
state.pushed[url] = runHistory.at;
|
||||
pushedThisRun += 1;
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
batch: batch.length,
|
||||
success: successCount,
|
||||
remain: result.remain ?? null,
|
||||
notSameSite: result.not_same_site ?? null,
|
||||
notValid: result.not_valid ?? null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (successCount < batch.length) break;
|
||||
if (Number(result.remain ?? 0) <= 0) break;
|
||||
} catch (error) {
|
||||
runHistory.batches.push({
|
||||
size: batch.length,
|
||||
error: error.message,
|
||||
details: error.details ?? null,
|
||||
});
|
||||
console.error(JSON.stringify({ batch: batch.length, error: error.message, details: error.details ?? null }, null, 2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state.lastRunAt = runHistory.at;
|
||||
state.history = [...(state.history ?? []), runHistory].slice(-30);
|
||||
saveState(args.stateFile, state);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
pushedThisRun,
|
||||
totalTracked: Object.keys(state.pushed).length,
|
||||
pendingRemaining: allUrls.filter((url) => !state.pushed[url]).length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error?.stack || error?.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [[ -f "${ROOT}/.env" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "${ROOT}/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
if [[ ! -x "${NODE_BIN}" ]]; then
|
||||
NODE_BIN="$(command -v node)"
|
||||
fi
|
||||
|
||||
exec "${NODE_BIN}" "${ROOT}/scripts/push-baidu-sitemap.mjs" "$@"
|
||||
Reference in New Issue
Block a user