Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ jobs:
- uses: actions/checkout@v4
- name: Run check-consistency.sh
run: bash scripts/check-consistency.sh
- name: Validate self-host Compose
run: |
bash scripts/selfhost.sh init
test "$(stat --format=%a infra/.env.selfhost)" = "600"
! grep -Eq '__[A-Z_]+__' infra/.env.selfhost
ENV_FILE=.env.selfhost docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost config --quiet
rm infra/.env.selfhost

selfhost-smoke:
name: self-host · build + health smoke
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Initialize self-host environment
run: |
bash scripts/selfhost.sh init
test "$(stat --format=%a infra/.env.selfhost)" = "600"
! grep -Eq '__[A-Z_]+__' infra/.env.selfhost
- name: Build and start self-host stack
run: |
docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost build
docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost up -d --wait --wait-timeout 600
- name: Create self-host user
run: |
printf 'smoke-password' | docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost run --rm -T paper \
uv run python scripts/create_user.py --email smoke@example.invalid --subject console:smoke --password-stdin
- name: Diagnose failed self-host smoke
if: failure()
run: |
docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost ps
docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost logs
- name: Remove self-host stack
if: always()
run: docker compose -f infra/docker-compose.prod.yml -f infra/docker-compose.selfhost.yml --env-file infra/.env.selfhost down -v --remove-orphans

orchestration-typecheck:
name: orchestration · typecheck + test
Expand All @@ -47,7 +82,7 @@ jobs:
run: pnpm test

web-typecheck:
name: web · typecheck + test + build
name: web · typecheck + build
runs-on: ubuntu-latest
defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Cargo.lock
.env.local
.env.*.local
.env.prod
infra/.env.selfhost
*.pem
*.key

Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,41 @@ Where each capability stands today. Live module inventory and the end-to-end dec

## Quick Start

### Docker self-hosting (recommended)

This is the supported Docker path for a personal server or local machine. It builds the complete stack locally: PostgreSQL/TimescaleDB, Redis, migrations, data, paper, research, factor, Mastra, and the Operator Console.

Prerequisites: Docker Engine with Docker Compose v2, Git, and OpenSSL. Clone the repository, then initialize the local-only environment file and start the stack:

```bash
git clone https://github.com/mirror29/inalpha.git
cd inalpha
bash scripts/selfhost.sh init
bash scripts/selfhost.sh up
```

The console is available only on the host at <http://127.0.0.1:3001>. Wait until `bash scripts/selfhost.sh status` shows the application services healthy, then create the first login account. The prompt reads the password without echoing it or placing it in shell history:

```bash
bash scripts/selfhost.sh create-user --email you@example.com
```

Sign in at <http://127.0.0.1:3001>, open **LLM Settings**, and add your provider, model, and personal API key. Every authenticated user supplies their own key; it is encrypted in the database with `LLM_CONFIG_ENCRYPTION_KEY`. Do not add provider API keys to `infra/.env.selfhost`: authenticated production mode deliberately has no shared system-key fallback.

Useful operations:

```bash
bash scripts/selfhost.sh logs [service]
bash scripts/selfhost.sh status
bash scripts/selfhost.sh down
```

#### Public deployment

The self-host Compose file intentionally exposes only `127.0.0.1:3001`. For remote access, place your own TLS-terminating Caddy, Nginx, or Cloudflare Tunnel in front of the Dashboard and proxy only that address. Never publish PostgreSQL, Redis, Mastra, or the Python service ports, and do not expose the login or API-key settings page over bare HTTP.

### Local development

### 1 · Install dependencies

