feat(hololake): add mobile server account and capability proxy

This commit is contained in:
冰朔 2026-08-02 21:49:53 +08:00
commit 19bd38c6e3
11 changed files with 1436 additions and 1 deletions

View file

@ -10,6 +10,12 @@ const { sendSmtpMail } = require("./smtp-mailer");
const { executeRegisteredAction } = require("./action-client");
const { enqueueDeploymentEvent } = require("./deployment-event");
const { GuanghuRouter, loadDevices } = require("./guanghu-router");
const { HoloLakeSessionManager } = require("./hololake-session");
const {
HoloLakeAiGateway,
HoloLakeKnowledgeProvider,
loadAiProviders,
} = require("./hololake-capabilities");
const {
loadRegistry: loadRepoPushRegistry,
receiveBundle,
@ -71,6 +77,72 @@ function createApp(options = {}) {
smtpUser: process.env.SMTP_USER || ownerEmail,
smtpPass: process.env.QQ_SMTP_AUTH_CODE || "",
}));
const hololakePepper = String(
options.hololakeSessionPepper
|| process.env.HOLOLAKE_SESSION_PEPPER
|| "",
);
const hololakeSessionManager = options.hololakeSessionManager || (
hololakePepper
? new HoloLakeSessionManager({
registeredEmails: approvers.map(item => item.email),
pepper: hololakePepper,
stateFile: Object.prototype.hasOwnProperty.call(options, "hololakeSessionStateFile")
? options.hololakeSessionStateFile
: (
process.env.HOLOLAKE_SESSION_STATE_FILE
|| "/var/lib/guanghu/lake-lamp-authz/hololake-sessions.json"
),
otpTtlSeconds: Number(
options.hololakeOtpTtlSeconds
|| process.env.HOLOLAKE_OTP_TTL
|| 10 * 60,
),
sessionTtlSeconds: Number(
options.hololakeSessionTtlSeconds
|| process.env.HOLOLAKE_ACCOUNT_SESSION_TTL
|| 24 * 60 * 60,
),
requestLimit: Number(
options.hololakeOtpRequestLimit
|| process.env.HOLOLAKE_OTP_REQUEST_LIMIT
|| 6,
),
sendEmail,
})
: null
);
const hololakeKnowledgePath = String(
options.hololakeKnowledgeRepositoryPath
|| process.env.HOLOLAKE_KNOWLEDGE_REPOSITORY_PATH
|| "",
);
const hololakeKnowledgeProvider = options.hololakeKnowledgeProvider || (
hololakeKnowledgePath
? new HoloLakeKnowledgeProvider({
repositoryId: "bingshuo/hololake-knowledge-base",
repositoryPath: hololakeKnowledgePath,
maxArchiveBytes: Number(
options.hololakeKnowledgeMaxArchiveBytes
|| process.env.HOLOLAKE_KNOWLEDGE_MAX_ARCHIVE_BYTES
|| 128 * 1024 * 1024,
),
})
: null
);
const hololakeAiProvidersFile = String(
options.hololakeAiProvidersFile
|| process.env.HOLOLAKE_AI_PROVIDERS_FILE
|| "",
);
const hololakeAiProviders = options.hololakeAiProviders || (
hololakeAiProvidersFile ? loadAiProviders(hololakeAiProvidersFile) : {}
);
const hololakeAiGateway = options.hololakeAiGateway || (
Object.keys(hololakeAiProviders).length > 0
? new HoloLakeAiGateway({ providers: hololakeAiProviders })
: null
);
const mapGate = options.mapGate || new MapGate({
mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps",
stateFile: Object.prototype.hasOwnProperty.call(options, "mapStateFile") ? options.mapStateFile : (process.env.LAKE_LAMP_MAP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/map-acks.json"),
@ -139,6 +211,23 @@ function createApp(options = {}) {
return { ok: true, order: issued.order };
}
function authenticateHoloLake(req) {
if (!hololakeSessionManager) {
return { ok: false, status: 503, error: "hololake_session_unavailable" };
}
const token = bearer(req);
if (!token) return { ok: false, status: 401, error: "session_required" };
const deviceId = String(req.headers["x-hololake-device-id"] || "");
const authenticated = hololakeSessionManager.authenticate(token, deviceId);
if (!authenticated.ok) {
return {
...authenticated,
status: authenticated.error === "session_device_mismatch" ? 403 : 401,
};
}
return { ...authenticated, token, deviceId };
}
function bindAndDeliver(created) {
const inspected = manager.inspectHandoff(created.handoffToken);
if (!inspected.ok) return { delivered: 0, approver: null, order: null };
@ -190,6 +279,206 @@ function createApp(options = {}) {
},
});
if (
req.method === "POST"
&& url.pathname === "/api/hololake/session/email/request"
) {
if (!hololakeSessionManager) {
return json(res, 503, failure("hololake_session_unavailable"));
}
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
const headerDeviceId = String(
req.headers["x-hololake-device-id"] || "",
);
if (
headerDeviceId
&& body.device_id
&& !safeEqual(headerDeviceId, String(body.device_id))
) {
return json(res, 400, failure("invalid_device"));
}
const requested = await hololakeSessionManager.requestOtp({
email: body.email,
deviceId: String(body.device_id || headerDeviceId),
networkKey: clientAddress(req),
});
if (!requested.accepted) {
return json(
res,
requested.error === "rate_limited" ? 429 : 400,
failure(requested.error),
);
}
return json(res, 202, {
accepted: true,
request_id: requested.request_id,
expires_in: Number(
options.hololakeOtpTtlSeconds
|| process.env.HOLOLAKE_OTP_TTL
|| 10 * 60,
),
next_step: "如果邮箱已登记,输入邮件中的六位验证码。",
});
}
if (
req.method === "POST"
&& url.pathname === "/api/hololake/session/email/verify"
) {
if (!hololakeSessionManager) {
return json(res, 503, failure("hololake_session_unavailable"));
}
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
const headerDeviceId = String(
req.headers["x-hololake-device-id"] || "",
);
if (
headerDeviceId
&& body.device_id
&& !safeEqual(headerDeviceId, String(body.device_id))
) {
return json(res, 400, failure("invalid_device"));
}
const verified = hololakeSessionManager.verifyOtp({
requestId: body.request_id,
code: body.code,
deviceId: String(body.device_id || headerDeviceId),
});
if (!verified.ok) return json(res, 403, failure(verified.error));
return json(res, 200, verified);
}
if (
(req.method === "GET" || req.method === "DELETE")
&& url.pathname === "/api/hololake/session"
) {
const authenticated = authenticateHoloLake(req);
if (!authenticated.ok) {
return json(
res,
authenticated.status,
failure(authenticated.error),
);
}
if (req.method === "DELETE") {
const revoked = hololakeSessionManager.revoke(
authenticated.token,
authenticated.deviceId,
);
return json(res, revoked.ok ? 200 : 401, revoked);
}
return json(res, 200, {
ok: true,
session: authenticated.session,
});
}
if (
req.method === "GET"
&& url.pathname === "/api/hololake/knowledge/manifest"
) {
const authenticated = authenticateHoloLake(req);
if (!authenticated.ok) {
return json(
res,
authenticated.status,
failure(authenticated.error),
);
}
if (!hololakeKnowledgeProvider) {
return json(res, 503, failure("knowledge_repository_unavailable"));
}
try {
return json(res, 200, hololakeKnowledgeProvider.manifest());
} catch {
return json(res, 503, failure("knowledge_repository_unavailable"));
}
}
if (
req.method === "GET"
&& url.pathname === "/api/hololake/knowledge/archive"
) {
const authenticated = authenticateHoloLake(req);
if (!authenticated.ok) {
return json(
res,
authenticated.status,
failure(authenticated.error),
);
}
if (!hololakeKnowledgeProvider) {
return json(res, 503, failure("knowledge_repository_unavailable"));
}
try {
const archive = hololakeKnowledgeProvider.archive(
url.searchParams.get("commit"),
);
res.writeHead(200, {
"content-type": archive.content_type,
"content-length": archive.body.length,
"content-disposition": `attachment; filename="hololake-knowledge-${archive.commit}.zip"`,
"cache-control": "private, no-store",
"x-content-type-options": "nosniff",
"x-hololake-commit": archive.commit,
"x-content-sha256": archive.sha256,
});
res.end(archive.body);
return;
} catch (error) {
const code = error && error.message === "knowledge_commit_not_current"
? "knowledge_commit_not_current"
: "knowledge_archive_unavailable";
return json(res, code === "knowledge_commit_not_current" ? 409 : 503, failure(code));
}
}
if (
req.method === "GET"
&& url.pathname === "/api/hololake/ai/catalog"
) {
const authenticated = authenticateHoloLake(req);
if (!authenticated.ok) {
return json(
res,
authenticated.status,
failure(authenticated.error),
);
}
if (!hololakeAiGateway) {
return json(res, 503, failure("ai_gateway_unavailable"));
}
return json(res, 200, hololakeAiGateway.catalog());
}
if (
req.method === "POST"
&& url.pathname === "/api/hololake/ai/execute"
) {
const authenticated = authenticateHoloLake(req);
if (!authenticated.ok) {
return json(
res,
authenticated.status,
failure(authenticated.error),
);
}
if (!hololakeAiGateway) {
return json(res, 503, failure("ai_gateway_unavailable"));
}
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
try {
return json(res, 200, await hololakeAiGateway.execute(body));
} catch (error) {
const code = String(error && error.message || "ai_gateway_failed");
const status = code.startsWith("ai_upstream_") ? 502 : 400;
return json(res, status, failure(code));
}
}
if (req.method === "POST" && url.pathname === "/api/repositories/resolve") {
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));