Afterglow | 余火
这首歌关于一个庆祝,庆祝什么呢,庆祝我们的遗憾,此时此刻我们是被我们过往所有的经历堆叠而成的,包括艰难的时刻,所以让我们点起火把。
———— 《当我们点起火把》 DOUDOU
开源地址
同时感谢:谷歌大善人和他的 AI Studio,Gemini-3-Flash 以及 Antigravity
UI 速览
你也可以前往 CF 部署的 web 版本体验:https://afterglow.realme.top/
| 首页 | 记录 - 编辑 | 记录 | 设置 |
|---|---|---|---|
功能
- 目标打卡,同时可以设置一个主目标,在首页主要区域显示。
- 随时补卡,无需任何前置条件
- 心情记录,记录下每一次打卡时的小感触
- 支持英语 / 中文切换
- 热力图(可选),查看到目前为止的数天内打卡频率
- 一言(可选),基于 API 的每日一言(zh/en 下 API 不同)
- webdav 备份,用于备份、恢复数据。
使用
Afterglow 使用 React 编写,且无后端服务,是一个「纯前端」打卡 APP。可以选择以下方式使用:
- 可以前往 * Release 页面下载随更新后编译的安卓 apk 文件
- 或者使用某个小老鼠在 Cloudflare 部署的网站:https://afterglow.realme.top/
数据
目前所有的数据使用本地的 IndexedDB,可以通过 webdav 进行备份 / 恢复。
后续会尝试支持 Supabase/Cloudflare 部署 API+DB
WebDAV 备份
数据很重要(嗯
所以为了有效保持自己的小习惯能被很好的记录,可以使用 webdav 进行备份。
个人使用坚果云(免费)来作为 webdav 备份(https://dav.jianguoyun.com/dav/),因为可能遇到的 CORS 问题,也支持自建一个 CORS 代理来绕过可能出现的请求失败问题。
具体而言,可以使用我在 deno 部署的项目:https://cors-proxy.heerheer.deno.net/?url=,也可以采用其他的开源自部署项目,我部署的版本代码放在文末~
未来计划
peer review (什么赛博监视)- 自部署服务端与数据库支持
- 将数据生成分享图 / 时段总结
附录
CORS-Proxy Deno
// main.ts — Deno Deploy CORS Proxy (safe-ish) // Deploy: https://deno.com/deploy // Usage: /?url=https%3A%2F%2Fexample.com%2Fapi const ALLOWED_ORIGINS = ["*"]; // 你也可以改成 ["https://yourdomain.com"] const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS","MKCOL","PROPFIND"];
// 目标站点白名单:只允许代理这些域名(强烈建议填写) // 例: ["api.github.com", "v1.hitokoto.cn"] const TARGET_HOST_ALLOWLIST = new Set<string>([
"v1.hitokoto.cn",
"thequoteshub.com",
// "api.example.com",
]);
// 允许转发的请求头(避免转发一些危险/无意义的头) const FORWARD_REQ_HEADERS = new Set([
"accept",
"accept-language",
"content-type",
"authorization",
"x-requested-with",
"user-agent",
]);
// 允许暴露给浏览器端读取的响应头 const EXPOSE_HEADERS = [
"content-type",
"content-length",
"date",
"etag",
"cache-control",
"expires",
"last-modified",
"x-request-id",
];
function pickCorsOrigin(origin: string | null) {
if (!origin) return "*";
if (ALLOWED_ORIGINS.includes("*")) return "*";
return ALLOWED_ORIGINS.includes(origin) ? origin : "null";
}
function isAllowedTarget(url: URL) {
// 仅允许 http/https if (!["http:", "https:"].includes(url.protocol)) return false;
// // 白名单校验 // return TARGET_HOST_ALLOWLIST.has(url.host); return true;
}
function buildCorsHeaders(origin: string | null) {
const allowOrigin = pickCorsOrigin(origin);
return new Headers({
"access-control-allow-origin": allowOrigin,
"access-control-allow-methods": ALLOWED_METHODS.join(", "),
"access-control-allow-headers": "Content-Type, Authorization, X-Requested-With,Depth",
"access-control-expose-headers": EXPOSE_HEADERS.join(", "),
"access-control-max-age": "86400",
"vary": "Origin",
});
}
function filterRequestHeaders(req: Request) {
const headers = new Headers();
for (const [k, v] of req.headers.entries()) {
const key = k.toLowerCase();
if (FORWARD_REQ_HEADERS.has(key)) headers.set(k, v);
}
return headers;
}
Deno.serve(async (req) => {
const url = new URL(req.url);
const origin = req.headers.get("origin");
// 预检请求直接返回 if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers: buildCorsHeaders(origin) });
}
if (!ALLOWED_METHODS.includes(req.method)) {
const h = buildCorsHeaders(origin);
return new Response("Method Not Allowed", { status: 405, headers: h });
}
// 读取目标 URL const targetParam = url.searchParams.get("url");
if (!targetParam) {
const h = buildCorsHeaders(origin);
return new Response("Missing ?url= encoded target URL", { status: 400, headers: h });
}
let targetUrl: URL;
try {
targetUrl = new URL(decodeURIComponent(targetParam));
} catch {
const h = buildCorsHeaders(origin);
return new Response("Invalid target url", { status: 400, headers: h });
}
if (!isAllowedTarget(targetUrl)) {
const h = buildCorsHeaders(origin);
return new Response("Target host not allowed", { status: 403, headers: h });
}
// 把当前请求的 query(除了 url)拼到目标上(可选) // 例如 /?url=...&a=1 => target?a=1 for (const [k, v] of url.searchParams.entries()) {
if (k === "url") continue;
targetUrl.searchParams.set(k, v);
}
// 构造转发请求 const forwardHeaders = filterRequestHeaders(req);
// 如果是 GET/HEAD 不带 body,其它方法允许透传 body const hasBody = !["GET", "HEAD"].includes(req.method);
const body = hasBody ? req.body : undefined;
let upstreamResp: Response;
try {
upstreamResp = await fetch(targetUrl.toString(), {
method: req.method,
headers: forwardHeaders,
body,
redirect: "follow",
});
} catch (e) {
const h = buildCorsHeaders(origin);
return new Response(`Upstream fetch failed: ${String(e)}`, { status: 502, headers: h });
}
// 复制响应头(过滤掉 hop-by-hop 以及冲突的 CORS 头) const respHeaders = new Headers();
for (const [k, v] of upstreamResp.headers.entries()) {
const key = k.toLowerCase();
if (key === "set-cookie") continue; // 浏览器也读不到,且有安全风险 if (key.startsWith("access-control-")) continue; // 我们自己控制 CORS
respHeaders.set(k, v);
}
// 注入 CORS 头 const corsHeaders = buildCorsHeaders(origin);
for (const [k, v] of corsHeaders.entries()) respHeaders.set(k, v);
return new Response(upstreamResp.body, {
status: upstreamResp.status,
statusText: upstreamResp.statusText,
headers: respHeaders,
});
});






评论区(暂无评论)