fix: bootstrap opencode default project
This commit is contained in:
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import { gzip } from "node:zlib";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
|
||||
import { copyProxyResponseHeaders, isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
|
||||
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
|
||||
import { UPSTREAM_UNAVAILABLE_ERROR_CODE, UPSTREAM_UNAVAILABLE_MESSAGE } from "./http.mjs";
|
||||
const gzipAsync = promisify(gzip);
|
||||
@@ -64,6 +64,7 @@ export function createCloudWebServer({
|
||||
}),
|
||||
opencodeUsername = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_USERNAME") || "",
|
||||
opencodePassword = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_PASSWORD") || "",
|
||||
opencodeDefaultProject = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_DEFAULT_PROJECT") || "",
|
||||
opencodeEventDirectoryRewrite = opencodeEventDirectoryRewriteFromEnv()
|
||||
}) {
|
||||
return createServer(async (request, response) => {
|
||||
@@ -79,7 +80,7 @@ export function createCloudWebServer({
|
||||
return;
|
||||
}
|
||||
if (isOpencodeProxyRequest(request, url, opencodeProxyHost)) {
|
||||
await proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeEventDirectoryRewrite, serviceId, sendJson });
|
||||
await proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject, opencodeEventDirectoryRewrite, serviceId, sendJson });
|
||||
return;
|
||||
}
|
||||
if (await handleCloudWebAuth({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson })) {
|
||||
@@ -729,7 +730,7 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c
|
||||
}
|
||||
}
|
||||
|
||||
async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeEventDirectoryRewrite = null, serviceId, sendJson }) {
|
||||
async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject = "", opencodeEventDirectoryRewrite = null, serviceId, sendJson }) {
|
||||
const traceContext = cloudWebTraceContext(request);
|
||||
const startedAtMs = Date.now();
|
||||
if (!cloudApiBaseUrl) {
|
||||
@@ -821,15 +822,17 @@ async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, c
|
||||
headers: opencodeUpstreamHeaders({ headers: { ...request.headers, ...cloudWebTraceHeaders(traceContext, serviceId) } }, body, authorization)
|
||||
};
|
||||
try {
|
||||
const proxyResult = await proxyCloudApiRequest({
|
||||
target,
|
||||
request: upstreamRequest,
|
||||
response,
|
||||
body: request.method === "GET" || request.method === "HEAD" ? "" : body,
|
||||
timeoutMs: opencodeProxyTimeoutMs,
|
||||
extraResponseHeaders,
|
||||
streamTransform
|
||||
});
|
||||
const proxyResult = shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject })
|
||||
? await proxyOpencodeHtmlRequest({ target, request: upstreamRequest, response, timeoutMs: opencodeProxyTimeoutMs, extraResponseHeaders, defaultProject: opencodeDefaultProject })
|
||||
: await proxyCloudApiRequest({
|
||||
target,
|
||||
request: upstreamRequest,
|
||||
response,
|
||||
body: request.method === "GET" || request.method === "HEAD" ? "" : body,
|
||||
timeoutMs: opencodeProxyTimeoutMs,
|
||||
extraResponseHeaders,
|
||||
streamTransform
|
||||
});
|
||||
emitOpencodeProxySpanAsync({
|
||||
traceContext,
|
||||
serviceId,
|
||||
@@ -890,6 +893,78 @@ function opencodeTargetUrl(url, opencodeUpstreamUrl) {
|
||||
return target;
|
||||
}
|
||||
|
||||
function shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject }) {
|
||||
if (!opencodeDefaultProject) return false;
|
||||
if (request.method !== "GET") return false;
|
||||
const accept = String(request.headers.accept || "");
|
||||
if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return false;
|
||||
if (target.pathname.startsWith("/assets/")) return false;
|
||||
if (STATIC_ASSET_EXTENSION_PATTERN.test(target.pathname)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function proxyOpencodeHtmlRequest({ target, request, response, timeoutMs, extraResponseHeaders, defaultProject }) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const upstream = await fetch(target, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
signal: controller.signal
|
||||
});
|
||||
const upstreamHeaders = Object.fromEntries(upstream.headers.entries());
|
||||
const contentType = String(upstream.headers.get("content-type") || "");
|
||||
const inputBody = Buffer.from(await upstream.arrayBuffer());
|
||||
const outputBody = /^text\/html(?:\s*;|$)/iu.test(contentType)
|
||||
? Buffer.from(opencodeInjectDefaultProjectHtml(inputBody.toString("utf8"), defaultProject), "utf8")
|
||||
: inputBody;
|
||||
response.writeHead(upstream.status, {
|
||||
...mergeOpencodeProxyResponseHeaders(copyProxyResponseHeaders(upstreamHeaders), extraResponseHeaders),
|
||||
"content-length": outputBody.length
|
||||
});
|
||||
response.end(outputBody);
|
||||
return {
|
||||
statusCode: upstream.status,
|
||||
streaming: false,
|
||||
bodyBytes: outputBody.length,
|
||||
opencodeDefaultProjectInjected: outputBody.length !== inputBody.length
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") error.timedOut = true;
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function opencodeInjectDefaultProjectHtml(html, defaultProject) {
|
||||
if (!html || html.includes("data-hwlab-opencode-default-project")) return html;
|
||||
const script = opencodeDefaultProjectBootstrapScript(defaultProject);
|
||||
if (html.includes("</head>")) return html.replace("</head>", `${script}</head>`);
|
||||
if (html.includes("</body>")) return html.replace("</body>", `${script}</body>`);
|
||||
return `${script}${html}`;
|
||||
}
|
||||
|
||||
function opencodeDefaultProjectBootstrapScript(defaultProject) {
|
||||
return `<script data-hwlab-opencode-default-project>(function(){try{var project=${safeInlineJson(defaultProject)};if(!project)return;var serverKey="opencode.global.dat:server";var server=JSON.parse(localStorage.getItem(serverKey)||"{}");var projects=server.projects&&typeof server.projects==="object"?server.projects:{};var local=Array.isArray(projects.local)?projects.local.slice():[];var found=false;local=local.map(function(item){if(!item||item.worktree!==project)return item;found=true;return Object.assign({},item,{expanded:true});});if(!found)local.unshift({worktree:project,expanded:true});projects.local=local;server.projects=projects;server.lastProject=Object.assign({},server.lastProject||{},{local:project});localStorage.setItem(serverKey,JSON.stringify(server));var layoutKey="opencode.global.dat:layout.page";var layout=JSON.parse(localStorage.getItem(layoutKey)||"{}");layout.workspaceExpanded=Object.assign({},layout.workspaceExpanded||{});layout.workspaceExpanded[project]=true;localStorage.setItem(layoutKey,JSON.stringify(layout));}catch(error){console.warn("HWLAB OpenCode default project bootstrap failed",error);}})();</script>`;
|
||||
}
|
||||
|
||||
function mergeOpencodeProxyResponseHeaders(baseHeaders = {}, extraHeaders = {}) {
|
||||
const result = { ...baseHeaders };
|
||||
for (const [key, value] of Object.entries(extraHeaders)) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const existingKey = Object.keys(result).find((item) => item.toLowerCase() === key.toLowerCase());
|
||||
if (existingKey && existingKey.toLowerCase() === "set-cookie") {
|
||||
const existing = Array.isArray(result[existingKey]) ? result[existingKey] : [result[existingKey]];
|
||||
const next = Array.isArray(value) ? value : [value];
|
||||
result[existingKey] = [...existing.filter(Boolean), ...next.filter(Boolean)];
|
||||
continue;
|
||||
}
|
||||
result[existingKey || key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function issueOpencodeTicket(cookieHeader) {
|
||||
pruneOpencodeTickets();
|
||||
const ticket = randomBytes(24).toString("base64url");
|
||||
|
||||
Reference in New Issue
Block a user