CookieCloud部署

一、概述

1.1 需求及已有的方案

接上次,部署 Rsshub 后,可以将好多的网站聚合起来,不过一些站点依赖于 cookie,每次手动抓取,重新写入 .env 变量,然后去重启服务不够优雅。早已经有人有这样的需求。

仓库地址

1.2 小小的改进

不过这小小的服务就又部署一个 docker,而我更喜欢无服务器部署。CloudflareKV 正好可以做这个。

二、部署到 Worker

2.1 准备凭据

挑两个长随机串作为 UUID 和密码:

1
2
3
4
5
6
7
# UUID(CookieCloud 当作"用户名",写到 Worker URL 路径里)
openssl rand -hex 16
# → 例如 a7f3b9c2d8e1f4a6b5c3d7e9f2a1b8c4

# 密码(端到端加密用,只在浏览器和 RSSHub 知道)
openssl rand -hex 24
# → 例如 9e2f8a1b5c7d3e6f4a9b2c8d1e5f7a3b6c9d2e4f1a8b5c7d

记录好,下面要用。

2.2 部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 装 wrangler(如果还没有)nodejs 环境。
npm install -g wrangler
wrangler login

# 用本仓库的 worker.js + wrangler.toml
mkdir cookiecloud-worker && cd cookiecloud-worker
# 把 worker.js 和 wrangler.toml 放进来

# 创建 KV 命名空间
wrangler kv namespace create COOKIE_KV
# 输出会给一个 id,类似:
# id = "abc123def456..."
# 把这个 id 填到 wrangler.toml 的 [[kv_namespaces]] → id 字段

# 部署
wrangler deploy

部署成功后会得到 URL,例如 https://cookiecloud-worker.yourname.workers.dev

:默认的域名被 GFW 墙掉了,需要绑定自己的一个域名。

  • worker.jss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* CookieCloud-compatible Cloudflare Worker (forensic build)
*
* Logs body in hex, then tries gzip / deflate / deflate-raw decompression.
* Whichever produces valid JSON wins.
*/

const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Content-Encoding',
'Access-Control-Max-Age': '86400',
};

const json = (obj, status = 200) =>
new Response(JSON.stringify(obj), {
status,
headers: { 'content-type': 'application/json; charset=utf-8', ...CORS_HEADERS },
});

function hexDump(buf, n = 32) {
const arr = new Uint8Array(buf).slice(0, n);
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(' ');
}

async function tryDecompress(buf, format) {
try {
const ds = new DecompressionStream(format);
const stream = new Blob([buf]).stream().pipeThrough(ds);
const decompressed = await new Response(stream).arrayBuffer();
return new TextDecoder('utf-8', { fatal: true }).decode(decompressed);
} catch (e) {
return null;
}
}

