Open Dots 是开源、可自托管的 AI 代理工作台,定位为 OpenAI Dots、Claude Cowork、ChatGPT agent 等付费产品的替代方案。它封装 Claude Code CLI,可在浏览器或 Telegram 中发起编码任务,并在一次性沙箱内执行。写文件、执行命令、Git 推送等高风险操作会先请求人工审批,还支持连接 GitHub、定时提示词、本地对话线程与运行历史。凭据加密存储,数据可存于 PostgreSQL 或 SQLite。早期原型,仅支持 Claude,暂无多代理协作。
./scripts/dev.shOpen Dots is an open-source, self-hosted workspace for running coding-agent tasks, aimed at people exploring paid AI agent products such as OpenAI Dots, Meta Muse, Grok Bot, Instinct, Manus Cue, Claude Cowork, and ChatGPT agent. Today it wraps the Claude Code CLI: run tasks from Telegram or the web, review risky actions before they run, connect GitHub, schedule recurring prompts, and chat with a locally signed-in Claude from a desktop app. It supports Claude only, has named agents but no multi-agent features yet, and is not a feature-for-feature replacement for those services. See docs/ROADMAP.md for what is planned.
Both interfaces use the same API, sandbox, and credentials for each user.
▶ Watch the Open Dots demo on YouTube
Status: Active development. See Known limitations before running it for others.
claude CLI (opt-in; see Local Claude).Open Dots gives individuals and developers an inspectable workspace they can run themselves, with coding tasks, visible approval steps, and local control over the app and its data. The web UI supports guest use without Telegram and can also link to a Telegram identity. Read Known limitations before running it for others.
DATABASE_BACKEND below)scripts/dev.shBOAT_ORG_ID below) — otherwise sandbox creation bills
the personal account and returns 402 Payment Required.scripts/dev.sh creates backend/.env from the example on first run, saves a
private encryption key and webhook token, and preserves existing secrets on
later runs. It also selects a trusted CA bundle for outbound HTTPS. Set
SSL_CERT_FILE in your shell or in backend/.env to use a custom PEM bundle;
certificate and hostname verification remain enabled. When Boat is configured,
the script checks its TLS certificate before starting services, without sending
credentials or creating a sandbox. You can start the API and web UI without a
Boat key; sandbox features then remain unavailable.
For manual setup, copy the example yourself:
cp backend/.env.example backend/.env
Fill in backend/.env:
| Var | Required | Notes |
|---|---|---|
BOAT_API_KEY | no (optional provider) | From the Boat dashboard. Leave empty to run without Boat (for example with only local Claude); anything that needs a Boat sandbox then answers "Boat is not configured". Without Boat the app is chat-only unless you enable local tools (LOCAL_TOOLS_ENABLED, see SETUP.md); schedules, Telegram tasks and GitHub push need Boat. If set, HOOK_TOKEN and PERMISSION_HOOK_BASE_URL are also required. |
BOAT_BASE_URL | no | Defaults to https://boat.dev/api/v1. |
BOAT_ORG_ID | see note above | Team/org id (e.g. team_...) to attach to sandbox creation so it bills the org instead of your personal account. |
PARALLEL_SEARCH_ENABLED | no | Set to true to add free Parallel web search and page fetching to Claude tasks. Defaults to false. See Optional web search. |
TOKEN_ENCRYPTION_KEYS | generated by dev.sh | Persistent Fernet key used to encrypt stored credentials. For manual setup, generate with uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Comma-separate multiple keys to support rotation. |
TELEGRAM_BOT_TOKEN | only for the bot / Telegram linking | From @BotFather. dev.sh skips the bot when this or the Boat key is missing. |
TELEGRAM_BOT_USERNAME | no | Your bot's @username (no @), shown as a clickable link in the web UI. Cosmetic only. |
FRONTEND_ORIGINS | no | Comma-separated origins allowed to call the API from a browser. Defaults to http://localhost:3000. |
HOOK_TOKEN | generated by dev.sh | Shared secret the permission hook's caller (the sandbox) must present, so only your backend's own sandboxes can submit permission decisions. For manual setup, generate with python -c "import secrets; print(secrets.token_urlsafe(32))". |
PERMISSION_HOOK_BASE_URL | only with Boat | The public URL from the ngrok-or-similar prerequisite above, no trailing slash — e.g. https://your-tunnel.ngrok-free.app. Baked into each sandbox's .claude/settings.json at task start. |
POSTGRES_* | no | Defaults match backend/docker-compose.yml. |
DATABASE_BACKEND | no | Empty (the default) means PostgreSQL. Set sqlite to use a local SQLite file instead (experimental, no Docker). An existing setup never changes database unless you set this. See docs/SETUP.md. |
SQLITE_PATH | no | SQLite file when DATABASE_BACKEND=sqlite. Defaults to ~/.open-dots/open-dots.db. |
SSL_CERT_FILE | no | Trusted PEM bundle for outbound HTTPS. Detected by dev.sh; set explicitly to add a custom CA. |
The web UI (frontend/) has its own .env.example — scripts/dev.sh copies
it to .env.local automatically on first run. Copy it yourself if you're
running the frontend separately, and point NEXT_PUBLIC_API_BASE_URL at the
backend if you're not using the defaults.
./scripts/dev.sh
From the repo root, this prepares local configuration, starts Postgres via
Docker when PostgreSQL is selected, waits for it to be healthy, installs
backend and frontend dependencies, runs migrations, and
starts the FastAPI app (http://127.0.0.1:8000, docs at /docs), the
Telegram bot when configured, and the web UI (http://127.0.0.1:3000). Without
a Boat key, the API and web UI still start, while sandbox tasks stay unavailable.
Ctrl+C stops everything it started (Postgres keeps running — docker compose -f backend/docker-compose.yml down to stop it too).
Skip pieces you don't need:
./scripts/dev.sh --no-bot # API + web UI only
./scripts/dev.sh --no-frontend # API + bot only
cd backend
docker compose up -d # Postgres
uv sync # install deps
uv run alembic upgrade head # migrations
uv run fastapi dev app/main.py # API (terminal 1)
uv run python -m app.telegram_bot # bot (terminal 2)
cd ../frontend
npm install && npm run dev # web UI (terminal 3)
The Telegram bot walks a user through:
/connect — creates a sandbox and starts an OAuth login for the agent
provider (currently Claude) inside it./connect_github (optional) — links the user's own GitHub account via
device-flow login, so tasks that git push do so as that user rather
than a shared/anonymous identity. No token is ever typed into the chat:
the bot posts a one-time code and a github.com/login/device link, the
user approves it in their browser, and the bot picks up the result on
its own./schedule — repeats a prompt on a cron schedule (hourly/daily/weekdays/
weekly, always in UTC) instead of running it once. /schedules lists
them, /pause//resume//unschedule manage one. Each run is reported
back in the chat — the result on success, the error on failure — and a
schedule that can't succeed (e.g. an expired login) is paused
automatically instead of retrying forever. See Scheduling
tasks below.The web UI covers the same ground from a browser:
/link to the bot, and the server links your signed-in session
to your real Telegram user id instead. (Sign in first: the web UI asks for
the owner passphrase, see docs/AUTH.md.)/connect, driven by REST
endpoints instead of chat messages (get-or-create the sandbox, start
login, paste the code back into the page)./connect_github: the page shows a one-time code and a
github.com/login/device link, and picks up the result on its own once
you approve it — no code to paste back.POST /api/v1/tasks endpoint, polls for the result, and lists past tasks
for that user. Finished tasks can be replied to in place to continue that
same Claude conversation, instead of starting a new unrelated one.
Checking "Repeat this on a schedule" turns the same prompt into a
recurring schedule instead (POST /api/v1/schedules) — pick a frequency
(hourly/daily/weekdays/weekly) and a time, in the browser's own timezone.
Schedules are listed below the composer with pause/resume/delete
controls. See Scheduling tasks.In Telegram:
/start/connect — creates a sandbox (can take a bit) and replies with a login
URL. Open it, complete the OAuth flow, and paste the code back into the
chat./connect_github (optional) — reply has a code and a link; open the link,
enter the code, approve it. No need to send anything back in the chat.create a webpage that says hello — the bot runs
it in your sandbox and replies with the result (or the error)./schedule daily 08:00 generate my report — repeats that prompt every
day at 08:00 UTC. /schedules shows it; /pause 1, /resume 1,
/unschedule 1 manage it by the id the bot gave you. The API process
must be running for it to actually fire — see Scheduling
tasks.In the web UI (http://localhost:3000), after setting the owner passphrase
on first start (or signing in):
/link to the
bot (the page picks this up automatically).Set PARALLEL_SEARCH_ENABLED=true in backend/.env and restart the backend
and bot processes to make Parallel Search MCP
available to new Claude task launches, including replies and scheduled runs.
It connects over HTTP to https://search.parallel.ai/mcp and provides
web_search and web_fetch without a Parallel account or API key. Anonymous
search uses Fast mode and is intended for light use, with rate limits.
For example, submit: “Use Parallel to find the latest Python release, then fetch its release page and summarize the changes.” Approve the MCP tool calls through the existing permission prompts when asked. Claude still needs its usual connected account. The setting is off by default and leaves saved MCP configuration in place.
A schedule (schedule table) is a saved prompt plus a cron expression and
timezone. Three background loops in the API process — not the bot
process — drive it (app/core/background.py, app/services/ schedule_service.py, all on a 30s interval):
poll_due_schedules — claims schedules whose next_run_at has
passed and creates a Tasks row for each occurrence (FOR UPDATE SKIP LOCKED, safe with more than one API instance), then starts it.sync_active_tasks — finishes tasks that have no one polling them,
the way a scheduled run does (nobody's watching a GET /tasks/{id} for
it). Also recovers a task orphaned by a crash mid-launch.report_finished_runs — for each finished scheduled run, reported
exactly once: sends the result on success, the error on failure, and
pauses the schedule if it can't succeed on its own (an expired login,
a missing credential) or after 3 failures in a row. Delivered over
Telegram, to users identified by a numeric (Telegram-linked) user_id
only — a web-only guest sees the outcome in the schedules list instead.Because these loops live in the API process, the API must be running for
a schedule to actually fire — creating one from Telegram doesn't require
the bot process to also run the schedule (it only writes the row), but
nothing fires without uv run fastapi dev app/main.py (or ./scripts/ dev.sh) up.
A schedule created from the web UI uses the browser's own timezone, captured at creation time. One created from Telegram is always UTC, since Telegram gives the bot no way to read the user's timezone — the bot's replies always spell out times in UTC.
Every risky action the AI attempts inside a sandbox — writing a file,
running a shell command, a git push, etc. — is intercepted by a
PermissionRequest hook before it runs, and checked against a per-user
rule table (permission_rule). No matching rule means the task pauses
(status waiting_approval) and you're asked, in whichever channel you're
using:
"Always allow" writes a standing rule so the same action doesn't ask again; "Allow once"/"Deny" apply only to that one attempt. An unanswered prompt times out (110s) to a safe default of deny rather than hanging forever.
Shell (Bash) commands get a further pass: a small classifier
(app/services/github_classifier.py) recognizes git subcommands and
scopes the rule to the specific operation (git status read-only,
git commit a local change, git push a publish) instead of treating every
shell command the same — so "always allow committing" doesn't also silently
allow pushing. Everything else run via Bash, and any tool without its own
classifier, falls back to one coarse bucket per tool.
Extending classification to other connectors (beyond git) is still open;
contributions welcome.
Telegram bot ─┐ ┌── PostgreSQL (default) or SQLite
Next.js web UI├── FastAPI backend ──────┤
Desktop app ──┘ │ └── encrypted credentials
(Electron, ├── Boat sandboxes (optional): tasks, schedules, GitHub push
local Claude) ├── permission prompts and rules (git command classifier)
├── task and schedule services (polling loops in the API process)
└── threads: local `claude` CLI turns, events stored then
streamed over SSE, approvals, resume after restart
Boat sandboxes are optional. Without a Boat key the app is chat-only unless local tools are enabled, and schedules, Telegram tasks and GitHub push are unavailable. The desktop app runs local Claude only. See docs/ARCHITECTURE.md for the target design.
.env).Optionally run prompts through your own signed-in claude CLI on the
machine running the backend, with no Boat sandbox. It is off by default and
only usable from that machine. The answer streams into the page as it is written.
It is chat-only unless you set LOCAL_TOOLS_ENABLED=true, which lets Claude
read and write files in its workspace folder and run commands, each call waiting
for your Allow or Deny in the page (an allowed command runs as you, with no
sandbox). See docs/SETUP.md.
A separate set of endpoints, /api/v1/threads, keeps conversations in the
database so they continue and survive a restart. The Chat page (/chat, linked
as "Open chat" on the home page) is their interface. See
docs/SETUP.md.
An Electron window around the local backend, for local Claude only: from the
desktop/ folder, npm run app (or the steps one at a time: npm run setup,
npm run build, npm run start). It
needs no .env file: it makes and keeps its own encryption key in the
operating system's secret storage, creates its SQLite database, and only its own
window can use the backend. npm run app does the setup and build on the first
run and then opens it. Tagged releases build unsigned installers (Windows NSIS
x64, macOS arm64 DMG) with GitHub Actions; neither has been verified on a
clean machine apart from a Windows smoke test, and there is no auto-update. See
docs/SETUP.md.
/connect
testing will accumulate them on the Boat account.chat device only within conversations), and the user_id older
routes take is not checked against the session. Sign-in lockouts are kept in
memory per process. Use HTTPS beyond a trusted private network.backend/tests/, desktop/test/). Boat-sandbox task and
sandbox services have only partial coverage, and there are no frontend
tests. The GitHub Actions workflows have not yet run on GitHub, so treat
them as untested until the first run.git command classifier only covers the verbs it explicitly knows
about (see github_classifier.py); an unrecognized verb defaults to the
safer "mutation" bucket rather than being assumed read-only./schedules, Telegram /schedule) poll on a 30s
interval, skip an occurrence missed by more than 15 minutes (recorded in
last_error), and notify only Telegram-linked users (a numeric user_id)
when TELEGRAM_BOT_TOKEN is set. Local routines
(docs/ROUTINES.md) do not need Boat: they poll every
5s, record missed and skipped occurrences, notify in the app (and
optionally one Telegram chat via ROUTINE_TELEGRAM_CHAT_ID), but only run
while the backend is running, do not retry failed or interrupted runs, and
do not de-duplicate webhook deliveries./api/v1/threads) and the Chat page are tested on SQLite only, and store transcripts (including
what Claude read, wrote and ran) in the database. A conversation can be deleted
from there (not a secure erase), but the Claude CLI's own copy in your .claude
folder is never touched.Issues and pull requests are welcome. Keep the README aligned with the backend and frontend behavior, and include focused tests when changing services or API behavior.
内容由编辑整理,部分信息来自 GitHub 与项目官网,版权归原作者所有。发现错误?报告问题