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
25 changes: 25 additions & 0 deletions .github/workflows/algolia-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
135 changes: 135 additions & 0 deletions scripts/algolia-index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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));
Expand Down
Loading