diff --git a/cwd-comments-admin/src/api/http.ts b/cwd-comments-admin/src/api/http.ts index 8c66460..de58b09 100644 --- a/cwd-comments-admin/src/api/http.ts +++ b/cwd-comments-admin/src/api/http.ts @@ -1,8 +1,19 @@ -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, ''); +const rawEnvApiBaseUrl = (import.meta.env.VITE_API_BASE_URL || '').trim(); + +function getApiBaseUrl(): string { + const stored = (localStorage.getItem('cwd_admin_api_base_url') || '').trim(); + const source = stored || rawEnvApiBaseUrl; + const apiBaseUrl = source.replace(/\/+$/, ''); + if (!apiBaseUrl) { + throw new Error('未配置 API 地址,请在登录页填写后重试'); + } + return apiBaseUrl; +} type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; async function request(method: HttpMethod, path: string, body?: unknown): Promise { + const apiBaseUrl = getApiBaseUrl(); const token = localStorage.getItem('cwd_admin_token'); const headers: HeadersInit = {}; if (body !== undefined) { @@ -11,7 +22,7 @@ async function request(method: HttpMethod, path: string, body?: unknown): Pro if (token) { headers['Authorization'] = `Bearer ${token}`; } - const res = await fetch(`${API_BASE_URL}${path}`, { + const res = await fetch(`${apiBaseUrl}${path}`, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined @@ -44,4 +55,3 @@ export function put(path: string, body?: unknown): Promise { export function del(path: string): Promise { return request('DELETE', path); } - diff --git a/cwd-comments-admin/src/views/CommentsView.vue b/cwd-comments-admin/src/views/CommentsView.vue index 20fef71..b6e544c 100644 --- a/cwd-comments-admin/src/views/CommentsView.vue +++ b/cwd-comments-admin/src/views/CommentsView.vue @@ -1,254 +1,421 @@ - diff --git a/cwd-comments-admin/src/views/LoginView.vue b/cwd-comments-admin/src/views/LoginView.vue index 2135780..e93989b 100644 --- a/cwd-comments-admin/src/views/LoginView.vue +++ b/cwd-comments-admin/src/views/LoginView.vue @@ -1,127 +1,156 @@ - diff --git a/cwd-comments-admin/vite.config.ts b/cwd-comments-admin/vite.config.ts index 59b4f41..60c8c59 100644 --- a/cwd-comments-admin/vite.config.ts +++ b/cwd-comments-admin/vite.config.ts @@ -4,7 +4,7 @@ import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], server: { - port: 5176 + port: 1226 } }); diff --git a/cwd-comments-api/.dev.vars.example b/cwd-comments-api/.dev.vars.example deleted file mode 100644 index a865df8..0000000 --- a/cwd-comments-api/.dev.vars.example +++ /dev/null @@ -1,5 +0,0 @@ -ALLOW_ORIGIN="http://localhost:4321,https://blog.example.top" -CF_FROM_EMAIL="noreply@yourdomain.com" -EMAIL_ADDRESS="admin@example.top" -ADMIN_NAME="Admin" -ADMIN_PASSWORD="password" diff --git a/cwd-comments-api/src/api/admin/getAdminEmail.ts b/cwd-comments-api/src/api/admin/getAdminEmail.ts index 88c7a7f..97be819 100644 --- a/cwd-comments-api/src/api/admin/getAdminEmail.ts +++ b/cwd-comments-api/src/api/admin/getAdminEmail.ts @@ -3,14 +3,14 @@ import { Bindings } from '../../bindings'; export const getAdminEmail = async (c: Context<{ Bindings: Bindings }>) => { try { - let email: string | null = null; - if (c.env.CWD_CONFIG_KV) { - email = await c.env.CWD_CONFIG_KV.get('settings:admin_notify_email'); - } - if (!email) { - email = c.env.EMAIL_ADDRESS || null; - } - return c.json({ email }); + await c.env.CWD_DB.prepare( + 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' + ).run(); + const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?') + .bind('admin_notify_email') + .first<{ value: string }>(); + const email = row?.value || c.env.EMAIL_ADDRESS || null; + return c.json({ email: email }); } catch (e: any) { return c.json({ message: e.message }, 500); } diff --git a/cwd-comments-api/src/api/admin/setAdminEmail.ts b/cwd-comments-api/src/api/admin/setAdminEmail.ts index d66c2e1..6c4add1 100644 --- a/cwd-comments-api/src/api/admin/setAdminEmail.ts +++ b/cwd-comments-api/src/api/admin/setAdminEmail.ts @@ -11,10 +11,12 @@ export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => { if (!email || !isValidEmail(email)) { return c.json({ message: '邮箱格式不正确' }, 400); } - if (!c.env.CWD_CONFIG_KV) { - return c.json({ message: '未配置 CWD_CONFIG_KV,无法保存设置' }, 500); - } - await c.env.CWD_CONFIG_KV.put('settings:admin_notify_email', email); + await c.env.CWD_DB.prepare( + 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' + ).run(); + await c.env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)') + .bind('admin_notify_email', email) + .run(); return c.json({ message: '保存成功' }); } catch (e: any) { return c.json({ message: e.message }, 500); diff --git a/cwd-comments-api/src/api/public/postComment.ts b/cwd-comments-api/src/api/public/postComment.ts index f08136b..26707ea 100644 --- a/cwd-comments-api/src/api/public/postComment.ts +++ b/cwd-comments-api/src/api/public/postComment.ts @@ -58,6 +58,17 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { // 3. 准备数据 const content = checkContent(rawContent); const author = checkContent(rawAuthor); + + console.log('PostComment:request', { + postSlug: post_slug, + hasParent: !!parent_id, + author, + email, + ip, + hasSendEmailBinding: !!c.env.SEND_EMAIL, + fromEmail: c.env.CF_FROM_EMAIL, + emailAddressEnv: c.env.EMAIL_ADDRESS + }); const uaParser = new UAParser(userAgent); const uaResult = uaParser.getResult(); @@ -88,12 +99,20 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { if (!success) throw new Error("Database insert failed"); + console.log('PostComment:inserted', { + postSlug: post_slug, + hasParent: !!parent_id, + ip + }); + // 5. 发送邮件通知 (后台异步执行,不阻塞用户响应) if (c.env.SEND_EMAIL && c.env.CF_FROM_EMAIL) { + console.log('PostComment:mailDispatch:start', { + hasParent: !!data.parent_id + }); c.executionCtx.waitUntil((async () => { try { if (data.parent_id) { - // 回复逻辑:查询父评论信息 const parentComment = await c.env.CWD_DB.prepare( "SELECT author, email, content_text FROM Comment WHERE id = ?" ).bind(data.parent_id).first<{ author: string, email: string, content_text: string }>(); @@ -104,6 +123,10 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { ).bind(parentComment.email).first<{ created_at: string }>(); const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000); if (canSendUserMail) { + console.log('PostComment:mailDispatch:userReply:send', { + toEmail: parentComment.email, + toName: parentComment.author + }); await sendCommentReplyNotification(c.env, { toEmail: parentComment.email, toName: parentComment.author, @@ -116,15 +139,23 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { await c.env.CWD_DB.prepare( "INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)" ).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run(); + console.log('PostComment:mailDispatch:userReply:logInserted', { + toEmail: parentComment.email + }); + } + if (!canSendUserMail) { + console.log('PostComment:mailDispatch:userReply:skippedByRateLimit', { + toEmail: parentComment.email + }); } } } else { - // 新评论通知站长 const adminEmailRow = await c.env.CWD_DB.prepare( "SELECT created_at FROM EmailLog WHERE type = 'admin-notify' ORDER BY created_at DESC LIMIT 1" ).first<{ created_at: string }>(); const canSendAdminMail = !adminEmailRow || (Date.now() - new Date(adminEmailRow.created_at).getTime() > 15 * 1000); if (canSendAdminMail) { + console.log('PostComment:mailDispatch:admin:send'); await sendCommentNotification(c.env, { postTitle: data.post_title, postUrl: data.post_url, @@ -134,12 +165,21 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { await c.env.CWD_DB.prepare( "INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)" ).bind('admin', 'admin-notify', ip, new Date().toISOString()).run(); + console.log('PostComment:mailDispatch:admin:logInserted'); + } + if (!canSendAdminMail) { + console.log('PostComment:mailDispatch:admin:skippedByRateLimit'); } } } catch (mailError) { console.error("Mail Notification Failed:", mailError); } })()); + } else { + console.log('PostComment:mailDispatch:skipNoBinding', { + hasSendEmailBinding: !!c.env.SEND_EMAIL, + fromEmail: c.env.CF_FROM_EMAIL + }); } return c.json({ message: "Comment submitted. Awaiting moderation." }); diff --git a/cwd-comments-api/src/bindings.ts b/cwd-comments-api/src/bindings.ts index 2177279..45b8b3e 100644 --- a/cwd-comments-api/src/bindings.ts +++ b/cwd-comments-api/src/bindings.ts @@ -1,7 +1,6 @@ export type Bindings = { CWD_DB: D1Database CWD_AUTH_KV: KVNamespace; - CWD_CONFIG_KV?: KVNamespace; ALLOW_ORIGIN: string CF_FROM_EMAIL?: string SEND_EMAIL?: { diff --git a/cwd-comments-api/src/index.ts b/cwd-comments-api/src/index.ts index d9b4b82..671035e 100644 --- a/cwd-comments-api/src/index.ts +++ b/cwd-comments-api/src/index.ts @@ -14,8 +14,24 @@ import { getAdminEmail } from './api/admin/getAdminEmail'; import { setAdminEmail } from './api/admin/setAdminEmail'; const app = new Hono<{ Bindings: Bindings }>(); +const VERSION = 'v0.0.1'; + +app.use('*', async (c, next) => { + console.log('Request:start', { + method: c.req.method, + path: c.req.path, + url: c.req.url, + hasDb: !!c.env.CWD_DB, + hasAuthKv: !!c.env.CWD_AUTH_KV + }); + const res = await next(); + console.log('Request:end', { + method: c.req.method, + path: c.req.path + }); + return res; +}); -// 跨域 app.use('/api/*', async (c, next) => { const corsMiddleware = customCors(c.env.ALLOW_ORIGIN); return corsMiddleware(c, next); @@ -25,7 +41,12 @@ app.use('/admin/*', async (c, next) => { return corsMiddleware(c, next); }); -// API +app.get('/', (c) => { + return c.html( + `CWD 评论部署成功,当前版本 ${VERSION},查看文档` + ); +}); + app.get('/api/comments', getComments); app.post('/api/comments', postComment); diff --git a/cwd-comments-api/src/utils/cors.ts b/cwd-comments-api/src/utils/cors.ts index e2130bc..058c857 100644 --- a/cwd-comments-api/src/utils/cors.ts +++ b/cwd-comments-api/src/utils/cors.ts @@ -1,23 +1,12 @@ import { cors } from 'hono/cors' -export const customCors = (allowOriginStr: string | undefined) => { - // 1. 将环境变量字符串解析为数组 - // 如果环境变量不存在,则默认为空数组 - const allowedOrigins = allowOriginStr - ? allowOriginStr.split(',').map(origin => origin.trim()) - : [] - +export const customCors = (_allowOriginStr: string | undefined) => { return cors({ - origin: (origin) => { - // 如果请求的 origin 在白名单中,或者是本地文件(null) - if (!origin || allowedOrigins.includes(origin)) { - return origin - } - }, + origin: '*', allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], exposeHeaders: ['Content-Length'], maxAge: 600, - credentials: true, + credentials: false, }) -} \ No newline at end of file +} diff --git a/cwd-comments-api/src/utils/email.ts b/cwd-comments-api/src/utils/email.ts index bae4dea..e08351c 100644 --- a/cwd-comments-api/src/utils/email.ts +++ b/cwd-comments-api/src/utils/email.ts @@ -17,6 +17,14 @@ export async function sendCommentReplyNotification( ) { const { toEmail, toName, postTitle, parentComment, replyAuthor, replyContent, postUrl } = params; + console.log('EmailReplyNotification:start', { + toEmail, + toName, + postTitle, + fromEmail: env.CF_FROM_EMAIL, + hasSendBinding: !!env.SEND_EMAIL + }); + const html = `

Hi ${toName}

@@ -39,6 +47,10 @@ export async function sendCommentReplyNotification( `; if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) { + console.error('EmailReplyNotification:missingBinding', { + hasSendBinding: !!env.SEND_EMAIL, + fromEmail: env.CF_FROM_EMAIL + }); throw new Error('未配置邮件发送绑定或发件人地址'); } @@ -48,6 +60,10 @@ export async function sendCommentReplyNotification( subject: `你在 example.com 上的评论有了新回复`, html }); + + console.log('EmailReplyNotification:sent', { + toEmail + }); } /** @@ -65,6 +81,13 @@ export async function sendCommentNotification( const { postTitle, postUrl, commentAuthor, commentContent } = params; const toEmail = await getAdminNotifyEmail(env); + console.log('EmailAdminNotification:start', { + toEmail, + postTitle, + fromEmail: env.CF_FROM_EMAIL, + hasSendBinding: !!env.SEND_EMAIL + }); + const html = `

${commentAuthor} 在文章《${postTitle}》下发表了评论:

@@ -76,6 +99,10 @@ export async function sendCommentNotification( `; if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) { + console.error('EmailAdminNotification:missingBinding', { + hasSendBinding: !!env.SEND_EMAIL, + fromEmail: env.CF_FROM_EMAIL + }); throw new Error('未配置邮件发送绑定或发件人地址'); } @@ -85,14 +112,31 @@ export async function sendCommentNotification( subject: `新评论通知:${postTitle}`, html }); + + console.log('EmailAdminNotification:sent', { + toEmail + }); } -// 读取管理员通知邮箱:优先 KV 设置,其次环境变量 async function getAdminNotifyEmail(env: Bindings): Promise { - if (env.CWD_CONFIG_KV) { - const val = await env.CWD_CONFIG_KV.get('settings:admin_notify_email'); - if (val) return val; + await env.CWD_DB.prepare( + 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' + ).run(); + const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?') + .bind('admin_notify_email') + .first<{ value: string }>(); + if (row?.value) { + console.log('EmailAdminNotification:useDbEmail', { + email: row.value + }); + return row.value; } - if (env.EMAIL_ADDRESS) return env.EMAIL_ADDRESS; + if (env.EMAIL_ADDRESS) { + console.log('EmailAdminNotification:useEnvEmail', { + email: env.EMAIL_ADDRESS + }); + return env.EMAIL_ADDRESS; + } + console.error('EmailAdminNotification:noAdminEmail'); throw new Error('未配置管理员通知邮箱'); } diff --git a/cwd-comments-api/wrangler.jsonc.example b/cwd-comments-api/wrangler.jsonc.example index b4ccee9..2c47499 100644 --- a/cwd-comments-api/wrangler.jsonc.example +++ b/cwd-comments-api/wrangler.jsonc.example @@ -7,9 +7,6 @@ "name": "cwd-comments", "main": "src/index.ts", "compatibility_date": "2026-01-03", - "observability": { - "enabled": true - }, "workers_dev": true, "preview_urls": true } \ No newline at end of file diff --git a/docs/guide/backend-config.md b/docs/guide/backend-config.md index 4a8f63e..b58d1fd 100644 --- a/docs/guide/backend-config.md +++ b/docs/guide/backend-config.md @@ -50,11 +50,10 @@ npm install ] ``` 如果`binding`字段不是`CWD_DB`,请修改为`CWD_DB` - + * **创建 KV 存储**,如果遇到提示,按回车继续 ```bash npx wrangler kv namespace create CWD_AUTH_KV - npx wrangler kv namespace create CWD_CONFIG_KV ``` 运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置 ```jsonc @@ -62,10 +61,6 @@ npm install { "binding": "CWD_AUTH_KV", "id": "xxxxxxx" // KV 存储 ID - }, - { - "binding": "CWD_CONFIG_KV", - "id": "xxxxxxx" // KV 存储 ID } ] ``` diff --git a/widget/src/components/CommentForm.js b/widget/src/components/CommentForm.js index da4bbe2..b0ea4c0 100644 --- a/widget/src/components/CommentForm.js +++ b/widget/src/components/CommentForm.js @@ -159,19 +159,15 @@ export class CommentForm extends Component { updateErrors(formErrors) { if (!this.elements.root) return; - // 昵称错误 - const authorInput = this.elements.root.querySelector('input[placeholder*="昵称"]'); + const authorInput = this.elements.root.querySelector('input[name="author"]'); this.updateFieldError(authorInput, formErrors?.author); - // 邮箱错误 - const emailInput = this.elements.root.querySelector('input[placeholder*="邮箱"]'); + const emailInput = this.elements.root.querySelector('input[name="email"]'); this.updateFieldError(emailInput, formErrors?.email); - // 网址错误 - const urlInput = this.elements.root.querySelector('input[placeholder*="网址"]'); + const urlInput = this.elements.root.querySelector('input[name="url"]'); this.updateFieldError(urlInput, formErrors?.url); - // 内容错误 const contentTextarea = this.elements.root.querySelector('textarea'); this.updateFieldError(contentTextarea, formErrors?.content); } @@ -216,7 +212,8 @@ export class CommentForm extends Component { className: `cwd-form-input ${error ? 'cwd-input-error' : ''}`, attributes: { type, - placeholder, + name: fieldName, + value: value || '', disabled: this.props.submitting, onInput: (e) => this.handleFieldChange(fieldName, e.target.value), }, @@ -230,9 +227,9 @@ export class CommentForm extends Component { * 设置输入框的值 */ setInputValues(root, form) { - const authorInput = root.querySelector('input[placeholder*="昵称"]'); - const emailInput = root.querySelector('input[placeholder*="邮箱"]'); - const urlInput = root.querySelector('input[placeholder*="网址"]'); + const authorInput = root.querySelector('input[name="author"]'); + const emailInput = root.querySelector('input[name="email"]'); + const urlInput = root.querySelector('input[name="url"]'); const contentTextarea = root.querySelector('textarea'); if (authorInput) authorInput.value = form.author || ''; diff --git a/widget/src/dev.js b/widget/src/dev.js index 8ff4304..e3b32c9 100644 --- a/widget/src/dev.js +++ b/widget/src/dev.js @@ -10,7 +10,7 @@ const STORAGE_KEY = 'cwd-dev-config'; // 默认配置 const DEFAULT_CONFIG = { apiBaseUrl: 'http://localhost:8788', - postSlug: 'demo-post', + postSlug: "https://zishu.me/message", theme: 'light', avatarPrefix: 'https://gravatar.com/avatar', }; diff --git a/widget/vite.config.js b/widget/vite.config.js index 80dfc51..3815d27 100644 --- a/widget/vite.config.js +++ b/widget/vite.config.js @@ -3,34 +3,30 @@ import { resolve } from 'path'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; export default defineConfig({ - plugins: [cssInjectedByJsPlugin()], - resolve: { - alias: { - '@': resolve(__dirname, 'src') - } - }, - build: { - lib: { - name: 'CWDComments', - entry: resolve(__dirname, 'src/index.js'), - formats: ['umd'], - fileName: (format) => `cwd-comments.js` - }, - rollupOptions: { - output: { - exports: 'named' - } - }, - sourcemap: false, - minify: 'terser', - terserOptions: { - compress: { - drop_console: false - } - } - }, - server: { - port: 5173, - open: false - } + plugins: [cssInjectedByJsPlugin()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + build: { + lib: { + name: 'CWDComments', + entry: resolve(__dirname, 'src/index.js'), + formats: ['umd'], + fileName: (format) => `cwd-comments.js`, + }, + rollupOptions: { + output: { + exports: 'named', + }, + }, + sourcemap: false, + minify: 'terser', + terserOptions: { + compress: { + drop_console: false, + }, + }, + }, });