diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml index 2e5e2a1..30779a6 100644 --- a/.github/workflows/algolia-index.yml +++ b/.github/workflows/algolia-index.yml @@ -46,3 +46,28 @@ jobs: exit 1 fi npm run algolia:index + + - name: Setup ossutil v2 + run: | + if [ ! -x "$HOME/ossutil" ]; then + curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.2/ossutil-2.1.2-linux-amd64.zip + unzip ossutil.zip + cd ossutil-2.1.2-linux-amd64 + chmod 755 ossutil + mv ossutil "$HOME/ossutil" + fi + + # 覆盖式单文件上传:覆盖 OSS 上残留的旧 search-index.json(8 月过期产物)。 + # 不能带 -r / --update(那只会增不删),必须覆盖根下同名文件。 + - name: Upload easyops search-index.json to OSS + env: + OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} + OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} + run: | + ~/ossutil cp scripts/_algolia_tmp/search-index.json \ + oss://cn-rdk-front-prod/case_doc/search-index.json \ + -e https://d-robotics.cc \ + -i $OSS_ACCESS_KEY_ID \ + -k $OSS_ACCESS_KEY_SECRET \ + --region cn-beijing \ + --retry-times=3 diff --git a/scripts/algolia-index.mjs b/scripts/algolia-index.mjs index 74fd8dc..1445aa0 100644 --- a/scripts/algolia-index.mjs +++ b/scripts/algolia-index.mjs @@ -16,8 +16,14 @@ */ import "dotenv/config"; import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { load } from "cheerio"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, ".."); + const APP_ID = process.env.ALGOLIA_APP_ID || "1VU781LYTV"; const INDEX_NAME = process.env.ALGOLIA_INDEX_NAME || "case_doc"; const SITE_URL = ensureTrailingSlash( @@ -638,6 +644,124 @@ async function smokeQuery() { } } +function relativePathFromUrl(url) { + try { + return new URL(url).pathname; + } catch { + return url; + } +} + +function deepestHeadingText(hierarchy) { + for (const lvl of ["lvl6", "lvl5", "lvl4", "lvl3", "lvl2", "lvl1"]) { + const text = (hierarchy?.[lvl] || "").trim(); + if (text) { + return text; + } + } + return ""; +} + +/** + * 把 Algolia DocSearch records 映射成 easyops `search-index.json` 同构格式(5 shard), + * 供 rdk-docs-mcp 读取(仅 zh-Hans)。 + */ +function buildEasyopsIndex(records) { + const zh = records.filter((r) => r.language === "zh-Hans"); + const byPage = new Map(); + for (const r of zh) { + const pagePath = relativePathFromUrl(r.url_without_anchor); + if (!byPage.has(pagePath)) { + byPage.set(pagePath, []); + } + byPage.get(pagePath).push(r); + } + + const pages = []; + const headings = []; + const pageSnippets = []; + const contentSnippets = []; + let id = 0; + let pageSeq = 0; + + for (const [u, recs] of byPage) { + pageSeq += 1; + let pageTitle = ""; + for (const r of recs) { + if (r.type === "lvl1" && r.hierarchy?.lvl1) { + pageTitle = r.hierarchy.lvl1.trim(); + break; + } + } + const breadcrumbs = (recs[0]?.hierarchy?.lvl0 || "") + .split(" / ") + .map((s) => s.trim()) + .filter(Boolean); + if (!pageTitle) { + pageTitle = breadcrumbs[breadcrumbs.length - 1] || ""; + } + pages.push({ i: ++id, t: pageTitle, u, b: breadcrumbs }); + + let firstHeading = ""; + for (const r of recs) { + if ( + r.type && + r.type.startsWith("lvl") && + r.type !== "lvl1" && + r.hierarchy?.[r.type] + ) { + firstHeading = r.hierarchy[r.type].trim(); + break; + } + } + if (firstHeading) { + pageSnippets.push({ i: ++id, t: firstHeading, s: pageTitle, u, p: pageSeq }); + } + + const seenHeadings = new Set(); + const seenContent = new Set(); + for (const r of recs) { + const type = r.type; + if (type && type.startsWith("lvl") && type !== "lvl1") { + if (r.anchor && !seenHeadings.has(r.anchor)) { + seenHeadings.add(r.anchor); + headings.push({ + i: ++id, + t: (r.hierarchy?.[type] || "").trim(), + u, + h: `#${r.anchor}`, + p: pageSeq, + }); + } + } else if (type === "content" && r.content) { + const text = r.content.trim(); + const key = `${r.anchor || ""}\u0000${text}`; + if (seenContent.has(key)) { + continue; + } + seenContent.add(key); + const doc = { i: ++id, t: text, u, p: pageSeq }; + const source = deepestHeadingText(r.hierarchy); + if (source) { + doc.s = source; + } + if (r.anchor) { + doc.h = `#${r.anchor}`; + } + contentSnippets.push(doc); + } + } + } + + return [ + { documents: pages }, + { documents: headings }, + { documents: pageSnippets }, + { documents: [] }, + { documents: contentSnippets }, + ]; +} + async function main() { if (!ADMIN_API_KEY) { console.error( @@ -662,6 +786,17 @@ async function main() { process.exit(1); } + const easyopsIndex = buildEasyopsIndex(records); + const easyopsPath = path.join(rootDir, "scripts/_algolia_tmp/search-index.json"); + mkdirSync(path.dirname(easyopsPath), { recursive: true }); + writeFileSync(easyopsPath, JSON.stringify(easyopsIndex)); + console.log( + `Wrote easyops search-index.json ` + + `(${easyopsIndex[0].documents.length} pages, ` + + `${easyopsIndex[1].documents.length} headings, ` + + `${easyopsIndex[4].documents.length} content snippets) to ${easyopsPath}`, + ); + if (DRY_RUN) { const sample = records[0]; console.log("Dry-run sample record:", JSON.stringify(sample, null, 2));