diff --git a/blog-admin/.gitignore b/blog-admin/.gitignore
index a0c49463..df347610 100644
--- a/blog-admin/.gitignore
+++ b/blog-admin/.gitignore
@@ -2,6 +2,7 @@
node_modules/
.wrangler/
.selftest-build/
+.mail-build/
*.log
# 机密(本地开发用,不提交)
diff --git a/blog-admin/public/admin/admin.js b/blog-admin/public/admin/admin.js
index 73f7e38b..0e58569b 100644
--- a/blog-admin/public/admin/admin.js
+++ b/blog-admin/public/admin/admin.js
@@ -346,6 +346,12 @@ function renderTabs() {
html += '';
}
+ // 国内直连入口:Cloudflare 晚高峰丢包,写文章卡的时候一键切到国内机后台
+ // (那边独立登录,点过去会由 Worker 先签一张「免登录票」换会话 Cookie)
+ html += '
' +
+ '线路
' +
+ '';
$('#tabs').innerHTML = html;
}
@@ -1846,6 +1852,19 @@ document.addEventListener('click', async (e) => {
return;
}
+ if (act === 'cn-line') {
+ // 一键跳国内机后台:先向 Worker 要一张「免登录票」(Worker 用共享令牌
+ // 签名,国内机验签后直接发会话 Cookie),失败就退回普通地址手动登录。
+ toast('正在打开国内线路…');
+ let url = 'https://writeapi.usj.cc/admin/';
+ try {
+ const d = await api('/editor/handoff');
+ if (d && d.url) url = d.url;
+ } catch (e) { /* 拿不到票就直开,让用户手动登录 */ }
+ window.open(url, '_blank', 'noopener');
+ return;
+ }
+
// --- 文章编辑
if (act === 'po-reload') { el.dataset.busy = '1'; try { await loadPosts(); } finally { delete el.dataset.busy; } return; }
if (act === 'po-prev') { state.po.page = Math.max(1, state.po.page - 1); await loadPosts(); return; }
diff --git a/blog-admin/src/index.ts b/blog-admin/src/index.ts
index 03926002..0ea846af 100644
--- a/blog-admin/src/index.ts
+++ b/blog-admin/src/index.ts
@@ -45,9 +45,13 @@ router.post('/editor/upload', E.upload);
router.get('/editor/git/status', E.gitStatus);
router.post('/editor/git/publish', E.publish);
router.post('/editor/git/sync', E.sync);
+router.get('/editor/handoff', E.handoff);
// ---------------------------------------------------------------- 评论
+// ★ /comments/lookup 必须注册在 /comments/:id 之前 —— 路由按注册顺序匹配,
+// 两段路径下 `:id` 会把 "lookup" 吃掉。
router.get('/comments', C.listComments);
+router.get('/comments/lookup', C.lookupCommenter);
router.post('/comments', C.createComment);
router.get('/comments/:id', C.getComment);
router.put('/comments/:id', C.updateComment);
diff --git a/blog-admin/src/lib/mail.ts b/blog-admin/src/lib/mail.ts
index f69b2c3c..2df21b57 100644
--- a/blog-admin/src/lib/mail.ts
+++ b/blog-admin/src/lib/mail.ts
@@ -107,115 +107,169 @@ export async function mailQuotaOk(env: Env, limit = 90): Promise {
}
// ============================================================================
-// 模板 —— 与 artalk.yml 里 mail_tpl / notify_tpl 指向的两个文件一致
-// 改模板只需要动下面两段字符串
+// 模板 —— 微信聊天式:灰底 #EDEDED、自己的评论绿色气泡(#95EC69)靠右、
+// 对方的白色气泡靠左、圆角方头像、时间居中 —— 用大家天天在看的聊天直觉
+// 来传达「谁说了什么」,一眼就懂,不需要解释。
//
-// 风格:聊天对话式(自己的评论靠右绿色气泡,对方靠左白色气泡),
-// 配色清新(浅薄荷底 + 白卡 + 鲜绿主色 #2fa872)。
+// 以站点的老模板(notice.html / reply.html)为底改:头像(weavatar,邮箱
+// md5,明文不出现在邮件里)、点气泡跳转评论页这些老设计都保留。
+//
+// 相比老模板的两处实质修复:
+// 1. 表情包尺寸内联到位:老模板靠 head 里 `.comment_img img{width:100px}`
+// 控制大小,而不少邮件客户端会把 head 的 `;
+
+/** 头像:40px 圆角方(微信样式),weavatar 按 Gravatar 规则用邮箱 md5 取图 */
+const AVATAR = (prefix: string, nick: string) =>
+ `
`;
+
+/**
+ * 气泡:bg 为 #95ec69(自己)/ #ffffff(对方)。
+ * 气泡本身是一个嵌套 (宽度随内容收缩、上限 360px),比 inline-block
+ * 在邮件客户端里稳得多 —— 图片的 max-width 百分比也能正确解析。
+ * align="right" 时气泡贴右(微信里自己说的话)。
+ */
+const BUBBLE = (bg: string, color: string, inner: string, align?: 'right') =>
+ ``;
+
+/** 回复提醒:自己的评论(绿泡靠右)+ 对方的新回复(白泡靠左),微信式上下两条 */
const REPLY_TPL = `
+
+${HEAD_STYLE}
-
-
+
+
-
-
{{site_name_first}}
-
{{site_name}}
+
+
{{parent_comment.datetime}}
+
+
-
-
-
《{{page_title}}》 · 对话
+
{{comment.datetime}}
-
-
{{parent_comment.name_first}}
-
-
{{parent_comment.nick}} · {{parent_comment.datetime}}
-
-
-
-
-
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
-
-
-
-
-
- 查看并回复
-
+
-
- 此邮件来自
{{site_name}},请勿直接回复
- 如果你不想再收到这类通知,可以在评论时关闭"接收邮件通知"
+
+
+
`;
-/** 管理员通知(原 notice.html) */
+/** 管理员通知:一条新评论(白泡靠左),微信式 */
const NOTICE_TPL = `
+
+${HEAD_STYLE}
-
-
+
+
-
-
{{site_name_first}}
-
{{site_name}}
+
+
{{comment.datetime}}
+
+
-
-
-
《{{page_title}}》 · 新留言
+
-
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
-
-
-
-
-
- 前往查看
-
-
-
-
`;
-/** 主题沿用 artalk.yml 里的 mail_subject */
-export const REPLY_SUBJECT = '{{site_name}} · 新的评论回复';
-export const ADMIN_SUBJECT = '{{site_name}} · 新的评论待审';
+/**
+ * 邮件主题。
+ * 回复:沿用老配置「新的评论回复」。
+ * 管理员:原来是「新的评论待审」——像系统告警,改成一句人话。
+ */
+export const REPLY_SUBJECT = '{{site_name}} · 您的评论收到了新回复';
+export const ADMIN_SUBJECT = '{{site_name}} · 您有一个新的评论';
// ------------------------------------------------------------------ 渲染
@@ -226,18 +280,27 @@ const esc = (v: unknown): string =>
/**
* 评论正文 → 邮件里可用的 HTML。
- * 先走 Markdown(保留正文里的原生 HTML),再把表情图加上内联宽度 ——
- * 原模板靠