feat(identity): redirect legacy Zhuyuan ids to ICE-P

This commit is contained in:
冰朔 2026-07-27 14:12:51 +08:00
commit aa6652a646
28 changed files with 491 additions and 106 deletions

View file

@ -6,6 +6,8 @@ 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");
function loadMap(filename = process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP) {
return JSON.parse(fs.readFileSync(filename, "utf8"));
@ -15,6 +17,45 @@ 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 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();
}
@ -37,13 +78,19 @@ function search(map, query) {
.map(item => item.repository);
}
function searchAll(repositoryMap, nodeMap, query) {
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 => {
@ -59,6 +106,8 @@ function searchAll(repositoryMap, nodeMap, query) {
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;
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" });
@ -73,12 +122,19 @@ function createServer(options = {}) {
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/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" }); }
const results = searchAll(map, nodeMap, query);
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,
@ -98,7 +154,47 @@ function createServer(options = {}) {
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);
return personaRoute ? json(res, 200, personaRoute, 300) : json(res, 404, { error: "route_not_found", id });
if (personaRoute) return json(res, 200, personaRoute, 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, {
@ -107,6 +203,8 @@ function createServer(options = {}) {
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",
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",
@ -157,6 +255,7 @@ function openApi() {
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/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" } } } }
}
@ -169,4 +268,7 @@ if (require.main === module) {
createServer().listen(port, host, () => process.stdout.write(`guanghu-ai-discovery listening on ${host}:${port}\n`));
}
module.exports = { createServer, loadMap, loadNodeMap, search, searchAll };
module.exports = {
createServer, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap,
resolveSubjectId, search, searchAll,
};