wicek:基于 Claude Code 的 Discord 任务型 AI 助手
xxczaki/wicek
wicek 是一个运行在 Discord 上的任务型 AI 助手,用约 1600 行 TypeScript 核心代码驱动 Claude Agent SDK,替代功能臃肿的第三方 Kubernetes operator openclaw-rocks。
Task-focused AI assistant running as a Discord bot, powered by Claude Code
A minimal Node.js application that drives the Claude Agent SDK and exposes it through Discord. Built to replace a bloated third-party Kubernetes operator (openclaw-rocks) with something lean and maintainable.
Warning
This project is experimental and should not be used directly as-is.
Motivation
OpenClaw is a capable AI agent platform with a broad feature set – multiple messaging channels, vector memory, browser automation, self-configuration, and more. For a single-user setup on a Raspberry Pi where only Discord and a handful of tools are needed, most of that goes unused. Wicek replaces it with ~1,600 lines of TypeScript for the core (Discord, Agent SDK, cron, memory), ~1,300 more for the optional webhooks, mail, and readers, a single Deployment, and a Helm chart.
What it does
- Discord integration – DMs, @mentions, and threaded conversations via discord.js
- Claude Agent SDK – runs
query()per request on a Pro/Max subscription (OAuth token, no API key), streaming results back to Discord with thinking (blockquotes), tool use, and text - Browser automation – Chromium sidecar on a virtual display with Chrome DevTools MCP, screenshots auto-attached to Discord
- Cron jobs – GitOps-defined scheduled prompts (e.g., daily ETF updates)
- Webhooks – GitHub CI failures and Grafana alerts start debounced agent runs (
POST /hooks/github,POST /hooks/grafana,GET /healthzonWEBHOOK_PORT, default 8080), exposed via Tailscale Funnel - Custom subagents –
.claude/agents/for ETF analysis, infrastructure ops, Home Assistant - File handling – receives Discord attachments, sends back generated files and screenshots
- Self-update – knows how to push changes through the GitOps pipeline (git -> ArgoCD)
- Long-term memory – Claude Code's native auto-memory on persistent storage
Deployment
Runs on a single-node K3s cluster (Raspberry Pi 5). See xxczaki/homelab for the full cluster setup and xxczaki/charts for the Helm chart.
Credential broker
Service credentials live in a sidecar (ghcr.io/xxczaki/wicek-broker, source in broker/), not in the agent container. It is mitmproxy with a small add-on, listening on 127.0.0.1:3128. The agent container sends its traffic through it via HTTP(S)_PROXY and trusts the broker's CA, so tools call the real URLs without credentials and the broker adds the real ones. The image sets a placeholder GH_TOKEN because gh refuses to run without one. Git needs no credential helper.
Only hosts listed in the config are intercepted. Everything else is tunneled through untouched, and the hosts in NO_PROXY (Claude API, Discord) bypass it. For a listed host, the broker drops client Authorization/Cookie/CSRF headers, adds its own, removes Set-Cookie and CSRF headers from the response, and logs one line per request without bodies or query strings. Auth types:
bearer–Authorization: Bearer <tokenFile>basic–Authorization: BasicfromusernameorusernameFile, pluspasswordFilehome-assistant–bearer, plus theaccess_tokenin the WebSocketauthmessageenable-banking–Authorization: Bearerwith a short-lived RS256 JWT signed withprivateKeyFile, itskidread fromapplicationIdFileunifi– logs in withusernameFile/passwordFile, keeps theTOKENcookie and CSRF token, logs in again after a 401imap– the broker answers the request itself as a read-only mail gateway forserver(port 993), logging in withusernameFile/passwordFile. Endpoints:GET /folders,/search(folder,from,to,subject,text,since,before,unseen,limit)/message(folder,uid) and/attachment(folder,uid,index, raw bytes up to 25 MB). It opens folders withEXAMINEand fetches withBODY.PEEK, so it can't change the mailbox or mark messages as read. Use a made-up host such ashttp://imap.brokerlogins– the broker answers the request itself with the logins in a 1Password vault, read with theopCLI and the service account token intokenFile. Endpoints:GET /list(item ids, titles, domains, and placeholders, never values) and/code?item=<id>(digit sequences from the newest email sent by the item's domain in the last 10 minutes, read over IMAP frommailServerwithmailUsernameFile/mailPasswordFile). Use a made-up host such ashttp://logins.broker
Optional per host: methods (allowlist) and insecureTls (self-signed upstreams). Top-level "blockUnlisted": true rejects every host not in the list instead of tunneling it. Hosts may use a leading *. wildcard. The config is read from BROKER_CONFIG (default /etc/broker/config.json):
{
"hosts": [
{
"host": "parsify.grafana.net",
"auth": { "type": "bearer", "tokenFile": "/run/broker/grafana-cloud/credential" }
}
]
}Mail is quarantined from the main agent. The gateway's tools (src/claude/mail.ts) belong to a mail-reader subagent that can't use anything else, and hooks stop every other agent and tool from reaching them or the gateway. Email can carry prompt injection, so its text only reaches an agent that can't act on it, and the main agent gets summaries.
The CA is generated once per pod in a sidecar-only volume. Only the certificate (/run/broker-ca/ca.pem) and a system bundle that includes it (bundle.pem) are shared with the agent. The sidecar also runs ssh-agent on SSH_AUTH_SOCK with the keys in SSH_KEY_FILES (space-separated), so the agent can use SSH keys without reading them.
Website logins: the user moves a Login item into the vault, and it's usable right away. The account-browser subagent (src/claude/logins.ts) lists the logins, types placeholders such as WICEK_LOGIN_<item id>_PASSWORD in the browser, and gets emailed login codes from /code. The browser sidecar is Chromium on a virtual display, so a login page that waits for a normal browser still renders. Its traffic goes through a second mitmdump process on BROKER_BROWSER_PORT (3129) that runs only browser.py: it intercepts every host, and when a request body contains a placeholder, it fetches that item and swaps in the value, but only if the request goes to one of the item's URLs or their subdomains. It never adds API credentials, so pages can't use the broker's other credentials. Chrome trusts the broker's CA through --ignore-certificate-errors-spki-list, with the hash the broker writes to /run/broker-ca/spki. The subagent has only browser and login tools and can't navigate off the listed domains, and hooks keep other agents from the login tools, the gateway, and typing placeholders into the browser.
Readers
A reader answers questions about one untrusted data source (e.g. bank transactions) from a separate pod: node dist/reader.js running an Agent SDK query with only Bash, in a Kata VM. Its credential broker runs as its own pod outside the VM with "blockUnlisted": true, so the reader can reach only that broker, and the broker only the hosts in its config – everything else gets a 403. The main agent calls a reader with the ask_reader tool (src/claude/readers.ts, configured by the READERS env var), and the bot posts the answer straight to Discord, so the main agent never sees text a third party could have written. The answer is an embed labeled as private, and a PostToolUse hook ends the main agent's turn right after it, so it can't add its own take.
Readers whose API needs a browser login get a redirect URL, READER_CALLBACK_URL (<webhooks host>/hooks/callback/<reader>). The reader saves a random state to <state dir>/pending-callback before handing out the login link. When the browser comes back, the webhook server shows a static page and forwards the query to the reader's POST /callback, which compares state in code (constant time, single use) before the model sees anything. The answer goes to the conversation that last asked that reader (in memory, else the owner's DMs). Without a matching state the public route does nothing.
The reader fetches the broker's CA from http://mitm.it/cert/pem through the broker before each question, since the broker generates a new one when it restarts. Its system prompt is the shared rules in src/reader.ts, the reader's own prompt mounted at READER_PROMPT_PATH, and <state dir>/notes.md – facts the user stated, never API data. It keeps state in READER_STATE_DIR.
Each POST /ask carries the main side's conversation key (dm:, thread:, or channel:), and the reader resumes one Agent SDK session per key (src/reader/sessions.ts), so follow-ups build on earlier answers. Transcripts contain API data, so they live under HOME (an emptyDir), not in the backed-up state directory. They're lost on restart and deleted after 12 idle hours or beyond 20 sessions.
AI disclosure
This project contains code generated by Large Language Models (LLMs), under human supervision and proofreading.
License
MIT
来源:GitHub · github.com