Skip to content

Commit 4d9a82e

Browse files
committed
Support Cloudflare Access for AI endpoint
1 parent a171c5a commit 4d9a82e

5 files changed

Lines changed: 52 additions & 3 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ https://rss-reader-proxy.<你的 workers.dev 子域>.workers.dev/rss?url={url}
7878
Worker 配置在 `wrangler.jsonc`
7979

8080
- `ALLOW_ORIGIN`:建议生产环境改成你的前端地址,例如 `https://zijian-z.github.io` 或你的 Pages 地址。
81+
- `ALLOW_CREDENTIALS`:默认 `false`。如果 AI 接口使用 Cloudflare Access Cookie 鉴权,需要设为 `true`,同时 `ALLOW_ORIGIN` 必须是明确的前端 Origin,不能是 `*`
8182
- `ALLOWED_HOSTS`:留空表示允许代理所有 http/https RSS 地址;如果要限制来源,可填逗号分隔的主机名。
8283
- `MAX_BYTES`:最大响应体字节数,默认 `8388608`
8384
- `AI_BASE_URL`:OpenAI 兼容接口的 base URL,默认 `https://api.openai.com/v1`
@@ -97,6 +98,17 @@ npx wrangler secret put AI_API_KEY
9798
https://api.plunox.site/ai/responses
9899
```
99100

101+
如果只想保护 AI 接口,建议在 Cloudflare Zero Trust 里给 `api.plunox.site/ai/*` 创建 Access 应用,并把 `/rss` 留在 Access 之外。前端设置里的“AI 鉴权”选择 `Cloudflare Access` 后,AI 请求会携带 Access 登录 Cookie;RSS 代理请求不会携带鉴权。
102+
103+
使用 Cloudflare Access 时还需要在 Access 应用中配置 CORS:
104+
105+
- Origin 填前端地址,例如 `https://zijian-z.github.io`
106+
- Methods 至少包含 `POST``OPTIONS`
107+
- Headers 至少包含 `content-type`
108+
- 允许 credentials。
109+
110+
首次使用前,可以先打开 `https://api.plunox.site/ai/health` 完成 Access 登录;登录成功后再回到阅读器使用 AI 按钮。
111+
100112
## 配置同步
101113

102114
设置里的“导出配置 JSON”只导出阅读设置、文件夹和订阅地址,不包含文章内容、已读状态、星标状态或本地缓存。

server/proxy.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export async function handleProxyRequest(request, env = {}) {
2323
return jsonResponse({ ok: true }, 200, config);
2424
}
2525

26+
if (requestUrl.pathname === "/ai/health") {
27+
return jsonResponse({ ok: true, ai: true }, 200, config);
28+
}
29+
2630
if (requestUrl.pathname === "/ai/responses") {
2731
return handleAiRequest(request, config);
2832
}
@@ -126,6 +130,7 @@ export function readProxyConfig(env = {}) {
126130
return {
127131
env,
128132
allowOrigin: String(env.ALLOW_ORIGIN || "*"),
133+
allowCredentials: isTruthy(env.ALLOW_CREDENTIALS),
129134
allowedHosts: parseAllowedHosts(env.ALLOWED_HOSTS || ""),
130135
maxBytes: positiveNumber(env.MAX_BYTES, DEFAULT_MAX_BYTES),
131136
};
@@ -230,13 +235,23 @@ function positiveNumber(value, fallback) {
230235
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
231236
}
232237

238+
function isTruthy(value) {
239+
return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
240+
}
241+
233242
function corsHeaders(config) {
234-
return {
243+
const headers = {
235244
"access-control-allow-origin": config.allowOrigin,
236245
"access-control-allow-methods": "GET, POST, OPTIONS",
237246
"access-control-allow-headers": "content-type",
238247
vary: "origin",
239248
};
249+
250+
if (config.allowCredentials && config.allowOrigin !== "*") {
251+
headers["access-control-allow-credentials"] = "true";
252+
}
253+
254+
return headers;
240255
}
241256

242257
function jsonResponse(payload, status, config) {

src/App.jsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,9 @@ function App() {
350350
setAiStatus({ articleId: article.id, message: "" });
351351

352352
try {
353+
const config = libraryRef.current.config;
353354
const contentText = stripHtml(article.content || article.excerpt || "").slice(0, 24000);
354-
const response = await fetch(validateAiWorkerUrl(libraryRef.current.config.aiWorkerUrl), {
355+
const requestOptions = {
355356
method: "POST",
356357
headers: {
357358
"content-type": "application/json",
@@ -363,7 +364,13 @@ function App() {
363364
url: article.link,
364365
content: contentText || article.title,
365366
}),
366-
});
367+
};
368+
369+
if (config.aiAuthMode === "cloudflareAccess") {
370+
requestOptions.credentials = "include";
371+
}
372+
373+
const response = await fetch(validateAiWorkerUrl(config.aiWorkerUrl), requestOptions);
367374

368375
const payload = await response.json().catch(() => ({}));
369376

@@ -1408,6 +1415,19 @@ function SettingsDialog({
14081415
type="url"
14091416
/>
14101417
</label>
1418+
<label>
1419+
<span>AI 鉴权</span>
1420+
<select
1421+
value={config.aiAuthMode || "none"}
1422+
onChange={(event) => onConfigChange({ aiAuthMode: event.target.value })}
1423+
>
1424+
<option value="none">不使用鉴权</option>
1425+
<option value="cloudflareAccess">Cloudflare Access</option>
1426+
</select>
1427+
<small className="field-note">
1428+
使用 Cloudflare Access 时,AI 请求会携带 Access 登录 Cookie;RSS 代理仍保持公开。
1429+
</small>
1430+
</label>
14111431
</section>
14121432

14131433
<section className="settings-section reader-settings-section">

src/lib/library.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const DEFAULT_CONFIG = {
1818
density: "comfortable",
1919
proxyTemplate: DEFAULT_PROXY_TEMPLATE,
2020
aiWorkerUrl: DEFAULT_AI_WORKER_URL,
21+
aiAuthMode: "none",
2122
};
2223

2324
export function createDefaultLibrary() {

wrangler.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"workers_dev": true,
66
"vars": {
77
"ALLOW_ORIGIN": "*",
8+
"ALLOW_CREDENTIALS": "false",
89
"ALLOWED_HOSTS": "",
910
"MAX_BYTES": "8388608",
1011
"AI_BASE_URL": "https://api.openai.com/v1",

0 commit comments

Comments
 (0)