fix: bootstrap opencode default project

This commit is contained in:
UniDesk Codex
2026-06-30 21:40:24 +08:00
parent 9a9f6afe90
commit e76a0b37d0
4 changed files with 162 additions and 12 deletions
+87 -12
View File
@@ -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");