客户端
CircleChat 是 Web 优先的应用,所有功能(聊天、群组、管理后台)都通过网页提供。除浏览器外还提供独立的桌面客户端;移动端直接使用手机浏览器访问,网页自带响应式布局。
网页(浏览器)
打开站点地址即用,无需安装。
- 响应式布局:窄屏 / 手机下左侧会话栏收成抽屉,设置收进侧栏面板;触摸设备上消息上下文菜单改为「长按」触发。触摸判定在
MessageItem.vue用matchMedia('(pointer: coarse)')完成。 - 多语言:支持 zh / en / ja,选择记到
localStorage。 - 服务端升级后硬刷新:浏览器务必硬刷新(
Ctrl+Shift+R,macOS 为Cmd+Shift+R)清理缓存的静态资源。
桌面客户端(CircleChat-Client)
独立仓库:CircleChat-Client,一个用 Rust 写的 WebView 外壳,把网页端承载在原生窗口里。
架构与定位
- 纯 WebView 外壳:底层用 wry 承载系统原生 WebView——Linux 用 WebKitGTK、macOS 用 WKWebView、Windows 用 WebView2。
Cargo.toml只引了wry+tao窗口层,未引入 Tauri 整套框架,因此安装包小、内存占用低。 - 原生配置窗口:首次配置用 iced 写的原生控件,非内嵌网页;关闭 wgpu、改用
tiny-skia软件渲染,编译更快、兼容性更好。 - 桌面能力:系统通知(经
window.__CIRCLECHAT__.notify)、文件下载落盘(落到系统下载目录)、站内外链接自动分流(站外交给系统默认程序,站内按规则在当前 WebView 打开)。 - 版本号:见
Cargo.toml(version = "0.1.5"),对外实际报0.1.5+<git short sha>。
下载与安装
推荐从 Releases 下载预编译安装包。
| 平台 | 安装包 | 安装方式 |
|---|---|---|
| Linux | .deb | sudo apt install ./circlechat-client_<版本>_amd64.deb |
| Linux | .rpm | sudo dnf install ./circlechat-client-<版本>-1.x86_64.rpm |
| Linux | .AppImage | chmod +x CircleChat-<版本>-x86_64.AppImage && ./CircleChat-<版本>-x86_64.AppImage |
| macOS | .dmg | 挂载后把 CircleChat.app 拖进「应用程序」 |
| Windows | .zip | 解压后运行 circlechat-client.exe |
当前发布包未签名
当前发布包未签名:macOS 首次打开会被 Gatekeeper 拦截(右键 → 打开);Windows 可能弹 SmartScreen。正式分发前需在 CI 里加证书签名 / 公证。
配置与启动流程
首次使用需填入 CircleChat 服务地址和共享密钥 APP_SECRET。
# 预编译二进制(路径按安装方式调整)
APP_SECRET='你的密钥' ./circlechat-client
# 或从源码运行
APP_SECRET='你的密钥' cargo run启动流程:
读本地配置 → 有地址?── 是 ──→ 打开 WebView
│
否
↓
弹出配置窗口(填地址)
↓
GET {地址}/api/app-manifest 校验站点身份
↓
通过 → 保存地址并进入 WebView
失败 → 窗口提示「无效的站点」,不保存、不进入- 首次启动:弹出「CircleChat 配置」窗口,填地址、点「保存并进入」,客户端拉
{地址}/api/app-manifest校验身份,通过才写配置并打开 WebView,失败提示「无效的站点」。 - 之后再启动:配置已有地址,直接打开 WebView,不再弹配置窗口;此时可不传
APP_SECRET,校验只发生在保存地址那一刻。 - 回到配置窗口:在 WebView 里按
Ctrl+Shift+R(macOSCmd+Shift+R)清掉配置并重启客户端,或直接删掉配置文件再启动。
配置文件与数据目录:
| 平台 | 配置 | 数据目录 |
|---|---|---|
| Linux | ~/.config/circlechat/config.json | ~/.local/share/circlechat/ |
| macOS | ~/Library/Application Support/com.CircleChat.CircleChat/config.json | 标准目录 |
| Windows | %APPDATA%\CircleChat\CircleChat\config\config.json | 标准目录 |
日志(配置路径、缓存目录、UA、下载、通知)打到 stdout,需留档可 2>&1 | tee run.log。
站点身份校验(/api/app-manifest)
保存地址前客户端请求:
GET {地址}/api/app-manifest期望返回:
{
"app_id": "circlechat",
"version": "1.0.0",
"timestamp": 1700000000,
"signature": "sha256(app_id+version+timestamp+SECRET)"
}服务端侧(server/lib/runtime.ts):
- 对来源放开:
Access-Control-Allow-Origin: *,仅GET/OPTIONS; - 不下发凭据、不认 Cookie,放在鉴权门槛之前;
- 签名
signature = SHA256(app_id + version + timestamp + APP_SECRET),APP_SECRET只用于签名、绝不下发; - 标识与版本来自
readAppInfo():环境变量CIRCLECHAT_APP_ID/APP_ID优先,缺省读package.json的name/version; APP_SECRET未配置时接口直接返回 503,不下发任何可被伪造的清单。
客户端侧三项全部通过才允许保存(src/site.rs):
app_id默认要求为circlechat,可用环境变量CIRCLECHAT_APP_ID覆盖——客户端与服务端必须配置成同一个值,签名才对得上。signature == sha256(app_id + version + timestamp + SECRET)的小写 hex(大小写不敏感,允许sha256=/sha256:前缀)。|本地时间 - timestamp| <= 300秒(防重放)。
SECRET 即环境变量 APP_SECRET,客户端与服务端必须设为同一个值。校验结果缓存在内存里(成功和失败都缓存),所以服务端修好之后需让用户重启客户端才能重新校验。
暴露给网页的接口
判断自己在客户端里:src/identity.rs 注入,先于页面脚本、所有路由 / 刷新都在,只在主 frame,iframe 无。
const isDesktop = !!window.__CIRCLECHAT_CLIENT__
// { name: 'circlechat-desktop', version: '0.1.5+abc1234', platform: 'linux' | 'macos' | 'windows' }这也对应其他识别方式:
- 登录页「下载桌面客户端」横幅(
DownloadClient.vue)只在getDesktopClient() === null时显示; src/utils/client.ts优先读这个全局变量,取不到才退化到 UA 正则/CircleChatDesktop\/([\d.]+)/。
发系统通知:src/notification.rs,经 window.__CIRCLECHAT__.notify:
const result = await window.__CIRCLECHAT__.notify({ title: '新消息', body: '张三:在吗?' })
// { ok: true, error: null } 或 { ok: false, error: '<原因>' }error 可能为:系统通知服务的报错(Linux 无通知守护进程、macOS 权限被拒…)、ipc-unavailable(不在客户端里,如浏览器调试)、timeout(10 秒内没结果)。标题上限 120 字符、正文 500 字符,超出截断。
另外两个标记只用于功能分支,不能做信任判定,普通浏览器可原样伪造:
| 机制 | 覆盖 | 范围 |
|---|---|---|
User-Agent 里的 CircleChatDesktop/<版本> | 服务端 + 前端 | 所有请求(子资源 / XHR / WebSocket 握手) |
X-CircleChat-Client: <版本> 请求头 | 服务端 | 仅入口文档那一次请求 |
后续接口请求要带标记,前端在拦截器里加:
axios.interceptors.request.use(cfg => {
if (window.__CIRCLECHAT_CLIENT__) cfg.headers['X-CircleChat-Client'] = window.__CIRCLECHAT_CLIENT__.version
return cfg
})要服务端能信任,需用 APP_SECRET 签名换 token,而非这几个标记。
缓存与其它行为
- 入口文档每次都重新请求(带
Cache-Control: no-cache, no-store),前端发版立刻生效。 - 其它资源(图片、字体、媒体、附件)按服务端响应头走 WebView 磁盘缓存。wry 默认是临时上下文,客户端显式指定了一个持久化目录。
- 下载落到系统下载目录,重名自动加
(1)、(2)。 - 站外链接(origin 不同,含子域、换协议)交给系统默认程序打开,不在 WebView 里开。
window.open/target="_blank"的站内链接在当前 WebView 打开,不弹新窗。- 清缓存:删掉数据目录里的
webview/即可。
从源码构建
需要 Rust 1.89+:
cargo build --releaseLinux 还需系统库:
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libdbus-1-dev libxkbcommon-devmacOS / Windows 走系统原生 WebView,无需额外依赖。
环境变量:
| 变量 | 必需 | 说明 |
|---|---|---|
APP_SECRET | 是(首次配置时) | 站点身份校验共享密钥,客户端与服务端必须一致 |
CIRCLECHAT_USER_AGENT | 否 | 整体覆盖 UA(内置 UA 写死了浏览器版本号,将来变旧时用它兜底) |
CIRCLECHAT_URL | 否 | 直接指定要加载的地址,跳过配置窗口(调试用) |
CIRCLECHAT_BUILD | 否 | 把 git short sha 编进二进制,本地构建时 build.rs 会自己去问 git |
移动端
目前没有独立的移动 App,在手机浏览器直接访问站点地址即可。网页本身是响应式的:
- 会话列表收成抽屉;
- 设置收进面板;
- 长按消息弹出上下文菜单;
- 功能与桌面端完全一致。