2026-08-02 21:49:53 +08:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const childProcess = require("node:child_process");
|
|
|
|
|
const crypto = require("node:crypto");
|
|
|
|
|
const fs = require("node:fs");
|
2026-08-03 22:27:09 +08:00
|
|
|
const os = require("node:os");
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
|
|
|
|
|
const MAX_KNOWLEDGE_PAGE_BYTES = 1024 * 1024;
|
2026-08-02 21:49:53 +08:00
|
|
|
|
|
|
|
|
class HoloLakeKnowledgeProvider {
|
|
|
|
|
constructor(options = {}) {
|
|
|
|
|
this.repositoryId = String(options.repositoryId || "");
|
|
|
|
|
this.repositoryPath = String(options.repositoryPath || "");
|
|
|
|
|
this.ref = "refs/heads/main";
|
|
|
|
|
this.maxArchiveBytes = Math.max(
|
|
|
|
|
1024 * 1024,
|
|
|
|
|
Number(options.maxArchiveBytes || 128 * 1024 * 1024),
|
|
|
|
|
);
|
|
|
|
|
if (!/^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(this.repositoryId)) {
|
|
|
|
|
throw new Error("knowledge_repository_id_invalid");
|
|
|
|
|
}
|
|
|
|
|
if (!this.repositoryPath || !fs.existsSync(this.repositoryPath)) {
|
|
|
|
|
throw new Error("knowledge_repository_unavailable");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest() {
|
|
|
|
|
const commit = this.git(["rev-parse", "--verify", `${this.ref}^{commit}`])
|
|
|
|
|
.toString("utf8")
|
|
|
|
|
.trim();
|
|
|
|
|
if (!/^[0-9a-f]{40}$/.test(commit)) {
|
|
|
|
|
throw new Error("knowledge_commit_invalid");
|
|
|
|
|
}
|
|
|
|
|
const committedAt = Number(
|
|
|
|
|
this.git(["show", "-s", "--format=%ct", commit]).toString("utf8").trim(),
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
schema: "guanghu.hololake-knowledge-manifest/v1",
|
|
|
|
|
repository: this.repositoryId,
|
|
|
|
|
ref: this.ref,
|
|
|
|
|
commit,
|
|
|
|
|
committed_at: committedAt,
|
|
|
|
|
archive_url: `/api/hololake/knowledge/archive?commit=${commit}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
archive(commit) {
|
|
|
|
|
const manifest = this.manifest();
|
|
|
|
|
if (!safeEqual(String(commit || ""), manifest.commit)) {
|
|
|
|
|
throw new Error("knowledge_commit_not_current");
|
|
|
|
|
}
|
|
|
|
|
const body = this.git(
|
|
|
|
|
["archive", "--format=zip", manifest.commit],
|
|
|
|
|
this.maxArchiveBytes + 1,
|
|
|
|
|
);
|
|
|
|
|
if (body.length > this.maxArchiveBytes) {
|
|
|
|
|
throw new Error("knowledge_archive_too_large");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
schema: "guanghu.hololake-knowledge-archive/v1",
|
|
|
|
|
repository: this.repositoryId,
|
|
|
|
|
commit: manifest.commit,
|
|
|
|
|
sha256: crypto.createHash("sha256").update(body).digest("hex"),
|
|
|
|
|
content_type: "application/zip",
|
|
|
|
|
body,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
writePage(request = {}) {
|
|
|
|
|
const pagePath = validateKnowledgePagePath(request.path);
|
|
|
|
|
if (typeof request.content !== "string") {
|
|
|
|
|
throw new Error("knowledge_page_content_invalid");
|
|
|
|
|
}
|
|
|
|
|
const content = Buffer.from(request.content, "utf8");
|
|
|
|
|
if (content.length > MAX_KNOWLEDGE_PAGE_BYTES) {
|
|
|
|
|
throw new Error("knowledge_page_too_large");
|
|
|
|
|
}
|
|
|
|
|
if (content.includes(0)) {
|
|
|
|
|
throw new Error("knowledge_page_content_invalid");
|
|
|
|
|
}
|
|
|
|
|
const current = this.manifest().commit;
|
|
|
|
|
const baseCommit = String(request.baseCommit || "");
|
|
|
|
|
if (!/^[0-9a-f]{40}$/.test(baseCommit) || !safeEqual(baseCommit, current)) {
|
|
|
|
|
throw new Error("knowledge_commit_conflict");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const temporary = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), "hololake-knowledge-index-"),
|
|
|
|
|
);
|
|
|
|
|
const indexFile = path.join(temporary, "index");
|
|
|
|
|
const gitEnv = {
|
|
|
|
|
...process.env,
|
|
|
|
|
GIT_INDEX_FILE: indexFile,
|
|
|
|
|
GIT_AUTHOR_NAME: "HoloLake Knowledge",
|
|
|
|
|
GIT_AUTHOR_EMAIL: "knowledge@guanghulake.invalid",
|
|
|
|
|
GIT_COMMITTER_NAME: "HoloLake Knowledge",
|
|
|
|
|
GIT_COMMITTER_EMAIL: "knowledge@guanghulake.invalid",
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
this.git(["read-tree", current], 1024 * 1024, { env: gitEnv });
|
|
|
|
|
const blob = this.git(
|
|
|
|
|
["hash-object", "-w", "--stdin"],
|
|
|
|
|
MAX_KNOWLEDGE_PAGE_BYTES + 1024,
|
|
|
|
|
{ env: gitEnv, input: content },
|
|
|
|
|
).toString("utf8").trim();
|
|
|
|
|
if (!/^[0-9a-f]{40}$/.test(blob)) {
|
|
|
|
|
throw new Error("knowledge_blob_invalid");
|
|
|
|
|
}
|
|
|
|
|
this.git(
|
|
|
|
|
["update-index", "--add", "--cacheinfo", "100644", blob, pagePath],
|
|
|
|
|
1024 * 1024,
|
|
|
|
|
{ env: gitEnv },
|
|
|
|
|
);
|
|
|
|
|
const tree = this.git(["write-tree"], 1024 * 1024, { env: gitEnv })
|
|
|
|
|
.toString("utf8")
|
|
|
|
|
.trim();
|
|
|
|
|
if (!/^[0-9a-f]{40}$/.test(tree)) {
|
|
|
|
|
throw new Error("knowledge_tree_invalid");
|
|
|
|
|
}
|
|
|
|
|
const nextCommit = this.git(
|
|
|
|
|
[
|
|
|
|
|
"commit-tree",
|
|
|
|
|
tree,
|
|
|
|
|
"-p",
|
|
|
|
|
current,
|
|
|
|
|
"-m",
|
|
|
|
|
`HoloLake knowledge: ${pagePath}`,
|
|
|
|
|
],
|
|
|
|
|
1024 * 1024,
|
|
|
|
|
{ env: gitEnv },
|
|
|
|
|
).toString("utf8").trim();
|
|
|
|
|
if (!/^[0-9a-f]{40}$/.test(nextCommit)) {
|
|
|
|
|
throw new Error("knowledge_commit_invalid");
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
this.git(
|
|
|
|
|
["update-ref", this.ref, nextCommit, current],
|
|
|
|
|
1024 * 1024,
|
|
|
|
|
{ env: gitEnv },
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
throw new Error("knowledge_commit_conflict");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
schema: "guanghu.hololake-knowledge-write-receipt/v1",
|
|
|
|
|
repository: this.repositoryId,
|
|
|
|
|
path: pagePath,
|
|
|
|
|
base_commit: current,
|
|
|
|
|
commit: nextCommit,
|
|
|
|
|
content_sha256: crypto.createHash("sha256").update(content).digest("hex"),
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
git(args, maxBuffer = 1024 * 1024, options = {}) {
|
2026-08-02 21:49:53 +08:00
|
|
|
return childProcess.execFileSync(
|
|
|
|
|
"git",
|
|
|
|
|
[`--git-dir=${this.repositoryPath}`, ...args],
|
|
|
|
|
{
|
|
|
|
|
encoding: "buffer",
|
|
|
|
|
maxBuffer,
|
2026-08-03 22:27:09 +08:00
|
|
|
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
|
|
|
env: options.env || process.env,
|
|
|
|
|
input: options.input,
|
2026-08-02 21:49:53 +08:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class HoloLakeAiGateway {
|
|
|
|
|
constructor(options = {}) {
|
|
|
|
|
this.fetchImpl = options.fetchImpl || fetch;
|
|
|
|
|
this.maxMessages = Math.max(1, Number(options.maxMessages || 64));
|
|
|
|
|
this.maxInputCharacters = Math.max(
|
|
|
|
|
1,
|
|
|
|
|
Number(options.maxInputCharacters || 120_000),
|
|
|
|
|
);
|
|
|
|
|
this.maxOutputTokens = Math.max(
|
|
|
|
|
16,
|
|
|
|
|
Number(options.maxOutputTokens || 16_384),
|
|
|
|
|
);
|
|
|
|
|
this.maxResponseBytes = Math.max(
|
|
|
|
|
64 * 1024,
|
|
|
|
|
Number(options.maxResponseBytes || 2 * 1024 * 1024),
|
|
|
|
|
);
|
|
|
|
|
this.providers = normalizeProviders(options.providers || {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalog() {
|
|
|
|
|
return {
|
|
|
|
|
schema: "guanghu.hololake-ai-catalog/v1",
|
|
|
|
|
providers: Object.entries(this.providers).map(([id, provider]) => ({
|
|
|
|
|
id,
|
|
|
|
|
name: provider.name || id,
|
|
|
|
|
models: provider.models,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async execute(request = {}) {
|
|
|
|
|
const providerId = String(request.provider || "");
|
|
|
|
|
const provider = this.providers[providerId];
|
|
|
|
|
if (!provider) throw new Error("ai_provider_not_registered");
|
|
|
|
|
const model = String(request.model || "");
|
|
|
|
|
if (!provider.models.includes(model)) {
|
|
|
|
|
throw new Error("ai_model_not_registered");
|
|
|
|
|
}
|
|
|
|
|
const messages = validateMessages(
|
|
|
|
|
request.messages,
|
|
|
|
|
this.maxMessages,
|
|
|
|
|
this.maxInputCharacters,
|
|
|
|
|
);
|
2026-08-03 22:27:09 +08:00
|
|
|
const tools = validateTools(request.tools);
|
2026-08-02 21:49:53 +08:00
|
|
|
const maxTokens = Number(request.max_tokens || 1024);
|
|
|
|
|
if (
|
|
|
|
|
!Number.isInteger(maxTokens)
|
|
|
|
|
|| maxTokens < 1
|
|
|
|
|
|| maxTokens > this.maxOutputTokens
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("ai_output_limit_exceeded");
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
const upstreamBody = {
|
|
|
|
|
model,
|
|
|
|
|
messages,
|
|
|
|
|
max_tokens: maxTokens,
|
|
|
|
|
stream: false,
|
|
|
|
|
};
|
|
|
|
|
if (tools.length > 0) {
|
|
|
|
|
upstreamBody.tools = tools;
|
|
|
|
|
upstreamBody.tool_choice = request.tool_choice === "none" ? "none" : "auto";
|
|
|
|
|
}
|
2026-08-02 21:49:53 +08:00
|
|
|
const response = await this.fetchImpl(`${provider.baseUrl}/chat/completions`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
authorization: `Bearer ${provider.apiKey}`,
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
2026-08-03 22:27:09 +08:00
|
|
|
body: JSON.stringify(upstreamBody),
|
2026-08-02 21:49:53 +08:00
|
|
|
signal: AbortSignal.timeout(provider.timeoutMs),
|
|
|
|
|
});
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
if (Buffer.byteLength(text) > this.maxResponseBytes) {
|
|
|
|
|
throw new Error("ai_response_too_large");
|
|
|
|
|
}
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const error = new Error("ai_upstream_failed");
|
|
|
|
|
error.status = response.status;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
let parsed;
|
|
|
|
|
try {
|
|
|
|
|
parsed = JSON.parse(text);
|
|
|
|
|
} catch {
|
|
|
|
|
throw new Error("ai_upstream_invalid_json");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
response: parsed,
|
|
|
|
|
receipt: {
|
|
|
|
|
schema: "guanghu.hololake-ai-receipt/v1",
|
|
|
|
|
state: "executed",
|
|
|
|
|
provider: providerId,
|
|
|
|
|
model,
|
|
|
|
|
upstream_status: response.status,
|
|
|
|
|
occurred_at: Date.now() / 1000,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadAiProviders(file) {
|
|
|
|
|
if (!file || !fs.existsSync(file)) return {};
|
|
|
|
|
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
|
|
|
if (!parsed || parsed.schema !== "guanghu.hololake-ai-providers/v1") {
|
|
|
|
|
throw new Error("ai_provider_registry_invalid");
|
|
|
|
|
}
|
|
|
|
|
return parsed.providers || {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeProviders(providers) {
|
|
|
|
|
const normalized = {};
|
|
|
|
|
for (const [id, value] of Object.entries(providers)) {
|
|
|
|
|
if (!/^[a-z0-9._-]{1,64}$/.test(id) || !value) continue;
|
|
|
|
|
const baseUrl = String(value.baseUrl || value.base_url || "").replace(/\/$/, "");
|
|
|
|
|
const apiKey = String(value.apiKey || value.api_key || "");
|
|
|
|
|
const models = Array.isArray(value.models)
|
|
|
|
|
? value.models.map(String).filter(model => /^[A-Za-z0-9._:/-]{1,160}$/.test(model))
|
|
|
|
|
: [];
|
|
|
|
|
let parsed;
|
|
|
|
|
try {
|
|
|
|
|
parsed = new URL(baseUrl);
|
|
|
|
|
} catch {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (parsed.protocol !== "https:" || !apiKey || models.length === 0) continue;
|
|
|
|
|
normalized[id] = {
|
|
|
|
|
baseUrl,
|
|
|
|
|
apiKey,
|
|
|
|
|
name: String(value.name || id).slice(0, 120),
|
|
|
|
|
models,
|
|
|
|
|
timeoutMs: Math.max(1_000, Number(value.timeoutMs || value.timeout_ms || 120_000)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateMessages(messages, maxMessages, maxInputCharacters) {
|
|
|
|
|
if (!Array.isArray(messages) || messages.length < 1) {
|
|
|
|
|
throw new Error("ai_messages_required");
|
|
|
|
|
}
|
|
|
|
|
if (messages.length > maxMessages) throw new Error("ai_input_too_large");
|
|
|
|
|
let characters = 0;
|
|
|
|
|
const normalized = messages.map(message => {
|
2026-08-03 22:27:09 +08:00
|
|
|
if (!message || !["system", "user", "assistant", "tool"].includes(message.role)) {
|
|
|
|
|
throw new Error("ai_message_invalid");
|
|
|
|
|
}
|
|
|
|
|
if (message.role === "tool") {
|
|
|
|
|
if (
|
|
|
|
|
typeof message.content !== "string"
|
|
|
|
|
|| !/^[A-Za-z0-9._:-]{1,160}$/.test(String(message.tool_call_id || ""))
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("ai_message_invalid");
|
|
|
|
|
}
|
|
|
|
|
characters += message.content.length;
|
|
|
|
|
return {
|
|
|
|
|
role: "tool",
|
|
|
|
|
tool_call_id: message.tool_call_id,
|
|
|
|
|
content: message.content,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const content = message.content === null && message.role === "assistant"
|
|
|
|
|
? null
|
|
|
|
|
: message.content;
|
|
|
|
|
if (typeof content !== "string" && content !== null) {
|
2026-08-02 21:49:53 +08:00
|
|
|
throw new Error("ai_message_invalid");
|
|
|
|
|
}
|
2026-08-03 22:27:09 +08:00
|
|
|
characters += typeof content === "string" ? content.length : 0;
|
|
|
|
|
const normalizedMessage = { role: message.role, content };
|
|
|
|
|
if (message.role === "assistant" && message.tool_calls !== undefined) {
|
|
|
|
|
normalizedMessage.tool_calls = validateToolCalls(message.tool_calls);
|
|
|
|
|
characters += normalizedMessage.tool_calls.reduce(
|
|
|
|
|
(total, call) => total + call.function.arguments.length,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return normalizedMessage;
|
2026-08-02 21:49:53 +08:00
|
|
|
});
|
|
|
|
|
if (characters > maxInputCharacters) throw new Error("ai_input_too_large");
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
function validateToolCalls(value) {
|
|
|
|
|
if (!Array.isArray(value) || value.length < 1 || value.length > 32) {
|
|
|
|
|
throw new Error("ai_message_invalid");
|
|
|
|
|
}
|
|
|
|
|
return value.map(call => {
|
|
|
|
|
if (
|
|
|
|
|
!call
|
|
|
|
|
|| call.type !== "function"
|
|
|
|
|
|| !/^[A-Za-z0-9._:-]{1,160}$/.test(String(call.id || ""))
|
|
|
|
|
|| !call.function
|
|
|
|
|
|| !/^[A-Za-z0-9_-]{1,80}$/.test(String(call.function.name || ""))
|
|
|
|
|
|| typeof call.function.arguments !== "string"
|
|
|
|
|
|| call.function.arguments.length > 32 * 1024
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("ai_message_invalid");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: call.id,
|
|
|
|
|
type: "function",
|
|
|
|
|
function: {
|
|
|
|
|
name: call.function.name,
|
|
|
|
|
arguments: call.function.arguments,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateTools(value) {
|
|
|
|
|
if (value === undefined || value === null) return [];
|
|
|
|
|
if (!Array.isArray(value) || value.length > 32) {
|
|
|
|
|
throw new Error("ai_tools_invalid");
|
|
|
|
|
}
|
|
|
|
|
const encoded = JSON.stringify(value);
|
|
|
|
|
if (Buffer.byteLength(encoded) > 128 * 1024) {
|
|
|
|
|
throw new Error("ai_tools_invalid");
|
|
|
|
|
}
|
|
|
|
|
return value.map(tool => {
|
|
|
|
|
if (
|
|
|
|
|
!tool
|
|
|
|
|
|| tool.type !== "function"
|
|
|
|
|
|| !tool.function
|
|
|
|
|
|| !/^[A-Za-z0-9_-]{1,80}$/.test(String(tool.function.name || ""))
|
|
|
|
|
|| typeof tool.function.description !== "string"
|
|
|
|
|
|| tool.function.description.length > 4_000
|
|
|
|
|
|| !tool.function.parameters
|
|
|
|
|
|| typeof tool.function.parameters !== "object"
|
|
|
|
|
|| Array.isArray(tool.function.parameters)
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("ai_tools_invalid");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
type: "function",
|
|
|
|
|
function: {
|
|
|
|
|
name: tool.function.name,
|
|
|
|
|
description: tool.function.description,
|
|
|
|
|
parameters: JSON.parse(JSON.stringify(tool.function.parameters)),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 21:49:53 +08:00
|
|
|
function safeEqual(left, right) {
|
|
|
|
|
const a = Buffer.from(String(left));
|
|
|
|
|
const b = Buffer.from(String(right));
|
|
|
|
|
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
function validateKnowledgePagePath(value) {
|
|
|
|
|
const pagePath = String(value || "").normalize("NFC");
|
|
|
|
|
if (
|
|
|
|
|
pagePath.length < 1
|
|
|
|
|
|| pagePath.length > 512
|
|
|
|
|
|| pagePath.startsWith("/")
|
|
|
|
|
|| pagePath.endsWith("/")
|
|
|
|
|
|| pagePath.includes("\\")
|
|
|
|
|
|| /[\0-\x1f\x7f]/u.test(pagePath)
|
|
|
|
|
|| !pagePath.endsWith(".md")
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("knowledge_page_path_invalid");
|
|
|
|
|
}
|
|
|
|
|
const components = pagePath.split("/");
|
|
|
|
|
if (
|
|
|
|
|
components.length < 1
|
|
|
|
|
|| components.length > 16
|
|
|
|
|
|| components.some(component => (
|
|
|
|
|
!component
|
|
|
|
|
|| component === "."
|
|
|
|
|
|| component === ".."
|
|
|
|
|
|| component.length > 120
|
|
|
|
|
|| component.toLocaleLowerCase("en-US") === ".git"
|
|
|
|
|
))
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("knowledge_page_path_invalid");
|
|
|
|
|
}
|
|
|
|
|
if (components.some(component => component.normalize("NFKC") === "本地密钥")) {
|
|
|
|
|
throw new Error("knowledge_secret_page_forbidden");
|
|
|
|
|
}
|
|
|
|
|
return pagePath;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 21:49:53 +08:00
|
|
|
module.exports = {
|
|
|
|
|
HoloLakeAiGateway,
|
|
|
|
|
HoloLakeKnowledgeProvider,
|
|
|
|
|
loadAiProviders,
|
|
|
|
|
};
|