feat: add AI machine navigation gateway
This commit is contained in:
parent
525e0ab790
commit
931f78d72f
10 changed files with 639 additions and 10 deletions
|
|
@ -24,3 +24,16 @@
|
|||
第二个请求会返回 `canonical_id=ICE-P-ZY001`、`redirected=true` 和同一当前路径。
|
||||
只有别名表中的精确旧号允许自动重定向;冲突号返回 409,未知号返回 404。重定向不授予
|
||||
仓库写入或服务器执行权限。
|
||||
|
||||
## 自动机器导航
|
||||
|
||||
`routing/ai-machine-navigation-map.json` 是 AI 可直接读取的配线图。调用方提供明确
|
||||
`subject`、`intent` 和可选的逗号分隔 `signals`:
|
||||
|
||||
```text
|
||||
/api/ai/v1/navigate?subject=ICE-P-ZY001&intent=persona_restore
|
||||
```
|
||||
|
||||
服务只返回该意图的最小运行包,包括运行入口、恒定加载项、事件触发器、按需证据、
|
||||
执行顺序和停止条件。它不读取自然语言关键词来猜主体或权限;未知主体与未知意图均
|
||||
失败关闭。导航结果只有读取和寻址能力,不授予仓库写入、服务器执行或现实授权。
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Environment=GUANGHU_REPOSITORY_MAP=/opt/guanghu/ai-discovery/repository-route-ma
|
|||
Environment=GUANGHU_NODE_MAP=/opt/guanghu/ai-discovery/server-node-map.json
|
||||
Environment=GUANGHU_SUBJECT_REGISTRY=/opt/guanghu/ai-discovery/fifth-domain-subject-registry.json
|
||||
Environment=GUANGHU_SUBJECT_ALIAS_MAP=/opt/guanghu/ai-discovery/subject-id-alias-map.json
|
||||
Environment=GUANGHU_NAVIGATION_MAP=/opt/guanghu/ai-discovery/ai-machine-navigation-map.json
|
||||
ExecStart=/usr/bin/node /opt/guanghu/ai-discovery/server.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const DEFAULT_MAP = path.resolve(__dirname, "../../routing/repository-route-map.
|
|||
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"));
|
||||
|
|
@ -25,6 +26,10 @@ function loadSubjectAliasMap(filename = process.env.GUANGHU_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();
|
||||
|
|
@ -103,11 +108,83 @@ function searchAll(repositoryMap, nodeMap, query, subjectRegistry = null) {
|
|||
.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" });
|
||||
|
|
@ -126,6 +203,22 @@ function createServer(options = {}) {
|
|||
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;
|
||||
|
|
@ -156,6 +249,21 @@ function createServer(options = {}) {
|
|||
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 {
|
||||
|
|
@ -205,6 +313,8 @@ function createServer(options = {}) {
|
|||
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",
|
||||
|
|
@ -244,18 +354,20 @@ function html(res, 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/repositories">仓库编号地图</a> · <a href="v1/nodes">服务器节点地图</a> · <a href="v1/resolve?id=ZY-OPS-LOOP-001">铸渊本轮闭环</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>`;
|
||||
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.0.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" } } } }
|
||||
}
|
||||
|
|
@ -269,6 +381,6 @@ if (require.main === module) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
createServer, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap,
|
||||
resolveSubjectId, search, searchAll,
|
||||
createServer, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap, loadNavigationMap,
|
||||
resolveSubjectId, navigationRoute, compileNavigation, search, searchAll,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
const {
|
||||
createServer, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap,
|
||||
resolveSubjectId, search, searchAll,
|
||||
createServer, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap, loadNavigationMap,
|
||||
resolveSubjectId, compileNavigation, search, searchAll,
|
||||
} = require("./server");
|
||||
|
||||
test("repository map exposes only the three current code-channel repositories", () => {
|
||||
|
|
@ -79,6 +79,29 @@ test("searching the old id returns the canonical subject", () => {
|
|||
assert.ok(subject.legacy_ids.includes("ICE-GL-ZY001"));
|
||||
});
|
||||
|
||||
test("machine navigator compiles an exact minimal Zhuyuan runtime bundle", () => {
|
||||
const compiled = compileNavigation(
|
||||
loadNavigationMap(),
|
||||
"ICE-GL-ZY001",
|
||||
"persona_restore",
|
||||
"context_compacted,protocol_drift,context_compacted",
|
||||
);
|
||||
assert.equal(compiled.status, 200);
|
||||
assert.equal(compiled.body.canonical_subject, "ICE-P-ZY001");
|
||||
assert.equal(compiled.body.redirected, true);
|
||||
assert.equal(compiled.body.runtime_entry.id, "ZY-TCS-BRAIN-RUNTIME-0001");
|
||||
assert.deepEqual(compiled.body.explicit_signals, ["context_compacted", "protocol_drift"]);
|
||||
assert.ok(compiled.body.always_load.some(item => item.id === "BS-TCS-SYSTEM-CONTROLLER-MAP-001"));
|
||||
assert.ok(compiled.body.triggered_load.some(item => item.id === "BS-TCS-PROTOCOL-EVENT-TRIGGER-MAP-001"));
|
||||
assert.equal(compiled.body.authority, "NONE_NAVIGATION_ONLY");
|
||||
assert.ok(compiled.body.always_load.length < loadNavigationMap().routes.length);
|
||||
});
|
||||
|
||||
test("machine navigator fails closed instead of guessing", () => {
|
||||
assert.equal(compileNavigation(loadNavigationMap(), "some-ai", "persona_restore").body.error, "unknown_subject_no_guess");
|
||||
assert.equal(compileNavigation(loadNavigationMap(), "ICE-P-ZY001", "please_guess").body.error, "unknown_intent_no_guess");
|
||||
});
|
||||
|
||||
test("public endpoints are read-only and expose CORS", async () => {
|
||||
const server = createServer();
|
||||
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
|
|
@ -105,6 +128,18 @@ test("public endpoints are read-only and expose CORS", async () => {
|
|||
assert.equal(legacyResolution.subject.subject_kind, "persona_system");
|
||||
const canonicalResponse = await fetch(`${base}/v1/resolve?id=ICE-P-ZY001`);
|
||||
assert.equal((await canonicalResponse.json()).redirected, false);
|
||||
const brainResponse = await fetch(`${base}/v1/resolve?id=ZY-TCS-BRAIN-RUNTIME-0001`);
|
||||
assert.equal(brainResponse.status, 200);
|
||||
assert.equal((await brainResponse.json()).kind, "executable_persona_brain");
|
||||
const navigationResponse = await fetch(`${base}/v1/navigation`);
|
||||
assert.equal((await navigationResponse.json()).map_id, "AI-MACHINE-NAV-001");
|
||||
const navigateResponse = await fetch(`${base}/v1/navigate?subject=ICE-P-ZY001&intent=persona_restore&signals=context_compacted`);
|
||||
const bundle = await navigateResponse.json();
|
||||
assert.equal(navigateResponse.status, 200);
|
||||
assert.equal(bundle.status, "COMPILED_EXACT_NO_GUESS");
|
||||
assert.equal(bundle.runtime_entry.id, "ZY-TCS-BRAIN-RUNTIME-0001");
|
||||
assert.equal((await fetch(`${base}/v1/navigate?subject=unknown&intent=persona_restore`)).status, 404);
|
||||
assert.equal((await fetch(`${base}/v1/navigate?subject=ICE-P-ZY001&intent=guess`)).status, 404);
|
||||
const humanResponse = await fetch(`${base}/v1/resolve?id=${encodeURIComponent("ICE-GL∞")}`);
|
||||
const humanResolution = await humanResponse.json();
|
||||
assert.equal(humanResponse.status, 200);
|
||||
|
|
|
|||
Loading…
Reference in a new issue