chore(release): v3.3.3 — Core UI bugfixes and AutoUpdate repairs

This commit is contained in:
diegosouzapw
2026-03-29 21:18:07 -03:00
parent 76a6d8292c
commit d69e7ec850
13 changed files with 217 additions and 71 deletions

View File

@@ -18,8 +18,8 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -36,8 +36,8 @@ jobs:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -55,8 +55,8 @@ jobs:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
@@ -74,8 +74,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
@@ -90,8 +90,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -109,8 +109,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -129,8 +129,8 @@ jobs:
INITIAL_PASSWORD: ci-test-password-for-integration
DATA_DIR: /tmp/omniroute-ci
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -145,8 +145,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

View File

@@ -25,24 +25,24 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
- name: Set up QEMU (for multi-arch builds)
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -61,7 +61,7 @@ jobs:
echo "Publishing Docker image: $IMAGE_NAME:$VERSION"
- name: Build and push multi-arch image
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
target: runner-base
@@ -83,7 +83,7 @@ jobs:
docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}"
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v5
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -22,7 +22,7 @@ jobs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -70,16 +70,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Cache node_modules
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -146,7 +146,7 @@ jobs:
fi
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: electron-${{ matrix.platform }}
path: release-assets/
@@ -157,12 +157,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true

View File

@@ -43,10 +43,10 @@ jobs:
environment: NPM_TOKEN
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
@@ -111,11 +111,11 @@ jobs:
run: |
VERSION="${{ steps.resolve.outputs.version }}"
TAG="${{ steps.resolve.outputs.tag }}"
echo "Configuring for GitHub Packages..."
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc
npm pkg set name="@diegosouzapw/omniroute"
if [ "$TAG" = "latest" ]; then
npm publish --registry=https://npm.pkg.github.com || echo "⚠️ Version ${VERSION} might already be published on GitHub."
else

View File

@@ -4,6 +4,17 @@
---
## [3.3.3] - 2026-03-29
### 🐛 Bug Fixes
- **CI/CD Reliability:** Patched GitHub Actions to stable dependency versions (`actions/checkout@v4`, `actions/upload-artifact@v4`) to mitigate unannounced builder environment deprecations.
- **Image Fallbacks:** Replaced arbitrary fallback chains in `ProviderIcon.tsx` with explicit asset validation to prevent UI loading `<Image>` components for files that don't exist, eliminating `404` errors in dashboard console logs (#745).
- **Admin Updater:** Dynamic source-installation detection for the dashboard Updater. Safely disables the `Update Now` button when OmniRoute is built locally rather than through npm, prompting for `git pull` (#743).
- **Update ERESOLVE Error:** Injected `package.json` overrides for `react`/`react-dom` and enabled `--legacy-peer-deps` within the internal automatic updater scripts to resolve breaking dependency tree conflicts with `@lobehub/ui`.
---
## [3.3.2] - 2026-03-29
### ✨ New Features

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.3.2
version: 3.3.3
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.3",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.3",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
@@ -162,6 +162,8 @@
},
"overrides": {
"dompurify": "^3.3.2",
"path-to-regexp": "^8.4.0"
"path-to-regexp": "^8.4.0",
"react": "$react",
"react-dom": "$react-dom"
}
}

View File

@@ -546,18 +546,22 @@ export default function HomePageClient({ machineId }) {
<div>
<p className="font-semibold text-sm">Update Available: v{versionInfo.latest}</p>
<p className="text-xs opacity-80 mt-0.5">
{t("updateAvailableDesc") ||
`You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.`}
{versionInfo.autoUpdateSupported
? t("updateAvailableDesc") ||
`You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.`
: versionInfo.autoUpdateError ||
"Manual update required for this installation type."}
</p>
</div>
</div>
<Button
size="sm"
onClick={handleUpdate}
disabled={updating}
onClick={versionInfo.autoUpdateSupported ? handleUpdate : undefined}
disabled={updating || !versionInfo.autoUpdateSupported}
className="shrink-0 ml-4 font-semibold"
title={versionInfo.autoUpdateError || ""}
>
{t("updateNow") || "Update Now"}
{versionInfo.autoUpdateSupported ? t("updateNow") || "Update Now" : "Manual Update"}
</Button>
</div>
)}

View File

