feat: CC-Web v1.0 — Claude Code Web Chat UI
功能特性: - WebSocket 流式对话、工具调用折叠、Markdown 渲染 - 多会话管理与续接、模型/权限模式切换 - 后台任务持久化(detached 进程 + PID 恢复) - 多渠道通知(PushPlus/Telegram/Server酱/飞书/QQ) - 密码管理(自动生成初始密码、首次改密、Web UI 改密) - 移动端适配、PWA 通知、斜杠指令 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Web 登录密码(可选,不设则首次启动自动生成随机密码)
|
||||||
|
# CC_WEB_PASSWORD=your-password-here
|
||||||
|
|
||||||
|
# 服务监听端口(默认 8002)
|
||||||
|
PORT=8002
|
||||||
|
|
||||||
|
# Claude CLI 路径(默认在 PATH 中查找 claude)
|
||||||
|
CLAUDE_PATH=claude
|
||||||
|
|
||||||
|
# PushPlus Token(可选,首次启动会自动迁移到 config/notify.json)
|
||||||
|
PUSHPLUS_TOKEN=
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
sessions/
|
||||||
|
logs/
|
||||||
|
.env
|
||||||
|
config/notify.json
|
||||||
|
config/auth.json
|
||||||
228
README.md
Normal file
228
README.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# CC-Web
|
||||||
|
|
||||||
|
Claude Code 轻量级 Web 聊天界面 — 在浏览器中与 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 交互。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **实时对话** — WebSocket 流式传输,逐字显示 Claude 回复
|
||||||
|
- **工具调用折叠** — 自动折叠/展开 Claude 的工具调用过程,不干扰阅读
|
||||||
|
- **Markdown 渲染** — 完整的 Markdown + 代码高亮(highlight.js)
|
||||||
|
- **多会话管理** — 创建、切换、重命名、删除会话,自动保存历史
|
||||||
|
- **会话续接** — 基于 `--resume` 实现跨消息上下文保持
|
||||||
|
- **模型切换** — Opus / Sonnet / Haiku 随时切换
|
||||||
|
- **权限模式** — YOLO(全自动)/ Plan(确认后执行)/ Default(标准审批)
|
||||||
|
- **后台任务** — 关闭浏览器后 Claude 进程继续运行,完成后推送通知
|
||||||
|
- **多渠道通知** — 支持 PushPlus / Telegram / Server酱 / 飞书机器人 / QQ(Qmsg),Web UI 内可视化配置
|
||||||
|
- **进程持久化** — detached 进程 + PID 文件,服务重启不丢失运行中的任务
|
||||||
|
- **移动端适配** — 响应式布局,支持 PWA 通知
|
||||||
|
- **密码认证** — 自动生成初始密码、首次登录强制改密、Web UI 修改密码
|
||||||
|
- **斜杠指令** — `/clear` `/model` `/mode` `/cost` `/compact` `/help`
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- **Node.js** >= 18
|
||||||
|
- **Claude Code CLI** 已安装并配置(`claude` 命令可用)
|
||||||
|
```bash
|
||||||
|
npm install -g @anthropic-ai/claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/your-username/cc-web.git
|
||||||
|
cd cc-web
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 创建配置文件(可选,不设密码则首次启动自动生成)
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后访问 `http://localhost:8002`,输入密码即可使用。
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### 环境变量 (.env)
|
||||||
|
|
||||||
|
| 变量 | 必填 | 默认值 | 说明 |
|
||||||
|
|------|:---:|--------|------|
|
||||||
|
| `CC_WEB_PASSWORD` | 否 | 自动生成 | Web 登录密码(首次启动自动迁移到 `config/auth.json`) |
|
||||||
|
| `PORT` | 否 | `8002` | 服务监听端口 |
|
||||||
|
| `CLAUDE_PATH` | 否 | `claude` | Claude CLI 可执行文件路径 |
|
||||||
|
| `PUSHPLUS_TOKEN` | 否 | - | PushPlus Token(首次启动自动迁移到通知配置) |
|
||||||
|
|
||||||
|
### 通知配置
|
||||||
|
|
||||||
|
点击侧边栏底部的 **⚙ 设置按钮**,在 Web UI 中可视化配置推送通知:
|
||||||
|
|
||||||
|
| 通知方式 | 所需配置 | 获取方式 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| **PushPlus**(微信推送) | Token | [pushplus.plus](https://www.pushplus.plus/) 注册获取 |
|
||||||
|
| **Telegram** | Bot Token + Chat ID | [@BotFather](https://t.me/BotFather) 创建机器人 |
|
||||||
|
| **Server酱** | SendKey | [sct.ftqq.com](https://sct.ftqq.com/) 注册获取 |
|
||||||
|
| **飞书机器人** | Webhook URL | 飞书群 → 设置 → 群机器人 → 添加自定义机器人 |
|
||||||
|
| **QQ(Qmsg)** | Qmsg Key | [qmsg.zendee.cn](https://qmsg.zendee.cn/) 登录后获取,需添加接收 QQ 号 |
|
||||||
|
|
||||||
|
配置保存在 `config/notify.json`,Token 在 UI 中脱敏显示(仅显示前4后4位)。
|
||||||
|
|
||||||
|
### 密码管理
|
||||||
|
|
||||||
|
密码存储在 `config/auth.json`,支持自动生成与 Web UI 修改:
|
||||||
|
|
||||||
|
- **首次启动**(无 `.env` 密码、无 `auth.json`):自动生成 12 位随机密码,打印到控制台,首次登录强制修改
|
||||||
|
- **从 `.env` 迁移**:如已在 `.env` 设置 `CC_WEB_PASSWORD`,启动时自动迁移到 `auth.json`,无需改密
|
||||||
|
- **Web UI 修改**:设置面板 → 修改密码(需输入当前密码)
|
||||||
|
- **密码要求**:≥ 8 位,包含大写/小写/数字/特殊字符中的至少 2 种
|
||||||
|
- **改密后**:所有已登录会话失效,需重新认证
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
cc-web/
|
||||||
|
├── server.js # Node.js 后端(HTTP + WebSocket + 进程管理 + 通知)
|
||||||
|
├── public/
|
||||||
|
│ ├── index.html # 页面结构
|
||||||
|
│ ├── app.js # 前端逻辑(WebSocket 通信、UI 交互)
|
||||||
|
│ ├── style.css # 样式(和风暖色调主题)
|
||||||
|
│ └── sw.js # Service Worker(移动端推送通知)
|
||||||
|
├── config/
|
||||||
|
│ ├── notify.json # 通知渠道配置(运行时生成)
|
||||||
|
│ └── auth.json # 密码配置(运行时生成)
|
||||||
|
├── sessions/ # 对话历史 JSON 文件(运行时生成)
|
||||||
|
├── logs/ # 进程生命周期日志(运行时生成)
|
||||||
|
├── .env.example # 环境变量模板
|
||||||
|
├── .gitignore
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 进程模型
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器 ←WebSocket→ Node.js (server.js) ←文件I/O→ Claude CLI (detached)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每条用户消息 spawn 一个 `claude -p --output-format stream-json` 子进程
|
||||||
|
- 进程使用 `detached: true` + `proc.unref()`,独立于 Node.js 生命周期
|
||||||
|
- stdin/stdout/stderr 通过文件传递(`sessions/{id}-run/`),不使用 pipe
|
||||||
|
- PID 持久化到文件,服务重启后自动恢复(`recoverProcesses()`)
|
||||||
|
- 使用 `FileTailer` 实时监听输出文件变化,流式推送给前端
|
||||||
|
|
||||||
|
### 后台任务流程
|
||||||
|
|
||||||
|
1. 用户发送消息 → spawn Claude 进程
|
||||||
|
2. 用户关闭浏览器 → 进程继续运行(detached)
|
||||||
|
3. 进程完成 → PID 监控检测到退出
|
||||||
|
4. 发送推送通知(PushPlus/Telegram/...)
|
||||||
|
5. 用户重新打开 → 自动同步完成的回复
|
||||||
|
|
||||||
|
### 进程日志
|
||||||
|
|
||||||
|
日志文件 `logs/process.log`(JSONL 格式,自动轮转 2MB),记录完整的进程生命周期:
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `process_spawn` | 进程创建(PID、模式、模型) |
|
||||||
|
| `process_complete` | 进程完成(退出码、耗时、费用) |
|
||||||
|
| `ws_connect` / `ws_disconnect` | 客户端连接/断开 |
|
||||||
|
| `ws_resume_attach` | 客户端重连并挂载到运行中的进程 |
|
||||||
|
| `recovery_alive` / `recovery_dead` | 服务重启时恢复进程 |
|
||||||
|
| `heartbeat` | 每 60 秒活跃进程状态快照 |
|
||||||
|
|
||||||
|
查看日志:
|
||||||
|
```bash
|
||||||
|
tail -f logs/process.log | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生产部署
|
||||||
|
|
||||||
|
### systemd 服务
|
||||||
|
|
||||||
|
创建 `/etc/systemd/system/cc-web.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=CC-Web - Claude Code Web Chat UI
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=your-user
|
||||||
|
WorkingDirectory=/path/to/cc-web
|
||||||
|
ExecStart=/usr/bin/node server.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
# 重要:只杀 Node.js 进程,不杀 Claude 子进程
|
||||||
|
KillMode=process
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`KillMode=process` 非常重要**:确保 systemd 重启服务时只杀 Node.js 进程,Claude 子进程继续运行,服务恢复后自动重新挂载。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable cc-web
|
||||||
|
sudo systemctl start cc-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx 反向代理
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/fullchain.pem;
|
||||||
|
ssl_certificate_key /path/to/privkey.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8002;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# WebSocket 支持
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
|
# 长连接超时(Claude 任务可能运行较久)
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 斜杠指令
|
||||||
|
|
||||||
|
在输入框中输入 `/` 可查看所有指令:
|
||||||
|
|
||||||
|
| 指令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `/clear` | 清除当前会话(含 Claude 上下文) |
|
||||||
|
| `/model [名称]` | 查看/切换模型(opus, sonnet, haiku) |
|
||||||
|
| `/mode [模式]` | 查看/切换权限模式(yolo, plan, default) |
|
||||||
|
| `/cost` | 查看当前会话累计费用 |
|
||||||
|
| `/compact` | 压缩上下文(重置 Claude 会话但保留聊天记录) |
|
||||||
|
| `/help` | 显示帮助 |
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **后端**:Node.js + [ws](https://github.com/websockets/ws)(唯一依赖)
|
||||||
|
- **前端**:原生 HTML/CSS/JS,无构建步骤
|
||||||
|
- **CDN**:[marked.js](https://marked.js.org/)(Markdown)+ [highlight.js](https://highlightjs.org/)(代码高亮)
|
||||||
|
- **CLI**:[Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
36
package-lock.json
generated
Normal file
36
package-lock.json
generated
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "cc-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "cc-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
package.json
Normal file
11
package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "cc-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1374
public/app.js
Normal file
1374
public/app.js
Normal file
File diff suppressed because it is too large
Load Diff
92
public/index.html
Normal file
92
public/index.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<title>CC-Web</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Login -->
|
||||||
|
<div id="login-overlay" class="login-overlay">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-logo">CC</div>
|
||||||
|
<h2>CC-Web</h2>
|
||||||
|
<p>Claude Code Web Chat</p>
|
||||||
|
<form id="login-form">
|
||||||
|
<input type="password" id="login-password" placeholder="输入密码" autocomplete="current-password" autofocus>
|
||||||
|
<label class="remember-label">
|
||||||
|
<input type="checkbox" id="remember-pw"> 记住密码
|
||||||
|
</label>
|
||||||
|
<button type="submit">登录</button>
|
||||||
|
</form>
|
||||||
|
<div id="login-error" class="login-error" hidden>密码错误</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main App -->
|
||||||
|
<div id="app" class="app" hidden>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside id="sidebar" class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<button id="new-chat-btn" class="new-chat-btn">+ 新会话</button>
|
||||||
|
</div>
|
||||||
|
<div id="session-list" class="session-list"></div>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<button id="settings-btn" class="settings-btn" title="设置">⚙</button>
|
||||||
|
<span class="brand">CC-Web</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="sidebar-overlay" class="sidebar-overlay" hidden></div>
|
||||||
|
|
||||||
|
<!-- Chat -->
|
||||||
|
<main class="chat-main">
|
||||||
|
<header class="chat-header">
|
||||||
|
<button id="menu-btn" class="menu-btn" title="菜单">☰</button>
|
||||||
|
<span id="chat-title" class="chat-title">新会话</span>
|
||||||
|
<select id="mode-select" class="mode-select" title="权限模式">
|
||||||
|
<option value="yolo">YOLO</option>
|
||||||
|
<option value="default">默认</option>
|
||||||
|
<option value="plan">Plan</option>
|
||||||
|
</select>
|
||||||
|
<span id="cost-display" class="cost-display"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="messages" class="messages">
|
||||||
|
<div class="welcome-msg">
|
||||||
|
<div class="welcome-icon">✿</div>
|
||||||
|
<h3>欢迎使用 CC-Web</h3>
|
||||||
|
<p>开始与 Claude Code 对话</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slash command menu -->
|
||||||
|
<div id="cmd-menu" class="cmd-menu" hidden></div>
|
||||||
|
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<textarea id="msg-input" rows="1" placeholder="输入消息… 输入 / 查看指令" autocomplete="off"></textarea>
|
||||||
|
<button id="send-btn" class="send-btn" title="发送">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="abort-btn" class="abort-btn" title="停止" hidden>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<rect x="6" y="6" width="12" height="12" rx="2"></rect>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1039
public/style.css
Normal file
1039
public/style.css
Normal file
File diff suppressed because it is too large
Load Diff
28
public/sw.js
Normal file
28
public/sw.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// CC-Web Service Worker — handles push notifications on mobile
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(event.data.title || 'CC-Web', {
|
||||||
|
body: event.data.body || '',
|
||||||
|
icon: event.data.icon || undefined,
|
||||||
|
tag: 'cc-web-task',
|
||||||
|
renotify: true,
|
||||||
|
data: event.data.data || {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
event.waitUntil(
|
||||||
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
||||||
|
for (const client of clientList) {
|
||||||
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.clients.openWindow('/');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user