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,
});
});





