cdrop — 跨 OS 剪贴板与文件传输服务

Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。

- 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。
- 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。
- 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。
- 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。

架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
2026-06-15 21:38:28 +08:00
commit f21fa5b5e8
239 changed files with 29010 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import { useAppStore } from "../store";
import { refreshTokens } from "../features/auth/auth";
import { readInjectedApiBase, readInjectedDeviceType } from "../store/helpers";
import { toAsciiDeviceName } from "../utils/format";
import { t } from "../i18n";
// /api 请求的来源前缀:桌面 Windows 注入 loopback 代理地址(绕开会缓冲 SSE 的
// custom scheme),macOS / 浏览器为空(同源相对 URL)。读一次注入值即定。
const API_BASE = readInjectedApiBase();
// 客户端类型(browser / macos / windows / linux),随每个请求发给后端登记设备。
const DEVICE_TYPE = readInjectedDeviceType();
function withBase(input: RequestInfo): RequestInfo
{
if (typeof input === "string" && API_BASE && input.startsWith("/"))
{
return API_BASE + input;
}
return input;
}
// Centralised fetch wrapper:
// - in dev, attaches Authorization: Bearer <VITE_CDROP_DEV_TOKEN> + X-Dev-User
// - in prod, attaches Authorization: Bearer <accessToken from store>
// - always sets X-Device-Name from the store
// - in prod, on a 401 response transparently refreshes the access token via
// /api/auth/refresh and retries once
//
// Returns the fetch Response untouched so callers can drive streaming bodies
// (SSE, relay GET stream) themselves.
export async function apiFetch(input: RequestInfo, init?: RequestInit): Promise<Response>
{
let r = await doFetch(input, init);
if (r.status === 401 && useAppStore.getState().authMode === "prod")
{
const ok = await refreshTokens();
if (ok)
{
r = await doFetch(input, init);
}
}
return r;
}
async function doFetch(input: RequestInfo, init?: RequestInit): Promise<Response>
{
const state = useAppStore.getState();
const headers = new Headers(init?.headers ?? {});
if (state.authMode === "dev")
{
const devToken = import.meta.env.VITE_CDROP_DEV_TOKEN;
if (!devToken)
{
throw new Error(t("errors.devTokenMissing"));
}
headers.set("Authorization", `Bearer ${devToken}`);
if (state.user?.id)
{
headers.set("X-Dev-User", state.user.id);
}
}
else
{
if (!state.accessToken)
{
throw new Error(t("errors.noAccessToken"));
}
headers.set("Authorization", `Bearer ${state.accessToken}`);
}
if (state.selfDeviceName)
{
// Device names are ASCII-only; coerce here so a legacy non-ASCII name
// can't make Headers.set throw (the server sanitises identically).
const safeName = toAsciiDeviceName(state.selfDeviceName);
if (safeName) { headers.set("X-Device-Name", safeName); }
}
headers.set("X-Device-Type", DEVICE_TYPE);
return fetch(withBase(input), { ...init, headers });
}
export async function apiJSON<T>(input: RequestInfo, init?: RequestInit): Promise<T>
{
const r = await apiFetch(input, init);
if (!r.ok)
{
const text = await r.text().catch(() => r.statusText);
throw new Error(`${r.status}: ${text}`);
}
if (r.status === 204) { return undefined as T; }
return ( await r.json() ) as T;
}