123 lines
6.2 KiB
JavaScript
123 lines
6.2 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const http = require("node:http");
|
||
|
|
const path = require("node:path");
|
||
|
|
|
||
|
|
const DEFAULT_MAP = path.resolve(__dirname, "../../routing/repository-route-map.json");
|
||
|
|
|
||
|
|
function loadMap(filename = process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP) {
|
||
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalize(value) {
|
||
|
|
return String(value || "").toLowerCase().replace(/[\s·._/-]+/g, " ").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function search(map, query) {
|
||
|
|
const terms = normalize(query).split(" ").filter(Boolean);
|
||
|
|
if (!terms.length) return map.repositories;
|
||
|
|
return map.repositories
|
||
|
|
.map(repository => {
|
||
|
|
const haystack = normalize([
|
||
|
|
repository.code, repository.slug, repository.name_zh, repository.role,
|
||
|
|
repository.state, ...(repository.keywords || []),
|
||
|
|
].join(" "));
|
||
|
|
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
|
||
|
|
return { repository, score };
|
||
|
|
})
|
||
|
|
.filter(item => item.score > 0)
|
||
|
|
.sort((a, b) => b.score - a.score || a.repository.code.localeCompare(b.repository.code))
|
||
|
|
.map(item => item.repository);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createServer(options = {}) {
|
||
|
|
const mapFile = options.mapFile || process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP;
|
||
|
|
return http.createServer((req, res) => {
|
||
|
|
const url = new URL(req.url, "http://localhost");
|
||
|
|
if (req.method !== "GET") return json(res, 405, { error: "method_not_allowed" });
|
||
|
|
if (url.pathname === "/health") return json(res, 200, { ok: true, service: "guanghu-ai-discovery", mode: "read-only" });
|
||
|
|
|
||
|
|
let map;
|
||
|
|
try { map = loadMap(mapFile); } catch { return json(res, 503, { error: "route_map_unavailable" }); }
|
||
|
|
|
||
|
|
if (url.pathname === "/" || url.pathname === "/index.html") return html(res, entryPage(map));
|
||
|
|
if (url.pathname === "/v1/repositories" || url.pathname === "/v1/manifest") return json(res, 200, map, 300);
|
||
|
|
if (url.pathname === "/v1/search") {
|
||
|
|
const query = String(url.searchParams.get("q") || "").slice(0, 200);
|
||
|
|
const results = search(map, query);
|
||
|
|
return json(res, 200, {
|
||
|
|
schema: "guanghu.ai-search-response/v1",
|
||
|
|
query,
|
||
|
|
map_id: map.map_id,
|
||
|
|
map_version: map.version,
|
||
|
|
count: results.length,
|
||
|
|
results,
|
||
|
|
}, 60);
|
||
|
|
}
|
||
|
|
if (url.pathname === "/v1/resolve") {
|
||
|
|
const id = String(url.searchParams.get("id") || "").toUpperCase();
|
||
|
|
const repository = map.repositories.find(item => item.code === id || item.slug.toUpperCase() === id);
|
||
|
|
return repository ? json(res, 200, repository, 300) : json(res, 404, { error: "route_not_found", id });
|
||
|
|
}
|
||
|
|
if (url.pathname === "/openapi.json") return json(res, 200, openApi(), 3600);
|
||
|
|
if (url.pathname === "/well-known") return json(res, 200, {
|
||
|
|
schema: "guanghu.ai-discovery/v1",
|
||
|
|
name: "光湖语言世界 · 第五域",
|
||
|
|
canonical_repository: map.repositories[0].primary.url,
|
||
|
|
repository_map: map.canonical_api,
|
||
|
|
search_api: "https://guanghulab.com/api/ai/v1/search?q={query}",
|
||
|
|
resolve_api: "https://guanghulab.com/api/ai/v1/resolve?id={REPO_CODE}",
|
||
|
|
openapi: "https://guanghulab.com/api/ai/openapi.json",
|
||
|
|
access: "public-read-only"
|
||
|
|
}, 3600);
|
||
|
|
return json(res, 404, { error: "not_found" });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function json(res, status, body, maxAge = 0) {
|
||
|
|
res.writeHead(status, {
|
||
|
|
"content-type": "application/json; charset=utf-8",
|
||
|
|
"cache-control": maxAge ? `public, max-age=${maxAge}` : "no-store",
|
||
|
|
"access-control-allow-origin": "*",
|
||
|
|
"x-content-type-options": "nosniff",
|
||
|
|
});
|
||
|
|
res.end(JSON.stringify(body, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
function html(res, body) {
|
||
|
|
res.writeHead(200, {
|
||
|
|
"content-type": "text/html; charset=utf-8",
|
||
|
|
"cache-control": "public, max-age=300",
|
||
|
|
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
||
|
|
"x-content-type-options": "nosniff",
|
||
|
|
});
|
||
|
|
res.end(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
function entryPage(map) {
|
||
|
|
const rows = map.repositories.map(item => `<li><a href="${item.primary.url}">${item.code} · ${item.name_zh}</a><small>${item.state}</small></li>`).join("");
|
||
|
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>光湖语言世界 · AI API 入口</title><meta name="description" content="光湖语言世界第五域公开只读编号检索 API"></head><body><main><p>GUANGHU AI DISCOVERY</p><h1>光湖语言世界 · 第五域</h1><p>AI 请先读取仓库编号地图,再按 REPO 编号解析国内主路径。新加坡地址仅为历史备用。</p><nav><a href="v1/repositories">仓库编号地图</a> · <a href="v1/search?q=光湖语言世界%20第五域">示例检索</a> · <a href="openapi.json">OpenAPI</a></nav><ul>${rows}</ul></main><style>:root{color-scheme:dark}body{margin:0;background:#061416;color:#dff7f1;font:17px/1.7 system-ui;padding:6vw}main{max-width:900px;margin:auto}h1{font-size:clamp(36px,7vw,72px)}a{color:#79dfc8}li{margin:14px 0;padding:16px;border:1px solid #28534c;border-radius:12px;display:flex;justify-content:space-between}small{color:#8fb5ad}</style></body></html>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function openApi() {
|
||
|
|
return {
|
||
|
|
openapi: "3.1.0",
|
||
|
|
info: { title: "光湖语言世界 · 第五域 AI Discovery API", version: "1.0.0" },
|
||
|
|
servers: [{ url: "https://guanghulab.com/api/ai" }],
|
||
|
|
paths: {
|
||
|
|
"/v1/repositories": { get: { summary: "读取最新仓库编号路径映射", responses: { "200": { description: "Repository route map" } } } },
|
||
|
|
"/v1/search": { get: { summary: "按中文、编号或项目名检索", parameters: [{ name: "q", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Search results" } } } },
|
||
|
|
"/v1/resolve": { get: { summary: "解析 REPO 编号", parameters: [{ name: "id", in: "query", required: true, schema: { type: "string", example: "REPO-001" } }], responses: { "200": { description: "Resolved repository" }, "404": { description: "Unknown route" } } } }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
if (require.main === module) {
|
||
|
|
const host = process.env.GUANGHU_AI_HOST || "127.0.0.1";
|
||
|
|
const port = Number(process.env.GUANGHU_AI_PORT || 3922);
|
||
|
|
createServer().listen(port, host, () => process.stdout.write(`guanghu-ai-discovery listening on ${host}:${port}\n`));
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { createServer, loadMap, search };
|