项目文件夹

文件

173 行
5.2 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
const corsHeaders = {
"access-control-allow-methods": "GET,POST,OPTIONS",
"access-control-allow-headers": "content-type",
"access-control-max-age": "86400"
};
const jsonHeaders = { ...corsHeaders, "content-type": "application/json; charset=utf-8" };
const SYSTEM_PROMPT =
"你是「夜航员」,一座沿海灯塔的值班助手。回答准确、克制、有温度,像在雾夜里把航道说清楚。直接回答用户的问题,不要自我介绍,不要使用markdown标题堆砌。";
function hasValue(value) {
return typeof value === "string" && value.trim().length > 0;
}
function requiredEnv(name) {
const value = process.env[name];
if (!hasValue(value)) {
throw new Error(`${name} is not configured`);
}
return value;
}
function json(statusCode, body) {
return { statusCode, headers: jsonHeaders, body: JSON.stringify(body) };
}
function requestPath(event) {
return (event && (event.path || event.rawPath)) || "/api/cloud/health";
}
function requestMethod(event) {
return (event && (event.method || event.httpMethod)) || "GET";
}
function parseBody(event) {
if (!event || !event.body) {
return {};
}
const raw = event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf8") : event.body;
try {
return JSON.parse(raw);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`invalid JSON body: ${detail}`);
}
}
function modelConfigured() {
return (
hasValue(process.env.OPENAI_BASE_URL) &&
hasValue(process.env.OPENAI_API_KEY) &&
!process.env.OPENAI_API_KEY.includes("replace-with") &&
hasValue(process.env.OPENAI_MODEL)
);
}
function health() {
return json(200, {
ok: true,
service: "beacon-watch",
keeper: "夜航员",
bindings: {
model: modelConfigured()
}
});
}
function normalizeMessages(body) {
const messages = [];
if (Array.isArray(body.messages)) {
for (const item of body.messages) {
if (!item || (item.role !== "user" && item.role !== "assistant")) {
continue;
}
if (typeof item.content !== "string" || !item.content.trim()) {
continue;
}
messages.push({
role: item.role,
content: item.content.trim().slice(0, 4000)
});
if (messages.length >= 24) {
break;
}
}
}
if (typeof body.question === "string" && body.question.trim()) {
messages.push({ role: "user", content: body.question.trim().slice(0, 4000) });
}
return messages;
}
async function chat(event) {
const body = parseBody(event);
const messages = normalizeMessages(body);
if (!messages.length || messages[messages.length - 1].role !== "user") {
return json(400, { ok: false, error: "请先写下要问的话" });
}
if (!modelConfigured()) {
return json(503, {
ok: false,
error: "值班室还没有接通模型令牌。用带 write:model 权限的 PAT 执行 wehub model token create 后写入函数 env。"
});
}
const baseURL = requiredEnv("OPENAI_BASE_URL").replace(/\/+$/, "");
const apiKey = requiredEnv("OPENAI_API_KEY");
const model = requiredEnv("OPENAI_MODEL");
const chatURL = baseURL.endsWith("/v1") ? `${baseURL}/chat/completions` : `${baseURL}/v1/chat/completions`;
let response;
try {
response = await fetch(chatURL, {
method: "POST",
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
body: JSON.stringify({
model,
messages: [{ role: "system", content: SYSTEM_PROMPT }, ...messages],
thinking: { type: "disabled" }
}),
signal: AbortSignal.timeout(60_000)
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
return json(502, { ok: false, error: `灯塔链路中断:${detail}` });
}
let payload;
try {
payload = await response.json();
} catch (error) {
return json(502, { ok: false, error: `模型返回了无法阅读的报文(${response.status}` });
}
if (!response.ok) {
const detail = payload && payload.error && payload.error.message;
return json(502, { ok: false, error: detail || `模型请求失败:${response.status}` });
}
const answer =
payload &&
payload.choices &&
payload.choices[0] &&
payload.choices[0].message &&
payload.choices[0].message.content;
if (typeof answer !== "string" || !answer.trim()) {
return json(502, { ok: false, error: "值班室沉默了,没有带回答复" });
}
return json(200, { ok: true, answer: answer.trim() });
}
exports.handler = async function handler(event) {
const path = requestPath(event);
const method = requestMethod(event).toUpperCase();
try {
if (method === "OPTIONS") {
return { statusCode: 204, headers: corsHeaders, body: "" };
}
if (method === "GET" && path.endsWith("/health")) {
return health();
}
if (method === "POST" && path.endsWith("/chat")) {
return await chat(event);
}
return json(200, {
ok: true,
routes: ["GET /api/cloud/health", "POST /api/cloud/chat"]
});
} catch (error) {
return json(500, { ok: false, error: error instanceof Error ? error.message : String(error) });
}
};