From 6046eb494cb6d41fe17005e5085f6bdf3cb2a79f Mon Sep 17 00:00:00 2001 From: zqlit Date: Mon, 5 Oct 2026 07:36:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E6=A8=A1=E6=9D=BF=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=8C=96=20+=20=E8=AF=84=E8=AE=BA=E8=80=81=E5=8F=8B?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=A1=AB=E5=85=85=20+=20CF=20=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=9B=BD=E5=86=85=E7=BA=BF=E8=B7=AF=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 邮件模板重做成微信聊天式:灰底、自己评论绿色气泡靠右、对方白色气泡靠左、 40px 圆角方头像(weavatar,邮箱 md5 不外泄)、时间居中、#07C160 主按钮; 布局全嵌套表格(Outlook 稳),表情内联 max-height:28px(修掉「表情太大」)、 配图限宽 100%;管理员主题从「新的评论待审」改成「您有一个新的评论」 - 老友自动填充:新增 /comments/lookup(只回昵称+网址,绝不含邮箱;IP 限速 30 次/10 分钟)+ 主题 comment-autofill 模块(填昵称弹提示、填邮箱自动带出 上次的昵称/网址,事件委托不怕 Artalk 重建编辑器) - CF 后台新增「国内线路」一键跳:/editor/handoff 用共享令牌对时间戳 HMAC 签名 → editor-api /admin/handoff 验签发会话 Cookie(2 分钟有效、防重放), 晚高峰丢包时直达国内机后台免重新登录 --- blog-admin/.gitignore | 1 + blog-admin/public/admin/admin.js | 19 ++ blog-admin/src/index.ts | 4 + blog-admin/src/lib/mail.ts | 217 +++++++++++------- blog-admin/src/routes/comments.ts | 38 +++ blog-admin/src/routes/editor.ts | 36 +++ blog-admin/tools/preview-mail.ts | 28 ++- editor-api/server.mjs | 39 ++++ .../assets/js/modules/comment-autofill.js | 141 ++++++++++++ themes/Ying/layouts/partials/footer.html | 7 +- 10 files changed, 443 insertions(+), 87 deletions(-) create mode 100644 themes/Ying/assets/js/modules/comment-autofill.js 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) => + `${nick}`; + +/** + * 气泡:bg 为 #95ec69(自己)/ #ffffff(对方)。 + * 气泡本身是一个嵌套 (宽度随内容收缩、上限 360px),比 inline-block + * 在邮件客户端里稳得多 —— 图片的 max-width 百分比也能正确解析。 + * align="right" 时气泡贴右(微信里自己说的话)。 + */ +const BUBBLE = (bg: string, color: string, inner: string, align?: 'right') => + `
+ +
${inner}
`; + +/** 回复提醒:自己的评论(绿泡靠右)+ 对方的新回复(白泡靠左),微信式上下两条 */ const REPLY_TPL = ` + +${HEAD_STYLE} - -
+ +
-
- {{site_name_first}} - {{site_name}} +
你在《{{page_title}}》的评论有回复
+
{{parent_comment.datetime}}
+ +
+ + + + + +
+
{{parent_comment.nick}}
+ ${BUBBLE('#95ec69', '#111111', `${BODY_OPEN}{{parent_comment.content}}${BODY_CLOSE}`, 'right')} +
${AVATAR('parent_comment', '{{parent_comment.nick}}')}
-
-
-
《{{page_title}}》 · 对话
+
{{comment.datetime}}
-
-
{{parent_comment.name_first}}
-
-
{{parent_comment.nick}} · {{parent_comment.datetime}}
-
{{parent_comment.content}}
-
-
- -
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
{{comment.content}}
-
-
-
- - - 查看并回复 - +
+ + + + + +
${AVATAR('comment', '{{comment.nick}}')} +
{{comment.nick}}
+ ${BUBBLE('#ffffff', '#111111', `${BODY_OPEN}{{comment.content}}${BODY_CLOSE}`)} +
-
- 此邮件来自 {{site_name}},请勿直接回复
- 如果你不想再收到这类通知,可以在评论时关闭"接收邮件通知" + + +
+ + +
+ 查看并回复 +
+
+ +
+ 此邮件来自{{site_name}},请勿直接回复
+ 不想再收到这类提醒,可以在评论时取消勾选「接收邮件通知」
`; -/** 管理员通知(原 notice.html) */ +/** 管理员通知:一条新评论(白泡靠左),微信式 */ const NOTICE_TPL = ` + +${HEAD_STYLE} - -
+ +
-
- {{site_name_first}} - {{site_name}} +
《{{page_title}}》有了新的评论
+
{{comment.datetime}}
+ +
+ + + + + +
${AVATAR('comment', '{{comment.nick}}')} +
{{comment.nick}}
+ ${BUBBLE('#ffffff', '#111111', `${BODY_OPEN}{{comment.content}}${BODY_CLOSE}`)} +
-
-
-
《{{page_title}}》 · 新留言
+ + +
+ + +
+ 前往查看 +
+
-
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
{{comment.content}}
-
-
-
- - - 前往查看 - -
- -
- 此邮件来自 {{site_name}},请勿直接回复 +
+ 此邮件来自{{site_name}}博客,请勿直接回复
`; -/** 主题沿用 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),再把表情图加上内联宽度 —— - * 原模板靠