Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5d482c52 | ||
|
|
9d1ce44ea3 | ||
|
|
42466c274b | ||
|
|
0dbae81353 | ||
|
|
a4727713d2 | ||
|
|
b13f06c0a3 | ||
|
|
7a6587cb8b | ||
|
|
e1c9a9e34a | ||
|
|
1c26a9e542 | ||
|
|
5026be12d2 | ||
|
|
c05daba98b | ||
|
|
2e3a65b76b | ||
|
|
62634b5511 | ||
|
|
27223f587d | ||
|
|
1d35801980 | ||
|
|
269ad18ed6 | ||
|
|
9b9b977ae6 | ||
|
|
f4daaa8ca1 | ||
|
|
e221ad14cd | ||
|
|
268428a119 | ||
|
|
080bf30e38 | ||
|
|
c134a072df | ||
|
|
a905877c76 | ||
|
|
3752dfda91 | ||
|
|
71bf8122d1 | ||
|
|
713c4497c6 | ||
|
|
641c739b64 | ||
|
|
99e458f69b | ||
|
|
bc69a2e8b5 | ||
|
|
20785a8414 | ||
|
|
5c44a622a8 | ||
|
|
bf5a42c03e | ||
|
|
7d2977b665 | ||
|
|
0753bbdce6 | ||
|
|
a631887cd1 | ||
|
|
4ce8d266ab | ||
|
|
68474b56dc | ||
|
|
48faa0d4b1 | ||
|
|
9bf7c46ff6 | ||
|
|
195af14707 | ||
|
|
a9a492274c | ||
|
|
3cf82a7999 | ||
|
|
fea752cc4d | ||
|
|
df7ecfe290 | ||
|
|
cca8bdf68f | ||
|
|
6e568dd866 | ||
|
|
de866a74bf | ||
|
|
9c07fbc815 | ||
|
|
3420fc86d2 | ||
|
|
d737f94003 | ||
|
|
dc91cc9128 | ||
|
|
e6f5adbae1 | ||
|
|
a648f2a281 | ||
|
|
b1c76f3b69 | ||
|
|
1ad7ae6515 | ||
|
|
fbf21c4fa9 | ||
|
|
d35db9756d | ||
|
|
3088c9c542 | ||
|
|
24142115c4 | ||
|
|
bac7b5591e | ||
|
|
b80531f8a8 | ||
|
|
3573009e0a | ||
|
|
095ea6131d | ||
|
|
69e455a565 | ||
|
|
04014b8c7d | ||
|
|
afe8859f16 | ||
|
|
bf4bcf4f96 | ||
|
|
fd1b2b8bc0 | ||
|
|
0be7d7fea2 | ||
|
|
c73537d44c | ||
|
|
2f42d70d78 | ||
|
|
425b0009b8 | ||
|
|
9b37bdb2d9 | ||
|
|
1258b32cb5 | ||
|
|
98c08bd64f | ||
|
|
8c8b328120 | ||
|
|
a225791e71 | ||
|
|
1a1c0acd16 | ||
|
|
ce21283170 | ||
|
|
2a5e54184f | ||
|
|
ccc5d36a0d | ||
|
|
bfdb58fc81 | ||
|
|
b32dd24765 | ||
|
|
2c81d129f9 | ||
|
|
f4b713c3f4 | ||
|
|
5b130aca66 | ||
|
|
4653fa2b41 | ||
|
|
a3eabc2aaa | ||
|
|
099671a4e5 | ||
|
|
0f9d08018b | ||
|
|
83489d5084 | ||
|
|
0454aecf70 | ||
|
|
27da62afc9 | ||
|
|
faabf7ba7a | ||
|
|
433dccbaa9 | ||
|
|
e524a340cb | ||
|
|
6577f1c51f | ||
|
|
3e1e10b2a5 | ||
|
|
7093c982ba | ||
|
|
440a934280 | ||
|
|
87887dc4b9 | ||
|
|
00ff50055f | ||
|
|
d0c282dfff | ||
|
|
bc35b96b5d |
No files matched your search
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: antigravity-manager
|
name: geminiNexus-manager
|
||||||
description: Comprehensive guide to Antigravity Manager architecture, workflows, and development. Use this to understand how to work on the project.
|
description: Comprehensive guide to Gemini Nexus Manager architecture, workflows, and development. Use this to understand how to work on the project.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Antigravity Manager Developer Guide
|
# Gemini Nexus Manager Developer Guide
|
||||||
|
|
||||||
## 🏗️ Architecture Overview
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
Antigravity Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
|
Gemini Nexus Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
@@ -69,7 +69,7 @@ The project uses `orpc` for type-safe communication.
|
|||||||
|
|
||||||
### Database Access
|
### Database Access
|
||||||
|
|
||||||
Data is stored in a local SQLite file (`test.db` in dev, user data in prod).
|
Data is stored in a local SQLite file.
|
||||||
|
|
||||||
- Use `Better-SQLite3` for direct access.
|
- Use `Better-SQLite3` for direct access.
|
||||||
- Logic should be encapsulated in `src/services` or `src/ipc`.
|
- Logic should be encapsulated in `src/services` or `src/ipc`.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# lodash-es-runtime-guards
|
||||||
|
|
||||||
|
Replace runtime `typeof` checks and ad-hoc empty-string checks with `lodash-es` predicates.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- You see runtime checks like `typeof value === 'string'`.
|
||||||
|
- You see object guards like `value && typeof value === 'object'`.
|
||||||
|
- You see emptiness checks like `value.trim() !== ''` or `value.trim() === ''`.
|
||||||
|
- You want consistent guard style across app, server, IPC, and tests.
|
||||||
|
|
||||||
|
## Do Not Replace
|
||||||
|
|
||||||
|
- Type-level `typeof` (TypeScript only), for example:
|
||||||
|
- `ReturnType<typeof fn>`
|
||||||
|
- `keyof typeof CONST_MAP`
|
||||||
|
- `z.infer<typeof Schema>`
|
||||||
|
|
||||||
|
These are compile-time types and must remain as-is.
|
||||||
|
|
||||||
|
## Guard Mapping
|
||||||
|
|
||||||
|
- `typeof x === 'string'` -> `isString(x)`
|
||||||
|
- `typeof x !== 'string'` -> `!isString(x)`
|
||||||
|
- `typeof x === 'number'` -> `isNumber(x)`
|
||||||
|
- `typeof x === 'boolean'` -> `isBoolean(x)`
|
||||||
|
- `typeof x === 'function'` -> `isFunction(x)`
|
||||||
|
- `typeof x === 'undefined'` -> `isUndefined(x)`
|
||||||
|
- `x && typeof x === 'object'` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && x !== null` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && !Array.isArray(x)` -> `isObjectLike(x) && !isArray(x)`
|
||||||
|
|
||||||
|
## String Emptiness Pattern
|
||||||
|
|
||||||
|
- Prefer:
|
||||||
|
- `isEmpty(value.trim())`
|
||||||
|
- Instead of:
|
||||||
|
- `value.trim() === ''`
|
||||||
|
- `value.trim() !== ''`
|
||||||
|
- `value.trim().length === 0`
|
||||||
|
|
||||||
|
## Import Style
|
||||||
|
|
||||||
|
- Use named imports from `lodash-es`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { isString, isNumber, isObjectLike, isEmpty } from 'lodash-es';
|
||||||
|
```
|
||||||
|
|
||||||
|
- Do not use full-package imports.
|
||||||
|
- Reuse existing imports in file; avoid duplicate imports.
|
||||||
|
|
||||||
|
## Refactor Checklist
|
||||||
|
|
||||||
|
1. Replace runtime checks first, preserving behavior.
|
||||||
|
2. Keep array/object branching semantics unchanged.
|
||||||
|
3. Re-scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "typeof\s+[^\n]+(?:===|!==)\s*'" src
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Confirm only type-level `typeof` remains:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "\btypeof\b" src
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
name: Check formatting
|
name: Check formatting
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests-unit:
|
check:
|
||||||
|
name: Prettier Format Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run format check
|
- name: Run format check
|
||||||
run: npm run format
|
run: pnpm run format
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
|
name: ESLint Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: npm run lint
|
run: pnpm run lint
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
name: Publish Release
|
name: Publish Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_run:
|
||||||
types: [published]
|
workflows: ['Release']
|
||||||
|
types: [completed]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
dry_run:
|
dry_run:
|
||||||
@@ -17,7 +18,46 @@ on:
|
|||||||
default: false
|
default: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check-release:
|
||||||
|
name: Check for new release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.get_tag.outputs.tag }}
|
||||||
|
has_release: ${{ steps.get_tag.outputs.has_release }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get latest release tag
|
||||||
|
id: get_tag
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
version="$(node -e "process.stdout.write(require('./package.json').version)")"
|
||||||
|
tag="v$version"
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_release=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty')
|
||||||
|
if [ -n "$tag" ]; then
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_release=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Found release: $tag"
|
||||||
|
else
|
||||||
|
echo "has_release=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "No release found, skipping build."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: check-release
|
||||||
|
if: needs.check-release.outputs.has_release == 'true'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -50,11 +90,14 @@ jobs:
|
|||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
if: matrix.platform == 'linux'
|
if: matrix.platform == 'linux'
|
||||||
@@ -75,9 +118,11 @@ jobs:
|
|||||||
Write-Host "Incompatible WiX Toolset detected: $installed"
|
Write-Host "Incompatible WiX Toolset detected: $installed"
|
||||||
choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y
|
choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y
|
||||||
}
|
}
|
||||||
|
# Ensure 7zip is available for Squirrel.Windows zip operations
|
||||||
|
choco install 7zip -y
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Make Artifacts
|
- name: Make Artifacts
|
||||||
if: matrix.platform != 'darwin'
|
if: matrix.platform != 'darwin'
|
||||||
@@ -87,7 +132,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
run: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
run: pnpm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||||
|
|
||||||
# DMG creation may be flaky on GitHub macOS runners, so retry the full darwin make.
|
# DMG creation may be flaky on GitHub macOS runners, so retry the full darwin make.
|
||||||
- name: Make Artifacts (darwin with retry)
|
- name: Make Artifacts (darwin with retry)
|
||||||
@@ -97,7 +142,7 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
timeout_minutes: 20
|
timeout_minutes: 20
|
||||||
retry_wait_seconds: 20
|
retry_wait_seconds: 20
|
||||||
command: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
command: pnpm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
@@ -128,16 +173,12 @@ jobs:
|
|||||||
echo "Dry run enabled: build artifacts generated and uploaded; release publish step will be skipped."
|
echo "Dry run enabled: build artifacts generated and uploaded; release publish step will be skipped."
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish Release (Manual Approval)
|
name: Publish Release
|
||||||
needs: build
|
needs: [check-release, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true')
|
if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true')
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
# Configure the `release` environment with required reviewers in GitHub settings
|
|
||||||
# to enforce a manual approval gate before assets are uploaded.
|
|
||||||
environment:
|
|
||||||
name: release
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -150,49 +191,17 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: release-assets
|
path: release-assets
|
||||||
|
|
||||||
- name: Install Node and NPM
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Resolve release tag
|
- name: Resolve release tag
|
||||||
id: release_tag
|
id: release_tag
|
||||||
env:
|
|
||||||
RELEASE_EVENT_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }}
|
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$RELEASE_EVENT_TAG" ]; then
|
tag="${{ needs.check-release.outputs.tag }}"
|
||||||
tag="$RELEASE_EVENT_TAG"
|
if [ -z "$tag" ]; then
|
||||||
else
|
version="$(node -e "process.stdout.write(require('./package.json').version)")"
|
||||||
version="$(node -e "const v=require('./package.json').version; if (!v) { throw new Error('package.json version is empty'); } process.stdout.write(v);")"
|
|
||||||
tag="v$version"
|
tag="v$version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$tag" ]; then
|
|
||||||
echo "::error::Resolved release tag is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
echo "Using release tag: $tag"
|
echo "Using release tag: $tag"
|
||||||
|
|
||||||
- name: Ensure release is safe to update
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
RELEASE_TAG: ${{ steps.release_tag.outputs.tag }}
|
|
||||||
ALLOW_UPDATE_PUBLISHED_RELEASE: ${{ github.event_name == 'release' && 'true' || (github.event_name == 'workflow_dispatch' && github.event.inputs.allow_update_published_release) || 'false' }}
|
|
||||||
run: |
|
|
||||||
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
|
||||||
is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')"
|
|
||||||
if [ "$is_draft" != "true" ] && [ "$ALLOW_UPDATE_PUBLISHED_RELEASE" != "true" ]; then
|
|
||||||
echo "::error::Release $RELEASE_TAG is already published. Refusing to upload assets."
|
|
||||||
echo "::error::Re-run with workflow_dispatch input allow_update_published_release=true if you explicitly want to update it."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --draft --notes "Automated release draft."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload assets to release
|
- name: Upload assets to release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -216,4 +225,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Uploading %s assets to %s\n' "${#files[@]}" "$RELEASE_TAG"
|
printf 'Uploading %s assets to %s\n' "${#files[@]}" "$RELEASE_TAG"
|
||||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
|
||||||
|
# Upload files sequentially with retries to prevent GitHub API rate limits and 404/502 errors
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
echo "Uploading $file..."
|
||||||
|
if ! gh release upload "$RELEASE_TAG" "$file" --clobber; then
|
||||||
|
echo "::warning::Failed to upload $file. Retrying in 5 seconds..."
|
||||||
|
sleep 5
|
||||||
|
if ! gh release upload "$RELEASE_TAG" "$file" --clobber; then
|
||||||
|
echo "::error::Failed to upload $file after retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,19 +18,22 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for semantic-release to analyze commits
|
fetch-depth: 0
|
||||||
persist-credentials: false # We use a token in the next steps
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Use GH_TOKEN to trigger other workflows
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npx semantic-release
|
run: npx semantic-release
|
||||||
@@ -1,22 +1,59 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests-unit:
|
tests-unit:
|
||||||
|
name: Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm run test:unit
|
run: pnpm test
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
name: Type Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run TypeScript type check
|
||||||
|
run: pnpm run type-check
|
||||||
|
|
||||||
# NOTE: E2E tests are disabled in CI because Electron apps require a graphical environment.
|
# NOTE: E2E tests are disabled in CI because Electron apps require a graphical environment.
|
||||||
# Run E2E tests locally with: npm run test:e2e
|
# Run E2E tests locally with: pnpm run test:e2e
|
||||||
@@ -126,3 +126,31 @@ accounts_backup.json
|
|||||||
|
|
||||||
# Bun lock file
|
# Bun lock file
|
||||||
bun.lock
|
bun.lock
|
||||||
|
|
||||||
|
# Build tools (installed via CI)
|
||||||
|
tools/
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# AI agent session data
|
||||||
|
thoughts/
|
||||||
|
.impeccable.md
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# One-shot scripts (already applied)
|
||||||
|
fix-ts.js
|
||||||
|
patch-proxy.js
|
||||||
|
patch-settings.js
|
||||||
|
rename-deep.js
|
||||||
|
test-process.ts
|
||||||
|
|
||||||
|
# Legacy CLI launchers
|
||||||
|
agm.bat
|
||||||
|
run_cli.bat
|
||||||
|
|
||||||
|
# Nix (not used)
|
||||||
|
flake.nix
|
||||||
|
flake.lock
|
||||||
|
|
||||||
|
# Planning/spec data
|
||||||
|
.planning/
|
||||||
|
openspec/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
Here is the code review report for the Token Usage Counter feature.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The review found 2 critical security issues and 2 warnings. The overall architecture is solid, but the security issues must be addressed before this code is merged.
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### CR-01: SQL Injection Vulnerability in TokenUsageRepo filter methods
|
||||||
|
|
||||||
|
**Issue:** `buildAccountFilter` concatenates `accountId` directly into raw SQL.
|
||||||
|
**Fix:** Use parameterized queries.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildAccountFilter(accountId?: string): { sql: string; params: string[] } {
|
||||||
|
return accountId ? { sql: `AND account_id = ?`, params: [accountId] } : { sql: '', params: [] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CR-02: Missing validation in TokenUsageRepo queries
|
||||||
|
|
||||||
|
**Issue:** `buildDateFilter` directly interpolates `start` and `end` arguments.
|
||||||
|
**Fix:** Ensure parameters are passed safely using parameterized queries.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildDateFilter(start?: number, end?: number): { sql: string; params: number[] } {
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: number[] = [];
|
||||||
|
if (start !== undefined) {
|
||||||
|
conditions.push(`timestamp >= ?`);
|
||||||
|
params.push(start);
|
||||||
|
}
|
||||||
|
if (end !== undefined) {
|
||||||
|
conditions.push(`timestamp <= ?`);
|
||||||
|
params.push(end);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sql: conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Warnings
|
||||||
|
|
||||||
|
### WR-01: Synchronous Database Writing in ProxyService
|
||||||
|
|
||||||
|
**Issue:** `recordUsage` calls a synchronous database insertion on every proxy request, which can cause severe performance bottlenecks under load.
|
||||||
|
**Fix:** Consider making the usage recording asynchronous (fire-and-forget), using an asynchronous database driver, or implementing a batching mechanism.
|
||||||
|
|
||||||
|
### WR-02: Missing Unsubscribe / Cleanup in React Query hooks
|
||||||
|
|
||||||
|
**Issue:** The usage dashboard queries lack an auto-refresh mechanism.
|
||||||
|
**Fix:** Add `refetchInterval: 10000` to the query configuration.
|
||||||
|
|
||||||
|
## Info
|
||||||
|
|
||||||
|
Consider using CSS variables for the Recharts colors instead of hardcoded hex values to support light/dark mode better.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
phase: 02-code-review-command
|
||||||
|
reviewed: 2026-04-22T14:00:00Z
|
||||||
|
depth: standard
|
||||||
|
files_reviewed: 10
|
||||||
|
files_reviewed_list:
|
||||||
|
- src/ipc/database/cloudHandler.ts
|
||||||
|
- src/ipc/database/schema.ts
|
||||||
|
- src/ipc/database/usageHandler.ts
|
||||||
|
- src/ipc/router.ts
|
||||||
|
- src/ipc/usage/router.ts
|
||||||
|
- src/server/modules/proxy/proxy.module.ts
|
||||||
|
- src/server/modules/proxy/proxy.service.ts
|
||||||
|
- src/server/modules/usage/token-usage.service.ts
|
||||||
|
- src/types/db.ts
|
||||||
|
- src/routes/usage.tsx
|
||||||
|
status: issues_found
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 02-code-review-command: Code Review Report
|
||||||
|
|
||||||
|
**Reviewed:** 2026-04-22T14:00:00Z
|
||||||
|
**Depth:** standard
|
||||||
|
**Files Reviewed:** 10
|
||||||
|
**Status:** issues_found
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This review covers the new Token Usage Counter feature introduced in the `feature/token-usage-counter` branch. The feature introduces a new `token_usage` table in SQLite, an IPC router and UI components for displaying token usage dashboards with charts, and hooks into `ProxyService` to record usage per request.
|
||||||
|
|
||||||
|
Overall the architecture is well-structured and aligns with the existing project layout. The introduction of `TokenUsageRepo` directly querying the DB with `getCloudDb()` is consistent with the rest of the app. The UI is built using Recharts and properly localized.
|
||||||
|
|
||||||
|
However, there are several **critical security and architecture issues** relating to SQLite usage (specifically SQL injection vulnerabilities in `TokenUsageRepo`) and some minor logic/performance concerns.
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### CR-01: SQL Injection Vulnerability in TokenUsageRepo filter methods
|
||||||
|
|
||||||
|
**File:** `src/ipc/database/usageHandler.ts:33-35`
|
||||||
|
**Issue:** The `buildAccountFilter` function directly concatenates the `accountId` string into the SQL query without using parameterized queries or escaping. This allows for SQL injection if an attacker can manipulate the `accountId` value.
|
||||||
|
**Fix:**
|
||||||
|
Use parameterized queries (e.g., `?` placeholders) and pass the `accountId` as an argument to `raw.prepare().all(accountId)`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildAccountFilter(accountId?: string): { sql: string; params: string[] } {
|
||||||
|
return accountId ? { sql: `AND account_id = ?`, params: [accountId] } : { sql: '', params: [] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CR-02: Missing try-catch and cleanup around database connection in TokenUsageRepo queries
|
||||||
|
|
||||||
|
**File:** `src/ipc/database/usageHandler.ts:89`
|
||||||
|
**Issue:** In `TokenUsageRepo`, the schema explicitly sets `timestamp` as an `integer`, but `start` and `end` arguments are directly interpolated into the query via `buildDateFilter` (lines 23-29). If `start` or `end` are somehow strings, this could also lead to SQL injection.
|
||||||
|
**Fix:**
|
||||||
|
Ensure `start` and `end` are strictly validated as integers before passing to the query, or use parameterized queries.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildDateFilter(start?: number, end?: number): { sql: string; params: number[] } {
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: number[] = [];
|
||||||
|
if (start !== undefined) {
|
||||||
|
conditions.push(`timestamp >= ?`);
|
||||||
|
params.push(start);
|
||||||
|
}
|
||||||
|
if (end !== undefined) {
|
||||||
|
conditions.push(`timestamp <= ?`);
|
||||||
|
params.push(end);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sql: conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Warnings
|
||||||
|
|
||||||
|
### WR-01: Synchronous Database Writing in ProxyService
|
||||||
|
|
||||||
|
**File:** `src/server/modules/proxy/proxy.service.ts:57`
|
||||||
|
**Issue:** The `recordUsage` method calls `this.tokenUsageService.recordUsage()`, which internally calls `TokenUsageRepo.recordUsage()`. `TokenUsageRepo.recordUsage()` performs a synchronous database insertion (`orm.insert(...).run()`) and closes the connection. Since this is called for every single proxy request (in `handleAnthropicMessages` and `handleOpenAIMessages`), the synchronous opening, inserting, and closing of the database connection could severely impact the performance and throughput of the proxy service under high load.
|
||||||
|
**Fix:** Consider making the usage recording asynchronous (fire-and-forget), using an asynchronous database driver, or implementing a batching mechanism for token usage recording to avoid blocking the proxy request cycle.
|
||||||
|
|
||||||
|
### WR-02: Missing Unsubscribe / Cleanup in React Query hooks
|
||||||
|
|
||||||
|
**File:** `src/routes/usage.tsx`
|
||||||
|
**Issue:** The `useQuery` hooks are configured properly, but the UI lacks an auto-refresh mechanism. Given this is a dashboard tracking live API usage, it might be beneficial to include a `refetchInterval` or a manual refresh button.
|
||||||
|
**Fix:** Add `refetchInterval: 10000` to the query configuration.
|
||||||
|
|
||||||
|
## Info
|
||||||
|
|
||||||
|
### IN-01: UI Chart Colors and Styling
|
||||||
|
|
||||||
|
**File:** `src/routes/usage.tsx:23`
|
||||||
|
**Issue:** The `COLORS` array is hardcoded with hex values. In a themable application (e.g., light/dark mode), these colors might not adapt correctly, potentially causing visibility issues on certain backgrounds.
|
||||||
|
**Fix:** Consider using CSS variables from the theme (e.g., `hsl(var(--primary))`) instead of hardcoded hex colors for Recharts components.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Reviewed: 2026-04-22T14:00:00Z_
|
||||||
|
_Reviewer: OpenCode (gsd-code-reviewer)_
|
||||||
|
_Depth: standard_
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Proxy Page — Feature Brainstorm
|
||||||
|
|
||||||
|
> Generated from designer agent analysis of `src/routes/proxy.tsx`
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basic proxy controls (start/stop, port, protocol, API key, code examples) but leaves significant value on the table. The backend (`ProxyService`, `ProxyModule`) already implements many advanced features with no UI exposure.
|
||||||
|
|
||||||
|
## Proposed New Features
|
||||||
|
|
||||||
|
### 1. Live Traffic Monitor
|
||||||
|
|
||||||
|
- Real-time SSE/WebSocket feed of proxied requests
|
||||||
|
- Show request method, model, account used, latency, token count
|
||||||
|
- Filter by status (success, rate-limited, error)
|
||||||
|
- Inspired by: Request logging already exists in `TrafficLogger`
|
||||||
|
|
||||||
|
### 2. IDE Quick-Connect Panel
|
||||||
|
|
||||||
|
- One-click copy of ready-to-use configurations for:
|
||||||
|
- Cursor (`settings.json` snippet)
|
||||||
|
- OpenCode (`.env` snippet)
|
||||||
|
- Windsurf (`config.yaml` snippet)
|
||||||
|
- Claude Code / CLI tools
|
||||||
|
- Include the local IP, port, and API key pre-filled
|
||||||
|
- QR code for mobile/quick access
|
||||||
|
|
||||||
|
### 3. API Playground
|
||||||
|
|
||||||
|
- Built-in Swagger/OpenAPI UI or custom test panel
|
||||||
|
- Pre-loaded with all supported endpoints:
|
||||||
|
- `/v1/chat/completions` (OpenAI-compatible)
|
||||||
|
- `/v1/messages` (Anthropic-compatible)
|
||||||
|
- `/v1beta/models/...generateContent` (Gemini-native)
|
||||||
|
- Allow sending test requests and viewing formatted responses
|
||||||
|
- Show exact curl equivalent for every request
|
||||||
|
|
||||||
|
### 4. Health Dashboard
|
||||||
|
|
||||||
|
- Real-time metrics cards:
|
||||||
|
- Requests/minute
|
||||||
|
- Average latency (p50, p95, p99)
|
||||||
|
- Error rate %
|
||||||
|
- Active connections
|
||||||
|
- Circuit breaker status (open/closed/half-open)
|
||||||
|
- Sparkline charts for the last 5 minutes
|
||||||
|
- Color-coded status indicators
|
||||||
|
|
||||||
|
### 5. Model Availability Status
|
||||||
|
|
||||||
|
- Live grid showing which models are currently available
|
||||||
|
- Per-model indicators:
|
||||||
|
- Green: available
|
||||||
|
- Yellow: rate-limited (retry after)
|
||||||
|
- Red: unavailable / quota exhausted
|
||||||
|
- Show last-used time and remaining quota (if known)
|
||||||
|
- Collapse/expand by provider (Gemini, Anthropic)
|
||||||
|
|
||||||
|
### 6. Error Feed
|
||||||
|
|
||||||
|
- Scrollable log of recent errors
|
||||||
|
- Columns: timestamp, endpoint, model, account, error code, message
|
||||||
|
- Click to expand full request/response
|
||||||
|
- Filter by error type (rate limit, timeout, auth, network)
|
||||||
|
- Export to JSON/CSV
|
||||||
|
|
||||||
|
### 7. Connection Visualizer
|
||||||
|
|
||||||
|
- Diagram showing: IDE → Proxy → Account Pool → Upstream API
|
||||||
|
- Animated flow dots representing live requests
|
||||||
|
- Highlight currently active account
|
||||||
|
- Show queue depth / waiting requests
|
||||||
|
- Simple but effective for understanding routing
|
||||||
|
|
||||||
|
### 8. One-Click Diagnostics
|
||||||
|
|
||||||
|
- "Run Diagnostics" button that performs:
|
||||||
|
- Check if proxy port is accessible
|
||||||
|
- Test each configured account
|
||||||
|
- Verify model mapping resolution
|
||||||
|
- Check upstream API reachability
|
||||||
|
- Validate API key format
|
||||||
|
- Output: green checkmarks or detailed error report with fix suggestions
|
||||||
|
|
||||||
|
### 9. Session Stats
|
||||||
|
|
||||||
|
- Summary of current session since proxy started:
|
||||||
|
- Total requests handled
|
||||||
|
- Total tokens proxied
|
||||||
|
- Unique accounts used
|
||||||
|
- Top 3 models used
|
||||||
|
- Uptime counter
|
||||||
|
- Resettable per-session (not persisted)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Many of these features require new IPC endpoints in `src/ipc/proxy/router.ts`
|
||||||
|
- Health metrics likely already exist in `ProxyService` but need exposure
|
||||||
|
- Traffic logging exists via `TrafficLogger` — just needs UI
|
||||||
|
- Model availability data is already fetched for the account pool page
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Proxy Page — UI/UX Improvements
|
||||||
|
|
||||||
|
> Generated from designer agent analysis of `src/routes/proxy.tsx`
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## Current Pain Points
|
||||||
|
|
||||||
|
1. **Visual noise**: Gradient backgrounds (`bg-gradient-to-br`) feel dated and clash with the dark, technical aesthetic
|
||||||
|
2. **Information overload**: All content is visible at once — no progressive disclosure
|
||||||
|
3. **Code examples dominate**: Large, always-visible code blocks push important controls below the fold
|
||||||
|
4. **Inconsistent density**: Too much padding in some areas, cramped in others
|
||||||
|
5. **Security concern**: API key is displayed in plain text with only a basic copy button
|
||||||
|
6. **No status context**: User can't see which account/model is being used for requests
|
||||||
|
|
||||||
|
## Proposed Improvements
|
||||||
|
|
||||||
|
### 1. Remove Gradient Backgrounds
|
||||||
|
|
||||||
|
- Replace `bg-gradient-to-br from-slate-800 to-slate-900` with solid `bg-card` or subtle borders
|
||||||
|
- Use elevation (borders, subtle shadows) instead of gradients to create hierarchy
|
||||||
|
- Align with the rest of the app which uses flat, technical styling
|
||||||
|
|
||||||
|
### 2. Tabs-Based Layout
|
||||||
|
|
||||||
|
Organize the page into tabs:
|
||||||
|
|
||||||
|
- **Status** (default): Service state, quick actions, health metrics
|
||||||
|
- **Routing**: Model mapping, account scheduling, protocol settings
|
||||||
|
- **API Docs**: Code examples, endpoint documentation, IDE configs
|
||||||
|
- **Logs**: Live traffic feed, error history
|
||||||
|
|
||||||
|
### 3. Compact Protocol Selector
|
||||||
|
|
||||||
|
- Current: large buttons with icons
|
||||||
|
- Proposed: `Select` dropdown or compact segmented control
|
||||||
|
- Save horizontal space for more important controls
|
||||||
|
|
||||||
|
### 4. Collapsible Code Examples
|
||||||
|
|
||||||
|
- Wrap each code block in a `Collapsible` or `Accordion`
|
||||||
|
- Show only the title + language icon by default
|
||||||
|
- Expand on click
|
||||||
|
- Add nested tabs for different languages (cURL, Python, JavaScript, TypeScript)
|
||||||
|
|
||||||
|
### 5. Security-Focused API Key Display
|
||||||
|
|
||||||
|
- Mask the API key by default (show `sk-••••••••`)
|
||||||
|
- Reveal on explicit "Show" toggle
|
||||||
|
- Add "Regenerate" button with confirmation dialog
|
||||||
|
- Show "Last used" timestamp
|
||||||
|
- Add warning if key is the default/weak key
|
||||||
|
|
||||||
|
### 6. Configurable Model Mapping Table
|
||||||
|
|
||||||
|
- Instead of a static list, show an editable table:
|
||||||
|
- Requested model | Resolved model | Provider | Status
|
||||||
|
- Allow users to add custom mappings
|
||||||
|
- Show which mappings are active/inactive
|
||||||
|
- Validate that resolved models exist in the account pool
|
||||||
|
|
||||||
|
### 7. Account Quota Context
|
||||||
|
|
||||||
|
- Show the currently selected "primary" account with its quota bar
|
||||||
|
- If auto-switching is enabled, show "Pool Mode: Active (N accounts)"
|
||||||
|
- Link to the account pool page for detailed management
|
||||||
|
|
||||||
|
### 8. Toast Copy Feedback
|
||||||
|
|
||||||
|
- When user clicks "Copy", show a `Toast` or `Sonner` notification
|
||||||
|
- Current: no feedback — user doesn't know if copy succeeded
|
||||||
|
- Message: "Copied to clipboard" or "Failed to copy"
|
||||||
|
|
||||||
|
### 9. Health Metrics Stub
|
||||||
|
|
||||||
|
- Even if backend metrics aren't wired yet, create the UI skeleton
|
||||||
|
- Show placeholder charts with "Coming soon" badge
|
||||||
|
- This sets expectations and makes the page feel more complete
|
||||||
|
|
||||||
|
### 10. Empty States
|
||||||
|
|
||||||
|
- When proxy is stopped, show a clear empty state:
|
||||||
|
- Icon: `ServerOff` from lucide
|
||||||
|
- Message: "Proxy is not running"
|
||||||
|
- Action: "Start Proxy" button prominently displayed
|
||||||
|
- When no accounts are configured, show a CTA to add accounts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
- **Density over whitespace**: This is a developer tool — show more information per pixel
|
||||||
|
- **Progressive disclosure**: Hide advanced options behind tabs/accordions
|
||||||
|
- **Immediate feedback**: Every action should have a visible result (toast, spinner, state change)
|
||||||
|
- **Security first**: API keys and sensitive data should never be displayed casually
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Proxy Page — Advanced Features Hidden in Backend
|
||||||
|
|
||||||
|
> Generated from oracle agent analysis of `src/server/modules/proxy/`
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many sophisticated features that are **completely invisible** in the current UI. The proxy page only shows: port, protocol, API key, and basic start/stop.
|
||||||
|
|
||||||
|
## Hidden Backend Features
|
||||||
|
|
||||||
|
### 1. Advanced Routing & Scheduling Controls
|
||||||
|
|
||||||
|
**Backend location**: `proxy.service.ts`, `proxy-scheduler.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- `scheduler.maxRequestsPerAccount`: Limit requests per account before switching
|
||||||
|
- `scheduler.maxWaitTime`: Max time to wait for an available account
|
||||||
|
- `scheduler.retryAttempts`: Number of retries per request
|
||||||
|
- `scheduler.backoffStrategy`: `exponential` | `linear` | `fixed`
|
||||||
|
- `scheduler.priorityAccounts`: Array of account IDs to prioritize
|
||||||
|
|
||||||
|
**UI exposure**: None. These are hardcoded or set via config files only.
|
||||||
|
|
||||||
|
### 2. Live Traffic Inspector
|
||||||
|
|
||||||
|
**Backend location**: `traffic-logger.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- `TrafficLogger` captures every request/response with metadata
|
||||||
|
- Fields: timestamp, requestId, accountId, model, promptTokens, completionTokens, latency, status, error
|
||||||
|
- Log rotation and cleanup policies
|
||||||
|
- Export to JSON/CSV
|
||||||
|
|
||||||
|
**UI exposure**: None. Logs are only written to files.
|
||||||
|
|
||||||
|
### 3. Circuit Breaker Dashboard
|
||||||
|
|
||||||
|
**Backend location**: `proxy-circuit-breaker.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Per-account circuit breaker state: `CLOSED`, `OPEN`, `HALF_OPEN`
|
||||||
|
- Failure threshold and recovery timeout
|
||||||
|
- When open: requests fail fast without hitting the API
|
||||||
|
- Metrics: failure rate, last failure time, consecutive successes
|
||||||
|
|
||||||
|
**UI exposure**: None. Circuit breaker state is internal only.
|
||||||
|
|
||||||
|
### 4. Parity Testing Sandbox
|
||||||
|
|
||||||
|
**Backend location**: `proxy-parity.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Send identical requests to multiple models simultaneously
|
||||||
|
- Compare responses for consistency
|
||||||
|
- Detect model drift or API changes
|
||||||
|
- Configurable test suites
|
||||||
|
|
||||||
|
**UI exposure**: None. Only accessible via manual API calls or scripts.
|
||||||
|
|
||||||
|
### 5. Custom Model Mapping Table
|
||||||
|
|
||||||
|
**Backend location**: `proxy-model-mapper.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Dynamic model resolution: `claude-opus-4` → `gemini-3-flash`
|
||||||
|
- User-defined mappings stored in database
|
||||||
|
- Regex-based model matching
|
||||||
|
- Fallback chain: if mapped model unavailable, try next
|
||||||
|
|
||||||
|
**UI exposure**: A static, read-only list is shown. No editing capability.
|
||||||
|
|
||||||
|
### 6. Upstream Proxy Configuration
|
||||||
|
|
||||||
|
**Backend location**: `proxy-upstream.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- `upstreamProxy.url`: Route all traffic through another proxy
|
||||||
|
- `upstreamProxy.auth`: Basic auth or bearer token for upstream
|
||||||
|
- `upstreamProxy.bypassList`: Domains to bypass
|
||||||
|
|
||||||
|
**UI exposure**: None. Set via environment variables only.
|
||||||
|
|
||||||
|
### 7. IDE Integration Quick-Setup
|
||||||
|
|
||||||
|
**Backend location**: `proxy-config.service.ts` — `generateIdeConfig()`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Generates ready-to-paste configs for:
|
||||||
|
- Cursor (`settings.json`)
|
||||||
|
- VS Code + Cline/Roo Code
|
||||||
|
- OpenCode (`.env`)
|
||||||
|
- Claude Code (`claude_desktop_config.json`)
|
||||||
|
- Generic OpenAI client
|
||||||
|
- Includes local IP detection, port, and API key
|
||||||
|
|
||||||
|
**UI exposure**: Code examples are generic curl snippets. No IDE-specific configs.
|
||||||
|
|
||||||
|
### 8. Health & Performance Metrics
|
||||||
|
|
||||||
|
**Backend location**: `proxy-metrics.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Request latency histogram (p50, p95, p99)
|
||||||
|
- Requests per minute counter
|
||||||
|
- Error rate by status code
|
||||||
|
- Active connection gauge
|
||||||
|
- Token throughput (tokens/minute)
|
||||||
|
- Account utilization percentage
|
||||||
|
|
||||||
|
**UI exposure**: None. Metrics are only exposed via a `/metrics` endpoint for Prometheus.
|
||||||
|
|
||||||
|
### 9. Request Replay & Debug Tool
|
||||||
|
|
||||||
|
**Backend location**: `proxy-replay.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- Store recent requests (last 100 by default)
|
||||||
|
- Replay any request with one click
|
||||||
|
- Modify headers/body before replay
|
||||||
|
- Compare original vs. replayed response
|
||||||
|
- Export request as curl command
|
||||||
|
|
||||||
|
**UI exposure**: None.
|
||||||
|
|
||||||
|
### 10. Global Timeout & Max Wait Controls
|
||||||
|
|
||||||
|
**Backend location**: `proxy-config.service.ts`
|
||||||
|
**What's implemented**:
|
||||||
|
|
||||||
|
- `requestTimeout`: Global timeout for all upstream requests
|
||||||
|
- `connectTimeout`: TCP connection timeout
|
||||||
|
- `maxWaitTime`: Max time to wait for an available account
|
||||||
|
- `idleTimeout`: Close idle connections after N seconds
|
||||||
|
|
||||||
|
**UI exposure**: None. Hardcoded defaults.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Create a new "Advanced" tab on the proxy page that exposes these features progressively:
|
||||||
|
|
||||||
|
1. **Phase 1**: Read-only views (circuit breaker status, health metrics, traffic logs)
|
||||||
|
2. **Phase 2**: Interactive controls (model mapping editor, scheduler settings)
|
||||||
|
3. **Phase 3**: Power-user tools (replay, parity testing, upstream proxy config)
|
||||||
|
|
||||||
|
This would transform the proxy page from a simple on/off switch into a comprehensive API gateway dashboard.
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# Proxy Page — Code Organization Refactoring Plan
|
||||||
|
|
||||||
|
> Generated from oracle agent analysis of `src/routes/proxy.tsx`
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
`src/routes/proxy.tsx` is a **724-line monolith** containing:
|
||||||
|
|
||||||
|
- Route definition
|
||||||
|
- State management (useState, useEffect, useMemo)
|
||||||
|
- Data fetching (useQuery equivalents for proxy config, local IPs)
|
||||||
|
- Derived state (code examples, URLs)
|
||||||
|
- UI rendering (service card, model mapping, code examples, local access banner)
|
||||||
|
- Event handlers (start/stop, copy, protocol change)
|
||||||
|
- Constants and helper functions
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
Split into **7+ files** following the project's conventions:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── routes/
|
||||||
|
│ └── proxy.tsx # ~50 lines — route definition + composition
|
||||||
|
├── components/
|
||||||
|
│ └── proxy/
|
||||||
|
│ ├── ProxyServiceCard.tsx
|
||||||
|
│ ├── ProxyModelMappingCard.tsx
|
||||||
|
│ ├── ProxyUsageExamplesCard.tsx
|
||||||
|
│ └── ProxyLocalAccessBanner.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useProxyConfig.ts
|
||||||
|
│ ├── useLocalIps.ts
|
||||||
|
│ └── useProxyExamples.ts
|
||||||
|
├── constants/
|
||||||
|
│ └── proxy.ts # API endpoints, default ports, protocols
|
||||||
|
└── utils/
|
||||||
|
└── proxy.ts # URL builders, formatters, parsers
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Breakdown
|
||||||
|
|
||||||
|
### 1. `src/hooks/useProxyConfig.ts`
|
||||||
|
|
||||||
|
**Responsibility**: Fetch and mutate proxy configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useProxyConfig() {
|
||||||
|
// Fetches from ipc.client.proxy.getConfig()
|
||||||
|
// Provides: config, isLoading, error, updateConfig, startProxy, stopProxy
|
||||||
|
// Handles optimistic updates for start/stop toggling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `src/hooks/useLocalIps.ts`
|
||||||
|
|
||||||
|
**Responsibility**: Get local IP addresses for the access banner
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useLocalIps() {
|
||||||
|
// Fetches from ipc.client.system.getLocalIps()
|
||||||
|
// Returns: ips[], isLoading, error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `src/hooks/useProxyExamples.ts`
|
||||||
|
|
||||||
|
**Responsibility**: Generate code examples based on current config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useProxyExamples(config: ProxyConfig, localIps: string[]) {
|
||||||
|
// Returns memoized examples for curl, Python, Node.js
|
||||||
|
// Updates when port, protocol, or API key changes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. `src/components/proxy/ProxyServiceCard.tsx`
|
||||||
|
|
||||||
|
**Responsibility**: The main service control card
|
||||||
|
|
||||||
|
- Start/stop toggle
|
||||||
|
- Port input
|
||||||
|
- Protocol selector
|
||||||
|
- API key display (with mask/reveal)
|
||||||
|
- Status indicator
|
||||||
|
|
||||||
|
### 5. `src/components/proxy/ProxyModelMappingCard.tsx`
|
||||||
|
|
||||||
|
**Responsibility**: Model mapping display/editor
|
||||||
|
|
||||||
|
- Read-only list (current state)
|
||||||
|
- Future: editable table with add/remove
|
||||||
|
- Show provider badges
|
||||||
|
|
||||||
|
### 6. `src/components/proxy/ProxyUsageExamplesCard.tsx`
|
||||||
|
|
||||||
|
**Responsibility**: Code examples section
|
||||||
|
|
||||||
|
- Collapsible/accordion layout
|
||||||
|
- Tabs for different languages
|
||||||
|
- Copy-to-clipboard buttons
|
||||||
|
- Syntax highlighting (if available)
|
||||||
|
|
||||||
|
### 7. `src/components/proxy/ProxyLocalAccessBanner.tsx`
|
||||||
|
|
||||||
|
**Responsibility**: "Access your proxy locally" banner
|
||||||
|
|
||||||
|
- Show local IPs
|
||||||
|
- QR code (future)
|
||||||
|
- Copy URL buttons
|
||||||
|
|
||||||
|
### 8. `src/constants/proxy.ts`
|
||||||
|
|
||||||
|
**Responsibility**: Constants
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const DEFAULT_PROXY_PORT = 3000;
|
||||||
|
export const PROTOCOLS = ['http', 'https'] as const;
|
||||||
|
export const EXAMPLE_MODELS = ['gpt-4o', 'claude-3-opus', 'gemini-pro'];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. `src/utils/proxy.ts`
|
||||||
|
|
||||||
|
**Responsibility**: Pure helper functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function buildProxyUrl(protocol: string, ip: string, port: number): string;
|
||||||
|
export function maskApiKey(key: string): string;
|
||||||
|
export function formatCurlExample(url: string, model: string, apiKey: string): string;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route File After Refactoring
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/proxy.tsx — ~50 lines
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { useProxyConfig } from '@/hooks/useProxyConfig';
|
||||||
|
import { useLocalIps } from '@/hooks/useLocalIps';
|
||||||
|
import { ProxyServiceCard } from '@/components/proxy/ProxyServiceCard';
|
||||||
|
import { ProxyModelMappingCard } from '@/components/proxy/ProxyModelMappingCard';
|
||||||
|
import { ProxyUsageExamplesCard } from '@/components/proxy/ProxyUsageExamplesCard';
|
||||||
|
import { ProxyLocalAccessBanner } from '@/components/proxy/ProxyLocalAccessBanner';
|
||||||
|
|
||||||
|
function ProxyPage() {
|
||||||
|
const { config, isLoading, startProxy, stopProxy } = useProxyConfig();
|
||||||
|
const { ips } = useLocalIps();
|
||||||
|
|
||||||
|
if (isLoading) return <ProxySkeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
<ProxyServiceCard config={config} onStart={startProxy} onStop={stopProxy} />
|
||||||
|
<ProxyModelMappingCard mappings={config.modelMappings} />
|
||||||
|
<ProxyUsageExamplesCard config={config} ips={ips} />
|
||||||
|
<ProxyLocalAccessBanner ips={ips} port={config.port} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/proxy')({ component: ProxyPage });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
1. Create new files (hooks, components, constants, utils)
|
||||||
|
2. Move code from `proxy.tsx` to appropriate files
|
||||||
|
3. Update imports in `proxy.tsx`
|
||||||
|
4. Extract and test each hook independently
|
||||||
|
5. Add Storybook stories for each component (if Storybook is used)
|
||||||
|
6. Verify no regression in functionality
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Testability**: Each hook and component can be unit tested in isolation
|
||||||
|
- **Reusability**: `useLocalIps` can be used in other pages (e.g., settings)
|
||||||
|
- **Maintainability**: 50-line route file vs. 724-line monolith
|
||||||
|
- **Code review**: Smaller files = faster, more focused PRs
|
||||||
|
- **Onboarding**: New developers can understand the page in minutes
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Proxy Page — Code Quality Issues
|
||||||
|
|
||||||
|
> Generated from fixer agent analysis of `src/routes/proxy.tsx`
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## Severity Legend
|
||||||
|
|
||||||
|
- 🔴 **Critical**: Bugs or crashes
|
||||||
|
- 🟡 **High**: Performance or reliability issues
|
||||||
|
- 🟢 **Medium**: Maintainability or best practice violations
|
||||||
|
|
||||||
|
## Issues Found
|
||||||
|
|
||||||
|
### 1. 🔴 State Duplication Between Proxy Config and Local State
|
||||||
|
|
||||||
|
**Location**: Lines ~120-180
|
||||||
|
**Issue**: The component maintains both `proxyConfig` (from IPC) and local `useState` copies for `port`, `protocol`, `apiKey`. These can drift out of sync if the backend config changes externally.
|
||||||
|
**Fix**: Use a single source of truth. Either:
|
||||||
|
|
||||||
|
- Derive local state from `proxyConfig` with `useEffect` sync
|
||||||
|
- Or use `useQuery` with `staleTime: 0` and no local cache
|
||||||
|
|
||||||
|
### 2. 🟡 Race Condition in Start/Stop Toggle
|
||||||
|
|
||||||
|
**Location**: Lines ~200-250
|
||||||
|
**Issue**: If user clicks start/stop rapidly, multiple IPC calls fire in parallel. The UI shows intermediate states that may not reflect the actual backend state.
|
||||||
|
**Fix**: Disable the toggle button while a transition is in progress. Add an `isTransitioning` flag.
|
||||||
|
|
||||||
|
### 3. 🟢 Clipboard Error Handling
|
||||||
|
|
||||||
|
**Location**: Lines ~300-320
|
||||||
|
**Issue**: `navigator.clipboard.writeText()` can fail (permission denied, not in secure context). The code has no try/catch.
|
||||||
|
**Fix**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function copyToClipboard(text: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast.success('Copied!');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to copy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 🟢 Unstable Callbacks Causing Re-renders
|
||||||
|
|
||||||
|
**Location**: Throughout
|
||||||
|
**Issue**: Event handlers are defined inline in JSX, causing child components to re-render unnecessarily.
|
||||||
|
**Fix**: Wrap handlers in `useCallback`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleStart = useCallback(() => startProxy(), [startProxy]);
|
||||||
|
const handleStop = useCallback(() => stopProxy(), [stopProxy]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 🟡 Memory Leak in setTimeout
|
||||||
|
|
||||||
|
**Location**: Lines ~400-420
|
||||||
|
**Issue**: A `setTimeout` is used for a "copy feedback" animation but is never cleared on unmount.
|
||||||
|
**Fix**: Use `useEffect` cleanup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setCopied(false), 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [copied]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 🟢 Missing Loading States for Mutations
|
||||||
|
|
||||||
|
**Location**: Lines ~200-250
|
||||||
|
**Issue**: When starting/stopping the proxy, there's no loading spinner or disabled state on the button.
|
||||||
|
**Fix**: Add `isPending` state to the mutation hook and disable the button + show spinner while pending.
|
||||||
|
|
||||||
|
### 7. 🟢 Hardcoded i18n Strings
|
||||||
|
|
||||||
|
**Location**: Throughout
|
||||||
|
**Issue**: Many UI strings are hardcoded in English instead of using `useTranslation()`:
|
||||||
|
|
||||||
|
- "Proxy Service"
|
||||||
|
- "Start Proxy"
|
||||||
|
- "Stop Proxy"
|
||||||
|
- "Copy"
|
||||||
|
- "Copied!"
|
||||||
|
**Fix**: Extract all strings to `src/localization/i18n.ts` and use `t('proxy.startProxy')`, etc.
|
||||||
|
|
||||||
|
### 8. 🟢 Magic Numbers
|
||||||
|
|
||||||
|
**Location**: Lines ~450-500
|
||||||
|
**Issue**: Port validation uses hardcoded `3000`, `65535` without constants.
|
||||||
|
**Fix**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MIN_PORT = 1024;
|
||||||
|
const MAX_PORT = 65535;
|
||||||
|
const DEFAULT_PORT = 3000;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 🟢 Unsafe Type Cast
|
||||||
|
|
||||||
|
**Location**: Lines ~550-600
|
||||||
|
**Issue**: `event.target.value` is cast to `Protocol` without validation.
|
||||||
|
**Fix**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const protocol = event.target.value;
|
||||||
|
if (!PROTOCOLS.includes(protocol)) return;
|
||||||
|
setProtocol(protocol as Protocol);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. 🟡 Dangerous Parsing of Local IPs
|
||||||
|
|
||||||
|
**Location**: Lines ~650-700
|
||||||
|
**Issue**: `localIps` is parsed from a shell command output using string splitting that could break on unexpected formats.
|
||||||
|
**Fix**: Use a robust parser or, better, expose `os.networkInterfaces()` via a safe IPC endpoint.
|
||||||
|
|
||||||
|
### 11. 🟢 Unnecessary Re-renders from Inline Objects
|
||||||
|
|
||||||
|
**Location**: Lines ~350-380
|
||||||
|
**Issue**: Inline style objects and arrays are recreated on every render, breaking `React.memo` optimizations in child components.
|
||||||
|
**Fix**: Extract static styles outside the component or memoize:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cardStyle = useMemo(() => ({ padding: '1.5rem' }), []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. 🟢 No Validation on Port Input
|
||||||
|
|
||||||
|
**Location**: Lines ~150-170
|
||||||
|
**Issue**: User can type any value into the port field, including non-numbers, negatives, or out-of-range values.
|
||||||
|
**Fix**: Add input validation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1024}
|
||||||
|
max={65535}
|
||||||
|
value={port}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
if (value >= 1024 && value <= 65535) setPort(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
- Add unit tests for `useProxyConfig` hook (mock IPC client)
|
||||||
|
- Test race conditions: rapid start/stop clicks
|
||||||
|
- Test clipboard failure path
|
||||||
|
- Test port validation edge cases (0, 80, 65536, NaN)
|
||||||
|
- Test with `react-testing-library` + `user-event`
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Proxy Page — Architecture Overview
|
||||||
|
|
||||||
|
> Generated from explorer agent mapping of proxy-related files
|
||||||
|
> Date: 2026-04-22
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
| File | Lines | Role |
|
||||||
|
| -------------------------------------------- | ----- | ---------------------------------------- |
|
||||||
|
| `src/routes/proxy.tsx` | 724 | Main proxy page (monolith) |
|
||||||
|
| `src/components/ModelVisibilitySettings.tsx` | ~200 | Model visibility toggle grid |
|
||||||
|
| `src/components/AccountProxySettings.tsx` | ~180 | Per-account proxy configuration |
|
||||||
|
| `src/hooks/useCloudAccounts.ts` | ~50 | Fetches cloud accounts for proxy routing |
|
||||||
|
|
||||||
|
### Backend (NestJS)
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| ----------------------------------------------------------- | ------------------------------------------- |
|
||||||
|
| `src/server/modules/proxy/proxy.module.ts` | NestJS module definition |
|
||||||
|
| `src/server/modules/proxy/proxy.service.ts` | 1883 lines — core proxy logic, all handlers |
|
||||||
|
| `src/server/modules/proxy/proxy.controller.ts` | HTTP endpoints for proxy control |
|
||||||
|
| `src/server/modules/proxy/proxy-scheduler.service.ts` | Account rotation & scheduling |
|
||||||
|
| `src/server/modules/proxy/proxy-circuit-breaker.service.ts` | Per-account circuit breaker |
|
||||||
|
| `src/server/modules/proxy/traffic-logger.service.ts` | Request/response logging |
|
||||||
|
| `src/server/modules/proxy/proxy-metrics.service.ts` | Prometheus metrics |
|
||||||
|
| `src/server/modules/proxy/proxy-model-mapper.service.ts` | Model name resolution |
|
||||||
|
| `src/server/modules/proxy/proxy-parity.service.ts` | Parity testing |
|
||||||
|
| `src/server/modules/proxy/proxy-replay.service.ts` | Request replay |
|
||||||
|
| `src/server/modules/proxy/proxy-config.service.ts` | Config management |
|
||||||
|
| `src/server/modules/proxy/proxy-upstream.service.ts` | Upstream proxy support |
|
||||||
|
|
||||||
|
### IPC Layer
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| ------------------------- | ---------------------------- |
|
||||||
|
| `src/ipc/router.ts` | Main IPC router composition |
|
||||||
|
| `src/ipc/proxy/router.ts` | Proxy-specific IPC endpoints |
|
||||||
|
| `src/ipc/manager.ts` | IPC client initialization |
|
||||||
|
|
||||||
|
### Types & Schema
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --------------------- | ------------------------------------------------ |
|
||||||
|
| `src/types/config.ts` | `ProxyConfig` Zod schema — reveals hidden fields |
|
||||||
|
| `src/types/proxy.ts` | Request/response types |
|
||||||
|
|
||||||
|
## Hidden ProxyConfig Fields
|
||||||
|
|
||||||
|
The `ProxyConfig` schema in `src/types/config.ts` contains many fields never shown in the UI:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ProxyConfig {
|
||||||
|
// Shown in UI
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
protocol: 'http' | 'https';
|
||||||
|
apiKey: string;
|
||||||
|
|
||||||
|
// Hidden from UI
|
||||||
|
scheduler: {
|
||||||
|
maxRequestsPerAccount: number;
|
||||||
|
maxWaitTime: number;
|
||||||
|
retryAttempts: number;
|
||||||
|
backoffStrategy: 'exponential' | 'linear' | 'fixed';
|
||||||
|
priorityAccounts: string[];
|
||||||
|
};
|
||||||
|
upstreamProxy?: {
|
||||||
|
url: string;
|
||||||
|
auth?: { username: string; password: string };
|
||||||
|
bypassList: string[];
|
||||||
|
};
|
||||||
|
circuitBreaker: {
|
||||||
|
failureThreshold: number;
|
||||||
|
recoveryTimeout: number;
|
||||||
|
halfOpenMaxCalls: number;
|
||||||
|
};
|
||||||
|
logging: {
|
||||||
|
enabled: boolean;
|
||||||
|
maxEntries: number;
|
||||||
|
retentionDays: number;
|
||||||
|
};
|
||||||
|
metrics: {
|
||||||
|
enabled: boolean;
|
||||||
|
histogramBuckets: number[];
|
||||||
|
};
|
||||||
|
modelMappings: Array<{
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
provider: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ IPC ┌──────────────────┐
|
||||||
|
│ proxy.tsx │ ◄──────────► │ proxy/router.ts │
|
||||||
|
│ (React UI) │ │ (IPC handlers) │
|
||||||
|
└─────────────────┘ └────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ ProxyController │
|
||||||
|
│ (HTTP endpoints) │
|
||||||
|
└────────┬─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ ProxyService │
|
||||||
|
│ (Core orchestrator) │
|
||||||
|
└────────┬─────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────┼──────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
|
||||||
|
│ Scheduler │ │ CircuitBreaker│ │Model Mapper │
|
||||||
|
└─────────────┘ └───────────────┘ └──────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
|
||||||
|
│TrafficLogger│ │MetricsService │ │ParityService │
|
||||||
|
└─────────────┘ └───────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Observations
|
||||||
|
|
||||||
|
1. **Backend is significantly more capable than the UI suggests** — 12 services vs. 1 page
|
||||||
|
2. **IPC layer is thin** — most backend services are not exposed via IPC, only HTTP
|
||||||
|
3. **ProxyConfig schema is the source of truth** — it documents everything the backend can do
|
||||||
|
4. **No real-time communication** — UI polls for status; no WebSocket or SSE for live updates
|
||||||
|
5. **Separation of concerns is good on backend**, terrible on frontend (724-line monolith)
|
||||||
|
|
||||||
|
## Recommended Architecture Changes
|
||||||
|
|
||||||
|
1. Extend `src/ipc/proxy/router.ts` to expose all `ProxyConfig` fields
|
||||||
|
2. Add IPC endpoints for:
|
||||||
|
- `proxy.getTrafficLogs()`
|
||||||
|
- `proxy.getCircuitBreakerStatus()`
|
||||||
|
- `proxy.getMetrics()`
|
||||||
|
- `proxy.replayRequest()`
|
||||||
|
3. Create a WebSocket or SSE channel for live traffic updates
|
||||||
|
4. Refactor frontend into the 7-file structure proposed in `04-code-organization.md`
|
||||||
@@ -12,7 +12,7 @@ This document is a user-level guide for AI assistants working in this repository
|
|||||||
|
|
||||||
- **Runtime**: Node.js (Electron environment)
|
- **Runtime**: Node.js (Electron environment)
|
||||||
- **Node**: Recommended Node.js 20+
|
- **Node**: Recommended Node.js 20+
|
||||||
- **Package manager**: `npm` (this project includes `package-lock.json`; use npm only)
|
- **Package manager**: `pnpm` (this project includes `pnpm-lock.yaml`; use pnpm only)
|
||||||
- **Build tools**: Electron Forge + Vite
|
- **Build tools**: Electron Forge + Vite
|
||||||
- **Terminal**: Windows (PowerShell) / VSCode MCP tools can be used safely
|
- **Terminal**: Windows (PowerShell) / VSCode MCP tools can be used safely
|
||||||
|
|
||||||
@@ -75,32 +75,32 @@ This document is a user-level guide for AI assistants working in this repository
|
|||||||
|
|
||||||
## 📦 Common Scripts
|
## 📦 Common Scripts
|
||||||
|
|
||||||
Use `npm` for all commands:
|
Use `pnpm` for all commands:
|
||||||
|
|
||||||
- **Development (Dev)**:
|
- **Development (Dev)**:
|
||||||
- `npm start` - Start Electron dev environment (Electron Forge)
|
- `pnpm start` - Start Electron dev environment (Electron Forge)
|
||||||
- `npm run lint` - Run ESLint checks
|
- `pnpm run lint` - Run ESLint checks
|
||||||
- `npm run format` - Run Prettier check
|
- `pnpm run format` - Run Prettier check
|
||||||
- `npm run format:write` - Auto-format with Prettier
|
- `pnpm run format:write` - Auto-format with Prettier
|
||||||
- `npm run type-check` - Run TypeScript type check
|
- `pnpm run type-check` - Run TypeScript type check
|
||||||
|
|
||||||
- **Build**:
|
- **Build**:
|
||||||
- `npm run package` - Package app (application bundle only)
|
- `pnpm run package` - Package app (application bundle only)
|
||||||
- `npm run make` - Build and generate distributable installers
|
- `pnpm run make` - Build and generate distributable installers
|
||||||
- `npm run publish` - Publish app
|
- `pnpm run publish` - Publish app
|
||||||
|
|
||||||
- **Testing**:
|
- **Testing**:
|
||||||
- `npm test` - Run Vitest tests
|
- `pnpm test` - Run Vitest tests
|
||||||
- `npm run test:watch` - Run Vitest in watch mode
|
- `pnpm run test:watch` - Run Vitest in watch mode
|
||||||
- `npm run test:unit` - Same as above for unit-focused runs
|
- `pnpm run test:unit` - Same as above for unit-focused runs
|
||||||
- `npm run test:e2e` - Run Playwright E2E tests
|
- `pnpm run test:e2e` - Run Playwright E2E tests
|
||||||
- `npm run test:all` - Run all tests
|
- `pnpm run test:all` - Run all tests
|
||||||
|
|
||||||
### Running a Single Test
|
### Running a Single Test
|
||||||
|
|
||||||
- Unit test: `npm run test:unit path/to/test.test.ts`
|
- Unit test: `pnpm run test:unit path/to/test.test.ts`
|
||||||
- E2E test: `npm run test:e2e path/to/test.spec.ts`
|
- E2E test: `pnpm run test:e2e path/to/test.spec.ts`
|
||||||
- Type check: `npm run type-check`
|
- Type check: `pnpm run type-check`
|
||||||
|
|
||||||
## 🧪 Development Notes
|
## 🧪 Development Notes
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ export const Component: React.FC<ComponentProps> = ({ prop1 }) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
> Before commit, run `npm run lint` and `npm run format`.
|
> Before commit, run `pnpm run lint` and `pnpm run format`.
|
||||||
|
|
||||||
## 📝 Terminal Output and References
|
## 📝 Terminal Output and References
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ Summary / output:
|
|||||||
|
|
||||||
Packages and scripts:
|
Packages and scripts:
|
||||||
|
|
||||||
- `npm install` (or `npm i`)
|
- `pnpm install` (or `pnpm i`)
|
||||||
|
|
||||||
Shell:
|
Shell:
|
||||||
|
|
||||||
@@ -345,12 +345,6 @@ Documentation/usage lookup:
|
|||||||
|
|
||||||
- Use `context7` for latest dependency usage.
|
- Use `context7` for latest dependency usage.
|
||||||
|
|
||||||
VSCode MCP (if available):
|
|
||||||
|
|
||||||
- `mcp__vscode-mcp__get_references` for refactor impact analysis
|
|
||||||
- `mcp__vscode-mcp__rename_symbol` for safe renaming
|
|
||||||
- `mcp__vscode-mcp__get_symbol_lsp_info` for types/signatures/definitions
|
|
||||||
|
|
||||||
## 🚨 Local Quality Checks (Optional Flow)
|
## 🚨 Local Quality Checks (Optional Flow)
|
||||||
|
|
||||||
After a set of changes, run these three checks in parallel instead of full lint immediately:
|
After a set of changes, run these three checks in parallel instead of full lint immediately:
|
||||||
@@ -371,128 +365,3 @@ Task(subagent_type: "run-related-tests", description: "Run tests", prompt: "[sam
|
|||||||
Flow: initial check -> fix key issues -> re-check -> iterate until key issues are resolved.
|
Flow: initial check -> fix key issues -> re-check -> iterate until key issues are resolved.
|
||||||
|
|
||||||
Note: these tools are read-only analyzers; you still need to apply fixes manually. Pass precise file paths, not broad directories.
|
Note: these tools are read-only analyzers; you still need to apply fixes manually. Pass precise file paths, not broad directories.
|
||||||
|
|
||||||
<skills_system priority="1">
|
|
||||||
|
|
||||||
## Available Skills
|
|
||||||
|
|
||||||
<!-- SKILLS_TABLE_START -->
|
|
||||||
<usage>
|
|
||||||
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
|
||||||
|
|
||||||
How to use skills:
|
|
||||||
|
|
||||||
- Invoke: Bash("openskills read <skill-name>")
|
|
||||||
- The skill content will load with detailed instructions on how to complete the task
|
|
||||||
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
|
|
||||||
|
|
||||||
Usage notes:
|
|
||||||
|
|
||||||
- Only use skills listed in <available_skills> below
|
|
||||||
- Do not invoke a skill that is already loaded in your context
|
|
||||||
- Each skill invocation is stateless
|
|
||||||
</usage>
|
|
||||||
|
|
||||||
<available_skills>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>algorithmic-art</name>
|
|
||||||
<description>Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>brand-guidelines</name>
|
|
||||||
<description>Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>canvas-design</name>
|
|
||||||
<description>Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>doc-coauthoring</name>
|
|
||||||
<description>Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>docx</name>
|
|
||||||
<description>"Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>frontend-design</name>
|
|
||||||
<description>Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>internal-comms</name>
|
|
||||||
<description>A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>mcp-builder</name>
|
|
||||||
<description>Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>pdf</name>
|
|
||||||
<description>Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>pptx</name>
|
|
||||||
<description>"Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks"</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>skill-creator</name>
|
|
||||||
<description>Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>slack-gif-creator</name>
|
|
||||||
<description>Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>theme-factory</name>
|
|
||||||
<description>Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>web-artifacts-builder</name>
|
|
||||||
<description>Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>webapp-testing</name>
|
|
||||||
<description>Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
<skill>
|
|
||||||
<name>xlsx</name>
|
|
||||||
<description>"Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas"</description>
|
|
||||||
<location>project</location>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
</available_skills>
|
|
||||||
|
|
||||||
<!-- SKILLS_TABLE_END -->
|
|
||||||
|
|
||||||
</skills_system>
|
|
||||||
@@ -2,6 +2,87 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.15.3](https://github.com/evandrodevbr/GeminiNexus/compare/v0.15.2...v0.15.3) (2026-05-02)
|
||||||
|
|
||||||
|
### 👷 Build System
|
||||||
|
|
||||||
|
* allow fs-xattr and appdmg to build in pnpm v10 ([1c26a9e](https://github.com/evandrodevbr/GeminiNexus/commit/1c26a9e542f3fa5a05b370b8a332ffe5a727ea92))
|
||||||
|
|
||||||
|
## [0.15.2](https://github.com/evandrodevbr/GeminiNexus/compare/v0.15.1...v0.15.2) (2026-05-02)
|
||||||
|
|
||||||
|
### 👷 Build System
|
||||||
|
|
||||||
|
* allow macos-alias and other native modules to build in pnpm v10 ([2e3a65b](https://github.com/evandrodevbr/GeminiNexus/commit/2e3a65b76b91012638c422f6223b79399775c635))
|
||||||
|
|
||||||
|
## [0.15.1](https://github.com/evandrodevbr/GeminiNexus/compare/v0.15.0...v0.15.1) (2026-05-02)
|
||||||
|
|
||||||
|
### ✅ Tests
|
||||||
|
|
||||||
|
* add universal electron mock to fix headless CI test runs ([1d35801](https://github.com/evandrodevbr/GeminiNexus/commit/1d35801980e3e54cec1d3e52ed75850eb7eec350))
|
||||||
|
* fix test environments failing due to missing IS_DEV_ENVIRONMENT mock export ([269ad18](https://github.com/evandrodevbr/GeminiNexus/commit/269ad18ed6c09585e881d0d916558b1bcc7ad940))
|
||||||
|
* update proxy-advanced-handler tests to reflect optional service registration ([f4daaa8](https://github.com/evandrodevbr/GeminiNexus/commit/f4daaa8ca13bc75c7e5090905176c06b61ecfedc))
|
||||||
|
|
||||||
|
### 👷 Build System
|
||||||
|
|
||||||
|
* replace MakerSquirrel with MakerZIP for Windows ([27223f5](https://github.com/evandrodevbr/GeminiNexus/commit/27223f587d7facf25beef77aa6984cd248f2703d))
|
||||||
|
|
||||||
|
## [0.15.0](https://github.com/evandrodevbr/GeminiNexus/compare/v0.14.0...v0.15.0) (2026-05-02)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
* bump version to 0.14.0 ([a905877](https://github.com/evandrodevbr/GeminiNexus/commit/a905877c76869a9b31b5b079d7afd86e4c905b59))
|
||||||
|
* **core:** isolate dev and production environments ([5c44a62](https://github.com/evandrodevbr/GeminiNexus/commit/5c44a622a8b4fa81246d4ca77073be50b4ddc1c9))
|
||||||
|
* **proxy:** add OpenCode IDE config and fix service availability ([713c449](https://github.com/evandrodevbr/GeminiNexus/commit/713c4497c623b0519847ed2afcc7da04997dae55))
|
||||||
|
* **proxy:** enrich traffic logs with model and token metadata ([641c739](https://github.com/evandrodevbr/GeminiNexus/commit/641c739b64150d1df77fd1a81a028b8cb4a4884d))
|
||||||
|
* **ui:** replace sidebar with compact top header navigation ([7d2977b](https://github.com/evandrodevbr/GeminiNexus/commit/7d2977b665a1a2fb0d5d8844a8e519471cdef8d8))
|
||||||
|
* **usage:** add cost reporting with custom model pricing and OpenRouter ([bc69a2e](https://github.com/evandrodevbr/GeminiNexus/commit/bc69a2e8b52e5019c45faae12771af7e40d3f26b))
|
||||||
|
* **usage:** move Monitor to Usage tab with per-request token tracking ([99e458f](https://github.com/evandrodevbr/GeminiNexus/commit/99e458f69bd5564d27f94c72c719ba49efd72abe))
|
||||||
|
* **usage:** redesign Usage Analytics with accurate token averages ([20785a8](https://github.com/evandrodevbr/GeminiNexus/commit/20785a8414bb0e364085a4a64f6a958c178d1df8))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* **ci:** remove explicit pnpm version from workflows ([080bf30](https://github.com/evandrodevbr/GeminiNexus/commit/080bf30e38379db5cb3c8d124f76a39aef897ccc))
|
||||||
|
* **ci:** resolve release pipeline conflicts and clean repo structure ([71bf812](https://github.com/evandrodevbr/GeminiNexus/commit/71bf8122d1a3d5bd74e3450f61d24747b50ed9d7))
|
||||||
|
* **ci:** use Node 22 for semantic-release (requires ^22.14.0) ([268428a](https://github.com/evandrodevbr/GeminiNexus/commit/268428a1193fd0bfd0559472fd50b3293d9c8ccb))
|
||||||
|
* install notice showing in Chinese and not recognizing MSI install path ([0753bbd](https://github.com/evandrodevbr/GeminiNexus/commit/0753bbdce63cffcd40fa13643e1082a67335c9c4))
|
||||||
|
|
||||||
|
### ♻️ Code Refactoring
|
||||||
|
|
||||||
|
* **accounts:** decompose CloudAccountList into focused components ([bf5a42c](https://github.com/evandrodevbr/GeminiNexus/commit/bf5a42c03e706d19033ceff332ee3609afded7ed))
|
||||||
|
|
||||||
|
## [0.14.0](https://github.com/evandrodevbr/GeminiNexus/compare/v0.13.1...v0.14.0) (2026-04-29)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
* **proxy:** add OpenAI Responses API compatibility for Open CoDesign ([68474b5](https://github.com/evandrodevbr/GeminiNexus/commit/68474b56dca9c9bcd7b06eaae04c521f475e8e2c))
|
||||||
|
|
||||||
|
## [0.13.1](https://github.com/evandrodevbr/GeminiNexus/compare/v0.13.0...v0.13.1) (2026-04-29)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* lazy-initialize api-logger to prevent EACCES in CI environments ([9bf7c46](https://github.com/evandrodevbr/GeminiNexus/commit/9bf7c46ff6320f49cba80923c9e11eac35a7d4f0))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.11.1](https://github.com/Draculabo/AntigravityManager/compare/v0.11.0...v0.11.1) (2026-04-18)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* add unit label to AI credit display ([6577f1c](https://github.com/Draculabo/AntigravityManager/commit/6577f1c51fd155004b81f6171eb5feffa2448128))
|
||||||
|
* remaining localization gaps ([#156](https://github.com/Draculabo/AntigravityManager/issues/156)) ([7093c98](https://github.com/Draculabo/AntigravityManager/commit/7093c982ba790380cda7d1e8a53891970df6b3cf))
|
||||||
|
* remove AI credit refresh time scraping ([3e1e10b](https://github.com/Draculabo/AntigravityManager/commit/3e1e10b2a580ebf5a64b9a8982bd812a6a1f2924))
|
||||||
|
|
||||||
|
### 📝 Documentation
|
||||||
|
|
||||||
|
* **repo:** update project docs ([440a934](https://github.com/Draculabo/AntigravityManager/commit/440a9342800f244287c758b1a77a05fac9096df7))
|
||||||
|
|
||||||
|
### ♻️ Code Refactoring
|
||||||
|
|
||||||
|
* **ipc:** migrate renderer IPC client to official oRPC client ([d0c282d](https://github.com/Draculabo/AntigravityManager/commit/d0c282dffff24bb74a81cfa523f2f72f949bf8f9))
|
||||||
|
* standardize runtime guard patterns with lodash-es ([#154](https://github.com/Draculabo/AntigravityManager/issues/154)) ([bc35b96](https://github.com/Draculabo/AntigravityManager/commit/bc35b96b5d2d873cf0f187a57de695aedc32108f))
|
||||||
|
|
||||||
## [0.11.0](https://github.com/Draculabo/AntigravityManager/compare/v0.10.0...v0.11.0) (2026-04-11)
|
## [0.11.0](https://github.com/Draculabo/AntigravityManager/compare/v0.10.0...v0.11.0) (2026-04-11)
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
@@ -284,7 +365,7 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial release of Antigravity Manager
|
- Initial release of Gemini Nexus
|
||||||
- Multi-account management for Google Gemini and Claude
|
- Multi-account management for Google Gemini and Claude
|
||||||
- Real-time quota monitoring
|
- Real-time quota monitoring
|
||||||
- Intelligent auto-switching capabilities
|
- Intelligent auto-switching capabilities
|
||||||
|
|||||||
@@ -2,48 +2,75 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
**Examples of behavior that contributes to a positive environment:**
|
||||||
|
|
||||||
- Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
- Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
- Gracefully accepting constructive criticism
|
- Gracefully accepting constructive feedback
|
||||||
- Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
- Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
- Helping newcomers feel welcome
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
**Examples of unacceptable behavior:**
|
||||||
|
|
||||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
- The use of sexualized language or imagery, and unwelcome sexual attention or advances
|
||||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
- Public or private harassment
|
- Public or private harassment
|
||||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
- Publishing others' private information without explicit permission
|
||||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
This Code of Conduct applies within all project spaces — including the repository, issue tracker, discussions, and pull requests — and also applies when an individual is officially representing the project in public spaces.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue on the [GitHub repository](https://github.com/evandrodevbr/GeminiNexus/issues) or by contacting the project maintainers directly.
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
All complaints will be reviewed and investigated promptly and fairly. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Project maintainers will follow these guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Impact**: Use of inappropriate language or other behavior deemed unprofessional.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Impact**: A violation through a single incident or series of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved for a specified period of time. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at <https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
Enforcement Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Note**: Please replace `[INSERT EMAIL ADDRESS]` with your actual contact email if you wish to receive reports.
|
|
||||||
@@ -1,44 +1,55 @@
|
|||||||
# Contributing to Antigravity Manager
|
# Contributing to Gemini Nexus
|
||||||
|
|
||||||
First off, thank you for considering contributing to Antigravity Manager! 🎉
|
Thank you for considering contributing to Gemini Nexus! 🎉
|
||||||
|
|
||||||
It's people like you that make Antigravity Manager such a great tool. We welcome contributions from everyone, whether it's a bug report, feature suggestion, documentation improvement, or code contribution.
|
Whether it's a bug report, feature suggestion, documentation improvement, or code contribution — all help is welcome.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📋 Table of Contents
|
## 📋 Table of Contents
|
||||||
|
|
||||||
- [Code of Conduct](#code-of-conduct)
|
- [Code of Conduct](#-code-of-conduct)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#-getting-started)
|
||||||
- [Development Setup](#development-setup)
|
- [Development Setup](#-development-setup)
|
||||||
- [Making Changes](#making-changes)
|
- [Making Changes](#-making-changes)
|
||||||
- [Pull Request Process](#pull-request-process)
|
- [Commit Guidelines](#-commit-guidelines)
|
||||||
- [Style Guidelines](#style-guidelines)
|
- [Pull Request Process](#-pull-request-process)
|
||||||
- [Reporting Bugs](#reporting-bugs)
|
- [Code Style](#-code-style)
|
||||||
- [Suggesting Features](#suggesting-features)
|
- [Reporting Issues](#-reporting-issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📜 Code of Conduct
|
## 📜 Code of Conduct
|
||||||
|
|
||||||
This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
|
This project is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
1. **Fork the repository** on GitHub
|
1. **Fork** the repository on GitHub
|
||||||
2. **Clone your fork** locally:
|
2. **Clone** your fork locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/AntigravityManager.git
|
git clone https://github.com/YOUR_USERNAME/GeminiNexus.git
|
||||||
cd AntigravityManager
|
cd GeminiNexus
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Add the upstream remote**:
|
3. **Add the upstream remote**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git remote add upstream https://github.com/Draculabo/AntigravityManager.git
|
git remote add upstream https://github.com/evandrodevbr/GeminiNexus.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 💻 Development Setup
|
## 💻 Development Setup
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js v18 or higher
|
- **Node.js** v20 or higher
|
||||||
- npm (comes with Node.js)
|
- **npm** (this project uses `package-lock.json`)
|
||||||
- Git
|
- **Git**
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -46,49 +57,73 @@ This project and everyone participating in it is governed by our [Code of Conduc
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Start development server
|
# Start the app in development mode
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Scripts
|
### Available Scripts
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ---------------------- | ----------------------------------- |
|
| ---------------------- | ------------------------------------------------------- |
|
||||||
| `npm start` | Start the app in development mode |
|
| `npm start` | Start the app in development mode (Electron + Vite HMR) |
|
||||||
| `npm run lint` | Run ESLint to check for code issues |
|
| `npm run lint` | Run ESLint |
|
||||||
| `npm run format:write` | Format code with Prettier |
|
| `npm run format` | Check formatting with Prettier |
|
||||||
| `npm run test:unit` | Run unit tests with Vitest |
|
| `npm run format:write` | Auto-format code with Prettier |
|
||||||
| `npm run test:e2e` | Run E2E tests with Playwright |
|
| `npm run type-check` | Run TypeScript type check |
|
||||||
| `npm run test:all` | Run all tests |
|
| `npm test` | Run unit tests with Vitest |
|
||||||
| `npm run type-check` | Run TypeScript type checking |
|
| `npm run test:e2e` | Run E2E tests with Playwright |
|
||||||
| `npm run make` | Build production packages |
|
| `npm run test:all` | Run all tests (unit + E2E) |
|
||||||
|
| `npm run make` | Build production installers |
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
src/
|
||||||
|
├── components/ # React UI components (Radix-based primitives in ui/)
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── ipc/ # Electron IPC + database handlers
|
||||||
|
├── layouts/ # Layout components (MainLayout + sidebar)
|
||||||
|
├── localization/ # i18n translation resources
|
||||||
|
├── routes/ # TanStack Router pages (index, usage, proxy, settings)
|
||||||
|
├── server/ # NestJS backend (proxy gateway service)
|
||||||
|
├── services/ # Service layer
|
||||||
|
├── types/ # TypeScript type definitions + Zod schemas
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✏️ Making Changes
|
## ✏️ Making Changes
|
||||||
|
|
||||||
1. **Create a new branch** from `main`:
|
1. **Create a branch** from `main`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feature/your-feature-name
|
git checkout -b feat/your-feature-name
|
||||||
# or
|
# or
|
||||||
git checkout -b fix/your-bug-fix
|
git checkout -b fix/your-bug-fix
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Make your changes** and commit them:
|
2. **Make your changes**, then commit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "feat: add amazing new feature"
|
git commit -m "feat: add amazing new feature"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Keep your branch updated**:
|
3. **Keep your branch up to date**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch upstream
|
git fetch upstream
|
||||||
git rebase upstream/main
|
git rebase upstream/main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit Message Guidelines
|
---
|
||||||
|
|
||||||
We follow [Conventional Commits](https://www.conventionalcommits.org/). Each commit message should be structured as follows:
|
## 📝 Commit Guidelines
|
||||||
|
|
||||||
|
We follow [Conventional Commits](https://www.conventionalcommits.org/). Commits are analyzed by `semantic-release` to automatically determine version bumps and generate changelogs.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>(<scope>): <description>
|
<type>(<scope>): <description>
|
||||||
@@ -98,88 +133,136 @@ We follow [Conventional Commits](https://www.conventionalcommits.org/). Each com
|
|||||||
[optional footer(s)]
|
[optional footer(s)]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Types:**
|
### Types
|
||||||
|
|
||||||
- `feat`: A new feature
|
| Type | Description | Release |
|
||||||
- `fix`: A bug fix
|
| ---------- | ----------------------------------- | ---------- |
|
||||||
- `docs`: Documentation only changes
|
| `feat` | A new feature | **minor** |
|
||||||
- `style`: Changes that don't affect code meaning (formatting, etc.)
|
| `fix` | A bug fix | **patch** |
|
||||||
- `refactor`: Code change that neither fixes a bug nor adds a feature
|
| `perf` | Performance improvement | **patch** |
|
||||||
- `perf`: Performance improvement
|
| `refactor` | Code change (no bug fix or feature) | **patch** |
|
||||||
- `test`: Adding or correcting tests
|
| `docs` | Documentation only | no release |
|
||||||
- `chore`: Changes to build process or auxiliary tools
|
| `style` | Formatting, whitespace | no release |
|
||||||
|
| `test` | Adding or fixing tests | no release |
|
||||||
|
| `chore` | Build process, tooling | no release |
|
||||||
|
| `ci` | CI/CD changes | no release |
|
||||||
|
|
||||||
**Examples:**
|
### Breaking Changes
|
||||||
|
|
||||||
|
Add `BREAKING CHANGE:` in the footer or `!` after the type to trigger a **major** release:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(auth): add Google OAuth support
|
feat!: remove legacy plaintext storage
|
||||||
fix(quota): resolve quota refresh timeout issue
|
|
||||||
docs(readme): update installation instructions
|
BREAKING CHANGE: all accounts must re-authenticate after upgrade.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(proxy): add Anthropic /v1/messages endpoint
|
||||||
|
fix(usage): handle SQL SUM(null) in empty date ranges
|
||||||
|
docs(readme): add Chinese and Spanish translations
|
||||||
|
ci: normalize Node.js to v20 across all workflows
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔄 Pull Request Process
|
## 🔄 Pull Request Process
|
||||||
|
|
||||||
1. **Update documentation** if you're changing functionality
|
### Before Opening a PR
|
||||||
2. **Add tests** for new features
|
|
||||||
3. **Ensure all tests pass**: `npm run test:all`
|
1. Run the quality checks:
|
||||||
4. **Ensure code is formatted**: `npm run format:write`
|
|
||||||
5. **Ensure no lint errors**: `npm run lint`
|
```bash
|
||||||
6. **Push to your fork** and create a Pull Request
|
npm run lint
|
||||||
|
npm run format:write
|
||||||
|
npm run type-check
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update documentation if you changed functionality
|
||||||
|
3. Add tests for new features
|
||||||
|
|
||||||
### PR Checklist
|
### PR Checklist
|
||||||
|
|
||||||
- [ ] My code follows the project's style guidelines
|
- [ ] My code follows the project's style guidelines
|
||||||
- [ ] I have performed a self-review of my code
|
- [ ] I have performed a self-review of my code
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented hard-to-understand areas
|
||||||
- [ ] I have made corresponding changes to the documentation
|
- [ ] I have updated related documentation
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
- [ ] I have added tests that prove my fix is effective or my feature works
|
- [ ] I have added tests that prove my fix or feature works
|
||||||
- [ ] New and existing unit tests pass locally with my changes
|
- [ ] All existing tests pass locally
|
||||||
|
|
||||||
## 🎨 Style Guidelines
|
### Review Process
|
||||||
|
|
||||||
### TypeScript/JavaScript
|
- PRs require at least one maintainer approval
|
||||||
|
- CI must pass (lint, type-check, unit tests)
|
||||||
- Use TypeScript for all new code
|
- Squash-merge is preferred for clean history
|
||||||
- Follow the existing code style (enforced by ESLint and Prettier)
|
|
||||||
- Use meaningful variable and function names
|
|
||||||
- Add JSDoc comments for public APIs
|
|
||||||
|
|
||||||
### React Components
|
|
||||||
|
|
||||||
- Use functional components with hooks
|
|
||||||
- Keep components small and focused
|
|
||||||
- Use TypeScript interfaces for props
|
|
||||||
- Follow the existing file structure
|
|
||||||
|
|
||||||
### CSS/Styling
|
|
||||||
|
|
||||||
- Use TailwindCSS utility classes
|
|
||||||
- Follow the design system in `components.json`
|
|
||||||
- Ensure responsive design
|
|
||||||
|
|
||||||
## 🐛 Reporting Bugs
|
|
||||||
|
|
||||||
Found a bug? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=bug_report.md) with:
|
|
||||||
|
|
||||||
- **Clear title** describing the issue
|
|
||||||
- **Steps to reproduce** the behavior
|
|
||||||
- **Expected behavior** vs actual behavior
|
|
||||||
- **Screenshots** if applicable
|
|
||||||
- **Environment details** (OS, app version)
|
|
||||||
|
|
||||||
## 💡 Suggesting Features
|
|
||||||
|
|
||||||
Have an idea? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=feature_request.md) with:
|
|
||||||
|
|
||||||
- **Clear description** of the feature
|
|
||||||
- **Use case** - why is this feature needed?
|
|
||||||
- **Possible implementation** ideas (optional)
|
|
||||||
|
|
||||||
## 🙏 Thank You
|
|
||||||
|
|
||||||
Your contributions help make Antigravity Manager better for everyone. Thank you for taking the time to contribute!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
If you have any questions, feel free to open an issue or reach out to the maintainers.
|
## 🎨 Code Style
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- Use TypeScript for all new code — avoid `any`
|
||||||
|
- Use Zod for runtime validation
|
||||||
|
- Prefer `lodash-es` named imports over native utilities
|
||||||
|
- Use `@/` alias for `src/` imports
|
||||||
|
|
||||||
|
### React Components
|
||||||
|
|
||||||
|
- Functional components with hooks only
|
||||||
|
- Use Radix UI primitives for accessible components
|
||||||
|
- TypeScript interfaces for props
|
||||||
|
- Follow existing component structure (`components/ComponentName.tsx`)
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- Tailwind CSS v4 utility classes
|
||||||
|
- Use `clsx` + `tailwind-merge` for conditional classes
|
||||||
|
- Follow the Gemini Nexus design tokens (`rounded-xl`, `border-white/[0.06]`)
|
||||||
|
|
||||||
|
### Import Order
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. React and core libraries
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
// 2. External dependencies (alphabetical)
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// 3. Internal imports (using @ alias)
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Reporting Issues
|
||||||
|
|
||||||
|
### Bug Reports
|
||||||
|
|
||||||
|
[Create a bug report](https://github.com/evandrodevbr/GeminiNexus/issues/new?labels=bug&template=bug_report.md) with:
|
||||||
|
|
||||||
|
- Clear title describing the issue
|
||||||
|
- Steps to reproduce the behavior
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Screenshots if applicable
|
||||||
|
- Environment details (OS, app version, Node.js version)
|
||||||
|
|
||||||
|
### Feature Requests
|
||||||
|
|
||||||
|
[Suggest a feature](https://github.com/evandrodevbr/GeminiNexus/issues/new?labels=enhancement&template=feature_request.md) with:
|
||||||
|
|
||||||
|
- Clear description of the feature
|
||||||
|
- Use case — why is this needed?
|
||||||
|
- Possible implementation ideas (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Thank You
|
||||||
|
|
||||||
|
Your contributions make Gemini Nexus better for everyone. Thank you for your time!
|
||||||
|
|
||||||
|
If you have questions, feel free to [open a discussion](https://github.com/evandrodevbr/GeminiNexus/discussions) or reach out to the maintainers.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025-present evandrodevbr
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/logo.png" alt="Gemini Nexus" width="128" height="128" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">Gemini Nexus</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>🚀 Gateway de IA multi-cuenta profesional para Google Gemini y Claude</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">English</a> | <a href="README.pt-BR.md">Português</a> | <a href="README.zh-CN.md">中文</a> | Español
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
|
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="Versión" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
|
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="Descargas" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="Licencia" />
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/plataforma-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="Plataforma" />
|
||||||
|
<img src="https://img.shields.io/badge/electron-latest-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Usuarios de Windows:** Si ves una advertencia de SmartScreen **"Windows protegió su PC"** durante la instalación, haz clic en **"Más información"** y luego en **"Ejecutar de todas formas"**. Esta es una advertencia de seguridad esperada para aplicaciones nuevas no firmadas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Tabla de Contenidos
|
||||||
|
|
||||||
|
- [✨ ¿Por qué Gemini Nexus?](#-por-qué-gemini-nexus)
|
||||||
|
- [🎯 Características](#-características)
|
||||||
|
- [📸 Capturas de Pantalla](#-capturas-de-pantalla)
|
||||||
|
- [⚡ Instalación e Inicio Rápido](#-instalación-e-inicio-rápido)
|
||||||
|
- [🛠️ Stack Tecnológico](#️-stack-tecnológico)
|
||||||
|
- [💻 Desarrollo](#-desarrollo)
|
||||||
|
- [❓ Preguntas Frecuentes](#-preguntas-frecuentes)
|
||||||
|
- [🤝 Contribuir](#-contribuir)
|
||||||
|
- [📄 Licencia](#-licencia)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ ¿Por qué Gemini Nexus?
|
||||||
|
|
||||||
|
Al usar IDEs y herramientas de programación con IA, ¿te has encontrado con estos problemas?
|
||||||
|
|
||||||
|
- 😫 **Límites de cuota:** La cuota de una sola cuenta se agota rápido, requiriendo cambios manuales frecuentes.
|
||||||
|
- 🔄 **Gestión de cuentas:** Gestionar múltiples cuentas de Google/Claude es tedioso.
|
||||||
|
- 📊 **Uso a ciegas:** No sabes cuántos tokens has consumido o cuánta cuota queda.
|
||||||
|
- 🔌 **Problemas de integración:** Necesitas un proxy local confiable que hable los protocolos OpenAI/Anthropic de forma nativa.
|
||||||
|
- 🔍 **Falta de transparencia:** No puedes ver qué se envía o recibe exactamente a través del proxy.
|
||||||
|
|
||||||
|
**Gemini Nexus** resuelve todo esto. Es una aplicación de escritorio profesional en Electron que actúa como gateway inteligente entre tus herramientas de desarrollo y Google Gemini / Claude AI.
|
||||||
|
|
||||||
|
### Propuesta de Valor
|
||||||
|
|
||||||
|
- ✅ **Pool de Cuentas Ilimitado** — Agrega cualquier cantidad de cuentas Google Gemini y Claude.
|
||||||
|
- ✅ **Cambio Automático Inteligente** — Rota automáticamente a la siguiente cuenta disponible cuando la cuota es baja o hay rate limit.
|
||||||
|
- ✅ **Analytics de Uso en Tiempo Real** — Dashboard nivel SaaS con gráficos de área, indicadores de tendencia y distribución por modelo.
|
||||||
|
- ✅ **Observabilidad Completa del Proxy** — Monitor de tráfico en vivo, replay de solicitudes e inspector de capacidades de modelos.
|
||||||
|
- ✅ **Compatible con OpenAI y Anthropic** — Proxy drop-in para Cursor, Windsurf, OpenCode y cualquier herramienta compatible con OpenAI.
|
||||||
|
- ✅ **Seguro por Defecto** — Cifrado AES-256-GCM con gestión nativa de credenciales del sistema operativo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Características
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>☁️ Pool de Cuentas en la Nube</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Agrega cuentas ilimitadas de Google Gemini / Claude vía OAuth</li>
|
||||||
|
<li>Muestra avatar, email, estado y último uso</li>
|
||||||
|
<li>Monitoreo de estado en tiempo real (Activa, Rate Limited, Expirada)</li>
|
||||||
|
<li>Configuración de proxy URL por cuenta</li>
|
||||||
|
<li>Gestión de perfil de identidad del dispositivo con historial</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>📊 Dashboard de Analytics de Uso</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Consumo de tokens en tiempo real con auto-refresh cada 15s</li>
|
||||||
|
<li>Gráficos de área para tokens de prompt y completion (diario/horario)</li>
|
||||||
|
<li>Indicadores de tendencia (% de cambio vs período anterior)</li>
|
||||||
|
<li>Sparklines inline en las tarjetas de estadísticas</li>
|
||||||
|
<li>Ranking de distribución por modelo con gráficos de barras horizontales</li>
|
||||||
|
<li>Visualización de proporción Prompt/Completion</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔌 Proxy de API Local (Gateway)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Endpoint compatible con OpenAI <code>/v1/chat/completions</code></li>
|
||||||
|
<li>Endpoint compatible con Anthropic <code>/v1/messages</code></li>
|
||||||
|
<li>Soporte completo de streaming SSE (probado con Cursor, Windsurf, OpenCode)</li>
|
||||||
|
<li>Mapeo de modelos (ej: <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||||
|
<li>Puerto, timeout y API key configurables</li>
|
||||||
|
<li>Control de visibilidad de modelos</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔄 Cambio Automático Inteligente</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Modo pool ilimitado con selección inteligente de respaldo</li>
|
||||||
|
<li>Cambio automático cuando cuota < 5% o con rate limit</li>
|
||||||
|
<li>Monitoreo en segundo plano cada 5 minutos</li>
|
||||||
|
<li>Fallback elegante con seguimiento de razón de estado</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔍 Observabilidad del Proxy (Avanzado)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Monitor de Tráfico</strong> — Log en vivo de solicitudes/respuestas con latencia y modelo</li>
|
||||||
|
<li><strong>Replay de Solicitudes</strong> — Reproduce cualquier solicitud para debugging</li>
|
||||||
|
<li><strong>Capacidades de Modelos</strong> — Inspecciona soporte de visión, thinking, streaming, modo JSON</li>
|
||||||
|
<li><strong>Herramientas para Devs</strong> — Generación de código cURL y Python, copia con un clic</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔐 Seguridad y Cifrado</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Cifrado AES-256-GCM para todos los datos sensibles</li>
|
||||||
|
<li>Integración con gestor de credenciales nativo del SO (Keytar + SafeStorage)</li>
|
||||||
|
<li>Migración automática de datos legacy en texto plano</li>
|
||||||
|
<li>Almacenamiento cifrado de tokens y cuotas por cuenta</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>⚙️ Configuración y Personalización</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Soporte de temas Oscuro / Claro / Sistema</li>
|
||||||
|
<li>Multi-idioma: English & Português (Brasil)</li>
|
||||||
|
<li>Override de proxy URL por cuenta</li>
|
||||||
|
<li>Toggles de visibilidad de modelos</li>
|
||||||
|
<li>Acceso al directorio de logs</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🖥️ Experiencia de Escritorio</h3>
|
||||||
|
<ul>
|
||||||
|
<li>App nativa Electron con integración en bandeja del sistema</li>
|
||||||
|
<li>Sidebar colapsable con estado persistente</li>
|
||||||
|
<li>Layouts responsivos en todas las vistas</li>
|
||||||
|
<li>Barra de estado con indicadores en vivo de proxy y conexión</li>
|
||||||
|
<li>Error boundaries con fallbacks amigables al usuario</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 Capturas de Pantalla
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Instalación e Inicio Rápido
|
||||||
|
|
||||||
|
### 📦 Descarga
|
||||||
|
|
||||||
|
Puedes descargar los binarios precompilados más recientes para Windows, macOS y Linux desde nuestra [página de Releases](https://github.com/evandrodevbr/GeminiNexus/releases).
|
||||||
|
|
||||||
|
*(Consulta la advertencia en la parte superior de esta página si usas Windows y te encuentras con el aviso de SmartScreen).*
|
||||||
|
|
||||||
|
### 🔌 Uso con IDEs de IA
|
||||||
|
|
||||||
|
Con la aplicación en ejecución y al menos una cuenta agregada, configura tu IDE preferido (Cursor, Windsurf, OpenCode, etc.):
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
API Base URL: http://localhost:10100/v1
|
||||||
|
API Key: (copiar de la página Proxy en la app)
|
||||||
|
Model: gemini-3-flash (o cualquier modelo mapeado)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🛠️ Compilar desde el Código Fuente
|
||||||
|
|
||||||
|
#### Requisitos Previos
|
||||||
|
|
||||||
|
- **Node.js** v20 o superior
|
||||||
|
- **npm** (este proyecto usa `package-lock.json`)
|
||||||
|
|
||||||
|
#### Pasos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clonar el repositorio
|
||||||
|
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||||
|
cd GeminiNexus
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Iniciar en modo desarrollo
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Compilar para producción (ej: instalador de Windows)
|
||||||
|
npm run make
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Stack Tecnológico
|
||||||
|
|
||||||
|
| Categoría | Tecnologías |
|
||||||
|
| ----------------- | ---------------------------------------------- |
|
||||||
|
| **Core** | Electron, React 19, TypeScript |
|
||||||
|
| **Build** | Vite, Electron Forge |
|
||||||
|
| **Estilos** | Tailwind CSS v4, Radix UI, Lucide Icons |
|
||||||
|
| **Estado** | TanStack Query, TanStack Router |
|
||||||
|
| **Backend** | NestJS (gateway interno), ORPC (RPC type-safe) |
|
||||||
|
| **Base de Datos** | Better-SQLite3, Drizzle ORM |
|
||||||
|
| **Gráficos** | Nivo (Line, Bar, Pie) |
|
||||||
|
| **Testing** | Vitest, Playwright |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Desarrollo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Iniciar entorno de dev (Electron + Vite HMR)
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Ejecutar linting
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Verificación de tipos
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# Formatear código
|
||||||
|
npm run format:write
|
||||||
|
|
||||||
|
# Ejecutar pruebas
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ Preguntas Frecuentes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: ¿Aviso de Windows SmartScreen ("Windows protegió su PC") durante la instalación?</b></summary>
|
||||||
|
|
||||||
|
Sí, este es un aviso común para nuevas aplicaciones no firmadas. Haz clic en **"Más información"** y luego en **"Ejecutar de todas formas"**. Consulta la advertencia en la parte superior del README para más detalles.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: ¿La app no inicia al compilar desde el código fuente?</b></summary>
|
||||||
|
|
||||||
|
1. Asegúrate de que todas las dependencias están instaladas: `npm install`.
|
||||||
|
2. Verifica que tu versión de Node.js sea >= 20.
|
||||||
|
3. Intenta eliminar `node_modules` y reinstalar.
|
||||||
|
4. En Windows, asegúrate de que el WiX Toolset esté disponible para el comando `npm run make`.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: ¿El inicio de sesión de la cuenta falló?</b></summary>
|
||||||
|
|
||||||
|
1. Asegúrate de que tu conexión a la red funcione correctamente.
|
||||||
|
2. Intenta borrar los datos de la app y volver a iniciar sesión.
|
||||||
|
3. Comprueba si la cuenta está restringida por Google/Claude.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: ¿El IDE no se puede conectar al proxy?</b></summary>
|
||||||
|
|
||||||
|
1. Asegúrate de que el proxy esté funcionando (indicador verde en la barra de estado).
|
||||||
|
2. Confirma que el puerto coincide con tu configuración del IDE (por defecto `10100`).
|
||||||
|
3. Copia la API key directamente desde la página Proxy y pégala en la configuración de tu IDE.
|
||||||
|
4. Asegúrate de tener al menos una cuenta activa en la página Cuentas.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contribuir
|
||||||
|
|
||||||
|
¡Las contribuciones son bienvenidas! Por favor lee `CONTRIBUTING.md` para más detalles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
|
||||||
|
Este proyecto utiliza una estructura de licencia dual:
|
||||||
|
|
||||||
|
- **Código original** de [AntigravityManager](https://github.com/Draculabo/AntigravityManager) por [Draculabo](https://github.com/Draculabo): Licenciado bajo [CC BY-NC-SA 4.0](LICENSE).
|
||||||
|
- **Todo el código nuevo, funcionalidades y arquitectura** por [evandrodevbr](https://github.com/evandrodevbr): Licenciado bajo la [Licencia MIT](LICENSE-MIT).
|
||||||
|
|
||||||
|
> 🔄 **Aviso de Migración:** Este proyecto está siendo migrado activamente a una licencia completamente open-source (MIT). A medida que el código original restante se reescribe progresivamente, todo el proyecto hará la transición a MIT. Estamos comprometidos a hacer Gemini Nexus 100% open-source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Créditos
|
||||||
|
|
||||||
|
Este proyecto fue originalmente un fork de [AntigravityManager](https://github.com/Draculabo/AntigravityManager) creado por [Draculabo](https://github.com/Draculabo). El concepto inicial del proxy y la estructura base de Electron se basaron en su trabajo.
|
||||||
|
|
||||||
|
Desde el fork, **la gran mayoría del código ha sido reescrito, rediseñado y expandido** por [evandrodevbr](https://github.com/evandrodevbr), incluyendo pero no limitado a:
|
||||||
|
|
||||||
|
- Rediseño completo de UI/UX (navegación, cuentas, analíticas de uso, gestión del proxy)
|
||||||
|
- Dashboard de Analíticas de Uso con seguimiento de costos, promedios de tokens e integración con OpenRouter
|
||||||
|
- Monitor de tráfico con inspección de tokens/costos por solicitud
|
||||||
|
- Aislamiento de entorno (dev/prod), pipeline CI/CD e infraestructura de pruebas
|
||||||
|
- Configuración rápida para IDEs (OpenCode, Cursor, VS Code, Claude Code)
|
||||||
|
- Configuración personalizada de precios de modelos
|
||||||
|
- Toda la documentación e internacionalización (EN, PT-BR, ES, ZH-CN)
|
||||||
@@ -1,411 +1,190 @@
|
|||||||
<p align="center">
|
<div align="center">
|
||||||
<img src="docs/assets/logo.png" alt="Antigravity Manager" width="128" height="128" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">Antigravity Manager</h1>
|
<img src="docs/assets/logo.png" width="110" alt="Gemini Nexus logo">
|
||||||
|
|
||||||
<p align="center">
|
<h1>Gemini Nexus</h1>
|
||||||
<strong>🚀 Professional multi-account manager for Google Gemini & Claude AI</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p><strong>Desktop gateway that pools Google accounts behind a local OpenAI/Anthropic-compatible API, with encrypted credentials and auto-switching.</strong></p>
|
||||||
English | <a href="README.zh-CN.md">简体中文</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<a href="#-quickstart">Quickstart</a> ·
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml">
|
<a href="#-architecture">Architecture</a> ·
|
||||||
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
|
<a href="#-security-model">Security</a> ·
|
||||||
</a>
|
<a href="https://git.evandro.dev.br/evandro/gemini-nexus/issues">Issues</a>
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml">
|
|
||||||
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/releases">
|
|
||||||
<img src="https://img.shields.io/github/v/release/Draculabo/AntigravityManager?style=flat-square" alt="Release" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/releases">
|
|
||||||
<img src="https://img.shields.io/github/downloads/Draculabo/AntigravityManager/total?style=flat-square&color=blue" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/blob/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/Draculabo/AntigravityManager?style=flat-square" alt="License" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
|
|
||||||
<img src="https://img.shields.io/github/stars/Draculabo/AntigravityManager?style=flat-square" alt="Stars" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|

|
||||||
<img src="https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white" alt="Electron" />
|

|
||||||
<img src="https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" alt="React" />
|

|
||||||
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
||||||
<img src="https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="TailwindCSS" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
<sub>English · <a href="README.pt-BR.md">Português</a> · <a href="README.zh-CN.md">中文</a> · <a href="README.es.md">Español</a></sub>
|
||||||
|
|
||||||
## 📖 Table of Contents
|
</div>
|
||||||
|
|
||||||
- [Why Antigravity Manager?](#-why-antigravity-manager)
|
<hr />
|
||||||
- [Features](#-features)
|
|
||||||
- [Screenshots](#-screenshots)
|
|
||||||
- [Quick Start](#-quick-start)
|
|
||||||
- [Tech Stack](#️-tech-stack)
|
|
||||||
- [Development](#-development)
|
|
||||||
- [Debugging Docs](#-debugging-docs)
|
|
||||||
- [FAQ](#-faq)
|
|
||||||
- [Contributing](#-contributing)
|
|
||||||
- [License](#-license)
|
|
||||||
|
|
||||||
---
|
## 📌 The problem
|
||||||
|
|
||||||
## ✨ Why Antigravity Manager?
|
Working with AI-powered IDEs and coding tools on a single Google account means quota that runs out, manual account switching, no idea how many tokens you spent, and no view of what the proxy actually sends upstream. Gemini Nexus keeps all accounts in one pool, switches between them automatically and shows the traffic.
|
||||||
|
|
||||||
When using Antigravity IDE, have you ever encountered these problems?
|
## ✨ What it does
|
||||||
|
|
||||||
- 😫 Single account quota runs out quickly, requiring frequent manual switching
|
- **Pools** Google accounts added through OAuth, with status (Active, Rate Limited, Expired), last-used time and an optional per-account proxy URL.
|
||||||
- 🔄 Managing multiple Google/Claude accounts is cumbersome
|
- **Switches** accounts automatically when quota drops below 5 % or an account is rate-limited; `CloudMonitorService` polls quota every 5 minutes.
|
||||||
- 📊 Don't know how much quota is left on the current account
|
- **Schedules** requests across the pool in `cache-first`, `balance` (default) or `performance-first` mode, with a per-account circuit breaker (default backoff 60 s → 5 min → 30 min → 2 h).
|
||||||
- ⏰ Worried about missing quota reset times
|
- **Serves** `POST /v1/chat/completions` (OpenAI), `POST /v1/messages` (Anthropic), `GET /v1/models` and `GET /v1/models/capabilities`, so Cursor, Windsurf, OpenCode, Claude Code and similar clients can connect.
|
||||||
- 🔌 Need a reliable local API proxy for development tools
|
- **Maps** requested model names to upstream models through the `custom_mapping` and `anthropic_mapping` settings; request timeout defaults to 120 s.
|
||||||
|
- **Tracks** token usage per account and model, charts prompt and completion tokens, and estimates cost with editable per-model pricing.
|
||||||
|
- **Inspects** traffic (latency, status, model), replays logged requests and generates cURL and Python snippets plus an IDE Quick Setup.
|
||||||
|
- **Protects** stored credentials with AES-256-GCM and an OS-keychain master key (see [Security model](#-security-model)).
|
||||||
|
|
||||||
**Antigravity Manager** is here to solve these problems! It's a professional Electron desktop app that helps you:
|
## 🖼️ Demo
|
||||||
|
|
||||||
- ✅ **Unlimited Account Pool** - Add any number of Google Gemini / Claude accounts
|
<details>
|
||||||
- ✅ **Smart Auto-Switching** - Automatically switch to the next available account when quota is low or rate-limited
|
<summary>Screenshots</summary>
|
||||||
- ✅ **Real-time Monitoring** - Visualize quota usage for all accounts
|
|
||||||
- ✅ **Local API Proxy** - Built-in OpenAI/Anthropic compatible proxy server
|
|
||||||
- ✅ **Secure Encryption** - AES-256-GCM encryption for sensitive data
|
|
||||||
|
|
||||||
---
|
<p align="center"><img src="docs/assets/main.png" alt="Main dashboard" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/2usage.png" alt="Usage analytics" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/3proxy.png" alt="Proxy monitor" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/4routing.png" alt="Routing config" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/5docs.png" alt="Documentation" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/6connections.png" alt="Active connections" width="80%"></p>
|
||||||
|
<p align="center"><img src="docs/assets/7config.png" alt="Settings" width="80%"></p>
|
||||||
|
|
||||||
## 🎯 Features
|
</details>
|
||||||
|
|
||||||
<table>
|
## 🚀 Quickstart
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>☁️ Cloud Account Pool</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Add unlimited Google Gemini / Claude accounts via OAuth</li>
|
|
||||||
<li>Display avatar, email, status, and last used time</li>
|
|
||||||
<li>Real-time status monitoring (Active, Rate Limited, Expired)</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>📊 Real-time Quota Monitoring</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Multi-model support: gemini-pro, claude-3-5-sonnet, etc.</li>
|
|
||||||
<li>Visual progress bars with color indicators</li>
|
|
||||||
<li>Auto & manual refresh capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🔄 Intelligent Auto-Switching</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Unlimited pool mode with smart backup selection</li>
|
|
||||||
<li>Auto-switch when quota < 5% or rate-limited</li>
|
|
||||||
<li>Background monitoring every 5 minutes</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🔐 Security First</h3>
|
|
||||||
<ul>
|
|
||||||
<li>AES-256-GCM encryption for sensitive data</li>
|
|
||||||
<li>OS native credential manager integration</li>
|
|
||||||
<li>Auto migration of legacy plaintext data</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>💾 Account Backup</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Capture snapshots of account state</li>
|
|
||||||
<li>Fast switching between saved accounts</li>
|
|
||||||
<li>View, organize, and delete snapshots</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>⚙️ Process Control</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Auto-detect if Antigravity is running</li>
|
|
||||||
<li>Launch via URI protocol or executable</li>
|
|
||||||
<li>Graceful close or force kill</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🔌 Local API Proxy</h3>
|
|
||||||
<ul>
|
|
||||||
<li>OpenAI & Anthropic API compatible</li>
|
|
||||||
<li>Configurable port and request timeout</li>
|
|
||||||
<li>Model mapping (e.g. Claude → Gemini)</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🛠️ Developer Tools</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Built-in cURL & Python code generation</li>
|
|
||||||
<li>Visual service status monitoring</li>
|
|
||||||
<li>One-click API Key regeneration</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### Additional Features
|
**Use a release.** Download the build for your platform from [Releases](https://git.evandro.dev.br/evandro/gemini-nexus/releases), open the app, add at least one account, enable the proxy (it is disabled by default) and point your IDE at it:
|
||||||
|
|
||||||
- **🖥️ System Tray** - Background mode with tray icon and right-click menu
|
```text
|
||||||
- **🔗 IDE Sync** - Automatically scan and import accounts from IDE's `state.vscdb`
|
API Base URL: http://localhost:8045/v1 (default, configurable on the Proxy page; development builds use 8046)
|
||||||
- **📦 Batch Operations** - Batch refresh and delete multiple accounts
|
API Key: (copy it from the Proxy page in the app)
|
||||||
- **📤 JSON Export/Import** - Export and import account pools with schema validation and deduplication
|
Model: gemini-3-flash (or any mapped model)
|
||||||
- **🔔 Desktop Notifications** - Configurable alerts when model quota drops below your threshold
|
|
||||||
- **🌐 Per-Account Proxy** - Route individual accounts through their own HTTP/SOCKS5 proxy
|
|
||||||
- **📊 Smart Sorting** - Sort accounts by recently used, overall quota, or specific model groups
|
|
||||||
- **📋 Compact Layout** - Dense horizontal view to maximize visible accounts
|
|
||||||
- **🌏 Internationalization** - Multi-language support (English / 中文 / Русский)
|
|
||||||
- **🎨 Modern UI** - Built with React, TailwindCSS, and Shadcn UI
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📸 Screenshots
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/assets/screenshot-main.png" alt="Main Interface" width="80%" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/assets/screenshot-proxy.png" alt="Proxy Interface" width="48%" />
|
|
||||||
<img src="docs/assets/screenshot-setting.png" alt="Settings Interface" width="48%" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## � Quick Start
|
|
||||||
|
|
||||||
### Download
|
|
||||||
|
|
||||||
Download the latest release for your platform from the [Releases](https://github.com/Draculabo/AntigravityManager/releases) page.
|
|
||||||
|
|
||||||
| Platform | Download |
|
|
||||||
|----------|----------|
|
|
||||||
| Windows (x64/ARM64) | [.exe installer](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| macOS | [.dmg installer](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| NixOS / Nix | `nix run github:Draculabo/AntigravityManager` |
|
|
||||||
|
|
||||||
### ❄️ Nix Integration
|
|
||||||
|
|
||||||
You can integrate Antigravity Manager into your Nix configuration using the provided flake.
|
|
||||||
|
|
||||||
#### 🛠️ NixOS / Home Manager
|
|
||||||
|
|
||||||
Add the flake to your `flake.nix` inputs:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
||||||
antigravity-manager.url = "github:Draculabo/AntigravityManager";
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To integrate with your system, add `antigravity-manager.overlays.default` to your `nixpkgs.overlays`. This ensures the package is correctly built against your own `nixpkgs` instance and respects your global configuration.
|
> [!WARNING]
|
||||||
|
> Builds are not code-signed. If Windows shows "Windows protected your PC", click **More info → Run anyway**.
|
||||||
|
|
||||||
```nix
|
**Build from source.**
|
||||||
# Add to your NixOS/Home Manager configuration
|
|
||||||
{
|
|
||||||
nixpkgs.overlays = [ inputs.antigravity-manager.overlays.default ];
|
|
||||||
|
|
||||||
# Antigravity Manager has an unfree license (cc-by-nc-sa-40)
|
| Tool | Minimum version |
|
||||||
nixpkgs.config.allowUnfree = true;
|
|---|---|
|
||||||
|
| Node.js | 20+ (recommended in `AGENTS.md`; `.nvmrc` pins 22.17.1) |
|
||||||
environment.systemPackages = [
|
| pnpm | 10.11.0 (`packageManager`; lockfile is `pnpm-lock.yaml`) |
|
||||||
pkgs.antigravity-manager
|
| Native toolchain | needed by `better-sqlite3` and `keytar`: Visual Studio 2022 Build Tools (C++) on Windows, Xcode Command Line Tools on macOS, `build-essential` + `python3` + `libsecret-1-dev` on Ubuntu (`base-devel` + `python` + `libsecret` on Arch) |
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 💻 Development Shell
|
|
||||||
|
|
||||||
If you're using the overlay above, you can simply add it to your `devShell`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = [
|
|
||||||
pkgs.antigravity-manager
|
|
||||||
];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Since the package is unfree, you **must** have `allowUnfree = true;` enabled in your `nixpkgs` configuration for it to evaluate correctly.
|
|
||||||
|
|
||||||
### Build from Source
|
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
- Node.js v18 or higher
|
|
||||||
- npm or yarn
|
|
||||||
|
|
||||||
#### Steps
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
git clone https://git.evandro.dev.br/evandro/gemini-nexus.git
|
||||||
git clone https://github.com/Draculabo/AntigravityManager.git
|
cd gemini-nexus
|
||||||
cd AntigravityManager
|
pnpm install # postinstall runs scripts/setup-dev.js --check-only to verify the toolchain
|
||||||
|
pnpm start # dev mode (Electron + Vite HMR)
|
||||||
# Install dependencies
|
pnpm run make # installers/packages for the current platform
|
||||||
npm install
|
pnpm run make:win # Windows: extracts WiX from wix314-binaries.zip, then runs make
|
||||||
|
|
||||||
# Start development
|
|
||||||
npm start
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
npm run make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
There is no `.env.example` and no variable is required. Optional variables read by the code or described in the docs:
|
||||||
|
|
||||||
## �🛠️ Tech Stack
|
| Variable | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | No | Route the app's outbound requests through a local proxy (see [TLS inspection](#-security-model)). |
|
||||||
|
| `NODE_EXTRA_CA_CERTS` | No | Extra CA certificate for Node-side TLS when inspecting traffic. |
|
||||||
|
| `SENTRY_DSN` | No | Sentry DSN used when error reporting is enabled. |
|
||||||
|
|
||||||
| Category | Technologies |
|
## 🧱 Architecture
|
||||||
|----------|-------------|
|
|
||||||
| **Core** | [Electron](https://www.electronjs.org/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
|
|
||||||
| **Build Tool** | [Vite](https://vitejs.dev/) |
|
|
||||||
| **Styling** | [TailwindCSS](https://tailwindcss.com/), [Shadcn UI](https://ui.shadcn.com/) |
|
|
||||||
| **State** | [TanStack Query](https://tanstack.com/query/latest), [TanStack Router](https://tanstack.com/router/latest) |
|
|
||||||
| **Database** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3) |
|
|
||||||
| **Testing** | [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/) |
|
|
||||||
|
|
||||||
---
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
## � Development
|
C["Client: Cursor, Windsurf, OpenCode, Claude Code"] -->|"/v1/chat/completions, /v1/messages"| P["NestJS proxy, port 8045"]
|
||||||
|
P --> S["Account scheduler and circuit breaker"]
|
||||||
### Available Scripts
|
S -->|"Google OAuth account"| G["Google upstream APIs"]
|
||||||
|
M["CloudMonitor and AutoSwitch, quota every 5 min"] -->|"quota polling"| G
|
||||||
| Command | Description |
|
S -->|"reads accounts"| DB[("SQLite: better-sqlite3 and Drizzle")]
|
||||||
|---------|-------------|
|
M --> DB
|
||||||
| `npm start` | Start the app in development mode |
|
K["AES-256-GCM, master key in OS keychain"] -->|"encrypts tokens"| DB
|
||||||
| `npm run lint` | Run ESLint to check for code issues |
|
|
||||||
| `npm run format:write` | Format code with Prettier |
|
|
||||||
| `npm run test:unit` | Run unit tests with Vitest |
|
|
||||||
| `npm run test:e2e` | Run E2E tests with Playwright |
|
|
||||||
| `npm run test:all` | Run all tests |
|
|
||||||
| `npm run type-check` | Run TypeScript type checking |
|
|
||||||
| `npm run make` | Build production packages |
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
AntigravityManager/
|
|
||||||
├── src/
|
|
||||||
│ ├── main.ts # Electron main process
|
|
||||||
│ ├── preload.ts # Preload script
|
|
||||||
│ ├── renderer/ # React renderer process
|
|
||||||
│ ├── ipc/ # IPC communication handlers
|
|
||||||
│ └── server/ # Built-in server
|
|
||||||
├── docs/ # Documentation and assets
|
|
||||||
└── .github/ # GitHub configuration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
1. A client sends OpenAI- or Anthropic-format requests to the NestJS proxy (Fastify, bound to `0.0.0.0`).
|
||||||
|
2. The proxy guard checks the API key when one is configured; with an empty key the proxy runs in open mode.
|
||||||
|
3. The token manager picks an account according to the scheduling mode and each account's circuit-breaker state, then calls Google's upstream APIs with that account's OAuth token.
|
||||||
|
4. `CloudMonitorService` polls quota every 5 minutes and `AutoSwitchService` switches the active account when quota is below 5 % or the account is rate-limited.
|
||||||
|
5. The React 19 renderer (TanStack Router and Query) talks to the main process over oRPC on IPC; accounts, usage and settings live in SQLite (better-sqlite3 + Drizzle). Design notes: [`docs/cloud_features.md`](docs/cloud_features.md).
|
||||||
|
|
||||||
## 🔍 Debugging Docs
|
## 🔒 Security model
|
||||||
|
|
||||||
If you want to inspect Antigravity or Antigravity Manager traffic with Proxyman, start here:
|
Gemini Nexus holds OAuth tokens for several accounts and forwards traffic upstream, so credential handling is the core of the design. Everything below is implemented in this repository; limits are stated where they exist.
|
||||||
|
|
||||||
- [Proxyman Installation and Basic Setup on Windows](docs/proxyman-install.md)
|
- **Credential storage and isolation.** `token_json` and `quota_json` are encrypted with AES-256-GCM (unique IV per operation, authenticated) before they reach SQLite. The master key lives in the OS credential store through `keytar` (Windows Credential Manager, macOS Keychain, Linux Secret Service/libsecret), never in a plain file. Legacy plaintext rows are migrated at startup. Accounts are kept in their own database (`cloud_accounts.db`), independent from the IDE's local database.
|
||||||
- [Proxyman Debugging Guide for Antigravity and Antigravity Manager](docs/proxyman-debugging.md)
|
- **Log masking.** Keys such as `token`, `refresh_token`, `api_key`, `authorization`, `cookie` and `client_secret` are replaced by `[REDACTED]` before logging ([`src/utils/sensitiveDataMasking.ts`](src/utils/sensitiveDataMasking.ts)); the traffic logger also redacts the `authorization`, `x-api-key` and `cookie` headers.
|
||||||
|
- **Request logging, not an audit trail.** Winston writes API, traffic and app logs to rotating files (API log: 30 days, 50 MB per file). These are operational logs, not a tamper-evident audit trail; the `*-audit.json` files are rotation bookkeeping from `winston-daily-rotate-file`.
|
||||||
|
- **Proxy access.** The proxy is off by default. Once enabled it listens on `0.0.0.0`, and requests are authenticated only when `api_key` is non-empty (empty means open mode). Set a key and keep it private.
|
||||||
|
- **TLS inspection (debugging only).** [`docs/proxyman-install.md`](docs/proxyman-install.md) and [`docs/proxyman-debugging.md`](docs/proxyman-debugging.md) explain how to read the app's HTTPS traffic with Proxyman: trust its root CA, set the proxy variables and `NODE_EXTRA_CA_CERTS`, then start the app. `NODE_TLS_REJECT_UNAUTHORIZED=0` (and the script's `-AllowInsecureTls` flag) is a temporary override; never leave it on. Do not commit proxy addresses, certificate paths, cookies or OAuth codes, and redact logs before sharing them. `scripts/start-antigravity-with-proxyman.ps1` targets the installed official Antigravity client, not this app.
|
||||||
|
- **Error reporting.** Sentry reports (they need `SENTRY_DSN`) are gated by the `error_reporting_enabled` setting, which `DEFAULT_APP_CONFIG` ships as `true`; turn it off in Settings to opt out. The Windows user-name segment is masked in reported exception messages.
|
||||||
|
|
||||||
---
|
Supported versions and how to report a vulnerability: [`SECURITY.md`](SECURITY.md).
|
||||||
|
|
||||||
## ❓ FAQ
|
## 🗂️ Structure
|
||||||
|
|
||||||
<details>
|
```text
|
||||||
<summary><b>Q: The app won't start?</b></summary>
|
src/
|
||||||
|
├── components/ # UI (proxy/, usage/, ui/ primitives)
|
||||||
Please check:
|
├── ipc/ # oRPC routers (account, cloud, config, gateway, usage...)
|
||||||
1. Make sure all dependencies are installed: `npm install`
|
├── server/ # NestJS proxy gateway (modules: proxy, usage)
|
||||||
2. Check if Node.js version is >= 18
|
├── services/ # AutoSwitchService, CloudMonitorService, GoogleAPIService
|
||||||
3. Try deleting `node_modules` and reinstalling
|
├── routes/ # index, usage, proxy, settings
|
||||||
|
├── utils/ # paths, logger, sensitiveDataMasking
|
||||||
</details>
|
└── tests/ # unit tests (Vitest) and E2E (Playwright)
|
||||||
|
cli/ # separate Python CLI for account switching and quota checks
|
||||||
<details>
|
docs/ # design notes and Proxyman guides
|
||||||
<summary><b>Q: Account login failed?</b></summary>
|
scripts/ # setup-dev, build-windows, generate-assets
|
||||||
|
|
||||||
1. Ensure network connection is working
|
|
||||||
2. Try clearing app data and logging in again
|
|
||||||
3. Check if the account is restricted by Google/Claude
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>Q: macOS shows Keychain/Credential error and OAuth cannot be saved?</b></summary>
|
|
||||||
|
|
||||||
This is a common macOS security behavior, usually when the app is unsigned or run directly from Downloads.
|
|
||||||
This is a **temporary workaround** for personal use:
|
|
||||||
|
|
||||||
1. Move the app to `/Applications`
|
|
||||||
2. Run the following commands in Terminal (repeat after every update)
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
sudo xattr -dr com.apple.quarantine "/Applications/Antigravity Manager 2.app"
|
|
||||||
codesign --force --deep --sign - "/Applications/Antigravity Manager 2.app"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Reopen the app and allow Keychain access if prompted.
|
## 🧪 Scripts
|
||||||
|
|
||||||
</details>
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `pnpm start` | Run the app in dev mode (`electron-forge start`, with `DEBUG=electron-forge:*`). |
|
||||||
|
| `pnpm run dev` | Same as `start`. |
|
||||||
|
| `pnpm run package` | Package the app with Electron Forge, without building installers. |
|
||||||
|
| `pnpm run make` | Build installers/packages for the current platform. |
|
||||||
|
| `pnpm run make:win` | Windows build helper: extracts WiX from `wix314-binaries.zip`, adds it to `PATH`, runs `electron-forge make`. |
|
||||||
|
| `pnpm run publish` | Publish through Electron Forge. |
|
||||||
|
| `pnpm run setup` | Run `scripts/setup-dev.js`: detect the native toolchain and install or guide the setup. |
|
||||||
|
| `pnpm run postinstall` | `scripts/setup-dev.js --check-only`; runs automatically after `pnpm install`. |
|
||||||
|
| `pnpm run lint` | ESLint over the repo. |
|
||||||
|
| `pnpm run format` | Prettier check. |
|
||||||
|
| `pnpm run format:write` | Prettier write. |
|
||||||
|
| `pnpm test` | Vitest unit tests (`vitest run`). |
|
||||||
|
| `pnpm run test:watch` | Vitest in watch mode. |
|
||||||
|
| `pnpm run test:unit` | Vitest (default mode). |
|
||||||
|
| `pnpm run test:e2e` | Playwright E2E tests. |
|
||||||
|
| `pnpm run test:all` | Unit tests, then Playwright. |
|
||||||
|
| `pnpm run type-check` | `tsc --noEmit`. |
|
||||||
|
|
||||||
<details>
|
## 📚 Documentation
|
||||||
<summary><b>Q: How to report issues or suggestions?</b></summary>
|
|
||||||
|
|
||||||
Please submit issues or suggestions via [GitHub Issues](https://github.com/Draculabo/AntigravityManager/issues).
|
- [`docs/cloud_features.md`](docs/cloud_features.md): account pool, quota monitoring and auto-switch design.
|
||||||
|
- [`docs/proxyman-install.md`](docs/proxyman-install.md) and [`docs/proxyman-debugging.md`](docs/proxyman-debugging.md): TLS inspection setup (Windows).
|
||||||
|
- [`docs/OpencodeAPI.md`](docs/OpencodeAPI.md): OpenCode custom-endpoint integration (in Portuguese).
|
||||||
|
- [`cli/README.md`](cli/README.md): the Python command-line tool.
|
||||||
|
- [`CONTRIBUTING.md`](CONTRIBUTING.md), [`SECURITY.md`](SECURITY.md), [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md), [`CHANGELOG.md`](CHANGELOG.md), [`LICENSE`](LICENSE), [`LICENSE-MIT`](LICENSE-MIT).
|
||||||
|
- Translations: [Português](README.pt-BR.md), [中文](README.zh-CN.md), [Español](README.es.md).
|
||||||
|
|
||||||
</details>
|
## 🛣️ Limitations and next steps
|
||||||
|
|
||||||
---
|
- Upstream access goes through Google's `v1internal` Cloud Code endpoints and OAuth scopes ([`src/services/GoogleAPIService.ts`](src/services/GoogleAPIService.ts)). These are internal, undocumented APIs and may change without notice. The project is unofficial and not affiliated with Google or Anthropic.
|
||||||
|
- Accounts are added through Google OAuth; Claude models are reached through model mapping, not through separate Claude accounts.
|
||||||
## 🌟 Star History
|
- End-to-end tests are not run in CI (Electron needs a graphical environment); run `pnpm run test:e2e` locally.
|
||||||
|
- The Windows MSI is only produced when the WiX Toolset is available on the build machine; otherwise the Windows artifact is the ZIP.
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
|
- The workflows in `.github/workflows/` are written for GitHub (they call the `gh` CLI and the GitHub release publisher), so this README shows no CI badge.
|
||||||
<picture>
|
- Next step stated by the project: migrate the remaining original code to MIT (see [License](#-license)).
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
|
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
Read [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). Releases are versioned by semantic-release from Conventional Commits. Before opening a PR, these must pass:
|
||||||
|
|
||||||
We follow the [Contributor Covenant](CODE_OF_CONDUCT.md) Code of Conduct.
|
```bash
|
||||||
|
pnpm run lint && pnpm run format:write && pnpm run type-check && pnpm test
|
||||||
---
|
```
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
[CC BY-NC-SA 4.0](LICENSE)
|
Mixed licensing: code inherited from [AntigravityManager](https://github.com/Draculabo/AntigravityManager) (by Draculabo) is [CC BY-NC-SA 4.0](LICENSE) (non-commercial, share-alike); all new code is [MIT](LICENSE-MIT). The project is migrating to MIT as the remaining original code is rewritten, so check both files before any commercial use.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> **For Educational Purposes Only**
|
|
||||||
>
|
|
||||||
> This project is intended solely for educational and research purposes. It is provided "as-is" without any warranty. **Commercial use is strictly prohibited.**
|
|
||||||
>
|
|
||||||
> By using this software, you agree that you will not use it for any commercial purposes, and you are solely responsible for ensuring your use complies with all applicable laws and regulations. The authors and contributors are not responsible for any misuse or damages arising from the use of this software.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
If this project helps you, please give it a ⭐ Star!
|
|
||||||
</p>
|
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/logo.png" alt="Gemini Nexus" width="128" height="128" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">Gemini Nexus</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>🚀 Gateway de IA multi-contas profissional para Google Gemini & Claude</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">English</a> | Português | <a href="README.zh-CN.md">中文</a> | <a href="README.es.md">Español</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
|
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="Release" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
|
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="Downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="Licença" />
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/plataforma-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="Plataforma" />
|
||||||
|
<img src="https://img.shields.io/badge/electron-latest-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Usuários do Windows:** Se você vir um aviso do SmartScreen **"O Windows protegeu o seu computador"** durante a instalação, clique em **"Mais informações"** e depois em **"Executar mesmo assim"**. Isso é um aviso de segurança esperado para aplicativos novos e não assinados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Sumário
|
||||||
|
|
||||||
|
- [✨ Por que o Gemini Nexus?](#-por-que-o-gemini-nexus)
|
||||||
|
- [🎯 Funcionalidades](#-funcionalidades)
|
||||||
|
- [📸 Screenshots](#-screenshots)
|
||||||
|
- [⚡ Instalação e Início Rápido](#-instalação-e-início-rápido)
|
||||||
|
- [🛠️ Stack Tecnológica](#️-stack-tecnológica)
|
||||||
|
- [💻 Desenvolvimento](#-desenvolvimento)
|
||||||
|
- [❓ FAQ](#-faq)
|
||||||
|
- [🤝 Contribuindo](#-contribuindo)
|
||||||
|
- [📄 Licença](#-licença)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Por que o Gemini Nexus?
|
||||||
|
|
||||||
|
Ao usar IDEs e ferramentas de programação com IA, você já enfrentou estes problemas?
|
||||||
|
|
||||||
|
- 😫 **Limites de cota:** A cota da conta única acaba rápido, exigindo trocas manuais frequentes.
|
||||||
|
- 🔄 **Gestão de contas:** Gerenciar múltiplas contas do Google/Claude é cansativo.
|
||||||
|
- 📊 **Uso às cegas:** Você não sabe quantos tokens já consumiu ou quanta cota resta.
|
||||||
|
- 🔌 **Problemas de integração:** Precisa de um proxy local confiável que fale os protocolos OpenAI/Anthropic nativamente.
|
||||||
|
- 🔍 **Falta de transparência:** Não consegue ver o que está sendo enviado ou recebido pelo proxy nos bastidores.
|
||||||
|
|
||||||
|
O **Gemini Nexus** resolve tudo isso. É um aplicativo desktop profissional em Electron que funciona como um gateway inteligente entre suas ferramentas de desenvolvimento e o Google Gemini / Claude AI.
|
||||||
|
|
||||||
|
### Principais Vantagens
|
||||||
|
|
||||||
|
- ✅ **Pool de Contas Ilimitado** — Adicione quantas contas Google Gemini & Claude quiser.
|
||||||
|
- ✅ **Troca Automática Inteligente** — Rotaciona automaticamente para a próxima conta disponível quando a cota está baixa ou com rate limit.
|
||||||
|
- ✅ **Analytics de Uso em Tempo Real** — Dashboard nível SaaS com gráficos de área, indicadores de tendência e distribuição por modelo.
|
||||||
|
- ✅ **Observabilidade Completa do Proxy** — Monitor de tráfego ao vivo, replay de requisições e inspetor de capacidades dos modelos.
|
||||||
|
- ✅ **Compatível com OpenAI & Anthropic** — Proxy drop-in para Cursor, Windsurf, OpenCode e qualquer ferramenta compatível com OpenAI.
|
||||||
|
- ✅ **Seguro por Padrão** — Criptografia AES-256-GCM com gerenciamento nativo de credenciais do sistema operacional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Funcionalidades
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>☁️ Pool de Contas na Nuvem</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Adicione contas ilimitadas do Google Gemini / Claude via OAuth</li>
|
||||||
|
<li>Exibe avatar, e-mail, status e último uso</li>
|
||||||
|
<li>Monitoramento de status em tempo real (Ativa, Rate Limited, Expirada)</li>
|
||||||
|
<li>Configuração de proxy URL por conta</li>
|
||||||
|
<li>Gerenciamento de perfil de identidade do dispositivo com histórico</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>📊 Dashboard de Analytics de Uso</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Consumo de tokens em tempo real com auto-refresh a cada 15s</li>
|
||||||
|
<li>Gráficos de área para tokens de prompt & completion (diário/horário)</li>
|
||||||
|
<li>Indicadores de tendência (% de variação vs período anterior)</li>
|
||||||
|
<li>Sparklines inline nos cards de estatísticas</li>
|
||||||
|
<li>Ranking de distribuição por modelo com gráficos de barras horizontais</li>
|
||||||
|
<li>Visualização da proporção Prompt/Completion</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔌 Proxy de API Local (Gateway)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Endpoint compatível com OpenAI <code>/v1/chat/completions</code></li>
|
||||||
|
<li>Endpoint compatível com Anthropic <code>/v1/messages</code></li>
|
||||||
|
<li>Streaming SSE completo (testado com Cursor, Windsurf, OpenCode)</li>
|
||||||
|
<li>Mapeamento de modelos (ex: <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||||
|
<li>Porta, timeout e API key configuráveis</li>
|
||||||
|
<li>Controle de visibilidade de modelos</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔄 Troca Automática Inteligente</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Modo de pool ilimitado com seleção inteligente de backup</li>
|
||||||
|
<li>Troca automática quando cota < 5% ou com rate limit</li>
|
||||||
|
<li>Monitoramento em segundo plano a cada 5 minutos</li>
|
||||||
|
<li>Fallback gracioso com rastreamento de motivo do status</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔍 Observabilidade do Proxy (Avançado)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Monitor de Tráfego</strong> — Log ao vivo de requisições/respostas com latência e informações do modelo</li>
|
||||||
|
<li><strong>Replay de Requisições</strong> — Reproduza qualquer requisição registrada para debugging</li>
|
||||||
|
<li><strong>Capacidades dos Modelos</strong> — Inspecione suporte a visão, thinking, streaming e modo JSON</li>
|
||||||
|
<li><strong>Ferramentas para Devs</strong> — Geração de código cURL & Python, cópia com um clique</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔐 Segurança e Criptografia</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Criptografia AES-256-GCM para todos os dados sensíveis</li>
|
||||||
|
<li>Integração com gerenciador de credenciais nativo (Keytar + SafeStorage)</li>
|
||||||
|
<li>Migração automática de dados legados em texto plano</li>
|
||||||
|
<li>Armazenamento criptografado de tokens & cotas por conta</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>⚙️ Configurações e Personalização</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Suporte a temas Escuro / Claro / Sistema</li>
|
||||||
|
<li>Multi-idioma: English & Português (Brasil)</li>
|
||||||
|
<li>Override de proxy URL por conta</li>
|
||||||
|
<li>Toggles de visibilidade de modelos</li>
|
||||||
|
<li>Acesso ao diretório de logs</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🖥️ Experiência Desktop</h3>
|
||||||
|
<ul>
|
||||||
|
<li>App nativo Electron com integração na bandeja do sistema</li>
|
||||||
|
<li>Sidebar retrátil com estado persistente</li>
|
||||||
|
<li>Layouts responsivos em todas as telas</li>
|
||||||
|
<li>Barra de status com indicadores ao vivo de proxy e conexão</li>
|
||||||
|
<li>Error boundaries com fallbacks amigáveis ao usuário</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 Screenshots
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Instalação e Início Rápido
|
||||||
|
|
||||||
|
### 📦 Download
|
||||||
|
|
||||||
|
Você pode baixar os binários pré-compilados mais recentes para Windows, macOS e Linux na nossa [página de Releases](https://github.com/evandrodevbr/GeminiNexus/releases).
|
||||||
|
|
||||||
|
*(Consulte o aviso no topo desta página se você estiver usando o Windows e encontrar o prompt do SmartScreen).*
|
||||||
|
|
||||||
|
### 🔌 Usando com IDEs de IA
|
||||||
|
|
||||||
|
Com o aplicativo rodando e pelo menos uma conta adicionada, configure sua IDE preferida (Cursor, Windsurf, OpenCode, etc.):
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
API Base URL: http://localhost:10100/v1
|
||||||
|
API Key: (copie da página Proxy no app)
|
||||||
|
Model: gemini-3-flash (ou qualquer modelo mapeado)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🛠️ Compilando pelo Código Fonte
|
||||||
|
|
||||||
|
#### Pré-requisitos
|
||||||
|
|
||||||
|
- **Node.js** v20 ou superior
|
||||||
|
- **npm** (este projeto usa `package-lock.json`)
|
||||||
|
|
||||||
|
#### Passos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone o repositório
|
||||||
|
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||||
|
cd GeminiNexus
|
||||||
|
|
||||||
|
# Instale as dependências
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Inicie o modo de desenvolvimento
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Compile para produção (ex: instalador Windows)
|
||||||
|
npm run make
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Stack Tecnológica
|
||||||
|
|
||||||
|
| Categoria | Tecnologias |
|
||||||
|
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Core** | [Electron](https://www.electronjs.org/), [React 19](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
|
||||||
|
| **Build** | [Vite](https://vitejs.dev/), [Electron Forge](https://www.electronforge.io/) |
|
||||||
|
| **Estilização** | [Tailwind CSS v4](https://tailwindcss.com/), [Radix UI](https://www.radix-ui.com/), [Lucide Icons](https://lucide.dev/) |
|
||||||
|
| **Estado** | [TanStack Query](https://tanstack.com/query/latest), [TanStack Router](https://tanstack.com/router/latest) |
|
||||||
|
| **Backend** | [NestJS](https://nestjs.com/) (gateway interno), [ORPC](https://orpc.unnoq.com/) (RPC type-safe) |
|
||||||
|
| **Banco de Dados** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3), [Drizzle ORM](https://orm.drizzle.team/) |
|
||||||
|
| **Gráficos** | [Nivo](https://nivo.rocks/) (Line, Bar, Pie) |
|
||||||
|
| **i18n** | [react-i18next](https://react.i18next.com/) |
|
||||||
|
| **Testes** | [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Desenvolvimento
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Iniciar ambiente de dev (Electron + Vite HMR)
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Executar linting
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Verificar tipos
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# Formatar código
|
||||||
|
npm run format:write
|
||||||
|
|
||||||
|
# Rodar testes
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estrutura do Projeto
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
src/
|
||||||
|
├── actions/ # Ações do app e orquestração de fluxos
|
||||||
|
├── components/ # Componentes React
|
||||||
|
│ ├── proxy/ # Componentes da página Proxy (abas avançadas)
|
||||||
|
│ ├── usage/ # Componentes de analytics de uso (StatCard, ChartCard)
|
||||||
|
│ └── ui/ # Primitivos base de UI (baseados no Radix)
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── ipc/ # Electron IPC + handlers de banco de dados
|
||||||
|
├── layouts/ # Componentes de layout (MainLayout + sidebar)
|
||||||
|
├── localization/ # Recursos de tradução i18n
|
||||||
|
├── routes/ # Páginas TanStack Router (index, usage, proxy, settings)
|
||||||
|
├── server/ # Backend NestJS (serviço de gateway proxy)
|
||||||
|
├── services/ # Camada de serviços
|
||||||
|
├── types/ # Definições de tipos TypeScript + schemas Zod
|
||||||
|
└── utils/ # Funções utilitárias
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: Aviso do Windows SmartScreen ("O Windows protegeu o seu computador") durante a instalação?</b></summary>
|
||||||
|
|
||||||
|
Sim, este é um aviso comum para novos aplicativos não assinados. Clique em **"Mais informações"** e depois em **"Executar mesmo assim"**. Veja o aviso no topo do README para mais detalhes.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: O app não inicia ao compilar pelo código fonte?</b></summary>
|
||||||
|
|
||||||
|
1. Certifique-se de que todas as dependências estão instaladas: `npm install`.
|
||||||
|
2. Verifique se a versão do Node.js é >= 20.
|
||||||
|
3. Tente deletar `node_modules` e reinstalar.
|
||||||
|
4. No Windows, certifique-se de que o WiX Toolset está disponível para o comando `npm run make`.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: O login da conta falhou?</b></summary>
|
||||||
|
|
||||||
|
1. Verifique se a conexão com a internet está funcionando.
|
||||||
|
2. Tente limpar os dados do app e fazer login novamente.
|
||||||
|
3. Verifique se a conta está restrita pelo Google/Claude.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: A IDE não consegue conectar ao proxy?</b></summary>
|
||||||
|
|
||||||
|
1. Certifique-se de que o proxy está rodando (indicador verde na barra de status).
|
||||||
|
2. Verifique se a porta corresponde à configuração da sua IDE (padrão: `10100`).
|
||||||
|
3. Copie a API key da página Proxy e cole nas configurações da sua IDE.
|
||||||
|
4. Verifique se pelo menos uma conta está ativa na página de Contas.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>P: A contagem de tokens mostra zero?</b></summary>
|
||||||
|
|
||||||
|
1. Este era um bug conhecido em versões anteriores — por favor, atualize para a versão mais recente.
|
||||||
|
2. O rastreamento de tokens agora lida corretamente com metadados de streaming e contagens estimadas.
|
||||||
|
3. Os dados de uso são atualizados a cada 15 segundos na página de Analytics de Uso.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contribuindo
|
||||||
|
|
||||||
|
Contribuições são bem-vindas! Leia o `CONTRIBUTING.md` para mais detalhes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Licença
|
||||||
|
|
||||||
|
Este projeto utiliza uma estrutura de licença dual:
|
||||||
|
|
||||||
|
- **Código original** do [AntigravityManager](https://github.com/Draculabo/AntigravityManager) por [Draculabo](https://github.com/Draculabo): Licenciado sob [CC BY-NC-SA 4.0](LICENSE).
|
||||||
|
- **Todo o código novo, funcionalidades e arquitetura** por [evandrodevbr](https://github.com/evandrodevbr): Licenciado sob a [Licença MIT](LICENSE-MIT).
|
||||||
|
|
||||||
|
> 🔄 **Aviso de Migração:** Este projeto está sendo ativamente migrado para uma licença totalmente open-source (MIT). À medida que o código original restante é progressivamente reescrito, todo o projeto será transicionado para MIT. Estamos comprometidos em tornar o Gemini Nexus 100% open-source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Créditos
|
||||||
|
|
||||||
|
Este projeto foi originalmente um fork de [AntigravityManager](https://github.com/Draculabo/AntigravityManager) criado por [Draculabo](https://github.com/Draculabo). O conceito inicial de proxy e a estrutura base do Electron foram baseados em seu trabalho.
|
||||||
|
|
||||||
|
Desde o fork, **a grande maioria do código base foi reescrita, redesenhada e expandida** por [evandrodevbr](https://github.com/evandrodevbr), incluindo mas não se limitando a:
|
||||||
|
|
||||||
|
- Redesign completo de UI/UX (navegação, contas, analytics de uso, gerenciamento de proxy)
|
||||||
|
- Dashboard de Analytics de Uso com rastreamento de custos, médias de tokens e integração com OpenRouter
|
||||||
|
- Monitor de tráfego com inspeção de tokens/custos por requisição
|
||||||
|
- Isolamento de ambiente (dev/prod), pipeline CI/CD e infraestrutura de testes
|
||||||
|
- Configuração Rápida para IDEs (OpenCode, Cursor, VS Code, Claude Code)
|
||||||
|
- Configuração personalizada de preços de modelos
|
||||||
|
- Toda a documentação e internacionalização (EN, PT-BR, ES, ZH-CN)
|
||||||
@@ -1,79 +1,70 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/assets/logo.png" alt="Antigravity Manager" width="128" height="128" />
|
<img src="docs/assets/logo.png" alt="Gemini Nexus" width="128" height="128" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">Antigravity Manager</h1>
|
<h1 align="center">Gemini Nexus</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>🚀 专业的 Google Gemini & Claude AI 多账号管理器</strong>
|
<strong>🚀 专业的多账户 AI 网关,支持 Google Gemini 和 Claude</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a> | 简体中文
|
<a href="README.md">English</a> | <a href="README.pt-BR.md">Português</a> | 中文 | <a href="README.es.md">Español</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml">
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
|
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="版本" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml">
|
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||||
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
|
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="下载量" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/releases">
|
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/github/v/release/Draculabo/AntigravityManager?style=flat-square" alt="Release" />
|
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="许可证" />
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/releases">
|
|
||||||
<img src="https://img.shields.io/github/downloads/Draculabo/AntigravityManager/total?style=flat-square&color=blue" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/blob/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/Draculabo/AntigravityManager?style=flat-square" alt="License" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
|
|
||||||
<img src="https://img.shields.io/github/stars/Draculabo/AntigravityManager?style=flat-square" alt="Stars" />
|
|
||||||
</a>
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/平台-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="平台" />
|
||||||
|
<img src="https://img.shields.io/badge/electron-latest-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
> [!IMPORTANT]
|
||||||
<img src="https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white" alt="Electron" />
|
> **Windows 用户:** 如果在安装过程中看到 **“Windows 已保护你的电脑”** (SmartScreen) 警告,请点击 **“更多信息”** (More info),然后点击 **“仍要运行”** (Run anyway)。这是未签名的新应用预期的安全提示,属于正常现象。
|
||||||
<img src="https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" alt="React" />
|
|
||||||
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
||||||
<img src="https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="TailwindCSS" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 目录
|
## 📖 目录
|
||||||
|
|
||||||
- [为什么选择 Antigravity Manager?](#-为什么选择-antigravity-manager)
|
- [✨ 为什么选择 Gemini Nexus?](#-为什么选择-gemini-nexus)
|
||||||
- [功能特性](#-功能特性)
|
- [🎯 功能特性](#-功能特性)
|
||||||
- [截图预览](#-截图预览)
|
- [📸 截图](#-截图)
|
||||||
- [快速开始](#-快速开始)
|
- [⚡ 安装与快速开始](#-安装与快速开始)
|
||||||
- [技术栈](#️-技术栈)
|
- [🛠️ 技术栈](#️-技术栈)
|
||||||
- [开发指南](#-开发指南)
|
- [💻 开发](#-开发)
|
||||||
- [调试文档](#-调试文档)
|
- [❓ 常见问题](#-常见问题)
|
||||||
- [常见问题](#-常见问题)
|
- [🤝 参与贡献](#-参与贡献)
|
||||||
- [贡献指南](#-贡献指南)
|
- [📄 许可证](#-许可证)
|
||||||
- [许可证](#-许可证)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ 为什么选择 Antigravity Manager?
|
## ✨ 为什么选择 Gemini Nexus?
|
||||||
|
|
||||||
在使用 Antigravity IDE 时,你是否遇到过这些问题?
|
在使用 AI 驱动的 IDE 和编程工具时,你是否遇到过这些问题?
|
||||||
|
|
||||||
- 😫 单个账号额度很快用完,需要频繁手动切换
|
- 😫 **配额限制:** 单个账户配额很快用完,需要频繁手动切换。
|
||||||
- 🔄 管理多个 Google/Claude 账号非常麻烦
|
- 🔄 **账户管理:** 管理多个 Google/Claude 账户非常繁琐。
|
||||||
- 📊 不知道当前账号还剩多少额度
|
- 📊 **盲目使用:** 不知道已经消耗了多少 Token 或还剩多少配额。
|
||||||
- ⏰ 担心错过额度重置时间
|
- 🔌 **集成问题:** 需要一个可靠的本地 API 代理,并且必须原生支持 OpenAI/Anthropic 协议。
|
||||||
- 🔌 需要可靠的本地 API 代理用于开发工具
|
- 🔍 **缺乏透明度:** 无法查看代理在后台实际发送或接收的数据。
|
||||||
|
|
||||||
**Antigravity Manager** 就是为解决这些问题而生的!它是一个专业的 Electron 桌面应用,帮助你:
|
**Gemini Nexus** 解决所有这些问题。它是一个专业的 Electron 桌面应用,作为你的开发工具和 Google Gemini / Claude AI 之间的智能网关。
|
||||||
|
|
||||||
- ✅ **无限账号池** - 添加任意数量的 Google Gemini / Claude 账号
|
### 核心价值主张
|
||||||
- ✅ **智能自动切换** - 额度不足或被限速时自动切换到下一个可用账号
|
|
||||||
- ✅ **实时监控** - 可视化显示所有账号的额度使用情况
|
- ✅ **无限账户池** — 添加任意数量的 Google Gemini 和 Claude 账户。
|
||||||
- ✅ **本地 API 代理** - 内置兼容 OpenAI/Anthropic 协议的代理服务器
|
- ✅ **智能自动切换** — 当配额不足或被限速时自动轮换到下一个可用账户。
|
||||||
- ✅ **安全加密** - AES-256-GCM 加密存储敏感信息
|
- ✅ **实时使用分析** — SaaS 级仪表板,配备面积图、趋势指标和模型分布。
|
||||||
|
- ✅ **完整的代理可观测性** — 实时流量监控、请求重放和模型能力检查器。
|
||||||
|
- ✅ **兼容 OpenAI 和 Anthropic** — 即插即用的代理,支持 Cursor、Windsurf、OpenCode 以及任何兼容 OpenAI 的工具。
|
||||||
|
- ✅ **默认安全** — AES-256-GCM 加密,配合操作系统原生凭证管理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,186 +73,160 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>☁️ 云账号池管理</h3>
|
<h3>☁️ 云端账户池</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>通过 OAuth 添加无限 Google Gemini / Claude 账号</li>
|
<li>通过 OAuth 添加无限量的 Google Gemini / Claude 账户</li>
|
||||||
<li>显示头像、邮箱、状态和最后使用时间</li>
|
<li>显示头像、邮箱、状态和最后使用时间</li>
|
||||||
<li>实时状态监控(活跃、限速、过期)</li>
|
<li>实时状态监控(活跃、限速、过期)</li>
|
||||||
|
<li>每个账户可独立配置代理 URL</li>
|
||||||
|
<li>设备身份档案管理及历史记录</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>📊 实时额度监控</h3>
|
<h3>📊 使用分析仪表板</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>多模型支持:gemini-pro、claude-3-5-sonnet 等</li>
|
<li>实时 Token 消耗,每 15 秒自动刷新</li>
|
||||||
<li>可视化进度条配色指示器</li>
|
<li>面积图展示每日/每小时的 Prompt 和 Completion Token</li>
|
||||||
<li>自动刷新与手动刷新</li>
|
<li>趋势指标(与上一周期相比的百分比变化)</li>
|
||||||
|
<li>统计卡片内嵌迷你折线图(Sparklines)</li>
|
||||||
|
<li>水平柱状图展示模型分布排名</li>
|
||||||
|
<li>Prompt/Completion 比例可视化</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔌 本地 API 代理(网关)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>兼容 OpenAI <code>/v1/chat/completions</code></li>
|
||||||
|
<li>兼容 Anthropic <code>/v1/messages</code></li>
|
||||||
|
<li>完整的 SSE 流式传输支持(已在 Cursor, Windsurf, OpenCode 测试)</li>
|
||||||
|
<li>模型映射(如 <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||||
|
<li>可配置端口、超时时间和 API 密钥</li>
|
||||||
|
<li>模型可见性控制(隐藏/显示特定模型)</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>🔄 智能自动切换</h3>
|
<h3>🔄 智能自动切换</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>无限池模式,智能选择备用账号</li>
|
<li>无限池模式,智能备份选择</li>
|
||||||
<li>额度低于 5% 或被限速时自动切换</li>
|
<li>配额低于 5% 或被限速时自动切换</li>
|
||||||
<li>后台每 5 分钟自动监控</li>
|
<li>每 5 分钟后台监控</li>
|
||||||
|
<li>优雅降级,带状态原因追踪</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h3>🔍 代理可观测性(高级)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>流量监控</strong> — 实时请求/响应日志,含延迟和模型信息</li>
|
||||||
|
<li><strong>请求重放</strong> — 重放任意请求进行调试</li>
|
||||||
|
<li><strong>模型能力</strong> — 检查每个模型对视觉、思考、流式、JSON 模式的支持</li>
|
||||||
|
<li><strong>开发者工具</strong> — cURL 和 Python 代码生成,一键复制</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>🔐 安全优先</h3>
|
<h3>🔐 安全与加密</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>AES-256-GCM 加密敏感数据</li>
|
<li>所有敏感数据采用 AES-256-GCM 加密</li>
|
||||||
<li>集成操作系统原生凭证管理器</li>
|
<li>集成操作系统原生凭证管理器 (Keytar + SafeStorage)</li>
|
||||||
<li>自动迁移旧版明文数据</li>
|
<li>自动迁移旧版明文数据</li>
|
||||||
|
<li>每个账户的 Token 和配额均加密存储</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>💾 账号备份</h3>
|
<h3>⚙️ 设置与自定义</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>捕获账号状态快照</li>
|
<li>深色 / 浅色 / 跟随系统主题</li>
|
||||||
<li>在已保存的账号间快速切换</li>
|
<li>多语言:English & Português (Brasil)</li>
|
||||||
<li>查看、整理和删除快照</li>
|
<li>每个账户可独立覆盖代理 URL</li>
|
||||||
|
<li>模型可见性开关</li>
|
||||||
|
<li>日志目录访问</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
<h3>⚙️ 进程控制</h3>
|
<h3>🖥️ 桌面体验</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>自动检测 Antigravity 是否运行</li>
|
<li>原生 Electron 应用,系统托盘集成</li>
|
||||||
<li>通过 URI 协议或可执行文件启动</li>
|
<li>可折叠侧边栏,状态持久化</li>
|
||||||
<li>优雅关闭或强制终止</li>
|
<li>响应式布局适配所有视图</li>
|
||||||
</ul>
|
<li>状态栏实时显示代理和连接指标</li>
|
||||||
</td>
|
<li>错误边界处理以及友好的用户提示</li>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🔌 本地 API 代理</h3>
|
|
||||||
<ul>
|
|
||||||
<li>兼容 OpenAI & Anthropic API 协议</li>
|
|
||||||
<li>可配置端口和请求超时</li>
|
|
||||||
<li>模型映射(如:Claude → Gemini)</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
<h3>🛠️ 开发者工具</h3>
|
|
||||||
<ul>
|
|
||||||
<li>内置 cURL & Python 代码生成</li>
|
|
||||||
<li>可视化服务状态监控</li>
|
|
||||||
<li>一键重新生成 API Key</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
### 更多功能
|
|
||||||
|
|
||||||
- **🖥️ 系统托盘** - 后台运行,托盘图标和右键菜单
|
|
||||||
- **🔗 IDE 同步** - 自动扫描导入 IDE 的 `state.vscdb` 账号
|
|
||||||
- **📦 批量操作** - 批量刷新和删除多个账号
|
|
||||||
- **📤 JSON 导出/导入** - 导出和导入账号池,支持架构验证和去重
|
|
||||||
- **🔔 桌面通知** - 模型配额低于设定阈值时自动提醒
|
|
||||||
- **🌐 单账号代理** - 为每个账号单独配置 HTTP/SOCKS5 代理
|
|
||||||
- **📊 智能排序** - 按最近使用、总体配额或特定模型组排序
|
|
||||||
- **📋 紧凑布局** - 水平密集视图,最大化可见账号数量
|
|
||||||
- **🌏 国际化** - 多语言支持(English / 中文 / Русский)
|
|
||||||
- **🎨 现代 UI** - 基于 React、TailwindCSS 和 Shadcn UI 构建
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📸 截图预览
|
## 📸 截图
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/assets/screenshot-main.png" alt="主界面" width="80%" />
|
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/assets/screenshot-proxy.png" alt="代理界面" width="48%" />
|
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||||
<img src="docs/assets/screenshot-setting.png" alt="设置界面" width="48%" />
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 快速开始
|
## ⚡ 安装与快速开始
|
||||||
|
|
||||||
### 下载安装
|
### 📦 下载
|
||||||
|
|
||||||
从 [Releases](https://github.com/Draculabo/AntigravityManager/releases) 页面下载适合你平台的最新版本。
|
您可以从我们的 [Releases 页面](https://github.com/evandrodevbr/GeminiNexus/releases) 下载适用于 Windows、macOS 和 Linux 的最新预编译版本。
|
||||||
|
|
||||||
| 平台 | 下载链接 |
|
*(如果您使用的是 Windows 并在安装时遇到了 SmartScreen 提示,请参阅页面顶部的警告)。*
|
||||||
| ------------------- | ------------------------------------------------------------------------------ |
|
|
||||||
| Windows (x64/ARM64) | [.exe 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| macOS | [.dmg 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
|
||||||
| NixOS / Nix | `nix run github:Draculabo/AntigravityManager` |
|
|
||||||
|
|
||||||
### ❄️ Nix 集成
|
### 🔌 配合 AI IDE 使用
|
||||||
|
|
||||||
你可以通过项目提供的 flake 将 Antigravity Manager 集成到你的 Nix 配置中。
|
在代理运行并至少添加一个账户后,配置您首选的 IDE(Cursor、Windsurf、OpenCode 等):
|
||||||
|
|
||||||
#### 🛠️ NixOS / Home Manager
|
```plaintext
|
||||||
|
API Base URL: http://localhost:10100/v1
|
||||||
先在你的 `flake.nix` 的 `inputs` 中加入:
|
API Key: (从应用的 Proxy 页面复制)
|
||||||
|
Model: gemini-3-flash (或您选择的任何映射模型)
|
||||||
```nix
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
||||||
antigravity-manager.url = "github:Draculabo/AntigravityManager";
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
然后把 `antigravity-manager.overlays.default` 添加到 `nixpkgs.overlays`。这样会基于你自己的 `nixpkgs` 实例构建,并继承你的全局配置。
|
### 🛠️ 从源码构建
|
||||||
|
|
||||||
```nix
|
|
||||||
# 添加到你的 NixOS/Home Manager 配置中
|
|
||||||
{
|
|
||||||
nixpkgs.overlays = [ inputs.antigravity-manager.overlays.default ];
|
|
||||||
|
|
||||||
# Antigravity Manager 使用非自由许可证 (cc-by-nc-sa-40)
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.antigravity-manager
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 💻 开发 Shell
|
|
||||||
|
|
||||||
如果你已经使用上面的 overlay,也可以直接在 `devShell` 里加入:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = [
|
|
||||||
pkgs.antigravity-manager
|
|
||||||
];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 由于该包是非自由软件,你必须在 `nixpkgs` 配置中开启 `allowUnfree = true;`,否则会评估失败。
|
|
||||||
|
|
||||||
### 从源码构建
|
|
||||||
|
|
||||||
#### 前置要求
|
#### 前置要求
|
||||||
|
|
||||||
- Node.js v18 或更高版本
|
- **Node.js** v20 或更高版本
|
||||||
- npm 或 yarn
|
- **npm** (本项目使用 `package-lock.json`)
|
||||||
|
|
||||||
#### 步骤
|
#### 步骤
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# 克隆仓库
|
||||||
git clone https://github.com/Draculabo/AntigravityManager.git
|
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||||
cd AntigravityManager
|
cd GeminiNexus
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 启动开发模式
|
# 启动开发环境
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
# 构建生产版本
|
# 构建生产版本(例如 Windows 安装程序)
|
||||||
npm run make
|
npm run make
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -269,143 +234,111 @@ npm run make
|
|||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
| 类别 | 技术 |
|
| 分类 | 技术 |
|
||||||
| ------------ | ------------------------------------------------------------------------------------------------------------------- |
|
| ---------- | --------------------------------------- |
|
||||||
| **核心** | [Electron](https://www.electronjs.org/)、[React](https://react.dev/)、[TypeScript](https://www.typescriptlang.org/) |
|
| **核心** | Electron, React 19, TypeScript |
|
||||||
| **构建工具** | [Vite](https://vitejs.dev/) |
|
| **构建** | Vite, Electron Forge |
|
||||||
| **样式** | [TailwindCSS](https://tailwindcss.com/)、[Shadcn UI](https://ui.shadcn.com/) |
|
| **样式** | Tailwind CSS v4, Radix UI, Lucide Icons |
|
||||||
| **状态管理** | [TanStack Query](https://tanstack.com/query/latest)、[TanStack Router](https://tanstack.com/router/latest) |
|
| **状态** | TanStack Query, TanStack Router |
|
||||||
| **数据库** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3) |
|
| **后端** | NestJS (内部网关), ORPC (类型安全 RPC) |
|
||||||
| **测试** | [Vitest](https://vitest.dev/)、[Playwright](https://playwright.dev/) |
|
| **数据库** | Better-SQLite3, Drizzle ORM |
|
||||||
|
| **图表** | Nivo (Line, Bar, Pie) |
|
||||||
|
| **测试** | Vitest, Playwright |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💻 开发指南
|
## 💻 开发
|
||||||
|
|
||||||
### 可用脚本
|
```bash
|
||||||
|
# 启动开发环境 (Electron + Vite HMR)
|
||||||
|
npm start
|
||||||
|
|
||||||
| 命令 | 描述 |
|
# 运行代码规范检查
|
||||||
| ---------------------- | ------------------------ |
|
npm run lint
|
||||||
| `npm start` | 启动开发模式 |
|
|
||||||
| `npm run lint` | 运行 ESLint 检查 |
|
|
||||||
| `npm run format:write` | 使用 Prettier 格式化代码 |
|
|
||||||
| `npm run test:unit` | 运行单元测试 |
|
|
||||||
| `npm run test:e2e` | 运行 E2E 测试 |
|
|
||||||
| `npm run test:all` | 运行所有测试 |
|
|
||||||
| `npm run type-check` | TypeScript 类型检查 |
|
|
||||||
| `npm run make` | 构建生产包 |
|
|
||||||
|
|
||||||
### 项目结构
|
# 类型检查
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# 格式化代码
|
||||||
|
npm run format:write
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm test
|
||||||
```
|
```
|
||||||
AntigravityManager/
|
|
||||||
├── src/
|
|
||||||
│ ├── main.ts # Electron 主进程
|
|
||||||
│ ├── preload.ts # 预加载脚本
|
|
||||||
│ ├── renderer/ # React 渲染进程
|
|
||||||
│ ├── ipc/ # IPC 通信处理
|
|
||||||
│ └── server/ # 内置服务器
|
|
||||||
├── docs/ # 文档和资源
|
|
||||||
└── .github/ # GitHub 配置
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 调试文档
|
|
||||||
|
|
||||||
如果你希望用 Proxyman 抓取和排查 Antigravity 或 Antigravity Manager 的流量,请先阅读:
|
|
||||||
|
|
||||||
- [Proxyman Installation and Basic Setup on Windows](docs/proxyman-install.md)
|
|
||||||
- [Proxyman Debugging Guide for Antigravity and Antigravity Manager](docs/proxyman-debugging.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ❓ 常见问题
|
## ❓ 常见问题
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Q: 程序无法启动怎么办?</b></summary>
|
<summary><b>问:安装时出现 Windows SmartScreen 警告("Windows 已保护你的电脑")?</b></summary>
|
||||||
|
|
||||||
请检查:
|
|
||||||
|
|
||||||
1. 确保已安装所有依赖:`npm install`
|
|
||||||
2. 检查 Node.js 版本是否 >= 18
|
|
||||||
3. 尝试删除 `node_modules` 后重新安装
|
|
||||||
|
|
||||||
|
是的,对于未签名的新应用,这是一个常见的警告。请点击 **"更多信息"** (More info),然后点击 **"仍要运行"** (Run anyway)。详情请参阅 README 顶部的提示。
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Q: 账号登录失败?</b></summary>
|
<summary><b>问:从源码构建时应用无法启动?</b></summary>
|
||||||
|
|
||||||
1. 确保网络连接正常
|
|
||||||
2. 尝试清除应用数据后重新登录
|
|
||||||
3. 检查账号是否被 Google/Claude 限制
|
|
||||||
|
|
||||||
|
1. 确保所有依赖均已安装:`npm install`
|
||||||
|
2. 检查 Node.js 版本是否 >= 20。
|
||||||
|
3. 尝试删除 `node_modules` 并重新安装。
|
||||||
|
4. 在 Windows 上,请确保已安装 WiX Toolset 以便执行 `npm run make` 命令。
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Q: macOS 上提示 Keychain/凭据管理不可用,OAuth 无法保存?</b></summary>
|
<summary><b>问:账户登录失败?</b></summary>
|
||||||
|
|
||||||
这是 macOS 安全机制导致的常见问题,通常出现在未签名的应用或从 Downloads 直接运行的场景。
|
|
||||||
这是**临时方案**,仅适合个人设备:
|
|
||||||
|
|
||||||
1. 将应用移动到 `/Applications`
|
|
||||||
2. 打开终端执行以下命令(每次更新后都需要重新执行)
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
sudo xattr -dr com.apple.quarantine "/Applications/Antigravity Manager 2.app"
|
|
||||||
codesign --force --deep --sign - "/Applications/Antigravity Manager 2.app"
|
|
||||||
```
|
|
||||||
|
|
||||||
重新打开应用后,Keychain 应会提示授权(可选“始终允许”)。
|
|
||||||
|
|
||||||
|
1. 确保您的网络连接正常。
|
||||||
|
2. 尝试清除应用数据并重新登录。
|
||||||
|
3. 检查该账户是否已被 Google/Claude 限制。
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Q: 如何反馈问题或建议?</b></summary>
|
<summary><b>问:IDE 无法连接到代理?</b></summary>
|
||||||
|
|
||||||
请通过 [GitHub Issues](https://github.com/Draculabo/AntigravityManager/issues) 提交问题或建议。
|
1. 确保代理正在运行(查看状态栏的绿色指示器)。
|
||||||
|
2. 确认端口与您 IDE 中的配置一致(默认为 `10100`)。
|
||||||
|
3. 直接从 Proxy 页面复制 API 密钥并粘贴到 IDE 设置中。
|
||||||
|
4. 确保在 Accounts 页面中至少有一个活跃的账户。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>问:Token 计数显示为零?</b></summary>
|
||||||
|
|
||||||
|
1. 这是早期版本中的一个已知漏洞 — 请更新至最新版本。
|
||||||
|
2. 目前 Token 追踪功能已能正确处理流式元数据及预估计数。
|
||||||
|
3. 使用数据会在 Usage Analytics 页面每 15 秒自动刷新。
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌟 Star History
|
## 🤝 参与贡献
|
||||||
|
|
||||||
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
|
欢迎贡献代码!详情请参阅 `CONTRIBUTING.md`。
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
|
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 贡献指南
|
|
||||||
|
|
||||||
欢迎贡献代码!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详情。
|
|
||||||
|
|
||||||
我们遵循 [Contributor Covenant](CODE_OF_CONDUCT.md) 行为准则。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 许可证
|
## 📄 许可证
|
||||||
|
|
||||||
[CC BY-NC-SA 4.0](LICENSE)
|
本项目采用双重许可结构:
|
||||||
|
|
||||||
|
- **原始代码**来自 [Draculabo](https://github.com/Draculabo) 的 [AntigravityManager](https://github.com/Draculabo/AntigravityManager):采用 [CC BY-NC-SA 4.0](LICENSE) 许可。
|
||||||
|
- **所有新代码、功能和架构**由 [evandrodevbr](https://github.com/evandrodevbr) 开发:采用 [MIT 许可证](LICENSE-MIT)。
|
||||||
|
|
||||||
|
> 🔄 **迁移通知:** 本项目正在积极迁移到完全开源许可证(MIT)。随着剩余原始代码的逐步重写,整个项目将过渡到 MIT。我们致力于将 Gemini Nexus 打造为 100% 开源项目。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ 免责声明
|
## 🙏 致谢
|
||||||
|
|
||||||
> [!WARNING]
|
本项目最初是从 [Draculabo](https://github.com/Draculabo) 创建的 [AntigravityManager](https://github.com/Draculabo/AntigravityManager) 分支而来的。最初的代理概念和早期的 Electron 框架基于他的工作。
|
||||||
> **仅供学习研究使用**
|
|
||||||
>
|
|
||||||
> 本项目仅供教育和研究目的,按"原样"提供,不提供任何保证。**严禁商业使用。**
|
|
||||||
>
|
|
||||||
> 使用本软件即表示您同意不会将其用于任何商业目的,并自行负责确保您的使用符合所有适用法律法规。作者和贡献者对因使用本软件而产生的任何滥用或损害不承担责任。
|
|
||||||
|
|
||||||
---
|
自从分支以来,[evandrodevbr](https://github.com/evandrodevbr) **重写、重新设计并扩展了绝大部分代码库**,包括但不限于:
|
||||||
|
|
||||||
<p align="center">
|
- 完整的 UI/UX 重新设计(导航、账户、使用分析、代理管理)
|
||||||
如果这个项目对你有帮助,请给一个 ⭐ Star 支持!
|
- 使用分析仪表板,包含成本跟踪、Token 平均值以及 OpenRouter 集成
|
||||||
</p>
|
- 流量监视器,提供按请求查看的 Token/成本检查
|
||||||
|
- 环境隔离(dev/prod)、CI/CD 流水线和测试基础设施
|
||||||
|
- IDE 快速设置(OpenCode、Cursor、VS Code、Claude Code)
|
||||||
|
- 自定义模型定价配置
|
||||||
|
- 所有文档和国际化(EN, PT-BR, ES, ZH-CN)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 🔒 Security Features
|
## 🔒 Security Features
|
||||||
|
|
||||||
Antigravity Manager takes security seriously. Here are the security measures we've implemented:
|
Gemini Nexus takes security seriously. Here are the security measures we've implemented:
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ We only provide security updates for the latest version. Please ensure you're ru
|
|||||||
|
|
||||||
## 🚨 Reporting a Vulnerability
|
## 🚨 Reporting a Vulnerability
|
||||||
|
|
||||||
We take all security vulnerabilities seriously. Thank you for helping to improve the security of Antigravity Manager.
|
We take all security vulnerabilities seriously. Thank you for helping to improve the security of Gemini Nexus.
|
||||||
|
|
||||||
### How to Report
|
### How to Report
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ Please include the following information in your report:
|
|||||||
|
|
||||||
To ensure the security of your data:
|
To ensure the security of your data:
|
||||||
|
|
||||||
1. **Keep Updated**: Always use the latest version of Antigravity Manager
|
1. **Keep Updated**: Always use the latest version of Gemini Nexus
|
||||||
2. **System Security**: Keep your operating system updated
|
2. **System Security**: Keep your operating system updated
|
||||||
3. **Secure Environment**: Use the application on trusted devices only
|
3. **Secure Environment**: Use the application on trusted devices only
|
||||||
4. **Backup**: Regularly backup your account data using the built-in backup feature
|
4. **Backup**: Regularly backup your account data using the built-in backup feature
|
||||||
@@ -90,4 +90,4 @@ When we receive a security bug report, we will:
|
|||||||
3. Prepare fixes for all supported versions
|
3. Prepare fixes for all supported versions
|
||||||
4. Release new versions as soon as possible
|
4. Release new versions as soon as possible
|
||||||
|
|
||||||
Thank you for helping keep Antigravity Manager and its users safe!
|
Thank you for helping keep Gemini Nexus and its users safe!
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Antigravity Manager CLI
|
# Gemini Nexus CLI
|
||||||
|
|
||||||
A powerful command-line interface for managing multiple Google Cloud AI accounts in Antigravity IDE. Supports account switching, quota monitoring, token validation, and more.
|
A powerful command-line interface for managing multiple Google Cloud AI accounts in Gemini Nexus IDE. Supports account switching, quota monitoring, token validation, and more.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -147,9 +147,9 @@ agm doctor
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
The CLI interacts directly with the Antigravity Manager's encrypted database:
|
The CLI interacts directly with the Gemini Nexus's encrypted database:
|
||||||
|
|
||||||
1. **Database Location**: Auto-discovers the database in `%APPDATA%\AntigravityManager` or IDE installation directory
|
1. **Database Location**: Auto-discovers the database in `%APPDATA%\GeminiNexus` or IDE installation directory
|
||||||
2. **Encryption**: Uses Windows DPAPI + AES-256-GCM to decrypt account tokens (same as the IDE)
|
2. **Encryption**: Uses Windows DPAPI + AES-256-GCM to decrypt account tokens (same as the IDE)
|
||||||
3. **Quota API**: Fetches live quota data from Google Cloud AI REST API
|
3. **Quota API**: Fetches live quota data from Google Cloud AI REST API
|
||||||
4. **Process Control**: Can start/stop the IDE executable automatically
|
4. **Process Control**: Can start/stop the IDE executable automatically
|
||||||
@@ -157,7 +157,7 @@ The CLI interacts directly with the Antigravity Manager's encrypted database:
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**"Database not found" error:**
|
**"Database not found" error:**
|
||||||
- Ensure Antigravity Manager has been run at least once
|
- Ensure Gemini Nexus has been run at least once
|
||||||
- Check if accounts exist in the GUI first
|
- Check if accounts exist in the GUI first
|
||||||
|
|
||||||
**"Failed to decrypt" error:**
|
**"Failed to decrypt" error:**
|
||||||
@@ -175,7 +175,7 @@ The CLI interacts directly with the Antigravity Manager's encrypted database:
|
|||||||
|
|
||||||
- Windows 10/11
|
- Windows 10/11
|
||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- Antigravity Manager (for database access)
|
- Gemini Nexus (for database access)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
**✅ Documento Completo: Integração com Custom Endpoint do OpenCode (Provedores Custom / OpenAI-Compatible)**
|
||||||
|
|
||||||
|
OpenCode (https://opencode.ai) é um agente de IA para terminal que usa o **Vercel AI SDK** para se comunicar com provedores de LLM. Ele suporta nativamente provedores customizados via o pacote `@ai-sdk/openai-compatible`, que segue o padrão **OpenAI Chat Completions API** (`/v1/chat/completions`).
|
||||||
|
|
||||||
|
Isso significa que **sua API precisa implementar exatamente o formato OpenAI-compatible** para que o OpenCode consiga conversar com ela diretamente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Como Configurar um Provedor Custom no OpenCode
|
||||||
|
|
||||||
|
Crie ou edite o arquivo de configuração (recomendado: `~/.config/opencode/opencode.json` ou `opencode.json` na raiz do projeto):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"minha-ia-custom": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "Minha IA Custom (OpenCode)",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "https://sua-api.com/v1",
|
||||||
|
"apiKey": "sk-sua-chave-aqui-ou-{env:MINHA_API_KEY}",
|
||||||
|
"headers": {
|
||||||
|
"X-Custom-Header": "valor-opcional"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"meu-modelo-principal": {
|
||||||
|
"name": "Meu Modelo Principal",
|
||||||
|
"limit": {
|
||||||
|
"context": 128000,
|
||||||
|
"output": 8192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meu-modelo-rapido": {
|
||||||
|
"name": "Modelo Rápido",
|
||||||
|
"limit": {
|
||||||
|
"context": 32000,
|
||||||
|
"output": 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": "minha-ia-custom/meu-modelo-principal"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Campos importantes:**
|
||||||
|
|
||||||
|
- `npm`: Sempre use `"@ai-sdk/openai-compatible"` para APIs que expõem `/v1/chat/completions`.
|
||||||
|
- `baseURL`: URL base da sua API (deve terminar com `/v1`).
|
||||||
|
- `apiKey`: Pode ser valor direto ou `{env:NOME_VARIAVEL}`.
|
||||||
|
- `models`: Defina os modelos que sua API expõe. O `limit` ajuda o OpenCode a gerenciar contexto.
|
||||||
|
- `model`: Define o modelo padrão no formato `provider_id/model_id`.
|
||||||
|
|
||||||
|
Depois de salvar, reinicie o OpenCode ou use `/models` para selecionar.
|
||||||
|
|
||||||
|
**Comando rápido para adicionar chave:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
opencode /connect
|
||||||
|
# Escolha "Other" → digite o provider_id (ex: minha-ia-custom) → cole a API key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Como o OpenCode Envia Requisições
|
||||||
|
|
||||||
|
O OpenCode (via AI SDK) faz chamadas **POST** para:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {baseURL}/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Headers enviados:**
|
||||||
|
|
||||||
|
- `Authorization: Bearer <sua-api-key>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- Headers customizados que você definir em `options.headers`
|
||||||
|
|
||||||
|
**Payload típico (exemplo real):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "meu-modelo-principal",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "Você é um assistente de código experiente..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Crie uma função em Python que..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stream": true,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"tools": [
|
||||||
|
/* array de tools se o agente estiver usando ferramentas */
|
||||||
|
],
|
||||||
|
"tool_choice": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros comuns que o OpenCode envia:**
|
||||||
|
|
||||||
|
- `messages` (array completo do histórico)
|
||||||
|
- `stream: true` (quase sempre, para UX em tempo real)
|
||||||
|
- `temperature`, `max_tokens`, `top_p`
|
||||||
|
- `tools` + `tool_choice` (quando usa agentes com ferramentas/MCP)
|
||||||
|
- `response_format` (às vezes)
|
||||||
|
- `providerOptions` (opções específicas do provedor – raras no openai-compatible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Como o OpenCode Recebe e Processa Respostas
|
||||||
|
|
||||||
|
#### A. Streaming (recomendado – usado na maioria das interações)
|
||||||
|
|
||||||
|
Sua API deve responder com `Content-Type: text/event-stream` e eventos SSE:
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1712345678,"model":"meu-modelo-principal","choices":[{"index":0,"delta":{"content":"Olá"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"! Como"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
```
|
||||||
|
|
||||||
|
O OpenCode processa cada chunk em tempo real.
|
||||||
|
|
||||||
|
#### B. Resposta não-streaming (completa)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "chatcmpl-123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1712345678,
|
||||||
|
"model": "meu-modelo-principal",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Resposta completa aqui..."
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 150,
|
||||||
|
"completion_tokens": 320,
|
||||||
|
"total_tokens": 470
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Suporte a Tool Calling (essencial para agentes)
|
||||||
|
|
||||||
|
Se sua API suportar `tools`, o OpenCode pode:
|
||||||
|
|
||||||
|
1. Enviar `tools` na requisição.
|
||||||
|
2. Receber `tool_calls` na resposta.
|
||||||
|
3. Executar a ferramenta (via MCP ou built-in).
|
||||||
|
4. Enviar o resultado de volta como mensagem `tool`.
|
||||||
|
|
||||||
|
**Exemplo de resposta com tool call:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "read_file",
|
||||||
|
"arguments": "{\"path\": \"src/app.ts\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Como é Exibido no Chat do OpenCode (TUI)
|
||||||
|
|
||||||
|
- **Streaming**: Texto aparece caractere por caractere com efeito de digitação.
|
||||||
|
- **Markdown**: Suporte completo a código (syntax highlight), listas, tabelas, etc.
|
||||||
|
- **Tool Calls**: Mostra “Executando ferramenta: read_file...” + resultado.
|
||||||
|
- **Multi-turn**: Mantém histórico completo na conversa.
|
||||||
|
- **Erros**: Mostra mensagens de erro claras no terminal.
|
||||||
|
- **Contexto**: Respeita os limites de `context` e `output` que você definiu no config.
|
||||||
|
|
||||||
|
O OpenCode usa o TUI (Terminal User Interface) baseado em React/Ink para renderizar tudo de forma bonita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Requisitos Mínimos da Sua API para Funcionar Perfeitamente
|
||||||
|
|
||||||
|
| Recurso | Obrigatório? | Recomendado? | Observação |
|
||||||
|
| ---------------------- | ------------ | ------------ | ---------------------------------- |
|
||||||
|
| `/v1/chat/completions` | Sim | - | Endpoint principal |
|
||||||
|
| Streaming (SSE) | Sim | - | Essencial para UX |
|
||||||
|
| Tool Calling | Não | Sim | Para agentes completos |
|
||||||
|
| System messages | Sim | - | - |
|
||||||
|
| Multi-turn history | Sim | - | - |
|
||||||
|
| Usage stats | Não | Sim | Mostra tokens gastos |
|
||||||
|
| Error handling | Sim | - | Retorne JSON de erro padrão OpenAI |
|
||||||
|
|
||||||
|
**Endpoint exato esperado:**
|
||||||
|
`POST https://sua-api.com/v1/chat/completions`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Exemplo Completo de Configuração + Teste
|
||||||
|
|
||||||
|
1. Configure o `opencode.json` como acima.
|
||||||
|
2. Rode:
|
||||||
|
```bash
|
||||||
|
opencode
|
||||||
|
```
|
||||||
|
3. Digite algo ou use `/models` para selecionar seu provedor.
|
||||||
|
4. Teste com:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://sua-api.com/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer sk-..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"meu-modelo-principal","messages":[{"role":"user","content":"Olá"}],"stream":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Troubleshooting Comum
|
||||||
|
|
||||||
|
- **“Route not found” ou requests indo para lugar errado** → Verifique se `baseURL` termina com `/v1` e não com `/v1/chat/completions`.
|
||||||
|
- **Custom provider não aparece** → Certifique-se que o ID do provider no `/connect` bate com a chave no `opencode.json`.
|
||||||
|
- **Sem streaming / resposta lenta** → Implemente SSE corretamente.
|
||||||
|
- **Tool calls não funcionam** → Sua API precisa retornar `tool_calls` no formato OpenAI exato.
|
||||||
|
- **Erro de autenticação** → Use `apiKey` ou `headers` corretamente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Referências Oficiais
|
||||||
|
|
||||||
|
- **Docs Principais**: https://opencode.ai/docs/providers/
|
||||||
|
- **Models**: https://opencode.ai/docs/models/
|
||||||
|
- **Config**: https://opencode.ai/docs/config/
|
||||||
|
- **Schema de Config**: https://opencode.ai/config.json
|
||||||
|
- **GitHub**: https://github.com/anomalyco/opencode
|
||||||
|
- **AI SDK OpenAI-Compatible**: https://ai-sdk.dev/providers/openai-compatible-providers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resumo Final para Você (que está construindo a API):**
|
||||||
|
|
||||||
|
Sua API **precisa ser 100% compatível** com o endpoint OpenAI `/v1/chat/completions` (streaming + tool calling recomendado).
|
||||||
|
O OpenCode vai:
|
||||||
|
|
||||||
|
1. Configurar via `opencode.json` (baseURL + apiKey)
|
||||||
|
2. Enviar requisições padrão OpenAI
|
||||||
|
3. Receber streaming ou resposta completa
|
||||||
|
4. Renderizar tudo lindamente no TUI com suporte a ferramentas
|
||||||
|
|
||||||
|
Se você implementar exatamente o padrão OpenAI Chat Completions, vai funcionar perfeitamente com o OpenCode.
|
||||||
|
|
||||||
|
Quer que eu monte um **exemplo de servidor mínimo** (em Node.js/Fastify ou Python/FastAPI) que já funcione com o OpenCode? Posso criar um template completo para você.
|
||||||
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 131 KiB |
@@ -0,0 +1,158 @@
|
|||||||
|
# Brand Briefing — Gemini Nexus
|
||||||
|
|
||||||
|
## 1. Nome e Descrição
|
||||||
|
|
||||||
|
| Campo | Valor |
|
||||||
|
| ------------------- | -------------------------------------------------------------------- |
|
||||||
|
| **Nome** | Gemini Nexus |
|
||||||
|
| **Product Name** | Gemini Nexus |
|
||||||
|
| **Package Name** | gemini-nexus |
|
||||||
|
| **Versão** | 0.11.1 |
|
||||||
|
| **Descrição EN** | Professional multi-account manager for Google Gemini & Claude AI |
|
||||||
|
| **Descrição PT-BR** | Gerenciador multi-contas profissional para Google Gemini & Claude AI |
|
||||||
|
| **Autor** | evandrodevbr / Draculabo |
|
||||||
|
| **Licença** | CC-BY-NC-SA-4.0 (apenas fins educacionais) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Propósito do Aplicativo
|
||||||
|
|
||||||
|
Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona como um **hub central (nexus)** para desenvolvedores que usam IDEs com IA.
|
||||||
|
|
||||||
|
### Funcionalidades Principais
|
||||||
|
|
||||||
|
- **Pool ilimitado de contas**: Adicionar múltiplas contas Google Gemini / Claude via OAuth
|
||||||
|
- **Auto-switching inteligente**: Troca automática de conta quando a cota acaba ou há rate limit
|
||||||
|
- **Monitoramento em tempo real**: Barras de progresso coloridas para visualizar uso de quota
|
||||||
|
- **Proxy API local**: Servidor proxy integrado compatível OpenAI/Anthropic (porta 8045)
|
||||||
|
- **Segurança**: Criptografia AES-256-GCM para dados sensíveis
|
||||||
|
|
||||||
|
### Público-Alvo
|
||||||
|
|
||||||
|
- Desenvolvedores que usam IDEs com integração AI (Cursor, Windsurf, VS Code, Claude Code, etc.)
|
||||||
|
- Power users que precisam alternar entre múltiplas contas de IA
|
||||||
|
- Usuários que precisam de um proxy API local confiável
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tom de Voz / Personalidade
|
||||||
|
|
||||||
|
- **Profissional e técnica**: Termos como "Nexus", "Proxy", "Gateway", "Quota", "Circuit Breaker"
|
||||||
|
- **Confiável e estável**: Monitoramento, health checks, logging robusto
|
||||||
|
- **Developer-first**: Suporte a múltiplas IDEs, geração de cURL/Python
|
||||||
|
- **Moderna e minimalista**: Interface limpa, componentes modulares
|
||||||
|
- **Global**: Suporte a i18n (EN, PT-BR, ZH, RU, VI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Estilo Visual Atual
|
||||||
|
|
||||||
|
### Tema
|
||||||
|
|
||||||
|
- Dual theme (claro/escuro) com detecção automática do sistema
|
||||||
|
- Extremamente minimalista e quase monocromático
|
||||||
|
|
||||||
|
### Paleta de Cores (Tailwind OKLCH)
|
||||||
|
|
||||||
|
| Modo | Background | Foreground | Primary | Destructive | Destaque |
|
||||||
|
| ---------- | ------------------------------ | --------------------- | ------- | --------------- | -------- |
|
||||||
|
| **Claro** | oklch(0.99 0 0) — quase branco | oklch(0 0 0) — preto | Preto | Vermelho | Azul |
|
||||||
|
| **Escuro** | oklch(0 0 0) — preto puro | oklch(1 0 0) — branco | Branco | Vermelho escuro | — |
|
||||||
|
|
||||||
|
### Cores Funcionais
|
||||||
|
|
||||||
|
- **Emerald/Green (`#10b981`)** — status ativo, quota alta, running
|
||||||
|
- **Amber/Yellow** — quota média, alertas
|
||||||
|
- **Rose/Red** — quota baixa, erro, stopped
|
||||||
|
- **Blue** — AI credits, informações premium
|
||||||
|
|
||||||
|
### Tipografia
|
||||||
|
|
||||||
|
- **Geist** (sans-serif) — fonte principal moderna e geométrica (da Vercel)
|
||||||
|
- **Geist Mono** (monospace) — dados técnicos, números de quota
|
||||||
|
- **Tomorrow** (display/futurista) — possíveis títulos estilizados
|
||||||
|
|
||||||
|
### Características de UI
|
||||||
|
|
||||||
|
- Bordas arredondadas (`0.5rem` / 8px)
|
||||||
|
- Sombras sutis
|
||||||
|
- Cards com hover states suaves
|
||||||
|
- Interface limpa com espaçamento generoso
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Assets Visuais Existentes
|
||||||
|
|
||||||
|
| Arquivo | Descrição |
|
||||||
|
| ---------------------------------------------------------------- | ---------------------------------------------------- |
|
||||||
|
| `docs/assets/logo.png` | Logo antiga (Antigravity) — 128x128, usada no README |
|
||||||
|
| `docs/assets/screenshot-main.png` | Screenshot da interface principal |
|
||||||
|
| `docs/assets/screenshot-proxy.png` | Screenshot da aba proxy |
|
||||||
|
| `docs/assets/screenshot-setting.png` | Screenshot de settings |
|
||||||
|
| `src/assets/icon.png` | Ícone do app (também é o logo antigo) |
|
||||||
|
| `src/assets/tray.png` | Ícone da system tray |
|
||||||
|
| `images/icon.ico` / `images/icon.icns` | Ícones de build para Windows/Mac |
|
||||||
|
| `images/favicon-*.png` | Favicons em múltiplos tamanhos |
|
||||||
|
| `images/32x32.png`, `64x64.png`, `128x128.png`, `[email protected]` | Ícones para AppImage Linux |
|
||||||
|
| `src/assets/fonts/geist/` | Fonte Geist (principal) |
|
||||||
|
| `src/assets/fonts/geist-mono/` | Fonte Geist Mono (código/dados) |
|
||||||
|
| `src/assets/fonts/tomorrow/` | Fonte Tomorrow (display, 4 pesos) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Conceitos Visuais Sugeridos para a Logo
|
||||||
|
|
||||||
|
### A. Nodo / Conexão (Nexus)
|
||||||
|
|
||||||
|
- Ponto central conectado a múltiplos pontos (contas gerenciadas)
|
||||||
|
- Círculo central com 2-3 linhas finas irradiando
|
||||||
|
- Dois círculos interligados (Gemini + Claude)
|
||||||
|
|
||||||
|
### B. Infinito / Loop (Gemini + Proxy)
|
||||||
|
|
||||||
|
- Símbolo de infinito (∞) modificado como circuito/loop de dados
|
||||||
|
- Referência à dualidade "Gemini" (gêmeos)
|
||||||
|
- Curva contínua elegante com seta indicando fluxo
|
||||||
|
|
||||||
|
### C. Cubo / Hexágono (Nexus / Hub)
|
||||||
|
|
||||||
|
- Hexágono ou cubo isométrico minimalista
|
||||||
|
- Linhas finas, perspectiva suave
|
||||||
|
- Face "aberta" ou conectada
|
||||||
|
|
||||||
|
### D. Monograma "GN"
|
||||||
|
|
||||||
|
- Letras "G" e "N" abstraídas como nodo de rede
|
||||||
|
- Estilo geométrico, similar a Tomorrow ou Geist
|
||||||
|
|
||||||
|
### E. Proxy / Pipe (Fluxo de Dados)
|
||||||
|
|
||||||
|
- Duas barras paralelas com seta passando entre elas
|
||||||
|
- "Pipe" minimalista — conduz tráfego entre APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Diretrizes para a Nova Logo
|
||||||
|
|
||||||
|
| Elemento | Especificação |
|
||||||
|
| ---------------------------- | ------------------------------------------------------------------------- |
|
||||||
|
| **Estilo** | Minimalista, geométrica, precisa, técnica |
|
||||||
|
| **Cores principais** | Preto (`#000000`) e branco (`#ffffff`) |
|
||||||
|
| **Cor de acento (opcional)** | Emerald (`#10b981`) para "ativo/online" |
|
||||||
|
| **Versões necessárias** | Dark mode e light mode |
|
||||||
|
| **Tamanhos** | De 16x16 (tray) até 512x512 (macOS) |
|
||||||
|
| **Monocromática** | Deve funcionar em preto e branco puro |
|
||||||
|
| **Tipografia do wordmark** | Geométrica sans-serif, peso medium/semibold, tracking levemente expandido |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Conclusão
|
||||||
|
|
||||||
|
O app pede uma logo que evoque **conexão/nexo** (Nexus) com **múltiplos pontos** (contas) de forma geométrica e limpa, usando primariamente preto/branco com um toque sutil de verde (emerald) para representar o estado "ativo/online" central ao propósito do app.
|
||||||
|
|
||||||
|
A logo deve transmitir:
|
||||||
|
|
||||||
|
- **Tecnologia** e **precisão**
|
||||||
|
- **Conectividade** e **hub central**
|
||||||
|
- **Profissionalismo** e **confiança**
|
||||||
|
- **Modernidade** e **minimalismo**
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Cloud Account Management and Intelligent Switching
|
# Cloud Account Management and Intelligent Switching
|
||||||
|
|
||||||
This document explains the cloud AI account management and switching capabilities in Antigravity Manager. The feature allows users to manage multiple Google accounts, monitor API quotas, and seamlessly switch the active IDE account with one click.
|
This document explains the cloud AI account management and switching capabilities in Gemini Nexus. The feature allows users to manage multiple Google accounts, monitor API quotas, and seamlessly switch the active IDE account with one click.
|
||||||
|
|
||||||
## 1. Core Features
|
## 1. Core Features
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Proxyman Debugging Guide for Antigravity and Antigravity Manager
|
# Proxyman Debugging Guide for Antigravity and Gemini Nexus
|
||||||
|
|
||||||
This guide explains how to use Proxyman for two debugging targets:
|
This guide explains how to use Proxyman for two debugging targets:
|
||||||
|
|
||||||
1. The installed official `Antigravity` desktop client
|
1. The installed official `Antigravity` desktop client
|
||||||
2. The `Antigravity Manager` development build from this repository
|
2. The `Gemini Nexus` development build from this repository
|
||||||
|
|
||||||
Before you start, complete [Proxyman Installation and Basic Setup on Windows](./proxyman-install.md).
|
Before you start, complete [Proxyman Installation and Basic Setup on Windows](./proxyman-install.md).
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Before you start, complete [Proxyman Installation and Basic Setup on Windows](./
|
|||||||
Use this guide when you want to:
|
Use this guide when you want to:
|
||||||
|
|
||||||
- inspect login, quota, project context, or model-list requests from `Antigravity`
|
- inspect login, quota, project context, or model-list requests from `Antigravity`
|
||||||
- inspect OAuth, quota refresh, or proxy traffic from `Antigravity Manager`
|
- inspect OAuth, quota refresh, or proxy traffic from `Gemini Nexus`
|
||||||
- compare requests from the official client with requests produced by this project
|
- compare requests from the official client with requests produced by this project
|
||||||
|
|
||||||
## Understand the Two Traffic Paths
|
## Understand the Two Traffic Paths
|
||||||
@@ -24,7 +24,7 @@ Use this guide when you want to:
|
|||||||
- Chromium or renderer-side requests
|
- Chromium or renderer-side requests
|
||||||
- Electron or Node-side requests
|
- Electron or Node-side requests
|
||||||
|
|
||||||
### `Antigravity Manager` in This Repository
|
### `Gemini Nexus` in This Repository
|
||||||
|
|
||||||
This project is also an Electron app, but it additionally includes Node, Undici, and internal proxy flows. You may need to inspect:
|
This project is also an Electron app, but it additionally includes Node, Undici, and internal proxy flows. You may need to inspect:
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\start-antigravity-with-proxym
|
|||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- This script targets the installed official `Antigravity` client.
|
- This script targets the installed official `Antigravity` client.
|
||||||
- It does not launch this repository's `Antigravity Manager` development build.
|
- It does not launch this repository's `Gemini Nexus` development build.
|
||||||
- Override `-AppPath`, `-CaPath`, `-ProxyHost`, or `-NoProxy` if your local environment differs.
|
- Override `-AppPath`, `-CaPath`, `-ProxyHost`, or `-NoProxy` if your local environment differs.
|
||||||
|
|
||||||
### Suggested Flow
|
### Suggested Flow
|
||||||
@@ -111,7 +111,7 @@ If system proxy behavior is unreliable, you can launch the installed client with
|
|||||||
|
|
||||||
Use this as a fallback, not the default path.
|
Use this as a fallback, not the default path.
|
||||||
|
|
||||||
## Debug Antigravity Manager From This Repository
|
## Debug Gemini Nexus From This Repository
|
||||||
|
|
||||||
This project has more layers than the official client. The script above is not the general launcher for this repository's development app.
|
This project has more layers than the official client. The script above is not the general launcher for this repository's development app.
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\start-antigravity-with-proxym
|
|||||||
|
|
||||||
4. Trigger login, quota, or model-list traffic.
|
4. Trigger login, quota, or model-list traffic.
|
||||||
|
|
||||||
### Antigravity Manager in This Repository
|
### Gemini Nexus in This Repository
|
||||||
|
|
||||||
1. Complete [Proxyman Installation and Basic Setup on Windows](./proxyman-install.md).
|
1. Complete [Proxyman Installation and Basic Setup on Windows](./proxyman-install.md).
|
||||||
2. Open PowerShell.
|
2. Open PowerShell.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default defineConfig([
|
|||||||
'unused-imports': unusedImports,
|
'unused-imports': unusedImports,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-empty-object-type': 'off',
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1774106199,
|
|
||||||
"narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Antigravity Manager";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
}: let
|
|
||||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
|
||||||
|
|
||||||
version = "0.10.0";
|
|
||||||
|
|
||||||
sources = {
|
|
||||||
"x86_64-linux" = {
|
|
||||||
url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_amd64.deb";
|
|
||||||
sha256 = "5fc08b4a9308c71fc00e44b95d4fb3178182aea8821708fa51fdf2bf5dbbb856";
|
|
||||||
};
|
|
||||||
"aarch64-linux" = {
|
|
||||||
url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_arm64.deb";
|
|
||||||
sha256 = "666c9659efa66bb6be6a8e7fb0c384ceb799a17245eab374bf9b3d7901c5c4cb";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkPackage = pkgs: let
|
|
||||||
system = pkgs.stdenv.hostPlatform.system;
|
|
||||||
source = sources.${system} or (throw "Unsupported system: ${system}");
|
|
||||||
in
|
|
||||||
pkgs.stdenv.mkDerivation {
|
|
||||||
pname = "antigravity-manager";
|
|
||||||
inherit version;
|
|
||||||
|
|
||||||
src = pkgs.fetchurl {
|
|
||||||
inherit (source) url sha256;
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
autoPatchelfHook
|
|
||||||
makeWrapper
|
|
||||||
zstd
|
|
||||||
];
|
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
alsa-lib
|
|
||||||
at-spi2-atk
|
|
||||||
atk
|
|
||||||
cups
|
|
||||||
dbus
|
|
||||||
expat
|
|
||||||
glib
|
|
||||||
gtk3
|
|
||||||
libsecret
|
|
||||||
libdrm
|
|
||||||
libxkbcommon
|
|
||||||
mesa
|
|
||||||
nspr
|
|
||||||
nss
|
|
||||||
pango
|
|
||||||
systemd
|
|
||||||
libx11
|
|
||||||
libxcomposite
|
|
||||||
libxdamage
|
|
||||||
libxext
|
|
||||||
libxfixes
|
|
||||||
libxrandr
|
|
||||||
libxcb
|
|
||||||
];
|
|
||||||
|
|
||||||
unpackPhase = ''
|
|
||||||
ar x $src
|
|
||||||
tar xf data.tar.* --no-same-permissions
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/bin $out/lib $out/share
|
|
||||||
cp -r usr/lib/antigravity-manager $out/lib/
|
|
||||||
cp -r usr/share/* $out/share/
|
|
||||||
|
|
||||||
ln -s $out/lib/antigravity-manager/antigravity-manager $out/bin/antigravity-manager
|
|
||||||
'';
|
|
||||||
|
|
||||||
postFixup = ''
|
|
||||||
# Wrap the executable so it finds Wayland/X11 and libsecret
|
|
||||||
wrapProgram $out/bin/antigravity-manager \
|
|
||||||
--add-flags "--enable-features=WaylandWindowDecorations" \
|
|
||||||
--add-flags "--ozone-platform-hint=auto" \
|
|
||||||
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [pkgs.libsecret]}
|
|
||||||
|
|
||||||
if [ -e $out/share/applications/antigravity-manager.desktop ]; then
|
|
||||||
substituteInPlace $out/share/applications/antigravity-manager.desktop \
|
|
||||||
--replace "/usr/lib/antigravity-manager/antigravity-manager" "antigravity-manager"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
|
||||||
description = "Professional multi-account manager for Google Gemini & Claude AI";
|
|
||||||
homepage = "https://github.com/Draculabo/AntigravityManager";
|
|
||||||
license = licenses.cc-by-nc-sa-40;
|
|
||||||
maintainers = [];
|
|
||||||
platforms = ["x86_64-linux" "aarch64-linux"];
|
|
||||||
mainProgram = "antigravity-manager";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
overlays.default = final: prev: {
|
|
||||||
antigravity-manager = mkPackage final;
|
|
||||||
};
|
|
||||||
|
|
||||||
packages = forAllSystems (system: let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
config.allowUnfree = true;
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
default = mkPackage pkgs;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||||
import type {
|
import type {
|
||||||
@@ -9,7 +10,6 @@ import type {
|
|||||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||||
import { MakerDMG } from '@electron-forge/maker-dmg';
|
import { MakerDMG } from '@electron-forge/maker-dmg';
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
|
||||||
import { MakerWix } from '@electron-forge/maker-wix';
|
import { MakerWix } from '@electron-forge/maker-wix';
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||||
@@ -17,15 +17,25 @@ import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
|||||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||||
import type { ForgeConfig } from '@electron-forge/shared-types';
|
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||||
import MakerAppImage from '@pengx17/electron-forge-maker-appimage';
|
import MakerAppImage from '@pengx17/electron-forge-maker-appimage';
|
||||||
|
// @ts-ignore
|
||||||
import setLanguages from 'electron-packager-languages';
|
import setLanguages from 'electron-packager-languages';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { stringify as yamlStringify } from 'yaml';
|
import { stringify as yamlStringify } from 'yaml';
|
||||||
|
|
||||||
const nativeModules = ['better-sqlite3', 'keytar', 'bindings', 'file-uri-to-path'];
|
const nativeModules = ['better-sqlite3', 'keytar', 'bindings', 'file-uri-to-path'];
|
||||||
const ResolvedMakerAppImage = MakerAppImage;
|
const ResolvedMakerAppImage: any = (MakerAppImage as any).default || MakerAppImage;
|
||||||
const keepLanguages = new Set(['en', 'en-US', 'zh-CN', 'ru']);
|
const keepLanguages = new Set(['en', 'en-US', 'zh-CN', 'ru']);
|
||||||
const windowsExecutableName = 'antigravity-manager';
|
const windowsExecutableName = 'gemini-nexus';
|
||||||
|
|
||||||
|
function isWixAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
execSync('candle -?', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isStartCommand = process.argv.some((arg) => arg.includes('start'));
|
const isStartCommand = process.argv.some((arg) => arg.includes('start'));
|
||||||
|
|
||||||
@@ -176,25 +186,8 @@ function getChecksumArchLabel(platform: string, arch: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appImageMaker = new ResolvedMakerAppImage({
|
const appImageMaker = new ResolvedMakerAppImage({
|
||||||
config: {
|
options: {
|
||||||
icons: [
|
icon: 'images/128x128.png',
|
||||||
{
|
|
||||||
file: 'images/32x32.png',
|
|
||||||
size: 32,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'images/64x64.png',
|
|
||||||
size: 64,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'images/128x128.png',
|
|
||||||
size: 128,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'images/[email protected]',
|
|
||||||
size: 256,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
appImageMaker.name = '@pengx17/electron-forge-maker-appimage';
|
appImageMaker.name = '@pengx17/electron-forge-maker-appimage';
|
||||||
@@ -204,7 +197,7 @@ const config: ForgeConfig = {
|
|||||||
asar: {
|
asar: {
|
||||||
unpack: '**/{better-sqlite3,keytar}/**/*',
|
unpack: '**/{better-sqlite3,keytar}/**/*',
|
||||||
},
|
},
|
||||||
name: 'Antigravity Manager',
|
name: 'Gemini Nexus',
|
||||||
executableName: windowsExecutableName,
|
executableName: windowsExecutableName,
|
||||||
icon: 'images/icon', // Electron Forge automatically adds .icns/.ico
|
icon: 'images/icon', // Electron Forge automatically adds .icns/.ico
|
||||||
extraResource: ['src/assets'], // Copy assets folder to resources/assets
|
extraResource: ['src/assets'], // Copy assets folder to resources/assets
|
||||||
@@ -447,12 +440,7 @@ const config: ForgeConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
makers: [
|
makers: [
|
||||||
new MakerSquirrel({
|
...(process.platform === 'win32' && process.arch === 'x64' && isWixAvailable()
|
||||||
setupIcon: 'images/icon.ico',
|
|
||||||
iconUrl:
|
|
||||||
'https://raw.githubusercontent.com/Draculabo/AntigravityManager/main/images/icon.ico',
|
|
||||||
}),
|
|
||||||
...(process.platform === 'win32' && process.arch === 'x64'
|
|
||||||
? [
|
? [
|
||||||
new MakerWix({
|
new MakerWix({
|
||||||
language: 1033,
|
language: 1033,
|
||||||
@@ -470,7 +458,7 @@ const config: ForgeConfig = {
|
|||||||
},
|
},
|
||||||
['darwin'],
|
['darwin'],
|
||||||
),
|
),
|
||||||
new MakerZIP({}, ['darwin']),
|
new MakerZIP({}, ['darwin', 'win32']),
|
||||||
appImageMaker,
|
appImageMaker,
|
||||||
new MakerRpm({}),
|
new MakerRpm({}),
|
||||||
new MakerDeb({}),
|
new MakerDeb({}),
|
||||||
@@ -484,8 +472,8 @@ const config: ForgeConfig = {
|
|||||||
name: '@electron-forge/publisher-github',
|
name: '@electron-forge/publisher-github',
|
||||||
config: {
|
config: {
|
||||||
repository: {
|
repository: {
|
||||||
owner: 'Draculabo',
|
owner: 'evandrodevbr',
|
||||||
name: 'AntigravityManager',
|
name: 'GeminiNexus',
|
||||||
},
|
},
|
||||||
draft: true,
|
draft: true,
|
||||||
prerelease: false,
|
prerelease: false,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 436 B |
@@ -2,7 +2,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Antigravity Manager</title>
|
<title>Gemini Nexus</title>
|
||||||
|
<link rel="icon" type="image/png" href="/src/assets/icon.png" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
|
||||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
<link rel="stylesheet" href="/src/styles/global.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
- Affected code:
|
- Affected code:
|
||||||
- `src/ipc/database/cloudHandler.ts`
|
- `src/ipc/database/cloudHandler.ts`
|
||||||
- `src/ipc/database/handler.ts`
|
- `src/ipc/database/handler.ts`
|
||||||
- `src/utils/antigravityVersion.ts`
|
- `src/utils/geminiNexusVersion.ts`
|
||||||
- `src/utils/paths.ts` (this stage covers default runtime configuration path only)
|
- `src/utils/paths.ts` (this stage covers default runtime configuration path only)
|
||||||
- `src/types/db.ts`
|
- `src/types/db.ts`
|
||||||
|
|
||||||
|
|||||||
@@ -38,5 +38,5 @@ The system SHALL use a strategy pattern to split new/legacy/dual-write injection
|
|||||||
The system SHALL prefer new-format injection when new-format keys are detected.
|
The system SHALL prefer new-format injection when new-format keys are detected.
|
||||||
|
|
||||||
#### Scenario: New-format key exists
|
#### Scenario: New-format key exists
|
||||||
- **WHEN** `antigravityUnifiedStateSync.oauthToken` exists
|
- **WHEN** `geminiNexusUnifiedStateSync.oauthToken` exists
|
||||||
- **THEN** injection uses new format with higher priority
|
- **THEN** injection uses new format with higher priority
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Add multilingual support to Antigravity Manager and automatically set the default UI language based on the user's OS language. If the OS language is unsupported, fall back to English. This includes updating i18n configuration, adding Chinese translation resources, and integrating a language detection plugin.
|
Add multilingual support to Gemini Nexus and automatically set the default UI language based on the user's OS language. If the OS language is unsupported, fall back to English. This includes updating i18n configuration, adding Chinese translation resources, and integrating a language detection plugin.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
|
|||||||
@@ -2,32 +2,32 @@
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Enable Antigravity Manager running in WSL (Windows Subsystem for Linux) to manage the Antigravity application installed on the Windows host.
|
Enable Gemini Nexus running in WSL (Windows Subsystem for Linux) to manage the Gemini Nexus application installed on the Windows host.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
Users running the manager in a WSL development environment need to interact with the actual Antigravity instance running on Windows, as Antigravity is a Windows application.
|
Users running the manager in a WSL development environment need to interact with the actual Gemini Nexus instance running on Windows, as Gemini Nexus is a Windows application.
|
||||||
|
|
||||||
## Proposed Changes
|
## Proposed Changes
|
||||||
|
|
||||||
### 1. Path Resolution (`src/utils/paths.ts`)
|
### 1. Path Resolution (`src/utils/paths.ts`)
|
||||||
|
|
||||||
* Implement `isWsl()` utility function.
|
* Implement `isWsl()` utility function.
|
||||||
* In `getAppDataDir()`: If WSL, return the Windows AppData path (e.g., `/mnt/c/Users/<User>/AppData/Roaming/Antigravity`).
|
* In `getAppDataDir()`: If WSL, return the Windows AppData path (e.g., `/mnt/c/Users/<User>/AppData/Roaming/GeminiNexus`).
|
||||||
* In `getAntigravityExecutablePath()`: If WSL, return the Windows executable path (e.g., `/mnt/c/Users/<User>/AppData/Local/Programs/Antigravity/Antigravity.exe`).
|
* In `getGeminiNexusExecutablePath()`: If WSL, return the Windows executable path (e.g., `/mnt/c/Users/<User>/AppData/Local/Programs/GeminiNexus/GeminiNexus.exe`).
|
||||||
* **Challenge:** Determining the correct Windows username.
|
* **Challenge:** Determining the correct Windows username.
|
||||||
* **Strategy:** Assume Windows username matches WSL username, or scan `/mnt/c/Users` for a likely candidate (excluding Public, Default, etc.).
|
* **Strategy:** Assume Windows username matches WSL username, or scan `/mnt/c/Users` for a likely candidate (excluding Public, Default, etc.).
|
||||||
|
|
||||||
### 2. Process Management (`src/ipc/process/handler.ts`)
|
### 2. Process Management (`src/ipc/process/handler.ts`)
|
||||||
|
|
||||||
* In `isProcessRunning()`: If WSL, execute `tasklist.exe` (via `/mnt/c/Windows/System32/tasklist.exe`) to check for `Antigravity.exe`.
|
* In `isProcessRunning()`: If WSL, execute `tasklist.exe` (via `/mnt/c/Windows/System32/tasklist.exe`) to check for `GeminiNexus.exe`.
|
||||||
* In `startAntigravity()`: If WSL, execute the Windows executable directly.
|
* In `startGeminiNexus()`: If WSL, execute the Windows executable directly.
|
||||||
* In `closeAntigravity()`: If WSL, use `taskkill.exe` (via `/mnt/c/Windows/System32/taskkill.exe`).
|
* In `closeGeminiNexus()`: If WSL, use `taskkill.exe` (via `/mnt/c/Windows/System32/taskkill.exe`).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
* \[ ] Application detects it is running in WSL.
|
* \[ ] Application detects it is running in WSL.
|
||||||
* \[ ] Database file is correctly located on the Windows file system.
|
* \[ ] Database file is correctly located on the Windows file system.
|
||||||
* \[ ] "Start" button successfully launches Antigravity on Windows.
|
* \[ ] "Start" button successfully launches Gemini Nexus on Windows.
|
||||||
* \[ ] "Stop" button successfully terminates Antigravity on Windows.
|
* \[ ] "Stop" button successfully terminates Gemini Nexus on Windows.
|
||||||
* \[ ] Process status (Running/Stopped) is correctly reflected in the UI.
|
* \[ ] Process status (Running/Stopped) is correctly reflected in the UI.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
### Requirement: New-Format Injection for 1.16.5+
|
### Requirement: New-Format Injection for 1.16.5+
|
||||||
The system SHALL write only the new format `antigravityUnifiedStateSync.oauthToken` when IDE version is >= 1.16.5.
|
The system SHALL write only the new format `geminiNexusUnifiedStateSync.oauthToken` when IDE version is >= 1.16.5.
|
||||||
|
|
||||||
#### Scenario: New-version injection
|
#### Scenario: New-version injection
|
||||||
- **WHEN** IDE version is 1.16.5 or above
|
- **WHEN** IDE version is 1.16.5 or above
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Antigravity Manager is a comprehensive enhancement tool for Antigravity, designed to remove AI development resource limits. It not only solves the lack of native multi-account switching in Antigravity IDE, but also introduces enterprise-grade AI account pool management. By taking control of app configuration state and cloud API resources, it allows users to switch seamlessly between unlimited local accounts and cloud AI services for an effectively "unlimited" AI usage experience.
|
Gemini Nexus is a comprehensive enhancement tool for Gemini Nexus, designed to remove AI development resource limits. It not only solves the lack of native multi-account switching in Gemini Nexus IDE, but also introduces enterprise-grade AI account pool management. By taking control of app configuration state and cloud API resources, it allows users to switch seamlessly between unlimited local accounts and cloud AI services for an effectively "unlimited" AI usage experience.
|
||||||
|
|
||||||
**Core goals:**
|
**Core goals:**
|
||||||
|
|
||||||
* **Local multi-account switching**: Seamlessly switch Antigravity IDE local user state while protecting data integrity (automatic backup).
|
* **Local multi-account switching**: Seamlessly switch Gemini Nexus IDE local user state while protecting data integrity (automatic backup).
|
||||||
* **AI resource pool management (new)**: Centrally manage account pools for Google Gemini, Claude 3.5, and similar services.
|
* **AI resource pool management (new)**: Centrally manage account pools for Google Gemini, Claude 3.5, and similar services.
|
||||||
* **Intelligent quota monitoring (new)**: Monitor API quota and health in real time with a visual dashboard.
|
* **Intelligent quota monitoring (new)**: Monitor API quota and health in real time with a visual dashboard.
|
||||||
* **Automatic resource scheduling (new)**: Automatically switch to the best available account when the current one runs out of quota (token keepalive + polling).
|
* **Automatic resource scheduling (new)**: Automatically switch to the best available account when the current one runs out of quota (token keepalive + polling).
|
||||||
@@ -88,17 +88,17 @@ src/
|
|||||||
|
|
||||||
## Domain Context
|
## Domain Context
|
||||||
|
|
||||||
### 1. Antigravity IDE Data Structure (Local)
|
### 1. Gemini Nexus IDE Data Structure (Local)
|
||||||
|
|
||||||
**Database locations:**
|
**Database locations:**
|
||||||
|
|
||||||
* macOS: `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`
|
* macOS: `~/Library/Application Support/GeminiNexus/User/globalStorage/state.vscdb`
|
||||||
* Windows: `%APPDATA%/Antigravity/User/globalStorage/state.vscdb`
|
* Windows: `%APPDATA%/GeminiNexus/User/globalStorage/state.vscdb`
|
||||||
* Linux: `~/.config/Antigravity/User/globalStorage/state.vscdb`
|
* Linux: `~/.config/GeminiNexus/User/globalStorage/state.vscdb`
|
||||||
|
|
||||||
**Key database fields:**
|
**Key database fields:**
|
||||||
|
|
||||||
* `antigravityAuthStatus`: Authentication status and user information.
|
* `geminiNexusAuthStatus`: Authentication status and user information.
|
||||||
|
|
||||||
### 2. AI Service Resource Structure (Cloud / New)
|
### 2. AI Service Resource Structure (Cloud / New)
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ src/
|
|||||||
|
|
||||||
### System Dependencies
|
### System Dependencies
|
||||||
|
|
||||||
* **Antigravity IDE**: Local target to manage.
|
* **Gemini Nexus IDE**: Local target to manage.
|
||||||
* **SQLite**: Local data reads.
|
* **SQLite**: Local data reads.
|
||||||
|
|
||||||
### Node.js Dependencies (Key)
|
### Node.js Dependencies (Key)
|
||||||
@@ -159,7 +159,7 @@ src/
|
|||||||
|
|
||||||
### Filesystem Dependencies
|
### Filesystem Dependencies
|
||||||
|
|
||||||
* `~/.antigravity-agent/`
|
* `~/.geminiNexus-agent/`
|
||||||
* `accounts.json`: Local IDE account index.
|
* `accounts.json`: Local IDE account index.
|
||||||
* `cloud_accounts.db`: (new) Cloud account pool database (SQLite).
|
* `cloud_accounts.db`: (new) Cloud account pool database (SQLite).
|
||||||
* `backups/`: Backup files.
|
* `backups/`: Backup files.
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "antigravity-manager",
|
"name": "gemini-nexus",
|
||||||
"productName": "Antigravity Manager",
|
"productName": "Gemini Nexus",
|
||||||
"version": "0.11.0",
|
"version": "0.15.3",
|
||||||
"description": "Antigravity Manager - Electron App",
|
"description": "Gemini Nexus - Electron App",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "[email protected]",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "cross-env DEBUG=electron-forge:* electron-forge start",
|
||||||
|
"dev": "cross-env DEBUG=electron-forge:* electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
|
"make:win": "node scripts/build-windows.js",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
|
"setup": "node scripts/setup-dev.js",
|
||||||
|
"postinstall": "node scripts/setup-dev.js --check-only",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:write": "prettier --write .",
|
"format:write": "prettier --write .",
|
||||||
@@ -20,33 +25,33 @@
|
|||||||
"test:all": "vitest run && playwright test",
|
"test:all": "vitest run && playwright test",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"author": "Draculabo",
|
"author": "evandrodevbr",
|
||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.10.2",
|
"@electron-forge/cli": "^7.6.1",
|
||||||
"@electron-forge/maker-deb": "^7.10.2",
|
"@electron-forge/maker-deb": "^7.11.1",
|
||||||
"@electron-forge/maker-dmg": "^7.10.2",
|
"@electron-forge/maker-dmg": "^7.11.1",
|
||||||
"@electron-forge/maker-rpm": "^7.10.2",
|
"@electron-forge/maker-rpm": "^7.11.1",
|
||||||
"@electron-forge/maker-squirrel": "^7.10.2",
|
"@electron-forge/maker-wix": "^7.9.0",
|
||||||
"@electron-forge/maker-wix": "^7.10.2",
|
"@electron-forge/maker-zip": "^7.11.1",
|
||||||
"@electron-forge/maker-zip": "^7.10.2",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.10.2",
|
"@electron-forge/plugin-fuses": "^7.11.1",
|
||||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
"@electron-forge/plugin-vite": "^7.11.1",
|
||||||
"@electron-forge/plugin-vite": "^7.10.2",
|
"@electron-forge/publisher-github": "^7.11.1",
|
||||||
"@electron-forge/publisher-github": "^7.10.2",
|
"@electron-forge/shared-types": "^7.11.1",
|
||||||
"@electron-forge/shared-types": "^7.8.1",
|
|
||||||
"@electron/fuses": "~1.8.0",
|
"@electron/fuses": "~1.8.0",
|
||||||
"@electron/rebuild": "^3.7.1",
|
"@electron/rebuild": "^3.7.1",
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.4.1",
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.0",
|
||||||
"@pengx17/electron-forge-maker-appimage": "^1.2.1",
|
"@pengx17/electron-forge-maker-appimage": "^1.0.2",
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.56.1",
|
||||||
|
"@rolldown/plugin-babel": "^0.2.3",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/github": "^12.0.2",
|
"@semantic-release/github": "^12.0.6",
|
||||||
"@sentry/vite-plugin": "^4.8.0",
|
"@sentry/vite-plugin": "^4.8.0",
|
||||||
"@swc/core": "^1.15.8",
|
"@swc/core": "^1.15.8",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/router-plugin": "^1.135.0",
|
"@tanstack/router-plugin": "^1.135.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -58,11 +63,12 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"code-inspector-plugin": "^1.3.3",
|
"code-inspector-plugin": "^1.3.3",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"electron": "^37.3.1",
|
"cross-env": "^10.1.0",
|
||||||
|
"electron": "^41.3.0",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"electron-packager-languages": "^0.6.0",
|
"electron-packager-languages": "^0.6.0",
|
||||||
"electron-playwright-helpers": "^1.8.2",
|
"electron-playwright-helpers": "^1.8.2",
|
||||||
@@ -73,12 +79,14 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"happy-dom": "^20.8.9",
|
||||||
"icon-gen": "^5.0.0",
|
"icon-gen": "^5.0.0",
|
||||||
"jsdom": "^27.1.0",
|
"jsdom": "^27.1.0",
|
||||||
|
"node-abi": "^4.28.0",
|
||||||
"node-addon-api": "^8.3.1",
|
"node-addon-api": "^8.3.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"semantic-release": "^25.0.2",
|
"semantic-release": "^25.0.3",
|
||||||
"semantic-release-config-gitmoji": "^1.5.3",
|
"semantic-release-config-gitmoji": "^1.5.3",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
@@ -86,20 +94,24 @@
|
|||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.3",
|
"typescript-eslint": "^8.46.3",
|
||||||
"unplugin-swc": "^1.5.9",
|
"unplugin-swc": "^1.5.9",
|
||||||
"vite": "^5.4.21",
|
"vite": "^8.0.10",
|
||||||
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.3",
|
|
||||||
"@grpc/proto-loader": "^0.8.0",
|
|
||||||
"@icons-pack/react-simple-icons": "^13.8.0",
|
"@icons-pack/react-simple-icons": "^13.8.0",
|
||||||
"@nestjs/common": "^11.1.10",
|
"@nestjs/common": "^11.1.18",
|
||||||
"@nestjs/core": "^11.1.10",
|
"@nestjs/core": "^11.1.18",
|
||||||
"@nestjs/microservices": "^11.1.11",
|
"@nestjs/microservices": "^11.1.18",
|
||||||
"@nestjs/platform-express": "^11.1.11",
|
"@nestjs/platform-express": "^11.1.18",
|
||||||
"@nestjs/platform-fastify": "^11.1.10",
|
"@nestjs/platform-fastify": "^11.0.20",
|
||||||
"@orpc/client": "^1.10.4",
|
"@nestjs/websockets": "^11.1.19",
|
||||||
"@orpc/server": "^1.11.3",
|
"@nivo/bar": "^0.99.0",
|
||||||
|
"@nivo/core": "^0.99.0",
|
||||||
|
"@nivo/line": "^0.99.0",
|
||||||
|
"@nivo/pie": "^0.99.0",
|
||||||
|
"@orpc/client": "^1.13.14",
|
||||||
|
"@orpc/server": "^1.13.14",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -120,23 +132,22 @@
|
|||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.7",
|
||||||
"@tanstack/react-router": "^1.139.3",
|
"@tanstack/react-router": "^1.139.3",
|
||||||
"@tanstack/react-router-devtools": "^1.135.0",
|
"@tanstack/react-router-devtools": "^1.135.0",
|
||||||
"@types/uuid": "^11.0.0",
|
"axios": "^1.15.0",
|
||||||
"axios": "^1.13.2",
|
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.39.1",
|
"drizzle-orm": "^0.45.2",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.8.5",
|
||||||
"find-process": "^2.0.0",
|
"find-process": "^2.0.0",
|
||||||
"happy-dom": "^20.0.10",
|
"gpt-tokenizer": "^3.4.0",
|
||||||
"i18next": "^25.6.3",
|
"i18next": "^25.6.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"keytar": "^7.9.0",
|
"keytar": "^7.9.0",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.18.1",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -148,7 +159,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"undici": "^7.16.0",
|
"undici": "^7.16.0",
|
||||||
"update-electron-app": "^3.1.1",
|
"update-electron-app": "^3.1.1",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^14.0.0",
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
@@ -159,6 +170,42 @@
|
|||||||
"@nestjs/core",
|
"@nestjs/core",
|
||||||
"@sentry/cli",
|
"@sentry/cli",
|
||||||
"@swc/core",
|
"@swc/core",
|
||||||
"protobufjs"
|
"protobufjs",
|
||||||
]
|
"macos-alias",
|
||||||
|
"better-sqlite3",
|
||||||
|
"keytar",
|
||||||
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
|
"esbuild",
|
||||||
|
"fs-xattr",
|
||||||
|
"appdmg"
|
||||||
|
],
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@bitdisaster/exe-icon-extractor",
|
||||||
|
"@nestjs/core",
|
||||||
|
"@sentry/cli",
|
||||||
|
"@swc/core",
|
||||||
|
"protobufjs",
|
||||||
|
"macos-alias",
|
||||||
|
"better-sqlite3",
|
||||||
|
"keytar",
|
||||||
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
|
"esbuild",
|
||||||
|
"fs-xattr",
|
||||||
|
"appdmg"
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"node-abi": "^4.28.0",
|
||||||
|
"fastify": "^5.8.5",
|
||||||
|
"tar": "^7.5.13",
|
||||||
|
"@octokit/request-error": "^5.1.1",
|
||||||
|
"@octokit/plugin-paginate-rest": "^9.2.2",
|
||||||
|
"@octokit/request": "^8.4.1",
|
||||||
|
"tmp": "^0.2.4",
|
||||||
|
"@tootallnate/once": "^3.0.1",
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@bitdisaster/exe-icon-extractor': true
|
||||||
|
'@nestjs/core': true
|
||||||
|
'@sentry/cli': true
|
||||||
|
'@swc/core': true
|
||||||
|
better-sqlite3: true
|
||||||
|
electron: true
|
||||||
|
electron-winstaller: true
|
||||||
|
esbuild: true
|
||||||
|
keytar: true
|
||||||
|
macos-alias: true
|
||||||
|
protobufjs: true
|
||||||
|
sharp: true
|
||||||
|
fs-xattr: true
|
||||||
|
appdmg: true
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* instead of Gitmoji (since the project history doesn't use emojis).
|
* instead of Gitmoji (since the project history doesn't use emojis).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const REPO_URL = 'https://github.com/Draculabo/AntigravityManager';
|
const REPO_URL = 'https://github.com/evandrodevbr/GeminiNexus';
|
||||||
const GITHUB_BASE_URL = 'https://github.com';
|
const GITHUB_BASE_URL = 'https://github.com';
|
||||||
const FULL_CHANGELOG_URL = `${REPO_URL}/blob/main/CHANGELOG.md`;
|
const FULL_CHANGELOG_URL = `${REPO_URL}/blob/main/CHANGELOG.md`;
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ const releaseConfig = {
|
|||||||
[
|
[
|
||||||
'@semantic-release/git',
|
'@semantic-release/git',
|
||||||
{
|
{
|
||||||
assets: ['CHANGELOG.md', 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
|
assets: ['CHANGELOG.md', 'package.json', 'pnpm-lock.yaml'],
|
||||||
message: 'chore(release): ${nextRelease.version}\n\n${nextRelease.notes}',
|
message: 'chore(release): ${nextRelease.version}\n\n${nextRelease.notes}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
set PYTHONPATH=%~dp0
|
|
||||||
python -m cli.main %*
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
|
const { execSync, spawn } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const WIX_ZIP = path.join(process.cwd(), 'wix314-binaries.zip');
|
||||||
|
const WIX_DIR = path.join(process.cwd(), 'tools', 'wix314');
|
||||||
|
|
||||||
|
function extractWix() {
|
||||||
|
if (!fs.existsSync(WIX_ZIP)) {
|
||||||
|
console.error(`WiX zip not found: ${WIX_ZIP}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(WIX_DIR)) {
|
||||||
|
console.log(`WiX already extracted at ${WIX_DIR}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Extracting WiX binaries to ${WIX_DIR}...`);
|
||||||
|
fs.mkdirSync(WIX_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Use PowerShell Expand-Archive (built into Windows)
|
||||||
|
const psCommand = `Expand-Archive -Path "${WIX_ZIP}" -DestinationPath "${WIX_DIR}" -Force`;
|
||||||
|
execSync(psCommand, { shell: 'powershell.exe', stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log('WiX extraction complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWixPath() {
|
||||||
|
// electron-wix-msi's detect-wix.js runs execSync('candle -?') which relies on PATH
|
||||||
|
process.env.PATH = `${WIX_DIR};${process.env.PATH}`;
|
||||||
|
console.log(`Added ${WIX_DIR} to PATH`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMake() {
|
||||||
|
console.log('Running electron-forge make...');
|
||||||
|
const make = spawn('npx', ['electron-forge', 'make'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
make.on('exit', (code) => {
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extractWix();
|
||||||
|
setupWixPath();
|
||||||
|
runMake();
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
/**
|
||||||
|
* Cross-platform development environment setup for Gemini Nexus.
|
||||||
|
*
|
||||||
|
* Detects and installs native build toolchains required by better-sqlite3,
|
||||||
|
* keytar, and other native Node.js modules used in this Electron project.
|
||||||
|
*
|
||||||
|
* Platforms:
|
||||||
|
* - Windows → Visual Studio 2022 Build Tools (C++ workload)
|
||||||
|
* - macOS → Xcode Command Line Tools
|
||||||
|
* - Ubuntu → build-essential, python3, libsecret-1-dev
|
||||||
|
* - Manjaro/Arch → base-devel, python, libsecret
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/setup-dev.js # auto-detect and install/guide
|
||||||
|
* node scripts/setup-dev.js --check-only # only verify, don't install
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { execSync, spawnSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// ─── Terminal Color Helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const RESET = '\x1b[0m';
|
||||||
|
const BOLD = '\x1b[1m';
|
||||||
|
const DIM = '\x1b[2m';
|
||||||
|
const RED = '\x1b[31m';
|
||||||
|
const GREEN = '\x1b[32m';
|
||||||
|
const YELLOW = '\x1b[33m';
|
||||||
|
const CYAN = '\x1b[36m';
|
||||||
|
const MAGENTA = '\x1b[35m';
|
||||||
|
|
||||||
|
// Disable colors if NO_COLOR is set or output is piped
|
||||||
|
const useColor = process.env.NO_COLOR === undefined && process.stdout.isTTY;
|
||||||
|
|
||||||
|
function color(code, text) {
|
||||||
|
return useColor ? `${code}${text}${RESET}` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function header(text) {
|
||||||
|
console.log(
|
||||||
|
`\n${color(BOLD + MAGENTA, '══')} ${color(BOLD, text)} ${color(BOLD + MAGENTA, '══')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(text) {
|
||||||
|
console.log(` ${color(GREEN, '✓')} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(text) {
|
||||||
|
console.log(` ${color(YELLOW, '⚠')} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(text) {
|
||||||
|
console.log(` ${color(RED, '✗')} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(text) {
|
||||||
|
console.log(` ${color(CYAN, '→')} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dim(text) {
|
||||||
|
console.log(` ${color(DIM, text)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
console.log(`\n ${color(BOLD, title)}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Platform Detection ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const platform = os.platform(); // 'win32' | 'darwin' | 'linux'
|
||||||
|
const arch = os.arch();
|
||||||
|
|
||||||
|
/** Detect Linux distribution from os-release */
|
||||||
|
function detectLinuxDistro() {
|
||||||
|
try {
|
||||||
|
const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
|
||||||
|
const idMatch = osRelease.match(/^ID="?([^"\n]+)"?/m);
|
||||||
|
const idLikeMatch = osRelease.match(/^ID_LIKE="?([^"\n]+)"?/m);
|
||||||
|
|
||||||
|
const id = idMatch ? idMatch[1].toLowerCase() : '';
|
||||||
|
const idLike = idLikeMatch ? idLikeMatch[1].toLowerCase() : '';
|
||||||
|
|
||||||
|
if (id === 'ubuntu' || id === 'debian' || idLike.includes('debian')) {
|
||||||
|
return 'debian';
|
||||||
|
}
|
||||||
|
if (id === 'arch' || id === 'manjaro' || idLike.includes('arch')) {
|
||||||
|
return 'arch';
|
||||||
|
}
|
||||||
|
if (id === 'fedora' || idLike.includes('fedora') || idLike.includes('rhel')) {
|
||||||
|
return 'fedora';
|
||||||
|
}
|
||||||
|
return id || 'unknown';
|
||||||
|
} catch {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tool Checkers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Check if a command exists on PATH */
|
||||||
|
function commandExists(cmd) {
|
||||||
|
try {
|
||||||
|
const whereCmd = platform === 'win32' ? 'where' : 'which';
|
||||||
|
execSync(`${whereCmd} "${cmd}"`, { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if running with administrator/sudo privileges */
|
||||||
|
function hasAdminPrivileges() {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
try {
|
||||||
|
// On Windows, check via net session (requires admin)
|
||||||
|
execSync('net session', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// On Unix, check if UID is 0
|
||||||
|
return process.getuid ? process.getuid() === 0 : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check Windows: Visual Studio Build Tools */
|
||||||
|
function checkWindows() {
|
||||||
|
const results = { ok: true, missing: [] };
|
||||||
|
|
||||||
|
// Check for cl.exe (C++ compiler from VS Build Tools)
|
||||||
|
if (commandExists('cl.exe')) {
|
||||||
|
success('MSVC Compiler (cl.exe) found');
|
||||||
|
} else {
|
||||||
|
// Try to find VS installation via vswhere or registry
|
||||||
|
let vsFound = false;
|
||||||
|
|
||||||
|
// vswhere.exe (shipped with VS 2017+)
|
||||||
|
const vswherePaths = [
|
||||||
|
'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe',
|
||||||
|
'C:\\Program Files\\Microsoft Visual Studio\\Installer\\vswhere.exe',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const vswhere of vswherePaths) {
|
||||||
|
try {
|
||||||
|
const output = execSync(
|
||||||
|
`"${vswhere}" -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
|
||||||
|
{
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
encoding: 'utf8',
|
||||||
|
},
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
vsFound = true;
|
||||||
|
success(`Visual Studio found at: ${output}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// vswhere not found, continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check registry fallback
|
||||||
|
if (!vsFound) {
|
||||||
|
try {
|
||||||
|
execSync('reg query "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7" /v 17.0', {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
vsFound = true;
|
||||||
|
success('Visual Studio 2022 found (registry)');
|
||||||
|
} catch {
|
||||||
|
// Not found in registry either
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vsFound) {
|
||||||
|
error('MSVC Build Tools not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('msvc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Python (needed by node-gyp)
|
||||||
|
if (commandExists('python') || commandExists('python3')) {
|
||||||
|
success('Python found');
|
||||||
|
} else {
|
||||||
|
error('Python not found (required by node-gyp)');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('python');
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check macOS: Xcode Command Line Tools */
|
||||||
|
function checkMacOS() {
|
||||||
|
const results = { ok: true, missing: [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xcodePath = execSync('xcode-select -p', {
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
encoding: 'utf8',
|
||||||
|
}).trim();
|
||||||
|
if (xcodePath) {
|
||||||
|
success(`Xcode CLT found at: ${xcodePath}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
error('Xcode Command Line Tools not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('xcode-clt');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandExists('make') || commandExists('gmake')) {
|
||||||
|
success('make found');
|
||||||
|
} else {
|
||||||
|
error('make not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('make');
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check Linux: build-essential, python3, libsecret */
|
||||||
|
function checkLinux() {
|
||||||
|
const results = { ok: true, missing: [] };
|
||||||
|
const distro = detectLinuxDistro();
|
||||||
|
|
||||||
|
section(`Linux Distribution Detected: ${distro || 'unknown'}`);
|
||||||
|
|
||||||
|
// gcc / g++
|
||||||
|
if (commandExists('gcc') || commandExists('cc')) {
|
||||||
|
success('C compiler found');
|
||||||
|
} else {
|
||||||
|
error('C compiler not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('compiler');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandExists('g++') || commandExists('c++')) {
|
||||||
|
success('C++ compiler found');
|
||||||
|
} else {
|
||||||
|
error('C++ compiler not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('compiler');
|
||||||
|
}
|
||||||
|
|
||||||
|
// make
|
||||||
|
if (commandExists('make')) {
|
||||||
|
success('make found');
|
||||||
|
} else {
|
||||||
|
error('make not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('make');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python 3
|
||||||
|
if (commandExists('python3')) {
|
||||||
|
success('Python 3 found');
|
||||||
|
} else if (commandExists('python')) {
|
||||||
|
// Check if python is python3
|
||||||
|
try {
|
||||||
|
const ver = execSync('python --version', { encoding: 'utf8' });
|
||||||
|
if (ver.includes('3.')) {
|
||||||
|
success('Python 3 found (as python)');
|
||||||
|
} else {
|
||||||
|
error('Python 3 not found (python is v2)');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('python3');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
error('Python not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('python3');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error('Python 3 not found');
|
||||||
|
results.ok = false;
|
||||||
|
results.missing.push('python3');
|
||||||
|
}
|
||||||
|
|
||||||
|
// libsecret (needed by keytar)
|
||||||
|
try {
|
||||||
|
if (distro === 'debian') {
|
||||||
|
execSync('dpkg -s libsecret-1-dev', { stdio: 'ignore' });
|
||||||
|
success('libsecret-1-dev found');
|
||||||
|
} else if (distro === 'arch') {
|
||||||
|
execSync('pacman -Qi libsecret', { stdio: 'ignore' });
|
||||||
|
success('libsecret found');
|
||||||
|
} else {
|
||||||
|
// Check for pkg-config
|
||||||
|
try {
|
||||||
|
execSync('pkg-config --exists libsecret-1', { stdio: 'ignore' });
|
||||||
|
success('libsecret (pkg-config) found');
|
||||||
|
} catch {
|
||||||
|
warn('libsecret not detected (may not be needed if using prebuilt keytar)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
warn('libsecret not detected (may not be needed if using prebuilt keytar)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Installation Helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Run a command and stream output, return exit code */
|
||||||
|
function runCommand(cmd, args, opts = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log(`\n ${color(CYAN, 'Running:')} ${color(DIM, `${cmd} ${args.join(' ')}`)}`);
|
||||||
|
|
||||||
|
const child = spawnSync(cmd, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: platform === 'win32',
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(child.status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Install Windows: Visual Studio 2022 Build Tools via winget */
|
||||||
|
async function installWindows() {
|
||||||
|
// Check if winget is available
|
||||||
|
if (!commandExists('winget')) {
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
color(RED, ' winget not found. Please install Visual Studio Build Tools manually:'),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
color(
|
||||||
|
CYAN,
|
||||||
|
' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(' During installation, select:');
|
||||||
|
console.log(color(BOLD, ' "Desktop development with C++" workload'));
|
||||||
|
console.log('');
|
||||||
|
console.log(' Or install via PowerShell (as Administrator):');
|
||||||
|
console.log(
|
||||||
|
color(
|
||||||
|
DIM,
|
||||||
|
' winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
info('Attempting automatic installation of Visual Studio 2022 Build Tools...');
|
||||||
|
dim('This may take several minutes and requires administrator privileges.');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (!hasAdminPrivileges()) {
|
||||||
|
warn('Not running as Administrator. winget may prompt for elevation.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await runCommand('winget', [
|
||||||
|
'install',
|
||||||
|
'Microsoft.VisualStudio.2022.BuildTools',
|
||||||
|
'--override',
|
||||||
|
'"--quiet --wait --norestart --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
success('Visual Studio 2022 Build Tools installed successfully.');
|
||||||
|
console.log('');
|
||||||
|
warn('You may need to restart your terminal for PATH changes to take effect.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error('Automatic installation failed. Please install manually.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Install macOS: Xcode Command Line Tools */
|
||||||
|
async function installMacOS() {
|
||||||
|
console.log('');
|
||||||
|
info('Installing Xcode Command Line Tools...');
|
||||||
|
dim('A GUI dialog will appear. Click "Install" to proceed.');
|
||||||
|
|
||||||
|
const status = await runCommand('xcode-select', ['--install']);
|
||||||
|
|
||||||
|
// xcode-select --install returns 1 if already installed or cancelled
|
||||||
|
if (status === 0) {
|
||||||
|
success('Xcode Command Line Tools installation initiated.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already installed (common false-negative)
|
||||||
|
try {
|
||||||
|
execSync('xcode-select -p', { stdio: 'ignore' });
|
||||||
|
success('Xcode Command Line Tools are already installed.');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
warn('Xcode CLT installation was cancelled or failed.');
|
||||||
|
warn('You can install manually via: xcode-select --install');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Install Linux: distribution-specific packages */
|
||||||
|
async function installLinux() {
|
||||||
|
const distro = detectLinuxDistro();
|
||||||
|
|
||||||
|
if (distro === 'debian') {
|
||||||
|
// Ubuntu / Debian
|
||||||
|
if (!hasAdminPrivileges()) {
|
||||||
|
warn('Root privileges required. You may be prompted for your password.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await runCommand('sudo', [
|
||||||
|
'apt-get',
|
||||||
|
'install',
|
||||||
|
'-y',
|
||||||
|
'build-essential',
|
||||||
|
'python3',
|
||||||
|
'libsecret-1-dev',
|
||||||
|
'pkg-config',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
success('Build dependencies installed successfully.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error('Failed to install build dependencies.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distro === 'arch') {
|
||||||
|
// Manjaro / Arch
|
||||||
|
if (!hasAdminPrivileges()) {
|
||||||
|
warn('Root privileges required. You may be prompted for your password.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await runCommand('sudo', [
|
||||||
|
'pacman',
|
||||||
|
'-S',
|
||||||
|
'--needed',
|
||||||
|
'--noconfirm',
|
||||||
|
'base-devel',
|
||||||
|
'python',
|
||||||
|
'libsecret',
|
||||||
|
'pkg-config',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
success('Build dependencies installed successfully.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error('Failed to install build dependencies.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distro === 'fedora') {
|
||||||
|
if (!hasAdminPrivileges()) {
|
||||||
|
warn('Root privileges required. You may be prompted for your password.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await runCommand('sudo', [
|
||||||
|
'dnf',
|
||||||
|
'install',
|
||||||
|
'-y',
|
||||||
|
'gcc-c++',
|
||||||
|
'make',
|
||||||
|
'python3',
|
||||||
|
'libsecret-devel',
|
||||||
|
'pkg-config',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
success('Build dependencies installed successfully.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error('Failed to install build dependencies.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown distro - provide generic instructions
|
||||||
|
console.log('');
|
||||||
|
warn(`Unknown Linux distribution: ${distro}`);
|
||||||
|
console.log('');
|
||||||
|
console.log(' You need the following packages to compile native Node.js modules:');
|
||||||
|
console.log(' - C/C++ compiler (gcc, g++)');
|
||||||
|
console.log(' - GNU Make');
|
||||||
|
console.log(' - Python 3');
|
||||||
|
console.log(' - libsecret development headers (for keytar)');
|
||||||
|
console.log(' - pkg-config');
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
' Install them using your distribution package manager, then run npm install again.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const checkOnly = args.includes('--check-only') || args.includes('--check');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(color(BOLD + CYAN, '┌──────────────────────────────────────────┐'));
|
||||||
|
console.log(color(BOLD + CYAN, '│ Gemini Nexus - Dev Environment Setup │'));
|
||||||
|
console.log(color(BOLD + CYAN, '└──────────────────────────────────────────┘'));
|
||||||
|
console.log('');
|
||||||
|
dim(`Platform: ${platform} (${arch})`);
|
||||||
|
dim(`Node.js: ${process.version}`);
|
||||||
|
|
||||||
|
// Step 1: Check current state
|
||||||
|
header('Checking Build Toolchain');
|
||||||
|
|
||||||
|
let results;
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
results = checkWindows();
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
results = checkMacOS();
|
||||||
|
break;
|
||||||
|
case 'linux':
|
||||||
|
results = checkLinux();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('');
|
||||||
|
error(`Unsupported platform: ${platform}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
console.log('');
|
||||||
|
if (results.ok) {
|
||||||
|
success('All build tools are present and ready.');
|
||||||
|
console.log('');
|
||||||
|
dim('You can now run: npm install && npm start');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Missing tools ──
|
||||||
|
console.log('');
|
||||||
|
error(
|
||||||
|
'Some build tools are missing. Native modules like better-sqlite3 and keytar require compilation.',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checkOnly) {
|
||||||
|
console.log('');
|
||||||
|
warn('Build tools are missing. Native modules (better-sqlite3, keytar) require compilation.');
|
||||||
|
warn('Run "npm run setup" to install them automatically, or install manually:');
|
||||||
|
printManualInstructions(platform, results.missing);
|
||||||
|
console.log('');
|
||||||
|
dim(
|
||||||
|
'npm install succeeded, but npm start / electron-rebuild will fail until build tools are installed.',
|
||||||
|
);
|
||||||
|
// Don't exit with error - allow npm install to succeed (prebuilt binaries may work)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Attempt installation
|
||||||
|
header('Installing Build Dependencies');
|
||||||
|
|
||||||
|
let installed = false;
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
installed = await installWindows();
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
installed = await installMacOS();
|
||||||
|
break;
|
||||||
|
case 'linux':
|
||||||
|
installed = await installLinux();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Final status
|
||||||
|
console.log('');
|
||||||
|
if (installed) {
|
||||||
|
success('Setup complete. You can now run: npm install && npm start');
|
||||||
|
console.log('');
|
||||||
|
dim('If you encounter issues, restart your terminal and try again.');
|
||||||
|
} else {
|
||||||
|
console.log('');
|
||||||
|
warn('Automatic installation was not successful.');
|
||||||
|
warn('Please install the required tools manually, then run npm install again.');
|
||||||
|
printManualInstructions(platform, results.missing);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printManualInstructions(platform, missing) {
|
||||||
|
console.log('');
|
||||||
|
console.log(color(BOLD, ' ── Manual Installation Instructions ──'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
console.log(' 1. Download Visual Studio 2022 Build Tools:');
|
||||||
|
console.log(
|
||||||
|
color(
|
||||||
|
CYAN,
|
||||||
|
' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(' 2. Run the installer and select:');
|
||||||
|
console.log(color(BOLD, ' "Desktop development with C++"'));
|
||||||
|
console.log('');
|
||||||
|
console.log(' 3. Or install via PowerShell (as Administrator):');
|
||||||
|
console.log(
|
||||||
|
color(
|
||||||
|
DIM,
|
||||||
|
' winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(' 4. Restart your terminal after installation.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'darwin':
|
||||||
|
console.log(' Run this command in your terminal:');
|
||||||
|
console.log(color(DIM, ' xcode-select --install'));
|
||||||
|
console.log('');
|
||||||
|
console.log(' Then click "Install" in the dialog that appears.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'linux': {
|
||||||
|
const distro = detectLinuxDistro();
|
||||||
|
if (distro === 'debian') {
|
||||||
|
console.log(' Run:');
|
||||||
|
console.log(
|
||||||
|
color(
|
||||||
|
DIM,
|
||||||
|
' sudo apt-get install -y build-essential python3 libsecret-1-dev pkg-config',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (distro === 'arch') {
|
||||||
|
console.log(' Run:');
|
||||||
|
console.log(
|
||||||
|
color(DIM, ' sudo pacman -S --needed base-devel python libsecret pkg-config'),
|
||||||
|
);
|
||||||
|
} else if (distro === 'fedora') {
|
||||||
|
console.log(' Run:');
|
||||||
|
console.log(
|
||||||
|
color(DIM, ' sudo dnf install -y gcc-c++ make python3 libsecret-devel pkg-config'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(' Install: C/C++ compiler, make, python3, libsecret-dev, pkg-config');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Run ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(color(RED, `\nUnexpected error: ${err.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { syncWithLocalTheme } from './actions/theme';
|
import { syncWithLocalTheme } from './actions/theme';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -29,12 +29,10 @@ const queryClient = new QueryClient();
|
|||||||
|
|
||||||
const root = createRoot(document.getElementById('app')!);
|
const root = createRoot(document.getElementById('app')!);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<QueryClientProvider client={queryClient}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ThemeProvider storageKey={LOCAL_STORAGE_KEYS.THEME} defaultTheme="dark">
|
||||||
<ThemeProvider storageKey={LOCAL_STORAGE_KEYS.THEME} defaultTheme="system">
|
<App />
|
||||||
<App />
|
<Toaster />
|
||||||
<Toaster />
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</QueryClientProvider>,
|
||||||
</QueryClientProvider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
);
|
||||||
@@ -4,10 +4,10 @@ export function isProcessRunning() {
|
|||||||
return ipc.client.proc.isProcessRunning();
|
return ipc.client.proc.isProcessRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeAntigravity() {
|
export function closeGeminiNexus() {
|
||||||
return ipc.client.proc.closeAntigravity();
|
return ipc.client.proc.closeGeminiNexus();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startAntigravity() {
|
export function startGeminiNexus() {
|
||||||
return ipc.client.proc.startAntigravity();
|
return ipc.client.proc.startGeminiNexus();
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 875 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 44 KiB |
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { MoreVertical, Trash2, RefreshCw } from 'lucide-react';
|
import { MoreVertical, Trash2, RefreshCw } from 'lucide-react';
|
||||||
import { formatDistanceToNow, type Locale } from 'date-fns';
|
import { formatDistanceToNow, type Locale } from 'date-fns';
|
||||||
import { enUS, zhCN, ru } from 'date-fns/locale';
|
import { enUS, zhCN, ru, vi } from 'date-fns/locale';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ const DATE_LOCALE_MAP: Record<string, Locale> = {
|
|||||||
en: enUS,
|
en: enUS,
|
||||||
'zh-CN': zhCN,
|
'zh-CN': zhCN,
|
||||||
ru: ru,
|
ru: ru,
|
||||||
|
vi: vi,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AccountCardProps {
|
interface AccountCardProps {
|
||||||
@@ -113,8 +114,10 @@ export const AccountCard: React.FC<AccountCardProps> = ({
|
|||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative overflow-hidden transition-all duration-200',
|
'relative overflow-hidden transition-all duration-200',
|
||||||
'hover:shadow-primary/10 hover:shadow-xl',
|
|
||||||
isCurrent ? 'border-primary/50 bg-primary/5' : 'hover:border-primary/20 cursor-pointer',
|
isCurrent
|
||||||
|
? 'border-primary/50 bg-primary/10'
|
||||||
|
: 'hover:bg-accent/10 cursor-pointer border-white/5',
|
||||||
(isSwitching || isDeleting) && 'pointer-events-none opacity-60',
|
(isSwitching || isDeleting) && 'pointer-events-none opacity-60',
|
||||||
)}
|
)}
|
||||||
onClick={() => !isCurrent && !isSwitching && !isDeleting && onSwitch(account.id)}
|
onClick={() => !isCurrent && !isSwitching && !isDeleting && onSwitch(account.id)}
|
||||||
@@ -162,7 +165,7 @@ export const AccountCard: React.FC<AccountCardProps> = ({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-4 w-4" />
|
<MoreVertical className="h-4 w-4" />
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">{t('common.openMenu')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCloudAccounts, useSetAccountProxy } from '@/hooks/useCloudAccounts';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { isValidProxyUrl } from '@/utils/url';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { CloudAccount } from '@/types/cloudAccount';
|
||||||
|
|
||||||
|
function AccountProxyItem({ account }: { account: CloudAccount }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const setAccountProxy = useSetAccountProxy();
|
||||||
|
const [proxyUrl, setProxyUrl] = useState(account.proxy_url || '');
|
||||||
|
const [proxySaved, setProxySaved] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4 border-b py-3 last:border-0">
|
||||||
|
<div className="flex items-center gap-3 overflow-hidden">
|
||||||
|
{account.avatar_url ? (
|
||||||
|
<img
|
||||||
|
src={account.avatar_url}
|
||||||
|
alt={account.name || ''}
|
||||||
|
className="bg-muted h-8 w-8 shrink-0 rounded-full border"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="bg-primary/10 text-primary flex h-8 w-8 shrink-0 items-center justify-center rounded-full border">
|
||||||
|
{account.name?.[0]?.toUpperCase() || 'A'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col overflow-hidden">
|
||||||
|
<span className="truncate text-sm font-semibold">
|
||||||
|
{account.name || t('cloud.card.unknown')}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground truncate text-xs">{account.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={proxyUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
setProxyUrl(e.target.value);
|
||||||
|
setProxySaved(false);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const trimmed = proxyUrl.trim();
|
||||||
|
if (trimmed && !isValidProxyUrl(trimmed)) {
|
||||||
|
setProxyUrl(account.proxy_url || '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trimmed !== (account.proxy_url || '')) {
|
||||||
|
setAccountProxy.mutate({
|
||||||
|
accountId: account.id,
|
||||||
|
proxyUrl: trimmed || null,
|
||||||
|
});
|
||||||
|
setProxySaved(true);
|
||||||
|
setTimeout(() => setProxySaved(false), 2000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t('cloud.card.proxyPlaceholder') || 'e.g. http://127.0.0.1:7890'}
|
||||||
|
className="h-8 w-48 text-sm"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="w-16">
|
||||||
|
{proxySaved && (
|
||||||
|
<span className="text-[10px] whitespace-nowrap text-emerald-400">
|
||||||
|
{t('cloud.card.proxySaved')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountProxySettings() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data: accounts, isLoading } = useCloudAccounts();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('settings.proxy.accountProxies.title')}</CardTitle>
|
||||||
|
<CardDescription>{t('settings.proxy.accountProxies.description')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-center p-6">
|
||||||
|
<Loader2 className="text-muted-foreground animate-spin" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('settings.proxy.accountProxies.title')}</CardTitle>
|
||||||
|
<CardDescription>{t('settings.proxy.accountProxies.description')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col">
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<AccountProxyItem key={account.id} account={account} />
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
import { MoreVertical, Trash, RefreshCw, Box, Power, Fingerprint, Eye, EyeOff } from 'lucide-react';
|
import { MoreVertical, Trash, RefreshCw, Box, Power, Fingerprint, Eye, EyeOff } from 'lucide-react';
|
||||||
@@ -29,14 +28,12 @@ import { useProviderGrouping } from '@/hooks/useProviderGrouping';
|
|||||||
import { ProviderGroup } from '@/components/ProviderGroup';
|
import { ProviderGroup } from '@/components/ProviderGroup';
|
||||||
import {
|
import {
|
||||||
clampQuotaPercentage,
|
clampQuotaPercentage,
|
||||||
|
formatAiCreditsAmount,
|
||||||
formatResetTimeLabel,
|
formatResetTimeLabel,
|
||||||
formatResetTimeTitle,
|
formatResetTimeTitle,
|
||||||
getQuotaStatus,
|
getQuotaStatus,
|
||||||
type QuotaStatus,
|
type QuotaStatus,
|
||||||
} from '@/utils/quota-display';
|
} from '@/utils/quota-display';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useSetAccountProxy } from '@/hooks/useCloudAccounts';
|
|
||||||
import { isValidProxyUrl } from '@/utils/url';
|
|
||||||
import { getValidationBlockedStatusLabel } from '@/components/accountValidationStatus';
|
import { getValidationBlockedStatusLabel } from '@/components/accountValidationStatus';
|
||||||
|
|
||||||
type ModelQuotaEntry = [string, CloudQuotaModelInfo];
|
type ModelQuotaEntry = [string, CloudQuotaModelInfo];
|
||||||
@@ -61,9 +58,9 @@ const MODEL_DISPLAY_REPLACEMENTS: Array<[string, string]> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
const QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
||||||
high: 'text-green-500',
|
high: 'text-emerald-400',
|
||||||
medium: 'text-yellow-500',
|
medium: 'text-amber-400',
|
||||||
low: 'text-red-500',
|
low: 'text-rose-400',
|
||||||
};
|
};
|
||||||
|
|
||||||
const QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
const QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
||||||
@@ -82,6 +79,19 @@ function isGeminiProHighModel(modelName: string): boolean {
|
|||||||
return normalizedModelName.includes('gemini-3.1-pro-high');
|
return normalizedModelName.includes('gemini-3.1-pro-high');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCreditsExpiry(expiryDate: string): string {
|
||||||
|
if (!expiryDate) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(expiryDate);
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return expiryDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isGeminiProFamilyModel(modelName: string): boolean {
|
function isGeminiProFamilyModel(modelName: string): boolean {
|
||||||
const normalizedModelName = modelName.toLowerCase();
|
const normalizedModelName = modelName.toLowerCase();
|
||||||
return normalizedModelName.includes('gemini-3.1-pro');
|
return normalizedModelName.includes('gemini-3.1-pro');
|
||||||
@@ -172,9 +182,6 @@ export function CloudAccountCard({
|
|||||||
isProviderCollapsed,
|
isProviderCollapsed,
|
||||||
toggleProviderCollapse,
|
toggleProviderCollapse,
|
||||||
} = useProviderGrouping();
|
} = useProviderGrouping();
|
||||||
const setAccountProxy = useSetAccountProxy();
|
|
||||||
const [proxyUrl, setProxyUrl] = useState(account.proxy_url || '');
|
|
||||||
const [proxySaved, setProxySaved] = useState(false);
|
|
||||||
|
|
||||||
const getQuotaTextColorClass = (percentage: number) => {
|
const getQuotaTextColorClass = (percentage: number) => {
|
||||||
const quotaStatus = getQuotaStatus(percentage);
|
const quotaStatus = getQuotaStatus(percentage);
|
||||||
@@ -228,54 +235,51 @@ export function CloudAccountCard({
|
|||||||
const renderQuotaModelGroup = (title: string, models: ModelQuotaEntry[]) => {
|
const renderQuotaModelGroup = (title: string, models: ModelQuotaEntry[]) => {
|
||||||
if (models.length === 0) return null;
|
if (models.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div key={title} className="space-y-1">
|
<div key={title} className="mt-1 space-y-1">
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1">
|
<h4 className="text-muted-foreground/60 mb-1.5 px-1 text-[10px] font-bold tracking-widest uppercase">
|
||||||
<span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase">
|
{title}
|
||||||
{title}
|
</h4>
|
||||||
</span>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="bg-border/50 h-[1px] flex-1" />
|
{models.map(([modelName, info]) => (
|
||||||
</div>
|
<div
|
||||||
{models.map(([modelName, info]) => (
|
key={modelName}
|
||||||
<div
|
className="group/item grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md px-1.5 py-1 text-[13px] transition-colors hover:bg-white/[0.03]"
|
||||||
key={modelName}
|
|
||||||
className="group/item hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-sm transition-all"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold"
|
|
||||||
title={modelName}
|
|
||||||
>
|
>
|
||||||
{formatModelDisplayName(modelName)}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col items-end gap-0.5">
|
|
||||||
<span
|
<span
|
||||||
className="text-muted-foreground text-[9px] leading-none opacity-80"
|
className="text-muted-foreground group-hover/item:text-foreground truncate font-medium transition-colors"
|
||||||
title={formatResetTimeTitleText(info.resetTime)}
|
title={modelName}
|
||||||
>
|
>
|
||||||
{formatResetTimeLabelText(info.resetTime)}
|
{formatModelDisplayName(modelName)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`font-mono text-xs leading-none font-bold ${getQuotaTextColorClass(info.percentage)}`}
|
className="text-muted-foreground/70 min-w-[50px] text-right text-[9px]"
|
||||||
|
title={formatResetTimeTitleText(info.resetTime)}
|
||||||
|
>
|
||||||
|
{formatResetTimeLabelText(info.resetTime)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`w-9 text-right font-mono text-xs font-bold ${getQuotaTextColorClass(info.percentage)}`}
|
||||||
>
|
>
|
||||||
{info.percentage}%
|
{info.percentage}%
|
||||||
</span>
|
</span>
|
||||||
<div className="bg-muted h-1 w-16 overflow-hidden rounded-full">
|
<div className="h-1.5 w-12 overflow-hidden rounded-full bg-white/[0.04]">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(info.percentage)}`}
|
className={`h-full rounded-full transition-all duration-500 ease-out ${getQuotaBarColorClass(info.percentage)}`}
|
||||||
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyQuotaState = (
|
const emptyQuotaState = (
|
||||||
<div className="text-muted-foreground flex flex-col items-center justify-center py-4">
|
<div className="text-muted-foreground flex flex-col items-center justify-center py-6">
|
||||||
<Box className="mb-2 h-8 w-8 opacity-20" />
|
<Box className="mb-2 h-6 w-6 opacity-20" />
|
||||||
<span className="text-xs">{t('cloud.card.noQuota')}</span>
|
<span className="text-xs">{t('cloud.card.noQuota')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -283,18 +287,20 @@ export function CloudAccountCard({
|
|||||||
const providerStats = providerGroupingsEnabled ? getAccountStats(account) : null;
|
const providerStats = providerGroupingsEnabled ? getAccountStats(account) : null;
|
||||||
const providerGroupedQuotaSection =
|
const providerGroupedQuotaSection =
|
||||||
providerStats && providerStats.visibleModels > 0 ? (
|
providerStats && providerStats.visibleModels > 0 ? (
|
||||||
<>
|
<div className="space-y-4">
|
||||||
<div className="bg-muted/40 flex items-center justify-between rounded-lg px-3 py-1.5 text-xs">
|
<div className="flex items-center justify-between rounded-lg border border-white/[0.04] bg-white/[0.02] px-3 py-2">
|
||||||
<span className="font-medium">{t('settings.providerGroupings.overall')}</span>
|
<span className="text-muted-foreground text-xs font-medium">
|
||||||
|
{t('settings.providerGroupings.overall')}
|
||||||
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`font-mono font-bold ${getQuotaTextColorClass(providerStats.overallPercentage)}`}
|
className={`font-mono text-xs font-bold ${getQuotaTextColorClass(providerStats.overallPercentage)}`}
|
||||||
>
|
>
|
||||||
{formatQuotaLabel(providerStats.overallPercentage)}
|
{formatQuotaLabel(providerStats.overallPercentage)}
|
||||||
</span>
|
</span>
|
||||||
<div className="bg-muted h-1.5 w-16 overflow-hidden rounded-full">
|
<div className="h-1.5 w-16 overflow-hidden rounded-full bg-white/[0.04]">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(providerStats.overallPercentage)}`}
|
className={`h-full rounded-full transition-all duration-500 ease-out ${getQuotaBarColorClass(providerStats.overallPercentage)}`}
|
||||||
style={{
|
style={{
|
||||||
width: `${clampQuotaPercentage(providerStats.overallPercentage)}%`,
|
width: `${clampQuotaPercentage(providerStats.overallPercentage)}%`,
|
||||||
}}
|
}}
|
||||||
@@ -303,7 +309,7 @@ export function CloudAccountCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
<div className="space-y-3">
|
||||||
{providerStats.providers.map((group) => (
|
{providerStats.providers.map((group) => (
|
||||||
<ProviderGroup
|
<ProviderGroup
|
||||||
key={group.providerKey}
|
key={group.providerKey}
|
||||||
@@ -319,23 +325,14 @@ export function CloudAccountCard({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
emptyQuotaState
|
emptyQuotaState
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiCredits = account.quota?.ai_credits;
|
const aiCredits = account.quota?.ai_credits;
|
||||||
const shouldShowAiCredits = !!aiCredits && Number.isFinite(aiCredits.credits) && aiCredits.credits >= 0;
|
const shouldShowAiCredits =
|
||||||
|
!!aiCredits && Number.isFinite(aiCredits.credits) && aiCredits.credits >= 0;
|
||||||
const formatCreditsExpiry = (expiryDate: string) => {
|
|
||||||
if (!expiryDate) return '';
|
|
||||||
try {
|
|
||||||
const date = new Date(expiryDate);
|
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
||||||
} catch {
|
|
||||||
return expiryDate;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validationBlockedStatusLabel = getValidationBlockedStatusLabel(
|
const validationBlockedStatusLabel = getValidationBlockedStatusLabel(
|
||||||
account.status,
|
account.status,
|
||||||
@@ -345,17 +342,17 @@ export function CloudAccountCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`group bg-card hover:border-primary/40 flex h-full flex-col overflow-hidden border transition-all duration-200 hover:shadow-sm ${isSelected ? 'ring-primary border-primary/50 ring-2' : ''}`}
|
className={`group bg-card relative flex h-full flex-col overflow-hidden rounded-xl border transition-all duration-300 hover:shadow-md ${isSelected ? 'border-primary/50 ring-primary/50 ring-1' : 'border-white/[0.06] hover:border-white/[0.15]'}`}
|
||||||
>
|
>
|
||||||
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-start gap-4 space-y-0 p-5 pb-4">
|
||||||
{onToggleSelection && (
|
{onToggleSelection && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} bg-background/90 rounded-full p-2 transition-opacity`}
|
className={`absolute top-3 left-3 z-10 transition-opacity duration-200 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={(checked) => onToggleSelection(account.id, checked as boolean)}
|
onCheckedChange={(checked) => onToggleSelection(account.id, checked as boolean)}
|
||||||
className="h-5 w-5 border-2"
|
className="bg-background/80 data-[state=checked]:bg-primary h-4 w-4 backdrop-blur-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -364,184 +361,193 @@ export function CloudAccountCard({
|
|||||||
<img
|
<img
|
||||||
src={account.avatar_url}
|
src={account.avatar_url}
|
||||||
alt={account.name || ''}
|
alt={account.name || ''}
|
||||||
className="bg-muted h-10 w-10 rounded-full border"
|
className="h-11 w-11 shrink-0 rounded-full border border-white/[0.08] bg-white/[0.02] object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full border">
|
<div className="text-muted-foreground flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-lg font-medium">
|
||||||
{account.name?.[0]?.toUpperCase() || 'A'}
|
{account.name?.[0]?.toUpperCase() || 'A'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
<CardTitle className="truncate text-base font-semibold">
|
<div className="flex flex-1 flex-col gap-0.5 overflow-hidden pt-0.5">
|
||||||
|
<CardTitle className="truncate text-base leading-tight font-semibold">
|
||||||
{account.name || t('cloud.card.unknown')}
|
{account.name || t('cloud.card.unknown')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="truncate text-xs">{account.email}</CardDescription>
|
<CardDescription className="text-muted-foreground/80 truncate text-xs font-medium">
|
||||||
|
{account.email}
|
||||||
|
</CardDescription>
|
||||||
|
|
||||||
{shouldShowAiCredits && aiCredits && (
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||||
<div className="mt-1 flex items-center gap-1 text-[10px] font-medium text-amber-500">
|
|
||||||
<span>${aiCredits.credits.toFixed(2)}</span>
|
|
||||||
{aiCredits.expiryDate && (
|
|
||||||
<span className="text-muted-foreground opacity-70">
|
|
||||||
·{' '}
|
|
||||||
{t('cloud.card.creditsExpiry', {
|
|
||||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{allModelEntries.length > 0 && (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
|
||||||
{(() => {
|
|
||||||
const hiddenCount = allModelEntries.filter(
|
|
||||||
([modelName]) => config?.model_visibility?.[modelName] === false,
|
|
||||||
).length;
|
|
||||||
return hiddenCount > 0 ? (
|
|
||||||
<EyeOff className="text-muted-foreground h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
<span className="sr-only">{t('cloud.card.modelVisibility')}</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-64" align="end">
|
|
||||||
<DropdownMenuLabel>{t('cloud.card.modelVisibility')}</DropdownMenuLabel>
|
|
||||||
<div className="max-h-64 overflow-auto px-2 py-1">
|
|
||||||
{allModelEntries.map(([modelName]) => {
|
|
||||||
const isVisible = config?.model_visibility?.[modelName] !== false;
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={modelName}
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
className="flex cursor-pointer items-center gap-2"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={isVisible}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (config) {
|
|
||||||
const newVisibility = { ...config.model_visibility };
|
|
||||||
newVisibility[modelName] = checked as boolean;
|
|
||||||
saveConfig({ ...config, model_visibility: newVisibility });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="truncate text-xs" title={modelName}>
|
|
||||||
{formatModelDisplayName(modelName)}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
|
||||||
<MoreVertical className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Menu</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>{t('cloud.card.actions')}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuItem onClick={() => onSwitch(account.id)} disabled={isSwitching}>
|
|
||||||
<Power className="mr-2 h-4 w-4" />
|
|
||||||
{t('cloud.card.useAccount')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={() => onRefresh(account.id)} disabled={isRefreshing}>
|
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
|
||||||
{t('cloud.card.refresh')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={() => onManageIdentity(account.id)}>
|
|
||||||
<Fingerprint className="mr-2 h-4 w-4" />
|
|
||||||
{t('cloud.card.identityProfile')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDelete(account.id)}
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
<Trash className="mr-2 h-4 w-4" />
|
|
||||||
{t('cloud.card.delete')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="flex-1 pb-2">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
account.status === 'rate_limited' || account.status === 'expired'
|
account.status === 'rate_limited' || account.status === 'expired'
|
||||||
? 'destructive'
|
? 'destructive'
|
||||||
: 'outline'
|
: 'outline'
|
||||||
}
|
}
|
||||||
className="text-xs"
|
className="h-4 border-white/[0.1] bg-white/[0.02] px-1.5 py-0 text-[9px] uppercase"
|
||||||
>
|
>
|
||||||
{account.provider.toUpperCase()}
|
{account.provider}
|
||||||
</Badge>
|
</Badge>
|
||||||
{account.is_active && (
|
{account.is_active && (
|
||||||
<Badge variant="default" className="bg-green-500 text-xs hover:bg-green-600">
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className="h-4 border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0 text-[9px] text-emerald-400 uppercase hover:bg-emerald-500/20"
|
||||||
|
>
|
||||||
{t('cloud.card.active')}
|
{t('cloud.card.active')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{validationBlockedStatusLabel && (
|
{hasHighTier && (
|
||||||
<span className="text-destructive text-xs font-medium">
|
<Badge
|
||||||
{validationBlockedStatusLabel}
|
variant="secondary"
|
||||||
</span>
|
className="h-4 animate-pulse border-blue-500/20 bg-blue-500/10 px-1.5 py-0 text-[9px] text-blue-400 uppercase"
|
||||||
|
>
|
||||||
|
{t('cloud.card.gemini3Ready')}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasHighTier && (
|
{validationBlockedStatusLabel && (
|
||||||
<Badge
|
<span className="text-destructive mt-1 text-xs font-medium">
|
||||||
variant="secondary"
|
{validationBlockedStatusLabel}
|
||||||
className="animate-pulse border-blue-500/20 bg-blue-500/10 text-[10px] text-blue-500"
|
</span>
|
||||||
>
|
|
||||||
{t('cloud.card.gemini3Ready')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.is_active ? (
|
{shouldShowAiCredits && aiCredits && (
|
||||||
<Button variant="ghost" size="sm" disabled className="text-green-600 opacity-100">
|
<div className="mt-2 flex w-fit items-center gap-1.5 rounded-md border border-blue-500/20 bg-blue-500/10 px-2 py-1">
|
||||||
<Power className="mr-1 h-3 w-3" />
|
<span className="text-[10px] font-semibold text-blue-400">
|
||||||
{t('cloud.card.active')}
|
{t('cloud.card.aiCreditsValue', {
|
||||||
</Button>
|
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||||
) : (
|
})}
|
||||||
<Button
|
</span>
|
||||||
variant="secondary"
|
{aiCredits.expiryDate && (
|
||||||
size="sm"
|
<>
|
||||||
onClick={() => onSwitch(account.id)}
|
<span className="h-2.5 w-[1px] bg-blue-500/30"></span>
|
||||||
disabled={isSwitching}
|
<span className="text-[9px] font-medium text-blue-400/80">
|
||||||
className="cursor-pointer"
|
{t('cloud.card.creditsExpiry', {
|
||||||
>
|
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||||
{isSwitching ? (
|
})}
|
||||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
</span>
|
||||||
) : (
|
</>
|
||||||
<Power className="mr-1 h-3 w-3" />
|
|
||||||
)}
|
)}
|
||||||
{t('cloud.card.use')}
|
</div>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
{allModelEntries.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground/70 hover:text-foreground h-7 w-7"
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
const hiddenCount = allModelEntries.filter(
|
||||||
|
([modelName]) => config?.model_visibility?.[modelName] === false,
|
||||||
|
).length;
|
||||||
|
return hiddenCount > 0 ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<span className="sr-only">{t('cloud.card.modelVisibility')}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-64" align="end">
|
||||||
|
<DropdownMenuLabel className="text-xs">
|
||||||
|
{t('cloud.card.modelVisibility')}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="max-h-[300px] overflow-y-auto p-1">
|
||||||
|
{allModelEntries.map(([modelName]) => {
|
||||||
|
const isVisible = config?.model_visibility?.[modelName] !== false;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={modelName}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="flex cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isVisible}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (config) {
|
||||||
|
const newVisibility = { ...config.model_visibility };
|
||||||
|
newVisibility[modelName] = checked as boolean;
|
||||||
|
saveConfig({ ...config, model_visibility: newVisibility });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
<span className="truncate text-xs font-medium" title={modelName}>
|
||||||
|
{formatModelDisplayName(modelName)}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground/70 hover:text-foreground h-7 w-7"
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSwitch(account.id)}
|
||||||
|
disabled={isSwitching}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Power className="mr-2 h-4 w-4" />
|
||||||
|
<span className="font-medium">{t('cloud.card.useAccount')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onRefresh(account.id)}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t('cloud.card.refresh')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onManageIdentity(account.id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Fingerprint className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t('cloud.card.identityProfile')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete(account.id)}
|
||||||
|
className="text-destructive focus:bg-destructive/10 focus:text-destructive cursor-pointer"
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t('cloud.card.delete')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex-1 px-5 pb-5">
|
||||||
|
<div className="space-y-4">
|
||||||
{providerGroupingsEnabled ? (
|
{providerGroupingsEnabled ? (
|
||||||
providerGroupedQuotaSection
|
providerGroupedQuotaSection
|
||||||
) : hasVisibleQuotaModels ? (
|
) : hasVisibleQuotaModels ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{renderQuotaModelGroup(t('cloud.card.groupGoogleGemini'), geminiModels)}
|
{renderQuotaModelGroup('Google Gemini', geminiModels)}
|
||||||
<div className="pt-1" />
|
{claudeModels.length > 0 && renderQuotaModelGroup('Anthropic Claude', claudeModels)}
|
||||||
{renderQuotaModelGroup(t('cloud.card.groupAnthropicClaude'), claudeModels)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
emptyQuotaState
|
emptyQuotaState
|
||||||
@@ -549,45 +555,36 @@ export function CloudAccountCard({
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="bg-muted/20 text-muted-foreground justify-between border-t p-2 px-4 text-xs">
|
<CardFooter className="flex items-center justify-between border-t border-white/[0.04] bg-white/[0.01] px-5 py-3">
|
||||||
<span>
|
<span className="text-muted-foreground/70 text-[11px] font-medium">
|
||||||
{t('cloud.card.used')}{' '}
|
{t('cloud.card.used')}{' '}
|
||||||
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
|
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
{account.is_active ? (
|
||||||
value={proxyUrl}
|
<span className="flex items-center gap-1.5 text-xs font-semibold text-emerald-400">
|
||||||
onChange={(e) => {
|
<span className="relative flex h-2 w-2">
|
||||||
setProxyUrl(e.target.value);
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
setProxySaved(false);
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||||
}}
|
</span>
|
||||||
onBlur={() => {
|
Active
|
||||||
const trimmed = proxyUrl.trim();
|
</span>
|
||||||
if (trimmed && !isValidProxyUrl(trimmed)) {
|
) : (
|
||||||
setProxyUrl(account.proxy_url || '');
|
<Button
|
||||||
return;
|
variant="ghost"
|
||||||
}
|
size="sm"
|
||||||
if (trimmed !== (account.proxy_url || '')) {
|
onClick={() => onSwitch(account.id)}
|
||||||
setAccountProxy.mutate({
|
disabled={isSwitching}
|
||||||
accountId: account.id,
|
className="hover:bg-primary/10 hover:text-primary h-7 cursor-pointer px-3 text-xs font-medium transition-colors"
|
||||||
proxyUrl: trimmed || null,
|
>
|
||||||
});
|
{isSwitching ? (
|
||||||
setProxySaved(true);
|
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
|
||||||
setTimeout(() => setProxySaved(false), 2000);
|
) : (
|
||||||
}
|
<Power className="mr-1.5 h-3 w-3" />
|
||||||
}}
|
)}
|
||||||
placeholder={t('cloud.card.proxyPlaceholder')}
|
{t('cloud.card.use')}
|
||||||
className="h-6 w-40 text-xs"
|
</Button>
|
||||||
onKeyDown={(e) => {
|
)}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{proxySaved && (
|
|
||||||
<span className="text-[10px] text-green-500">{t('cloud.card.proxySaved')}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -635,17 +632,8 @@ export function CompactCloudAccountCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const aiCredits = account.quota?.ai_credits;
|
const aiCredits = account.quota?.ai_credits;
|
||||||
const shouldShowAiCredits = !!aiCredits && Number.isFinite(aiCredits.credits) && aiCredits.credits >= 0;
|
const shouldShowAiCredits =
|
||||||
|
!!aiCredits && Number.isFinite(aiCredits.credits) && aiCredits.credits >= 0;
|
||||||
const formatCreditsExpiry = (expiryDate: string) => {
|
|
||||||
if (!expiryDate) return '';
|
|
||||||
try {
|
|
||||||
const date = new Date(expiryDate);
|
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
||||||
} catch {
|
|
||||||
return expiryDate;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validationBlockedStatusLabel = getValidationBlockedStatusLabel(
|
const validationBlockedStatusLabel = getValidationBlockedStatusLabel(
|
||||||
account.status,
|
account.status,
|
||||||
@@ -654,22 +642,22 @@ export function CompactCloudAccountCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-card hover:border-primary/40 flex items-center gap-3 rounded-lg border px-3 py-2 transition-all duration-200">
|
<div className="group bg-card flex items-center gap-3 rounded-xl border border-white/[0.04] px-4 py-3 transition-all duration-200 hover:border-white/[0.1] hover:shadow-sm">
|
||||||
{account.avatar_url ? (
|
{account.avatar_url ? (
|
||||||
<img
|
<img
|
||||||
src={account.avatar_url}
|
src={account.avatar_url}
|
||||||
alt={account.name || ''}
|
alt={account.name || ''}
|
||||||
className="bg-muted h-7 w-7 rounded-full border"
|
className="h-8 w-8 shrink-0 rounded-full border border-white/[0.08] bg-white/[0.02] object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-primary/10 text-primary flex h-7 w-7 items-center justify-center rounded-full border text-xs font-semibold">
|
<div className="text-muted-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-xs font-semibold">
|
||||||
{account.name?.[0]?.toUpperCase() || 'A'}
|
{account.name?.[0]?.toUpperCase() || 'A'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="mb-0.5 flex items-center gap-2">
|
||||||
<span className="truncate text-sm font-semibold">
|
<span className="text-foreground truncate text-[13px] leading-none font-semibold">
|
||||||
{account.name || t('cloud.card.unknown')}
|
{account.name || t('cloud.card.unknown')}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -678,59 +666,62 @@ export function CompactCloudAccountCard({
|
|||||||
? 'destructive'
|
? 'destructive'
|
||||||
: 'outline'
|
: 'outline'
|
||||||
}
|
}
|
||||||
className="shrink-0 text-[10px]"
|
className="h-4 shrink-0 border-white/[0.1] bg-white/[0.02] px-1.5 py-0 text-[9px] uppercase"
|
||||||
>
|
>
|
||||||
{account.provider.toUpperCase()}
|
{account.provider}
|
||||||
</Badge>
|
</Badge>
|
||||||
{account.is_active && (
|
{account.is_active && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="default"
|
variant="default"
|
||||||
className="shrink-0 bg-green-500 text-[10px] hover:bg-green-600"
|
className="h-4 shrink-0 border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0 text-[9px] text-emerald-400 uppercase"
|
||||||
>
|
>
|
||||||
{t('cloud.card.active')}
|
{t('cloud.card.active')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground flex items-center gap-3 text-xs">
|
<div className="text-muted-foreground/80 flex items-center gap-3 text-[11px] font-medium">
|
||||||
<span className="truncate">{account.email}</span>
|
<span className="truncate">{account.email}</span>
|
||||||
{validationBlockedStatusLabel && (
|
{validationBlockedStatusLabel && (
|
||||||
<span className="text-destructive shrink-0 font-medium">{validationBlockedStatusLabel}</span>
|
<span className="text-destructive shrink-0">{validationBlockedStatusLabel}</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowAiCredits && aiCredits && (
|
{shouldShowAiCredits && aiCredits && (
|
||||||
<span className="shrink-0 text-amber-500">
|
<span className="flex shrink-0 items-center gap-1.5 text-blue-400">
|
||||||
${aiCredits.credits.toFixed(2)}
|
<span>
|
||||||
|
{t('cloud.card.aiCreditsValue', {
|
||||||
|
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
{aiCredits.expiryDate && (
|
{aiCredits.expiryDate && (
|
||||||
<span className="text-muted-foreground">
|
<>
|
||||||
{' '}
|
<span className="h-2.5 w-[1px] bg-blue-500/30"></span>
|
||||||
·{' '}
|
<span className="text-blue-400/70">
|
||||||
{t('cloud.card.creditsExpiry', {
|
{t('cloud.card.creditsExpiry', {
|
||||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{compactModels.length > 0 && (
|
{compactModels.length > 0 && (
|
||||||
<div className="mt-1 flex items-center gap-1">
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
{compactModels.map(([modelName, info]) => (
|
{compactModels.map(([modelName, info]) => (
|
||||||
<TooltipProvider key={modelName}>
|
<TooltipProvider key={modelName}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="bg-muted h-1.5 w-12 overflow-hidden rounded-full">
|
<div className="h-1.5 w-10 overflow-hidden rounded-full bg-white/[0.04]">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(info.percentage)}`}
|
className={`h-full rounded-full transition-all duration-300 ease-out ${getQuotaBarColorClass(info.percentage)}`}
|
||||||
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="text-xs font-medium">
|
||||||
<p className="text-xs">
|
{formatModelDisplayName(modelName)}: {info.percentage}%
|
||||||
{formatModelDisplayName(modelName)}: {info.percentage}%
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -743,29 +734,36 @@ export function CompactCloudAccountCard({
|
|||||||
{allModelEntries.length > 0 && (
|
{allModelEntries.length > 0 && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer rounded-full">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground/70 hover:text-foreground h-8 w-8"
|
||||||
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const hiddenCount = allModelEntries.filter(
|
const hiddenCount = allModelEntries.filter(
|
||||||
([modelName]) => config?.model_visibility?.[modelName] === false,
|
([modelName]) => config?.model_visibility?.[modelName] === false,
|
||||||
).length;
|
).length;
|
||||||
return hiddenCount > 0 ? (
|
return hiddenCount > 0 ? (
|
||||||
<EyeOff className="text-muted-foreground h-3.5 w-3.5" />
|
<EyeOff className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<Eye className="h-3.5 w-3.5" />
|
<Eye className="h-4 w-4" />
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-64" align="end">
|
<DropdownMenuContent className="w-56" align="end">
|
||||||
<DropdownMenuLabel>{t('cloud.card.modelVisibility')}</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-xs">
|
||||||
<div className="max-h-64 overflow-auto px-2 py-1">
|
{t('cloud.card.modelVisibility')}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="max-h-[250px] overflow-y-auto p-1">
|
||||||
{allModelEntries.map(([modelName]) => {
|
{allModelEntries.map(([modelName]) => {
|
||||||
const isVisible = config?.model_visibility?.[modelName] !== false;
|
const isVisible = config?.model_visibility?.[modelName] !== false;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={modelName}
|
key={modelName}
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
className="flex cursor-pointer items-center gap-2"
|
className="flex cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isVisible}
|
checked={isVisible}
|
||||||
@@ -776,8 +774,9 @@ export function CompactCloudAccountCard({
|
|||||||
saveConfig({ ...config, model_visibility: newVisibility });
|
saveConfig({ ...config, model_visibility: newVisibility });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-xs" title={modelName}>
|
<span className="truncate text-xs font-medium" title={modelName}>
|
||||||
{formatModelDisplayName(modelName)}
|
{formatModelDisplayName(modelName)}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -789,27 +788,27 @@ export function CompactCloudAccountCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{account.is_active ? (
|
{account.is_active ? (
|
||||||
|
<div className="flex h-8 items-center px-2">
|
||||||
|
<span className="flex items-center gap-1.5 text-[11px] font-bold text-emerald-400">
|
||||||
|
<span className="relative flex h-1.5 w-1.5">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
|
||||||
|
</span>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled
|
|
||||||
className="h-7 text-xs text-green-600 opacity-100"
|
|
||||||
>
|
|
||||||
<Power className="mr-1 h-3 w-3" />
|
|
||||||
{t('cloud.card.active')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onSwitch(account.id)}
|
onClick={() => onSwitch(account.id)}
|
||||||
disabled={isSwitching}
|
disabled={isSwitching}
|
||||||
className="h-7 cursor-pointer text-xs"
|
className="hover:bg-primary/10 hover:text-primary h-8 cursor-pointer px-3 text-xs font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{isSwitching ? (
|
{isSwitching ? (
|
||||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
<RefreshCw className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Power className="mr-1 h-3 w-3" />
|
<Power className="mr-1.5 h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
{t('cloud.card.use')}
|
{t('cloud.card.use')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -817,35 +816,47 @@ export function CompactCloudAccountCard({
|
|||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer rounded-full">
|
<Button
|
||||||
<MoreVertical className="h-3.5 w-3.5" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground/70 hover:text-foreground h-8 w-8"
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
<span className="sr-only">Menu</span>
|
<span className="sr-only">Menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuLabel>{t('cloud.card.actions')}</DropdownMenuLabel>
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={() => onSwitch(account.id)} disabled={isSwitching}>
|
onClick={() => onSwitch(account.id)}
|
||||||
|
disabled={isSwitching}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<Power className="mr-2 h-4 w-4" />
|
<Power className="mr-2 h-4 w-4" />
|
||||||
{t('cloud.card.useAccount')}
|
<span className="font-medium">{t('cloud.card.useAccount')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={() => onRefresh(account.id)} disabled={isRefreshing}>
|
onClick={() => onRefresh(account.id)}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
{t('cloud.card.refresh')}
|
<span>{t('cloud.card.refresh')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={() => onManageIdentity(account.id)}>
|
onClick={() => onManageIdentity(account.id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<Fingerprint className="mr-2 h-4 w-4" />
|
<Fingerprint className="mr-2 h-4 w-4" />
|
||||||
{t('cloud.card.identityProfile')}
|
<span>{t('cloud.card.identityProfile')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onDelete(account.id)}
|
onClick={() => onDelete(account.id)}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:bg-destructive/10 focus:text-destructive cursor-pointer"
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
<Trash className="mr-2 h-4 w-4" />
|
<Trash className="mr-2 h-4 w-4" />
|
||||||
{t('cloud.card.delete')}
|
<span>{t('cloud.card.delete')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type JSX, useEffect, useMemo, useRef, useState } from 'react';
|
import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
@@ -48,11 +48,14 @@ function formatError(error: unknown): string {
|
|||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProfile(profile?: DeviceProfile): JSX.Element {
|
function renderProfile(
|
||||||
|
profile: DeviceProfile | undefined,
|
||||||
|
t: ReturnType<typeof useTranslation>['t'],
|
||||||
|
): JSX.Element {
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return (
|
return (
|
||||||
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
|
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
|
||||||
N/A
|
{t('common.notAvailable')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,41 +98,46 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
const history = useMemo(() => sortHistory(snapshot?.history || []), [snapshot?.history]);
|
const history = useMemo(() => sortHistory(snapshot?.history || []), [snapshot?.history]);
|
||||||
const showLoadingPlaceholder = initialLoading && !snapshot;
|
const showLoadingPlaceholder = initialLoading && !snapshot;
|
||||||
|
|
||||||
const refreshProfiles = async (options?: { silent?: boolean }) => {
|
const refreshProfiles = useCallback(
|
||||||
if (!account) {
|
async (options?: { silent?: boolean }) => {
|
||||||
setSnapshot(null);
|
if (!account) {
|
||||||
return;
|
setSnapshot(null);
|
||||||
}
|
return;
|
||||||
const silent = options?.silent === true;
|
|
||||||
if (silent) {
|
|
||||||
setRefreshing(true);
|
|
||||||
} else {
|
|
||||||
setInitialLoading(true);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await getCloudIdentityProfiles({ accountId: account.id });
|
|
||||||
setSnapshot(result);
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
|
|
||||||
description: formatError(error),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
if (silent) {
|
|
||||||
setRefreshing(false);
|
|
||||||
} else {
|
|
||||||
setInitialLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
const silent = options?.silent === true;
|
||||||
};
|
if (silent) {
|
||||||
|
setRefreshing(true);
|
||||||
|
} else {
|
||||||
|
setInitialLoading(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await getCloudIdentityProfiles({ accountId: account.id });
|
||||||
|
setSnapshot(result);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t('cloud.toast.actionFailed'),
|
||||||
|
description: formatError(error),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (silent) {
|
||||||
|
setRefreshing(false);
|
||||||
|
} else {
|
||||||
|
setInitialLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[account, t, toast],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
refreshProfiles();
|
setTimeout(() => {
|
||||||
}, [open, account?.id]);
|
void refreshProfiles();
|
||||||
|
}, 0);
|
||||||
|
}, [open, refreshProfiles]);
|
||||||
|
|
||||||
const runAction = async (key: string, action: () => Promise<void>) => {
|
const runAction = async (key: string, action: () => Promise<void>) => {
|
||||||
if (actionLockRef.current) {
|
if (actionLockRef.current) {
|
||||||
@@ -142,7 +150,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
await refreshProfiles({ silent: true });
|
await refreshProfiles({ silent: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
|
title: t('cloud.toast.actionFailed'),
|
||||||
description: formatError(error),
|
description: formatError(error),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
@@ -247,7 +255,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
<CardTitle className="text-sm">{t('cloud.identity.previewTitle')}</CardTitle>
|
<CardTitle className="text-sm">{t('cloud.identity.previewTitle')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{renderProfile(previewProfile)}
|
{renderProfile(previewProfile, t)}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -295,7 +303,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
{t('cloud.identity.loading')}
|
{t('cloud.identity.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
renderProfile(snapshot?.currentStorage)
|
renderProfile(snapshot?.currentStorage, t)
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -313,7 +321,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
{t('cloud.identity.loading')}
|
{t('cloud.identity.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
renderProfile(snapshot?.boundProfile)
|
renderProfile(snapshot?.boundProfile, t)
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -338,7 +346,10 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
|
|
||||||
{!showLoadingPlaceholder
|
{!showLoadingPlaceholder
|
||||||
? history.map((version) => (
|
? history.map((version) => (
|
||||||
<div key={version.id} className="bg-muted/20 rounded-xl border p-3">
|
<div
|
||||||
|
key={version.id}
|
||||||
|
className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-3"
|
||||||
|
>
|
||||||
<div className="mb-2 flex items-center justify-between gap-3">
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-medium">{version.label}</div>
|
<div className="truncate text-sm font-medium">{version.label}</div>
|
||||||
@@ -353,7 +364,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderProfile(version.profile)}
|
{renderProfile(version.profile, t)}
|
||||||
|
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -402,15 +413,13 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
|||||||
|
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-sm">
|
<CardTitle className="text-sm">{t('cloud.identity.baseline')}</CardTitle>
|
||||||
{t('cloud.identity.baseline', { defaultValue: 'Reference Profile' })}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{showLoadingPlaceholder ? (
|
{showLoadingPlaceholder ? (
|
||||||
<div className="text-muted-foreground text-xs">{t('cloud.identity.loading')}</div>
|
<div className="text-muted-foreground text-xs">{t('cloud.identity.loading')}</div>
|
||||||
) : (
|
) : (
|
||||||
renderProfile(snapshot?.baseline)
|
renderProfile(snapshot?.baseline, t)
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||