diff --git a/.env.example b/.env.example index d32f014..e78bbd8 100644 --- a/.env.example +++ b/.env.example @@ -56,6 +56,11 @@ SMTP_PASS=your-password SMTP_FROM="即页 " APP_URL=http://localhost:8858 +# --- 反馈邮件接收地址(可选)--- +# 用户通过「问题反馈」提交的内容会发送到此邮箱。 +# 留空时按优先级回退:首个 admin 用户邮箱 → SMTP_FROM;均无则仅写库不发信。 +# FEEDBACK_EMAIL=you@example.com + # --- 开放注册(可选) --- # 设为 true 开放用户自助注册(需配合 SMTP 配置使用);不设或设为其他值则仅允许管理员创建用户。 # ALLOW_REGISTRATION=true diff --git a/AGENTS.md b/AGENTS.md index ae50768..0e37c1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -259,6 +259,7 @@ CI(`.github/workflows/ci.yml`)在 Node 20/22 矩阵上执行 `npm run lint` - `template_market_categories(id, slug UNIQUE, name, description, sort_order, is_enabled, created_at, updated_at)` - `content_template_installs(id, template_id, user_id, file_id, source_version, created_at, UNIQUE(template_id, user_id))` - `starred_templates(user_id, template_id, created_at)` +- `feedback(id, name, contact, content, category, user_id, ip, email_sent, status, created_at)` - `file_contents_fts` — FTS5 虚拟表 `(content, file_id UNINDEXED, tokenize='porter unicode61')` ### REST API @@ -281,6 +282,7 @@ CI(`.github/workflows/ci.yml`)在 Node 20/22 矩阵上执行 `npm run lint` - **Skills**:`/api/skills`、`/api/skills/:name`、`/api/skills/:name/download` - **短链**:`/s/:key`(文件)、`/t/:key`(市场模板) - **MCP 配置**:`/api/mcp/config` +- **问题反馈(公开)**:`POST /api/feedback` - **CLI 指南**:`/api/cli/guide` ### MCP 端点 @@ -374,7 +376,7 @@ CLI 与 MCP 共用同一套 REST API,是对等的两个客户端入口。 - 版本号以 `package.json` 为准。 - 推荐发版:`npm version patch|minor|major` → `git push origin main` → `git push origin vX.Y.Z`,由 `.github/workflows/release.yml` 自动执行 lint、test、build、校验 tag、发布到 npm。 - Docker:`Dockerfile` 多阶段构建(builder / frontend / runner),`EXPOSE 8858`;`docker-compose.yml` 映射 host 8858 → container 8858,挂载 `./data:/app/data`。 -- 环境变量完整清单(按需配置):`PORT`、`NODE_ENV`、`JPAGE_DATA_DIR`、`ADMIN_USER`、`ADMIN_PASSWORD`、`SESSION_SECRET`、`COOKIE_SECURE`、`MCP_TOKEN`、`MCP_IP`、`MCP_PROTOCOL`、`TOKEN_ENCRYPTION_KEY`、`SMTP_HOST`、`SMTP_PORT`、`SMTP_SECURE`、`SMTP_USER`、`SMTP_PASS`、`SMTP_FROM`、`APP_URL`、`ALLOW_REGISTRATION`、`GITHUB_CLIENT_ID`、`GITHUB_CLIENT_SECRET`、`GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET`、`WECHAT_OPEN_APP_ID`、`WECHAT_OPEN_APP_SECRET`、`MAX_FILE_VERSIONS`、`BACKUP_CRON`、`BACKUP_DIR`、`ICP_BEIAN`。 +- 环境变量完整清单(按需配置):`PORT`、`NODE_ENV`、`JPAGE_DATA_DIR`、`ADMIN_USER`、`ADMIN_PASSWORD`、`SESSION_SECRET`、`COOKIE_SECURE`、`MCP_TOKEN`、`MCP_IP`、`MCP_PROTOCOL`、`TOKEN_ENCRYPTION_KEY`、`SMTP_HOST`、`SMTP_PORT`、`SMTP_SECURE`、`SMTP_USER`、`SMTP_PASS`、`SMTP_FROM`、`APP_URL`、`FEEDBACK_EMAIL`、`ALLOW_REGISTRATION`、`GITHUB_CLIENT_ID`、`GITHUB_CLIENT_SECRET`、`GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET`、`WECHAT_OPEN_APP_ID`、`WECHAT_OPEN_APP_SECRET`、`MAX_FILE_VERSIONS`、`BACKUP_CRON`、`BACKUP_DIR`、`ICP_BEIAN`。 --- diff --git a/README.md b/README.md index 70eec3d..2f4c747 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ API 和 MCP 端点支持三种认证方式: | `SMTP_PASS` | 否 | SMTP 登录密码或授权码 | | `SMTP_FROM` | 否 | 发件人地址(如 `"即页 "`) | | `APP_URL` | 否 | 应用外部访问地址,用于拼接验证链接(如 `https://jpage.cn`) | +| `FEEDBACK_EMAIL` | 否 | 问题反馈邮件接收地址;留空时回退到首个管理员邮箱 → `SMTP_FROM` | | `GOOGLE_CLIENT_ID` | 否 | Google Web 应用 OAuth Client ID;与 `GOOGLE_CLIENT_SECRET` 同时配置后启用 Google 登录 | | `GOOGLE_CLIENT_SECRET` | 否 | Google Web 应用 OAuth Client Secret;仅保存在服务端环境变量中 | | `GOOGLE_HTTP_TIMEOUT_MS` | 否 | Google Token 与 OIDC 公钥请求超时,范围 1000-60000 毫秒,默认 10000 | @@ -372,6 +373,12 @@ jpage/ | `/api/skills/:name` | GET | skill 详情 | | `/api/skills/:name/download` | GET | ZIP 下载整个 skill 目录 | +### 问题反馈 + +| 端点 | 方法 | 说明 | +|---|---|---| +| `/api/feedback` | POST | 提交问题/功能建议(免登录,写入数据库并邮件通知管理员) | + 完整 API 文档见 [docs/api.md](docs/api.md)。 ## MCP / AI 集成 diff --git a/docker-compose.yml b/docker-compose.yml index 9a1072e..73a3bb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: - SMTP_PASS=${SMTP_PASS:-} - SMTP_FROM=${SMTP_FROM:-} - APP_URL=${APP_URL:-} + - FEEDBACK_EMAIL=${FEEDBACK_EMAIL:-} - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false} - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-} diff --git a/docs/api.md b/docs/api.md index 5977e84..309d1a3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -450,6 +450,33 @@ curl -b jpage.sid -OJ http://localhost:8858/api/skills/jpage/download --- +## 问题反馈(`/api/feedback`) + +免登录公开接口,用户(含匿名访客)提交问题/功能建议。反馈写入数据库,并向管理员发送邮件通知。 + +### `POST /api/feedback` + +**请求体**(JSON): + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `content` | string | 是 | 反馈正文,≤ 5000 字 | +| `category` | string | 否 | 类型:`feature`(功能建议)/ `bug`(问题反馈)/ `other`(其他),默认 `feature` | +| `name` | string | 否 | 提交者称呼,≤ 100 字 | +| `contact` | string | 否 | 联系方式(邮箱/微信等),≤ 100 字 | + +**限流**:每 IP 15 分钟最多 10 次。**收件邮箱**优先级:`FEEDBACK_EMAIL` → 首个 admin 用户邮箱 → `SMTP_FROM`;均无或 SMTP 未配置时仅写库不发信。 + +```bash +curl -X POST http://localhost:8858/api/feedback \ + -H "Content-Type: application/json" \ + -d '{"content":"希望增加暗色模式","category":"feature","contact":"alice@example.com"}' +``` + +返回 `{ "success": true, "id": }`。 + +--- + ## MCP 端点(`/mcp`) 独立的 MCP Streamable HTTP 端点。**全局 `MCP_TOKEN` 或用户级 API Token 任一即可挂载**(未配置任何 Token 时 `/mcp` 禁用)。 @@ -483,5 +510,6 @@ curl -b jpage.sid -OJ http://localhost:8858/api/skills/jpage/download | `ALLOW_REGISTRATION` | `false` | 设为 `true` 开放用户自助注册 | | `SMTP_HOST` 等 | — | SMTP 配置(`SMTP_HOST/PORT/SECURE/USER/PASS/FROM`),用于邮箱验证 | | `APP_URL` | `http://localhost:8858` | 应用外部访问地址,用于拼接验证链接 | +| `FEEDBACK_EMAIL` | — | 问题反馈邮件接收地址;留空时回退到首个管理员邮箱 → `SMTP_FROM` | | `GOOGLE_CLIENT_ID` | — | Google Web 应用 OAuth Client ID;与 Client Secret 同时配置后启用 Google 登录 | | `GOOGLE_CLIENT_SECRET` | — | Google Web 应用 OAuth Client Secret,仅限服务端保存 | diff --git a/eslint.config.mjs b/eslint.config.mjs index 9e97c9d..f5515c0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -74,6 +74,7 @@ const browserGlobals = { Event: 'readonly', CustomEvent: 'readonly', FileReader: 'readonly', + Image: 'readonly', // new Image() 用于二维码等图片可达性预检 FormData: 'readonly', Blob: 'readonly', DragEvent: 'readonly', diff --git a/migrations/028_feedback.js b/migrations/028_feedback.js new file mode 100644 index 0000000..9ca2419 --- /dev/null +++ b/migrations/028_feedback.js @@ -0,0 +1,33 @@ +// 用户问题反馈:功能建议 / 问题反馈 / 其他。 +// 反馈同时写库(不丢数据)并发送邮件给管理员;本表仅建结构,发信逻辑在 routes/feedback.js。 +// +// 字段说明: +// name 提交者称呼(可选) +// contact 联系方式:邮箱 / 微信等(可选) +// content 反馈正文(必填) +// category 类型白名单:feature / bug / other(默认 other) +// user_id 已登录用户则记录,匿名为 NULL(与匿名粘贴文件一致) +// ip 提交者 IP(审计与反滥用排查) +// email_sent 邮件是否投递成功:0=未发或失败,1=成功(SMTP 未配置时仍存库,值为 0) +// status 处理状态:new / read(预留管理后台,本期未使用) +// +// 幂等:CREATE TABLE 用 IF NOT EXISTS。 + +module.exports = { + name: 'feedback', + + async up(db, { dbRun }) { + await dbRun(db, `CREATE TABLE IF NOT EXISTS feedback ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + contact TEXT, + content TEXT NOT NULL, + category TEXT DEFAULT 'other', + user_id INTEGER, + ip TEXT, + email_sent INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'new', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`); + } +}; diff --git a/package.json b/package.json index ecd688f..a5693e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@code2rich/jpage", - "version": "1.6.6", + "version": "1.6.7", "description": "即页 — 零配置 HTML/Markdown 即时预览与分享工具", "main": "server.js", "bin": { diff --git a/public/css/style.css b/public/css/style.css index f04d5f5..3a68fea 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -5993,6 +5993,112 @@ html:not(.light) .btn-danger-soft { margin-top: 8px; line-height: 1.5; } + +/* ---------- 问题反馈弹窗 ---------- */ +.feedback-category-group { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 16px; +} +.feedback-category-chip { + padding: 7px 14px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg); + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all .15s; + white-space: nowrap; +} +.feedback-category-chip:hover { + border-color: var(--text-secondary); + color: var(--text); +} +.feedback-category-chip.active { + background: var(--primary); + color: #fff; + border-color: var(--primary); +} +.feedback-content-input { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 14px; + line-height: 1.6; + background: var(--card); + color: var(--text); + transition: border-color .2s, box-shadow .2s; + resize: vertical; + min-height: 96px; + font-family: inherit; +} +.feedback-content-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); +} +.feedback-content-foot { + display: flex; + justify-content: flex-end; + margin-top: 4px; +} +.feedback-content-count { + font-size: 12px; + color: var(--text-secondary); +} +.feedback-row { + display: flex; + gap: 12px; + margin-top: 12px; +} +.feedback-row .login-field { + flex: 1; + min-width: 0; +} +.feedback-wechat { + display: flex; + align-items: center; + gap: 14px; + margin-top: 16px; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--card) 94%, var(--bg) 6%); +} +.feedback-wechat-info { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} +.feedback-wechat-label { + font-size: 13px; + color: var(--text-secondary); +} +.feedback-wechat-id { + font-size: 14px; + font-weight: 600; + color: var(--text); +} +.feedback-wechat-qr { + width: 64px; + height: 64px; + border-radius: 8px; + flex-shrink: 0; + object-fit: cover; +} +@media (max-width: 480px) { + .feedback-row { + flex-direction: column; + } + .feedback-wechat { + flex-direction: column; + text-align: center; + } /* ============================================================ 创作市场 2026:策展式首页 真实模板预览优先,用层级与留白替代堆叠式筛选面板。 diff --git a/public/index.html b/public/index.html index 7ba0963..90b5bc6 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,7 @@ - +
@@ -349,7 +349,7 @@

准备好让内容流动起来了吗?

@@ -534,6 +534,10 @@

即页

用量统计 + + + + + + +