mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-05-02 00:00:23 +00:00
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build language matrix (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Build-1 (push) Has been cancelled
Publish to Docker Hub / Build and Push Docker (multi-arch) (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Integration Tests (push) Has been cancelled
CI / Security Tests (push) Has been cancelled
CI / i18n Validation (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Unit Tests-1 (push) Has been cancelled
CI / CI Dashboard (push) Has been cancelled
* feat(qoder): native cosy integration
* feat(qoder): implement native COSY encryption algorithm and remove CLI child instances, plus workflow bumps
* feat(resilience): context overflow fallback, OAuth token detection, empty content guard & context-optimized combo strategy
- Add isContextOverflowError + isContextOverflow detectors (400 + token-limit signals)
- Auto-fallback to next family model on context overflow in chatCore
- Add isEmptyContentResponse to catch fake-success empty responses, trigger fallback + recursive retry
- Add OAUTH_INVALID_TOKEN error type (T11) with isOAuthInvalidToken signal matching; warn instead of deactivating node
- Add getModelContextLimit helper in modelsDevSync (reads limit_context from synced capabilities)
- Upgrade getTokenLimit in contextManager to check models.dev DB before registry (fixes gemini-2.5-pro: 1000000→1048576)
- Add findLargerContextModel in modelFamilyFallback for context-aware model selection
- Add sortModelsByContextSize + context-optimized combo strategy in combo.ts
- Update context-manager unit test for corrected gemini-2.5-pro limit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(review): address Gemini code review — tool_calls path, infinite recursion, dedup signals, findLargerContextModel
- Fix isEmptyContentResponse: check message.tool_calls/delta.tool_calls instead
of firstChoice.tool_calls (wrong OpenAI API path, caused tool-call responses
to be falsely flagged as empty)
- Fix empty content fallback: replace recursive handleChatCore call (infinite
recursion risk + wrong model due to original body.model) with non-recursive
pattern — call executeProviderRequest, parse fallback response body, reassign
responseBody and fall through to existing processing
- Fix context overflow: use findLargerContextModel over family candidates first,
fall back to getNextFamilyFallback — ensures we pick a model with actually
larger context window on overflow
- Fix signal dedup: export CONTEXT_OVERFLOW_SIGNALS + CONTEXT_OVERFLOW_REGEX
from errorClassifier.ts; import shared regex in modelFamilyFallback.ts,
removing duplicate signal list and per-call RegExp construction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(UI): add context-optimized strategy to frontend schema and options
* fix(sse): preserve Responses API events in stream translation
When translating Claude-format responses (e.g. GLM) to Responses API
format for Codex CLI, the sanitizer stripped {event, data} structured
items to {"object":"chat.completion.chunk"}, losing all content and
the critical response.completed event.
Only run sanitizeStreamingChunk on OpenAI Chat Completions chunks,
skipping items that have the Responses API {event, data} structure.
* test(sse): add regression test for Claude→Responses stream sanitization
Verifies that {event,data} structured items from the Responses API
translator bypass sanitizeStreamingChunk when translating Claude-format
providers (e.g. GLM) to Responses API format for Codex CLI.
* fix(sse): strengthen Responses API event detection with response. prefix check
Use explicit `response.` prefix check instead of generic `event && data`
presence check, as recommended in PR review.
* fix: pin Next.js to 16.0.10 to prevent Turbopack hashed module bug
Remove ^ prefix from next and eslint-config-next to prevent
automatic upgrades to 16.1.x+ which introduced content-based
hashing for external module references in Turbopack.
Also remove duplicate Material Symbols @import from globals.css
(font already loaded via <link> in layout.tsx).
Fixes #509
* align cc-compatible cache handling with client passthrough
* chore: integrate resilience and turbopack fixes (PRs #992, #990, #987)
* chore(release): bump to v3.5.2 — changelog, docs, version sync
* docs(i18n): sync documentation updates to 33 languages
* fix(qoder): replace any with unknown to comply with strict any-budget
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Chris Staley <christopher-s@users.noreply.github.com>
Co-authored-by: Ivan <shanin-i2011@yandex.ru>
Co-authored-by: R.D. <rogerproself@gmail.com>
324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* OmniRoute — Prepublish Build Script
|
||
*
|
||
* Builds the Next.js app in standalone mode and copies output
|
||
* into the `app/` directory that gets published to npm.
|
||
*
|
||
* Run with: node scripts/prepublish.mjs
|
||
*/
|
||
|
||
import { execSync } from "node:child_process";
|
||
import {
|
||
existsSync,
|
||
mkdirSync,
|
||
cpSync,
|
||
rmSync,
|
||
writeFileSync,
|
||
readFileSync,
|
||
readdirSync,
|
||
statSync,
|
||
} from "node:fs";
|
||
import { join, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
const ROOT = join(__dirname, "..");
|
||
|
||
const APP_DIR = join(ROOT, "app");
|
||
|
||
console.log("🔨 OmniRoute — Building for npm publish...\n");
|
||
|
||
// ── Step 1: Clean previous app/ directory ──────────────────
|
||
if (existsSync(APP_DIR)) {
|
||
console.log(" 🧹 Cleaning previous app/ directory...");
|
||
rmSync(APP_DIR, { recursive: true, force: true });
|
||
}
|
||
|
||
// ── Step 2: Install dependencies ───────────────────────────
|
||
console.log(" 📦 Installing dependencies...");
|
||
execSync("npm install", { cwd: ROOT, stdio: "inherit" });
|
||
|
||
// ── Step 3: Build Next.js ──────────────────────────────────
|
||
console.log(" 🏗️ Building Next.js (standalone)...");
|
||
execSync("npx next build", {
|
||
cwd: ROOT,
|
||
stdio: "inherit",
|
||
env: {
|
||
...process.env,
|
||
// Force webpack codegen — Turbopack emits hashed require() calls for
|
||
// server external packages that break npm global installs (#394, #396, #398).
|
||
EXPERIMENTAL_TURBOPACK: "0",
|
||
NEXT_PRIVATE_BUILD_WORKER: "0",
|
||
},
|
||
});
|
||
|
||
// ── Step 4: Verify standalone output ───────────────────────
|
||
const standaloneDir = join(ROOT, ".next", "standalone");
|
||
const serverJs = join(standaloneDir, "server.js");
|
||
|
||
if (!existsSync(serverJs)) {
|
||
console.error("\n ❌ Standalone build not found at:", standaloneDir);
|
||
console.error(" Make sure next.config.mjs has: output: 'standalone'");
|
||
process.exit(1);
|
||
}
|
||
|
||
// ── Step 5: Copy standalone output to app/ ─────────────────
|
||
// ── Step 4.5: Check build for hashed external references ──────────────────────
|
||
// Warn if Turbopack-style hash suffixes are found — they will be resolved at
|
||
// runtime by the externals patch in next.config.mjs, but log for visibility.
|
||
{
|
||
const HASH_RE = /require\(["']([\w@./-]+-[0-9a-f]{16})["']\)/;
|
||
const scanDir = (dir, hits = []) => {
|
||
let entries = [];
|
||
try {
|
||
entries = readdirSync(dir);
|
||
} catch {
|
||
return hits;
|
||
}
|
||
for (const e of entries) {
|
||
const f = join(dir, e);
|
||
try {
|
||
if (statSync(f).isDirectory()) {
|
||
scanDir(f, hits);
|
||
continue;
|
||
}
|
||
if (!f.endsWith(".js")) continue;
|
||
const m = readFileSync(f, "utf8").match(HASH_RE);
|
||
if (m) hits.push({ file: f.replace(standaloneDir, "app"), mod: m[1] });
|
||
} catch {
|
||
continue;
|
||
}
|
||
}
|
||
return hits;
|
||
};
|
||
const hits = scanDir(join(standaloneDir, ".next", "server"));
|
||
if (hits.length > 0) {
|
||
console.warn(
|
||
" ⚠️ Hashed externals in build (will be auto-fixed at runtime by externals patch):"
|
||
);
|
||
hits.slice(0, 5).forEach((h) => console.warn());
|
||
if (hits.length > 5) console.warn();
|
||
} else {
|
||
console.log(" ✅ Build clean — no hashed externals found.");
|
||
}
|
||
}
|
||
|
||
console.log(" 📋 Copying standalone build to app/...");
|
||
mkdirSync(APP_DIR, { recursive: true });
|
||
cpSync(standaloneDir, APP_DIR, { recursive: true });
|
||
|
||
// ── Step 5.5: Sanitize hardcoded build-machine paths ───────
|
||
// Next.js standalone bakes absolute build-time paths into server.js and
|
||
// required-server-files.json (outputFileTracingRoot, appDir, turbopack root).
|
||
// Replace the build machine's absolute path with "." (current directory)
|
||
// so paths resolve relative to wherever the standalone app/ is installed.
|
||
console.log(" 🧹 Sanitizing build-machine paths...");
|
||
const buildRoot = ROOT.replace(/\\/g, "/"); // normalise for regex safety
|
||
const sanitizeTargets = [
|
||
join(APP_DIR, "server.js"),
|
||
join(APP_DIR, ".next", "required-server-files.json"),
|
||
];
|
||
let sanitisedCount = 0;
|
||
for (const filePath of sanitizeTargets) {
|
||
if (!existsSync(filePath)) continue;
|
||
let content = readFileSync(filePath, "utf8");
|
||
// Escape special regex characters in the path
|
||
const escaped = buildRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
const re = new RegExp(escaped, "g");
|
||
const matches = content.match(re);
|
||
if (matches) {
|
||
// Replace with "." so Next.js resolves paths relative to the standalone dir
|
||
content = content.replace(re, ".");
|
||
writeFileSync(filePath, content);
|
||
sanitisedCount += matches.length;
|
||
}
|
||
}
|
||
if (sanitisedCount > 0) {
|
||
console.log(` ✅ Sanitised ${sanitisedCount} hardcoded path references`);
|
||
} else {
|
||
console.log(" ℹ️ No hardcoded paths found to sanitise");
|
||
}
|
||
|
||
// ── Step 5.6: Strip Turbopack hashed externals from compiled chunks ─────────
|
||
// Even when Turbopack is disabled at build time, some instrumentation chunks
|
||
// may still emit require('package-<16hexchars>') instead of require('package').
|
||
// These hashed names don't exist in node_modules and cause MODULE_NOT_FOUND at
|
||
// runtime. We strip the hex suffix from all .js files in app/.next/server/
|
||
// to ensure all require() calls use the real package names.
|
||
{
|
||
const serverOutput = join(APP_DIR, ".next", "server");
|
||
const HASH_RE = /(['"\\])([a-z@][a-z0-9@./_-]+-[0-9a-f]{16})\1/g;
|
||
let patchedFiles = 0;
|
||
let patchedMatches = 0;
|
||
const walkDir = (dir) => {
|
||
let entries = [];
|
||
try {
|
||
entries = readdirSync(dir);
|
||
} catch {
|
||
return;
|
||
}
|
||
for (const entry of entries) {
|
||
const full = join(dir, entry);
|
||
try {
|
||
const st = statSync(full);
|
||
if (st.isDirectory()) {
|
||
walkDir(full);
|
||
continue;
|
||
}
|
||
if (!entry.endsWith(".js")) continue;
|
||
const src = readFileSync(full, "utf8");
|
||
let count = 0;
|
||
const patched = src.replace(HASH_RE, (_, q, name) => {
|
||
const base = name.replace(/-[0-9a-f]{16}$/, "");
|
||
count++;
|
||
return `${q}${base}${q}`;
|
||
});
|
||
if (count > 0) {
|
||
writeFileSync(full, patched);
|
||
patchedFiles++;
|
||
patchedMatches += count;
|
||
}
|
||
} catch {
|
||
/* skip unreadable files */
|
||
}
|
||
}
|
||
};
|
||
if (existsSync(serverOutput)) {
|
||
walkDir(serverOutput);
|
||
if (patchedMatches > 0) {
|
||
console.log(
|
||
` 🔧 Hash-strip: patched ${patchedMatches} hashed require() in ${patchedFiles} server chunk file(s)`
|
||
);
|
||
} else {
|
||
console.log(" ✅ Hash-strip: no hashed externals found in compiled chunks.");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Step 6: Copy static assets ─────────────────────────────
|
||
const staticSrc = join(ROOT, ".next", "static");
|
||
const staticDest = join(APP_DIR, ".next", "static");
|
||
if (existsSync(staticSrc)) {
|
||
console.log(" 📋 Copying static assets...");
|
||
mkdirSync(staticDest, { recursive: true });
|
||
cpSync(staticSrc, staticDest, { recursive: true });
|
||
}
|
||
|
||
// ── Step 7: Copy public/ assets ────────────────────────────
|
||
const publicSrc = join(ROOT, "public");
|
||
const publicDest = join(APP_DIR, "public");
|
||
if (existsSync(publicSrc)) {
|
||
console.log(" 📋 Copying public/ assets...");
|
||
mkdirSync(publicDest, { recursive: true });
|
||
cpSync(publicSrc, publicDest, { recursive: true });
|
||
}
|
||
|
||
// ── Step 8: Compile + copy MITM cert utilities ─────────────
|
||
const mitmSrc = join(ROOT, "src", "mitm");
|
||
const mitmDest = join(APP_DIR, "src", "mitm");
|
||
if (existsSync(mitmSrc)) {
|
||
console.log(" 🔨 Compiling MITM utilities (TypeScript → JavaScript)...");
|
||
mkdirSync(mitmDest, { recursive: true });
|
||
|
||
// Write a temporary tsconfig.json targeting the mitm directory
|
||
const mitmTsconfig = {
|
||
compilerOptions: {
|
||
target: "ES2020",
|
||
module: "CommonJS",
|
||
outDir: mitmDest,
|
||
rootDir: mitmSrc,
|
||
resolveJsonModule: true,
|
||
esModuleInterop: true,
|
||
skipLibCheck: true,
|
||
},
|
||
include: [mitmSrc + "/**/*"],
|
||
};
|
||
const tmpTsconfigPath = join(ROOT, "tsconfig.mitm.tmp.json");
|
||
writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2));
|
||
|
||
try {
|
||
execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" });
|
||
console.log(" ✅ MITM utilities compiled to app/src/mitm/");
|
||
} catch (err) {
|
||
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);
|
||
// Fallback: copy source files so at least they are present
|
||
cpSync(mitmSrc, mitmDest, { recursive: true });
|
||
} finally {
|
||
// Cleanup temp tsconfig
|
||
try {
|
||
rmSync(tmpTsconfigPath);
|
||
} catch {}
|
||
}
|
||
}
|
||
|
||
// ── Step 8.5: Bundle MCP server ────────────────────────────
|
||
const mcpSrcFile = join(ROOT, "open-sse", "mcp-server", "server.ts");
|
||
const mcpDestDir = join(APP_DIR, "open-sse", "mcp-server");
|
||
const mcpDestFile = join(mcpDestDir, "server.js");
|
||
|
||
if (existsSync(mcpSrcFile)) {
|
||
console.log(" 🔨 Bundling MCP Server (TypeScript → JavaScript)...");
|
||
mkdirSync(mcpDestDir, { recursive: true });
|
||
try {
|
||
execSync(
|
||
`npx esbuild open-sse/mcp-server/server.ts --bundle --platform=node --packages=external --format=esm --outfile=app/open-sse/mcp-server/server.js`,
|
||
{ cwd: ROOT, stdio: "inherit" }
|
||
);
|
||
console.log(" ✅ MCP Server bundled to app/open-sse/mcp-server/server.js");
|
||
} catch (err) {
|
||
console.warn(" ⚠️ MCP Server bundle error:", err.message);
|
||
}
|
||
}
|
||
|
||
// ── Step 9: Copy shared utilities needed at runtime ────────
|
||
const sharedApiKey = join(ROOT, "src", "shared", "utils", "apiKey.js");
|
||
const sharedApiKeyDest = join(APP_DIR, "src", "shared", "utils");
|
||
if (existsSync(sharedApiKey)) {
|
||
console.log(" 📋 Copying shared utilities...");
|
||
mkdirSync(sharedApiKeyDest, { recursive: true });
|
||
cpSync(sharedApiKey, join(sharedApiKeyDest, "apiKey.js"));
|
||
}
|
||
|
||
// ── Step 10: Ensure data/ directory exists ──────────────────
|
||
mkdirSync(join(APP_DIR, "data"), { recursive: true });
|
||
|
||
// ── Step 10.5: Copy @swc/helpers into standalone ───────────
|
||
// Next.js standalone tracer sometimes omits @swc/helpers from app/node_modules/,
|
||
// causing MODULE_NOT_FOUND at runtime. Always copy it explicitly.
|
||
const swcHelpersSrc = join(ROOT, "node_modules", "@swc", "helpers");
|
||
const swcHelpersDst = join(APP_DIR, "node_modules", "@swc", "helpers");
|
||
if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) {
|
||
console.log(" 📋 Copying @swc/helpers to standalone app/node_modules...");
|
||
mkdirSync(join(APP_DIR, "node_modules", "@swc"), { recursive: true });
|
||
cpSync(swcHelpersSrc, swcHelpersDst, { recursive: true });
|
||
console.log(" ✅ @swc/helpers included in standalone build.");
|
||
}
|
||
|
||
// ── Step 10.6: Remove large binaries from standalone build ──
|
||
// These directories contain platform-native binaries (.node, .asar) that
|
||
// trigger Z_DATA_ERROR during npm pack. They are not needed in the npm package.
|
||
const binaryDirsToRemove = ["vscode-extension", "electron", "logs"];
|
||
for (const dir of binaryDirsToRemove) {
|
||
const targetDir = join(APP_DIR, dir);
|
||
if (existsSync(targetDir)) {
|
||
console.log(` 🧹 Removing app/${dir}/ (not needed in npm package)...`);
|
||
rmSync(targetDir, { recursive: true, force: true });
|
||
console.log(` ✅ app/${dir}/ removed.`);
|
||
}
|
||
}
|
||
|
||
// ── Done ───────────────────────────────────────────────────
|
||
const appPkg = join(APP_DIR, "package.json");
|
||
if (existsSync(appPkg)) {
|
||
const pkg = JSON.parse(readFileSync(appPkg, "utf8"));
|
||
console.log(`\n ✅ Build complete!`);
|
||
console.log(` App directory: app/`);
|
||
console.log(` Server entry: app/server.js`);
|
||
} else {
|
||
console.log(`\n ✅ Build complete! (app/ ready for publish)`);
|
||
}
|
||
|
||
console.log("");
|