@@ -7,6 +7,7 @@ import Card from "@/shared/components/Card";
import Badge from "@/shared/components/Badge";
import QuotaProgressBar from "./QuotaProgressBar";
import { calculatePercentage } from "./utils";
import ProviderIcon from "@/shared/components/ProviderIcon";
const planVariants = {
free: "default",
@@ -70,15 +71,7 @@ export default function ProviderLimitCard({
{provider?.slice(0, 2).toUpperCase() || "PR"}
</span>
) : (
<Image
src={`/providers/${provider}.png`}
alt={provider || t("providerLimits")}
width={40}
height={40}
className="object-contain rounded-lg"
sizes="40px"
onError={() => setImgError(true)}
/>
<ProviderIcon providerId={provider} size={40} />
)}
</div>

View File

@@ -95,6 +95,17 @@ export async function POST(req: NextRequest) {
}
const config = getAutoUpdateConfig();
const validation = await validateAutoUpdateRuntime(config);
if (!validation.supported) {
return NextResponse.json(
{
success: false,
error: validation.reason || "Auto-update is not supported in this environment.",
},
{ status: 400 }
);
}
// If we are in docker-compose mode, use the detached shell script background updates
if (config.mode === "docker-compose") {
@@ -132,9 +143,13 @@ export async function POST(req: NextRequest) {
try {
// Step 1: Install
send({ step: "install", status: "running", message: `Installing omniroute@${latest}...` });
await execFileAsync("npm", ["install", "-g", `omniroute@${latest}`, "--ignore-scripts"], {
timeout: 300000,
});
await execFileAsync(
"npm",
["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"],
{
timeout: 300000,
}
);
send({ step: "install", status: "done", message: `Installed omniroute@${latest}` });
// Step 2: Rebuild native modules (critical for better-sqlite3)

View File

@@ -55,15 +55,26 @@ function shellQuote(value: string): string {
}
function parsePatchCommits(raw: string | undefined): string[] {
return (raw || "").split(/[\s,]+/).map((value) => value.trim()).filter(Boolean);
return (raw || "")
.split(/[\s,]+/)
.map((value) => value.trim())
.filter(Boolean);
}
export function getAutoUpdateConfig(env: NodeJS.ProcessEnv = process.env): AutoUpdateConfig {
const dataDir = env.DATA_DIR || "/tmp/omniroute";
const repoDir = env.AUTO_UPDATE_REPO_DIR || "/workspace/omniroute";
let mode = normalizeMode(env.AUTO_UPDATE_MODE);
if (mode === "npm") {
const fs = require("node:fs");
if (fs.existsSync(path.join(process.cwd(), ".git"))) {
mode = "source" as any;
}
}
return {
mode: normalizeMode(env.AUTO_UPDATE_MODE),
mode,
repoDir,
composeFile: env.AUTO_UPDATE_COMPOSE_FILE || path.join(repoDir, "docker-compose.yml"),
composeProfile: env.AUTO_UPDATE_COMPOSE_PROFILE || "cli",
@@ -97,6 +108,15 @@ export async function validateAutoUpdateRuntime(
execFileImpl: ExecFileLike = execFileAsync,
existsImpl: (targetPath: string) => Promise<boolean> = pathExists
): Promise<AutoUpdateValidation> {
if (config.mode === ("source" as any)) {
return {
supported: false,
reason:
"Manual 'git pull && npm install && npm run build' is required for source installations.",
composeCommand: null,
};
}
if (config.mode !== "docker-compose") {
return { supported: true, reason: null, composeCommand: null };
}
@@ -139,7 +159,8 @@ export async function validateAutoUpdateRuntime(
if (!composeCommand) {
return {
supported: false,
reason: "Neither docker compose nor docker-compose is available inside the OmniRoute container.",
reason:
"Neither docker compose nor docker-compose is available inside the OmniRoute container.",
composeCommand: null,
};
}
@@ -150,7 +171,7 @@ export async function validateAutoUpdateRuntime(
export function buildNpmUpdateScript(latest: string): string {
return [
"set -eu",
`npm install -g omniroute@${latest} --ignore-scripts`,
`npm install -g omniroute@${latest} --ignore-scripts --legacy-peer-deps`,
"if command -v pm2 >/dev/null 2>&1; then",
" pm2 restart omniroute || true",
"fi",
@@ -173,7 +194,7 @@ export function buildDockerComposeUpdateScript({
? 'docker compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"'
: 'docker-compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"';
const patchLines = config.patchCommits.length
? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(' ')}`]
? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(" ")}`]
: [];
return [
@@ -189,13 +210,13 @@ export function buildDockerComposeUpdateScript({
'git config --global --add safe.directory "$REPO_DIR" >/dev/null 2>&1 || true',
'if [ -n "$(git status --porcelain)" ]; then',
' echo "[AutoUpdate] Refusing update: git worktree has local changes." >&2',
' exit 1',
'fi',
" exit 1",
"fi",
'git fetch --tags "$REMOTE"',
'if ! git rev-parse -q --verify "refs/tags/$TARGET_TAG" >/dev/null 2>&1; then',
' echo "[AutoUpdate] Tag $TARGET_TAG not found on remote $REMOTE." >&2',
' exit 1',
'fi',
" exit 1",
"fi",
'backup_branch="autoupdate/pre-${TARGET_TAG#v}-$(date +%Y%m%d-%H%M%S)"',
'git branch "$backup_branch" >/dev/null 2>&1 || true',
'git checkout -B "autoupdate/${TARGET_TAG#v}" "$TARGET_TAG"',

View File

@@ -114,6 +114,99 @@ function GenericProviderIcon({ size }: { size: number }) {
);
}
const KNOWN_PNGS = new Set([
"aimlapi",
"alibaba",
"alicode-intl",
"alicode",
"anthropic-m",
"anthropic",
"antigravity",
"bailian-coding-plan",
"blackbox",
"brave-search",
"brave",
"cerebras",
"claude",
"cline",
"codex",
"cohere",
"continue",
"copilot",
"cursor",
"deepgram",
"deepseek",
"droid",
"exa-search",
"fireworks",
"gemini-cli",
"gemini",
"github",
"glm",
"groq",
"iflow",
"ironclaw",
"kilo-gateway",
"kilocode",
"kimi-coding-apikey",
"kimi-coding",
"kimi",
"kiro",
"longcat",
"minimax-cn",
"minimax",
"mistral",
"nanobot",
"nebius",
"nvidia",
"oai-cc",
"oai-r",
"ollama-cloud",
"openai",
"openclaw",
"openrouter",
"perplexity-search",
"perplexity",
"pollinations",
"qwen",
"roo",
"serper-search",
"serper",
"siliconflow",
"tavily-search",
"tavily",
"together",
"xai",
"zeroclaw",
]);
const KNOWN_SVGS = new Set([
"apikey",
"assemblyai",
"brave",
"cartesia",
"cloudflare-ai",
"comfyui",
"elevenlabs",
"exa-search",
"exa",
"huggingface",
"hyperbolic",
"inworld",
"nanobanana",
"oauth",
"opencode-go",
"opencode-zen",
"opencode",
"playht",
"puter",
"scaleway",
"sdwebui",
"synthetic",
"vertex",
"windsurf",
"zai",
]);
const ProviderIcon = memo(function ProviderIcon({
providerId,
size = 24,
@@ -121,10 +214,14 @@ const ProviderIcon = memo(function ProviderIcon({
className,
style,
}: ProviderIconProps) {
const lobehubId = LOBEHUB_PROVIDER_MAP[providerId.toLowerCase()] ?? null;
const normalizedId = providerId.toLowerCase();
const lobehubId = LOBEHUB_PROVIDER_MAP[normalizedId] ?? null;
const hasPng = KNOWN_PNGS.has(normalizedId);
const hasSvg = KNOWN_SVGS.has(normalizedId);
const [useLobehub, setUseLobehub] = useState(lobehubId !== null);
const [usePng, setUsePng] = useState(true);
const [useSvg, setUseSvg] = useState(true);
const [usePng, setUsePng] = useState(hasPng);
const [useSvg, setUseSvg] = useState(!hasPng && hasSvg);
if (useLobehub && lobehubId) {
return (
@@ -146,12 +243,15 @@ const ProviderIcon = memo(function ProviderIcon({
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<Image
src={`/providers/${providerId}.png`}
src={`/providers/${normalizedId}.png`}
alt={providerId}
width={size}
height={size}
style={{ objectFit: "contain" }}
onError={() => setUsePng(false)}
onError={() => {
setUsePng(false);
setUseSvg(hasSvg);
}}
unoptimized
/>
</span>
@@ -165,7 +265,7 @@ const ProviderIcon = memo(function ProviderIcon({
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<Image
src={`/providers/${providerId}.svg`}
src={`/providers/${normalizedId}.svg`}
alt={providerId}
width={size}
height={size}