From ac1c32f09c7490239fb0833961cc77eb978a3543 Mon Sep 17 00:00:00 2001 From: "Raka (SDI Head)" Date: Wed, 1 Apr 2026 04:46:32 +0000 Subject: [PATCH 1/5] feat(ui): add anthropic provider logo and OAuth token field - Add anthropic.png logo to model-providers assets - Register AnthropicPng in assets/png/index.ts - Add anthropic entry to MODEL_PROVIDER_ICONS - Add conditional form in model-detail.tsx: - Anthropic: shows OAuth Token field (ANTHROPIC_AUTH_TOKEN) - Other providers: shows standard API key field - Hide API Host field for Anthropic provider --- .../components/models/model-detail.tsx | 328 ++++++++++++------ frontend/src/assets/png/index.ts | 1 + .../assets/png/model-providers/anthropic.png | Bin 0 -> 4591 bytes frontend/src/constants/icons.ts | 2 + 4 files changed, 216 insertions(+), 115 deletions(-) create mode 100644 frontend/src/assets/png/model-providers/anthropic.png diff --git a/frontend/src/app/setting/components/models/model-detail.tsx b/frontend/src/app/setting/components/models/model-detail.tsx index 4a9d38cc..f64e8b55 100644 --- a/frontend/src/app/setting/components/models/model-detail.tsx +++ b/frontend/src/app/setting/components/models/model-detail.tsx @@ -41,6 +41,7 @@ import LinkButton from "@/components/valuecell/button/link-button"; const configSchema = z.object({ api_key: z.string(), base_url: z.string(), + auth_token: z.string().optional(), }); const addModelSchema = z.object({ @@ -76,10 +77,13 @@ export function ModelDetail({ provider }: ModelDetailProps) { const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [showApiKey, setShowApiKey] = useState(false); + const isAnthropicProvider = provider === "anthropic"; + const configForm = useForm({ defaultValues: { api_key: "", base_url: "", + auth_token: "", }, validators: { onSubmit: configSchema, @@ -88,8 +92,9 @@ export function ModelDetail({ provider }: ModelDetailProps) { if (!provider) return; updateConfig({ provider, - api_key: value.api_key, + api_key: isAnthropicProvider ? "" : value.api_key, base_url: value.base_url, + ...(isAnthropicProvider && { auth_token: value.auth_token }), }); }, }); @@ -179,124 +184,217 @@ export function ModelDetail({ provider }: ModelDetailProps) {
- - {(field) => ( - - - {t("settings.models.apiKey")} - -
- - field.handleChange(e.target.value)} - onBlur={() => configForm.handleSubmit()} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.currentTarget.blur(); - } + {isAnthropicProvider ? ( + /* Anthropic OAuth Token field */ + + {(field) => ( + + + OAuth Token + +
+ + field.handleChange(e.target.value)} + onBlur={() => configForm.handleSubmit()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }} + /> + + setShowApiKey(!showApiKey)} + aria-label={ + showApiKey + ? t("settings.models.hidePassword") + : t("settings.models.showPassword") + } + > + {showApiKey ? ( + + ) : ( + + )} + + + + +
+ {checkResult?.data && ( +
+ {checkResult.data.ok ? ( + + {t("settings.models.available")} + + ) : ( + + {t("settings.models.unavailable")} + {checkResult.data.error + ? `: ${checkResult.data.error}` + : ""} + + )} +
+ )} +

+ OpenClaw / Claude Code OAuth token. Set via{" "} + ANTHROPIC_AUTH_TOKEN{" "} + environment variable. +

+ +
+ )} +
+ ) : ( + /* Standard API Key field for other providers */ + + {(field) => ( + + - {checkingAvailability - ? t("settings.models.waitingForCheck") - : t("settings.models.checkAvailability")} - -
- {checkResult?.data && ( -
- {checkResult.data.ok ? ( - - {t("settings.models.available")} - {checkResult.data.status - ? ` (${checkResult.data.status})` - : ""} - - ) : ( - - {t("settings.models.unavailable")} - {checkResult.data.status - ? ` (${checkResult.data.status})` - : ""} - {checkResult.data.error - ? `: ${checkResult.data.error}` - : ""} - - )} + {t("settings.models.apiKey")} + +
+ + field.handleChange(e.target.value)} + onBlur={() => configForm.handleSubmit()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }} + /> + + setShowApiKey(!showApiKey)} + aria-label={ + showApiKey + ? t("settings.models.hidePassword") + : t("settings.models.showPassword") + } + > + {showApiKey ? ( + + ) : ( + + )} + + + + +
- )} - - {t("settings.models.getApiKey")} - - - - )} - + {checkResult?.data && ( +
+ {checkResult.data.ok ? ( + + {t("settings.models.available")} + {checkResult.data.status + ? ` (${checkResult.data.status})` + : ""} + + ) : ( + + {t("settings.models.unavailable")} + {checkResult.data.status + ? ` (${checkResult.data.status})` + : ""} + {checkResult.data.error + ? `: ${checkResult.data.error}` + : ""} + + )} +
+ )} + + {t("settings.models.getApiKey")} + + + + )} + + )} - {/* API Host section */} - - {(field) => ( - - - {t("settings.models.apiHost")} - - field.handleChange(e.target.value)} - onBlur={() => configForm.handleSubmit()} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.currentTarget.blur(); - } - }} - /> - - - )} - + {/* API Host section — hidden for Anthropic */} + {!isAnthropicProvider && ( + + {(field) => ( + + + {t("settings.models.apiHost")} + + field.handleChange(e.target.value)} + onBlur={() => configForm.handleSubmit()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }} + /> + + + )} + + )} {/* Models section */} diff --git a/frontend/src/assets/png/index.ts b/frontend/src/assets/png/index.ts index 4aaea61e..09fc604c 100644 --- a/frontend/src/assets/png/index.ts +++ b/frontend/src/assets/png/index.ts @@ -33,6 +33,7 @@ export { default as IconGroupPng } from "./icon-group.png"; export { default as IconGroupDarkPng } from "./icon-group-dark.png"; export { default as MessageGroupPng } from "./message-group.png"; export { default as MessageGroupDarkPng } from "./message-group-dark.png"; +export { default as AnthropicPng } from "./model-providers/anthropic.png"; export { default as AzurePng } from "./model-providers/azure.png"; export { default as DashScopePng } from "./model-providers/dashscope.png"; export { default as DeepSeekPng } from "./model-providers/deepseek.png"; diff --git a/frontend/src/assets/png/model-providers/anthropic.png b/frontend/src/assets/png/model-providers/anthropic.png new file mode 100644 index 0000000000000000000000000000000000000000..456369e4d9d6b2498d39c900ebad0597ca6905d8 GIT binary patch literal 4591 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 Jsxowa{(sy7i$MSY literal 0 HcmV?d00001 diff --git a/frontend/src/constants/icons.ts b/frontend/src/constants/icons.ts index 25fe155b..e9f9c187 100644 --- a/frontend/src/constants/icons.ts +++ b/frontend/src/constants/icons.ts @@ -1,4 +1,5 @@ import { + AnthropicPng, AzurePng, BinancePng, BlockchainPng, @@ -19,6 +20,7 @@ import { } from "@/assets/png"; export const MODEL_PROVIDER_ICONS = { + anthropic: AnthropicPng, openrouter: OpenRouterPng, siliconflow: SiliconFlowPng, openai: OpenAiPng, From 07394f34aadec3a629e917fa7c794be8322695a1 Mon Sep 17 00:00:00 2001 From: "Raka (SDI Head)" Date: Wed, 1 Apr 2026 04:52:39 +0000 Subject: [PATCH 2/5] feat(ui): hide API key field for Anthropic in trading strategy form - ai-model-form.tsx: conditionally hide API key input when provider=anthropic - Show info box explaining OAuth token is used from server environment - Prevents user confusion when adding trading strategy with Anthropic --- .../valuecell/form/ai-model-form.tsx | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/valuecell/form/ai-model-form.tsx b/frontend/src/components/valuecell/form/ai-model-form.tsx index 1ac510b5..27fe5240 100644 --- a/frontend/src/components/valuecell/form/ai-model-form.tsx +++ b/frontend/src/components/valuecell/form/ai-model-form.tsx @@ -33,6 +33,8 @@ export const AIModelForm = withForm({ refetch: fetchModelProviderDetail, } = useGetModelProviderDetail(provider); + const isAnthropic = provider === "anthropic"; + // Set the default provider once loaded and provider is not yet selected useEffect(() => { if (isLoadingProviders || !defaultProvider) return; @@ -100,14 +102,29 @@ export const AIModelForm = withForm({ }} - - {(field) => ( - - )} - + {/* Hide API key field for Anthropic — uses OAuth token from environment */} + {!isAnthropic && ( + + {(field) => ( + + )} + + )} + + {/* Anthropic: show info about OAuth token */} + {isAnthropic && ( +
+

OAuth Token

+

+ Anthropic uses an OAuth token ( + ANTHROPIC_AUTH_TOKEN) configured + in the server environment. No API key needed here. +

+
+ )} ); }, From 6e9530e959f519f801e1be78a866fb01f7e99946 Mon Sep 17 00:00:00 2001 From: "Raka (SDI Head)" Date: Wed, 1 Apr 2026 04:56:26 +0000 Subject: [PATCH 3/5] fix(i18n): add anthropic and ollama to strategy provider translations --- frontend/src/i18n/locales/en.json | 6 ++++-- frontend/src/i18n/locales/ja.json | 6 ++++-- frontend/src/i18n/locales/zh_CN.json | 6 ++++-- frontend/src/i18n/locales/zh_TW.json | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3eaf87c7..7d70daf1 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -176,7 +176,9 @@ "openai": "OpenAI", "openai-compatible": "OpenAI Compatible API", "openrouter": "OpenRouter", - "siliconflow": "SiliconFlow" + "siliconflow": "SiliconFlow", + "anthropic": "Anthropic", + "ollama": "Ollama" }, "title": "Trading Strategies", "add": "Add trading strategy", @@ -431,4 +433,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index b09f3649..afa70196 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -176,7 +176,9 @@ "openai": "OpenAI", "openai-compatible": "OpenAI互換API", "openrouter": "OpenRouter", - "siliconflow": "SiliconFlow" + "siliconflow": "SiliconFlow", + "anthropic": "Anthropic", + "ollama": "Ollama" }, "title": "取引戦略", "add": "取引戦略を追加", @@ -431,4 +433,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/locales/zh_CN.json b/frontend/src/i18n/locales/zh_CN.json index 36b478ab..10864516 100644 --- a/frontend/src/i18n/locales/zh_CN.json +++ b/frontend/src/i18n/locales/zh_CN.json @@ -176,7 +176,9 @@ "openai": "OpenAI", "openai-compatible": "OpenAI兼容API", "openrouter": "OpenRouter", - "siliconflow": "硅基流动" + "siliconflow": "硅基流动", + "anthropic": "Anthropic", + "ollama": "Ollama" }, "title": "交易策略", "add": "添加交易策略", @@ -431,4 +433,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/locales/zh_TW.json b/frontend/src/i18n/locales/zh_TW.json index 32247aba..5427041c 100644 --- a/frontend/src/i18n/locales/zh_TW.json +++ b/frontend/src/i18n/locales/zh_TW.json @@ -176,7 +176,9 @@ "openai": "OpenAI", "openai-compatible": "OpenAI相容API", "openrouter": "OpenRouter", - "siliconflow": "SiliconFlow" + "siliconflow": "SiliconFlow", + "anthropic": "Anthropic", + "ollama": "Ollama" }, "title": "交易策略", "add": "新增交易策略", @@ -431,4 +433,4 @@ } } } -} +} \ No newline at end of file From 2ed8eb076c3ebd327fe3288dc76c2b970c011ed1 Mon Sep 17 00:00:00 2001 From: "Raka (SDI Head)" Date: Wed, 1 Apr 2026 05:10:47 +0000 Subject: [PATCH 4/5] fix: anthropic save OAuth token + fix check availability Backend: - ProviderUpdateRequest: add auth_token field - update_provider_config: save auth_token via auth_token_env - check_model: Anthropic uses direct SDK call with OAuth headers Frontend: - api/setting.ts: pass auth_token in updateConfig mutation --- frontend/src/api/setting.ts | 2 + python/valuecell/server/api/routers/models.py | 40 +++++++++++++++++++ python/valuecell/server/api/schemas/model.py | 3 ++ 3 files changed, 45 insertions(+) diff --git a/frontend/src/api/setting.ts b/frontend/src/api/setting.ts index fc179de5..2574729a 100644 --- a/frontend/src/api/setting.ts +++ b/frontend/src/api/setting.ts @@ -70,12 +70,14 @@ export const useUpdateProviderConfig = () => { provider: string; api_key?: string; base_url?: string; + auth_token?: string; }) => apiClient.put>( `/models/providers/${params.provider}/config`, { api_key: params.api_key, base_url: params.base_url, + auth_token: params.auth_token, }, ), onSuccess: (_data, variables) => { diff --git a/python/valuecell/server/api/routers/models.py b/python/valuecell/server/api/routers/models.py index ec7797f6..467f0bbc 100644 --- a/python/valuecell/server/api/routers/models.py +++ b/python/valuecell/server/api/routers/models.py @@ -228,6 +228,11 @@ async def update_provider_config( api_key_env = connection.get("api_key_env") endpoint_env = connection.get("endpoint_env") + # Update OAuth token via env var (e.g. Anthropic) + auth_token_env = connection.get("auth_token_env") + if auth_token_env and (payload.auth_token is not None): + _set_env(auth_token_env, payload.auth_token) + # Update API key via env var # Accept empty string as a deliberate clear; skip only when field is omitted if api_key_env and (payload.api_key is not None): @@ -531,6 +536,41 @@ async def check_model( result.error = f"Runtime dependency missing: {e}" return SuccessResponse.create(data=result, msg="Live check failed") + # Anthropic: check via SDK directly using OAuth token + if provider == "anthropic": + auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN", "").strip() + if not auth_token: + result.ok = False + result.status = "auth_missing" + result.error = "ANTHROPIC_AUTH_TOKEN not set in environment" + return SuccessResponse.create(data=result, msg="Auth missing") + try: + import anthropic as _anthropic + _client = _anthropic.Anthropic( + api_key=None, + auth_token=auth_token, + default_headers={ + "accept": "application/json", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-beta": "claude-code-20250219,oauth-2025-04-20", + "user-agent": "claude-code/2.1.75", + "x-app": "cli", + }, + ) + _client.messages.create( + model=model_id, + max_tokens=1, + messages=[{"role": "user", "content": "ping"}], + ) + result.ok = True + result.status = "reachable" + return SuccessResponse.create(data=result, msg="Model reachable") + except Exception as _e: + result.ok = False + result.status = "request_failed" + result.error = str(_e)[:200] + return SuccessResponse.create(data=result, msg="Check failed") + # Prefer a direct minimal request for OpenAI-compatible providers. # This avoids hidden fallbacks and validates API key/auth. api_key = (payload.api_key or cfg.api_key or "").strip() diff --git a/python/valuecell/server/api/schemas/model.py b/python/valuecell/server/api/schemas/model.py index 6a72b426..96cdd30c 100644 --- a/python/valuecell/server/api/schemas/model.py +++ b/python/valuecell/server/api/schemas/model.py @@ -61,6 +61,9 @@ class ProviderUpdateRequest(BaseModel): base_url: Optional[str] = Field( None, description="New API base URL to set for provider" ) + auth_token: Optional[str] = Field( + None, description="OAuth token (e.g. Anthropic Claude Code token)" + ) class AddModelRequest(BaseModel): From 0d347646c67ca5dc4cb946e3aef7193dc178adbf Mon Sep 17 00:00:00 2001 From: "Raka (SDI Head)" Date: Wed, 1 Apr 2026 08:44:31 +0000 Subject: [PATCH 5/5] feat(anthropic): full OAuth token support for Anthropic provider - Add AnthropicProvider to factory with OAuth token + Claude Code headers - Fix validate_provider() to accept auth_token as alternative to api_key - Add Save button to Anthropic OAuth field in settings UI (replace onBlur) - Add auth_token_set field to provider detail API so UI can show saved status - Fix check availability: treat 429 rate_limit as success (token is valid) - Fix token persistence: empty string no longer clears saved auth token - Add Docker Compose setup with volume mount for user config persistence - Add CLAUDE.md project documentation Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 117 ++++++++++++++++++ docker-compose.yml | 62 ++++++++++ docker/DockerFile | 2 +- docker/Dockerfile.frontend | 23 ++++ docker/nginx.frontend.conf | 22 ++++ ecosystem.config.js | 28 +++++ .../components/models/model-detail.tsx | 18 ++- python/configs/providers/openrouter.yaml | 80 ++++-------- python/pyproject.toml | 3 +- python/uv.lock | 30 ++++- python/valuecell/adapters/models/factory.py | 62 ++++++++++ python/valuecell/config/manager.py | 18 ++- python/valuecell/server/api/routers/models.py | 22 +++- python/valuecell/server/api/schemas/model.py | 3 + 14 files changed, 422 insertions(+), 68 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile.frontend create mode 100644 docker/nginx.frontend.conf create mode 100644 ecosystem.config.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b68a28b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is ValueCell + +ValueCell is a community-driven, multi-agent platform for financial applications. It combines a Python FastAPI backend with a React/Tauri frontend to deliver AI-powered stock research, trading strategy automation, and portfolio management via a multi-agent orchestration system. + +## Development Commands + +### Backend (Python) + +```bash +# Install dependencies +uv sync + +# Run backend in dev mode +uv run python -m valuecell.server.main + +# Tests +uv run pytest ./python # All tests +uv run pytest ./python/tests/test_foo.py # Single file +uv run pytest -k "test_name" # Single test by name + +# Lint & format +make lint # ruff check +make format # ruff format + isort +``` + +### Frontend + +```bash +cd frontend +bun install +bun run dev # Dev server (port 1420) +bun run build # Production build +bun run typecheck # Type check (react-router typegen + tsc) +bun run lint # Biome lint +bun run lint:fix # Biome auto-fix +bun run format # Biome format +``` + +### Docker (full stack) + +```bash +docker compose up -d --build # Start everything +docker compose logs -f # Follow logs +``` + +### Quick start (full dev environment) + +```bash +bash start.sh # Linux/macOS — installs tools, syncs deps, starts both servers +``` + +## Architecture Overview + +### Backend Layers + +``` +FastAPI (server/) + └── Orchestrator (core/coordinate/orchestrator.py) + ├── Super Agent (core/super_agent/) — triage: answer OR hand off to Planner + ├── Planner (core/plan/planner.py) — converts intent → ExecutionPlan; triggers HITL + ├── Task Executor (core/task/executor.py) — runs plan tasks via A2A protocol + └── Event Router (core/event/) — maps A2A events → typed responses → UI stream + └── Conversation Store (core/conversation/) — SQLite persistence +Agents (agents/) + ├── research_agent/ — SEC EDGAR-based company analysis + ├── prompt_strategy_agent/ — LLM-driven trading strategies + ├── grid_agent/ — grid trading automation + └── news_agent/ — news retrieval & scheduled delivery +Adapters (adapters/) + ├── Yahoo Finance, AKShare, BaoStock — market data + ├── CCXT — 40+ exchange integrations + └── EDGAR — SEC filing retrieval +Storage + ├── SQLite (aiosqlite/SQLAlchemy async) — conversations, tasks, watchlists + └── LanceDB — vector embeddings +``` + +### Orchestration Flow + +1. **Super Agent** — fast triage; either answers directly or enriches the query and hands off to Planner +2. **Planner** — produces a typed `ExecutionPlan`; detects missing params; blocks for Human-in-the-Loop (HITL) approval/clarification when needed +3. **Task Executor** — executes plan tasks asynchronously via Agent2Agent (A2A) protocol +4. **Event Router** — translates `TaskStatusUpdateEvent` → `BaseResponse` subtypes, annotates with stable `item_id`, streams to UI and persists + +### Frontend Architecture + +- **Framework**: React 19 + React Router 7 + Vite (rolldown) +- **Desktop**: Tauri 2 (cross-platform app wrapper) +- **State**: Zustand stores; TanStack React Query for server sync +- **Forms**: TanStack React Form + Zod +- **UI**: Radix UI headless components + Tailwind CSS 4 + shadcn +- **Charts**: ECharts + TradingView integration +- **i18n**: i18next (en, zh_CN, zh_TW, ja) +- Key entry points: `frontend/src/root.tsx` (routing), `frontend/src/app/agent/chat.tsx` (main chat UI) + +### Configuration System (3-tier priority) + +1. Environment variables (highest) +2. `.env` file +3. `python/configs/*.yaml` files (defaults: `config.yaml`, `providers/`, `agents/`, `agent_cards/`) + +Copy `.env.example` to `.env` and set at least one LLM provider key (e.g. `OPENROUTER_API_KEY`). + +## Code Conventions (from AGENTS.md) + +- **Async-first**: all I/O must be async — use `httpx`, SQLAlchemy async, `anyio` +- **Type hints**: required on all public and internal APIs; prefer Pydantic models over `dict` +- **Imports**: avoid inline imports; use qualified imports for 3+ names from one module +- **Logging**: `loguru` with `{}` placeholders — `logger.info` for key events, `logger.warning` for recoverable errors, `logger.exception` only for unexpected errors +- **Error handling**: catch specific exceptions; max 2 nesting levels +- **Function size**: keep under 200 lines, max 10 parameters (prefer structs) +- **Runtime checks**: prefer Pydantic validation over `getattr`/`hasattr` +- **Python version**: 3.12+; package manager: `uv`; virtual env at `./python/.venv` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9a4c232f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +# ============================================================================= +# Valuecell — Docker Compose +# +# Services: +# backend - Python FastAPI (uvicorn), port 8000 +# frontend - React/Vite (nginx), port 3200 +# +# Usage: +# docker compose up -d --build +# docker compose logs -f +# ============================================================================= + +services: + + # --------------------------------------------------------------------------- + # Backend — Python FastAPI + # --------------------------------------------------------------------------- + backend: + build: + context: ./python + dockerfile: ../docker/DockerFile + image: valuecell-backend:latest + container_name: valuecell_backend + restart: unless-stopped + env_file: + - ./python/.env + environment: + # Skip stdin control thread — stdin is closed in Docker non-interactive mode + # which would cause immediate shutdown via control_loop EOF handler + ENV: local_dev + volumes: + - ./python/configs:/app/configs + - valuecell_userdata:/root/.config/valuecell + ports: + - "8000:8000" + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/system/health')\" || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + # --------------------------------------------------------------------------- + # Frontend — React/Vite (served via Nginx) + # --------------------------------------------------------------------------- + frontend: + build: + context: . + dockerfile: ./docker/Dockerfile.frontend + args: + VITE_API_BASE_URL: "http://10.11.2.150:8000/api/v1" + image: valuecell-frontend:latest + container_name: valuecell_frontend + restart: unless-stopped + ports: + - "3200:80" + depends_on: + backend: + condition: service_healthy + +volumes: + valuecell_userdata: diff --git a/docker/DockerFile b/docker/DockerFile index a854e391..e1543170 100644 --- a/docker/DockerFile +++ b/docker/DockerFile @@ -21,4 +21,4 @@ RUN --mount=type=cache,target=/root/.cache/uv \ EXPOSE 8000 # Run the application. -CMD ["uv", "run", "python", "main.py", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uv", "run", "python", "-m", "valuecell.server.main"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 00000000..6b0fd9ac --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,23 @@ +# ============================================================================= +# Valuecell Frontend — Pre-built Static Serve +# +# Frontend is pre-built on host (bun run build) due to CPU AVX requirement. +# This Dockerfile copies the build output and serves it with Nginx. +# +# To rebuild frontend: +# cd /root/.openclaw/apps/valuecell/frontend +# VITE_API_BASE_URL=http://10.11.2.150:8000/api/v1 bun run build +# docker compose build frontend +# ============================================================================= + +FROM nginx:alpine + +# Copy pre-built frontend assets +COPY frontend/build/client /usr/share/nginx/html + +# Copy nginx config +COPY docker/nginx.frontend.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx.frontend.conf b/docker/nginx.frontend.conf new file mode 100644 index 00000000..21ae68ef --- /dev/null +++ b/docker/nginx.frontend.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # SPA routing — fallback to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 00000000..35fb511c --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,28 @@ +module.exports = { + apps: [ + { + name: 'valuecell-backend', + script: 'uv', + args: 'run python -m valuecell.server.main', + cwd: '/root/.openclaw/apps/valuecell/python', + env: { + NODE_ENV: 'production', + HOST: '0.0.0.0', + PORT: '8000', + }, + interpreter: 'none', + }, + { + name: 'valuecell-frontend', + script: 'bun', + args: 'run start', + cwd: '/root/.openclaw/apps/valuecell/frontend', + env: { + NODE_ENV: 'production', + PORT: '3200', + HOST: '0.0.0.0', + }, + interpreter: 'none', + } + ] +}; diff --git a/frontend/src/app/setting/components/models/model-detail.tsx b/frontend/src/app/setting/components/models/model-detail.tsx index f64e8b55..e2a86ee2 100644 --- a/frontend/src/app/setting/components/models/model-detail.tsx +++ b/frontend/src/app/setting/components/models/model-detail.tsx @@ -203,11 +203,10 @@ export function ModelDetail({ provider }: ModelDetailProps) { placeholder="sk-ant-oat01-..." value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} - onBlur={() => configForm.handleSubmit()} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); - e.currentTarget.blur(); + configForm.handleSubmit(); } }} /> @@ -231,6 +230,16 @@ export function ModelDetail({ provider }: ModelDetailProps) { +