2026-03-31 18:47:00 +03:00
|
|
|
import express from 'express';
|
|
|
|
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
|
|
|
|
|
|
|
|
export const registerOpenCodeProxy = (app, deps) => {
|
|
|
|
|
const {
|
|
|
|
|
fs,
|
|
|
|
|
os,
|
|
|
|
|
path,
|
|
|
|
|
OPEN_CODE_READY_GRACE_MS,
|
|
|
|
|
getRuntime,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
ensureOpenCodeApiPrefix,
|
|
|
|
|
} = deps;
|
|
|
|
|
|
|
|
|
|
if (app.get('opencodeProxyConfigured')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runtime = getRuntime();
|
|
|
|
|
if (runtime.openCodePort) {
|
|
|
|
|
console.log(`Setting up proxy to OpenCode on port ${runtime.openCodePort}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log('Setting up OpenCode API gate (OpenCode not started yet)');
|
|
|
|
|
}
|
|
|
|
|
app.set('opencodeProxyConfigured', true);
|
|
|
|
|
|
|
|
|
|
// Ensure API prefix is detected before proxying
|
|
|
|
|
app.use('/api', (_req, _res, next) => {
|
|
|
|
|
ensureOpenCodeApiPrefix();
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Readiness gate — return 503 while OpenCode is starting/restarting
|
|
|
|
|
app.use('/api', (req, res, next) => {
|
|
|
|
|
if (
|
|
|
|
|
req.path.startsWith('/themes/custom') ||
|
|
|
|
|
req.path.startsWith('/push') ||
|
|
|
|
|
req.path.startsWith('/config/agents') ||
|
|
|
|
|
req.path.startsWith('/config/opencode-resolution') ||
|
|
|
|
|
req.path.startsWith('/config/settings') ||
|
|
|
|
|
req.path.startsWith('/config/skills') ||
|
|
|
|
|
req.path === '/config/reload' ||
|
|
|
|
|
req.path === '/health'
|
|
|
|
|
) {
|
|
|
|
|
return next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runtimeState = getRuntime();
|
|
|
|
|
const waitElapsed = runtimeState.openCodeNotReadySince === 0 ? 0 : Date.now() - runtimeState.openCodeNotReadySince;
|
|
|
|
|
const stillWaiting =
|
|
|
|
|
(!runtimeState.isOpenCodeReady && (runtimeState.openCodeNotReadySince === 0 || waitElapsed < OPEN_CODE_READY_GRACE_MS)) ||
|
|
|
|
|
runtimeState.isRestartingOpenCode ||
|
|
|
|
|
!runtimeState.openCodePort;
|
|
|
|
|
|
|
|
|
|
if (stillWaiting) {
|
|
|
|
|
return res.status(503).json({
|
|
|
|
|
error: 'OpenCode is restarting',
|
|
|
|
|
restarting: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Windows: session merge for cross-directory session listing
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
app.get('/api/session', async (req, res, next) => {
|
|
|
|
|
const rawUrl = req.originalUrl || req.url || '';
|
|
|
|
|
if (rawUrl.includes('directory=')) return next();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
|
|
|
const fetchOpts = {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: { Accept: 'application/json', ...authHeaders },
|
|
|
|
|
signal: AbortSignal.timeout(10000),
|
|
|
|
|
};
|
|
|
|
|
const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
|
|
|
|
|
const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : [];
|
|
|
|
|
const globalSessions = Array.isArray(globalPayload) ? globalPayload : [];
|
|
|
|
|
|
|
|
|
|
const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
|
|
|
|
let projectDirs = [];
|
|
|
|
|
try {
|
|
|
|
|
const settingsRaw = fs.readFileSync(settingsPath, 'utf8');
|
|
|
|
|
const settings = JSON.parse(settingsRaw);
|
|
|
|
|
projectDirs = (settings.projects || [])
|
|
|
|
|
.map((project) => (typeof project?.path === 'string' ? project.path.trim() : ''))
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const seen = new Set(
|
|
|
|
|
globalSessions
|
|
|
|
|
.map((session) => (session && typeof session.id === 'string' ? session.id : null))
|
|
|
|
|
.filter((id) => typeof id === 'string')
|
|
|
|
|
);
|
|
|
|
|
const extraSessions = [];
|
|
|
|
|
for (const dir of projectDirs) {
|
|
|
|
|
const candidates = Array.from(new Set([
|
|
|
|
|
dir,
|
|
|
|
|
dir.replace(/\\/g, '/'),
|
|
|
|
|
dir.replace(/\//g, '\\'),
|
|
|
|
|
]));
|
|
|
|
|
for (const candidateDir of candidates) {
|
|
|
|
|
const encoded = encodeURIComponent(candidateDir);
|
|
|
|
|
try {
|
|
|
|
|
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
|
|
|
|
|
if (dirRes.ok) {
|
|
|
|
|
const dirPayload = await dirRes.json().catch(() => []);
|
|
|
|
|
const dirSessions = Array.isArray(dirPayload) ? dirPayload : [];
|
|
|
|
|
for (const session of dirSessions) {
|
|
|
|
|
const id = session && typeof session.id === 'string' ? session.id : null;
|
|
|
|
|
if (id && !seen.has(id)) {
|
|
|
|
|
seen.add(id);
|
|
|
|
|
extraSessions.push(session);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const merged = [...globalSessions, ...extraSessions];
|
|
|
|
|
merged.sort((a, b) => {
|
|
|
|
|
const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
|
|
|
|
|
const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
|
|
|
|
|
return bTime - aTime;
|
|
|
|
|
});
|
|
|
|
|
console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`);
|
|
|
|
|
return res.json(merged);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`[SessionMerge] Error: ${error.message}, falling through`);
|
|
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// http-proxy-middleware handles SSE, large bodies, timeouts correctly
|
|
|
|
|
const apiProxy = createProxyMiddleware({
|
|
|
|
|
target: `http://127.0.0.1:${runtime.openCodePort || 3902}`,
|
|
|
|
|
changeOrigin: true,
|
|
|
|
|
pathRewrite: { '^/api': '' },
|
|
|
|
|
// Dynamic target — port can change after restart
|
|
|
|
|
router: () => {
|
|
|
|
|
const rt = getRuntime();
|
|
|
|
|
return `http://127.0.0.1:${rt.openCodePort || 3902}`;
|
|
|
|
|
},
|
|
|
|
|
on: {
|
|
|
|
|
proxyReq: (proxyReq) => {
|
|
|
|
|
// Inject OpenCode auth headers
|
|
|
|
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
|
|
|
if (authHeaders.Authorization) {
|
|
|
|
|
proxyReq.setHeader('Authorization', authHeaders.Authorization);
|
|
|
|
|
}
|
2026-04-01 14:24:46 +08:00
|
|
|
|
|
|
|
|
// Defensive: request identity encoding from upstream OpenCode.
|
|
|
|
|
// This avoids compressed-body/header mismatches in multi-proxy setups.
|
|
|
|
|
proxyReq.setHeader('accept-encoding', 'identity');
|
2026-03-31 18:47:00 +03:00
|
|
|
},
|
|
|
|
|
error: (err, _req, res) => {
|
|
|
|
|
console.error('[proxy] OpenCode proxy error:', err.message);
|
|
|
|
|
if (res && !res.headersSent && typeof res.status === 'function') {
|
|
|
|
|
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use('/api', apiProxy);
|
|
|
|
|
};
|