export default {
async fetch(request, env) {
const url = new URL(request.url);
const path = url.pathname.replace(/\/+$/, '') || '/';

if (request.method === 'OPTIONS') {
return new Response(null, { headers: CORS_HEADERS });
}

if (path === '/update' && request.method === 'POST') {
console.log('=== /update ===');
console.log('Content-Type:', request.headers.get('content-type'));
console.log('Content-Encoding:', request.headers.get('content-encoding'));

const buf = await request.arrayBuffer();
console.log('Body length:', buf.byteLength);
console.log('First 32 bytes (hex):', hexDump(buf, 32));

// Try plain UTF-8 first
let text = null;
try {
text = new TextDecoder('utf-8', { fatal: true }).decode(buf);
console.log('Plain UTF-8 OK. First 200 chars:', text.slice(0, 200));
} catch {
console.log('Not valid UTF-8, trying decompression...');
}

// Try each compression format
if (text === null) {
for (const fmt of ['gzip', 'deflate', 'deflate-raw']) {
const r = await tryDecompress(buf, fmt);
if (r !== null) {
console.log(`Decompressed with ${fmt}. First 200 chars:`, r.slice(0, 200));
text = r;
break;
}
}
}

if (text === null) {
console.log('Could not decode body in any known format.');
return json({ action: 'error', msg: 'cannot decode body' }, 400);
}

// Try parse JSON
let body = null;
try {
body = JSON.parse(text);
} catch (e) {
// try urlencoded
try {
body = Object.fromEntries(new URLSearchParams(text));
if (!body.uuid && !body.encrypted) body = null;
} catch { body = null; }
}
if (!body) {
console.log('Could not parse as JSON or urlencoded.');
return json({ action: 'error', msg: 'cannot parse body' }, 400);
}
console.log('Parsed keys:', Object.keys(body));

const uuid = body.uuid;
const encrypted = body.encrypted || body.data;
if (!uuid || !encrypted) {
return json({ action: 'error', msg: 'uuid or encrypted missing' }, 400);
}

if (env.ALLOWED_UUIDS) {
const allow = env.ALLOWED_UUIDS.split(',').map((s) => s.trim()).filter(Boolean);
if (!allow.includes(uuid)) {
return json({ action: 'error', msg: 'uuid not allowed' }, 403);
}
}

if (typeof encrypted !== 'string' || encrypted.length > 5_000_000) {
return json({ action: 'error', msg: 'payload too large' }, 413);
}

await env.COOKIE_KV.put(uuid, encrypted);
console.log('Stored OK. uuid:', uuid, 'encrypted len:', encrypted.length);
return json({ action: 'done' });
}

const m = path.match(/^\/get\/([^/]+)$/);
if (m && (request.method === 'GET' || request.method === 'POST')) {
const uuid = m[1];
const encrypted = await env.COOKIE_KV.get(uuid);
console.log('/get/' + uuid, '→', encrypted ? `len=${encrypted.length}` : 'empty');
if (!encrypted) return json({}, 404);
return json({ encrypted });
}

if (path === '/') {
return new Response(
'CookieCloud-compatible Worker is running.\n',
{ headers: { 'content-type': 'text/plain; charset=utf-8' } }
);
}

return json({ action: 'error', msg: 'not found' }, 404);
},
};
  • wrangler.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
name = "cookiecloud-worker"
main = "worker.js"
compatibility_date = "2025-05-01"

# KV namespace for storing encrypted cookie blobs.
# Create with: wrangler kv namespace create COOKIE_KV
# Then paste the returned id below.
[[kv_namespaces]]
binding = "COOKIE_KV"
id = ""

# Optional: lock down which UUIDs may write to this Worker.
# Comma-separated. Leave commented to allow any UUID (the UUID itself
# acts as the secret in CookieCloud's design).
# [vars]
# ALLOWED_UUIDS = "your-long-random-uuid-here"
[vars]
ALLOWED_UUIDS = ""

2.3 自建

  1. curl 验证
1
2
curl https://cookiecloud-worker.yourname.workers.dev/
# 应该返回:"CookieCloud-compatible Worker is running."
  1. 安装插件后检查

稍后检查即可。

三、配置浏览器扩展

3.1 安装

Chrome 商店Edge 商店

3.2 填配置

打开扩展弹窗,填入:

  • 服务器地址: https://cookiecloud-worker.yourname.workers.dev不要带末尾斜杠
  • 用户KEY (UUID): 上一步生成的 UUID
  • 端对端加密密码: 上一步生成的密码
  • 加密算法:默认,动态IV。
  • 同步时间间隔: 默认即可(也可设短一点,例如 60 分钟)

3.3 同步要点

  • 登录 B 站和知乎,确保它们的 cookie 是新的、有效的
  • 点扩展的 “测试同步” 按钮,确认成功
  • 如需选择性同步,可在扩展设置里限定 Cookie同步域名关键词bilibili.com,zhihu.com,避免上传无关网站的 cookie

四、RSSHub 端集成

4.1 拉取适配器并加入自定义规则

在 RSSHub 的 docker-compose 同目录下

1
2
3
4
5
6
7
8
9
git clone https://github.com/sgpublic/rsshub-cookiecloud.git cookiecloud
cd cookiecloud
git checkout next # 用 v2 分支

# 把本仓库的 bilibili.js 和 zhihu.js 放到 libs/cookies/ 下
# 注意:bilibili.js 里的 12345678 要改成你自己的 B 站 UID
# https://api.bilibili.com/x/web-interface/nav 里的 mid
cp /path/to/bilibili.js libs/cookies/bilibili.js
cp /path/to/zhihu.js libs/cookies/zhihu.js
  • bilibili.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Place this file under: cookiecloud/libs/cookies/bilibili.js
