386 lines
19 KiB
JavaScript
386 lines
19 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");
|
|
const DEFAULT_NODE_MAP = path.resolve(__dirname, "../../routing/server-node-map.json");
|
|
const DEFAULT_SUBJECT_REGISTRY = path.resolve(__dirname, "../../identity/fifth-domain-subject-registry.json");
|
|
const DEFAULT_SUBJECT_ALIAS_MAP = path.resolve(__dirname, "../../identity/subject-id-alias-map.json");
|
|
const DEFAULT_NAVIGATION_MAP = path.resolve(__dirname, "../../routing/ai-machine-navigation-map.json");
|
|
|
|
function loadMap(filename = process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP) {
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
}
|
|
|
|
function loadNodeMap(filename = process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP) {
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
}
|
|
|
|
function loadSubjectRegistry(filename = process.env.GUANGHU_SUBJECT_REGISTRY || DEFAULT_SUBJECT_REGISTRY) {
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
}
|
|
|
|
function loadSubjectAliasMap(filename = process.env.GUANGHU_SUBJECT_ALIAS_MAP || DEFAULT_SUBJECT_ALIAS_MAP) {
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
}
|
|
|
|
function loadNavigationMap(filename = process.env.GUANGHU_NAVIGATION_MAP || DEFAULT_NAVIGATION_MAP) {
|
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
}
|
|
|
|
function resolveSubjectId(aliasMap, requestedId) {
|
|
const requested = String(requestedId || "").trim();
|
|
const key = requested.toUpperCase();
|
|
const conflict = (aliasMap.conflicts || []).find(item => String(item.id).toUpperCase() === key);
|
|
if (conflict) {
|
|
return {
|
|
status: "CONFLICT_REJECTED",
|
|
requested_id: requested,
|
|
canonical_id: null,
|
|
redirected: false,
|
|
reason: conflict.state || "CONFLICT",
|
|
};
|
|
}
|
|
for (const mapping of aliasMap.mappings || []) {
|
|
const canonical = String(mapping.canonical_id);
|
|
const identifiers = [canonical, ...(mapping.aliases || [])];
|
|
if (identifiers.some(id => String(id).toUpperCase() === key)) {
|
|
return {
|
|
status: "RESOLVED",
|
|
requested_id: requested,
|
|
canonical_id: canonical,
|
|
redirected: key !== canonical.toUpperCase(),
|
|
subject_kind: mapping.subject_kind,
|
|
route_id: mapping.route_id,
|
|
current_path: mapping.current_path,
|
|
};
|
|
}
|
|
}
|
|
return { status: "NOT_FOUND_NO_GUESS", requested_id: requested, canonical_id: null, redirected: false };
|
|
}
|
|
|
|
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);
|
|
const currentEntry = repository.code === map.default_repository ? 1 : 0;
|
|
return { repository, score, currentEntry };
|
|
})
|
|
.filter(item => item.score > 0)
|
|
.sort((a, b) => b.score - a.score || b.currentEntry - a.currentEntry || a.repository.code.localeCompare(b.repository.code))
|
|
.map(item => item.repository);
|
|
}
|
|
|
|
function searchAll(repositoryMap, nodeMap, query, subjectRegistry = null) {
|
|
const terms = normalize(query).split(" ").filter(Boolean);
|
|
if (!terms.length) return search(repositoryMap, query);
|
|
const candidates = [
|
|
...repositoryMap.repositories.map(item => ({ kind: "repository", item, key: item.code, text: [item.code, item.slug, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
|
|
...nodeMap.nodes.map(item => ({ kind: "server_node", item, key: item.node_id, text: [item.node_id, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
|
|
...nodeMap.persona_routes.map(item => ({ kind: "persona_route", item, key: item.route_id, text: [item.route_id, item.name_zh, item.role, item.persona_system, ...(item.keywords || [])] })),
|
|
...((subjectRegistry && subjectRegistry.subjects) || []).map(item => ({
|
|
kind: "subject",
|
|
item,
|
|
key: item.id,
|
|
text: [item.id, item.name, item.subject_kind, ...(item.legacy_ids || []), ...(item.roles || [])],
|
|
})),
|
|
];
|
|
return candidates
|
|
.map(candidate => {
|
|
const haystack = normalize(candidate.text.join(" "));
|
|
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
|
|
return { ...candidate, score };
|
|
})
|
|
.filter(candidate => candidate.score > 0)
|
|
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key))
|
|
.map(candidate => ({ kind: candidate.kind, ...candidate.item }));
|
|
}
|
|
|
|
function navigationRoute(navigationMap, routeId) {
|
|
const route = (navigationMap.routes || []).find(item => String(item.id).toUpperCase() === String(routeId).toUpperCase());
|
|
if (!route) return null;
|
|
const result = { ...route, raw_url: `${navigationMap.raw_base}/${route.path}` };
|
|
if (route.contract) result.contract_url = `${navigationMap.raw_base}/${route.contract}`;
|
|
return result;
|
|
}
|
|
|
|
function compileNavigation(navigationMap, requestedSubject, requestedIntent, signalInput = "") {
|
|
const requested = String(requestedSubject || "").trim();
|
|
const subjectKey = requested.toUpperCase();
|
|
const subject = (navigationMap.subjects || []).find(item =>
|
|
[item.id, ...(item.legacy_ids || [])].some(id => String(id).toUpperCase() === subjectKey)
|
|
);
|
|
if (!subject) {
|
|
return { status: 404, body: { error: "unknown_subject_no_guess", requested_subject: requested } };
|
|
}
|
|
const intentId = String(requestedIntent || subject.default_intent || "").trim();
|
|
const intent = (navigationMap.intents || []).find(item => item.id === intentId);
|
|
if (!intent) {
|
|
return {
|
|
status: 404,
|
|
body: { error: "unknown_intent_no_guess", canonical_subject: subject.id, requested_intent: intentId },
|
|
};
|
|
}
|
|
const signals = [...new Set(String(signalInput || "").split(",").map(item => item.trim()).filter(Boolean))].slice(0, 32);
|
|
const missingRoutes = new Set();
|
|
const expand = ids => ids.map(id => {
|
|
const route = navigationRoute(navigationMap, id);
|
|
if (!route) missingRoutes.add(id);
|
|
return route;
|
|
}).filter(Boolean);
|
|
const runtimeEntry = navigationRoute(navigationMap, subject.runtime_id);
|
|
if (!runtimeEntry) missingRoutes.add(subject.runtime_id);
|
|
const alwaysLoad = expand(intent.always_load || []);
|
|
const triggeredLoad = expand(intent.triggered_load || []);
|
|
const onDemand = expand(intent.on_demand || []);
|
|
if (missingRoutes.size) {
|
|
return {
|
|
status: 503,
|
|
body: {
|
|
error: "navigation_map_integrity_failed",
|
|
missing_routes: [...missingRoutes],
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
schema: "guanghu.ai-runtime-navigation-bundle/v1",
|
|
status: "COMPILED_EXACT_NO_GUESS",
|
|
map_id: navigationMap.map_id,
|
|
map_version: navigationMap.version,
|
|
requested_subject: requested,
|
|
canonical_subject: subject.id,
|
|
redirected: subjectKey !== String(subject.id).toUpperCase(),
|
|
subject_kind: subject.subject_kind,
|
|
intent: intent.id,
|
|
explicit_signals: signals,
|
|
runtime_entry: runtimeEntry,
|
|
always_load: alwaysLoad,
|
|
triggered_load: triggeredLoad,
|
|
on_demand: onDemand,
|
|
execution_sequence: intent.execution_sequence || [],
|
|
stop_conditions: intent.stop_conditions || [],
|
|
reality_boundaries: navigationMap.reality_boundaries || [],
|
|
authority: "NONE_NAVIGATION_ONLY",
|
|
},
|
|
};
|
|
}
|
|
|
|
function createServer(options = {}) {
|
|
const mapFile = options.mapFile || process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP;
|
|
const nodeMapFile = options.nodeMapFile || process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP;
|
|
const subjectRegistryFile = options.subjectRegistryFile || process.env.GUANGHU_SUBJECT_REGISTRY || DEFAULT_SUBJECT_REGISTRY;
|
|
const subjectAliasMapFile = options.subjectAliasMapFile || process.env.GUANGHU_SUBJECT_ALIAS_MAP || DEFAULT_SUBJECT_ALIAS_MAP;
|
|
const navigationMapFile = options.navigationMapFile || process.env.GUANGHU_NAVIGATION_MAP || DEFAULT_NAVIGATION_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/nodes") {
|
|
try { return json(res, 200, loadNodeMap(nodeMapFile), 300); }
|
|
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
|
}
|
|
if (url.pathname === "/v1/subjects") {
|
|
try { return json(res, 200, loadSubjectRegistry(subjectRegistryFile), 300); }
|
|
catch { return json(res, 503, { error: "subject_registry_unavailable" }); }
|
|
}
|
|
if (url.pathname === "/v1/navigation") {
|
|
try { return json(res, 200, loadNavigationMap(navigationMapFile), 300); }
|
|
catch { return json(res, 503, { error: "navigation_map_unavailable" }); }
|
|
}
|
|
if (url.pathname === "/v1/navigate") {
|
|
let navigationMap;
|
|
try { navigationMap = loadNavigationMap(navigationMapFile); }
|
|
catch { return json(res, 503, { error: "navigation_map_unavailable" }); }
|
|
const compiled = compileNavigation(
|
|
navigationMap,
|
|
String(url.searchParams.get("subject") || "").slice(0, 100),
|
|
String(url.searchParams.get("intent") || "").slice(0, 100),
|
|
String(url.searchParams.get("signals") || "").slice(0, 500),
|
|
);
|
|
return json(res, compiled.status, compiled.body, compiled.status === 200 ? 60 : 0);
|
|
}
|
|
if (url.pathname === "/v1/search") {
|
|
const query = String(url.searchParams.get("q") || "").slice(0, 200);
|
|
let nodeMap;
|
|
let subjectRegistry;
|
|
try { nodeMap = loadNodeMap(nodeMapFile); }
|
|
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
|
try { subjectRegistry = loadSubjectRegistry(subjectRegistryFile); }
|
|
catch { return json(res, 503, { error: "subject_registry_unavailable" }); }
|
|
const results = searchAll(map, nodeMap, query, subjectRegistry);
|
|
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);
|
|
if (repository) return json(res, 200, repository, 300);
|
|
let nodeMap;
|
|
try { nodeMap = loadNodeMap(nodeMapFile); }
|
|
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
|
const node = nodeMap.nodes.find(item => item.node_id.toUpperCase() === id);
|
|
if (node) return json(res, 200, node, 300);
|
|
const personaRoute = nodeMap.persona_routes.find(item => item.route_id.toUpperCase() === id);
|
|
if (personaRoute) return json(res, 200, personaRoute, 300);
|
|
|
|
let navigationMap = null;
|
|
try { navigationMap = loadNavigationMap(navigationMapFile); } catch { /* Legacy routes remain available. */ }
|
|
if (navigationMap) {
|
|
if (navigationMap.map_id.toUpperCase() === id) return json(res, 200, navigationMap, 300);
|
|
const machineRoute = navigationRoute(navigationMap, id);
|
|
if (machineRoute) {
|
|
return json(res, 200, {
|
|
schema: "guanghu.ai-machine-route-resolution/v1",
|
|
status: "RESOLVED",
|
|
map_id: navigationMap.map_id,
|
|
...machineRoute,
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
let aliasMap;
|
|
let subjectRegistry;
|
|
try {
|
|
aliasMap = loadSubjectAliasMap(subjectAliasMapFile);
|
|
subjectRegistry = loadSubjectRegistry(subjectRegistryFile);
|
|
} catch {
|
|
return json(res, 503, { error: "subject_identity_map_unavailable" });
|
|
}
|
|
const exactSubject = subjectRegistry.subjects.find(item => item.id.toUpperCase() === id);
|
|
if (exactSubject) {
|
|
return json(res, 200, {
|
|
schema: "guanghu.subject-resolution/v1",
|
|
status: "RESOLVED",
|
|
requested_id: id,
|
|
canonical_id: exactSubject.id,
|
|
redirected: false,
|
|
subject_kind: exactSubject.subject_kind,
|
|
subject: exactSubject,
|
|
}, 300);
|
|
}
|
|
const identity = resolveSubjectId(aliasMap, id);
|
|
if (identity.status === "CONFLICT_REJECTED") {
|
|
return json(res, 409, { error: "subject_id_conflict", ...identity });
|
|
}
|
|
if (identity.canonical_id) {
|
|
const subject = subjectRegistry.subjects.find(item => item.id.toUpperCase() === identity.canonical_id.toUpperCase());
|
|
if (!subject) return json(res, 503, { error: "canonical_subject_missing", ...identity });
|
|
return json(res, 200, {
|
|
schema: "guanghu.subject-resolution/v1",
|
|
...identity,
|
|
subject,
|
|
navigation: {
|
|
route_id: identity.route_id,
|
|
path: identity.current_path,
|
|
rule: "Canonicalize the subject id before navigation; legacy ids grant no authority.",
|
|
},
|
|
}, 300);
|
|
}
|
|
return 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,
|
|
server_node_map: "https://guanghulab.com/api/ai/v1/nodes",
|
|
subject_registry: "https://guanghulab.com/api/ai/v1/subjects",
|
|
subject_alias_map: "https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/branch/main/identity/subject-id-alias-map.json",
|
|
machine_navigation_map: "https://guanghulab.com/api/ai/v1/navigation",
|
|
navigate_api: "https://guanghulab.com/api/ai/v1/navigate?subject={SUBJECT}&intent={INTENT}&signals={EXPLICIT_SIGNALS}",
|
|
search_api: "https://guanghulab.com/api/ai/v1/search?q={query}",
|
|
resolve_api: "https://guanghulab.com/api/ai/v1/resolve?id={NUMBER}",
|
|
openapi: "https://guanghulab.com/api/ai/openapi.json",
|
|
access: "public-read-only",
|
|
write_authorization: {
|
|
mode: "public-no-authority-workorder-then-owner-email-approval",
|
|
capabilities: "https://guanghulab.com/authz/api/public/capabilities",
|
|
create_workorder: "https://guanghulab.com/authz/api/public/workorders",
|
|
owner_handoff: "use request_url returned by create_workorder",
|
|
ttl_seconds: 3600,
|
|
request_credential_required: false
|
|
}
|
|
}, 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 请提交明确的主体、意图和事件信号,由机器导航器生成最小运行路径;不要预读整个仓库,也不要用自然语言关键词猜测权限。</p><nav><a href="v1/navigation">机器导航地图</a> · <a href="v1/navigate?subject=ICE-P-ZY001&intent=persona_restore">铸渊最小运行包</a> · <a href="v1/repositories">仓库编号地图</a> · <a href="v1/nodes">服务器节点地图</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.1.0" },
|
|
servers: [{ url: "https://guanghulab.com/api/ai" }],
|
|
paths: {
|
|
"/v1/repositories": { get: { summary: "读取最新仓库编号路径映射", responses: { "200": { description: "Repository route map" } } } },
|
|
"/v1/nodes": { get: { summary: "读取最新服务器节点与人格路径编号映射", responses: { "200": { description: "Server node map" } } } },
|
|
"/v1/subjects": { get: { summary: "读取人类、人格体与公共系统的分型身份注册表", responses: { "200": { description: "Subject registry" } } } },
|
|
"/v1/navigation": { get: { summary: "读取主体与意图驱动的机器导航地图", responses: { "200": { description: "Machine navigation map" } } } },
|
|
"/v1/navigate": { get: { summary: "按明确主体、意图与信号生成最小运行包", parameters: [{ name: "subject", in: "query", required: true, schema: { type: "string", example: "ICE-P-ZY001" } }, { name: "intent", in: "query", schema: { type: "string", example: "persona_restore" } }, { name: "signals", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Compiled exact navigation bundle" }, "404": { description: "Unknown subject or intent; no guessing" } } } },
|
|
"/v1/search": { get: { summary: "按中文、编号或项目名检索", parameters: [{ name: "q", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Search results" } } } },
|
|
"/v1/resolve": { get: { summary: "解析仓库、服务器节点或人格路径编号", parameters: [{ name: "id", in: "query", required: true, schema: { type: "string", example: "ZY-OPS-LOOP-001" } }], responses: { "200": { description: "Resolved numbered route" }, "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, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap, loadNavigationMap,
|
|
resolveSubjectId, navigationRoute, compileNavigation, search, searchAll,
|
|
};
|