```bash
Expand Down
35 changes: 35 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,41 @@ Inalpha 把*调度*和*算力*分开:agent runtime 负责扇出网格、聚合

## Quick Start

### Docker 自托管(推荐)

这是个人电脑或个人服务器的完整 Docker 启动路径:本地构建 PostgreSQL/TimescaleDB、Redis、迁移、data、paper、research、factor、Mastra 和操作者控制台。

前置条件:Docker Engine(含 Compose v2)、Git 与 OpenSSL。克隆仓库后,初始化仅保留在本机的环境文件,再启动全栈:

```bash
git clone https://github.com/mirror29/inalpha.git
cd inalpha
bash scripts/selfhost.sh init
bash scripts/selfhost.sh up
```

控制台只绑定在宿主机 <http://127.0.0.1:3001>。等待 `bash scripts/selfhost.sh status` 显示应用服务 healthy,再创建第一个登录账号;命令会隐藏密码输入,不会把密码写进 shell history:

```bash
bash scripts/selfhost.sh create-user --email you@example.com
```

在 <http://127.0.0.1:3001> 登录,打开 **LLM Settings**,填写你的 provider、model 与个人 API key。每位已认证用户各自提供 key,控制台用 `LLM_CONFIG_ENCRYPTION_KEY` 加密后写入数据库。不要把 provider API key 填进 `infra/.env.selfhost`:认证生产模式刻意不支持共享系统 key fallback。

常用操作:

```bash
bash scripts/selfhost.sh logs [service]
bash scripts/selfhost.sh status
bash scripts/selfhost.sh down
```

#### 公网访问

self-host Compose 故意只暴露 `127.0.0.1:3001`。需要远程访问时,用你自己的 Caddy、Nginx 或 Cloudflare Tunnel 为 Dashboard 做 TLS 终结,并且只代理该地址。不要公开 PostgreSQL、Redis、Mastra 或任何 Python service 端口;登录页和 API key 配置页不能通过裸 HTTP 暴露。

### 本地开发

### 1 · 安装依赖

```bash
Expand Down
36 changes: 36 additions & 0 deletions apps/dashboard/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,42 @@
"none": "—",
"truncated": "Showing the latest {n} — earlier ones aren't listed here."
},
"llm": {
"title": "LLM Settings",
"loading": "Loading…",
"loadFailed": "Could not load settings: {error}",
"noConfigs": "No configuration yet. Add an API key to start.",
"active": "Active",
"model": "Model: {model}",
"defaultModel": "default",
"key": "Key: {key}",
"delete": "Delete",
"provider": "Provider",
"customEndpoint": "Custom endpoint URL",
"customName": "Custom name",
"customNamePlaceholder": "My provider",
"customProvider": "Custom endpoint",
"modelOptional": "Model (optional)",
"modelPlaceholder": "Leave blank to use the default",
"apiKey": "API Key *",
"saving": "Saving…",
"save": "Save",
"cancel": "Cancel",
"addConfig": "Add configuration",
"saveFailed": "Could not save configuration",
"saveMissingId": "Could not save configuration: no configuration ID returned",
"saveActivationFailed": "Configuration was saved, but could not be activated",
"saved": "Configuration saved",
"activated": "Configuration activated",
"activationFailed": "Could not activate configuration",
"deleted": "Configuration deleted",
"deleteFailed": "Could not delete configuration",
"confirmDelete": "Delete configuration?",
"deleteDescription": "Delete the configuration for {provider}? This cannot be undone.",
"deleting": "Deleting…",
"sidebarEntry": "LLM Settings",
"invalidKey": "The active LLM API key is invalid or temporarily unavailable. Check the configuration and retry."
},
"chat": {
"title": "Agent",
"online": "online",
Expand Down
36 changes: 36 additions & 0 deletions apps/dashboard/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,42 @@
"none": "—",
"truncated": "仅显示最近 {n} 条 —— 更早的未在此列出。"
},
"llm": {
"title": "LLM 配置",
"loading": "加载中…",
"loadFailed": "加载失败:{error}",
"noConfigs": "暂无配置,请添加 API Key",
"active": "当前",
"model": "模型:{model}",
"defaultModel": "默认",
"key": "密钥:{key}",
"delete": "删除",
"provider": "供应商",
"customEndpoint": "自定义端点 URL",
"customName": "自定义名称",
"customNamePlaceholder": "我的供应商",
"customProvider": "自定义端点",
"modelOptional": "模型(可选)",
"modelPlaceholder": "留空使用默认",
"apiKey": "API Key *",
"saving": "保存中…",
"save": "保存",
"cancel": "取消",
"addConfig": "新增配置",
"saveFailed": "保存失败",
"saveMissingId": "保存失败:未返回配置 ID",
"saveActivationFailed": "配置已保存,但激活失败",
"saved": "配置已保存",
"activated": "已切换配置",
"activationFailed": "切换失败",
"deleted": "配置已删除",
"deleteFailed": "删除失败",
"confirmDelete": "确认删除",
"deleteDescription": "确定要删除配置 {provider} 吗?此操作无法撤销。",
"deleting": "删除中…",
"sidebarEntry": "LLM 配置",
"invalidKey": "当前 LLM API Key 无效或暂时不可用,请检查配置后重试"
},
"chat": {
"title": "Agent",
"online": "在线",
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { notFound } from "next/navigation";

import { ActivityFooter } from "@/components/activity/ActivityFooter";
import { ConsoleChat } from "@/components/chat/ConsoleChat";
import { LLMConfigGate } from "@/components/llm/LLMConfigGate";
import { ConsoleSidebar } from "@/components/shell/ConsoleSidebar";
import { Toaster } from "@/components/ui/sonner";
import { routing } from "@/i18n/routing";
Expand Down Expand Up @@ -42,6 +43,7 @@ export default async function LocaleLayout({
</main>
{/* 内嵌 agent 对话栏 —— 常驻 layout,切面切换不丢对话(见 ConsoleChat)。 */}
<ConsoleChat />
<LLMConfigGate />
{/* 常驻底部活动日志(终端风)—— 随时可回溯 agent 跨模块活动(见 ActivityFooter)。 */}
<ActivityFooter />
</div>
Expand Down
5 changes: 3 additions & 2 deletions apps/dashboard/src/components/chat/ChatThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function ChatThread({
onSwitchThread: (id: string) => void;
}) {
const t = useTranslations("chat");
const tLlm = useTranslations("llm");

const hook = useCopilotChatInternal();
const messages = (hook.messages ?? []) as unknown as AGMessage[];
Expand Down Expand Up @@ -91,14 +92,14 @@ export function ChatThread({
if (url.includes("/api/copilotkit") && res.status === 428) {
window.dispatchEvent(new CustomEvent("inalpha:open-llm-settings"));
} else if (url.includes("/api/copilotkit") && res.status === 401) {
setChatError("当前 LLM API Key 无效或暂时不可用,请检查配置后重试");
setChatError(tLlm("invalidKey"));
}
return res;
};
(patched as { __inalphaLLMCheck?: boolean }).__inalphaLLMCheck = true;
window.fetch = patched;
return () => { if (window.fetch === patched) window.fetch = orig; };
}, []);
}, [tLlm]);

const loadedThreadRef = useRef<string | null>(null);
const setMessagesRef = useRef(setMessages);
Expand Down
44 changes: 44 additions & 0 deletions apps/dashboard/src/components/llm/LLMConfigGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useEffect, useState } from "react";

import { LLMConfigModal } from "./LLMConfigModal";

const LS_DISMISSED = "inalpha-llm-config-dismissed";

/** 在所有控制台页面管理用户 LLM 配置入口。 */
export function LLMConfigGate() {
const [open, setOpen] = useState(false);

useEffect(() => {
const openSettings = () => setOpen(true);
window.addEventListener("inalpha:open-llm-settings", openSettings);

let mounted = true;
if (!localStorage.getItem(LS_DISMISSED)) {
void fetch("/api/user/settings")
.then((response) => response.ok ? response.json() : null)
.then((settings) => {
if (mounted && settings && (!settings.configs || settings.configs.length === 0)) {
setOpen(true);
}
})
.catch(() => {});
}

return () => {
mounted = false;
window.removeEventListener("inalpha:open-llm-settings", openSettings);
};
}, []);

return (
<LLMConfigModal
open={open}
onClose={() => {
setOpen(false);
localStorage.setItem(LS_DISMISSED, "1");
}}
/>
);
}
Loading
Loading