//
// RSSHub's Bilibili routes expect env vars of the form
// BILIBILI_COOKIE_{uid} — where {uid} is your numeric Bilibili user ID.
//
// To find your UID: log in to bilibili.com, open DevTools → Application →
// Cookies, look for "DedeUserID". Or visit:
// https://api.bilibili.com/x/web-interface/nav
//
// 要查找您的 UID:登录 bilibili.com,打开开发者工具 → 应用程序 → Cookies,查找“DedeUserID”。或者访问:
// https://api.bilibili.com/x/web-interface/nav
//
// Replace 12345678 below with your real UID. Add more entries if you
// 请将下方的 12345678 替换为您的真实 UID。
// want to inject cookies for multiple Bilibili accounts.

export default {
"BILIBILI_COOKIE_12345678": [
{
"domain": "bilibili.com"
}
]
}
  • zhihu.js 已经官方仓库已经有了,记得是复数。

4.2 修改你现有的 docker-compose

只动 rsshub 服务这一段。新增的 4 行环境变量、1 行 volume、1 行 command:

1
2
3
4
5
6
7
8
9
10
11
12
13
services:
rsshub:
# ... 你原来的配置都保留 ...
volumes:
# ... 你原来的 volumes ...
- ./cookiecloud:/app/cookiecloud # 新增
environment:
# ... 你原来的 environment ...
COOKIE_CLOUD_HOST: 'https://cookiecloud-worker.yourname.workers.dev' # 新增
COOKIE_CLOUD_UUID: 'xxxxxxxxxxx' # 新增(你的 UUID)
COOKIE_CLOUD_PASSWORD: 'xxxxxxxxxxxxxxx' # 新增(你的密码)
COOKIE_CLOUD_INTERVAL: 1800 # 新增(秒,每小时拉一次)
command: ["node", "/app/cookiecloud/index.js"] # 新增/替换原 command

关于 command:v2 适配器的 index.js 包揽了"启动 RSSHub + 周期拉 cookie"。你原本没显式 command 也没关系,加上这行即可。

4.3 完整 compose 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
services:
rsshub:
# two ways to enable Playwright:
# 启用 Playwright 的两种方法:
# * comment out marked lines, then use this image instead: diygod/rsshub:chromium-bundled
# 注释掉标记的行,然后使用此镜像:diygod/rsshub:chromium-bundled
# * (consumes more disk space and memory) leave everything unchanged
# (占 diygod/rsshub:chromium-bundled
image: diygod/rsshub:chromium-bundled # or ghcr.io/diygod/rsshub
restart: always
ports:
- '1200:1200'
env_file:
- .env
volumes:
# ... 原有 volumes ...
- ./cookiecloud:/app/cookiecloud
environment:
NODE_ENV: production
CACHE_TYPE: redis
REDIS_URL: 'redis://redis:6379/'
COOKIE_CLOUD_HOST: 'https://cc.xxxxxxx.cn' # 新增
COOKIE_CLOUD_UUID: 'xxxxxxxxxxx' # 新增(你的 UUID)
COOKIE_CLOUD_PASSWORD: 'xxxxxxxx' # 新增(你的密码)
COOKIE_CLOUD_INTERVAL: 1800 # 新增(秒,每小时拉一次)
command: ["node", "/app/cookiecloud/index.js"] # 新增/替换原 command
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
# test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz?key=$(ACCESS_KEY)']
interval: 30s
timeout: 10s
retries: 3
depends_on:
- redis
redis:
image: redis:alpine
restart: always
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 30s
timeout: 10s
retries: 5
start_period: 5s

volumes:
redis-data:

4.4 重启

1
2
docker compose up -d --force-recreate rsshub
docker compose logs -f rsshub

日志里看到 CookieCloud loaded. 就成功了。再过一会能看到 ZHIHU_COOKIEBILIBILI_COOKIE_xxx 被注入的日志。

五、验证

  1. 手动同步后验证,最好配合 RSSHub Radar
1
2
3
4
5
# 知乎
https://rsshub.bravexist.cn/zhihu/posts/people/liaoxuefeng

# 哔哩哔哩
https://rsshub.bravexist.cn/bilibili/user/video/327247876